@rigkit/runtime-client 0.2.0 → 0.2.2
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/package.json +1 -1
- package/src/api.ts +1 -0
- package/src/index.ts +1 -0
- package/src/manager.test.ts +119 -3
- package/src/manager.ts +171 -14
- package/src/schemas.ts +2 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -14,6 +14,7 @@ const OptionalString = Schema.optional(Schema.String);
|
|
|
14
14
|
export const RuntimeControlHealthEffectSchema = Schema.Struct({
|
|
15
15
|
ok: Schema.Boolean,
|
|
16
16
|
projectId: Schema.String,
|
|
17
|
+
runtimeFingerprint: OptionalString,
|
|
17
18
|
projectDir: Schema.String,
|
|
18
19
|
configPath: Schema.String,
|
|
19
20
|
statePath: OptionalString,
|
package/src/index.ts
CHANGED
package/src/manager.test.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
6
6
|
connectRemoteRuntime,
|
|
7
7
|
getOrStartRuntime,
|
|
8
8
|
projectIdFor,
|
|
9
|
+
runtimeFingerprintFor,
|
|
9
10
|
runtimePaths,
|
|
10
11
|
SUPPORTED_RUNTIME_API_VERSION,
|
|
11
12
|
} from "./manager.ts";
|
|
@@ -42,7 +43,7 @@ describe("runtime manager", () => {
|
|
|
42
43
|
expect(differentSource).not.toBe(first);
|
|
43
44
|
});
|
|
44
45
|
|
|
45
|
-
test("
|
|
46
|
+
test("keeps project ids stable while config fingerprints change", () => {
|
|
46
47
|
const root = mkdtempSync(join(tmpdir(), "rigkit-runtime-client-id-"));
|
|
47
48
|
try {
|
|
48
49
|
const projectDir = join(root, "project");
|
|
@@ -52,16 +53,62 @@ describe("runtime manager", () => {
|
|
|
52
53
|
|
|
53
54
|
const first = projectIdFor({ projectDir, configPath });
|
|
54
55
|
const second = projectIdFor({ projectDir, configPath });
|
|
56
|
+
const firstFingerprint = runtimeFingerprintFor({ projectDir, configPath });
|
|
55
57
|
writeFileSync(configPath, "export default { name: 'two' }\n");
|
|
56
58
|
const changed = projectIdFor({ projectDir, configPath });
|
|
59
|
+
const changedFingerprint = runtimeFingerprintFor({ projectDir, configPath });
|
|
57
60
|
|
|
58
61
|
expect(second).toBe(first);
|
|
59
|
-
expect(changed).
|
|
62
|
+
expect(changed).toBe(first);
|
|
63
|
+
expect(changedFingerprint).not.toBe(firstFingerprint);
|
|
60
64
|
} finally {
|
|
61
65
|
rmSync(root, { recursive: true, force: true });
|
|
62
66
|
}
|
|
63
67
|
});
|
|
64
68
|
|
|
69
|
+
test("restarts local runtimes when the runtime fingerprint changes", async () => {
|
|
70
|
+
const root = mkdtempSync(join(tmpdir(), "rigkit-runtime-client-restart-"));
|
|
71
|
+
let first: Awaited<ReturnType<typeof getOrStartRuntime>> | undefined;
|
|
72
|
+
let second: Awaited<ReturnType<typeof getOrStartRuntime>> | undefined;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const projectDir = join(root, "project");
|
|
76
|
+
const rigkitHome = join(root, "home");
|
|
77
|
+
const configPath = join(projectDir, "rig.config.ts");
|
|
78
|
+
mkdirSync(projectDir, { recursive: true });
|
|
79
|
+
writeFileSync(configPath, "export default { name: 'one' }\n");
|
|
80
|
+
writeFakeRuntimeBin(projectDir);
|
|
81
|
+
|
|
82
|
+
first = await getOrStartRuntime({
|
|
83
|
+
projectDir,
|
|
84
|
+
configPath,
|
|
85
|
+
rigkitHome,
|
|
86
|
+
idleMs: 60_000,
|
|
87
|
+
});
|
|
88
|
+
const firstHealth = await first.control.health();
|
|
89
|
+
|
|
90
|
+
writeFileSync(configPath, "export default { name: 'two' }\n");
|
|
91
|
+
second = await getOrStartRuntime({
|
|
92
|
+
projectDir,
|
|
93
|
+
configPath,
|
|
94
|
+
rigkitHome,
|
|
95
|
+
idleMs: 60_000,
|
|
96
|
+
});
|
|
97
|
+
const secondHealth = await second.control.health();
|
|
98
|
+
|
|
99
|
+
expect(second.handle.projectId).toBe(first.handle.projectId);
|
|
100
|
+
expect(second.paths.handlePath).toBe(first.paths.handlePath);
|
|
101
|
+
expect(second.handle.runtimeFingerprint).not.toBe(first.handle.runtimeFingerprint);
|
|
102
|
+
expect(second.handle.pid).not.toBe(first.handle.pid);
|
|
103
|
+
expect(secondHealth.runtimeFingerprint).toBe(second.handle.runtimeFingerprint);
|
|
104
|
+
expect(firstHealth.projectId).toBe(secondHealth.projectId);
|
|
105
|
+
} finally {
|
|
106
|
+
await second?.control.shutdown().catch(() => {});
|
|
107
|
+
await first?.control.shutdown().catch(() => {});
|
|
108
|
+
rmSync(root, { recursive: true, force: true });
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
65
112
|
test("derives handle, token, and lock paths from rigkit home", () => {
|
|
66
113
|
const paths = runtimePaths("sha256-test", "/tmp/rigkit-home");
|
|
67
114
|
|
|
@@ -229,3 +276,72 @@ describe("runtime manager", () => {
|
|
|
229
276
|
}
|
|
230
277
|
});
|
|
231
278
|
});
|
|
279
|
+
|
|
280
|
+
function writeFakeRuntimeBin(projectDir: string): void {
|
|
281
|
+
const binDir = join(projectDir, "node_modules", ".bin");
|
|
282
|
+
mkdirSync(binDir, { recursive: true });
|
|
283
|
+
const binPath = join(binDir, process.platform === "win32" ? "rigkit-project-runtime.cmd" : "rigkit-project-runtime");
|
|
284
|
+
writeFileSync(
|
|
285
|
+
binPath,
|
|
286
|
+
`#!/usr/bin/env bun
|
|
287
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
288
|
+
import { dirname, resolve } from "node:path";
|
|
289
|
+
|
|
290
|
+
const args = process.argv.slice(2);
|
|
291
|
+
const options = {};
|
|
292
|
+
for (let i = 1; i < args.length; i += 2) {
|
|
293
|
+
options[args[i].replace(/^--/, "")] = args[i + 1];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const token = readFileSync(options.token, "utf8").trim();
|
|
297
|
+
let server;
|
|
298
|
+
server = Bun.serve({
|
|
299
|
+
hostname: "127.0.0.1",
|
|
300
|
+
port: 0,
|
|
301
|
+
fetch(request) {
|
|
302
|
+
const url = new URL(request.url);
|
|
303
|
+
if (request.headers.get("authorization") !== \`Bearer \${token}\`) {
|
|
304
|
+
return Response.json({ error: { message: "Unauthorized" } }, { status: 401 });
|
|
305
|
+
}
|
|
306
|
+
if (url.pathname === "/health") {
|
|
307
|
+
return Response.json({
|
|
308
|
+
ok: true,
|
|
309
|
+
projectId: options["project-id"],
|
|
310
|
+
runtimeFingerprint: options["runtime-fingerprint"],
|
|
311
|
+
projectDir: resolve(options["project-dir"]),
|
|
312
|
+
configPath: resolve(options.config),
|
|
313
|
+
statePath: options.state ? resolve(options.state) : undefined,
|
|
314
|
+
engineVersion: "engine-test",
|
|
315
|
+
runtimeVersion: "runtime-test",
|
|
316
|
+
expiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
317
|
+
}, { headers: { "x-rigkit-api-version": "${SUPPORTED_RUNTIME_API_VERSION}" } });
|
|
318
|
+
}
|
|
319
|
+
if (url.pathname === "/shutdown") {
|
|
320
|
+
setTimeout(() => {
|
|
321
|
+
server.stop(true);
|
|
322
|
+
process.exit(0);
|
|
323
|
+
}, 0);
|
|
324
|
+
return Response.json({ ok: true }, { headers: { "x-rigkit-api-version": "${SUPPORTED_RUNTIME_API_VERSION}" } });
|
|
325
|
+
}
|
|
326
|
+
return Response.json({ error: { message: "Not found" } }, { status: 404 });
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const handle = {
|
|
331
|
+
projectId: options["project-id"],
|
|
332
|
+
runtimeFingerprint: options["runtime-fingerprint"],
|
|
333
|
+
projectDir: resolve(options["project-dir"]),
|
|
334
|
+
configPath: resolve(options.config),
|
|
335
|
+
statePath: options.state ? resolve(options.state) : undefined,
|
|
336
|
+
pid: process.pid,
|
|
337
|
+
url: \`http://127.0.0.1:\${server.port}\`,
|
|
338
|
+
tokenPath: resolve(options.token),
|
|
339
|
+
};
|
|
340
|
+
mkdirSync(dirname(options.handle), { recursive: true });
|
|
341
|
+
writeFileSync(options.handle, JSON.stringify(handle));
|
|
342
|
+
console.log(JSON.stringify({ type: "ready", url: handle.url, token }));
|
|
343
|
+
await new Promise(() => {});
|
|
344
|
+
`,
|
|
345
|
+
);
|
|
346
|
+
chmodSync(binPath, 0o755);
|
|
347
|
+
}
|
package/src/manager.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { spawn, type ChildProcessByStdio } from "node:child_process";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -93,13 +93,19 @@ async function getOrStartRuntimeUnsafe(options: GetOrStartRuntimeOptions): Promi
|
|
|
93
93
|
statePath,
|
|
94
94
|
source: options.source,
|
|
95
95
|
});
|
|
96
|
+
const runtimeFingerprint = runtimeFingerprintFor({
|
|
97
|
+
projectDir,
|
|
98
|
+
configPath,
|
|
99
|
+
statePath,
|
|
100
|
+
source: options.source,
|
|
101
|
+
});
|
|
96
102
|
const paths = runtimePaths(projectId, options.rigkitHome);
|
|
97
103
|
|
|
98
|
-
const existing = await tryExistingRuntime(paths, projectId);
|
|
104
|
+
const existing = await tryExistingRuntime(paths, projectId, runtimeFingerprint);
|
|
99
105
|
if (existing) return existing;
|
|
100
106
|
|
|
101
107
|
await withRuntimeLock(paths.lockPath, async () => {
|
|
102
|
-
const secondCheck = await tryExistingRuntime(paths, projectId);
|
|
108
|
+
const secondCheck = await tryExistingRuntime(paths, projectId, runtimeFingerprint);
|
|
103
109
|
if (secondCheck) return;
|
|
104
110
|
await startRuntime({
|
|
105
111
|
...options,
|
|
@@ -107,11 +113,12 @@ async function getOrStartRuntimeUnsafe(options: GetOrStartRuntimeOptions): Promi
|
|
|
107
113
|
configPath,
|
|
108
114
|
statePath,
|
|
109
115
|
projectId,
|
|
116
|
+
runtimeFingerprint,
|
|
110
117
|
paths,
|
|
111
118
|
});
|
|
112
119
|
});
|
|
113
120
|
|
|
114
|
-
const started = await tryExistingRuntime(paths, projectId);
|
|
121
|
+
const started = await tryExistingRuntime(paths, projectId, runtimeFingerprint);
|
|
115
122
|
if (!started) {
|
|
116
123
|
throw new RuntimeStartupError({
|
|
117
124
|
reason: "unhealthy-after-start",
|
|
@@ -128,13 +135,36 @@ export function projectIdFor(options: RuntimeProjectOptions): string {
|
|
|
128
135
|
hash.update(JSON.stringify({
|
|
129
136
|
projectDir: resolve(options.projectDir),
|
|
130
137
|
configPath,
|
|
131
|
-
configHash: configHashFor(configPath),
|
|
132
138
|
statePath: options.statePath ? resolve(options.statePath) : null,
|
|
133
139
|
source: options.source ?? null,
|
|
134
140
|
}));
|
|
135
141
|
return `sha256-${hash.digest("hex").slice(0, 32)}`;
|
|
136
142
|
}
|
|
137
143
|
|
|
144
|
+
export function runtimeFingerprintFor(options: RuntimeProjectOptions): string {
|
|
145
|
+
const projectDir = resolve(options.projectDir);
|
|
146
|
+
const configPath = resolve(options.configPath);
|
|
147
|
+
const statePath = options.statePath ? resolve(options.statePath) : null;
|
|
148
|
+
const hash = createHash("sha256");
|
|
149
|
+
|
|
150
|
+
hash.update("project\0");
|
|
151
|
+
hash.update(projectDir);
|
|
152
|
+
hash.update("\0config\0");
|
|
153
|
+
hash.update(configPath);
|
|
154
|
+
hash.update("\0state\0");
|
|
155
|
+
hash.update(statePath ?? "");
|
|
156
|
+
hash.update("\0source\0");
|
|
157
|
+
hash.update(JSON.stringify(options.source ?? null));
|
|
158
|
+
|
|
159
|
+
updateFileFingerprint(hash, "config", configPath);
|
|
160
|
+
for (const file of dotenvFilesFor(projectDir)) updateFileFingerprint(hash, "dotenv", file);
|
|
161
|
+
for (const file of projectFingerprintFiles(projectDir)) updateFileFingerprint(hash, "project-file", file);
|
|
162
|
+
updateProjectSurfaceFingerprint(hash, projectDir);
|
|
163
|
+
updateRigkitPackageFingerprint(hash, join(projectDir, "node_modules", "@rigkit"));
|
|
164
|
+
|
|
165
|
+
return `sha256-${hash.digest("hex")}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
138
168
|
export function runtimePaths(projectId: string, rigkitHome = defaultRigkitHome()): RuntimePaths {
|
|
139
169
|
const root = join(rigkitHome, "runtimes");
|
|
140
170
|
return {
|
|
@@ -149,11 +179,24 @@ export function defaultRigkitHome(): string {
|
|
|
149
179
|
return process.env.RIGKIT_HOME ? resolve(process.env.RIGKIT_HOME) : join(homedir(), ".rigkit");
|
|
150
180
|
}
|
|
151
181
|
|
|
152
|
-
async function tryExistingRuntime(
|
|
182
|
+
async function tryExistingRuntime(
|
|
183
|
+
paths: RuntimePaths,
|
|
184
|
+
projectId: string,
|
|
185
|
+
runtimeFingerprint: string,
|
|
186
|
+
): Promise<RuntimeClient | undefined> {
|
|
153
187
|
const handle = readHandle(paths.handlePath);
|
|
154
188
|
if (!handle || handle.projectId !== projectId) return undefined;
|
|
155
189
|
const token = readToken(handle.tokenPath);
|
|
156
|
-
if (!token)
|
|
190
|
+
if (!token) {
|
|
191
|
+
removeStale(paths);
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (handle.runtimeFingerprint !== runtimeFingerprint) {
|
|
196
|
+
await shutdownRuntime(handle, token);
|
|
197
|
+
removeStale(paths);
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
157
200
|
|
|
158
201
|
try {
|
|
159
202
|
const body = await createRuntimeHttpClient({ baseUrl: handle.url, token }).health();
|
|
@@ -164,6 +207,13 @@ async function tryExistingRuntime(paths: RuntimePaths, projectId: string): Promi
|
|
|
164
207
|
message: `runtime project mismatch`,
|
|
165
208
|
});
|
|
166
209
|
}
|
|
210
|
+
if (body.runtimeFingerprint !== runtimeFingerprint) {
|
|
211
|
+
throw new RuntimeConnectionError({
|
|
212
|
+
method: "GET",
|
|
213
|
+
path: "/health",
|
|
214
|
+
message: `runtime fingerprint mismatch`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
167
217
|
return createClient(handle, paths, token);
|
|
168
218
|
} catch (error) {
|
|
169
219
|
if (error instanceof RuntimeApiVersionError) throw error;
|
|
@@ -174,6 +224,7 @@ async function tryExistingRuntime(paths: RuntimePaths, projectId: string): Promi
|
|
|
174
224
|
|
|
175
225
|
async function startRuntime(input: GetOrStartRuntimeOptions & {
|
|
176
226
|
projectId: string;
|
|
227
|
+
runtimeFingerprint: string;
|
|
177
228
|
paths: RuntimePaths;
|
|
178
229
|
}): Promise<void> {
|
|
179
230
|
mkdirSync(input.paths.root, { recursive: true });
|
|
@@ -183,6 +234,8 @@ async function startRuntime(input: GetOrStartRuntimeOptions & {
|
|
|
183
234
|
"serve",
|
|
184
235
|
"--project-id",
|
|
185
236
|
input.projectId,
|
|
237
|
+
"--runtime-fingerprint",
|
|
238
|
+
input.runtimeFingerprint,
|
|
186
239
|
"--project-dir",
|
|
187
240
|
input.projectDir,
|
|
188
241
|
"--config",
|
|
@@ -355,7 +408,7 @@ function readReadyLine(proc: ChildProcessByStdio<null, Readable, null>, paths: R
|
|
|
355
408
|
reason: "startup-timeout",
|
|
356
409
|
projectDir,
|
|
357
410
|
message: `Timed out waiting for Rigkit runtime to start`,
|
|
358
|
-
}));
|
|
411
|
+
}), { kill: true });
|
|
359
412
|
}, 15_000);
|
|
360
413
|
|
|
361
414
|
const cleanup = () => {
|
|
@@ -374,10 +427,12 @@ function readReadyLine(proc: ChildProcessByStdio<null, Readable, null>, paths: R
|
|
|
374
427
|
resolvePromise(line);
|
|
375
428
|
};
|
|
376
429
|
|
|
377
|
-
function fail(error: unknown) {
|
|
430
|
+
function fail(error: unknown, options: { kill?: boolean } = {}) {
|
|
378
431
|
if (settled) return;
|
|
379
432
|
settled = true;
|
|
380
433
|
cleanup();
|
|
434
|
+
if (options.kill) killRuntimeProcess(proc);
|
|
435
|
+
removeStale(paths);
|
|
381
436
|
rejectPromise(error);
|
|
382
437
|
}
|
|
383
438
|
|
|
@@ -414,15 +469,117 @@ function readReadyLine(proc: ChildProcessByStdio<null, Readable, null>, paths: R
|
|
|
414
469
|
});
|
|
415
470
|
}
|
|
416
471
|
|
|
472
|
+
function killRuntimeProcess(proc: ChildProcessByStdio<null, Readable, null>): void {
|
|
473
|
+
if (!proc.pid) return;
|
|
474
|
+
try {
|
|
475
|
+
proc.kill("SIGTERM");
|
|
476
|
+
} catch {
|
|
477
|
+
// Best effort. The startup path will discard the stale handle either way.
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
417
481
|
function removeStale(paths: RuntimePaths): void {
|
|
418
482
|
rmSync(paths.handlePath, { force: true });
|
|
419
483
|
}
|
|
420
484
|
|
|
421
|
-
function
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
485
|
+
async function shutdownRuntime(handle: RuntimeHandle, token: string): Promise<void> {
|
|
486
|
+
try {
|
|
487
|
+
await createRuntimeHttpClient({ baseUrl: handle.url, token }).shutdown();
|
|
488
|
+
} catch {
|
|
489
|
+
if (handle.pid !== process.pid) {
|
|
490
|
+
try {
|
|
491
|
+
process.kill(handle.pid);
|
|
492
|
+
} catch {
|
|
493
|
+
// Best effort. The stale handle is still removed below.
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function updateFileFingerprint(hash: ReturnType<typeof createHash>, label: string, path: string): void {
|
|
500
|
+
hash.update(`\0${label}\0${path}\0`);
|
|
501
|
+
if (!existsSync(path)) {
|
|
502
|
+
hash.update("missing");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const stat = statSync(path);
|
|
507
|
+
if (!stat.isFile()) {
|
|
508
|
+
hash.update(`not-file:${stat.mode}`);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
hash.update(readFileSync(path));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function projectFingerprintFiles(projectDir: string): string[] {
|
|
516
|
+
return [
|
|
517
|
+
"package.json",
|
|
518
|
+
"bun.lock",
|
|
519
|
+
"bun.lockb",
|
|
520
|
+
"pnpm-lock.yaml",
|
|
521
|
+
"package-lock.json",
|
|
522
|
+
"yarn.lock",
|
|
523
|
+
].map((file) => join(projectDir, file));
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function updateProjectSurfaceFingerprint(hash: ReturnType<typeof createHash>, projectDir: string): void {
|
|
527
|
+
if (!existsSync(projectDir)) return;
|
|
528
|
+
const ignored = new Set([".git", ".rigkit", "node_modules", "dist", "build", ".next", ".astro"]);
|
|
529
|
+
const entries = readdirSync(projectDir, { withFileTypes: true })
|
|
530
|
+
.filter((entry) => !ignored.has(entry.name))
|
|
531
|
+
.map((entry) => `${entry.name}:${entry.isDirectory() ? "dir" : entry.isFile() ? "file" : "other"}`)
|
|
532
|
+
.sort();
|
|
533
|
+
hash.update("\0project-surface\0");
|
|
534
|
+
hash.update(entries.join("\n"));
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function updateRigkitPackageFingerprint(hash: ReturnType<typeof createHash>, scopeDir: string): void {
|
|
538
|
+
if (!existsSync(scopeDir)) return;
|
|
539
|
+
const packageDirs = readdirSync(scopeDir, { withFileTypes: true })
|
|
540
|
+
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
|
|
541
|
+
.map((entry) => join(scopeDir, entry.name))
|
|
542
|
+
.sort();
|
|
543
|
+
|
|
544
|
+
for (const packageDir of packageDirs) {
|
|
545
|
+
updateFileFingerprint(hash, "rigkit-package", join(packageDir, "package.json"));
|
|
546
|
+
for (const file of collectFiles(join(packageDir, "src"))) {
|
|
547
|
+
updateFileFingerprint(hash, "rigkit-source", file);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function collectFiles(root: string): string[] {
|
|
553
|
+
if (!existsSync(root)) return [];
|
|
554
|
+
const out: string[] = [];
|
|
555
|
+
const visit = (dir: string) => {
|
|
556
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
557
|
+
const path = join(dir, entry.name);
|
|
558
|
+
if (entry.isDirectory()) {
|
|
559
|
+
visit(path);
|
|
560
|
+
} else if (entry.isFile()) {
|
|
561
|
+
out.push(path);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
};
|
|
565
|
+
visit(root);
|
|
566
|
+
return out.sort();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function dotenvFilesFor(projectDir: string): string[] {
|
|
570
|
+
const files: string[] = [];
|
|
571
|
+
let current = projectDir;
|
|
572
|
+
|
|
573
|
+
while (true) {
|
|
574
|
+
const candidate = join(current, ".env");
|
|
575
|
+
if (existsSync(candidate)) files.unshift(candidate);
|
|
576
|
+
|
|
577
|
+
const parent = dirname(current);
|
|
578
|
+
if (parent === current) break;
|
|
579
|
+
current = parent;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return files;
|
|
426
583
|
}
|
|
427
584
|
|
|
428
585
|
function isFileExistsError(error: unknown): boolean {
|
package/src/schemas.ts
CHANGED
|
@@ -38,6 +38,7 @@ function runtimeClientSchema<T, I>(schema: Schema.Schema<T, I, never>): RuntimeC
|
|
|
38
38
|
|
|
39
39
|
export const RuntimeHandleEffectSchema = Schema.Struct({
|
|
40
40
|
projectId: Schema.String,
|
|
41
|
+
runtimeFingerprint: Schema.optional(Schema.String),
|
|
41
42
|
projectDir: Schema.String,
|
|
42
43
|
configPath: Schema.String,
|
|
43
44
|
statePath: Schema.optional(Schema.String),
|
|
@@ -59,6 +60,7 @@ export const RuntimeReadyEffectSchema = Schema.Struct({
|
|
|
59
60
|
export const RuntimeHealthEffectSchema = Schema.Struct({
|
|
60
61
|
ok: Schema.Boolean,
|
|
61
62
|
projectId: Schema.String,
|
|
63
|
+
runtimeFingerprint: Schema.optional(Schema.String),
|
|
62
64
|
projectDir: Schema.optional(Schema.String),
|
|
63
65
|
configPath: Schema.optional(Schema.String),
|
|
64
66
|
statePath: Schema.optional(Schema.String),
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const RIGKIT_RUNTIME_CLIENT_VERSION = "0.2.
|
|
1
|
+
export const RIGKIT_RUNTIME_CLIENT_VERSION = "0.2.2";
|