@xfey/tutti 0.1.29 → 0.1.30

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 (37) hide show
  1. package/README.md +1 -1
  2. package/dist/server-shell/cli/args.d.ts +9 -0
  3. package/dist/server-shell/cli/args.js +55 -5
  4. package/dist/server-shell/cli/cli.js +46 -7
  5. package/dist/server-shell/cli/errors.d.ts +1 -1
  6. package/dist/server-shell/cli/errors.js +6 -1
  7. package/dist/server-shell/cli/host-lifecycle.d.ts +1 -0
  8. package/dist/server-shell/cli/host-lifecycle.js +155 -41
  9. package/dist/server-shell/cli/host-relay-connection-manager.d.ts +50 -0
  10. package/dist/server-shell/cli/host-relay-connection-manager.js +447 -0
  11. package/dist/server-shell/cli/host-relay-status.d.ts +4 -0
  12. package/dist/server-shell/cli/host-relay-status.js +74 -15
  13. package/dist/server-shell/cli/host-runtime-endpoint.js +26 -0
  14. package/dist/server-shell/cli/host-server-runtime.d.ts +2 -2
  15. package/dist/server-shell/cli/host-server-runtime.js +19 -15
  16. package/dist/server-shell/cli/launch-command.d.ts +1 -0
  17. package/dist/server-shell/cli/launch-command.js +8 -10
  18. package/dist/server-shell/cli/launch.d.ts +1 -0
  19. package/dist/server-shell/cli/launch.js +7 -3
  20. package/dist/server-shell/cli/local-control-client.d.ts +5 -0
  21. package/dist/server-shell/cli/local-control-client.js +9 -0
  22. package/dist/server-shell/cli/machine-local.d.ts +8 -0
  23. package/dist/server-shell/cli/machine-local.js +29 -0
  24. package/dist/server-shell/cli/machine-project-inspector.d.ts +81 -0
  25. package/dist/server-shell/cli/machine-project-inspector.js +205 -0
  26. package/dist/server-shell/cli/project-resolver.js +22 -32
  27. package/dist/server-shell/cli/relay-registration.d.ts +5 -2
  28. package/dist/server-shell/cli/relay-registration.js +7 -3
  29. package/dist/server-shell/cli/runtime-commands.d.ts +30 -4
  30. package/dist/server-shell/cli/runtime-commands.js +230 -113
  31. package/dist/server-shell/http/routes/local-control.d.ts +10 -0
  32. package/dist/server-shell/http/routes/local-control.js +29 -0
  33. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +23 -0
  34. package/node_modules/@tutti/relay-client/dist/host-control.js +70 -3
  35. package/node_modules/@tutti/relay-client/dist/tunnel-types.d.ts +6 -2
  36. package/node_modules/@tutti/relay-client/dist/tunnel.js +13 -2
  37. package/package.json +1 -1
@@ -20,6 +20,9 @@ function registrationUrl(relayUrl) {
20
20
  function joinTokenUrl(relayUrl) {
21
21
  return new URL("/host-control/v1/join-token", relayUrl);
22
22
  }
23
+ function archiveProjectUrl(relayUrl) {
24
+ return new URL("/host-control/v1/projects/archive", relayUrl);
25
+ }
23
26
  function uploadResolveUrl(relayUrl) {
24
27
  return new URL("/host-control/v1/uploads/resolve", relayUrl);
25
28
  }
@@ -82,6 +85,19 @@ function createRefreshJoinTokenPayload(options) {
82
85
  }
83
86
  return payload;
84
87
  }
88
+ function createArchiveRelayProjectPayload(options) {
89
+ return {
90
+ project_identity: {
91
+ source: "git_config",
92
+ project_id: options.projectId,
93
+ },
94
+ relay_project_ref: options.relayProjectRef,
95
+ host_auth: {
96
+ kind: "host_registration_secret_v1",
97
+ secret: options.hostRegistrationSecret,
98
+ },
99
+ };
100
+ }
85
101
  function createResolveRelayUploadPayload(options) {
86
102
  return {
87
103
  project_identity: {
@@ -116,7 +132,7 @@ function relayErrorCodeFromBody(value) {
116
132
  }
117
133
  return stringField(value.error, "code");
118
134
  }
119
- async function fetchWithTimeout(fetchImpl, url, init, timeoutMs) {
135
+ async function fetchWithTimeout(fetchImpl, url, init, timeoutMs, externalSignal) {
120
136
  const controller = new AbortController();
121
137
  const timeout = setTimeout(() => {
122
138
  controller.abort();
@@ -124,7 +140,9 @@ async function fetchWithTimeout(fetchImpl, url, init, timeoutMs) {
124
140
  try {
125
141
  return await fetchImpl(url, {
126
142
  ...init,
127
- signal: controller.signal,
143
+ signal: externalSignal === undefined
144
+ ? controller.signal
145
+ : AbortSignal.any([controller.signal, externalSignal]),
128
146
  });
129
147
  }
130
148
  finally {
@@ -147,6 +165,21 @@ function assertRefreshJoinTokenResponse(value) {
147
165
  }
148
166
  return value;
149
167
  }
168
+ function assertArchiveRelayProjectResponse(value) {
169
+ if (!isRecord(value) ||
170
+ stringField(value, "command_id") === undefined ||
171
+ !isRecord(value.disposition)) {
172
+ throw new RelayHostControlClientError("relay_registration_failed", "Relay project archive response is invalid", false);
173
+ }
174
+ const kind = stringField(value.disposition, "kind");
175
+ if (kind !== "archived" && kind !== "already_archived" && kind !== "interrupted") {
176
+ throw new RelayHostControlClientError("relay_registration_failed", "Relay project archive disposition is invalid", false);
177
+ }
178
+ if (kind === "interrupted" && value.disposition.retry_with_new_command_id !== true) {
179
+ throw new RelayHostControlClientError("relay_registration_failed", "Relay project archive disposition is invalid", false);
180
+ }
181
+ return value;
182
+ }
150
183
  function assertResolveRelayUploadResponse(value) {
151
184
  if (!isRecord(value) || !isRecord(value.upload) || !isRecord(value.get)) {
152
185
  throw new RelayHostControlClientError("relay_registration_failed", "Relay upload resolve response is invalid", false);
@@ -176,7 +209,7 @@ export async function registerRelayHostConnection(options) {
176
209
  issued_at: issuedAt.toISOString(),
177
210
  payload: createRegisterPayload(options),
178
211
  }),
179
- }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
212
+ }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS, options.signal);
180
213
  }
181
214
  catch {
182
215
  throw new RelayHostControlClientError("relay_unavailable", `Cannot reach Relay at ${options.relayUrl}`, true);
@@ -223,6 +256,40 @@ export async function refreshRelayJoinToken(options) {
223
256
  }
224
257
  return assertRefreshJoinTokenResponse(await response.json());
225
258
  }
259
+ export async function archiveRelayProject(options) {
260
+ const fetchImpl = options.fetch ?? fetch;
261
+ const commandId = options.commandId ?? createIdempotencyKey();
262
+ const issuedAt = options.issuedAt ?? new Date();
263
+ let response;
264
+ try {
265
+ response = await fetchWithTimeout(fetchImpl, archiveProjectUrl(options.relayUrl), {
266
+ method: "POST",
267
+ headers: {
268
+ accept: "application/json",
269
+ "content-type": "application/json",
270
+ },
271
+ body: JSON.stringify({
272
+ command_id: commandId,
273
+ issued_at: issuedAt.toISOString(),
274
+ payload: createArchiveRelayProjectPayload(options),
275
+ }),
276
+ }, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
277
+ }
278
+ catch {
279
+ throw new RelayHostControlClientError("relay_unavailable", `Cannot reach Relay at ${options.relayUrl}`, true);
280
+ }
281
+ if (!response.ok) {
282
+ let relayErrorCode;
283
+ try {
284
+ relayErrorCode = relayErrorCodeFromBody(await response.json());
285
+ }
286
+ catch {
287
+ relayErrorCode = undefined;
288
+ }
289
+ throw new RelayHostControlClientError(response.status >= 500 ? "relay_unavailable" : "relay_registration_failed", `Relay project archive failed with HTTP ${response.status}`, response.status >= 500, response.status, relayErrorCode);
290
+ }
291
+ return assertArchiveRelayProjectResponse(await response.json());
292
+ }
226
293
  export async function resolveRelayUpload(options) {
227
294
  const fetchImpl = options.fetch ?? fetch;
228
295
  const response = await (async () => {
@@ -35,7 +35,7 @@ export type RelayHostTunnelStreamRequestHandler = (request: RelayHostTunnelReque
35
35
  export type RelayHostTunnelWebSocketLike = {
36
36
  on(event: "open", listener: () => void): void;
37
37
  on(event: "message", listener: (data: unknown) => void): void;
38
- on(event: "close", listener: () => void): void;
38
+ on(event: "close", listener: (code?: number, reason?: unknown) => void): void;
39
39
  on(event: "error", listener: (error: unknown) => void): void;
40
40
  send(data: string, callback?: (error?: Error) => void): void;
41
41
  close(code?: number, reason?: string): void;
@@ -46,9 +46,13 @@ export type RelayHostTunnelConnector = (options: {
46
46
  }) => RelayHostTunnelWebSocketLike;
47
47
  export type RelayHostTunnelHandle = {
48
48
  connected: Promise<void>;
49
- closed: Promise<void>;
49
+ closed: Promise<RelayHostTunnelCloseResult>;
50
50
  close: (code?: number, reason?: string) => void;
51
51
  };
52
+ export type RelayHostTunnelCloseResult = {
53
+ code?: number;
54
+ reason?: string;
55
+ };
52
56
  export type ConnectRelayHostTunnelOptions = {
53
57
  tunnelUrl: string;
54
58
  relayProjectRef: RelayProjectRef;
@@ -1,4 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
+ import { redactAndTruncateText } from "@tutti/shared/utils";
2
3
  import { chunkToBase64, internalErrorResponse, parseHostRequestFrame, parseHostStreamCancelFrame, sendFrame, sendResponse, } from "./tunnel-frames.js";
3
4
  const loadNativeModule = createRequire(import.meta.url);
4
5
  function defaultConnector(options) {
@@ -123,7 +124,7 @@ export function connectRelayHostTunnel(options) {
123
124
  rejectConnected(error instanceof Error ? error : new Error("Relay host tunnel failed"));
124
125
  }
125
126
  });
126
- socket.on("close", () => {
127
+ socket.on("close", (code, reason) => {
127
128
  for (const controller of streamControllers.values()) {
128
129
  controller.abort();
129
130
  }
@@ -131,7 +132,17 @@ export function connectRelayHostTunnel(options) {
131
132
  if (!connectedSettled) {
132
133
  rejectConnected(new Error("Relay host tunnel closed before it opened"));
133
134
  }
134
- resolveClosed();
135
+ const normalizedReason = typeof reason === "string"
136
+ ? reason
137
+ : Buffer.isBuffer(reason)
138
+ ? reason.toString("utf8")
139
+ : undefined;
140
+ resolveClosed({
141
+ ...(code === undefined ? {} : { code }),
142
+ ...(normalizedReason === undefined || normalizedReason === ""
143
+ ? {}
144
+ : { reason: redactAndTruncateText(normalizedReason, 120) }),
145
+ });
135
146
  });
136
147
  return {
137
148
  connected,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfey/tutti",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",