@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.
- package/README.md +1 -1
- package/dist/server-shell/cli/args.d.ts +9 -0
- package/dist/server-shell/cli/args.js +55 -5
- package/dist/server-shell/cli/cli.js +46 -7
- package/dist/server-shell/cli/errors.d.ts +1 -1
- package/dist/server-shell/cli/errors.js +6 -1
- package/dist/server-shell/cli/host-lifecycle.d.ts +1 -0
- package/dist/server-shell/cli/host-lifecycle.js +155 -41
- package/dist/server-shell/cli/host-relay-connection-manager.d.ts +50 -0
- package/dist/server-shell/cli/host-relay-connection-manager.js +447 -0
- package/dist/server-shell/cli/host-relay-status.d.ts +4 -0
- package/dist/server-shell/cli/host-relay-status.js +74 -15
- package/dist/server-shell/cli/host-runtime-endpoint.js +26 -0
- package/dist/server-shell/cli/host-server-runtime.d.ts +2 -2
- package/dist/server-shell/cli/host-server-runtime.js +19 -15
- package/dist/server-shell/cli/launch-command.d.ts +1 -0
- package/dist/server-shell/cli/launch-command.js +8 -10
- package/dist/server-shell/cli/launch.d.ts +1 -0
- package/dist/server-shell/cli/launch.js +7 -3
- package/dist/server-shell/cli/local-control-client.d.ts +5 -0
- package/dist/server-shell/cli/local-control-client.js +9 -0
- package/dist/server-shell/cli/machine-local.d.ts +8 -0
- package/dist/server-shell/cli/machine-local.js +29 -0
- package/dist/server-shell/cli/machine-project-inspector.d.ts +81 -0
- package/dist/server-shell/cli/machine-project-inspector.js +205 -0
- package/dist/server-shell/cli/project-resolver.js +22 -32
- package/dist/server-shell/cli/relay-registration.d.ts +5 -2
- package/dist/server-shell/cli/relay-registration.js +7 -3
- package/dist/server-shell/cli/runtime-commands.d.ts +30 -4
- package/dist/server-shell/cli/runtime-commands.js +230 -113
- package/dist/server-shell/http/routes/local-control.d.ts +10 -0
- package/dist/server-shell/http/routes/local-control.js +29 -0
- package/node_modules/@tutti/relay-client/dist/host-control.d.ts +23 -0
- package/node_modules/@tutti/relay-client/dist/host-control.js +70 -3
- package/node_modules/@tutti/relay-client/dist/tunnel-types.d.ts +6 -2
- package/node_modules/@tutti/relay-client/dist/tunnel.js +13 -2
- 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:
|
|
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<
|
|
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
|
-
|
|
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,
|