@xfey/tutti 0.1.32 → 0.1.33
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/README.md +4 -3
- package/dist/server-shell/cli/args.d.ts +4 -0
- package/dist/server-shell/cli/args.js +10 -1
- package/dist/server-shell/cli/cli.js +22 -3
- package/dist/server-shell/cli/host-server-runtime.js +2 -10
- package/dist/server-shell/http/routes/project-api/project-timeline-projection.js +4 -4
- package/dist/server-shell/http/static-web.d.ts +1 -0
- package/dist/server-shell/http/static-web.js +14 -1
- package/dist/server-shell/local-console/browser-open.d.ts +5 -0
- package/dist/server-shell/local-console/browser-open.js +40 -0
- package/dist/server-shell/local-console/folder-picker.d.ts +21 -0
- package/dist/server-shell/local-console/folder-picker.js +109 -0
- package/dist/server-shell/local-console/index.d.ts +4 -0
- package/dist/server-shell/local-console/index.js +4 -0
- package/dist/server-shell/local-console/managed-console.d.ts +9 -0
- package/dist/server-shell/local-console/managed-console.js +94 -0
- package/dist/server-shell/local-console/project-service.d.ts +57 -0
- package/dist/server-shell/local-console/project-service.js +217 -0
- package/dist/server-shell/local-console/runtime-endpoint.d.ts +21 -0
- package/dist/server-shell/local-console/runtime-endpoint.js +74 -0
- package/dist/server-shell/local-console/server.d.ts +17 -0
- package/dist/server-shell/local-console/server.js +301 -0
- package/dist/server-shell/local-console/session.d.ts +33 -0
- package/dist/server-shell/local-console/session.js +116 -0
- package/package.json +1 -1
- package/web/assets/index-BcZpeSaX.css +1 -0
- package/web/assets/index-DKf4jz_E.js +29 -0
- package/web/assets/tutti_avatar-BBhaZGi3.png +0 -0
- package/web/index.html +6 -3
- package/web/assets/index-17FuaN3j.js +0 -29
- package/web/assets/index-C3nAJcU3.css +0 -1
- package/web/assets/tutti_avatar-DAVuzlig.png +0 -0
package/README.md
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## 作用
|
|
4
4
|
|
|
5
|
-
`apps/server` 是 Tutti 的项目 host service
|
|
5
|
+
`apps/server` 是 Tutti 的项目 host service,也是机器级 Local Console 的进程入口。
|
|
6
6
|
|
|
7
7
|
它负责:
|
|
8
8
|
|
|
9
9
|
- `tutti launch` 后的项目绑定、恢复和接管入口
|
|
10
|
+
- 裸 `tutti` 启动 / 复用的 loopback Local Console 与既有项目 Host 编排
|
|
10
11
|
- Fastify API、SSE、packaged static Web 产物服务和 server 生命周期
|
|
11
12
|
- SQLite store、migrations、repository 与协作真相读写
|
|
12
13
|
- `control-plane`、scheduler、`procedure-engine`、`run-engine`、`workspace-ops` 与 OpenAI provider 接入
|
|
@@ -24,8 +25,8 @@
|
|
|
24
25
|
|
|
25
26
|
## 当前状态
|
|
26
27
|
|
|
27
|
-
- 已具备 Fastify host shell、后台 host manager CLI
|
|
28
|
-
- Host server 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、health
|
|
28
|
+
- 已具备 Fastify host shell、后台 host manager CLI 套件、机器级 Local Console、Dev Runner、Host Project API、SSE、SQLite store、collaboration-state、workspace read-state、project timeline observability helper、Control Plane、Procedure / Run、workspace-ops、checks、Relay staged upload finalize、user skills 文件服务、artifacts 可视化产物查看层和 OpenAI / Codex provider 接入。
|
|
29
|
+
- Host server 与 Local Console 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、health、host-local control 和 `/local-console/*` API 不被 SPA fallback 吞掉。
|
|
29
30
|
- Root `pack:dry-run` 会先清理 build output、重新构建,再复制 `apps/web/dist` 到 package-local `web/`,生成临时 staging `@xfey/tutti` artifact,并校验 npm pack 文件列表包含 CLI、migrations、prompts、Web index、bundled `@tutti/shared` / `@tutti/relay-client` 以及它们的外部 runtime dependencies,同时排除源码、source map、日志、SQLite、`.env`、`.tutti-dev` 与测试产物。
|
|
30
31
|
- Root `smoke:packed-cli` 同样从干净 build output 生成 staging `@xfey/tutti` tarball、在临时目录安装,并验证 SQLite native binding、`tutti --help` / `tutti launch --help` / `tutti archive --help` / `tutti --version`、migrations、prompts、Web static index、bundled internal dependencies、bundled external runtime dependencies、CLI QR dependency、仓库锁定的 `@openai/codex` binary 可执行性和临时全局安装路径;该 smoke 不需要 Tutti source repo 存在。
|
|
31
32
|
- `prompts/` 是所有模型-facing prompt、指令模板和 Codex 执行模板的统一落点;长 prompt 不内联在 TypeScript 中。
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export type CliCommand = {
|
|
2
|
+
kind: "console";
|
|
3
|
+
} | {
|
|
2
4
|
kind: "help";
|
|
3
5
|
topic?: string;
|
|
4
6
|
} | {
|
|
@@ -12,6 +14,8 @@ export type CliCommand = {
|
|
|
12
14
|
kind: "internal-host-run";
|
|
13
15
|
workspacePath: string;
|
|
14
16
|
yes: boolean;
|
|
17
|
+
} | {
|
|
18
|
+
kind: "internal-console-run";
|
|
15
19
|
} | {
|
|
16
20
|
kind: "ps";
|
|
17
21
|
manage: boolean;
|
|
@@ -49,7 +49,10 @@ function readOptionalTarget(args) {
|
|
|
49
49
|
export function parseCliArgs(argv, cwd = process.cwd()) {
|
|
50
50
|
const args = [...argv];
|
|
51
51
|
const command = args.shift();
|
|
52
|
-
if (command === undefined
|
|
52
|
+
if (command === undefined) {
|
|
53
|
+
return { kind: "console" };
|
|
54
|
+
}
|
|
55
|
+
if (command === "--help" || command === "-h" || command === "help") {
|
|
53
56
|
return { kind: "help", ...(args[0] === undefined ? {} : { topic: args[0] }) };
|
|
54
57
|
}
|
|
55
58
|
if (command === "--version" || command === "-v" || command === "version") {
|
|
@@ -83,6 +86,12 @@ export function parseCliArgs(argv, cwd = process.cwd()) {
|
|
|
83
86
|
}
|
|
84
87
|
if (command === "internal") {
|
|
85
88
|
const internalCommand = args.shift();
|
|
89
|
+
if (internalCommand === "console-run") {
|
|
90
|
+
if (args.length > 0) {
|
|
91
|
+
throw new LaunchError("git_bootstrap_failed", "Internal console-run does not accept options", "Internal commands are managed by Tutti.");
|
|
92
|
+
}
|
|
93
|
+
return { kind: "internal-console-run" };
|
|
94
|
+
}
|
|
86
95
|
if (internalCommand !== "host-run") {
|
|
87
96
|
throw new LaunchError("git_bootstrap_failed", "Unsupported internal command", "Internal commands are managed by Tutti.");
|
|
88
97
|
}
|
|
@@ -6,23 +6,28 @@ import { resolveExistingProjectContext } from "./project-resolver.js";
|
|
|
6
6
|
import { runProviderSetupTui } from "./provider-tui.js";
|
|
7
7
|
import { runArchiveCommand, runDoctorCommand, runForgetCommand, runInviteCommand, runLogsCommand, runProviderStatusCommand, runPsManageCommand, runPsCommand, runStopCommand, } from "./runtime-commands.js";
|
|
8
8
|
import { readCliVersion } from "./version.js";
|
|
9
|
+
import { ensureLocalConsole, openLocalConsoleBrowser, runLocalConsoleProcess, } from "../local-console/index.js";
|
|
9
10
|
const HELP_TEXT = `Usage:
|
|
11
|
+
tutti
|
|
10
12
|
tutti launch [target] [--yes] [--foreground]
|
|
11
13
|
tutti ps [--manage] [--all]
|
|
12
14
|
tutti invite [target]
|
|
13
15
|
tutti stop [target]
|
|
14
|
-
tutti archive <target> [--yes]
|
|
15
|
-
tutti forget <target> [--yes]
|
|
16
|
-
tutti logs [target] [--tail 120]
|
|
17
16
|
tutti provider setup [target]
|
|
18
17
|
tutti provider status [target]
|
|
18
|
+
|
|
19
|
+
Advanced and diagnostic commands:
|
|
20
|
+
tutti logs [target] [--tail 120]
|
|
19
21
|
tutti doctor [target]
|
|
22
|
+
tutti archive <target> [--yes]
|
|
23
|
+
tutti forget <target> [--yes]
|
|
20
24
|
|
|
21
25
|
Options:
|
|
22
26
|
-h, --help Show help.
|
|
23
27
|
-v, --version Show version.
|
|
24
28
|
|
|
25
29
|
Notes:
|
|
30
|
+
tutti opens the machine-local project console in your browser.
|
|
26
31
|
launch starts the host in the background by default.
|
|
27
32
|
provider setup stores credentials in the machine-local Tutti provider store.
|
|
28
33
|
`;
|
|
@@ -151,9 +156,20 @@ async function runProviderSetupCommand(target) {
|
|
|
151
156
|
].join("\n"));
|
|
152
157
|
process.stdout.write("\n");
|
|
153
158
|
}
|
|
159
|
+
async function runLocalConsoleCommand() {
|
|
160
|
+
const console = await ensureLocalConsole();
|
|
161
|
+
if (openLocalConsoleBrowser(console.url)) {
|
|
162
|
+
process.stdout.write(`Tutti Local Console is ready at ${console.endpoint.base_url}/console\n`);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
process.stdout.write(["Open Tutti Local Console in a browser:", console.url, ""].join("\n"));
|
|
166
|
+
}
|
|
154
167
|
try {
|
|
155
168
|
const command = parseCliArgs(process.argv.slice(2));
|
|
156
169
|
switch (command.kind) {
|
|
170
|
+
case "console":
|
|
171
|
+
await runLocalConsoleCommand();
|
|
172
|
+
break;
|
|
157
173
|
case "help":
|
|
158
174
|
process.stdout.write(helpForTopic(command.topic));
|
|
159
175
|
break;
|
|
@@ -172,6 +188,9 @@ try {
|
|
|
172
188
|
case "internal-host-run":
|
|
173
189
|
await runInternalHostRunCommand(command);
|
|
174
190
|
break;
|
|
191
|
+
case "internal-console-run":
|
|
192
|
+
await runLocalConsoleProcess();
|
|
193
|
+
break;
|
|
175
194
|
case "ps":
|
|
176
195
|
if (command.manage) {
|
|
177
196
|
await runPsManageCommand({ all: command.all });
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { basename, join, resolve } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
1
|
+
import { basename, join } from "node:path";
|
|
4
2
|
import { serializeRuntimeLogLine } from "@tutti/shared/utils";
|
|
5
3
|
import { completeRelayUpload, resolveRelayUpload } from "@tutti/relay-client";
|
|
6
4
|
import { ApprovalRequestManager } from "../../approvals/index.js";
|
|
@@ -14,6 +12,7 @@ import { readProviderUsageProjection, recordProviderUsageEvent, } from "../../pr
|
|
|
14
12
|
import { resolveProjectOpenAiRunPipelineRunner } from "../../run-pipeline/openai.js";
|
|
15
13
|
import { openHostProjectStore } from "../../store/index.js";
|
|
16
14
|
import { cleanupStaleRunWorkspaces } from "../../workspace-ops/index.js";
|
|
15
|
+
import { resolvePackagedWebStaticRoot } from "../http/static-web.js";
|
|
17
16
|
import { AGENT_CONTEXT_BASE_PATH, AgentContextTokenRegistry, } from "../http/routes/agent-context.js";
|
|
18
17
|
import { readHostRepoProjection } from "../http/routes/project-api.js";
|
|
19
18
|
import { WorkspaceEventBus } from "../http/workspace-events.js";
|
|
@@ -38,13 +37,6 @@ function createHostRuntimeLogger(options) {
|
|
|
38
37
|
},
|
|
39
38
|
};
|
|
40
39
|
}
|
|
41
|
-
function resolvePackagedWebStaticRoot(env = process.env) {
|
|
42
|
-
if (env.TUTTI_WEB_STATIC_ROOT !== undefined && env.TUTTI_WEB_STATIC_ROOT.trim() !== "") {
|
|
43
|
-
return resolve(env.TUTTI_WEB_STATIC_ROOT);
|
|
44
|
-
}
|
|
45
|
-
const packagedRoot = fileURLToPath(new URL("../../../web", import.meta.url));
|
|
46
|
-
return existsSync(join(packagedRoot, "index.html")) ? packagedRoot : undefined;
|
|
47
|
-
}
|
|
48
40
|
function createAgentContextRuntime(options) {
|
|
49
41
|
return {
|
|
50
42
|
baseUrl: new URL(AGENT_CONTEXT_BASE_PATH, options.baseUrl).toString(),
|
|
@@ -12,10 +12,10 @@ const RUN_USAGE_SOURCES = [
|
|
|
12
12
|
const TASK_ACCEPTED_USAGE_SOURCES = ["task_compile"];
|
|
13
13
|
const TRACE_CONSENSUS_LIMIT = 3;
|
|
14
14
|
const TRACE_TASK_CHANGE_LIMIT = 2;
|
|
15
|
-
const TRACE_CONTRACT_SCOPE_LIMIT =
|
|
16
|
-
const TRACE_CHANGED_PATH_LIMIT =
|
|
17
|
-
const TRACE_CHECKPOINT_ITEM_LIMIT =
|
|
18
|
-
const TRACE_GENERATED_FILE_LIMIT =
|
|
15
|
+
const TRACE_CONTRACT_SCOPE_LIMIT = 40;
|
|
16
|
+
const TRACE_CHANGED_PATH_LIMIT = 100;
|
|
17
|
+
const TRACE_CHECKPOINT_ITEM_LIMIT = 50;
|
|
18
|
+
const TRACE_GENERATED_FILE_LIMIT = 50;
|
|
19
19
|
const TRACE_SOURCE_SNIPPET_LIMIT = 3;
|
|
20
20
|
const TRACE_SOURCE_SNIPPET_BODY_LIMIT = 280;
|
|
21
21
|
const TRACE_CHANGE_LINK_LIMIT = 3;
|
|
@@ -3,4 +3,5 @@ export type HostWebStaticOptions = {
|
|
|
3
3
|
root: string;
|
|
4
4
|
};
|
|
5
5
|
export declare function registerHostWebStaticRoutes(app: FastifyInstance, options: HostWebStaticOptions): void;
|
|
6
|
+
export declare function resolvePackagedWebStaticRoot(env?: NodeJS.ProcessEnv): string | undefined;
|
|
6
7
|
//# sourceMappingURL=static-web.d.ts.map
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import fastifyStatic from "@fastify/static";
|
|
4
5
|
const BLOCKED_SPA_FALLBACK_PREFIXES = [
|
|
5
6
|
"/api/",
|
|
6
7
|
"/health",
|
|
7
8
|
"/host-local/",
|
|
9
|
+
"/local-console/",
|
|
8
10
|
"/assets/",
|
|
9
11
|
];
|
|
10
12
|
function requestPath(request) {
|
|
@@ -49,4 +51,15 @@ export function registerHostWebStaticRoutes(app, options) {
|
|
|
49
51
|
sendIndex(reply, indexHtml);
|
|
50
52
|
});
|
|
51
53
|
}
|
|
54
|
+
export function resolvePackagedWebStaticRoot(env = process.env) {
|
|
55
|
+
if (env.TUTTI_WEB_STATIC_ROOT !== undefined && env.TUTTI_WEB_STATIC_ROOT.trim() !== "") {
|
|
56
|
+
return resolve(env.TUTTI_WEB_STATIC_ROOT);
|
|
57
|
+
}
|
|
58
|
+
const packagedRoot = fileURLToPath(new URL("../../../web", import.meta.url));
|
|
59
|
+
if (existsSync(join(packagedRoot, "index.html"))) {
|
|
60
|
+
return packagedRoot;
|
|
61
|
+
}
|
|
62
|
+
const workspaceWebDist = fileURLToPath(new URL("../../../../web/dist", import.meta.url));
|
|
63
|
+
return existsSync(join(workspaceWebDist, "index.html")) ? workspaceWebDist : undefined;
|
|
64
|
+
}
|
|
52
65
|
//# sourceMappingURL=static-web.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { constants, accessSync } from "node:fs";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
function executableInPath(command, env) {
|
|
5
|
+
for (const directory of (env.PATH ?? "").split(delimiter)) {
|
|
6
|
+
if (directory.trim() === "") {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
const candidate = join(directory, command);
|
|
10
|
+
try {
|
|
11
|
+
accessSync(candidate, constants.X_OK);
|
|
12
|
+
return candidate;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// Keep searching PATH.
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function openLocalConsoleBrowser(url, options = {}) {
|
|
21
|
+
const platform = options.platform ?? process.platform;
|
|
22
|
+
const env = options.env ?? process.env;
|
|
23
|
+
const command = platform === "darwin"
|
|
24
|
+
? "/usr/bin/open"
|
|
25
|
+
: platform === "linux" && (env.DISPLAY !== undefined || env.WAYLAND_DISPLAY !== undefined)
|
|
26
|
+
? executableInPath("xdg-open", env)
|
|
27
|
+
: null;
|
|
28
|
+
if (command === null) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const child = spawn(command, [url], {
|
|
32
|
+
detached: true,
|
|
33
|
+
env,
|
|
34
|
+
stdio: "ignore",
|
|
35
|
+
});
|
|
36
|
+
child.on("error", () => undefined);
|
|
37
|
+
child.unref();
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=browser-open.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type LocalFolderPickerKind = "macos" | "zenity" | "kdialog" | "unavailable";
|
|
2
|
+
export type LocalFolderPickResult = {
|
|
3
|
+
kind: "selected";
|
|
4
|
+
path: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: "cancelled";
|
|
7
|
+
} | {
|
|
8
|
+
kind: "unavailable";
|
|
9
|
+
message: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function detectLocalFolderPicker(platform?: NodeJS.Platform, env?: NodeJS.ProcessEnv): {
|
|
12
|
+
kind: LocalFolderPickerKind;
|
|
13
|
+
executable?: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function pickLocalFolder(options?: {
|
|
16
|
+
prompt?: string;
|
|
17
|
+
platform?: NodeJS.Platform;
|
|
18
|
+
env?: NodeJS.ProcessEnv;
|
|
19
|
+
}): Promise<LocalFolderPickResult>;
|
|
20
|
+
export declare function createLocalProjectFolder(parentPath: string, childName: string): string;
|
|
21
|
+
//# sourceMappingURL=folder-picker.d.ts.map
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { constants, accessSync, existsSync, mkdirSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, delimiter, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
function executableInPath(command, env) {
|
|
7
|
+
for (const directory of (env.PATH ?? "").split(delimiter)) {
|
|
8
|
+
if (directory.trim() === "") {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const candidate = join(directory, command);
|
|
12
|
+
try {
|
|
13
|
+
accessSync(candidate, constants.X_OK);
|
|
14
|
+
return candidate;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
// Keep searching PATH.
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
export function detectLocalFolderPicker(platform = process.platform, env = process.env) {
|
|
23
|
+
if (platform === "darwin") {
|
|
24
|
+
return { kind: "macos", executable: "/usr/bin/osascript" };
|
|
25
|
+
}
|
|
26
|
+
if (platform !== "linux" || (env.DISPLAY === undefined && env.WAYLAND_DISPLAY === undefined)) {
|
|
27
|
+
return { kind: "unavailable" };
|
|
28
|
+
}
|
|
29
|
+
const zenity = executableInPath("zenity", env);
|
|
30
|
+
if (zenity !== null) {
|
|
31
|
+
return { kind: "zenity", executable: zenity };
|
|
32
|
+
}
|
|
33
|
+
const kdialog = executableInPath("kdialog", env);
|
|
34
|
+
if (kdialog !== null) {
|
|
35
|
+
return { kind: "kdialog", executable: kdialog };
|
|
36
|
+
}
|
|
37
|
+
return { kind: "unavailable" };
|
|
38
|
+
}
|
|
39
|
+
function normalizeSelectedPath(stdout) {
|
|
40
|
+
const value = stdout.trim();
|
|
41
|
+
if (value === "") {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const selected = resolve(value);
|
|
45
|
+
if (!existsSync(selected) || !statSync(selected).isDirectory()) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
return selected;
|
|
49
|
+
}
|
|
50
|
+
export async function pickLocalFolder(options = {}) {
|
|
51
|
+
const prompt = options.prompt ?? "Choose a Tutti project folder";
|
|
52
|
+
const picker = detectLocalFolderPicker(options.platform, options.env);
|
|
53
|
+
if (picker.kind === "unavailable" || picker.executable === undefined) {
|
|
54
|
+
return {
|
|
55
|
+
kind: "unavailable",
|
|
56
|
+
message: "No desktop folder picker is available. Enter an absolute folder path instead.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const args = picker.kind === "macos"
|
|
60
|
+
? ["-e", `POSIX path of (choose folder with prompt ${JSON.stringify(prompt)})`]
|
|
61
|
+
: picker.kind === "zenity"
|
|
62
|
+
? ["--file-selection", "--directory", `--title=${prompt}`]
|
|
63
|
+
: ["--getexistingdirectory", resolve(process.cwd()), "--title", prompt];
|
|
64
|
+
try {
|
|
65
|
+
const result = await execFileAsync(picker.executable, args, {
|
|
66
|
+
encoding: "utf8",
|
|
67
|
+
env: options.env ?? process.env,
|
|
68
|
+
maxBuffer: 16 * 1024,
|
|
69
|
+
timeout: 120_000,
|
|
70
|
+
});
|
|
71
|
+
const selected = normalizeSelectedPath(result.stdout);
|
|
72
|
+
return selected === null ? { kind: "cancelled" } : { kind: "selected", path: selected };
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return { kind: "cancelled" };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function validateChildName(value) {
|
|
79
|
+
const name = value.trim();
|
|
80
|
+
const hasControlCharacter = [...name].some((character) => {
|
|
81
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
82
|
+
return codePoint <= 0x1f || codePoint === 0x7f;
|
|
83
|
+
});
|
|
84
|
+
if (name === "" ||
|
|
85
|
+
name === "." ||
|
|
86
|
+
name === ".." ||
|
|
87
|
+
name !== basename(name) ||
|
|
88
|
+
hasControlCharacter) {
|
|
89
|
+
throw new Error("Project folder name must be a single valid directory name.");
|
|
90
|
+
}
|
|
91
|
+
return name;
|
|
92
|
+
}
|
|
93
|
+
export function createLocalProjectFolder(parentPath, childName) {
|
|
94
|
+
const parent = resolve(parentPath.trim());
|
|
95
|
+
if (!existsSync(parent) || !statSync(parent).isDirectory()) {
|
|
96
|
+
throw new Error("Parent folder does not exist or is not a directory.");
|
|
97
|
+
}
|
|
98
|
+
accessSync(parent, constants.W_OK);
|
|
99
|
+
const child = resolve(parent, validateChildName(childName));
|
|
100
|
+
if (dirname(child) !== parent) {
|
|
101
|
+
throw new Error("Project folder must be created directly inside the selected parent.");
|
|
102
|
+
}
|
|
103
|
+
if (existsSync(child)) {
|
|
104
|
+
throw new Error("A file or folder with that name already exists.");
|
|
105
|
+
}
|
|
106
|
+
mkdirSync(child, { mode: 0o755 });
|
|
107
|
+
return child;
|
|
108
|
+
}
|
|
109
|
+
//# sourceMappingURL=folder-picker.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type LocalConsoleRuntimeEndpoint } from "./runtime-endpoint.js";
|
|
2
|
+
export declare function ensureLocalConsole(options?: {
|
|
3
|
+
cwd?: string;
|
|
4
|
+
env?: NodeJS.ProcessEnv;
|
|
5
|
+
}): Promise<{
|
|
6
|
+
url: string;
|
|
7
|
+
endpoint: LocalConsoleRuntimeEndpoint;
|
|
8
|
+
}>;
|
|
9
|
+
//# sourceMappingURL=managed-console.d.ts.map
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { resolveTuttiHome } from "../../providers/openai/index.js";
|
|
4
|
+
import { deleteLocalConsoleRuntimeEndpoint, readLocalConsoleRuntimeEndpoint, } from "./runtime-endpoint.js";
|
|
5
|
+
import { LOCAL_CONSOLE_API_BASE } from "./server.js";
|
|
6
|
+
const READY_TIMEOUT_MS = 15_000;
|
|
7
|
+
const READY_POLL_MS = 150;
|
|
8
|
+
function delay(milliseconds) {
|
|
9
|
+
return new Promise((resolveDelay) => setTimeout(resolveDelay, milliseconds));
|
|
10
|
+
}
|
|
11
|
+
function cliEntrypoint() {
|
|
12
|
+
const entrypoint = process.argv[1];
|
|
13
|
+
if (entrypoint === undefined || entrypoint.trim() === "") {
|
|
14
|
+
throw new Error("Cannot locate the Tutti CLI entrypoint.");
|
|
15
|
+
}
|
|
16
|
+
return entrypoint;
|
|
17
|
+
}
|
|
18
|
+
async function endpointReady(endpoint) {
|
|
19
|
+
try {
|
|
20
|
+
const response = await fetch(`${endpoint.base_url}${LOCAL_CONSOLE_API_BASE}/health`, {
|
|
21
|
+
signal: AbortSignal.timeout(1_000),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const value = (await response.json());
|
|
27
|
+
return value.status === "ready";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function spawnDetachedConsole(options) {
|
|
34
|
+
const child = spawn(process.execPath, [...process.execArgv, cliEntrypoint(), "internal", "console-run"], {
|
|
35
|
+
cwd: options.cwd,
|
|
36
|
+
detached: true,
|
|
37
|
+
env: options.env,
|
|
38
|
+
stdio: "ignore",
|
|
39
|
+
});
|
|
40
|
+
child.unref();
|
|
41
|
+
}
|
|
42
|
+
async function waitForConsoleEndpoint(tuttiHome) {
|
|
43
|
+
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
44
|
+
while (Date.now() < deadline) {
|
|
45
|
+
const endpoint = readLocalConsoleRuntimeEndpoint(tuttiHome);
|
|
46
|
+
if (endpoint !== null && (await endpointReady(endpoint))) {
|
|
47
|
+
return endpoint;
|
|
48
|
+
}
|
|
49
|
+
await delay(READY_POLL_MS);
|
|
50
|
+
}
|
|
51
|
+
throw new Error("Tutti Local Console did not become ready. Run `tutti doctor` for diagnostics.");
|
|
52
|
+
}
|
|
53
|
+
async function ensureConsoleEndpoint(options) {
|
|
54
|
+
const existing = readLocalConsoleRuntimeEndpoint(options.tuttiHome);
|
|
55
|
+
if (existing !== null && (await endpointReady(existing))) {
|
|
56
|
+
return existing;
|
|
57
|
+
}
|
|
58
|
+
if (existing !== null) {
|
|
59
|
+
deleteLocalConsoleRuntimeEndpoint({
|
|
60
|
+
tuttiHome: options.tuttiHome,
|
|
61
|
+
expectedControlToken: existing.control_token,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
spawnDetachedConsole({ cwd: options.cwd, env: options.env });
|
|
65
|
+
return await waitForConsoleEndpoint(options.tuttiHome);
|
|
66
|
+
}
|
|
67
|
+
async function issueBrowserAccess(endpoint, currentDirectory) {
|
|
68
|
+
const response = await fetch(`${endpoint.base_url}${LOCAL_CONSOLE_API_BASE}/access-tokens`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
authorization: `Bearer ${endpoint.control_token}`,
|
|
72
|
+
accept: "application/json",
|
|
73
|
+
"content-type": "application/json",
|
|
74
|
+
},
|
|
75
|
+
body: JSON.stringify({ current_directory: currentDirectory }),
|
|
76
|
+
signal: AbortSignal.timeout(3_000),
|
|
77
|
+
});
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error("Tutti Local Console could not create a browser access link.");
|
|
80
|
+
}
|
|
81
|
+
const value = (await response.json());
|
|
82
|
+
if (typeof value.access_token !== "string" || value.access_token.trim() === "") {
|
|
83
|
+
throw new Error("Tutti Local Console returned an invalid browser access link.");
|
|
84
|
+
}
|
|
85
|
+
return `${endpoint.base_url}/console#access_token=${encodeURIComponent(value.access_token)}`;
|
|
86
|
+
}
|
|
87
|
+
export async function ensureLocalConsole(options = {}) {
|
|
88
|
+
const cwd = resolve(options.cwd ?? process.cwd());
|
|
89
|
+
const env = options.env ?? process.env;
|
|
90
|
+
const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
|
|
91
|
+
const endpoint = await ensureConsoleEndpoint({ cwd, env, tuttiHome });
|
|
92
|
+
return { endpoint, url: await issueBrowserAccess(endpoint, cwd) };
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=managed-console.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { ProjectId } from "@tutti/shared/ids";
|
|
2
|
+
import { type RuntimeProjectRow } from "../cli/runtime-commands.js";
|
|
3
|
+
export type LocalConsoleProject = {
|
|
4
|
+
project_id: ProjectId;
|
|
5
|
+
display_name: string;
|
|
6
|
+
workspace_path: string;
|
|
7
|
+
status: RuntimeProjectRow["status"];
|
|
8
|
+
provider_status: "configured" | "not_configured" | "invalid";
|
|
9
|
+
relay_project_ref?: string;
|
|
10
|
+
join_url?: string;
|
|
11
|
+
relay_connection_status?: string;
|
|
12
|
+
};
|
|
13
|
+
export type LocalConsoleProviderInput = {
|
|
14
|
+
base_url: string;
|
|
15
|
+
api_key: string;
|
|
16
|
+
model: string;
|
|
17
|
+
};
|
|
18
|
+
export type LocalConsoleLaunchResult = {
|
|
19
|
+
project: LocalConsoleProject;
|
|
20
|
+
join_url: string;
|
|
21
|
+
join_token_expires_at?: string;
|
|
22
|
+
};
|
|
23
|
+
export declare class LocalConsoleProjectError extends Error {
|
|
24
|
+
readonly code: string;
|
|
25
|
+
readonly statusCode: number;
|
|
26
|
+
constructor(code: string, message: string, statusCode?: number);
|
|
27
|
+
}
|
|
28
|
+
export declare class LocalConsoleProjectService {
|
|
29
|
+
#private;
|
|
30
|
+
constructor(options?: {
|
|
31
|
+
cwd?: string;
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
});
|
|
34
|
+
listProjects(): Promise<LocalConsoleProject[]>;
|
|
35
|
+
discoverModels(input: {
|
|
36
|
+
baseUrl: string;
|
|
37
|
+
apiKey: string;
|
|
38
|
+
}): Promise<{
|
|
39
|
+
kind: "ok";
|
|
40
|
+
models: string[];
|
|
41
|
+
} | {
|
|
42
|
+
kind: "unavailable";
|
|
43
|
+
reason: string;
|
|
44
|
+
}>;
|
|
45
|
+
launchProject(input: {
|
|
46
|
+
workspacePath: string;
|
|
47
|
+
provider?: LocalConsoleProviderInput;
|
|
48
|
+
}): Promise<LocalConsoleLaunchResult>;
|
|
49
|
+
refreshInvite(projectId: string): Promise<{
|
|
50
|
+
join_url: string;
|
|
51
|
+
expires_at?: string;
|
|
52
|
+
}>;
|
|
53
|
+
stopProject(projectId: string): Promise<{
|
|
54
|
+
message: string;
|
|
55
|
+
}>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=project-service.d.ts.map
|