@xfey/tutti 0.1.60 → 0.1.62

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.
Files changed (90) hide show
  1. package/README.md +2 -2
  2. package/dist/collaboration-state/clarification-records.d.ts +0 -9
  3. package/dist/collaboration-state/clarification-records.js +0 -57
  4. package/dist/collaboration-state/index.d.ts +6 -5
  5. package/dist/collaboration-state/index.js +5 -4
  6. package/dist/collaboration-state/messages.d.ts +4 -6
  7. package/dist/collaboration-state/messages.js +22 -29
  8. package/dist/collaboration-state/scratchpad-source-state.d.ts +4 -12
  9. package/dist/collaboration-state/scratchpad-source-state.js +24 -58
  10. package/dist/collaboration-state/scratchpad.d.ts +1 -2
  11. package/dist/collaboration-state/scratchpad.js +0 -59
  12. package/dist/collaboration-state/storage-types.d.ts +25 -5
  13. package/dist/collaboration-state/task-compile-context.d.ts +40 -0
  14. package/dist/collaboration-state/task-compile-context.js +273 -0
  15. package/dist/collaboration-state/types.d.ts +39 -9
  16. package/dist/control-plane/clarification-commands.d.ts +0 -1
  17. package/dist/control-plane/clarification-commands.js +0 -1
  18. package/dist/control-plane/index.js +16 -3
  19. package/dist/control-plane/project-context-bootstrap.d.ts +4 -4
  20. package/dist/control-plane/project-context-bootstrap.js +7 -12
  21. package/dist/control-plane/scratchpad-auto-refresh.js +12 -1
  22. package/dist/control-plane/scratchpad-refresh-start.js +77 -94
  23. package/dist/control-plane/scratchpad-refresh.js +3 -1
  24. package/dist/control-plane/scratchpad-source-messages.d.ts +15 -7
  25. package/dist/control-plane/scratchpad-source-messages.js +53 -69
  26. package/dist/control-plane/task-compile-clarification-context.d.ts +9 -0
  27. package/dist/control-plane/task-compile-clarification-context.js +70 -0
  28. package/dist/control-plane/task-compile-continuation.js +28 -56
  29. package/dist/control-plane/task-compile-output.js +50 -14
  30. package/dist/control-plane/task-compile-start.js +119 -55
  31. package/dist/control-plane/workflows/openai.d.ts +2 -1
  32. package/dist/control-plane/workflows/openai.js +13 -27
  33. package/dist/control-plane/workflows/types.d.ts +27 -17
  34. package/dist/server-shell/cli/args.d.ts +4 -0
  35. package/dist/server-shell/cli/args.js +11 -0
  36. package/dist/server-shell/cli/cli.js +35 -11
  37. package/dist/server-shell/cli/host-server-runtime.d.ts +2 -0
  38. package/dist/server-shell/cli/host-server-runtime.js +12 -5
  39. package/dist/server-shell/cli/local-control-client.d.ts +5 -1
  40. package/dist/server-shell/cli/local-control-client.js +19 -0
  41. package/dist/server-shell/desktop-integration/manager.d.ts +2 -0
  42. package/dist/server-shell/desktop-integration/manager.js +19 -31
  43. package/dist/server-shell/http/routes/local-control.d.ts +2 -0
  44. package/dist/server-shell/http/routes/local-control.js +24 -0
  45. package/dist/server-shell/local-console/invocation-context.js +10 -1
  46. package/dist/server-shell/local-console/operation-coordinator.d.ts +16 -0
  47. package/dist/server-shell/local-console/operation-coordinator.js +50 -0
  48. package/dist/server-shell/local-console/package-update-completion.d.ts +25 -0
  49. package/dist/server-shell/local-console/package-update-completion.js +91 -0
  50. package/dist/server-shell/local-console/package-update-hosts.d.ts +17 -0
  51. package/dist/server-shell/local-console/package-update-hosts.js +92 -0
  52. package/dist/server-shell/local-console/package-update-lock.d.ts +46 -0
  53. package/dist/server-shell/local-console/package-update-lock.js +223 -0
  54. package/dist/server-shell/local-console/package-update-log.d.ts +20 -0
  55. package/dist/server-shell/local-console/package-update-log.js +75 -0
  56. package/dist/server-shell/local-console/package-update-process.d.ts +22 -0
  57. package/dist/server-shell/local-console/package-update-process.js +252 -0
  58. package/dist/server-shell/local-console/package-update-record.d.ts +53 -0
  59. package/dist/server-shell/local-console/package-update-record.js +180 -0
  60. package/dist/server-shell/local-console/package-update-recovery.d.ts +34 -0
  61. package/dist/server-shell/local-console/package-update-recovery.js +140 -0
  62. package/dist/server-shell/local-console/package-update-service.d.ts +72 -0
  63. package/dist/server-shell/local-console/package-update-service.js +400 -0
  64. package/dist/server-shell/local-console/package-update-worker.d.ts +55 -0
  65. package/dist/server-shell/local-console/package-update-worker.js +297 -0
  66. package/dist/server-shell/local-console/project-service.d.ts +2 -0
  67. package/dist/server-shell/local-console/project-service.js +16 -12
  68. package/dist/server-shell/local-console/server.d.ts +9 -0
  69. package/dist/server-shell/local-console/server.js +92 -4
  70. package/dist/server-shell/package-installation/command.d.ts +36 -0
  71. package/dist/server-shell/package-installation/command.js +162 -0
  72. package/dist/server-shell/package-installation/discovery.d.ts +51 -0
  73. package/dist/server-shell/package-installation/discovery.js +137 -0
  74. package/dist/server-shell/package-installation/index.d.ts +5 -0
  75. package/dist/server-shell/package-installation/index.js +5 -0
  76. package/dist/server-shell/package-installation/installation.d.ts +34 -0
  77. package/dist/server-shell/package-installation/installation.js +123 -0
  78. package/dist/server-shell/package-installation/semver.d.ts +9 -0
  79. package/dist/server-shell/package-installation/semver.js +84 -0
  80. package/migrations/0014_scratchpad_task_compile_context.sql +147 -0
  81. package/migrations/README.md +2 -1
  82. package/package.json +1 -1
  83. package/prompts/procedures/README.md +3 -3
  84. package/prompts/procedures/scratchpad-refresh.md +17 -9
  85. package/prompts/procedures/task-compile.md +6 -3
  86. package/prompts/prompt-flow-map.md +21 -14
  87. package/web/assets/{homepage-motion-scene-CY7o4hnR.js → homepage-motion-scene-CRmrkkEH.js} +1 -1
  88. package/web/assets/{index-B08r3x8o.js → index-C9JrplHV.js} +14 -14
  89. package/web/assets/{index-DpabiRgp.css → index-IZKcmU_g.css} +1 -1
  90. package/web/index.html +2 -2
@@ -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
- requestIdleShutdown: () => {
285
- const execution = controlPlane.getExecutionStatus();
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, join, resolve } from "node:path";
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 { createLinuxDesktopAdapter, findDesktopExecutable, linuxDesktopRequirementsAvailable, } from "./linux.js";
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
- if (packageIdentity.name !== PUBLISHED_CLI_PACKAGE_NAME ||
167
- (options.requireStableGlobal !== false &&
168
- !stableGlobalPackage({ packageRoot: packageIdentity.root, env, runner }))) {
169
- return unavailable("package_not_global", "Desktop integration requires a stable global @xfey/tutti installation.");
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 (env.TUTTI_HOME !== undefined && env.TUTTI_HOME.trim() !== "") {
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: resolveTuttiHome(undefined, cwd),
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 ["HOME", "LANG", "LC_ALL", "LC_CTYPE", "LOGNAME", "TMPDIR", "USER"]) {
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
@@ -0,0 +1,92 @@
1
+ import { inspectMachineProject, } from "../cli/machine-project-inspector.js";
2
+ import { readHostLocalPackageUpdateReadiness, requestHostLocalIdleShutdown, } from "../cli/local-control-client.js";
3
+ import { createRuntimeEndpointProbe, waitForHostShutdown } from "../cli/host-runtime-endpoint.js";
4
+ import { spawnDetachedHost, waitForManagedHostReady } from "../cli/managed-host.js";
5
+ import { resolveManagedProjectContext } from "../cli/project-resolver.js";
6
+ const PACKAGE_UPDATE_HOST_SHUTDOWN_WAIT_MS = 5_000;
7
+ export async function stopPackageUpdateHost(options) {
8
+ const fetchImpl = options.fetchImpl ?? fetch;
9
+ let inspection;
10
+ try {
11
+ inspection = await inspectMachineProject(options.projectId, {
12
+ tuttiHome: options.tuttiHome,
13
+ fetchImpl,
14
+ });
15
+ }
16
+ catch {
17
+ return "host_identity_unverified";
18
+ }
19
+ if (inspection.host.kind !== "running" ||
20
+ !inspection.host.identity_verified ||
21
+ inspection.endpoint.kind !== "valid") {
22
+ return "host_identity_unverified";
23
+ }
24
+ const endpoint = inspection.endpoint.record;
25
+ try {
26
+ const readiness = await readHostLocalPackageUpdateReadiness({ endpoint, fetchImpl });
27
+ if (readiness === "busy") {
28
+ return "host_busy";
29
+ }
30
+ if (readiness === "unsupported") {
31
+ return "host_update_unsupported";
32
+ }
33
+ const disposition = await requestHostLocalIdleShutdown({ endpoint, fetchImpl });
34
+ if (disposition === "busy") {
35
+ return "host_busy";
36
+ }
37
+ if (disposition !== "accepted") {
38
+ return "host_update_unsupported";
39
+ }
40
+ await waitForHostShutdown({
41
+ endpoint,
42
+ probeRuntimeEndpoint: createRuntimeEndpointProbe(fetchImpl),
43
+ shutdownWaitMs: PACKAGE_UPDATE_HOST_SHUTDOWN_WAIT_MS,
44
+ });
45
+ return "stopped";
46
+ }
47
+ catch {
48
+ return "host_identity_unverified";
49
+ }
50
+ }
51
+ export async function restorePackageUpdateHosts(options) {
52
+ const restored = [];
53
+ const failed = [];
54
+ for (const projectId of [...new Set(options.projectIds)].sort()) {
55
+ try {
56
+ const inspection = await inspectMachineProject(projectId, {
57
+ tuttiHome: options.tuttiHome,
58
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
59
+ });
60
+ if (inspection.host.kind === "running" && inspection.host.identity_verified) {
61
+ restored.push(projectId);
62
+ continue;
63
+ }
64
+ const project = resolveManagedProjectContext({
65
+ target: projectId,
66
+ env: { ...options.env, TUTTI_HOME: options.tuttiHome },
67
+ });
68
+ if (!project.workspace_exists || project.provider_config.status !== "configured") {
69
+ failed.push(projectId);
70
+ continue;
71
+ }
72
+ spawnDetachedHost({
73
+ workspaceRoot: project.workspace_root,
74
+ env: { ...options.env, TUTTI_HOME: options.tuttiHome },
75
+ inheritProcessEnv: false,
76
+ });
77
+ await waitForManagedHostReady({
78
+ tuttiHome: options.tuttiHome,
79
+ projectId,
80
+ workspaceRoot: project.workspace_root,
81
+ inviteMode: "preserve",
82
+ ...(options.fetchImpl === undefined ? {} : { fetchImpl: options.fetchImpl }),
83
+ });
84
+ restored.push(projectId);
85
+ }
86
+ catch {
87
+ failed.push(projectId);
88
+ }
89
+ }
90
+ return { restored_project_ids: restored, failed_project_ids: failed };
91
+ }
92
+ //# sourceMappingURL=package-update-hosts.js.map
@@ -0,0 +1,46 @@
1
+ export type PackageUpdateLockInspection = {
2
+ status: "unlocked";
3
+ } | {
4
+ status: "active";
5
+ } | {
6
+ status: "recovery_required";
7
+ };
8
+ export type PackageUpdateLockLease = {
9
+ path: string;
10
+ transferTo: (pid: number) => void;
11
+ release: () => void;
12
+ };
13
+ export type PackageUpdateLockAcquisition = {
14
+ status: "acquired";
15
+ lease: PackageUpdateLockLease;
16
+ } | {
17
+ status: "active" | "recovery_required";
18
+ };
19
+ export declare function packageUpdateLockPath(tuttiHome: string): string;
20
+ export declare function inspectPackageUpdateLock(options: {
21
+ tuttiHome: string;
22
+ now?: () => number;
23
+ staleMs?: number;
24
+ processAlive?: (pid: number) => boolean;
25
+ }): PackageUpdateLockInspection;
26
+ export declare function tryAcquirePackageUpdateLock(options: {
27
+ tuttiHome: string;
28
+ now?: () => number;
29
+ pid?: number;
30
+ staleMs?: number;
31
+ processAlive?: (pid: number) => boolean;
32
+ }): PackageUpdateLockAcquisition;
33
+ export declare function removeRecoverablePackageUpdateLock(options: {
34
+ tuttiHome: string;
35
+ now?: () => number;
36
+ staleMs?: number;
37
+ processAlive?: (pid: number) => boolean;
38
+ }): boolean;
39
+ export declare function transferPackageUpdateLockFromCurrentProcess(options: {
40
+ tuttiHome: string;
41
+ nextPid: number;
42
+ now?: () => number;
43
+ }): boolean;
44
+ export declare function releasePackageUpdateLockForCurrentProcess(tuttiHome: string): boolean;
45
+ export declare function releasePackageUpdateLockForExpectedPid(tuttiHome: string, expectedPid: number): boolean;
46
+ //# sourceMappingURL=package-update-lock.d.ts.map