@xfey/tutti 0.1.32 → 0.1.34
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 +5 -4
- package/dist/server-shell/cli/args.d.ts +10 -0
- package/dist/server-shell/cli/args.js +40 -1
- package/dist/server-shell/cli/cli.js +136 -5
- package/dist/server-shell/cli/errors.d.ts +1 -1
- package/dist/server-shell/cli/host-runtime-endpoint.d.ts +1 -0
- package/dist/server-shell/cli/host-runtime-endpoint.js +10 -5
- package/dist/server-shell/cli/host-server-runtime.js +2 -10
- package/dist/server-shell/cli/machine-project-inspector.d.ts +1 -0
- package/dist/server-shell/cli/machine-project-inspector.js +1 -0
- package/dist/server-shell/cli/managed-host.d.ts +1 -0
- package/dist/server-shell/cli/managed-host.js +6 -4
- package/dist/server-shell/cli/runtime-commands.d.ts +1 -0
- package/dist/server-shell/cli/runtime-commands.js +5 -1
- 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 +20 -0
- package/dist/server-shell/local-console/folder-picker.js +78 -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/invocation-context.d.ts +21 -0
- package/dist/server-shell/local-console/invocation-context.js +56 -0
- package/dist/server-shell/local-console/lifecycle-lock.d.ts +13 -0
- package/dist/server-shell/local-console/lifecycle-lock.js +135 -0
- package/dist/server-shell/local-console/managed-console.d.ts +47 -0
- package/dist/server-shell/local-console/managed-console.js +312 -0
- package/dist/server-shell/local-console/project-service.d.ts +61 -0
- package/dist/server-shell/local-console/project-service.js +262 -0
- package/dist/server-shell/local-console/runtime-endpoint.d.ts +35 -0
- package/dist/server-shell/local-console/runtime-endpoint.js +90 -0
- package/dist/server-shell/local-console/server.d.ts +29 -0
- package/dist/server-shell/local-console/server.js +408 -0
- package/dist/server-shell/local-console/session.d.ts +28 -0
- package/dist/server-shell/local-console/session.js +168 -0
- package/package.json +1 -1
- package/web/assets/index-B84rQ4YJ.js +29 -0
- package/web/assets/index-C7RDpM1L.css +1 -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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ProjectId } from "@tutti/shared/ids";
|
|
2
|
+
import { type RuntimeProjectRow } from "../cli/runtime-commands.js";
|
|
3
|
+
import { type LocalConsoleInvocationContext } from "./invocation-context.js";
|
|
4
|
+
export type LocalConsoleProject = {
|
|
5
|
+
project_id: ProjectId;
|
|
6
|
+
display_name: string;
|
|
7
|
+
workspace_path: string;
|
|
8
|
+
status: RuntimeProjectRow["status"];
|
|
9
|
+
provider_status: "configured" | "not_configured" | "invalid";
|
|
10
|
+
relay_project_ref?: string;
|
|
11
|
+
join_url?: string;
|
|
12
|
+
relay_connection_status?: string;
|
|
13
|
+
activity_at?: string;
|
|
14
|
+
host_version?: string;
|
|
15
|
+
update_required?: boolean;
|
|
16
|
+
};
|
|
17
|
+
export type LocalConsoleProviderInput = {
|
|
18
|
+
base_url: string;
|
|
19
|
+
api_key: string;
|
|
20
|
+
model: string;
|
|
21
|
+
};
|
|
22
|
+
export type LocalConsoleLaunchResult = {
|
|
23
|
+
project: LocalConsoleProject;
|
|
24
|
+
join_url: string;
|
|
25
|
+
join_token_expires_at?: string;
|
|
26
|
+
};
|
|
27
|
+
export declare class LocalConsoleProjectError extends Error {
|
|
28
|
+
readonly code: string;
|
|
29
|
+
readonly statusCode: number;
|
|
30
|
+
constructor(code: string, message: string, statusCode?: number);
|
|
31
|
+
}
|
|
32
|
+
export declare class LocalConsoleProjectService {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(options: {
|
|
35
|
+
tuttiHome: string;
|
|
36
|
+
serviceEnvironment?: NodeJS.ProcessEnv;
|
|
37
|
+
});
|
|
38
|
+
listProjects(context: LocalConsoleInvocationContext): Promise<LocalConsoleProject[]>;
|
|
39
|
+
discoverModels(input: {
|
|
40
|
+
baseUrl: string;
|
|
41
|
+
apiKey: string;
|
|
42
|
+
}): Promise<{
|
|
43
|
+
kind: "ok";
|
|
44
|
+
models: string[];
|
|
45
|
+
} | {
|
|
46
|
+
kind: "unavailable";
|
|
47
|
+
reason: string;
|
|
48
|
+
}>;
|
|
49
|
+
launchProject(input: {
|
|
50
|
+
workspacePath: string;
|
|
51
|
+
provider?: LocalConsoleProviderInput;
|
|
52
|
+
}, context: LocalConsoleInvocationContext): Promise<LocalConsoleLaunchResult>;
|
|
53
|
+
refreshInvite(projectId: string, context: LocalConsoleInvocationContext): Promise<{
|
|
54
|
+
join_url: string;
|
|
55
|
+
expires_at?: string;
|
|
56
|
+
}>;
|
|
57
|
+
stopProject(projectId: string, context: LocalConsoleInvocationContext): Promise<{
|
|
58
|
+
message: string;
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=project-service.d.ts.map
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { configureProjectOpenAiProvider, discoverOpenAiModels, readOpenAiProviderConfigProjection, } from "../../providers/openai/index.js";
|
|
4
|
+
import { formatCliErrorReason, LaunchError } from "../cli/errors.js";
|
|
5
|
+
import { prepareLaunchProject } from "../cli/launch.js";
|
|
6
|
+
import { rotateHostLocalInvite } from "../cli/local-control-client.js";
|
|
7
|
+
import { readMachineProjectBinding, readMachineRuntimeEndpoint } from "../cli/machine-local.js";
|
|
8
|
+
import { spawnDetachedHost, waitForManagedHostReady } from "../cli/managed-host.js";
|
|
9
|
+
import { resolveManagedProjectContext } from "../cli/project-resolver.js";
|
|
10
|
+
import { listRuntimeProjects, runStopCommand, } from "../cli/runtime-commands.js";
|
|
11
|
+
import { readCliVersion } from "../cli/version.js";
|
|
12
|
+
import { createLocalConsoleOperationEnvironment, } from "./invocation-context.js";
|
|
13
|
+
export class LocalConsoleProjectError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
statusCode;
|
|
16
|
+
constructor(code, message, statusCode = 422) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.statusCode = statusCode;
|
|
20
|
+
this.name = "LocalConsoleProjectError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function validateWorkspacePath(value) {
|
|
24
|
+
const path = resolve(value.trim());
|
|
25
|
+
if (value.trim() === "" || !existsSync(path) || !statSync(path).isDirectory()) {
|
|
26
|
+
throw new LocalConsoleProjectError("project_folder_invalid", "Choose an existing project folder before launching.");
|
|
27
|
+
}
|
|
28
|
+
return path;
|
|
29
|
+
}
|
|
30
|
+
function validateProvider(input) {
|
|
31
|
+
const provider = {
|
|
32
|
+
base_url: input.base_url.trim(),
|
|
33
|
+
api_key: input.api_key.trim(),
|
|
34
|
+
model: input.model.trim(),
|
|
35
|
+
};
|
|
36
|
+
if (provider.base_url === "" || provider.api_key === "" || provider.model === "") {
|
|
37
|
+
throw new LocalConsoleProjectError("provider_configuration_required", "Base URL, API key, and model are required for a new Provider configuration.");
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const url = new URL(provider.base_url);
|
|
41
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
42
|
+
throw new Error("unsupported protocol");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
throw new LocalConsoleProjectError("provider_base_url_invalid", "Enter a valid HTTP or HTTPS Provider Base URL.");
|
|
47
|
+
}
|
|
48
|
+
return provider;
|
|
49
|
+
}
|
|
50
|
+
function projectFromRuntimeRow(row, providerStatus, activityAt) {
|
|
51
|
+
const currentVersion = readCliVersion();
|
|
52
|
+
return {
|
|
53
|
+
project_id: row.project_id,
|
|
54
|
+
display_name: row.display_name,
|
|
55
|
+
workspace_path: row.workspace_root,
|
|
56
|
+
status: row.status,
|
|
57
|
+
provider_status: providerStatus,
|
|
58
|
+
...(row.relay_project_ref === undefined ? {} : { relay_project_ref: row.relay_project_ref }),
|
|
59
|
+
...(row.join_url === undefined ? {} : { join_url: row.join_url }),
|
|
60
|
+
...(row.relay_connection_status === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { relay_connection_status: row.relay_connection_status }),
|
|
63
|
+
...(activityAt === undefined ? {} : { activity_at: activityAt }),
|
|
64
|
+
...(row.host_version === undefined ? {} : { host_version: row.host_version }),
|
|
65
|
+
...(row.host_version === undefined || row.host_version === currentVersion
|
|
66
|
+
? {}
|
|
67
|
+
: { update_required: true }),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function readProjectActivityAt(tuttiHome, row) {
|
|
71
|
+
if (row.last_connected_at !== undefined) {
|
|
72
|
+
return row.last_connected_at;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const endpoint = readMachineRuntimeEndpoint(tuttiHome, row.project_id);
|
|
76
|
+
if (endpoint !== null) {
|
|
77
|
+
return endpoint.updated_at;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Invalid endpoint state is already represented by the project status.
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
return readMachineProjectBinding(tuttiHome, row.project_id)?.updated_at;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
export class LocalConsoleProjectService {
|
|
91
|
+
#tuttiHome;
|
|
92
|
+
#serviceEnvironment;
|
|
93
|
+
#launches = new Map();
|
|
94
|
+
#mutationTail = Promise.resolve();
|
|
95
|
+
constructor(options) {
|
|
96
|
+
this.#tuttiHome = resolve(options.tuttiHome);
|
|
97
|
+
this.#serviceEnvironment = options.serviceEnvironment ?? process.env;
|
|
98
|
+
}
|
|
99
|
+
#operationOptions(context) {
|
|
100
|
+
return {
|
|
101
|
+
cwd: context.currentDirectory,
|
|
102
|
+
env: createLocalConsoleOperationEnvironment({
|
|
103
|
+
serviceEnvironment: this.#serviceEnvironment,
|
|
104
|
+
invocationEnvironment: context.environment,
|
|
105
|
+
tuttiHome: this.#tuttiHome,
|
|
106
|
+
}),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
#runMutation(operation) {
|
|
110
|
+
const result = this.#mutationTail.then(operation, operation);
|
|
111
|
+
this.#mutationTail = result.then(() => undefined, () => undefined);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
async listProjects(context) {
|
|
115
|
+
const operation = this.#operationOptions(context);
|
|
116
|
+
const rows = await listRuntimeProjects({
|
|
117
|
+
all: true,
|
|
118
|
+
...operation,
|
|
119
|
+
});
|
|
120
|
+
return rows.map((row) => {
|
|
121
|
+
const provider = readOpenAiProviderConfigProjection({
|
|
122
|
+
tuttiHome: this.#tuttiHome,
|
|
123
|
+
projectId: row.project_id,
|
|
124
|
+
});
|
|
125
|
+
return projectFromRuntimeRow(row, provider.status, readProjectActivityAt(this.#tuttiHome, row));
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async discoverModels(input) {
|
|
129
|
+
const provider = validateProvider({
|
|
130
|
+
base_url: input.baseUrl,
|
|
131
|
+
api_key: input.apiKey,
|
|
132
|
+
model: "model-discovery",
|
|
133
|
+
});
|
|
134
|
+
return await discoverOpenAiModels({
|
|
135
|
+
apiBaseUrl: provider.base_url,
|
|
136
|
+
apiKey: provider.api_key,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async launchProject(input, context) {
|
|
140
|
+
const workspacePath = validateWorkspacePath(input.workspacePath);
|
|
141
|
+
const active = this.#launches.get(workspacePath);
|
|
142
|
+
if (active !== undefined) {
|
|
143
|
+
return await active;
|
|
144
|
+
}
|
|
145
|
+
const launch = this.#runMutation(async () => await this.#launchProject({
|
|
146
|
+
workspacePath,
|
|
147
|
+
...(input.provider === undefined ? {} : { provider: validateProvider(input.provider) }),
|
|
148
|
+
}, context)).finally(() => {
|
|
149
|
+
this.#launches.delete(workspacePath);
|
|
150
|
+
});
|
|
151
|
+
this.#launches.set(workspacePath, launch);
|
|
152
|
+
return await launch;
|
|
153
|
+
}
|
|
154
|
+
async #launchProject(input, context) {
|
|
155
|
+
const operation = this.#operationOptions(context);
|
|
156
|
+
let preparation;
|
|
157
|
+
try {
|
|
158
|
+
preparation = await prepareLaunchProject({
|
|
159
|
+
workspacePath: input.workspacePath,
|
|
160
|
+
yes: true,
|
|
161
|
+
...operation,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
throw this.#normalizeError(error);
|
|
166
|
+
}
|
|
167
|
+
if (input.provider !== undefined) {
|
|
168
|
+
const configured = await configureProjectOpenAiProvider({
|
|
169
|
+
tuttiHome: preparation.tutti_home,
|
|
170
|
+
projectId: preparation.project_id,
|
|
171
|
+
apiBaseUrl: input.provider.base_url,
|
|
172
|
+
apiKey: input.provider.api_key,
|
|
173
|
+
defaultModel: input.provider.model,
|
|
174
|
+
});
|
|
175
|
+
if (configured.kind === "validation_failed") {
|
|
176
|
+
throw new LocalConsoleProjectError(configured.reason, "Provider validation failed. Check the Base URL, API key, and selected model.");
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
else if (preparation.provider_status !== "configured") {
|
|
180
|
+
throw new LocalConsoleProjectError("provider_configuration_required", "This project does not have a valid saved Provider. Configure it in the launch form.");
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
spawnDetachedHost({
|
|
184
|
+
workspaceRoot: preparation.workspace_root,
|
|
185
|
+
env: operation.env,
|
|
186
|
+
inheritProcessEnv: false,
|
|
187
|
+
});
|
|
188
|
+
const ready = await waitForManagedHostReady({
|
|
189
|
+
tuttiHome: preparation.tutti_home,
|
|
190
|
+
projectId: preparation.project_id,
|
|
191
|
+
workspaceRoot: preparation.workspace_root,
|
|
192
|
+
});
|
|
193
|
+
if (ready.join_url === undefined) {
|
|
194
|
+
throw new LocalConsoleProjectError("relay_registration_failed", "The host started, but Relay did not return a visible join link.");
|
|
195
|
+
}
|
|
196
|
+
const projects = await this.listProjects(context);
|
|
197
|
+
const project = projects.find((item) => item.project_id === preparation.project_id);
|
|
198
|
+
if (project === undefined) {
|
|
199
|
+
throw new LocalConsoleProjectError("project_not_found", "The launched project could not be read from the machine project list.");
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
project,
|
|
203
|
+
join_url: ready.join_url,
|
|
204
|
+
...(ready.join_token_expires_at === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { join_token_expires_at: ready.join_token_expires_at }),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
throw this.#normalizeError(error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async refreshInvite(projectId, context) {
|
|
214
|
+
return await this.#runMutation(async () => {
|
|
215
|
+
const project = resolveManagedProjectContext({
|
|
216
|
+
target: projectId,
|
|
217
|
+
...this.#operationOptions(context),
|
|
218
|
+
});
|
|
219
|
+
if (project.runtime_endpoint === null) {
|
|
220
|
+
throw new LocalConsoleProjectError("host_not_running", "Launch this project before creating an invite link.");
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const status = await rotateHostLocalInvite({ endpoint: project.runtime_endpoint });
|
|
224
|
+
const joinUrl = status.relay?.join_url;
|
|
225
|
+
if (joinUrl === undefined) {
|
|
226
|
+
throw new LocalConsoleProjectError("relay_registration_failed", "Relay did not return a visible invite link.");
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
join_url: joinUrl,
|
|
230
|
+
...(status.relay?.join_token_expires_at === undefined
|
|
231
|
+
? {}
|
|
232
|
+
: { expires_at: status.relay.join_token_expires_at }),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
throw this.#normalizeError(error);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async stopProject(projectId, context) {
|
|
241
|
+
return await this.#runMutation(async () => {
|
|
242
|
+
try {
|
|
243
|
+
return {
|
|
244
|
+
message: await runStopCommand(projectId, this.#operationOptions(context)),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
throw this.#normalizeError(error);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
#normalizeError(error) {
|
|
253
|
+
if (error instanceof LocalConsoleProjectError) {
|
|
254
|
+
return error;
|
|
255
|
+
}
|
|
256
|
+
if (error instanceof LaunchError) {
|
|
257
|
+
return new LocalConsoleProjectError(error.code, error.message);
|
|
258
|
+
}
|
|
259
|
+
return new LocalConsoleProjectError("local_console_failed", formatCliErrorReason(error), 500);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
//# sourceMappingURL=project-service.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type LocalConsoleRuntimeEndpoint = {
|
|
2
|
+
schema_version: 2;
|
|
3
|
+
instance_id: string;
|
|
4
|
+
pid: number;
|
|
5
|
+
base_url: string;
|
|
6
|
+
control_token: string;
|
|
7
|
+
version: string;
|
|
8
|
+
entrypoint: string;
|
|
9
|
+
started_at: string;
|
|
10
|
+
};
|
|
11
|
+
export type LegacyLocalConsoleRuntimeEndpoint = {
|
|
12
|
+
schema_version: 1;
|
|
13
|
+
pid: number;
|
|
14
|
+
base_url: string;
|
|
15
|
+
control_token: string;
|
|
16
|
+
started_at: string;
|
|
17
|
+
};
|
|
18
|
+
export type ReadableLocalConsoleRuntimeEndpoint = LocalConsoleRuntimeEndpoint | LegacyLocalConsoleRuntimeEndpoint;
|
|
19
|
+
export declare function createLocalConsoleSecret(): string;
|
|
20
|
+
export declare function readLocalConsoleRuntimeEndpoint(tuttiHome: string): ReadableLocalConsoleRuntimeEndpoint | null;
|
|
21
|
+
export declare function writeLocalConsoleRuntimeEndpoint(options: {
|
|
22
|
+
tuttiHome: string;
|
|
23
|
+
instanceId: string;
|
|
24
|
+
pid: number;
|
|
25
|
+
baseUrl: string;
|
|
26
|
+
controlToken: string;
|
|
27
|
+
version: string;
|
|
28
|
+
entrypoint: string;
|
|
29
|
+
now?: () => Date;
|
|
30
|
+
}): LocalConsoleRuntimeEndpoint;
|
|
31
|
+
export declare function deleteLocalConsoleRuntimeEndpoint(options: {
|
|
32
|
+
tuttiHome: string;
|
|
33
|
+
expectedControlToken?: string;
|
|
34
|
+
}): void;
|
|
35
|
+
//# sourceMappingURL=runtime-endpoint.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
function endpointPath(tuttiHome) {
|
|
5
|
+
return join(tuttiHome, "runtime", "local-console.json");
|
|
6
|
+
}
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function hasSharedEndpointFields(value) {
|
|
11
|
+
return (typeof value.pid === "number" &&
|
|
12
|
+
Number.isInteger(value.pid) &&
|
|
13
|
+
typeof value.base_url === "string" &&
|
|
14
|
+
/^http:\/\/127\.0\.0\.1:\d+$/u.test(value.base_url) &&
|
|
15
|
+
typeof value.control_token === "string" &&
|
|
16
|
+
value.control_token.length >= 32 &&
|
|
17
|
+
typeof value.started_at === "string");
|
|
18
|
+
}
|
|
19
|
+
function isEndpoint(value) {
|
|
20
|
+
if (!isRecord(value) || !hasSharedEndpointFields(value)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
if (value.schema_version === 1) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
return (value.schema_version === 2 &&
|
|
27
|
+
typeof value.instance_id === "string" &&
|
|
28
|
+
value.instance_id.length >= 16 &&
|
|
29
|
+
typeof value.version === "string" &&
|
|
30
|
+
value.version.trim() !== "" &&
|
|
31
|
+
typeof value.entrypoint === "string" &&
|
|
32
|
+
value.entrypoint.trim() !== "");
|
|
33
|
+
}
|
|
34
|
+
export function createLocalConsoleSecret() {
|
|
35
|
+
return randomBytes(32).toString("base64url");
|
|
36
|
+
}
|
|
37
|
+
export function readLocalConsoleRuntimeEndpoint(tuttiHome) {
|
|
38
|
+
const path = endpointPath(tuttiHome);
|
|
39
|
+
if (!existsSync(path)) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
44
|
+
return isEndpoint(value) ? value : null;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function writeLocalConsoleRuntimeEndpoint(options) {
|
|
51
|
+
const record = {
|
|
52
|
+
schema_version: 2,
|
|
53
|
+
instance_id: options.instanceId,
|
|
54
|
+
pid: options.pid,
|
|
55
|
+
base_url: options.baseUrl,
|
|
56
|
+
control_token: options.controlToken,
|
|
57
|
+
version: options.version,
|
|
58
|
+
entrypoint: options.entrypoint,
|
|
59
|
+
started_at: (options.now ?? (() => new Date()))().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
if (!isEndpoint(record)) {
|
|
62
|
+
throw new Error("Local Console runtime endpoint is invalid.");
|
|
63
|
+
}
|
|
64
|
+
const path = endpointPath(options.tuttiHome);
|
|
65
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
66
|
+
chmodSync(dirname(path), 0o700);
|
|
67
|
+
const temporaryPath = `${path}.${process.pid}.tmp`;
|
|
68
|
+
writeFileSync(temporaryPath, `${JSON.stringify(record, null, 2)}\n`, {
|
|
69
|
+
encoding: "utf8",
|
|
70
|
+
mode: 0o600,
|
|
71
|
+
});
|
|
72
|
+
chmodSync(temporaryPath, 0o600);
|
|
73
|
+
renameSync(temporaryPath, path);
|
|
74
|
+
chmodSync(path, 0o600);
|
|
75
|
+
return record;
|
|
76
|
+
}
|
|
77
|
+
export function deleteLocalConsoleRuntimeEndpoint(options) {
|
|
78
|
+
const path = endpointPath(options.tuttiHome);
|
|
79
|
+
if (!existsSync(path)) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (options.expectedControlToken !== undefined) {
|
|
83
|
+
const current = readLocalConsoleRuntimeEndpoint(options.tuttiHome);
|
|
84
|
+
if (current === null || current.control_token !== options.expectedControlToken) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
unlinkSync(path);
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=runtime-endpoint.js.map
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type FastifyInstance } from "fastify";
|
|
2
|
+
export declare const LOCAL_CONSOLE_API_BASE = "/local-console/v1";
|
|
3
|
+
type LocalConsoleRuntimeMetadata = {
|
|
4
|
+
instanceId: string;
|
|
5
|
+
version: string;
|
|
6
|
+
entrypoint: string;
|
|
7
|
+
startedAt: string;
|
|
8
|
+
pid: number;
|
|
9
|
+
};
|
|
10
|
+
type LocalConsoleServerOptions = {
|
|
11
|
+
controlToken: string;
|
|
12
|
+
cwd: string;
|
|
13
|
+
tuttiHome?: string;
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
now?: () => Date;
|
|
16
|
+
webStaticRoot?: string | false;
|
|
17
|
+
runtime?: LocalConsoleRuntimeMetadata;
|
|
18
|
+
onActivity?: () => void;
|
|
19
|
+
onShutdown?: () => void;
|
|
20
|
+
};
|
|
21
|
+
export declare function createLocalConsoleServer(options: LocalConsoleServerOptions): FastifyInstance;
|
|
22
|
+
export declare function runLocalConsoleProcess(options?: {
|
|
23
|
+
cwd?: string;
|
|
24
|
+
env?: NodeJS.ProcessEnv;
|
|
25
|
+
now?: () => Date;
|
|
26
|
+
idleTimeoutMs?: number;
|
|
27
|
+
}): Promise<void>;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=server.d.ts.map
|