@xfey/tutti 0.1.23 → 0.1.24
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.
|
@@ -4,7 +4,7 @@ import { formatLaunchLifecycleResult, startLaunchProject, waitForForegroundHostS
|
|
|
4
4
|
import { prepareLaunchProject } from "./launch.js";
|
|
5
5
|
import { spawnDetachedHost, waitForManagedHostReady } from "./managed-host.js";
|
|
6
6
|
import { resolveLaunchWorkspacePath } from "./project-resolver.js";
|
|
7
|
-
import { formatJoinLinkValidity, formatTerminalLink
|
|
7
|
+
import { formatJoinLinkValidity, formatTerminalLink } from "./terminal-qr.js";
|
|
8
8
|
import { renderTuttiTerminalLogo } from "./terminal-logo.js";
|
|
9
9
|
import { runProviderSetupTui } from "./provider-tui.js";
|
|
10
10
|
import { LaunchError } from "./errors.js";
|
|
@@ -25,69 +25,69 @@ export async function confirmFromTty(request) {
|
|
|
25
25
|
readline.close();
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
-
async function waitForEnter() {
|
|
29
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
30
|
-
return;
|
|
31
|
-
}
|
|
32
|
-
const readline = createInterface({
|
|
33
|
-
input: process.stdin,
|
|
34
|
-
output: process.stdout,
|
|
35
|
-
});
|
|
36
|
-
try {
|
|
37
|
-
await readline.question("");
|
|
38
|
-
}
|
|
39
|
-
finally {
|
|
40
|
-
readline.close();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
28
|
function formatProjectLabel(workspaceRoot, projectId) {
|
|
44
29
|
return basename(workspaceRoot) || projectId;
|
|
45
30
|
}
|
|
31
|
+
function visibleTerminalLength(line) {
|
|
32
|
+
let stripped = line;
|
|
33
|
+
while (true) {
|
|
34
|
+
const start = stripped.indexOf("\u001B]8;;");
|
|
35
|
+
if (start < 0) {
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
const end = stripped.indexOf("\u001B\\", start);
|
|
39
|
+
if (end < 0) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
stripped = `${stripped.slice(0, start)}${stripped.slice(end + 2)}`;
|
|
43
|
+
}
|
|
44
|
+
while (true) {
|
|
45
|
+
const start = stripped.indexOf("\u001B[");
|
|
46
|
+
if (start < 0) {
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const end = Array.from(stripped.slice(start + 2)).findIndex((character) => {
|
|
50
|
+
const codePoint = character.codePointAt(0);
|
|
51
|
+
return (codePoint !== undefined &&
|
|
52
|
+
((codePoint >= 0x41 && codePoint <= 0x5a) ||
|
|
53
|
+
(codePoint >= 0x61 && codePoint <= 0x7a)));
|
|
54
|
+
});
|
|
55
|
+
if (end < 0) {
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
stripped = `${stripped.slice(0, start)}${stripped.slice(start + 3 + end)}`;
|
|
59
|
+
}
|
|
60
|
+
return stripped.length;
|
|
61
|
+
}
|
|
46
62
|
function borderedBlock(lines) {
|
|
47
|
-
const width = Math.max(...lines.map(
|
|
63
|
+
const width = Math.max(...lines.map(visibleTerminalLength));
|
|
48
64
|
const top = `╭${"─".repeat(width + 2)}╮`;
|
|
49
65
|
const bottom = `╰${"─".repeat(width + 2)}╯`;
|
|
50
|
-
const body = lines.map((line) => `│ ${line.
|
|
66
|
+
const body = lines.map((line) => `│ ${line}${" ".repeat(width - visibleTerminalLength(line))} │`);
|
|
51
67
|
return [top, ...body, bottom].join("\n");
|
|
52
68
|
}
|
|
53
69
|
export function renderCompletionPage(options) {
|
|
54
70
|
const isTty = options.tty ?? process.stdout.isTTY === true;
|
|
55
71
|
const projectLabel = formatProjectLabel(options.workspaceRoot, options.projectId);
|
|
56
72
|
const joinUrl = formatTerminalLink(options.joinUrl, { hyperlinks: options.hyperlinks === true });
|
|
73
|
+
const joinValidity = formatJoinLinkValidity({
|
|
74
|
+
...(options.joinTokenExpiresAt === undefined
|
|
75
|
+
? {}
|
|
76
|
+
: { expiresAt: options.joinTokenExpiresAt }),
|
|
77
|
+
...(options.joinTokenReusable === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { reusable: options.joinTokenReusable }),
|
|
80
|
+
includeRemaining: false,
|
|
81
|
+
});
|
|
57
82
|
const summaryLines = [
|
|
58
83
|
"Tutti is hosting this project.",
|
|
59
84
|
`Project: ${projectLabel}`,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
`Relay: ${options.relayUrl}`,
|
|
63
|
-
];
|
|
64
|
-
const nextSteps = [
|
|
65
|
-
"Next:",
|
|
66
|
-
" Open the join URL or scan the QR code.",
|
|
67
|
-
" `tutti invite` rotates a fresh link.",
|
|
68
|
-
" `tutti ps` shows running hosts.",
|
|
69
|
-
" `tutti stop` stops this host.",
|
|
85
|
+
`Join URL: ${joinUrl}`,
|
|
86
|
+
joinValidity,
|
|
70
87
|
];
|
|
71
88
|
return [
|
|
72
89
|
...(isTty ? [CLEAR, renderTuttiTerminalLogo({ tty: true }), ""] : []),
|
|
73
90
|
isTty ? borderedBlock(summaryLines) : summaryLines.join("\n"),
|
|
74
|
-
"",
|
|
75
|
-
`Join URL: ${joinUrl}`,
|
|
76
|
-
formatJoinLinkValidity({
|
|
77
|
-
...(options.joinTokenExpiresAt === undefined
|
|
78
|
-
? {}
|
|
79
|
-
: { expiresAt: options.joinTokenExpiresAt }),
|
|
80
|
-
...(options.joinTokenReusable === undefined
|
|
81
|
-
? {}
|
|
82
|
-
: { reusable: options.joinTokenReusable }),
|
|
83
|
-
}),
|
|
84
|
-
"",
|
|
85
|
-
"QR code:",
|
|
86
|
-
renderTerminalQr(options.joinUrl),
|
|
87
|
-
"",
|
|
88
|
-
...nextSteps,
|
|
89
|
-
"",
|
|
90
|
-
isTty ? "[Enter] close this view. The host keeps running." : "The host keeps running.",
|
|
91
91
|
].join("\n");
|
|
92
92
|
}
|
|
93
93
|
async function ensureProviderConfigured(preparation) {
|
|
@@ -150,8 +150,6 @@ export async function runBackgroundLaunchCommand(options) {
|
|
|
150
150
|
joinUrl: ready.join_url,
|
|
151
151
|
projectId: preparation.project_id,
|
|
152
152
|
workspaceRoot: preparation.workspace_root,
|
|
153
|
-
relayUrl: preparation.relay_url,
|
|
154
|
-
branch: preparation.branch,
|
|
155
153
|
tty: process.stdout.isTTY === true,
|
|
156
154
|
hyperlinks: process.stdout.isTTY === true,
|
|
157
155
|
...(ready.join_token_expires_at === undefined
|
|
@@ -161,6 +159,5 @@ export async function runBackgroundLaunchCommand(options) {
|
|
|
161
159
|
? {}
|
|
162
160
|
: { joinTokenReusable: ready.join_token_reusable }),
|
|
163
161
|
})}\n`);
|
|
164
|
-
await waitForEnter();
|
|
165
162
|
}
|
|
166
163
|
//# sourceMappingURL=launch-command.js.map
|
|
@@ -9,13 +9,21 @@ const SHOW_CURSOR = "\u001B[?25h";
|
|
|
9
9
|
const BLINK_ON = "\u001B[5m";
|
|
10
10
|
const BLINK_OFF = "\u001B[25m";
|
|
11
11
|
const CLEAR = "\u001B[2J\u001B[H";
|
|
12
|
-
const
|
|
12
|
+
const FIELD_BOX_WIDTH = 60;
|
|
13
13
|
function providerSetupCancelled() {
|
|
14
14
|
return new LaunchError("provider_setup_cancelled", "Provider setup cancelled.", "Run the command again when you are ready.");
|
|
15
15
|
}
|
|
16
16
|
function renderFieldInput(options) {
|
|
17
17
|
const value = options.secret && options.value.length > 0 ? "*".repeat(options.value.length) : options.value;
|
|
18
|
-
|
|
18
|
+
const visibleValue = value.length >= FIELD_BOX_WIDTH ? `<${value.slice(-(FIELD_BOX_WIDTH - 2))}` : value;
|
|
19
|
+
const cursor = `${BLINK_ON}_${BLINK_OFF}`;
|
|
20
|
+
const plainLength = visibleValue.length + 1;
|
|
21
|
+
const padding = Math.max(0, FIELD_BOX_WIDTH - plainLength);
|
|
22
|
+
return [
|
|
23
|
+
`+${"-".repeat(FIELD_BOX_WIDTH + 2)}+`,
|
|
24
|
+
`| ${visibleValue}${cursor}${" ".repeat(padding)} |`,
|
|
25
|
+
`+${"-".repeat(FIELD_BOX_WIDTH + 2)}+`,
|
|
26
|
+
];
|
|
19
27
|
}
|
|
20
28
|
function renderProviderField(state) {
|
|
21
29
|
if (state.step === "base-url") {
|
|
@@ -24,7 +32,6 @@ function renderProviderField(state) {
|
|
|
24
32
|
"OpenAI-compatible Responses API base URL; must expose POST /responses.",
|
|
25
33
|
"",
|
|
26
34
|
...renderFieldInput({
|
|
27
|
-
label: "Base URL",
|
|
28
35
|
value: state.baseUrl,
|
|
29
36
|
}),
|
|
30
37
|
"",
|
|
@@ -36,7 +43,6 @@ function renderProviderField(state) {
|
|
|
36
43
|
"Stored only in the machine-local Tutti credential store.",
|
|
37
44
|
"",
|
|
38
45
|
...renderFieldInput({
|
|
39
|
-
label: "API Key",
|
|
40
46
|
value: state.apiKey,
|
|
41
47
|
secret: true,
|
|
42
48
|
}),
|
|
@@ -29,6 +29,9 @@ export function formatJoinLinkValidity(options) {
|
|
|
29
29
|
if (options.expiresAt !== undefined) {
|
|
30
30
|
const expiresAt = new Date(options.expiresAt);
|
|
31
31
|
if (!Number.isNaN(expiresAt.getTime())) {
|
|
32
|
+
if (options.includeRemaining === false) {
|
|
33
|
+
return `Join link valid until: ${options.expiresAt}`;
|
|
34
|
+
}
|
|
32
35
|
const now = options.now ?? new Date();
|
|
33
36
|
const remainingMs = expiresAt.getTime() - now.getTime();
|
|
34
37
|
const suffix = remainingMs <= 0
|