@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.
- package/dist/server-shell/cli/args.d.ts +0 -4
- package/dist/server-shell/cli/args.js +0 -17
- package/dist/server-shell/cli/cli.js +1 -13
- package/dist/server-shell/cli/launch-command.d.ts +0 -4
- package/dist/server-shell/cli/launch-command.js +4 -13
- package/dist/server-shell/cli/provider-tui.js +56 -29
- package/dist/server-shell/cli/terminal-logo.d.ts +5 -0
- package/dist/server-shell/cli/terminal-logo.js +27 -0
- package/package.json +1 -1
|
@@ -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,
|
|
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
|
-
...(
|
|
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
|
-
"
|
|
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
|
|
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
|
-
|
|
21
|
+
renderTuttiTerminalLogo({ tty: options.tty === true }),
|
|
22
|
+
"",
|
|
23
|
+
"Provider setup",
|
|
19
24
|
"",
|
|
20
25
|
`Project: ${options.projectName}`,
|
|
21
|
-
|
|
26
|
+
`${instruction} Press Ctrl-C to cancel.`,
|
|
22
27
|
"",
|
|
23
28
|
fieldLine({
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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 === "
|
|
105
|
-
state.
|
|
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.
|
|
118
|
-
state.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|