@xfey/tutti 0.1.22 → 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.
- package/dist/server-shell/cli/host-lifecycle.js +2 -19
- package/dist/server-shell/cli/launch-command.d.ts +9 -0
- package/dist/server-shell/cli/launch-command.js +64 -31
- package/dist/server-shell/cli/provider-tui.js +10 -4
- package/dist/server-shell/cli/terminal-qr.d.ts +4 -0
- package/dist/server-shell/cli/terminal-qr.js +20 -0
- package/package.json +1 -1
|
@@ -10,28 +10,11 @@ import { HOST_SERVER_VERSION, startForegroundHostServer, } from "./host-server-r
|
|
|
10
10
|
import { deleteMachineRuntimeEndpoint, readHostRegistrationSecret, readMachineProjectBinding, readMachineRuntimeEndpoint, } from "./machine-local.js";
|
|
11
11
|
import { prepareLaunchProject, resolveLaunchLocalContext, } from "./launch.js";
|
|
12
12
|
import { registerHostWithRelay, relayErrorToLaunchError, } from "./relay-registration.js";
|
|
13
|
-
import { formatJoinLinkValidity } from "./terminal-qr.js";
|
|
13
|
+
import { formatJoinLinkValidity, formatTerminalLink } from "./terminal-qr.js";
|
|
14
14
|
export { createRuntimeEndpointProbe } from "./host-runtime-endpoint.js";
|
|
15
15
|
const DEFAULT_SHUTDOWN_WAIT_MS = 2_000;
|
|
16
|
-
const OSC8_START = "\u001B]8;;";
|
|
17
|
-
const OSC8_END = "\u001B]8;;\u001B\\";
|
|
18
|
-
function isTerminalSafeHttpUrl(value) {
|
|
19
|
-
if (!value.startsWith("http://") && !value.startsWith("https://")) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
return Array.from(value).every((character) => {
|
|
23
|
-
const codePoint = character.codePointAt(0);
|
|
24
|
-
return codePoint !== undefined && codePoint > 0x1f && codePoint !== 0x7f;
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
function terminalLink(value, options) {
|
|
28
|
-
if (options.hyperlinks !== true || !isTerminalSafeHttpUrl(value)) {
|
|
29
|
-
return value;
|
|
30
|
-
}
|
|
31
|
-
return `${OSC8_START}${value}\u001B\\${value}${OSC8_END}`;
|
|
32
|
-
}
|
|
33
16
|
function urlLine(label, value, options) {
|
|
34
|
-
return `${label}: ${
|
|
17
|
+
return `${label}: ${formatTerminalLink(value, options)}`;
|
|
35
18
|
}
|
|
36
19
|
function endpointWithoutToken(endpoint) {
|
|
37
20
|
return {
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { LaunchConfirmationRequest } from "./git-bootstrap.js";
|
|
2
2
|
export declare function confirmFromTty(request: LaunchConfirmationRequest): Promise<boolean>;
|
|
3
|
+
export declare function renderCompletionPage(options: {
|
|
4
|
+
joinUrl: string;
|
|
5
|
+
joinTokenExpiresAt?: string;
|
|
6
|
+
joinTokenReusable?: boolean;
|
|
7
|
+
projectId: string;
|
|
8
|
+
workspaceRoot: string;
|
|
9
|
+
tty?: boolean;
|
|
10
|
+
hyperlinks?: boolean;
|
|
11
|
+
}): string;
|
|
3
12
|
export declare function runForegroundLaunchCommand(options: {
|
|
4
13
|
target?: string;
|
|
5
14
|
yes: boolean;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
2
3
|
import { formatLaunchLifecycleResult, startLaunchProject, waitForForegroundHostShutdown } from "./host-lifecycle.js";
|
|
3
4
|
import { prepareLaunchProject } from "./launch.js";
|
|
4
5
|
import { spawnDetachedHost, waitForManagedHostReady } from "./managed-host.js";
|
|
5
6
|
import { resolveLaunchWorkspacePath } from "./project-resolver.js";
|
|
6
|
-
import { formatJoinLinkValidity,
|
|
7
|
+
import { formatJoinLinkValidity, formatTerminalLink } from "./terminal-qr.js";
|
|
7
8
|
import { renderTuttiTerminalLogo } from "./terminal-logo.js";
|
|
8
9
|
import { runProviderSetupTui } from "./provider-tui.js";
|
|
9
10
|
import { LaunchError } from "./errors.js";
|
|
@@ -24,40 +25,69 @@ export async function confirmFromTty(request) {
|
|
|
24
25
|
readline.close();
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
function formatProjectLabel(workspaceRoot, projectId) {
|
|
29
|
+
return basename(workspaceRoot) || projectId;
|
|
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)}`;
|
|
30
43
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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)}`;
|
|
40
59
|
}
|
|
60
|
+
return stripped.length;
|
|
41
61
|
}
|
|
42
|
-
function
|
|
43
|
-
const
|
|
62
|
+
function borderedBlock(lines) {
|
|
63
|
+
const width = Math.max(...lines.map(visibleTerminalLength));
|
|
64
|
+
const top = `╭${"─".repeat(width + 2)}╮`;
|
|
65
|
+
const bottom = `╰${"─".repeat(width + 2)}╯`;
|
|
66
|
+
const body = lines.map((line) => `│ ${line}${" ".repeat(width - visibleTerminalLength(line))} │`);
|
|
67
|
+
return [top, ...body, bottom].join("\n");
|
|
68
|
+
}
|
|
69
|
+
export function renderCompletionPage(options) {
|
|
70
|
+
const isTty = options.tty ?? process.stdout.isTTY === true;
|
|
71
|
+
const projectLabel = formatProjectLabel(options.workspaceRoot, options.projectId);
|
|
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
|
+
});
|
|
82
|
+
const summaryLines = [
|
|
83
|
+
"Tutti is hosting this project.",
|
|
84
|
+
`Project: ${projectLabel}`,
|
|
85
|
+
`Join URL: ${joinUrl}`,
|
|
86
|
+
joinValidity,
|
|
87
|
+
];
|
|
44
88
|
return [
|
|
45
89
|
...(isTty ? [CLEAR, renderTuttiTerminalLogo({ tty: true }), ""] : []),
|
|
46
|
-
|
|
47
|
-
"",
|
|
48
|
-
`Join URL: ${options.joinUrl}`,
|
|
49
|
-
formatJoinLinkValidity({
|
|
50
|
-
...(options.joinTokenExpiresAt === undefined
|
|
51
|
-
? {}
|
|
52
|
-
: { expiresAt: options.joinTokenExpiresAt }),
|
|
53
|
-
...(options.joinTokenReusable === undefined
|
|
54
|
-
? {}
|
|
55
|
-
: { reusable: options.joinTokenReusable }),
|
|
56
|
-
}),
|
|
57
|
-
"",
|
|
58
|
-
renderTerminalQr(options.joinUrl),
|
|
59
|
-
"",
|
|
60
|
-
"[Enter] to continue",
|
|
90
|
+
isTty ? borderedBlock(summaryLines) : summaryLines.join("\n"),
|
|
61
91
|
].join("\n");
|
|
62
92
|
}
|
|
63
93
|
async function ensureProviderConfigured(preparation) {
|
|
@@ -118,6 +148,10 @@ export async function runBackgroundLaunchCommand(options) {
|
|
|
118
148
|
}
|
|
119
149
|
process.stdout.write(`${renderCompletionPage({
|
|
120
150
|
joinUrl: ready.join_url,
|
|
151
|
+
projectId: preparation.project_id,
|
|
152
|
+
workspaceRoot: preparation.workspace_root,
|
|
153
|
+
tty: process.stdout.isTTY === true,
|
|
154
|
+
hyperlinks: process.stdout.isTTY === true,
|
|
121
155
|
...(ready.join_token_expires_at === undefined
|
|
122
156
|
? {}
|
|
123
157
|
: { joinTokenExpiresAt: ready.join_token_expires_at }),
|
|
@@ -125,6 +159,5 @@ export async function runBackgroundLaunchCommand(options) {
|
|
|
125
159
|
? {}
|
|
126
160
|
: { joinTokenReusable: ready.join_token_reusable }),
|
|
127
161
|
})}\n`);
|
|
128
|
-
await waitForEnter();
|
|
129
162
|
}
|
|
130
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
|
}),
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
export declare function formatTerminalLink(value: string, options?: {
|
|
2
|
+
hyperlinks?: boolean;
|
|
3
|
+
}): string;
|
|
1
4
|
export declare function renderTerminalQr(input: string): string;
|
|
2
5
|
export declare function formatJoinLinkValidity(options: {
|
|
3
6
|
expiresAt?: string;
|
|
4
7
|
reusable?: boolean;
|
|
5
8
|
now?: Date;
|
|
9
|
+
includeRemaining?: boolean;
|
|
6
10
|
}): string;
|
|
7
11
|
//# sourceMappingURL=terminal-qr.d.ts.map
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
const require = createRequire(import.meta.url);
|
|
3
|
+
const OSC8_START = "\u001B]8;;";
|
|
4
|
+
const OSC8_END = "\u001B]8;;\u001B\\";
|
|
5
|
+
function isTerminalSafeHttpUrl(value) {
|
|
6
|
+
if (!value.startsWith("http://") && !value.startsWith("https://")) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
return Array.from(value).every((character) => {
|
|
10
|
+
const codePoint = character.codePointAt(0);
|
|
11
|
+
return codePoint !== undefined && codePoint > 0x1f && codePoint !== 0x7f;
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function formatTerminalLink(value, options = {}) {
|
|
15
|
+
if (options.hyperlinks !== true || !isTerminalSafeHttpUrl(value)) {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return `${OSC8_START}${value}\u001B\\${value}${OSC8_END}`;
|
|
19
|
+
}
|
|
3
20
|
export function renderTerminalQr(input) {
|
|
4
21
|
const qrcode = require("qrcode-terminal");
|
|
5
22
|
let output = "";
|
|
@@ -12,6 +29,9 @@ export function formatJoinLinkValidity(options) {
|
|
|
12
29
|
if (options.expiresAt !== undefined) {
|
|
13
30
|
const expiresAt = new Date(options.expiresAt);
|
|
14
31
|
if (!Number.isNaN(expiresAt.getTime())) {
|
|
32
|
+
if (options.includeRemaining === false) {
|
|
33
|
+
return `Join link valid until: ${options.expiresAt}`;
|
|
34
|
+
}
|
|
15
35
|
const now = options.now ?? new Date();
|
|
16
36
|
const remainingMs = expiresAt.getTime() - now.getTime();
|
|
17
37
|
const suffix = remainingMs <= 0
|