@xfey/tutti 0.1.61 → 0.1.63
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 +2 -2
- package/dist/server-shell/cli/args.d.ts +4 -0
- package/dist/server-shell/cli/args.js +11 -0
- package/dist/server-shell/cli/cli.js +35 -11
- package/dist/server-shell/cli/host-server-runtime.d.ts +2 -0
- package/dist/server-shell/cli/host-server-runtime.js +12 -5
- package/dist/server-shell/cli/local-control-client.d.ts +5 -1
- package/dist/server-shell/cli/local-control-client.js +19 -0
- package/dist/server-shell/desktop-integration/manager.d.ts +2 -0
- package/dist/server-shell/desktop-integration/manager.js +19 -31
- package/dist/server-shell/http/routes/local-control.d.ts +2 -0
- package/dist/server-shell/http/routes/local-control.js +24 -0
- package/dist/server-shell/local-console/invocation-context.js +10 -1
- package/dist/server-shell/local-console/operation-coordinator.d.ts +16 -0
- package/dist/server-shell/local-console/operation-coordinator.js +50 -0
- package/dist/server-shell/local-console/package-update-completion.d.ts +25 -0
- package/dist/server-shell/local-console/package-update-completion.js +91 -0
- package/dist/server-shell/local-console/package-update-hosts.d.ts +17 -0
- package/dist/server-shell/local-console/package-update-hosts.js +92 -0
- package/dist/server-shell/local-console/package-update-lock.d.ts +46 -0
- package/dist/server-shell/local-console/package-update-lock.js +223 -0
- package/dist/server-shell/local-console/package-update-log.d.ts +20 -0
- package/dist/server-shell/local-console/package-update-log.js +75 -0
- package/dist/server-shell/local-console/package-update-process.d.ts +22 -0
- package/dist/server-shell/local-console/package-update-process.js +252 -0
- package/dist/server-shell/local-console/package-update-record.d.ts +53 -0
- package/dist/server-shell/local-console/package-update-record.js +180 -0
- package/dist/server-shell/local-console/package-update-recovery.d.ts +34 -0
- package/dist/server-shell/local-console/package-update-recovery.js +140 -0
- package/dist/server-shell/local-console/package-update-service.d.ts +72 -0
- package/dist/server-shell/local-console/package-update-service.js +400 -0
- package/dist/server-shell/local-console/package-update-worker.d.ts +55 -0
- package/dist/server-shell/local-console/package-update-worker.js +297 -0
- package/dist/server-shell/local-console/project-service.d.ts +2 -0
- package/dist/server-shell/local-console/project-service.js +16 -12
- package/dist/server-shell/local-console/server.d.ts +9 -0
- package/dist/server-shell/local-console/server.js +92 -4
- package/dist/server-shell/package-installation/command.d.ts +36 -0
- package/dist/server-shell/package-installation/command.js +162 -0
- package/dist/server-shell/package-installation/discovery.d.ts +51 -0
- package/dist/server-shell/package-installation/discovery.js +137 -0
- package/dist/server-shell/package-installation/index.d.ts +5 -0
- package/dist/server-shell/package-installation/index.js +5 -0
- package/dist/server-shell/package-installation/installation.d.ts +34 -0
- package/dist/server-shell/package-installation/installation.js +123 -0
- package/dist/server-shell/package-installation/semver.d.ts +9 -0
- package/dist/server-shell/package-installation/semver.js +84 -0
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-CY7o4hnR.js → homepage-motion-scene-CRmrkkEH.js} +1 -1
- package/web/assets/{index-B08r3x8o.js → index-C9JrplHV.js} +14 -14
- package/web/assets/{index-DpabiRgp.css → index-IZKcmU_g.css} +1 -1
- package/web/index.html +2 -2
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
它负责:
|
|
8
8
|
|
|
9
9
|
- `tutti launch` 后的项目绑定、恢复和接管入口
|
|
10
|
-
- 裸 `tutti` 经 lifecycle lock 启动 / 复用的版本感知 loopback Local Console、独立 invocation context
|
|
10
|
+
- 裸 `tutti` 经 lifecycle lock 启动 / 复用的版本感知 loopback Local Console、独立 invocation context、既有项目 Host 编排,以及可信全局npm安装的idle-only worker / completion自更新交接
|
|
11
11
|
- 首次 eligible 裸 `tutti`、显式 `tutti desktop` 与严格 `tutti://projects` bridge 共用的用户级 desktop integration manager
|
|
12
12
|
- Fastify API、SSE、packaged static Web 产物服务和 server 生命周期
|
|
13
13
|
- SQLite store、migrations、repository 与协作真相读写
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
## 当前状态
|
|
30
30
|
|
|
31
|
-
- 已具备 Fastify host shell、后台 host manager CLI 套件、用户级 desktop integration / strict `tutti://projects` bridge、带 `service status / stop / restart`、卸载前 `shutdown`、并发启动锁、版本替换、strict terminal / desktop browser context、idle / package update cleanup 与私有日志的机器级 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 接入。
|
|
31
|
+
- 已具备 Fastify host shell、后台 host manager CLI 套件、用户级 desktop integration / strict `tutti://projects` bridge、带 `service status / stop / restart`、卸载前 `shutdown`、并发启动锁、版本替换、strict terminal / desktop browser context、idle / package update cleanup、全机readiness、exact npm global install、跨版本worker / recovery与私有日志的机器级 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 接入。
|
|
32
32
|
- 新 Host project 的空群聊会由 collaboration-state 幂等写入一条 Tutti 欢迎消息;它不经过 browser message command,不触发 `@tutti`、Chat read-state、Scratchpad 或 Timeline / execution 流程。
|
|
33
33
|
- Host server 与 Local Console 支持在显式 `webStatic.root` 或 packaged `web/` 存在时服务 Web 静态产物;`/api/*`、SSE、health、host-local control 和 `/local-console/*` API 不被 SPA fallback 吞掉。
|
|
34
34
|
- 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` 与测试产物。
|
|
@@ -138,6 +138,17 @@ export function parseCliArgs(argv, cwd = process.cwd()) {
|
|
|
138
138
|
}
|
|
139
139
|
if (command === "internal") {
|
|
140
140
|
const internalCommand = args.shift();
|
|
141
|
+
if (internalCommand === "package-update-worker" ||
|
|
142
|
+
internalCommand === "package-update-complete") {
|
|
143
|
+
if (args.length > 0) {
|
|
144
|
+
throw new LaunchError("git_bootstrap_failed", `Internal ${internalCommand} does not accept options`, "Internal commands are managed by Tutti.");
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
kind: internalCommand === "package-update-worker"
|
|
148
|
+
? "internal-package-update-worker"
|
|
149
|
+
: "internal-package-update-complete",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
141
152
|
if (internalCommand === "console-run") {
|
|
142
153
|
if (args.length > 0) {
|
|
143
154
|
throw new LaunchError("git_bootstrap_failed", "Internal console-run does not accept options", "Internal commands are managed by Tutti.");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { resolveTuttiHome } from "../../providers/openai/index.js";
|
|
3
4
|
import { formatCliError, formatCliErrorReason, LaunchError } from "./errors.js";
|
|
4
5
|
import { parseCliArgs, shouldEnsureDesktopIntegrationAutomatically } from "./args.js";
|
|
5
6
|
import { confirmTextFromTty, runBackgroundLaunchCommand, runForegroundLaunchCommand, runInternalHostRunCommand, } from "./launch-command.js";
|
|
@@ -9,6 +10,8 @@ import { runArchiveCommand, runDoctorCommand, runForgetCommand, runInviteCommand
|
|
|
9
10
|
import { readCliVersion } from "./version.js";
|
|
10
11
|
import { ensureDesktopIntegrationAutomatically, parseTuttiDesktopUrl, runDesktopIntegrationCommand, TUTTI_DESKTOP_PROJECTS_URL, TuttiDesktopProtocolError, } from "../desktop-integration/index.js";
|
|
11
12
|
import { ensureLocalConsole, openLocalConsoleBrowser, readLocalConsoleServiceStatus, restartLocalConsoleService, runLocalConsoleProcess, stopLocalConsoleService, } from "../local-console/index.js";
|
|
13
|
+
import { runPackageUpdateCompletionProtocol, runPackageUpdateWorkerProtocol, } from "../local-console/package-update-process.js";
|
|
14
|
+
import { recoverPackageUpdateForConsoleStartup } from "../local-console/package-update-recovery.js";
|
|
12
15
|
const HELP_TEXT = `Usage:
|
|
13
16
|
tutti
|
|
14
17
|
tutti launch [target] [--yes] [--foreground]
|
|
@@ -209,13 +212,30 @@ function ensureDesktopIntegrationForConsole() {
|
|
|
209
212
|
process.stderr.write("Tutti could not update its desktop entry. Run `tutti desktop` for details.\n");
|
|
210
213
|
}
|
|
211
214
|
}
|
|
215
|
+
async function ensureUserLocalConsole(options) {
|
|
216
|
+
const env = process.env;
|
|
217
|
+
const entrypointArgument = process.argv[1];
|
|
218
|
+
if (entrypointArgument !== undefined && entrypointArgument.trim() !== "") {
|
|
219
|
+
const recovery = await recoverPackageUpdateForConsoleStartup({
|
|
220
|
+
tuttiHome: resolveTuttiHome(env.TUTTI_HOME, options.cwd),
|
|
221
|
+
cliEntrypoint: resolve(entrypointArgument),
|
|
222
|
+
cwd: options.cwd,
|
|
223
|
+
env,
|
|
224
|
+
source: options.source,
|
|
225
|
+
});
|
|
226
|
+
if (recovery.status === "completed") {
|
|
227
|
+
return recovery.url;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return (await ensureLocalConsole({ cwd: options.cwd, env, source: options.source })).url;
|
|
231
|
+
}
|
|
212
232
|
async function runLocalConsoleCommand() {
|
|
213
|
-
const
|
|
214
|
-
if (openLocalConsoleBrowser(
|
|
215
|
-
process.stdout.write(`Tutti is ready at ${
|
|
233
|
+
const url = await ensureUserLocalConsole({ cwd: process.cwd(), source: "terminal" });
|
|
234
|
+
if (openLocalConsoleBrowser(url)) {
|
|
235
|
+
process.stdout.write(`Tutti is ready at ${new URL(url).origin}/console\n`);
|
|
216
236
|
return;
|
|
217
237
|
}
|
|
218
|
-
process.stdout.write(["Open Tutti in a browser:",
|
|
238
|
+
process.stdout.write(["Open Tutti in a browser:", url, ""].join("\n"));
|
|
219
239
|
}
|
|
220
240
|
function desktopResultMessage(result) {
|
|
221
241
|
if (result.status === "installed") {
|
|
@@ -239,9 +259,9 @@ function runDesktopCommand(action) {
|
|
|
239
259
|
return desktopResultMessage(runDesktopIntegrationCommand(action));
|
|
240
260
|
}
|
|
241
261
|
async function runDesktopOpenCommand(value) {
|
|
242
|
-
const
|
|
262
|
+
const desktopUrl = value ?? TUTTI_DESKTOP_PROJECTS_URL;
|
|
243
263
|
try {
|
|
244
|
-
parseTuttiDesktopUrl(
|
|
264
|
+
parseTuttiDesktopUrl(desktopUrl);
|
|
245
265
|
}
|
|
246
266
|
catch (error) {
|
|
247
267
|
if (error instanceof TuttiDesktopProtocolError) {
|
|
@@ -250,11 +270,9 @@ async function runDesktopOpenCommand(value) {
|
|
|
250
270
|
throw error;
|
|
251
271
|
}
|
|
252
272
|
const cliEntrypoint = process.argv[1];
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
});
|
|
257
|
-
if (!openLocalConsoleBrowser(console.url)) {
|
|
273
|
+
const cwd = cliEntrypoint === undefined ? process.cwd() : dirname(resolve(cliEntrypoint));
|
|
274
|
+
const url = await ensureUserLocalConsole({ source: "desktop", cwd });
|
|
275
|
+
if (!openLocalConsoleBrowser(url)) {
|
|
258
276
|
throw new LaunchError("desktop_open_failed", "The Local Console started, but no desktop browser opener is available.", "Run `tutti` from a terminal to print a one-time browser URL.");
|
|
259
277
|
}
|
|
260
278
|
}
|
|
@@ -378,6 +396,12 @@ try {
|
|
|
378
396
|
case "internal-desktop-open":
|
|
379
397
|
await runDesktopOpenCommand(command.url);
|
|
380
398
|
break;
|
|
399
|
+
case "internal-package-update-worker":
|
|
400
|
+
await runPackageUpdateWorkerProtocol();
|
|
401
|
+
break;
|
|
402
|
+
case "internal-package-update-complete":
|
|
403
|
+
await runPackageUpdateCompletionProtocol();
|
|
404
|
+
break;
|
|
381
405
|
case "ps":
|
|
382
406
|
if (command.manage) {
|
|
383
407
|
await runPsManageCommand({ all: command.all });
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import type { ExecutionStatusProjection } from "@tutti/shared/schemas/api";
|
|
2
3
|
import { type HostProjectStore } from "../../store/index.js";
|
|
3
4
|
import type { createHostServer } from "../http/create-server.js";
|
|
4
5
|
import type { HostLocalLaunchStatus } from "../http/routes/local-control.js";
|
|
@@ -8,6 +9,7 @@ import type { HostRelayConnectionManager } from "./host-relay-connection-manager
|
|
|
8
9
|
import type { LaunchPreparationResult } from "./launch.js";
|
|
9
10
|
import { type MachineRuntimeEndpointRecord } from "./machine-local.js";
|
|
10
11
|
export declare const HOST_SERVER_VERSION: string;
|
|
12
|
+
export declare function packageUpdateReadinessFromExecutionStatus(execution: Pick<ExecutionStatusProjection, "status" | "active_activity" | "active_activities" | "blocked_by">): "ready" | "busy";
|
|
11
13
|
export type HostServerFactory = typeof createHostServer;
|
|
12
14
|
export type HostServerHandle = {
|
|
13
15
|
app: FastifyInstance;
|
|
@@ -23,6 +23,15 @@ import { relayStateFromLaunchStatus } from "./host-relay-status.js";
|
|
|
23
23
|
import { appendHostLogLine, readHostRegistrationSecret, createMachineRuntimeEndpointToken, deleteMachineRuntimeEndpoint, ensureHostLogFile, getHostLogFilePath, updateMachineProjectDisplayNameSnapshot, writeMachineRuntimeEndpoint, } from "./machine-local.js";
|
|
24
24
|
import { readProjectDescription, readProjectDisplayName, writeProjectDescription, writeProjectDisplayName, } from "./project-identity.js";
|
|
25
25
|
export const HOST_SERVER_VERSION = readCliVersion();
|
|
26
|
+
export function packageUpdateReadinessFromExecutionStatus(execution) {
|
|
27
|
+
const active = execution.active_activity !== undefined || (execution.active_activities?.length ?? 0) > 0;
|
|
28
|
+
return active ||
|
|
29
|
+
execution.blocked_by !== undefined ||
|
|
30
|
+
execution.status === "running" ||
|
|
31
|
+
execution.status === "pending"
|
|
32
|
+
? "busy"
|
|
33
|
+
: "ready";
|
|
34
|
+
}
|
|
26
35
|
function createHostRuntimeLogger(options) {
|
|
27
36
|
return {
|
|
28
37
|
info(fields, message) {
|
|
@@ -279,13 +288,11 @@ export async function startForegroundHostServer(options) {
|
|
|
279
288
|
});
|
|
280
289
|
let close = async () => { };
|
|
281
290
|
let relayConnectionManager;
|
|
291
|
+
const packageUpdateReadiness = () => packageUpdateReadinessFromExecutionStatus(controlPlane.getExecutionStatus());
|
|
282
292
|
const localControl = {
|
|
283
293
|
token,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
const hasActiveActivities = execution.active_activity !== undefined || (execution.active_activities?.length ?? 0) > 0;
|
|
287
|
-
return hasActiveActivities ? "busy" : "accepted";
|
|
288
|
-
},
|
|
294
|
+
getPackageUpdateReadiness: packageUpdateReadiness,
|
|
295
|
+
requestIdleShutdown: () => packageUpdateReadiness() === "ready" ? "accepted" : "busy",
|
|
289
296
|
requestShutdown: () => {
|
|
290
297
|
void close();
|
|
291
298
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { HostLocalIdleShutdownDisposition, HostLocalLaunchStatus, HostLocalProjectProjection, HostLocalProviderConfigBody } from "../http/routes/local-control.js";
|
|
1
|
+
import type { HostLocalIdleShutdownDisposition, HostLocalLaunchStatus, HostLocalPackageUpdateReadiness, HostLocalProjectProjection, HostLocalProviderConfigBody } from "../http/routes/local-control.js";
|
|
2
2
|
import type { HostProviderConfigProjection } from "../http/routes/project-api/types.js";
|
|
3
3
|
import type { MachineRuntimeEndpointRecord } from "./machine-local.js";
|
|
4
4
|
export type LocalControlFetch = (input: string | URL, init?: RequestInit) => Promise<Response>;
|
|
@@ -6,6 +6,10 @@ export declare function readHostLocalLaunchStatus(options: {
|
|
|
6
6
|
endpoint: MachineRuntimeEndpointRecord;
|
|
7
7
|
fetchImpl?: LocalControlFetch;
|
|
8
8
|
}): Promise<HostLocalLaunchStatus>;
|
|
9
|
+
export declare function readHostLocalPackageUpdateReadiness(options: {
|
|
10
|
+
endpoint: MachineRuntimeEndpointRecord;
|
|
11
|
+
fetchImpl?: LocalControlFetch;
|
|
12
|
+
}): Promise<HostLocalPackageUpdateReadiness>;
|
|
9
13
|
export declare function rotateHostLocalInvite(options: {
|
|
10
14
|
endpoint: MachineRuntimeEndpointRecord;
|
|
11
15
|
fetchImpl?: LocalControlFetch;
|
|
@@ -35,6 +35,25 @@ export async function readHostLocalLaunchStatus(options) {
|
|
|
35
35
|
path: "/host-local/v1/launch-status",
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
export async function readHostLocalPackageUpdateReadiness(options) {
|
|
39
|
+
const response = await fetchWithTimeout(options.fetchImpl ?? fetch, new URL("/host-local/v1/package-update-readiness", options.endpoint.base_url), {
|
|
40
|
+
headers: {
|
|
41
|
+
accept: "application/json",
|
|
42
|
+
authorization: `Bearer ${options.endpoint.token}`,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
if (response.status === 404) {
|
|
46
|
+
return "unsupported";
|
|
47
|
+
}
|
|
48
|
+
if (!response.ok) {
|
|
49
|
+
throw new Error(`host-local package-update-readiness returned HTTP ${response.status}`);
|
|
50
|
+
}
|
|
51
|
+
const body = (await response.json());
|
|
52
|
+
if (body.status !== "ready" && body.status !== "busy" && body.status !== "unsupported") {
|
|
53
|
+
throw new Error("host-local package-update-readiness returned an invalid response");
|
|
54
|
+
}
|
|
55
|
+
return body.status;
|
|
56
|
+
}
|
|
38
57
|
export async function rotateHostLocalInvite(options) {
|
|
39
58
|
return await requestLocalJson({
|
|
40
59
|
...options,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type SyncPackageCommandRunner } from "../package-installation/index.js";
|
|
1
2
|
import { type DesktopCommandRunner, type DesktopInstallationIdentity, type DesktopPlatformAdapter } from "./platform.js";
|
|
2
3
|
export type DesktopIntegrationResult = {
|
|
3
4
|
status: "installed";
|
|
@@ -32,6 +33,7 @@ declare function createRealContext(options: {
|
|
|
32
33
|
platform?: NodeJS.Platform;
|
|
33
34
|
now?: () => Date;
|
|
34
35
|
runner?: DesktopCommandRunner;
|
|
36
|
+
packageRunner?: SyncPackageCommandRunner;
|
|
35
37
|
requireStableGlobal?: boolean;
|
|
36
38
|
}): ManagerContext | DesktopIntegrationResult;
|
|
37
39
|
export declare function ensureDesktopIntegrationAutomatically(options?: Parameters<typeof createRealContext>[0]): DesktopIntegrationResult;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { existsSync, realpathSync } from "node:fs";
|
|
2
1
|
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, isAbsolute,
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
4
3
|
import { fileURLToPath } from "node:url";
|
|
5
4
|
import { resolveTuttiHome } from "../../providers/openai/index.js";
|
|
6
5
|
import { readCliPackageIdentity } from "../cli/version.js";
|
|
7
|
-
import {
|
|
6
|
+
import { resolveGlobalPackageInstallation, } from "../package-installation/index.js";
|
|
7
|
+
import { createLinuxDesktopAdapter, linuxDesktopRequirementsAvailable } from "./linux.js";
|
|
8
8
|
import { createMacosDesktopAdapter, macosDesktopRequirementsAvailable } from "./macos.js";
|
|
9
9
|
import { runDesktopCommand, } from "./platform.js";
|
|
10
10
|
import { readDesktopIntegrationRecord, writeDesktopIntegrationRecord, } from "./record.js";
|
|
11
|
-
const PUBLISHED_CLI_PACKAGE_NAME = "@xfey/tutti";
|
|
12
11
|
function desktopAssetRoot() {
|
|
13
12
|
return resolve(dirname(fileURLToPath(import.meta.url)), "../../../desktop-assets");
|
|
14
13
|
}
|
|
@@ -136,39 +135,28 @@ export function removeDesktopIntegration(context) {
|
|
|
136
135
|
return unavailable("operation_failed", "Tutti could not remove the user-level desktop entry.", context.adapter.entryPath);
|
|
137
136
|
}
|
|
138
137
|
}
|
|
139
|
-
function stableGlobalPackage(options) {
|
|
140
|
-
const npmExecutable = findDesktopExecutable("npm", options.env);
|
|
141
|
-
if (npmExecutable === null) {
|
|
142
|
-
return false;
|
|
143
|
-
}
|
|
144
|
-
const result = options.runner(npmExecutable, ["root", "--global"]);
|
|
145
|
-
const globalRoot = result.stdout.trim();
|
|
146
|
-
if (result.status !== 0 || !isAbsolute(globalRoot)) {
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
const expectedRoot = join(globalRoot, ...PUBLISHED_CLI_PACKAGE_NAME.split("/"));
|
|
150
|
-
if (!existsSync(expectedRoot)) {
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
try {
|
|
154
|
-
return realpathSync(expectedRoot) === realpathSync(options.packageRoot);
|
|
155
|
-
}
|
|
156
|
-
catch {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
138
|
function createRealContext(options) {
|
|
161
139
|
const cwd = resolve(options.cwd ?? process.cwd());
|
|
162
140
|
const env = options.env ?? process.env;
|
|
163
141
|
const platform = options.platform ?? process.platform;
|
|
164
142
|
const runner = options.runner ?? runDesktopCommand;
|
|
165
143
|
const packageIdentity = readCliPackageIdentity();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
144
|
+
const tuttiHome = resolveTuttiHome(env.TUTTI_HOME, cwd);
|
|
145
|
+
if (options.requireStableGlobal !== false) {
|
|
146
|
+
const packageInstallation = resolveGlobalPackageInstallation({
|
|
147
|
+
packageIdentity,
|
|
148
|
+
tuttiHome,
|
|
149
|
+
defaultTuttiHome: resolveTuttiHome(undefined, cwd),
|
|
150
|
+
env,
|
|
151
|
+
...(options.packageRunner === undefined ? {} : { runner: options.packageRunner }),
|
|
152
|
+
});
|
|
153
|
+
if (packageInstallation.status === "unavailable") {
|
|
154
|
+
return packageInstallation.reasonCode === "custom_home"
|
|
155
|
+
? unavailable("custom_tutti_home", "Desktop integration is available only for the default Tutti home.")
|
|
156
|
+
: unavailable("package_not_global", "Desktop integration requires a stable global @xfey/tutti installation.");
|
|
157
|
+
}
|
|
170
158
|
}
|
|
171
|
-
if (
|
|
159
|
+
else if (tuttiHome !== resolveTuttiHome(undefined, cwd)) {
|
|
172
160
|
return unavailable("custom_tutti_home", "Desktop integration is available only for the default Tutti home.");
|
|
173
161
|
}
|
|
174
162
|
if (platform !== "darwin" && platform !== "linux") {
|
|
@@ -188,7 +176,7 @@ function createRealContext(options) {
|
|
|
188
176
|
? createMacosDesktopAdapter({ homeDir, assetRoot, runner })
|
|
189
177
|
: createLinuxDesktopAdapter({ homeDir, assetRoot, env, runner });
|
|
190
178
|
return {
|
|
191
|
-
tuttiHome
|
|
179
|
+
tuttiHome,
|
|
192
180
|
adapter,
|
|
193
181
|
identity: {
|
|
194
182
|
packageVersion: packageIdentity.version,
|
|
@@ -14,10 +14,12 @@ export type HostLocalControlOptions = {
|
|
|
14
14
|
get: () => ProviderConfigProjection;
|
|
15
15
|
configure: (input: HostLocalProviderConfigBody) => Promise<ProviderConfigProjection>;
|
|
16
16
|
};
|
|
17
|
+
getPackageUpdateReadiness?: () => HostLocalPackageUpdateReadiness;
|
|
17
18
|
requestIdleShutdown?: () => HostLocalIdleShutdownDisposition;
|
|
18
19
|
requestShutdown: () => void;
|
|
19
20
|
};
|
|
20
21
|
export type HostLocalIdleShutdownDisposition = "accepted" | "busy" | "unsupported";
|
|
22
|
+
export type HostLocalPackageUpdateReadiness = "ready" | "busy" | "unsupported";
|
|
21
23
|
export type HostLocalLaunchStatus = {
|
|
22
24
|
project_id?: ProjectId;
|
|
23
25
|
relay_connection?: {
|
|
@@ -55,6 +55,14 @@ const HostLocalIdleShutdownResponseSchema = {
|
|
|
55
55
|
disposition: { type: "string", enum: ["accepted", "busy", "unsupported"] },
|
|
56
56
|
},
|
|
57
57
|
};
|
|
58
|
+
const HostLocalPackageUpdateReadinessResponseSchema = {
|
|
59
|
+
type: "object",
|
|
60
|
+
required: ["status"],
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
properties: {
|
|
63
|
+
status: { type: "string", enum: ["ready", "busy", "unsupported"] },
|
|
64
|
+
},
|
|
65
|
+
};
|
|
58
66
|
const HostLocalProviderConfigBodySchema = {
|
|
59
67
|
type: "object",
|
|
60
68
|
required: ["api_key"],
|
|
@@ -252,6 +260,12 @@ export function registerHostLocalControlRoutes(app, options) {
|
|
|
252
260
|
throw new HostLocalControlUnauthorizedError();
|
|
253
261
|
}
|
|
254
262
|
}
|
|
263
|
+
function packageUpdateReadiness() {
|
|
264
|
+
if (draining || activeMutations > 0) {
|
|
265
|
+
return "busy";
|
|
266
|
+
}
|
|
267
|
+
return options.getPackageUpdateReadiness?.() ?? "unsupported";
|
|
268
|
+
}
|
|
255
269
|
app.get("/host-local/v1/launch-status", {
|
|
256
270
|
schema: {
|
|
257
271
|
response: {
|
|
@@ -262,6 +276,16 @@ export function registerHostLocalControlRoutes(app, options) {
|
|
|
262
276
|
requireLocalToken(request.headers.authorization);
|
|
263
277
|
return options.getLaunchStatus?.() ?? {};
|
|
264
278
|
});
|
|
279
|
+
app.get("/host-local/v1/package-update-readiness", {
|
|
280
|
+
schema: {
|
|
281
|
+
response: {
|
|
282
|
+
200: HostLocalPackageUpdateReadinessResponseSchema,
|
|
283
|
+
},
|
|
284
|
+
},
|
|
285
|
+
}, (request) => {
|
|
286
|
+
requireLocalToken(request.headers.authorization);
|
|
287
|
+
return { status: packageUpdateReadiness() };
|
|
288
|
+
});
|
|
265
289
|
app.post("/host-local/v1/relay/reconnect", {
|
|
266
290
|
schema: {
|
|
267
291
|
response: {
|
|
@@ -37,7 +37,16 @@ export function createDesktopLocalConsoleInvocationContext(options) {
|
|
|
37
37
|
}
|
|
38
38
|
export function createLocalConsoleServiceEnvironment(options) {
|
|
39
39
|
const environment = { TUTTI_HOME: options.tuttiHome };
|
|
40
|
-
for (const key of [
|
|
40
|
+
for (const key of [
|
|
41
|
+
"HOME",
|
|
42
|
+
"LANG",
|
|
43
|
+
"LC_ALL",
|
|
44
|
+
"LC_CTYPE",
|
|
45
|
+
"LOGNAME",
|
|
46
|
+
"PATH",
|
|
47
|
+
"TMPDIR",
|
|
48
|
+
"USER",
|
|
49
|
+
]) {
|
|
41
50
|
const value = options.env[key];
|
|
42
51
|
if (value !== undefined && value !== "") {
|
|
43
52
|
environment[key] = value;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class LocalConsoleOperationConflictError extends Error {
|
|
2
|
+
readonly statusCode = 409;
|
|
3
|
+
readonly code = "package_update_in_progress";
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
export type LocalConsolePackageUpdateOperationLease = {
|
|
7
|
+
release: () => void;
|
|
8
|
+
};
|
|
9
|
+
export type LocalConsoleOperationState = "idle" | "project_busy" | "package_update";
|
|
10
|
+
export declare class LocalConsoleOperationCoordinator {
|
|
11
|
+
#private;
|
|
12
|
+
state(): LocalConsoleOperationState;
|
|
13
|
+
runProjectOperation<T>(operation: () => Promise<T>): Promise<T>;
|
|
14
|
+
tryBeginPackageUpdate(): LocalConsolePackageUpdateOperationLease | null;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=operation-coordinator.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export class LocalConsoleOperationConflictError extends Error {
|
|
2
|
+
statusCode = 409;
|
|
3
|
+
code = "package_update_in_progress";
|
|
4
|
+
constructor() {
|
|
5
|
+
super("Tutti is restarting to install an update.");
|
|
6
|
+
this.name = "LocalConsoleOperationConflictError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class LocalConsoleOperationCoordinator {
|
|
10
|
+
#operationTail = Promise.resolve();
|
|
11
|
+
#pendingOperations = 0;
|
|
12
|
+
#packageUpdateOwner;
|
|
13
|
+
state() {
|
|
14
|
+
if (this.#packageUpdateOwner !== undefined) {
|
|
15
|
+
return "package_update";
|
|
16
|
+
}
|
|
17
|
+
return this.#pendingOperations > 0 ? "project_busy" : "idle";
|
|
18
|
+
}
|
|
19
|
+
runProjectOperation(operation) {
|
|
20
|
+
if (this.#packageUpdateOwner !== undefined) {
|
|
21
|
+
return Promise.reject(new LocalConsoleOperationConflictError());
|
|
22
|
+
}
|
|
23
|
+
this.#pendingOperations += 1;
|
|
24
|
+
const result = this.#operationTail.then(operation, operation);
|
|
25
|
+
this.#operationTail = result.then(() => undefined, () => undefined);
|
|
26
|
+
return result.finally(() => {
|
|
27
|
+
this.#pendingOperations = Math.max(0, this.#pendingOperations - 1);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
tryBeginPackageUpdate() {
|
|
31
|
+
if (this.#packageUpdateOwner !== undefined || this.#pendingOperations > 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const owner = Symbol("localConsolePackageUpdate");
|
|
35
|
+
this.#packageUpdateOwner = owner;
|
|
36
|
+
let released = false;
|
|
37
|
+
return {
|
|
38
|
+
release: () => {
|
|
39
|
+
if (released) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
released = true;
|
|
43
|
+
if (this.#packageUpdateOwner === owner) {
|
|
44
|
+
this.#packageUpdateOwner = undefined;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=operation-coordinator.js.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { PackageUpdateLogger } from "./package-update-log.js";
|
|
2
|
+
import { type PackageUpdateRuntimeRecord } from "./package-update-record.js";
|
|
3
|
+
export type PackageUpdateCompletionResult = {
|
|
4
|
+
status: "completed";
|
|
5
|
+
restart_failure_count: number;
|
|
6
|
+
browser_opened: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare function runPackageUpdateCompletion(options: {
|
|
9
|
+
tuttiHome: string;
|
|
10
|
+
cliEntrypoint: string;
|
|
11
|
+
env: NodeJS.ProcessEnv;
|
|
12
|
+
currentVersion?: string;
|
|
13
|
+
logger?: Pick<PackageUpdateLogger, "append">;
|
|
14
|
+
restoreHosts?: (record: PackageUpdateRuntimeRecord) => Promise<{
|
|
15
|
+
restored_project_ids: string[];
|
|
16
|
+
failed_project_ids: string[];
|
|
17
|
+
}>;
|
|
18
|
+
ensureConsole?: () => Promise<{
|
|
19
|
+
url: string;
|
|
20
|
+
}>;
|
|
21
|
+
openBrowser?: (url: string) => boolean;
|
|
22
|
+
releaseLock?: () => boolean;
|
|
23
|
+
now?: () => Date;
|
|
24
|
+
}): Promise<PackageUpdateCompletionResult>;
|
|
25
|
+
//# sourceMappingURL=package-update-completion.d.ts.map
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { readCliVersion } from "../cli/version.js";
|
|
3
|
+
import { ensureLocalConsole } from "./managed-console.js";
|
|
4
|
+
import { openLocalConsoleBrowser } from "./browser-open.js";
|
|
5
|
+
import { releasePackageUpdateLockForCurrentProcess } from "./package-update-lock.js";
|
|
6
|
+
import { PackageUpdateLogger } from "./package-update-log.js";
|
|
7
|
+
import { readPackageUpdateRuntimeRecord, updatePackageUpdateRuntimeRecord, } from "./package-update-record.js";
|
|
8
|
+
import { restorePackageUpdateHosts } from "./package-update-hosts.js";
|
|
9
|
+
export async function runPackageUpdateCompletion(options) {
|
|
10
|
+
const currentVersion = options.currentVersion ?? readCliVersion();
|
|
11
|
+
const read = readPackageUpdateRuntimeRecord({ tuttiHome: options.tuttiHome });
|
|
12
|
+
if (read.status !== "valid" ||
|
|
13
|
+
read.record.phase !== "restarting" ||
|
|
14
|
+
read.record.target_version !== currentVersion ||
|
|
15
|
+
read.record.updater_pid !== process.pid) {
|
|
16
|
+
throw new Error("Package update completion record is unavailable or does not match this CLI.");
|
|
17
|
+
}
|
|
18
|
+
const logger = options.logger ??
|
|
19
|
+
new PackageUpdateLogger({
|
|
20
|
+
tuttiHome: options.tuttiHome,
|
|
21
|
+
privatePaths: [options.tuttiHome],
|
|
22
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
23
|
+
});
|
|
24
|
+
const releaseLock = options.releaseLock ?? (() => releasePackageUpdateLockForCurrentProcess(options.tuttiHome));
|
|
25
|
+
const log = (entry) => {
|
|
26
|
+
try {
|
|
27
|
+
logger.append(entry);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Completion correctness must not depend on the diagnostic log remaining writable.
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
try {
|
|
34
|
+
const restored = await (options.restoreHosts ??
|
|
35
|
+
((record) => restorePackageUpdateHosts({
|
|
36
|
+
tuttiHome: options.tuttiHome,
|
|
37
|
+
projectIds: record.restore_project_ids,
|
|
38
|
+
env: options.env,
|
|
39
|
+
})))(read.record);
|
|
40
|
+
updatePackageUpdateRuntimeRecord({
|
|
41
|
+
tuttiHome: options.tuttiHome,
|
|
42
|
+
current: read.record,
|
|
43
|
+
phase: "restarting",
|
|
44
|
+
updaterPid: process.pid,
|
|
45
|
+
restartFailureCount: restored.failed_project_ids.length,
|
|
46
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
47
|
+
});
|
|
48
|
+
const consoleResult = await (options.ensureConsole ??
|
|
49
|
+
(() => ensureLocalConsole({
|
|
50
|
+
cwd: dirname(options.cliEntrypoint),
|
|
51
|
+
env: options.env,
|
|
52
|
+
source: "desktop",
|
|
53
|
+
})))();
|
|
54
|
+
if (!releaseLock()) {
|
|
55
|
+
throw new Error("Package update completion could not release its machine lock.");
|
|
56
|
+
}
|
|
57
|
+
const browserOpened = (options.openBrowser ?? openLocalConsoleBrowser)(consoleResult.url);
|
|
58
|
+
log({
|
|
59
|
+
level: restored.failed_project_ids.length === 0 ? "info" : "warn",
|
|
60
|
+
message: restored.failed_project_ids.length === 0
|
|
61
|
+
? "Package update completed."
|
|
62
|
+
: "Package update completed with Host restore failures.",
|
|
63
|
+
phase: "restarting",
|
|
64
|
+
...(restored.failed_project_ids.length === 0 ? {} : { reasonCode: "host_restore_failed" }),
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
status: "completed",
|
|
68
|
+
restart_failure_count: restored.failed_project_ids.length,
|
|
69
|
+
browser_opened: browserOpened,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
updatePackageUpdateRuntimeRecord({
|
|
74
|
+
tuttiHome: options.tuttiHome,
|
|
75
|
+
current: read.record,
|
|
76
|
+
phase: "failed",
|
|
77
|
+
updaterPid: process.pid,
|
|
78
|
+
failureReasonCode: "completion_failed",
|
|
79
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
80
|
+
});
|
|
81
|
+
releaseLock();
|
|
82
|
+
log({
|
|
83
|
+
level: "error",
|
|
84
|
+
message: "Package update completion failed.",
|
|
85
|
+
phase: "failed",
|
|
86
|
+
reasonCode: "completion_failed",
|
|
87
|
+
});
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=package-update-completion.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProjectId } from "@tutti/shared/ids";
|
|
2
|
+
export type PackageUpdateHostStopResult = "stopped" | "host_busy" | "host_identity_unverified" | "host_update_unsupported";
|
|
3
|
+
export declare function stopPackageUpdateHost(options: {
|
|
4
|
+
tuttiHome: string;
|
|
5
|
+
projectId: ProjectId;
|
|
6
|
+
fetchImpl?: typeof fetch;
|
|
7
|
+
}): Promise<PackageUpdateHostStopResult>;
|
|
8
|
+
export declare function restorePackageUpdateHosts(options: {
|
|
9
|
+
tuttiHome: string;
|
|
10
|
+
projectIds: readonly ProjectId[];
|
|
11
|
+
env: NodeJS.ProcessEnv;
|
|
12
|
+
fetchImpl?: typeof fetch;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
restored_project_ids: ProjectId[];
|
|
15
|
+
failed_project_ids: ProjectId[];
|
|
16
|
+
}>;
|
|
17
|
+
//# sourceMappingURL=package-update-hosts.d.ts.map
|