@xfey/tutti 0.1.7 → 0.1.8

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,34 +1,37 @@
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";
8
9
  function fieldLine(options) {
9
- const active = options.index === options.selected;
10
- const marker = active ? ">" : " ";
10
+ const marker = options.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 `${marker} ${options.label.padEnd(10)} ${value}${options.active ? " _" : ""}`;
13
13
  }
14
14
  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.";
15
18
  const lines = [
16
19
  CLEAR,
17
20
  HIDE_CURSOR,
18
- "Tutti Provider Setup",
21
+ renderTuttiTerminalLogo({ tty: options.tty === true }),
22
+ "",
23
+ "Provider setup",
19
24
  "",
20
25
  `Project: ${options.projectName}`,
21
- "Use Up/Down to move, type to edit, Enter to validate, Ctrl-C to cancel.",
26
+ `${instruction} Press Ctrl-C to cancel.`,
22
27
  "",
23
28
  fieldLine({
24
- index: 0,
25
- selected: options.state.selected,
29
+ active: options.state.step === "base-url",
26
30
  label: "Base URL",
27
31
  value: options.state.baseUrl,
28
32
  }),
29
33
  fieldLine({
30
- index: 1,
31
- selected: options.state.selected,
34
+ active: options.state.step === "api-key",
32
35
  label: "API Key",
33
36
  value: options.state.apiKey,
34
37
  secret: true,
@@ -74,14 +77,18 @@ function waitForKeypress(stdin) {
74
77
  }
75
78
  async function readProviderForm(options) {
76
79
  const state = {
77
- selected: 0,
80
+ step: options.initialStep ?? "base-url",
78
81
  baseUrl: options.initialBaseUrl ?? DEFAULT_BASE_URL,
79
- apiKey: "",
82
+ apiKey: options.initialApiKey ?? "",
80
83
  error: undefined,
81
84
  };
82
85
  return await new Promise((resolve, reject) => {
83
86
  const render = () => {
84
- options.stdout.write(renderProviderForm({ projectName: options.projectName, state }));
87
+ options.stdout.write(renderProviderForm({
88
+ projectName: options.projectName,
89
+ state,
90
+ tty: options.stdout.isTTY === true,
91
+ }));
85
92
  };
86
93
  const cleanup = () => {
87
94
  options.stdin.off("keypress", onKeypress);
@@ -97,25 +104,36 @@ async function readProviderForm(options) {
97
104
  return;
98
105
  }
99
106
  if (key.name === "escape") {
107
+ if (state.step === "api-key") {
108
+ state.step = "base-url";
109
+ state.error = undefined;
110
+ render();
111
+ return;
112
+ }
100
113
  cleanup();
101
114
  reject(new Error("Provider setup cancelled"));
102
115
  return;
103
116
  }
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;
117
+ if (key.name === "tab") {
118
+ state.step = state.step === "base-url" ? "api-key" : "base-url";
112
119
  state.error = undefined;
113
120
  render();
114
121
  return;
115
122
  }
116
123
  if (key.name === "return") {
117
- if (state.baseUrl.trim() === "" || state.apiKey.trim() === "") {
118
- state.error = "Base URL and API key are required.";
124
+ if (state.step === "base-url") {
125
+ if (state.baseUrl.trim() === "") {
126
+ state.error = "Base URL is required.";
127
+ render();
128
+ return;
129
+ }
130
+ state.step = "api-key";
131
+ state.error = undefined;
132
+ render();
133
+ return;
134
+ }
135
+ if (state.apiKey.trim() === "") {
136
+ state.error = "API key is required.";
119
137
  render();
120
138
  return;
121
139
  }
@@ -124,7 +142,7 @@ async function readProviderForm(options) {
124
142
  return;
125
143
  }
126
144
  if (key.name === "backspace") {
127
- if (state.selected === 0) {
145
+ if (state.step === "base-url") {
128
146
  state.baseUrl = state.baseUrl.slice(0, -1);
129
147
  }
130
148
  else {
@@ -135,7 +153,7 @@ async function readProviderForm(options) {
135
153
  return;
136
154
  }
137
155
  if (character !== undefined && character >= " " && character !== "\u007F") {
138
- if (state.selected === 0) {
156
+ if (state.step === "base-url") {
139
157
  state.baseUrl += character;
140
158
  }
141
159
  else {
@@ -153,13 +171,15 @@ async function readProviderForm(options) {
153
171
  render();
154
172
  });
155
173
  }
156
- function renderChecking(projectName, frame) {
174
+ function renderChecking(projectName, frame, tty) {
157
175
  const frames = ["|", "/", "-", "\\"];
158
176
  const indicator = frames[frame % frames.length] ?? "|";
159
177
  return [
160
178
  CLEAR,
161
179
  HIDE_CURSOR,
162
- "Tutti Provider Setup",
180
+ renderTuttiTerminalLogo({ tty }),
181
+ "",
182
+ "Provider setup",
163
183
  "",
164
184
  `Project: ${projectName}`,
165
185
  "",
@@ -173,21 +193,27 @@ export async function runProviderSetupTui(options) {
173
193
  throw new Error("Provider setup requires an interactive terminal.");
174
194
  }
175
195
  let initialBaseUrl = options.initialBaseUrl;
196
+ let initialApiKey = "";
197
+ let initialStep = "base-url";
176
198
  while (true) {
177
199
  const input = await readProviderForm({
178
200
  projectName: options.projectName,
179
201
  ...(initialBaseUrl === undefined ? {} : { initialBaseUrl }),
202
+ ...(initialApiKey === "" ? {} : { initialApiKey }),
203
+ initialStep,
180
204
  stdin,
181
205
  stdout,
182
206
  });
183
207
  initialBaseUrl = input.baseUrl;
208
+ initialApiKey = input.apiKey;
209
+ initialStep = "api-key";
184
210
  let frame = 0;
185
211
  const interval = setInterval(() => {
186
- stdout.write(renderChecking(options.projectName, frame));
212
+ stdout.write(renderChecking(options.projectName, frame, stdout.isTTY === true));
187
213
  frame += 1;
188
214
  }, 120);
189
215
  try {
190
- stdout.write(renderChecking(options.projectName, frame));
216
+ stdout.write(renderChecking(options.projectName, frame, stdout.isTTY === true));
191
217
  const result = await configureProjectOpenAiProvider({
192
218
  tuttiHome: options.tuttiHome,
193
219
  projectId: options.projectId,
@@ -207,12 +233,13 @@ export async function runProviderSetupTui(options) {
207
233
  stdout.write(renderProviderForm({
208
234
  projectName: options.projectName,
209
235
  state: {
210
- selected: 1,
236
+ step: "api-key",
211
237
  baseUrl: input.baseUrl,
212
238
  apiKey: input.apiKey,
213
239
  error: validationFailureMessage(result),
214
240
  },
215
241
  footer: "Press any key to edit and try again.",
242
+ tty: stdout.isTTY === true,
216
243
  }));
217
244
  await waitForKeypress(stdin);
218
245
  }
@@ -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,27 @@
1
+ const RESET = "\u001B[0m";
2
+ const ACTION_STRONG = "\u001B[38;2;8;121;95m";
3
+ // Static Braille rendering of docs/design/Tutti.png, generated from the alpha mask.
4
+ const TUTTI_BRAILLE_LOGO = [
5
+ " ⣀⣤⣶⣶⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡄ ⣠⣶⣶⣦ ⢀⣤⣶⣶⣄",
6
+ " ⢀⣾⣿⣿⠿⠛⠛⠛⢉⣿⣿⣿⠏⠉⠉⠛⠛⠛⠛⠛⠁ ⢀⣾⣿⠏⣿⣿⡇⣠⣿⡿⠙⣿⣿",
7
+ " ⢸⣿⣿⡇ ⢸⣿⣿⣿ ⣾⣿⡏⢀⣿⣿⢡⣿⣿⠃⣸⣿⡟ ⢀⣴⣶⣤",
8
+ " ⠈⠿⣿⠟ ⣿⣿⣿⡇ ⢸⣿⣿⣡⣾⡿⠃⣾⣿⣟⣰⣿⠟⠁ ⠘⠿⠿⠛",
9
+ " ⢰⣿⣿⣿⠁ ⣤⣶⡦ ⣴⣶⡆⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠆⣤⣶⣦",
10
+ " ⣸⣿⣿⡿ ⢰⣿⣿⠇ ⢰⣿⣿⠇⠈⠉⣿⣿⡏⠉⠉⠉⢹⣿⣿⡏⠉⠉⠁ ⢠⣿⣿⡟",
11
+ " ⣿⣿⣿⡇ ⣾⣿⣿ ⢠⣿⣿⡿ ⢀⣿⣿⣇ ⢸⣿⣿⡇ ⣸⣿⣿⠇",
12
+ " ⢰⣿⣿⣿⠃ ⣿⣿⣿⣴⣿⣿⣿⣇⣀⣴⣿⣿⣿⣿⣄⣀⣠⣾⣿⣿⣧⣀⣀⣀⣴⣿⣿⣿",
13
+ " ⠸⣿⣿⡿ ⠹⣿⣿⡿⠃⠻⣿⣿⣿⠟⠋⠈⠻⣿⣿⣿⡿⠛⠙⠿⣿⣿⣿⣿⠟⣿⣿⡿",
14
+ ].join("\n");
15
+ function supportsColor(options) {
16
+ return options.color !== false && process.env.NO_COLOR === undefined;
17
+ }
18
+ export function renderTuttiTerminalLogo(options = {}) {
19
+ if (options.tty !== true) {
20
+ return "Tutti";
21
+ }
22
+ if (!supportsColor(options)) {
23
+ return TUTTI_BRAILLE_LOGO;
24
+ }
25
+ return `${ACTION_STRONG}${TUTTI_BRAILLE_LOGO}${RESET}`;
26
+ }
27
+ //# 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.8",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",