@reasonlayer/sdk 0.0.1
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/LICENSE +201 -0
- package/README.md +35 -0
- package/bin/rl.mjs +75 -0
- package/dist/builder.d.ts +38 -0
- package/dist/builder.js +41 -0
- package/dist/bundler/index.d.ts +32 -0
- package/dist/bundler/index.js +143 -0
- package/dist/bundler/lint.d.ts +13 -0
- package/dist/bundler/lint.js +74 -0
- package/dist/bundler/node.d.ts +10 -0
- package/dist/bundler/node.js +55 -0
- package/dist/bundler/smoke.d.ts +12 -0
- package/dist/bundler/smoke.js +70 -0
- package/dist/bundler/worker.d.ts +31 -0
- package/dist/bundler/worker.js +214 -0
- package/dist/cli/args.d.ts +9 -0
- package/dist/cli/args.js +71 -0
- package/dist/cli/config.d.ts +38 -0
- package/dist/cli/config.js +94 -0
- package/dist/cli/convex.d.ts +9 -0
- package/dist/cli/convex.js +25 -0
- package/dist/cli/format.d.ts +11 -0
- package/dist/cli/format.js +140 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +721 -0
- package/dist/cli/keychain.d.ts +21 -0
- package/dist/cli/keychain.js +100 -0
- package/dist/cli/login-browser.d.ts +22 -0
- package/dist/cli/login-browser.js +106 -0
- package/dist/cli/mcp.d.ts +3 -0
- package/dist/cli/mcp.js +36 -0
- package/dist/cli/operations.d.ts +106 -0
- package/dist/cli/operations.js +623 -0
- package/dist/cli/sdk-package.d.ts +18 -0
- package/dist/cli/sdk-package.js +45 -0
- package/dist/cli/storage-upload.d.ts +9 -0
- package/dist/cli/storage-upload.js +21 -0
- package/dist/cli/watcher-compile.d.ts +1 -0
- package/dist/cli/watcher-compile.js +20 -0
- package/dist/cli/watcher-process.d.ts +1 -0
- package/dist/cli/watcher-process.js +312 -0
- package/dist/cli/worker-host.d.ts +10 -0
- package/dist/cli/worker-host.js +120 -0
- package/dist/cli/workspace-files.d.ts +12 -0
- package/dist/cli/workspace-files.js +91 -0
- package/dist/cli/workspace-local.d.ts +27 -0
- package/dist/cli/workspace-local.js +310 -0
- package/dist/cli/workspace-session.d.ts +13 -0
- package/dist/cli/workspace-session.js +12 -0
- package/dist/compile.d.ts +27 -0
- package/dist/compile.js +834 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +5 -0
- package/dist/schedule-validation.d.ts +1 -0
- package/dist/schedule-validation.js +16 -0
- package/dist/selector-schema.d.ts +26 -0
- package/dist/selector-schema.js +780 -0
- package/dist/selectors.d.ts +25 -0
- package/dist/selectors.js +99 -0
- package/dist/testkit.d.ts +32 -0
- package/dist/testkit.js +161 -0
- package/dist/types.d.ts +184 -0
- package/dist/types.js +110 -0
- package/node_modules/@reasonlayer/integrations/dist/catalog.d.ts +4937 -0
- package/node_modules/@reasonlayer/integrations/dist/catalog.js +20 -0
- package/node_modules/@reasonlayer/integrations/dist/generated/slack.d.ts +3014 -0
- package/node_modules/@reasonlayer/integrations/dist/generated/slack.js +3101 -0
- package/node_modules/@reasonlayer/integrations/dist/index.d.ts +2 -0
- package/node_modules/@reasonlayer/integrations/dist/index.js +1 -0
- package/node_modules/@reasonlayer/integrations/dist/internal/definitions.d.ts +44 -0
- package/node_modules/@reasonlayer/integrations/dist/internal/definitions.js +52 -0
- package/node_modules/@reasonlayer/integrations/dist/internal/transport.d.ts +7 -0
- package/node_modules/@reasonlayer/integrations/dist/internal/transport.js +48 -0
- package/node_modules/@reasonlayer/integrations/dist/testkit.d.ts +1 -0
- package/node_modules/@reasonlayer/integrations/dist/testkit.js +1 -0
- package/node_modules/@reasonlayer/integrations/package.json +26 -0
- package/node_modules/@reasonlayer/protocol/dist/dispatch.d.ts +10 -0
- package/node_modules/@reasonlayer/protocol/dist/dispatch.js +58 -0
- package/node_modules/@reasonlayer/protocol/dist/envelope.d.ts +9 -0
- package/node_modules/@reasonlayer/protocol/dist/envelope.js +1 -0
- package/node_modules/@reasonlayer/protocol/dist/environments.d.ts +7 -0
- package/node_modules/@reasonlayer/protocol/dist/environments.js +7 -0
- package/node_modules/@reasonlayer/protocol/dist/events.d.ts +313 -0
- package/node_modules/@reasonlayer/protocol/dist/events.js +20 -0
- package/node_modules/@reasonlayer/protocol/dist/index.d.ts +18 -0
- package/node_modules/@reasonlayer/protocol/dist/index.js +9 -0
- package/node_modules/@reasonlayer/protocol/dist/ir.d.ts +252 -0
- package/node_modules/@reasonlayer/protocol/dist/ir.js +116 -0
- package/node_modules/@reasonlayer/protocol/dist/permissions.d.ts +5 -0
- package/node_modules/@reasonlayer/protocol/dist/permissions.js +30 -0
- package/node_modules/@reasonlayer/protocol/dist/readmodels.d.ts +617 -0
- package/node_modules/@reasonlayer/protocol/dist/readmodels.js +36 -0
- package/node_modules/@reasonlayer/protocol/dist/runtime.d.ts +67 -0
- package/node_modules/@reasonlayer/protocol/dist/runtime.js +66 -0
- package/node_modules/@reasonlayer/protocol/dist/scheduling.d.ts +69 -0
- package/node_modules/@reasonlayer/protocol/dist/scheduling.js +126 -0
- package/node_modules/@reasonlayer/protocol/dist/selector.d.ts +23 -0
- package/node_modules/@reasonlayer/protocol/dist/selector.js +77 -0
- package/node_modules/@reasonlayer/protocol/dist/tasks.d.ts +69 -0
- package/node_modules/@reasonlayer/protocol/dist/tasks.js +6 -0
- package/node_modules/@reasonlayer/protocol/dist/validate.d.ts +6 -0
- package/node_modules/@reasonlayer/protocol/dist/validate.js +18 -0
- package/node_modules/@reasonlayer/protocol/package.json +21 -0
- package/node_modules/@reasonlayer/runner/dist/environment-materializer.d.ts +13 -0
- package/node_modules/@reasonlayer/runner/dist/environment-materializer.js +92 -0
- package/node_modules/@reasonlayer/runner/dist/flue-transcript.d.ts +17 -0
- package/node_modules/@reasonlayer/runner/dist/flue-transcript.js +182 -0
- package/node_modules/@reasonlayer/runner/dist/index.d.ts +184 -0
- package/node_modules/@reasonlayer/runner/dist/index.js +884 -0
- package/node_modules/@reasonlayer/runner/dist/mcp-supervisor.d.ts +15 -0
- package/node_modules/@reasonlayer/runner/dist/mcp-supervisor.js +142 -0
- package/node_modules/@reasonlayer/runner/dist/sandbox-env.d.ts +36 -0
- package/node_modules/@reasonlayer/runner/dist/sandbox-env.js +171 -0
- package/node_modules/@reasonlayer/runner/dist/task-child.d.ts +1 -0
- package/node_modules/@reasonlayer/runner/dist/task-child.js +72 -0
- package/node_modules/@reasonlayer/runner/package.json +21 -0
- package/package.json +65 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface StorageUpload {
|
|
2
|
+
storageId: string;
|
|
3
|
+
sha256: string;
|
|
4
|
+
size: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function uploadStorage(requestUploadUrl: () => Promise<string>, bytes: Uint8Array, options: {
|
|
7
|
+
contentType?: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}): Promise<StorageUpload>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export async function uploadStorage(requestUploadUrl, bytes, options) {
|
|
3
|
+
const uploadUrl = await requestUploadUrl();
|
|
4
|
+
const response = await fetch(uploadUrl, {
|
|
5
|
+
method: "POST",
|
|
6
|
+
headers: { "content-type": options.contentType ?? "application/octet-stream" },
|
|
7
|
+
body: bytes,
|
|
8
|
+
});
|
|
9
|
+
if (!response.ok) {
|
|
10
|
+
throw new Error(`storage upload failed for ${options.label} (HTTP ${response.status}): ${await response.text()}`);
|
|
11
|
+
}
|
|
12
|
+
const result = await response.json();
|
|
13
|
+
if (!result.storageId) {
|
|
14
|
+
throw new Error(`storage upload response for ${options.label} did not include storageId`);
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
storageId: result.storageId,
|
|
18
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
19
|
+
size: bytes.byteLength,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { buildWorkflow } from "../bundler/index.js";
|
|
2
|
+
const [entry, outDir] = process.argv.slice(2);
|
|
3
|
+
if (!entry || !outDir) {
|
|
4
|
+
process.stderr.write("watcher compiler requires <entry> <out-dir>\n");
|
|
5
|
+
process.exitCode = 2;
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
try {
|
|
9
|
+
const result = await buildWorkflow(entry, outDir);
|
|
10
|
+
process.stdout.write(`${JSON.stringify(result.ok
|
|
11
|
+
? { ok: true, manifest: result.manifest, flueVersion: result.flueVersion }
|
|
12
|
+
: { ok: false, errors: result.errors })}\n`);
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
process.stdout.write(`${JSON.stringify({
|
|
16
|
+
ok: false,
|
|
17
|
+
errors: [{ message: error instanceof Error ? error.message : String(error) }],
|
|
18
|
+
})}\n`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { constants, watch } from "node:fs";
|
|
4
|
+
import { access, readFile, rm, symlink } from "node:fs/promises";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
import { FN } from "@reasonlayer/protocol";
|
|
8
|
+
import { RlConvexClient } from "./convex.js";
|
|
9
|
+
import { uploadStorage } from "./storage-upload.js";
|
|
10
|
+
import { parseLocalWorkspaceSession, } from "./workspace-session.js";
|
|
11
|
+
import { changedWorkspaceFiles, deletedWorkspacePaths, materializeWorkspaceSnapshot, refreshWorkspacePaths, scanWorkspace, workspaceManifest, } from "./workspace-files.js";
|
|
12
|
+
const DEBOUNCE_MS = 150;
|
|
13
|
+
const HEARTBEAT_MS = 30_000;
|
|
14
|
+
const COMPILE_TIMEOUT_MS = 30_000;
|
|
15
|
+
const IDLE_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
|
16
|
+
async function exists(path) {
|
|
17
|
+
try {
|
|
18
|
+
await access(path, constants.F_OK);
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function checkoutSdkVersion(checkoutPath) {
|
|
26
|
+
const manifestPath = join(checkoutPath, "node_modules", "@reasonlayer", "sdk", "package.json");
|
|
27
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
28
|
+
if (typeof manifest.version !== "string" || !manifest.version) {
|
|
29
|
+
throw new Error(`checkout SDK has no valid version in ${manifestPath}`);
|
|
30
|
+
}
|
|
31
|
+
return manifest.version;
|
|
32
|
+
}
|
|
33
|
+
async function upload(client, state, bytes, contentType = "application/octet-stream") {
|
|
34
|
+
return await uploadStorage(async () => await client.mutation(FN.workspacesGenerateUploadUrl, {
|
|
35
|
+
token: state.authoringToken,
|
|
36
|
+
sessionId: state.sessionId,
|
|
37
|
+
}), bytes, { contentType, label: "workspace artifact" });
|
|
38
|
+
}
|
|
39
|
+
async function mirrorChanges(client, state, changed, deleted) {
|
|
40
|
+
const missing = new Set(await client.query(FN.workspacesMissingBlobs, {
|
|
41
|
+
token: state.authoringToken,
|
|
42
|
+
sessionId: state.sessionId,
|
|
43
|
+
sha256: changed.map((file) => file.sha256),
|
|
44
|
+
}));
|
|
45
|
+
const storageByHash = new Map();
|
|
46
|
+
for (const file of changed) {
|
|
47
|
+
if (!missing.has(file.sha256) || storageByHash.has(file.sha256))
|
|
48
|
+
continue;
|
|
49
|
+
storageByHash.set(file.sha256, (await upload(client, state, file.bytes)).storageId);
|
|
50
|
+
}
|
|
51
|
+
await client.mutation(FN.workspacesMirror, {
|
|
52
|
+
token: state.authoringToken,
|
|
53
|
+
sessionId: state.sessionId,
|
|
54
|
+
files: changed.map((file) => ({
|
|
55
|
+
path: file.path,
|
|
56
|
+
sha256: file.sha256,
|
|
57
|
+
size: file.size,
|
|
58
|
+
...(storageByHash.get(file.sha256) ? { storageId: storageByHash.get(file.sha256) } : {}),
|
|
59
|
+
})),
|
|
60
|
+
deletedPaths: deleted,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async function compileChild(entry, outDir) {
|
|
64
|
+
const script = join(dirname(fileURLToPath(import.meta.url)), "watcher-compile.js");
|
|
65
|
+
return await new Promise((resolveCompile) => {
|
|
66
|
+
const child = spawn(process.execPath, [script, entry, outDir], {
|
|
67
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
68
|
+
});
|
|
69
|
+
let stdout = "";
|
|
70
|
+
let stderr = "";
|
|
71
|
+
let timedOut = false;
|
|
72
|
+
const timer = setTimeout(() => {
|
|
73
|
+
timedOut = true;
|
|
74
|
+
child.kill("SIGKILL");
|
|
75
|
+
}, COMPILE_TIMEOUT_MS);
|
|
76
|
+
child.stdout.on("data", (chunk) => (stdout += String(chunk)));
|
|
77
|
+
child.stderr.on("data", (chunk) => (stderr += String(chunk)));
|
|
78
|
+
child.on("error", (error) => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
resolveCompile({ ok: false, errors: [{ message: `compiler child crashed: ${error.message}` }] });
|
|
81
|
+
});
|
|
82
|
+
child.on("close", () => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
if (timedOut) {
|
|
85
|
+
resolveCompile({ ok: false, errors: [{ message: `compiler exceeded ${COMPILE_TIMEOUT_MS}ms wall-clock limit` }] });
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
resolveCompile(JSON.parse(stdout.trim()));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
resolveCompile({
|
|
93
|
+
ok: false,
|
|
94
|
+
errors: [{ message: `compiler child exited without a result${stderr ? `: ${stderr.trim()}` : ""}` }],
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
async function bundleParts(client, state, outDir, artifacts) {
|
|
101
|
+
const local = await Promise.all([
|
|
102
|
+
["steps", artifacts.steps],
|
|
103
|
+
...(artifacts.agents ? [["agents", artifacts.agents]] : []),
|
|
104
|
+
].map(async ([label, name]) => {
|
|
105
|
+
const bytes = await readFile(join(outDir, name));
|
|
106
|
+
return {
|
|
107
|
+
label,
|
|
108
|
+
bytes,
|
|
109
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
110
|
+
size: bytes.byteLength,
|
|
111
|
+
};
|
|
112
|
+
}));
|
|
113
|
+
const available = await client.query(FN.workspacesAvailableBundles, {
|
|
114
|
+
token: state.authoringToken,
|
|
115
|
+
sessionId: state.sessionId,
|
|
116
|
+
sha256: local.map((item) => item.sha256),
|
|
117
|
+
});
|
|
118
|
+
const existing = new Map(available.map((item) => [item.sha256, item]));
|
|
119
|
+
const result = [];
|
|
120
|
+
for (const item of local) {
|
|
121
|
+
const retained = existing.get(item.sha256);
|
|
122
|
+
result.push({
|
|
123
|
+
label: item.label,
|
|
124
|
+
sha256: item.sha256,
|
|
125
|
+
storageId: retained?.storageId ?? (await upload(client, state, item.bytes, "application/javascript")).storageId,
|
|
126
|
+
size: retained?.size ?? item.size,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
async function compileAndRecord(client, state, files, sdkVersion) {
|
|
132
|
+
const operationId = randomUUID();
|
|
133
|
+
const snapshotRoot = join(state.checkoutPath, ".rl", `snapshot-${operationId}`);
|
|
134
|
+
const outDir = join(state.checkoutPath, ".rl", `build-${operationId}`);
|
|
135
|
+
try {
|
|
136
|
+
await materializeWorkspaceSnapshot(files, snapshotRoot);
|
|
137
|
+
await symlink(join(state.checkoutPath, "node_modules"), join(snapshotRoot, "node_modules"), "dir");
|
|
138
|
+
const entry = join(snapshotRoot, "src", "workflow.ts");
|
|
139
|
+
const result = await compileChild(entry, outDir);
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
await client.mutation(FN.workspacesRecordCompileError, {
|
|
142
|
+
token: state.authoringToken,
|
|
143
|
+
sessionId: state.sessionId,
|
|
144
|
+
errors: result.errors,
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const irJson = await readFile(join(outDir, result.manifest.artifacts.ir), "utf8");
|
|
149
|
+
const bundle = await bundleParts(client, state, outDir, result.manifest.artifacts);
|
|
150
|
+
await client.mutation(FN.workspacesRecordCompile, {
|
|
151
|
+
token: state.authoringToken,
|
|
152
|
+
sessionId: state.sessionId,
|
|
153
|
+
manifest: workspaceManifest(files),
|
|
154
|
+
irJson,
|
|
155
|
+
bundle,
|
|
156
|
+
codeStepIds: result.manifest.codeStepIds,
|
|
157
|
+
sdkVersion,
|
|
158
|
+
...(result.flueVersion !== undefined ? { flueVersion: result.flueVersion } : {}),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
await Promise.all([
|
|
163
|
+
rm(snapshotRoot, { recursive: true, force: true }),
|
|
164
|
+
rm(outDir, { recursive: true, force: true }),
|
|
165
|
+
]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function watchesWorkspacePath(filename) {
|
|
169
|
+
if (filename === null)
|
|
170
|
+
return true;
|
|
171
|
+
const [root] = String(filename).split(/[\\/]/);
|
|
172
|
+
return root !== ".rl" && root !== "node_modules" && root !== "dist";
|
|
173
|
+
}
|
|
174
|
+
function workspaceChanges(root) {
|
|
175
|
+
let fullScan = true;
|
|
176
|
+
const paths = new Set();
|
|
177
|
+
let waiter;
|
|
178
|
+
let timer;
|
|
179
|
+
const release = () => {
|
|
180
|
+
if (!waiter)
|
|
181
|
+
return;
|
|
182
|
+
if (timer)
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
timer = setTimeout(() => {
|
|
185
|
+
timer = undefined;
|
|
186
|
+
const resolveWait = waiter;
|
|
187
|
+
waiter = undefined;
|
|
188
|
+
const batch = fullScan ? null : new Set(paths);
|
|
189
|
+
fullScan = false;
|
|
190
|
+
paths.clear();
|
|
191
|
+
resolveWait?.(batch);
|
|
192
|
+
}, DEBOUNCE_MS);
|
|
193
|
+
};
|
|
194
|
+
const watcher = watch(root, { recursive: true }, (_event, filename) => {
|
|
195
|
+
if (!watchesWorkspacePath(filename))
|
|
196
|
+
return;
|
|
197
|
+
if (filename === null)
|
|
198
|
+
fullScan = true;
|
|
199
|
+
else
|
|
200
|
+
paths.add(String(filename));
|
|
201
|
+
release();
|
|
202
|
+
});
|
|
203
|
+
return {
|
|
204
|
+
async wait() {
|
|
205
|
+
return await new Promise((resolveWait) => {
|
|
206
|
+
waiter = resolveWait;
|
|
207
|
+
if (fullScan || paths.size > 0)
|
|
208
|
+
release();
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
wake() {
|
|
212
|
+
fullScan = true;
|
|
213
|
+
release();
|
|
214
|
+
},
|
|
215
|
+
close() {
|
|
216
|
+
if (timer)
|
|
217
|
+
clearTimeout(timer);
|
|
218
|
+
watcher.close();
|
|
219
|
+
waiter?.(null);
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
async function main(sessionPath) {
|
|
224
|
+
const state = parseLocalWorkspaceSession(await readFile(sessionPath, "utf8"), sessionPath);
|
|
225
|
+
const sdkVersion = await checkoutSdkVersion(state.checkoutPath);
|
|
226
|
+
const client = new RlConvexClient({ convexUrl: state.convexUrl, token: state.authoringToken });
|
|
227
|
+
let stopped = false;
|
|
228
|
+
let drainRequested = false;
|
|
229
|
+
let lastError;
|
|
230
|
+
const changes = workspaceChanges(state.checkoutPath);
|
|
231
|
+
const stop = () => {
|
|
232
|
+
stopped = true;
|
|
233
|
+
changes.wake();
|
|
234
|
+
};
|
|
235
|
+
const drainAndStop = () => {
|
|
236
|
+
drainRequested = true;
|
|
237
|
+
changes.wake();
|
|
238
|
+
};
|
|
239
|
+
process.once("SIGTERM", drainAndStop);
|
|
240
|
+
process.once("SIGINT", drainAndStop);
|
|
241
|
+
let previous = new Map();
|
|
242
|
+
let lastActivityAt = Date.now();
|
|
243
|
+
const heartbeat = setInterval(() => {
|
|
244
|
+
if (Date.now() - lastActivityAt >= IDLE_TIMEOUT_MS) {
|
|
245
|
+
void client.mutation(FN.workspacesEndSession, {
|
|
246
|
+
token: state.authoringToken,
|
|
247
|
+
sessionId: state.sessionId,
|
|
248
|
+
}).finally(drainAndStop);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
void client.mutation(FN.workspacesHeartbeat, {
|
|
252
|
+
token: state.authoringToken,
|
|
253
|
+
sessionId: state.sessionId,
|
|
254
|
+
...(lastError ? { error: lastError } : {}),
|
|
255
|
+
}).then((result) => {
|
|
256
|
+
if (result.ended)
|
|
257
|
+
drainAndStop();
|
|
258
|
+
}).catch((error) => {
|
|
259
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
260
|
+
if (lastError.includes("authoring session expired"))
|
|
261
|
+
drainAndStop();
|
|
262
|
+
});
|
|
263
|
+
}, HEARTBEAT_MS);
|
|
264
|
+
heartbeat.unref();
|
|
265
|
+
try {
|
|
266
|
+
while (!stopped && await exists(state.checkoutPath)) {
|
|
267
|
+
const changedPaths = await changes.wait();
|
|
268
|
+
if (stopped || !await exists(state.checkoutPath))
|
|
269
|
+
break;
|
|
270
|
+
try {
|
|
271
|
+
const current = changedPaths === null
|
|
272
|
+
? await scanWorkspace(state.checkoutPath)
|
|
273
|
+
: await refreshWorkspacePaths(state.checkoutPath, previous, changedPaths);
|
|
274
|
+
const changed = changedWorkspaceFiles(current, previous);
|
|
275
|
+
const deleted = deletedWorkspacePaths(current, previous);
|
|
276
|
+
if (changed.length || deleted.length) {
|
|
277
|
+
lastActivityAt = Date.now();
|
|
278
|
+
await mirrorChanges(client, state, changed, deleted);
|
|
279
|
+
await compileAndRecord(client, state, current, sdkVersion);
|
|
280
|
+
previous = current;
|
|
281
|
+
}
|
|
282
|
+
lastError = undefined;
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
286
|
+
process.stderr.write(`${new Date().toISOString()} ${lastError}\n`);
|
|
287
|
+
changes.wake();
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (drainRequested)
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
finally {
|
|
295
|
+
clearInterval(heartbeat);
|
|
296
|
+
changes.close();
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const sessionPath = process.argv[2];
|
|
300
|
+
if (!sessionPath) {
|
|
301
|
+
process.stderr.write("watcher daemon requires a session path\n");
|
|
302
|
+
process.exitCode = 2;
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
try {
|
|
306
|
+
await main(sessionPath);
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
process.stderr.write(`watcher daemon: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
310
|
+
process.exitCode = 1;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
const SKEW_MS = 5 * 60 * 1000;
|
|
8
|
+
function json(res, status, body) {
|
|
9
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
10
|
+
res.end(JSON.stringify(body));
|
|
11
|
+
}
|
|
12
|
+
async function readBody(req) {
|
|
13
|
+
const chunks = [];
|
|
14
|
+
for await (const chunk of req) {
|
|
15
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
16
|
+
}
|
|
17
|
+
return Buffer.concat(chunks);
|
|
18
|
+
}
|
|
19
|
+
function validSignature(body, headers, secret) {
|
|
20
|
+
const ts = headers["x-rl-timestamp"];
|
|
21
|
+
const sig = headers["x-rl-signature"];
|
|
22
|
+
if (typeof ts !== "string" || typeof sig !== "string")
|
|
23
|
+
return false;
|
|
24
|
+
const timestamp = Number(ts);
|
|
25
|
+
if (!Number.isFinite(timestamp) || Math.abs(Date.now() - timestamp) > SKEW_MS)
|
|
26
|
+
return false;
|
|
27
|
+
const expected = createHmac("sha256", secret).update(`${ts}.${body.toString("utf8")}`).digest("hex");
|
|
28
|
+
try {
|
|
29
|
+
const actualBytes = Buffer.from(sig, "hex");
|
|
30
|
+
const expectedBytes = Buffer.from(expected, "hex");
|
|
31
|
+
return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function startWorkerHost(options) {
|
|
38
|
+
const secret = options.secret ?? process.env["RL_DISPATCH_SECRET"] ?? randomBytes(32).toString("hex");
|
|
39
|
+
const cache = new Map();
|
|
40
|
+
async function loadWorker(scriptUrl) {
|
|
41
|
+
let cached = cache.get(scriptUrl);
|
|
42
|
+
if (!cached) {
|
|
43
|
+
cached = (async () => {
|
|
44
|
+
const fetched = await fetch(scriptUrl);
|
|
45
|
+
if (!fetched.ok) {
|
|
46
|
+
throw new Error(`cannot fetch worker bundle (HTTP ${fetched.status}): ${await fetched.text()}`);
|
|
47
|
+
}
|
|
48
|
+
const dir = await mkdtemp(join(tmpdir(), "rlx-worker-host-"));
|
|
49
|
+
const file = join(dir, "worker.mjs");
|
|
50
|
+
await writeFile(file, await fetched.text());
|
|
51
|
+
return (await import(pathToFileURL(file).href));
|
|
52
|
+
})().catch((err) => {
|
|
53
|
+
cache.delete(scriptUrl);
|
|
54
|
+
throw err;
|
|
55
|
+
});
|
|
56
|
+
cache.set(scriptUrl, cached);
|
|
57
|
+
}
|
|
58
|
+
return await cached;
|
|
59
|
+
}
|
|
60
|
+
const server = createServer(async (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
if (req.method === "GET" && req.url === "/health") {
|
|
63
|
+
json(res, 200, { ok: true });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (req.method !== "POST") {
|
|
67
|
+
json(res, 405, { error: "POST only" });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const rawBody = await readBody(req);
|
|
71
|
+
if (!validSignature(rawBody, req.headers, secret)) {
|
|
72
|
+
json(res, 401, { error: "invalid Reasonlayer dispatch signature" });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const body = JSON.parse(rawBody.toString("utf8"));
|
|
76
|
+
if (!body.scriptUrl || !body.stepId) {
|
|
77
|
+
json(res, 400, { error: "request must include scriptUrl and stepId" });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const worker = await loadWorker(body.scriptUrl);
|
|
81
|
+
const workerRes = await worker.default.fetch(new Request("http://rlx-worker-host/invoke", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "content-type": "application/json" },
|
|
84
|
+
body: JSON.stringify({
|
|
85
|
+
stepId: body.stepId,
|
|
86
|
+
input: body.input,
|
|
87
|
+
env: body.env ?? {},
|
|
88
|
+
}),
|
|
89
|
+
}));
|
|
90
|
+
const text = await workerRes.text();
|
|
91
|
+
res.writeHead(workerRes.status, {
|
|
92
|
+
"content-type": workerRes.headers.get("content-type") ?? "application/json",
|
|
93
|
+
});
|
|
94
|
+
res.end(text);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
const failed = {
|
|
98
|
+
status: "failed",
|
|
99
|
+
error: { message: err instanceof Error ? err.message : String(err) },
|
|
100
|
+
};
|
|
101
|
+
json(res, 500, failed);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
105
|
+
server.once("error", rejectListen);
|
|
106
|
+
server.listen(options.port, options.host, () => {
|
|
107
|
+
server.off("error", rejectListen);
|
|
108
|
+
resolveListen();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
const address = server.address();
|
|
112
|
+
const port = typeof address === "object" && address ? address.port : options.port;
|
|
113
|
+
return {
|
|
114
|
+
url: `http://${options.host}:${port}`,
|
|
115
|
+
secret,
|
|
116
|
+
close: () => new Promise((resolveClose, rejectClose) => {
|
|
117
|
+
server.close((err) => (err ? rejectClose(err) : resolveClose()));
|
|
118
|
+
}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type WorkspaceFileState = {
|
|
2
|
+
path: string;
|
|
3
|
+
sha256: string;
|
|
4
|
+
size: number;
|
|
5
|
+
bytes: Buffer;
|
|
6
|
+
};
|
|
7
|
+
export declare function scanWorkspace(root: string): Promise<Map<string, WorkspaceFileState>>;
|
|
8
|
+
export declare function refreshWorkspacePaths(root: string, previous: Map<string, WorkspaceFileState>, paths: Iterable<string>): Promise<Map<string, WorkspaceFileState>>;
|
|
9
|
+
export declare function materializeWorkspaceSnapshot(files: Map<string, WorkspaceFileState>, target: string): Promise<void>;
|
|
10
|
+
export declare function changedWorkspaceFiles(current: Map<string, WorkspaceFileState>, previous: Map<string, WorkspaceFileState>): WorkspaceFileState[];
|
|
11
|
+
export declare function deletedWorkspacePaths(current: Map<string, WorkspaceFileState>, previous: Map<string, WorkspaceFileState>): string[];
|
|
12
|
+
export declare function workspaceManifest(files: Map<string, WorkspaceFileState>): Record<string, string>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
+
const IGNORED = new Set([".git", ".rl", "dist", "node_modules"]);
|
|
5
|
+
function ignored(path) {
|
|
6
|
+
return IGNORED.has(path.split(/[\\/]/)[0]);
|
|
7
|
+
}
|
|
8
|
+
async function readWorkspaceFile(root, absolute) {
|
|
9
|
+
const bytes = await readFile(absolute);
|
|
10
|
+
const path = relative(root, absolute).split("\\").join("/");
|
|
11
|
+
return {
|
|
12
|
+
path,
|
|
13
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
14
|
+
size: bytes.byteLength,
|
|
15
|
+
bytes,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
async function scanDirectory(root, directory, files) {
|
|
19
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (IGNORED.has(entry.name))
|
|
22
|
+
continue;
|
|
23
|
+
const absolute = join(directory, entry.name);
|
|
24
|
+
if (entry.isDirectory())
|
|
25
|
+
await scanDirectory(root, absolute, files);
|
|
26
|
+
else if (entry.isFile()) {
|
|
27
|
+
const file = await readWorkspaceFile(root, absolute);
|
|
28
|
+
files.set(file.path, file);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function scanWorkspace(root) {
|
|
33
|
+
const files = new Map();
|
|
34
|
+
await scanDirectory(root, root, files);
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
export async function refreshWorkspacePaths(root, previous, paths) {
|
|
38
|
+
const current = new Map(previous);
|
|
39
|
+
for (const rawPath of paths) {
|
|
40
|
+
const path = rawPath.split("\\").join("/");
|
|
41
|
+
if (!path || ignored(path))
|
|
42
|
+
continue;
|
|
43
|
+
const absolute = resolve(root, path);
|
|
44
|
+
const withinRoot = relative(resolve(root), absolute);
|
|
45
|
+
if (!withinRoot || withinRoot.startsWith("..") || isAbsolute(withinRoot))
|
|
46
|
+
continue;
|
|
47
|
+
const prefix = `${path.replace(/\/$/, "")}/`;
|
|
48
|
+
let stat;
|
|
49
|
+
try {
|
|
50
|
+
stat = await lstat(absolute);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.code !== "ENOENT")
|
|
54
|
+
throw error;
|
|
55
|
+
current.delete(path);
|
|
56
|
+
for (const existing of current.keys()) {
|
|
57
|
+
if (existing.startsWith(prefix))
|
|
58
|
+
current.delete(existing);
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (stat.isDirectory()) {
|
|
63
|
+
for (const existing of current.keys()) {
|
|
64
|
+
if (existing.startsWith(prefix))
|
|
65
|
+
current.delete(existing);
|
|
66
|
+
}
|
|
67
|
+
await scanDirectory(root, absolute, current);
|
|
68
|
+
}
|
|
69
|
+
else if (stat.isFile()) {
|
|
70
|
+
const file = await readWorkspaceFile(root, absolute);
|
|
71
|
+
current.set(file.path, file);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return current;
|
|
75
|
+
}
|
|
76
|
+
export async function materializeWorkspaceSnapshot(files, target) {
|
|
77
|
+
await Promise.all([...files.values()].map(async (file) => {
|
|
78
|
+
const path = join(target, file.path);
|
|
79
|
+
await mkdir(dirname(path), { recursive: true });
|
|
80
|
+
await writeFile(path, file.bytes);
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
export function changedWorkspaceFiles(current, previous) {
|
|
84
|
+
return [...current.values()].filter((file) => previous.get(file.path)?.sha256 !== file.sha256);
|
|
85
|
+
}
|
|
86
|
+
export function deletedWorkspacePaths(current, previous) {
|
|
87
|
+
return [...previous.keys()].filter((path) => !current.has(path));
|
|
88
|
+
}
|
|
89
|
+
export function workspaceManifest(files) {
|
|
90
|
+
return Object.fromEntries([...files.values()].map((file) => [file.path, file.sha256]));
|
|
91
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { OperationRuntime } from "./operations.js";
|
|
2
|
+
import { type LocalWorkspaceSession } from "./workspace-session.js";
|
|
3
|
+
export type { LocalWorkspaceSession } from "./workspace-session.js";
|
|
4
|
+
export interface StartWorkspaceResult {
|
|
5
|
+
sessionId: string;
|
|
6
|
+
workflowId: string;
|
|
7
|
+
authoringToken: string;
|
|
8
|
+
expiresAt: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function startDaemon(sessionPath: string): Promise<number>;
|
|
11
|
+
export declare function createCheckout(runtime: OperationRuntime, options: {
|
|
12
|
+
workflowId: string;
|
|
13
|
+
mode: "init" | "pull";
|
|
14
|
+
targetDir?: string;
|
|
15
|
+
}): Promise<{
|
|
16
|
+
checkoutPath: string;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
daemonPid: number;
|
|
19
|
+
files: number;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function findCheckoutRoot(cwd?: string): Promise<string>;
|
|
22
|
+
export declare function readLocalSession(cwd?: string): Promise<LocalWorkspaceSession>;
|
|
23
|
+
export declare function endLocalSession(runtime: OperationRuntime): Promise<{
|
|
24
|
+
workflowId: string;
|
|
25
|
+
sessionId: string;
|
|
26
|
+
checkoutPath: string;
|
|
27
|
+
}>;
|