@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.
- 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 +83 -42
- package/dist/server-shell/cli/terminal-logo.d.ts +5 -0
- package/dist/server-shell/cli/terminal-logo.js +19 -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,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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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.
|
|
118
|
-
state.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,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
|