@xfey/tutti 0.1.7 → 0.1.9

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.
@@ -18,10 +18,6 @@ export type CliCommand = {
18
18
  } | {
19
19
  kind: "stop";
20
20
  workspacePath?: string;
21
- } | {
22
- kind: "restart";
23
- workspacePath?: string;
24
- yes: boolean;
25
21
  } | {
26
22
  kind: "invite";
27
23
  workspacePath?: string;
@@ -134,23 +134,6 @@ export function parseCliArgs(argv, cwd = process.cwd()) {
134
134
  const workspacePath = readOptionalWorkspace(args, cwd);
135
135
  return { kind: "stop", ...(workspacePath === undefined ? {} : { workspacePath }) };
136
136
  }
137
- if (command === "restart") {
138
- if (hasHelpArg(args)) {
139
- return { kind: "help", topic: "restart" };
140
- }
141
- let yes = false;
142
- const positional = [];
143
- for (const arg of args) {
144
- if (arg === "--yes") {
145
- yes = true;
146
- }
147
- else {
148
- positional.push(arg);
149
- }
150
- }
151
- const workspacePath = readOptionalWorkspace(positional, cwd);
152
- return { kind: "restart", yes, ...(workspacePath === undefined ? {} : { workspacePath }) };
153
- }
154
137
  if (command === "invite") {
155
138
  if (hasHelpArg(args)) {
156
139
  return { kind: "help", topic: "invite" };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { formatCliError } from "./errors.js";
3
3
  import { parseCliArgs } from "./args.js";
4
- import { runBackgroundLaunchCommand, runForegroundLaunchCommand, runInternalHostRunCommand, runRestartCommand, } from "./launch-command.js";
4
+ import { runBackgroundLaunchCommand, runForegroundLaunchCommand, runInternalHostRunCommand, } from "./launch-command.js";
5
5
  import { resolveExistingProjectContext } from "./project-resolver.js";
6
6
  import { runProviderSetupTui } from "./provider-tui.js";
7
7
  import { runInviteCommand, runLogsCommand, runProviderStatusCommand, runPsManageCommand, runPsCommand, runStopCommand, } from "./runtime-commands.js";
@@ -11,7 +11,6 @@ const HELP_TEXT = `Usage:
11
11
  tutti ps [--manage]
12
12
  tutti invite [workspace_path]
13
13
  tutti stop [workspace_path]
14
- tutti restart [workspace_path] [--yes]
15
14
  tutti logs [workspace_path] [--tail 120]
16
15
  tutti provider setup [workspace_path]
17
16
  tutti provider status [workspace_path]
@@ -58,11 +57,6 @@ const STOP_HELP_TEXT = `Usage:
58
57
 
59
58
  Stops the background Tutti host for a project.
60
59
  `;
61
- const RESTART_HELP_TEXT = `Usage:
62
- tutti restart [workspace_path] [--yes]
63
-
64
- Stops and relaunches the background Tutti host for a project.
65
- `;
66
60
  const LOGS_HELP_TEXT = `Usage:
67
61
  tutti logs [workspace_path] [--tail 120]
68
62
 
@@ -89,9 +83,6 @@ function helpForTopic(topic) {
89
83
  if (topic === "stop") {
90
84
  return STOP_HELP_TEXT;
91
85
  }
92
- if (topic === "restart") {
93
- return RESTART_HELP_TEXT;
94
- }
95
86
  if (topic === "logs") {
96
87
  return LOGS_HELP_TEXT;
97
88
  }
@@ -169,9 +160,6 @@ try {
169
160
  case "stop":
170
161
  process.stdout.write(`${await runStopCommand(command.workspacePath)}\n`);
171
162
  break;
172
- case "restart":
173
- await runRestartCommand(command);
174
- break;
175
163
  case "invite":
176
164
  process.stdout.write(`${await runInviteCommand(command.workspacePath)}\n`);
177
165
  break;
@@ -12,8 +12,4 @@ export declare function runBackgroundLaunchCommand(options: {
12
12
  workspacePath: string;
13
13
  yes: boolean;
14
14
  }): Promise<void>;
15
- export declare function runRestartCommand(options: {
16
- workspacePath?: string;
17
- yes: boolean;
18
- }): Promise<void>;
19
15
  //# sourceMappingURL=launch-command.d.ts.map
@@ -1,13 +1,12 @@
1
1
  import { createInterface } from "node:readline/promises";
2
2
  import { basename } from "node:path";
3
- import { redactText } from "@tutti/shared/utils";
4
3
  import { formatLaunchLifecycleResult, startLaunchProject, waitForForegroundHostShutdown } from "./host-lifecycle.js";
5
4
  import { prepareLaunchProject } from "./launch.js";
6
5
  import { spawnDetachedHost, waitForManagedHostReady } from "./managed-host.js";
7
6
  import { renderTerminalQr } from "./terminal-qr.js";
7
+ import { renderTuttiTerminalLogo } from "./terminal-logo.js";
8
8
  import { runProviderSetupTui } from "./provider-tui.js";
9
9
  import { LaunchError } from "./errors.js";
10
- import { runStopCommand } from "./runtime-commands.js";
11
10
  const CLEAR = "\u001B[2J\u001B[H";
12
11
  export async function confirmFromTty(request) {
13
12
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
@@ -44,18 +43,18 @@ async function waitForEnter() {
44
43
  }
45
44
  }
46
45
  function renderCompletionPage(options) {
46
+ const isTty = process.stdout.isTTY === true;
47
47
  return [
48
- ...(process.stdout.isTTY ? [CLEAR] : []),
48
+ ...(isTty ? [CLEAR, renderTuttiTerminalLogo({ tty: true }), ""] : []),
49
49
  "Tutti is running in the background.",
50
50
  "",
51
51
  `Project: ${options.projectName}`,
52
- `Workspace: ${redactText(options.workspaceRoot)}`,
53
52
  "",
54
53
  `Join URL: ${options.joinUrl}`,
55
54
  "",
56
55
  renderTerminalQr(options.joinUrl),
57
56
  "",
58
- "按下回车关闭页面,Host 会继续在后台运行。",
57
+ "Press Enter to close this page. The host will keep running in the background.",
59
58
  ].join("\n");
60
59
  }
61
60
  async function ensureProviderConfigured(preparation) {
@@ -112,16 +111,8 @@ export async function runBackgroundLaunchCommand(options) {
112
111
  }
113
112
  process.stdout.write(`${renderCompletionPage({
114
113
  projectName: projectName(preparation),
115
- workspaceRoot: preparation.workspace_root,
116
114
  joinUrl: ready.join_url,
117
115
  })}\n`);
118
116
  await waitForEnter();
119
117
  }
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
118
  //# sourceMappingURL=launch-command.js.map
@@ -1,38 +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 { renderTuttiTerminalLogo } from "./terminal-logo.js";
4
5
  const DEFAULT_BASE_URL = "https://api.openai.com/v1";
5
6
  const HIDE_CURSOR = "\u001B[?25l";
6
7
  const SHOW_CURSOR = "\u001B[?25h";
7
8
  const CLEAR = "\u001B[2J\u001B[H";
9
+ const DIVIDER = "----------------------------------------------------------------";
8
10
  function fieldLine(options) {
9
- const active = options.index === options.selected;
10
- const marker = active ? ">" : " ";
11
11
  const value = options.secret && options.value.length > 0 ? "*".repeat(options.value.length) : options.value;
12
- return `${marker} ${options.label.padEnd(10)} ${value}${active ? " _" : ""}`;
12
+ return `> ${options.label}: ${value} _`;
13
+ }
14
+ function renderProviderField(state) {
15
+ if (state.step === "base-url") {
16
+ return [
17
+ "Step 1 of 2 - Base URL",
18
+ "Enter the base URL for your OpenAI-compatible provider.",
19
+ "",
20
+ fieldLine({
21
+ label: "Base URL",
22
+ value: state.baseUrl,
23
+ }),
24
+ "",
25
+ "Press Enter to continue. Press Ctrl-C to cancel.",
26
+ ];
27
+ }
28
+ return [
29
+ "Step 2 of 2 - API Key",
30
+ `Base URL: ${state.baseUrl}`,
31
+ "",
32
+ "Enter the API key for this provider. It will be stored only in the machine-local Tutti credential store.",
33
+ "",
34
+ fieldLine({
35
+ label: "API Key",
36
+ value: state.apiKey,
37
+ secret: true,
38
+ }),
39
+ "",
40
+ "Press Enter to validate. Press Esc to edit the base URL. Press Ctrl-C to cancel.",
41
+ ];
13
42
  }
14
43
  function renderProviderForm(options) {
15
44
  const lines = [
16
45
  CLEAR,
17
46
  HIDE_CURSOR,
18
- "Tutti Provider Setup",
47
+ renderTuttiTerminalLogo({ tty: options.tty === true }),
48
+ "",
49
+ "Provider setup",
50
+ DIVIDER,
51
+ "Tutti uses an OpenAI-compatible Responses API provider.",
52
+ "The base URL should expose POST /responses under the entered URL.",
53
+ `Default example: ${DEFAULT_BASE_URL}`,
54
+ DIVIDER,
19
55
  "",
20
56
  `Project: ${options.projectName}`,
21
- "Use Up/Down to move, type to edit, Enter to validate, Ctrl-C to cancel.",
22
57
  "",
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
- }),
58
+ ...renderProviderField(options.state),
36
59
  "",
37
60
  ];
38
61
  if (options.state.error !== undefined) {
@@ -74,14 +97,18 @@ function waitForKeypress(stdin) {
74
97
  }
75
98
  async function readProviderForm(options) {
76
99
  const state = {
77
- selected: 0,
100
+ step: options.initialStep ?? "base-url",
78
101
  baseUrl: options.initialBaseUrl ?? DEFAULT_BASE_URL,
79
- apiKey: "",
102
+ apiKey: options.initialApiKey ?? "",
80
103
  error: undefined,
81
104
  };
82
105
  return await new Promise((resolve, reject) => {
83
106
  const render = () => {
84
- options.stdout.write(renderProviderForm({ projectName: options.projectName, state }));
107
+ options.stdout.write(renderProviderForm({
108
+ projectName: options.projectName,
109
+ state,
110
+ tty: options.stdout.isTTY === true,
111
+ }));
85
112
  };
86
113
  const cleanup = () => {
87
114
  options.stdin.off("keypress", onKeypress);
@@ -97,25 +124,30 @@ async function readProviderForm(options) {
97
124
  return;
98
125
  }
99
126
  if (key.name === "escape") {
127
+ if (state.step === "api-key") {
128
+ state.step = "base-url";
129
+ state.error = undefined;
130
+ render();
131
+ return;
132
+ }
100
133
  cleanup();
101
134
  reject(new Error("Provider setup cancelled"));
102
135
  return;
103
136
  }
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
137
  if (key.name === "return") {
117
- if (state.baseUrl.trim() === "" || state.apiKey.trim() === "") {
118
- state.error = "Base URL and API key are required.";
138
+ if (state.step === "base-url") {
139
+ if (state.baseUrl.trim() === "") {
140
+ state.error = "Base URL is required.";
141
+ render();
142
+ return;
143
+ }
144
+ state.step = "api-key";
145
+ state.error = undefined;
146
+ render();
147
+ return;
148
+ }
149
+ if (state.apiKey.trim() === "") {
150
+ state.error = "API key is required.";
119
151
  render();
120
152
  return;
121
153
  }
@@ -124,7 +156,7 @@ async function readProviderForm(options) {
124
156
  return;
125
157
  }
126
158
  if (key.name === "backspace") {
127
- if (state.selected === 0) {
159
+ if (state.step === "base-url") {
128
160
  state.baseUrl = state.baseUrl.slice(0, -1);
129
161
  }
130
162
  else {
@@ -135,7 +167,7 @@ async function readProviderForm(options) {
135
167
  return;
136
168
  }
137
169
  if (character !== undefined && character >= " " && character !== "\u007F") {
138
- if (state.selected === 0) {
170
+ if (state.step === "base-url") {
139
171
  state.baseUrl += character;
140
172
  }
141
173
  else {
@@ -153,13 +185,15 @@ async function readProviderForm(options) {
153
185
  render();
154
186
  });
155
187
  }
156
- function renderChecking(projectName, frame) {
188
+ function renderChecking(projectName, frame, tty) {
157
189
  const frames = ["|", "/", "-", "\\"];
158
190
  const indicator = frames[frame % frames.length] ?? "|";
159
191
  return [
160
192
  CLEAR,
161
193
  HIDE_CURSOR,
162
- "Tutti Provider Setup",
194
+ renderTuttiTerminalLogo({ tty }),
195
+ "",
196
+ "Provider setup",
163
197
  "",
164
198
  `Project: ${projectName}`,
165
199
  "",
@@ -173,21 +207,27 @@ export async function runProviderSetupTui(options) {
173
207
  throw new Error("Provider setup requires an interactive terminal.");
174
208
  }
175
209
  let initialBaseUrl = options.initialBaseUrl;
210
+ let initialApiKey = "";
211
+ let initialStep = "base-url";
176
212
  while (true) {
177
213
  const input = await readProviderForm({
178
214
  projectName: options.projectName,
179
215
  ...(initialBaseUrl === undefined ? {} : { initialBaseUrl }),
216
+ ...(initialApiKey === "" ? {} : { initialApiKey }),
217
+ initialStep,
180
218
  stdin,
181
219
  stdout,
182
220
  });
183
221
  initialBaseUrl = input.baseUrl;
222
+ initialApiKey = input.apiKey;
223
+ initialStep = "api-key";
184
224
  let frame = 0;
185
225
  const interval = setInterval(() => {
186
- stdout.write(renderChecking(options.projectName, frame));
226
+ stdout.write(renderChecking(options.projectName, frame, stdout.isTTY === true));
187
227
  frame += 1;
188
228
  }, 120);
189
229
  try {
190
- stdout.write(renderChecking(options.projectName, frame));
230
+ stdout.write(renderChecking(options.projectName, frame, stdout.isTTY === true));
191
231
  const result = await configureProjectOpenAiProvider({
192
232
  tuttiHome: options.tuttiHome,
193
233
  projectId: options.projectId,
@@ -207,12 +247,13 @@ export async function runProviderSetupTui(options) {
207
247
  stdout.write(renderProviderForm({
208
248
  projectName: options.projectName,
209
249
  state: {
210
- selected: 1,
250
+ step: "api-key",
211
251
  baseUrl: input.baseUrl,
212
252
  apiKey: input.apiKey,
213
253
  error: validationFailureMessage(result),
214
254
  },
215
255
  footer: "Press any key to edit and try again.",
256
+ tty: stdout.isTTY === true,
216
257
  }));
217
258
  await waitForKeypress(stdin);
218
259
  }
@@ -0,0 +1,5 @@
1
+ export declare function renderTuttiTerminalLogo(options?: {
2
+ tty?: boolean;
3
+ color?: boolean;
4
+ }): string;
5
+ //# sourceMappingURL=terminal-logo.d.ts.map
@@ -0,0 +1,19 @@
1
+ // Static Braille rendering of docs/design/Tutti.png, generated from the alpha mask.
2
+ const TUTTI_BRAILLE_LOGO = [
3
+ " ⣀⣤⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡄ ⣠⣶⣶⣦ ⢀⣤⣶⣶⣄",
4
+ " ⢀⣾⣿⣿⠿⠛⠛⠛⢉⣿⣿⣿⠏⠉⠉⠛⠛⠛⠛⠛⠁ ⢀⣾⣿⠏⣿⣿⡇⣠⣿⡿⠙⣿⣿",
5
+ " ⢸⣿⣿⡇ ⢸⣿⣿⣿ ⣾⣿⡏⢀⣿⣿⢡⣿⣿⠃⣸⣿⡟ ⢀⣴⣶⣤",
6
+ " ⠈⠿⣿⠟ ⣿⣿⣿⡇ ⢸⣿⣿⣡⣾⡿⠃⣾⣿⣟⣰⣿⠟⠁ ⠘⠿⠿⠛",
7
+ " ⢰⣿⣿⣿⠁ ⣤⣶⡦ ⣴⣶⡆⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠆⣤⣶⣦",
8
+ " ⣸⣿⣿⡿ ⢰⣿⣿⠇ ⢰⣿⣿⠇⠈⠉⣿⣿⡏⠉⠉⠉⢹⣿⣿⡏⠉⠉⠁ ⢠⣿⣿⡟",
9
+ " ⣿⣿⣿⡇ ⣾⣿⣿ ⢠⣿⣿⡿ ⢀⣿⣿⣇ ⢸⣿⣿⡇ ⣸⣿⣿⠇",
10
+ " ⢰⣿⣿⣿⠃ ⣿⣿⣿⣴⣿⣿⣿⣇⣀⣴⣿⣿⣿⣿⣄⣀⣠⣾⣿⣿⣧⣀⣀⣀⣴⣿⣿⣿",
11
+ " ⠸⣿⣿⡿ ⠹⣿⣿⡿⠃⠻⣿⣿⣿⠟⠋⠈⠻⣿⣿⣿⡿⠛⠙⠿⣿⣿⣿⣿⠟⣿⣿⡿",
12
+ ].join("\n");
13
+ export function renderTuttiTerminalLogo(options = {}) {
14
+ if (options.tty !== true) {
15
+ return "Tutti";
16
+ }
17
+ return TUTTI_BRAILLE_LOGO;
18
+ }
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.7",
3
+ "version": "0.1.9",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",