streamtrust 0.1.1 → 0.3.0

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.
@@ -0,0 +1,515 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { evaluateLifecycle } from 'streamtrust/checks';
3
+ import { createRequire } from 'node:module';
4
+ import { spawn, execFileSync } from 'node:child_process';
5
+ import { mkdtemp, rm } from 'node:fs/promises';
6
+ import { tmpdir } from 'node:os';
7
+ import { createServer } from 'node:net';
8
+ import { readFile, writeFile } from 'node:fs/promises';
9
+ import { resolve, dirname } from 'node:path';
10
+ import { createHash, randomBytes } from 'node:crypto';
11
+
12
+ import { startProvider } from './provider.mjs';
13
+
14
+ const PIN = 'a378657583567310c841d652b0f6b00c83e2bd6b';
15
+ export async function until(check, label, ms = 20000) {
16
+ const end = Date.now() + ms;
17
+ while (Date.now() < end) {
18
+ if (await check()) return;
19
+ await new Promise((r) => setTimeout(r, 100));
20
+ }
21
+ throw new Error(`Timeout: ${label}`);
22
+ }
23
+ let phase = 'preflight';
24
+ async function createAdapter() {
25
+ if (!process.env.STREAMTRUST_LIBRECHAT_ROOT)
26
+ throw new Error(
27
+ 'Set STREAMTRUST_LIBRECHAT_ROOT to a built LibreChat checkout',
28
+ );
29
+ if (Number(process.versions.node.split('.')[0]) < 24)
30
+ throw new Error('LibreChat preset requires Node.js 24+');
31
+ const root = resolve(process.env.STREAMTRUST_LIBRECHAT_ROOT);
32
+ if (
33
+ ['.env', '.env.local', '.env.production'].some((name) =>
34
+ existsSync(resolve(root, name)),
35
+ )
36
+ )
37
+ throw new Error('Use an isolated checkout without environment files');
38
+ const expectedCommit = process.env.STREAMTRUST_LIBRECHAT_COMMIT || PIN;
39
+ if (!/^[a-f0-9]{40}$/.test(expectedCommit))
40
+ throw new Error('Expected commit must be a full Git SHA');
41
+ const configPort = Number(process.env.STREAMTRUST_LIBRECHAT_PORT || 0);
42
+ if (!Number.isInteger(configPort) || configPort < 0 || configPort > 65535)
43
+ throw new Error('Invalid LibreChat port');
44
+ const port =
45
+ configPort ||
46
+ (await new Promise((ok, fail) => {
47
+ const server = createServer();
48
+ server.once('error', fail);
49
+ server.listen(0, '127.0.0.1', () => {
50
+ const value = server.address().port;
51
+ server.close(() => ok(value));
52
+ });
53
+ }));
54
+ const baseUrl = `http://127.0.0.1:${port}`;
55
+ const commit = execFileSync('git', ['rev-parse', 'HEAD'], {
56
+ cwd: root,
57
+ encoding: 'utf8',
58
+ }).trim();
59
+ const dirty = () =>
60
+ execFileSync('git', ['status', '--porcelain', '--untracked-files=no'], {
61
+ cwd: root,
62
+ encoding: 'utf8',
63
+ }).trim();
64
+ if (commit !== expectedCommit || dirty())
65
+ throw new Error('Expected clean pinned LibreChat checkout');
66
+ const require = createRequire(resolve(process.cwd(), 'package.json'));
67
+ const { chromium } = require('playwright');
68
+ const { MongoMemoryServer } = require('mongodb-memory-server');
69
+ const { MongoClient } = require('mongodb');
70
+ const cleanups = [];
71
+ let cleaned = false;
72
+ const cleanup = async () => {
73
+ if (cleaned) return;
74
+ cleaned = true;
75
+ let cleanupFailed = false;
76
+ for (const close of cleanups.reverse()) {
77
+ try {
78
+ await close();
79
+ } catch {
80
+ cleanupFailed = true;
81
+ }
82
+ }
83
+ if (cleanupFailed) throw new Error('LibreChat cleanup failed');
84
+ };
85
+ try {
86
+ phase = 'disposable database startup';
87
+ const mongo = await MongoMemoryServer.create({
88
+ instance: { ip: '127.0.0.1', dbName: 'streamtrust' },
89
+ });
90
+ cleanups.push(() => mongo.stop());
91
+ const client = new MongoClient(mongo.getUri());
92
+ cleanups.push(() => client.close());
93
+ await client.connect();
94
+ const db = client.db('streamtrust');
95
+ const provider = await startProvider();
96
+ cleanups.push(() => provider.close());
97
+ const temporary = await mkdtemp(
98
+ resolve(tmpdir(), 'streamtrust-librechat-'),
99
+ );
100
+ cleanups.unshift(() => rm(temporary, { recursive: true, force: true }));
101
+ const config = resolve(temporary, 'librechat.yaml');
102
+ await writeFile(
103
+ config,
104
+ `version: 1.3.11\ncache: true\nendpoints:\n custom:\n - name: StreamTrust Fixture\n apiKey: fixture-not-a-real-key\n baseURL: ${provider.url}\n models:\n default: [fixture-model]\n fetch: false\n titleConvo: false\n modelDisplayLabel: Fixture\n`,
105
+ );
106
+ const runtime = process.execPath;
107
+ const env = {
108
+ // Do not forward provider keys, database URLs or application secrets.
109
+ ...Object.fromEntries(
110
+ ['PATH', 'HOME', 'TMPDIR', 'TEMP', 'SystemRoot']
111
+ .filter((k) => process.env[k])
112
+ .map((k) => [k, process.env[k]]),
113
+ ),
114
+ PATH: `${dirname(runtime)}:${process.env.PATH}`,
115
+ NODE_ENV: 'production',
116
+ HOST: '127.0.0.1',
117
+ PORT: String(port),
118
+ DOMAIN_CLIENT: baseUrl,
119
+ DOMAIN_SERVER: baseUrl,
120
+ MONGO_URI: mongo.getUri('streamtrust'),
121
+ CONFIG_PATH: config,
122
+ ENDPOINTS: 'custom',
123
+ OPENAI_API_KEY: '',
124
+ SEARCH: 'false',
125
+ NO_INDEX: 'true',
126
+ ALLOW_REGISTRATION: 'true',
127
+ ALLOW_SOCIAL_LOGIN: 'false',
128
+ ALLOW_EMAIL_LOGIN: 'true',
129
+ JWT_SECRET: randomBytes(32).toString('hex'),
130
+ JWT_REFRESH_SECRET: randomBytes(32).toString('hex'),
131
+ CREDS_KEY: randomBytes(32).toString('hex'),
132
+ CREDS_IV: randomBytes(16).toString('hex'),
133
+ SCHEDULES_SINGLE_PROCESS: 'true',
134
+ USE_REDIS: 'false',
135
+ USE_REDIS_STREAMS: 'false',
136
+ REGISTER_MAX: '100',
137
+ LOGIN_MAX: '100',
138
+ };
139
+ phase = 'application startup';
140
+ const app = spawn(runtime, ['api/server/index.js'], {
141
+ cwd: root,
142
+ env,
143
+ stdio: ['ignore', 'ignore', 'ignore'],
144
+ });
145
+ let appError = false;
146
+ app.on('error', () => {
147
+ appError = true;
148
+ });
149
+ cleanups.push(async () => {
150
+ if (!appError && app.exitCode === null && app.signalCode === null) {
151
+ const stopped = new Promise((r) => app.once('exit', () => r()));
152
+ app.kill('SIGTERM');
153
+ const timer = setTimeout(() => app.kill('SIGKILL'), 10000);
154
+ await stopped;
155
+ clearTimeout(timer);
156
+ }
157
+ });
158
+ phase = 'browser startup';
159
+ const browser = await chromium.launch(
160
+ !process.env.STREAMTRUST_BROWSER_CHANNEL
161
+ ? { headless: true }
162
+ : { channel: process.env.STREAMTRUST_BROWSER_CHANNEL, headless: true },
163
+ );
164
+ cleanups.push(() => browser.close());
165
+ phase = 'application readiness';
166
+ await until(
167
+ async () => {
168
+ if (appError || app.exitCode !== null)
169
+ throw new Error(
170
+ 'LibreChat startup failed; check the build and port configuration',
171
+ );
172
+ try {
173
+ return (await fetch(baseUrl, { signal: AbortSignal.timeout(2000) }))
174
+ .ok;
175
+ } catch {
176
+ return false;
177
+ }
178
+ },
179
+ 'app ready',
180
+ 120000,
181
+ );
182
+ phase = 'synthetic user registration';
183
+ const email = `verifier-${Date.now()}@example.com`,
184
+ password = `Fixture-${randomBytes(12).toString('hex')}!`;
185
+ const registration = await browser.newContext();
186
+ const page = await registration.newPage();
187
+ await page.goto(`${baseUrl}/register`);
188
+ await page.getByLabel('Full name').fill('StreamTrust Verifier');
189
+ await page.getByLabel('Email', { exact: true }).fill(email);
190
+ await page.getByTestId('password').fill(password);
191
+ await page.getByTestId('confirm_password').fill(password);
192
+ await page.getByLabel('Submit registration').click();
193
+ await page.waitForURL('**/login**');
194
+ await page.getByLabel('Email', { exact: true }).fill(email);
195
+ await page.getByLabel('Password', { exact: true }).fill(password);
196
+ await page.getByTestId('login-button').click();
197
+ await page
198
+ .getByRole('textbox', { name: 'Message input' })
199
+ .waitFor({ timeout: 30000 });
200
+ await page.close();
201
+ async function messages(id) {
202
+ return db
203
+ .collection('messages')
204
+ .find(
205
+ { conversationId: id },
206
+ {
207
+ projection: {
208
+ _id: 0,
209
+ messageId: 1,
210
+ parentMessageId: 1,
211
+ conversationId: 1,
212
+ text: 1,
213
+ content: 1,
214
+ isCreatedByUser: 1,
215
+ error: 1,
216
+ unfinished: 1,
217
+ },
218
+ },
219
+ )
220
+ .toArray();
221
+ }
222
+ const textOf = (m) =>
223
+ m.text ||
224
+ (m.content ?? [])
225
+ .filter((p) => p.type === 'text')
226
+ .map((p) =>
227
+ typeof p.text === 'string' ? p.text : (p.text?.value ?? ''),
228
+ )
229
+ .join('');
230
+ const hasError = (m) =>
231
+ (m.content ?? []).some((part) => part.type === 'error');
232
+ const observed = async (check) => {
233
+ const deadline = Date.now() + 15000;
234
+ while (Date.now() < deadline) {
235
+ if (await check()) return true;
236
+ await new Promise((r) => setTimeout(r, 100));
237
+ }
238
+ return false;
239
+ };
240
+ const visible = (p) => p.getByTestId('messages-view').innerText();
241
+ async function waitGeneration(p, index) {
242
+ await until(
243
+ () => provider.generations.length > index,
244
+ 'provider request',
245
+ );
246
+ const g = provider.generations[index];
247
+ await until(
248
+ async () => (await visible(p)).includes(g.prefix),
249
+ 'first rendered chunk',
250
+ );
251
+ return g;
252
+ }
253
+ return {
254
+ name: 'LibreChat',
255
+ scenarios: ['persistence', 'cancellation', 'regeneration', 'error-retry'],
256
+ async metadata() {
257
+ return {
258
+ evidencePolicy: 'summary-only-v1',
259
+ checkVersion: 1,
260
+ mongodb: (await db.admin().command({ buildInfo: 1 })).version,
261
+ upstream: 'https://github.com/LibreChat-AI/LibreChat',
262
+ commit,
263
+ trackedSourceModified: !!dirty(),
264
+ node: execFileSync(runtime, ['--version'], {
265
+ encoding: 'utf8',
266
+ }).trim(),
267
+ browser: browser.version(),
268
+ lockSha256: createHash('sha256')
269
+ .update(await readFile(resolve(root, 'package-lock.json')))
270
+ .digest('hex'),
271
+ scope:
272
+ 'Real native UI, authentication, application routes, generation jobs and MongoDB. Only external OpenAI-compatible provider responses are synthetic. Single server, in-memory stream store; no Redis, worker-kill, database-crash or multi-replica coverage. Regeneration is a branch, not overwriting history. No coordinator installed.',
273
+ };
274
+ },
275
+ async run(scenario, { repetition }) {
276
+ const context = registration;
277
+ const p = await context.newPage();
278
+ const network = [];
279
+
280
+ p.on('response', (r) => {
281
+ const path = new URL(r.url()).pathname;
282
+ if (path.startsWith('/api/') && /chat|abort|message/.test(path))
283
+ network.push({
284
+ path,
285
+ method: r.request().method(),
286
+ status: r.status(),
287
+ });
288
+ });
289
+ let first;
290
+ let second;
291
+ try {
292
+ await p.goto(`${baseUrl}/c/new`);
293
+ await p.getByRole('textbox', { name: 'Message input' }).waitFor();
294
+ const selector = p
295
+ .getByRole('button', { name: 'Select a model' })
296
+ .first();
297
+ if (
298
+ (await selector.count()) &&
299
+ !(await selector.innerText()).includes('fixture-model')
300
+ ) {
301
+ await selector.click();
302
+ await p
303
+ .getByRole('option', { name: 'StreamTrust Fixture', exact: true })
304
+ .click();
305
+ const model = p.getByRole('option', {
306
+ name: 'fixture-model',
307
+ exact: true,
308
+ });
309
+ if (await model.isVisible().catch(() => false)) await model.click();
310
+ }
311
+ const before = provider.generations.length;
312
+ await p
313
+ .getByRole('textbox', { name: 'Message input' })
314
+ .fill(
315
+ `StreamTrust ${scenario} ${repetition}. Write two sentences.`,
316
+ );
317
+ const admission = p.waitForResponse(
318
+ (r) =>
319
+ r.request().method() === 'POST' &&
320
+ new URL(r.url()).pathname.startsWith('/api/agents/chat') &&
321
+ !r.url().endsWith('/abort'),
322
+ );
323
+ await p
324
+ .getByRole('textbox', { name: 'Message input' })
325
+ .press('Enter');
326
+ first = await waitGeneration(p, before);
327
+ const accepted = await (await admission).json();
328
+ const id = accepted.conversationId;
329
+ if (!id || id === 'new')
330
+ throw new Error('Missing native admission conversation identity');
331
+ const checks = [];
332
+ let expected = first.prefix + first.suffix;
333
+ if (scenario === 'cancellation') {
334
+ await p
335
+ .getByRole('button', { name: 'Stop generating', exact: true })
336
+ .click();
337
+ await observed(() => first.closed);
338
+ expected = first.prefix;
339
+ checks.push({
340
+ name: 'Native server acknowledges Stop',
341
+ passed: await observed(() =>
342
+ network.some(
343
+ (e) =>
344
+ e.method === 'POST' &&
345
+ e.path.endsWith('/abort') &&
346
+ e.status === 200,
347
+ ),
348
+ ),
349
+ });
350
+ checks.push({
351
+ name: 'Stop aborts upstream HTTP response before completion',
352
+ passed: first.closed && !first.finished,
353
+ });
354
+ checks.push({
355
+ name: 'Late provider completion cannot be delivered',
356
+ passed: !provider.finish(first),
357
+ });
358
+ } else if (scenario === 'error-retry') {
359
+ provider.fail(first);
360
+ } else provider.finish(first);
361
+ await until(
362
+ async () =>
363
+ !(await p
364
+ .getByRole('button', { name: 'Stop generating', exact: true })
365
+ .count()),
366
+ 'generation settles',
367
+ );
368
+
369
+ if (scenario === 'error-retry') {
370
+ const errorSaved = await observed(async () =>
371
+ (await messages(id)).some(hasError),
372
+ );
373
+
374
+ checks.push({
375
+ name: 'Provider failure retained as a native error part',
376
+ passed: errorSaved,
377
+ });
378
+ await p.reload();
379
+ await p.getByRole('textbox', { name: 'Message input' }).waitFor();
380
+ const errorVisible = await observed(async () =>
381
+ (await visible(p)).includes(
382
+ 'model provider closed the connection',
383
+ ),
384
+ );
385
+ checks.push({
386
+ name: 'Provider error context survives reload before retry',
387
+ passed: errorVisible,
388
+ });
389
+ }
390
+ if (scenario === 'regeneration' || scenario === 'error-retry') {
391
+ const nextIndex = provider.generations.length;
392
+ await p
393
+ .getByRole('button', { name: 'Regenerate', exact: true })
394
+ .last()
395
+ .click();
396
+ second = await waitGeneration(p, nextIndex);
397
+ provider.finish(second);
398
+ expected = second.prefix + second.suffix;
399
+ await until(
400
+ async () =>
401
+ !(await p
402
+ .getByRole('button', { name: 'Stop generating', exact: true })
403
+ .count()),
404
+ 'retry settles',
405
+ );
406
+ }
407
+ await observed(async () =>
408
+ (await messages(id)).some(
409
+ (m) => !m.isCreatedByUser && textOf(m).trim() === expected.trim(),
410
+ ),
411
+ );
412
+ const durable = await messages(id);
413
+ const beforeReload = await visible(p);
414
+ await p.reload();
415
+ await p.getByRole('textbox', { name: 'Message input' }).waitFor();
416
+ await observed(async () => (await visible(p)).includes(expected));
417
+ const reloaded = await visible(p);
418
+ return evaluateLifecycle({
419
+ scenario,
420
+ expected,
421
+ original: first.prefix + first.suffix,
422
+ previousPrefix: first.prefix,
423
+ beforeReload,
424
+ reloaded,
425
+ messages: durable
426
+ .filter((m) => !m.isCreatedByUser)
427
+ .map((m) => ({
428
+ id: m.messageId,
429
+ parentId: m.parentMessageId,
430
+ text: textOf(m),
431
+ assistant: !m.isCreatedByUser,
432
+ unfinished: m.unfinished,
433
+ error: !!m.error || hasError(m),
434
+ })),
435
+ callsBeforeReload: second ? second.id : first.id,
436
+ callsAfterReload: provider.generations.length,
437
+ cancellation:
438
+ scenario === 'cancellation'
439
+ ? {
440
+ acknowledged: checks.find(
441
+ (c) => c.name === 'Native server acknowledges Stop',
442
+ ).passed,
443
+ providerClosed: first.closed,
444
+ providerFinished: first.finished,
445
+ lateCompletionRejected: checks.find(
446
+ (c) =>
447
+ c.name ===
448
+ 'Late provider completion cannot be delivered',
449
+ ).passed,
450
+ }
451
+ : undefined,
452
+ failure:
453
+ scenario === 'error-retry'
454
+ ? {
455
+ saved: checks.find(
456
+ (c) =>
457
+ c.name ===
458
+ 'Provider failure retained as a native error part',
459
+ ).passed,
460
+ visibleAfterReload: checks.find(
461
+ (c) =>
462
+ c.name ===
463
+ 'Provider error context survives reload before retry',
464
+ ).passed,
465
+ }
466
+ : undefined,
467
+ });
468
+ } catch {
469
+ // Browser/DB exceptions can contain prompts, selectors, URLs and credentials.
470
+ throw new Error(
471
+ 'LibreChat scenario could not complete (details withheld by summary-only policy)',
472
+ );
473
+ } finally {
474
+ if (first && !first.closed) provider.finish(first);
475
+ if (second && !second.closed) provider.finish(second);
476
+ await p.close();
477
+ }
478
+ },
479
+ close: cleanup,
480
+ };
481
+ } catch (error) {
482
+ await cleanup();
483
+ throw error;
484
+ }
485
+ }
486
+
487
+ export default async function safeFactory(context) {
488
+ try {
489
+ phase = 'preflight';
490
+ const adapter = await createAdapter();
491
+ const run = adapter.run.bind(adapter);
492
+ adapter.run = async (...args) => {
493
+ try {
494
+ return await run(...args);
495
+ } catch {
496
+ throw new Error(
497
+ 'LibreChat scenario could not complete (details withheld by summary-only policy)',
498
+ );
499
+ }
500
+ };
501
+ const metadata = adapter.metadata.bind(adapter);
502
+ adapter.metadata = async () => {
503
+ try {
504
+ return await metadata();
505
+ } catch {
506
+ throw new Error('LibreChat metadata unavailable (details withheld)');
507
+ }
508
+ };
509
+ return adapter;
510
+ } catch {
511
+ throw new Error(
512
+ `LibreChat setup failed during ${phase}. Check Node 24+, optional dependencies, built clean checkout without .env files, expected commit, and port. Raw details withheld.`,
513
+ );
514
+ }
515
+ }
@@ -0,0 +1,79 @@
1
+ import { createServer } from 'node:http';
2
+
3
+ export async function startProvider() {
4
+ const generations = [];
5
+ const chunk = (g, delta, finish = null) =>
6
+ g.response.write(
7
+ `data: ${JSON.stringify({ id: `chatcmpl-fixture-${g.id}`, object: 'chat.completion.chunk', created: 1, model: 'fixture-model', choices: [{ index: 0, delta: { content: delta }, finish_reason: finish }] })}\n\n`,
8
+ );
9
+ const server = createServer(async (req, res) => {
10
+ let body = '';
11
+ for await (const part of req) body += part;
12
+ if (req.method !== 'POST') {
13
+ res.setHeader('content-type', 'application/json');
14
+ res.end(
15
+ JSON.stringify({ data: [{ id: 'fixture-model', object: 'model' }] }),
16
+ );
17
+ return;
18
+ }
19
+ const input = JSON.parse(body);
20
+ if (!input.stream) {
21
+ res.setHeader('content-type', 'application/json');
22
+ res.end(
23
+ JSON.stringify({
24
+ choices: [
25
+ {
26
+ message: { role: 'assistant', content: 'Fixture conversation' },
27
+ finish_reason: 'stop',
28
+ },
29
+ ],
30
+ }),
31
+ );
32
+ return;
33
+ }
34
+ const id = generations.length + 1;
35
+ const g = {
36
+ id,
37
+ prefix: `Response ${id} begins.`,
38
+ suffix: ` Response ${id} completes.`,
39
+ closed: false,
40
+ finished: false,
41
+ failed: false,
42
+ path: req.url,
43
+ response: res,
44
+ };
45
+ generations.push(g);
46
+ res.on('close', () => {
47
+ g.closed = true;
48
+ });
49
+ res.writeHead(200, {
50
+ 'content-type': 'text/event-stream',
51
+ 'cache-control': 'no-cache',
52
+ });
53
+ chunk(g, g.prefix);
54
+ });
55
+ await new Promise((r) => server.listen(0, '127.0.0.1', r));
56
+ return {
57
+ url: `http://127.0.0.1:${server.address().port}/v1`,
58
+ generations,
59
+ finish(g) {
60
+ if (g.closed) return false;
61
+ chunk(g, g.suffix);
62
+ chunk(g, '', 'stop');
63
+ g.finished = true;
64
+ g.response.end('data: [DONE]\n\n');
65
+ return true;
66
+ },
67
+ fail(g) {
68
+ g.failed = true;
69
+ g.response.destroy();
70
+ },
71
+ observations() {
72
+ return generations.map(({ response, ...g }) => g);
73
+ },
74
+ async close() {
75
+ server.closeAllConnections();
76
+ await new Promise((r) => server.close(() => r()));
77
+ },
78
+ };
79
+ }
@@ -0,0 +1,42 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ const root = process.env.STREAMTRUST_LIBRECHAT_ROOT;
5
+ const pin =
6
+ process.env.STREAMTRUST_LIBRECHAT_COMMIT ||
7
+ 'a378657583567310c841d652b0f6b00c83e2bd6b';
8
+ if (
9
+ !root ||
10
+ Number(process.versions.node.split('.')[0]) < 24 ||
11
+ !/^[a-f0-9]{40}$/.test(pin)
12
+ )
13
+ throw new Error(
14
+ 'Set STREAMTRUST_LIBRECHAT_ROOT; use Node 24+ and a full commit SHA',
15
+ );
16
+ const path = resolve(root);
17
+ const run = (cmd, args, cwd) =>
18
+ execFileSync(cmd, args, { cwd, stdio: 'inherit' });
19
+ if (!existsSync(path)) {
20
+ run('git', [
21
+ 'clone',
22
+ '--no-checkout',
23
+ 'https://github.com/LibreChat-AI/LibreChat.git',
24
+ path,
25
+ ]);
26
+ run('git', ['checkout', '--detach', pin], path);
27
+ }
28
+ if (
29
+ execFileSync('git', ['rev-parse', 'HEAD'], {
30
+ cwd: path,
31
+ encoding: 'utf8',
32
+ }).trim() !== pin ||
33
+ execFileSync('git', ['status', '--porcelain', '--untracked-files=no'], {
34
+ cwd: path,
35
+ encoding: 'utf8',
36
+ }).trim()
37
+ )
38
+ throw new Error(
39
+ 'Checkout must be clean and match the expected commit; setup never resets existing source',
40
+ );
41
+ run('npm', ['ci', '--no-audit', '--no-fund'], path);
42
+ run('npm', ['run', 'frontend'], path);