@xfey/tutti 0.1.10 → 0.1.12
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/control-plane/context-sync.js +1 -0
- package/dist/control-plane/event-publishers.js +21 -12
- package/dist/control-plane/execution-status.js +4 -0
- package/dist/control-plane/project-context-bootstrap.d.ts +2 -0
- package/dist/control-plane/project-context-bootstrap.js +1 -0
- package/dist/control-plane/reference-summary-refresh.js +4 -0
- package/dist/control-plane/run-start.js +1 -0
- package/dist/control-plane/task-compile-continuation.js +1 -0
- package/dist/control-plane/task-compile-start.d.ts +2 -1
- package/dist/control-plane/task-compile-start.js +5 -0
- package/dist/control-plane/types.d.ts +2 -1
- package/dist/control-plane/workflows/openai.js +19 -0
- package/dist/control-plane/workflows/types.d.ts +6 -1
- package/dist/procedure-engine/index.d.ts +10 -1
- package/dist/procedure-engine/index.js +34 -4
- package/dist/providers/openai/app-server/json-rpc.d.ts +6 -1
- package/dist/providers/openai/app-server/json-rpc.js +15 -1
- package/dist/providers/openai/app-server/read-only-procedure.d.ts +2 -0
- package/dist/providers/openai/app-server/read-only-procedure.js +13 -1
- package/dist/providers/openai/app-server/runtime-telemetry.d.ts +18 -0
- package/dist/providers/openai/app-server/runtime-telemetry.js +130 -0
- package/dist/providers/openai/app-server/workspace-write-run.d.ts +2 -0
- package/dist/providers/openai/app-server/workspace-write-run.js +13 -1
- package/dist/run-engine/index.d.ts +2 -1
- package/dist/run-engine/index.js +14 -1
- package/dist/run-pipeline/task-run-invocation.js +9 -0
- package/dist/server-shell/cli/args.d.ts +9 -9
- package/dist/server-shell/cli/args.js +31 -31
- package/dist/server-shell/cli/cli.js +27 -27
- package/dist/server-shell/cli/errors.d.ts +2 -1
- package/dist/server-shell/cli/errors.js +55 -12
- package/dist/server-shell/cli/host-lifecycle.js +7 -0
- package/dist/server-shell/cli/launch-command.d.ts +2 -2
- package/dist/server-shell/cli/launch-command.js +25 -11
- package/dist/server-shell/cli/launch.js +0 -3
- package/dist/server-shell/cli/managed-host.d.ts +2 -0
- package/dist/server-shell/cli/managed-host.js +9 -18
- package/dist/server-shell/cli/project-resolver.d.ts +6 -0
- package/dist/server-shell/cli/project-resolver.js +125 -3
- package/dist/server-shell/cli/provider-tui.d.ts +19 -0
- package/dist/server-shell/cli/provider-tui.js +64 -38
- package/dist/server-shell/cli/runtime-commands.d.ts +5 -5
- package/dist/server-shell/cli/runtime-commands.js +68 -25
- package/dist/server-shell/cli/terminal-qr.d.ts +5 -0
- package/dist/server-shell/cli/terminal-qr.js +17 -0
- package/dist/server-shell/http/routes/project-api/openapi-execution-routes.d.ts +14 -0
- package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +14 -0
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +28 -0
- package/dist/server-shell/http/routes/project-api/schemas.d.ts +14 -0
- package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +50 -0
- package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.js +25 -0
- package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +4 -1
- package/package.json +1 -1
- package/web/assets/index-CDUT1Etl.js +29 -0
- package/web/assets/{index-D8xJY8Gr.css → index-DIQpGeKY.css} +1 -1
- package/web/index.html +2 -2
- package/web/assets/index-BMMQgQjz.js +0 -29
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import { basename } from "node:path";
|
|
3
2
|
import { formatLaunchLifecycleResult, startLaunchProject, waitForForegroundHostShutdown } from "./host-lifecycle.js";
|
|
4
3
|
import { prepareLaunchProject } from "./launch.js";
|
|
5
4
|
import { spawnDetachedHost, waitForManagedHostReady } from "./managed-host.js";
|
|
6
|
-
import {
|
|
5
|
+
import { resolveLaunchWorkspacePath } from "./project-resolver.js";
|
|
6
|
+
import { formatJoinLinkValidity, renderTerminalQr } from "./terminal-qr.js";
|
|
7
7
|
import { renderTuttiTerminalLogo } from "./terminal-logo.js";
|
|
8
8
|
import { runProviderSetupTui } from "./provider-tui.js";
|
|
9
9
|
import { LaunchError } from "./errors.js";
|
|
@@ -24,9 +24,6 @@ export async function confirmFromTty(request) {
|
|
|
24
24
|
readline.close();
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
-
function projectName(preparation) {
|
|
28
|
-
return basename(preparation.workspace_root) || preparation.project_id;
|
|
29
|
-
}
|
|
30
27
|
async function waitForEnter() {
|
|
31
28
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
32
29
|
return;
|
|
@@ -48,13 +45,19 @@ function renderCompletionPage(options) {
|
|
|
48
45
|
...(isTty ? [CLEAR, renderTuttiTerminalLogo({ tty: true }), ""] : []),
|
|
49
46
|
"Tutti is running in the background.",
|
|
50
47
|
"",
|
|
51
|
-
`Project: ${options.projectName}`,
|
|
52
|
-
"",
|
|
53
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
|
+
}),
|
|
54
57
|
"",
|
|
55
58
|
renderTerminalQr(options.joinUrl),
|
|
56
59
|
"",
|
|
57
|
-
"
|
|
60
|
+
"[Enter] to continue",
|
|
58
61
|
].join("\n");
|
|
59
62
|
}
|
|
60
63
|
async function ensureProviderConfigured(preparation) {
|
|
@@ -70,8 +73,11 @@ async function ensureProviderConfigured(preparation) {
|
|
|
70
73
|
});
|
|
71
74
|
}
|
|
72
75
|
export async function runForegroundLaunchCommand(options) {
|
|
76
|
+
const workspacePath = resolveLaunchWorkspacePath({
|
|
77
|
+
...(options.target === undefined ? {} : { target: options.target }),
|
|
78
|
+
});
|
|
73
79
|
const result = await startLaunchProject({
|
|
74
|
-
workspacePath
|
|
80
|
+
workspacePath,
|
|
75
81
|
yes: options.yes,
|
|
76
82
|
confirm: confirmFromTty,
|
|
77
83
|
});
|
|
@@ -90,8 +96,11 @@ export async function runInternalHostRunCommand(options) {
|
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
98
|
export async function runBackgroundLaunchCommand(options) {
|
|
99
|
+
const workspacePath = resolveLaunchWorkspacePath({
|
|
100
|
+
...(options.target === undefined ? {} : { target: options.target }),
|
|
101
|
+
});
|
|
93
102
|
const preparation = await prepareLaunchProject({
|
|
94
|
-
workspacePath
|
|
103
|
+
workspacePath,
|
|
95
104
|
yes: options.yes,
|
|
96
105
|
confirm: confirmFromTty,
|
|
97
106
|
});
|
|
@@ -108,8 +117,13 @@ export async function runBackgroundLaunchCommand(options) {
|
|
|
108
117
|
throw new LaunchError("relay_registration_failed", "Host started but Relay did not return a visible join URL", "Run `tutti invite` to rotate a fresh invite link.");
|
|
109
118
|
}
|
|
110
119
|
process.stdout.write(`${renderCompletionPage({
|
|
111
|
-
projectName: projectName(preparation),
|
|
112
120
|
joinUrl: ready.join_url,
|
|
121
|
+
...(ready.join_token_expires_at === undefined
|
|
122
|
+
? {}
|
|
123
|
+
: { joinTokenExpiresAt: ready.join_token_expires_at }),
|
|
124
|
+
...(ready.join_token_reusable === undefined
|
|
125
|
+
? {}
|
|
126
|
+
: { joinTokenReusable: ready.join_token_reusable }),
|
|
113
127
|
})}\n`);
|
|
114
128
|
await waitForEnter();
|
|
115
129
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { createProjectId } from "@tutti/shared/ids";
|
|
3
|
-
import { redactText } from "@tutti/shared/utils";
|
|
4
3
|
import { readOpenAiProviderConfigProjection, resolveTuttiHome, } from "../../providers/openai/index.js";
|
|
5
4
|
import { LaunchError } from "./errors.js";
|
|
6
5
|
import { assertSafeProjectRoot, ensureGitBootstrap, } from "./git-bootstrap.js";
|
|
@@ -101,8 +100,6 @@ export function formatLaunchPreparation(result) {
|
|
|
101
100
|
"Tutti launch prerequisites are ready",
|
|
102
101
|
"",
|
|
103
102
|
"Status: prepared",
|
|
104
|
-
`Workspace: ${redactText(result.workspace_root)}`,
|
|
105
|
-
`Project: ${result.project_id}`,
|
|
106
103
|
`Branch: ${result.branch}`,
|
|
107
104
|
`Relay: ${result.relay_url}`,
|
|
108
105
|
`Provider: ${result.provider_status}`,
|
|
@@ -4,6 +4,8 @@ import type { ProjectId } from "@tutti/shared/ids";
|
|
|
4
4
|
export type ManagedHostReadyResult = {
|
|
5
5
|
endpoint: MachineRuntimeEndpointRecord;
|
|
6
6
|
join_url?: string;
|
|
7
|
+
join_token_expires_at?: string;
|
|
8
|
+
join_token_reusable?: boolean;
|
|
7
9
|
};
|
|
8
10
|
export declare function spawnDetachedHost(options: {
|
|
9
11
|
workspaceRoot: string;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
2
|
import { resolve } from "node:path";
|
|
4
3
|
import { createRuntimeEndpointProbe } from "./host-runtime-endpoint.js";
|
|
5
|
-
import {
|
|
4
|
+
import { LaunchError } from "./errors.js";
|
|
5
|
+
import { readMachineRuntimeEndpoint } from "./machine-local.js";
|
|
6
6
|
import { readHostLocalLaunchStatus, rotateHostLocalInvite } from "./local-control-client.js";
|
|
7
7
|
const DEFAULT_READY_TIMEOUT_MS = 60_000;
|
|
8
8
|
const READY_POLL_INTERVAL_MS = 250;
|
|
@@ -38,15 +38,6 @@ export function spawnDetachedHost(options) {
|
|
|
38
38
|
});
|
|
39
39
|
child.unref();
|
|
40
40
|
}
|
|
41
|
-
function readHostLogTail(tuttiHome, maxLines = 80) {
|
|
42
|
-
try {
|
|
43
|
-
const text = readFileSync(getHostLogFilePath(tuttiHome), "utf8");
|
|
44
|
-
return text.split(/\r?\n/u).filter(Boolean).slice(-maxLines).join("\n");
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return "";
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
41
|
export async function waitForManagedHostReady(options) {
|
|
51
42
|
const deadline = Date.now() + (options.timeoutMs ?? DEFAULT_READY_TIMEOUT_MS);
|
|
52
43
|
const probe = createRuntimeEndpointProbe(options.fetchImpl ?? fetch);
|
|
@@ -83,6 +74,12 @@ export async function waitForManagedHostReady(options) {
|
|
|
83
74
|
return {
|
|
84
75
|
endpoint,
|
|
85
76
|
join_url: status.relay.join_url,
|
|
77
|
+
...(status.relay.join_token_expires_at === undefined
|
|
78
|
+
? {}
|
|
79
|
+
: { join_token_expires_at: status.relay.join_token_expires_at }),
|
|
80
|
+
...(status.relay.join_token_reusable === undefined
|
|
81
|
+
? {}
|
|
82
|
+
: { join_token_reusable: status.relay.join_token_reusable }),
|
|
86
83
|
};
|
|
87
84
|
}
|
|
88
85
|
lastReason = "host is running but Relay invite is not visible yet";
|
|
@@ -92,12 +89,6 @@ export async function waitForManagedHostReady(options) {
|
|
|
92
89
|
}
|
|
93
90
|
await delay(READY_POLL_INTERVAL_MS);
|
|
94
91
|
}
|
|
95
|
-
|
|
96
|
-
throw new Error([
|
|
97
|
-
`Tutti host did not become ready before timeout: ${lastReason}`,
|
|
98
|
-
tail.length === 0 ? "" : `Recent host log:\n${tail}`,
|
|
99
|
-
]
|
|
100
|
-
.filter(Boolean)
|
|
101
|
-
.join("\n"));
|
|
92
|
+
throw new LaunchError("existing_server_unhealthy", `Tutti host did not become ready before timeout: ${lastReason}`, "Run `tutti logs` for details, then retry `tutti launch`.");
|
|
102
93
|
}
|
|
103
94
|
//# sourceMappingURL=managed-host.js.map
|
|
@@ -11,7 +11,13 @@ export type ExistingProjectContext = {
|
|
|
11
11
|
provider_config: OpenAiProviderConfigProjection;
|
|
12
12
|
provider_base_url?: string;
|
|
13
13
|
};
|
|
14
|
+
export declare function resolveLaunchWorkspacePath(options: {
|
|
15
|
+
target?: string;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
}): string;
|
|
14
19
|
export declare function resolveExistingProjectContext(options: {
|
|
20
|
+
target?: string;
|
|
15
21
|
workspacePath?: string;
|
|
16
22
|
cwd?: string;
|
|
17
23
|
env?: NodeJS.ProcessEnv;
|
|
@@ -1,19 +1,141 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { ID_PREFIXES, isPrefixedId } from "@tutti/shared/ids";
|
|
2
4
|
import { readOpenAiProviderConfigProjection, readProjectOpenAiProviderConfig, resolveTuttiHome, } from "../../providers/openai/index.js";
|
|
3
5
|
import { LaunchError } from "./errors.js";
|
|
4
6
|
import { assertSafeProjectRoot } from "./git-bootstrap.js";
|
|
5
7
|
import { readMachineProjectBinding, readMachineRuntimeEndpoint, } from "./machine-local.js";
|
|
6
8
|
import { readProjectDisplayName, readProjectIdentity } from "./project-identity.js";
|
|
9
|
+
function projectIdsFromTuttiHome(tuttiHome) {
|
|
10
|
+
const projectsRoot = join(tuttiHome, "projects");
|
|
11
|
+
if (!existsSync(projectsRoot)) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
return readdirSync(projectsRoot, { withFileTypes: true })
|
|
15
|
+
.filter((entry) => entry.isDirectory() && isPrefixedId(entry.name, ID_PREFIXES.project))
|
|
16
|
+
.map((entry) => entry.name)
|
|
17
|
+
.sort();
|
|
18
|
+
}
|
|
19
|
+
function readRuntimeEndpoint(tuttiHome, projectId) {
|
|
20
|
+
try {
|
|
21
|
+
return readMachineRuntimeEndpoint(tuttiHome, projectId);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function readBinding(tuttiHome, projectId) {
|
|
28
|
+
try {
|
|
29
|
+
return readMachineProjectBinding(tuttiHome, projectId);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function readProjectTargetEntry(tuttiHome, projectId) {
|
|
36
|
+
const binding = readBinding(tuttiHome, projectId);
|
|
37
|
+
const endpoint = readRuntimeEndpoint(tuttiHome, projectId);
|
|
38
|
+
const workspaceRoot = binding?.workspace_root ?? endpoint?.workspace_root;
|
|
39
|
+
if (workspaceRoot === undefined) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const workspaceName = basename(workspaceRoot) || projectId;
|
|
43
|
+
const displayName = readProjectDisplayName(workspaceRoot) ?? workspaceName;
|
|
44
|
+
const aliases = Array.from(new Set([displayName, workspaceName].filter((alias) => alias.trim() !== "")));
|
|
45
|
+
return {
|
|
46
|
+
project_id: projectId,
|
|
47
|
+
workspace_root: workspaceRoot,
|
|
48
|
+
display_name: displayName,
|
|
49
|
+
aliases,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function listProjectTargetEntries(tuttiHome) {
|
|
53
|
+
return projectIdsFromTuttiHome(tuttiHome)
|
|
54
|
+
.map((projectId) => readProjectTargetEntry(tuttiHome, projectId))
|
|
55
|
+
.filter((entry) => entry !== null);
|
|
56
|
+
}
|
|
57
|
+
function normalizeTargetName(value) {
|
|
58
|
+
return value.trim().toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
function formatTargetMatch(entry) {
|
|
61
|
+
return `${entry.project_id} ${entry.display_name} ${entry.workspace_root}`;
|
|
62
|
+
}
|
|
63
|
+
function resolveSingleTargetMatch(target, matches) {
|
|
64
|
+
if (matches.length === 1) {
|
|
65
|
+
return matches[0];
|
|
66
|
+
}
|
|
67
|
+
if (matches.length > 1) {
|
|
68
|
+
throw new LaunchError("project_target_ambiguous", `Project target is ambiguous: ${target}`, "Use the unique project ID prefix shown by `tutti ps`, or the full project ID from this error.", { matches: matches.map(formatTargetMatch) });
|
|
69
|
+
}
|
|
70
|
+
throw new LaunchError("project_target_not_found", `Project target was not found: ${target}`, "Run `tutti ps` to list known projects, or pass a project workspace path.");
|
|
71
|
+
}
|
|
72
|
+
function resolveProjectTargetWorkspaceRoot(options) {
|
|
73
|
+
const target = options.target.trim();
|
|
74
|
+
if (target === "") {
|
|
75
|
+
return resolve(options.cwd, ".");
|
|
76
|
+
}
|
|
77
|
+
const pathCandidate = resolve(options.cwd, target);
|
|
78
|
+
if (existsSync(pathCandidate)) {
|
|
79
|
+
return pathCandidate;
|
|
80
|
+
}
|
|
81
|
+
const entries = listProjectTargetEntries(options.tuttiHome);
|
|
82
|
+
const exactId = entries.filter((entry) => entry.project_id === target);
|
|
83
|
+
if (exactId.length > 0) {
|
|
84
|
+
return resolveSingleTargetMatch(target, exactId).workspace_root;
|
|
85
|
+
}
|
|
86
|
+
if (target.startsWith(ID_PREFIXES.project)) {
|
|
87
|
+
const idPrefix = entries.filter((entry) => entry.project_id.startsWith(target));
|
|
88
|
+
if (idPrefix.length > 0) {
|
|
89
|
+
return resolveSingleTargetMatch(target, idPrefix).workspace_root;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const normalizedTarget = normalizeTargetName(target);
|
|
93
|
+
const nameMatches = entries.filter((entry) => entry.aliases.some((alias) => normalizeTargetName(alias) === normalizedTarget));
|
|
94
|
+
return resolveSingleTargetMatch(target, nameMatches).workspace_root;
|
|
95
|
+
}
|
|
96
|
+
export function resolveLaunchWorkspacePath(options) {
|
|
97
|
+
const cwd = options.cwd ?? process.cwd();
|
|
98
|
+
const env = options.env ?? process.env;
|
|
99
|
+
const target = options.target;
|
|
100
|
+
if (target === undefined) {
|
|
101
|
+
return resolve(cwd, ".");
|
|
102
|
+
}
|
|
103
|
+
const pathCandidate = resolve(cwd, target);
|
|
104
|
+
if (existsSync(pathCandidate)) {
|
|
105
|
+
return pathCandidate;
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return resolveProjectTargetWorkspaceRoot({
|
|
109
|
+
target,
|
|
110
|
+
cwd,
|
|
111
|
+
tuttiHome: resolveTuttiHome(env.TUTTI_HOME, cwd),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (error instanceof LaunchError &&
|
|
116
|
+
error.code === "project_target_not_found" &&
|
|
117
|
+
!target.startsWith(ID_PREFIXES.project)) {
|
|
118
|
+
return pathCandidate;
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
7
123
|
export function resolveExistingProjectContext(options) {
|
|
8
124
|
const cwd = options.cwd ?? process.cwd();
|
|
9
125
|
const env = options.env ?? process.env;
|
|
10
|
-
const
|
|
126
|
+
const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
|
|
127
|
+
const workspaceRoot = options.workspacePath === undefined
|
|
128
|
+
? resolveProjectTargetWorkspaceRoot({
|
|
129
|
+
target: options.target ?? ".",
|
|
130
|
+
cwd,
|
|
131
|
+
tuttiHome,
|
|
132
|
+
})
|
|
133
|
+
: resolve(cwd, options.workspacePath);
|
|
11
134
|
assertSafeProjectRoot(workspaceRoot);
|
|
12
135
|
const identity = readProjectIdentity(workspaceRoot);
|
|
13
136
|
if (identity.kind !== "present") {
|
|
14
137
|
throw new LaunchError("project_identity_conflict", "Current directory is not a Tutti project", "Run `tutti launch` from a project root first.");
|
|
15
138
|
}
|
|
16
|
-
const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
|
|
17
139
|
const binding = readMachineProjectBinding(tuttiHome, identity.project_id);
|
|
18
140
|
const projectConfig = readProjectOpenAiProviderConfig(tuttiHome, identity.project_id);
|
|
19
141
|
return {
|
|
@@ -1,14 +1,33 @@
|
|
|
1
|
+
import { type ConfigureProjectOpenAiProviderResult } from "../../providers/openai/index.js";
|
|
1
2
|
import type { ProjectId } from "@tutti/shared/ids";
|
|
3
|
+
type ProviderSetupLogEvent = {
|
|
4
|
+
ts: string;
|
|
5
|
+
level: "error";
|
|
6
|
+
scope: "provider_setup";
|
|
7
|
+
event: "provider_validation_failed" | "provider_validation_exception";
|
|
8
|
+
project_id: ProjectId;
|
|
9
|
+
reason?: string;
|
|
10
|
+
retryable?: boolean;
|
|
11
|
+
diagnostic?: unknown;
|
|
12
|
+
error?: unknown;
|
|
13
|
+
};
|
|
2
14
|
export type ProviderSetupResult = {
|
|
3
15
|
base_url: string;
|
|
4
16
|
redacted_key: string;
|
|
5
17
|
default_model: string;
|
|
6
18
|
validated_at: string;
|
|
7
19
|
};
|
|
20
|
+
export declare function providerValidationFailureMessage(result: ConfigureProjectOpenAiProviderResult): string;
|
|
21
|
+
export declare function writeProviderSetupValidationLog(options: {
|
|
22
|
+
tuttiHome: string;
|
|
23
|
+
event: Omit<ProviderSetupLogEvent, "ts" | "level" | "scope">;
|
|
24
|
+
now?: () => Date;
|
|
25
|
+
}): boolean;
|
|
8
26
|
export declare function runProviderSetupTui(options: {
|
|
9
27
|
tuttiHome: string;
|
|
10
28
|
projectId: ProjectId;
|
|
11
29
|
stdin?: NodeJS.ReadStream;
|
|
12
30
|
stdout?: NodeJS.WriteStream;
|
|
13
31
|
}): Promise<ProviderSetupResult>;
|
|
32
|
+
export {};
|
|
14
33
|
//# sourceMappingURL=provider-tui.d.ts.map
|
|
@@ -2,6 +2,7 @@ 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
4
|
import { LaunchError } from "./errors.js";
|
|
5
|
+
import { appendHostLogLine, getHostLogFilePath } from "./machine-local.js";
|
|
5
6
|
import { renderTuttiTerminalLogo } from "./terminal-logo.js";
|
|
6
7
|
const HIDE_CURSOR = "\u001B[?25l";
|
|
7
8
|
const SHOW_CURSOR = "\u001B[?25h";
|
|
@@ -40,7 +41,9 @@ function renderProviderField(state) {
|
|
|
40
41
|
secret: true,
|
|
41
42
|
}),
|
|
42
43
|
"",
|
|
43
|
-
|
|
44
|
+
state.error === undefined
|
|
45
|
+
? "[Enter] to validate [Esc] to edit Base URL"
|
|
46
|
+
: "[Enter] to retry [Esc] to edit Base URL",
|
|
44
47
|
];
|
|
45
48
|
}
|
|
46
49
|
function renderProviderForm(options) {
|
|
@@ -66,41 +69,54 @@ function renderProviderForm(options) {
|
|
|
66
69
|
}
|
|
67
70
|
return lines.join("\n");
|
|
68
71
|
}
|
|
69
|
-
function
|
|
72
|
+
export function providerValidationFailureMessage(result) {
|
|
70
73
|
if (result.kind === "configured") {
|
|
71
74
|
return "";
|
|
72
75
|
}
|
|
73
|
-
|
|
74
|
-
|
|
76
|
+
switch (result.reason) {
|
|
77
|
+
case "provider_auth_invalid":
|
|
78
|
+
return "API key was rejected.";
|
|
79
|
+
case "provider_model_unavailable":
|
|
80
|
+
return "Default model is unavailable for this provider.";
|
|
81
|
+
case "provider_quota_or_billing_required":
|
|
82
|
+
return "Provider quota or billing is not available.";
|
|
83
|
+
case "provider_rate_limited":
|
|
84
|
+
return "Provider is rate limited. Try again later.";
|
|
85
|
+
case "provider_network_error":
|
|
86
|
+
return "Could not reach a Responses API-compatible endpoint.";
|
|
87
|
+
}
|
|
75
88
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
89
|
+
function providerValidationExceptionMessage(logged) {
|
|
90
|
+
if (logged) {
|
|
91
|
+
return "Could not validate the provider connection. Details were written to the host log.";
|
|
92
|
+
}
|
|
93
|
+
return "Could not validate the provider connection. Host log could not be written.";
|
|
94
|
+
}
|
|
95
|
+
export function writeProviderSetupValidationLog(options) {
|
|
96
|
+
const logEvent = {
|
|
97
|
+
ts: (options.now ?? (() => new Date()))().toISOString(),
|
|
98
|
+
level: "error",
|
|
99
|
+
scope: "provider_setup",
|
|
100
|
+
...options.event,
|
|
101
|
+
};
|
|
102
|
+
const redactedLogEvent = {
|
|
103
|
+
...logEvent,
|
|
104
|
+
...(logEvent.error === undefined ? {} : { error: redactError(logEvent.error) }),
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
appendHostLogLine(getHostLogFilePath(options.tuttiHome), JSON.stringify(redactError(redactedLogEvent)));
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
97
113
|
}
|
|
98
114
|
async function readProviderForm(options) {
|
|
99
115
|
const state = {
|
|
100
116
|
step: options.initialStep ?? "base-url",
|
|
101
117
|
baseUrl: options.initialBaseUrl ?? "",
|
|
102
118
|
apiKey: options.initialApiKey ?? "",
|
|
103
|
-
error:
|
|
119
|
+
error: options.initialError,
|
|
104
120
|
};
|
|
105
121
|
return await new Promise((resolve, reject) => {
|
|
106
122
|
const render = () => {
|
|
@@ -204,16 +220,18 @@ export async function runProviderSetupTui(options) {
|
|
|
204
220
|
const stdin = options.stdin ?? process.stdin;
|
|
205
221
|
const stdout = options.stdout ?? process.stdout;
|
|
206
222
|
if (!stdin.isTTY || !stdout.isTTY) {
|
|
207
|
-
throw new
|
|
223
|
+
throw new LaunchError("provider_configuration_required", "Provider setup requires an interactive terminal.", "Run `tutti provider setup` from an interactive terminal.");
|
|
208
224
|
}
|
|
209
225
|
let initialBaseUrl;
|
|
210
226
|
let initialApiKey = "";
|
|
211
227
|
let initialStep = "base-url";
|
|
228
|
+
let initialError;
|
|
212
229
|
while (true) {
|
|
213
230
|
const input = await readProviderForm({
|
|
214
231
|
...(initialBaseUrl === undefined ? {} : { initialBaseUrl }),
|
|
215
232
|
...(initialApiKey === "" ? {} : { initialApiKey }),
|
|
216
233
|
initialStep,
|
|
234
|
+
...(initialError === undefined ? {} : { initialError }),
|
|
217
235
|
stdin,
|
|
218
236
|
stdout,
|
|
219
237
|
});
|
|
@@ -243,17 +261,17 @@ export async function runProviderSetupTui(options) {
|
|
|
243
261
|
validated_at: result.projection.validated_at,
|
|
244
262
|
};
|
|
245
263
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
264
|
+
writeProviderSetupValidationLog({
|
|
265
|
+
tuttiHome: options.tuttiHome,
|
|
266
|
+
event: {
|
|
267
|
+
event: "provider_validation_failed",
|
|
268
|
+
project_id: options.projectId,
|
|
269
|
+
reason: result.reason,
|
|
270
|
+
retryable: result.retryable,
|
|
271
|
+
...(result.diagnostic === undefined ? {} : { diagnostic: result.diagnostic }),
|
|
252
272
|
},
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}));
|
|
256
|
-
await waitForKeypress(stdin);
|
|
273
|
+
});
|
|
274
|
+
initialError = providerValidationFailureMessage(result);
|
|
257
275
|
}
|
|
258
276
|
catch (error) {
|
|
259
277
|
clearInterval(interval);
|
|
@@ -261,7 +279,15 @@ export async function runProviderSetupTui(options) {
|
|
|
261
279
|
if (error instanceof LaunchError) {
|
|
262
280
|
throw error;
|
|
263
281
|
}
|
|
264
|
-
|
|
282
|
+
const logged = writeProviderSetupValidationLog({
|
|
283
|
+
tuttiHome: options.tuttiHome,
|
|
284
|
+
event: {
|
|
285
|
+
event: "provider_validation_exception",
|
|
286
|
+
project_id: options.projectId,
|
|
287
|
+
error,
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
initialError = providerValidationExceptionMessage(logged);
|
|
265
291
|
}
|
|
266
292
|
}
|
|
267
293
|
}
|
|
@@ -11,6 +11,7 @@ export type RuntimeProjectRow = {
|
|
|
11
11
|
relay_project_ref?: string;
|
|
12
12
|
join_url?: string;
|
|
13
13
|
};
|
|
14
|
+
export declare function formatProjectIdForDisplay(projectId: ProjectId, projectIds: readonly ProjectId[]): string;
|
|
14
15
|
export declare function listRuntimeProjects(options?: {
|
|
15
16
|
env?: NodeJS.ProcessEnv;
|
|
16
17
|
cwd?: string;
|
|
@@ -21,12 +22,11 @@ export declare function runPsManageCommand(options?: {
|
|
|
21
22
|
stdin?: NodeJS.ReadStream;
|
|
22
23
|
stdout?: NodeJS.WriteStream;
|
|
23
24
|
}): Promise<void>;
|
|
24
|
-
export declare function runStopCommand(
|
|
25
|
-
export declare function runInviteCommand(
|
|
26
|
-
export declare function runProviderStatusCommand(
|
|
27
|
-
export declare function runLogsCommand(
|
|
25
|
+
export declare function runStopCommand(target?: string): Promise<string>;
|
|
26
|
+
export declare function runInviteCommand(target?: string): Promise<string>;
|
|
27
|
+
export declare function runProviderStatusCommand(target?: string): string;
|
|
28
|
+
export declare function runLogsCommand(target?: string, tailLines?: number): string;
|
|
28
29
|
export declare function readRuntimeEndpointForProject(tuttiHome: string, projectId: ProjectId): MachineRuntimeEndpointRecord | null;
|
|
29
30
|
export declare function runtimeEndpointPathForProject(tuttiHome: string, projectId: ProjectId): string;
|
|
30
31
|
export declare function projectLocalStoreRoot(tuttiHome: string, projectId: ProjectId): string;
|
|
31
|
-
export declare function resolveWorkspacePath(value: string | undefined): string;
|
|
32
32
|
//# sourceMappingURL=runtime-commands.d.ts.map
|