@codeyam-editor/codeyam-editor 0.1.0-staging.3db0f53 → 0.1.0-staging.4d8da9a

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.
Files changed (56) hide show
  1. package/npm/cli.js +5 -2
  2. package/npm/cloud-branch-switch.js +442 -0
  3. package/npm/cloud.js +2184 -0
  4. package/npm/container.js +33 -3
  5. package/npm/disk-pressure.js +89 -0
  6. package/npm/editor-dev.js +379 -37
  7. package/npm/editor.js +145 -52
  8. package/npm/empty-project-dir.js +33 -0
  9. package/npm/fleet-auth.js +111 -0
  10. package/npm/fleet-branch.js +49 -0
  11. package/npm/fleet-broker-death.js +44 -0
  12. package/npm/fleet-broker-debounce.js +63 -0
  13. package/npm/fleet-broker-status.js +45 -0
  14. package/npm/fleet-build-data.js +81 -0
  15. package/npm/fleet-cleanup.js +324 -0
  16. package/npm/fleet-effect-status.js +179 -0
  17. package/npm/fleet-env.js +125 -0
  18. package/npm/fleet-grouping.js +56 -0
  19. package/npm/fleet-job-badge.js +83 -0
  20. package/npm/fleet-memory.js +157 -0
  21. package/npm/fleet-plans.js +96 -0
  22. package/npm/fleet-pool-policy.js +78 -0
  23. package/npm/fleet-pool.js +401 -0
  24. package/npm/fleet-queue.js +40 -0
  25. package/npm/fleet-reconcile.js +93 -0
  26. package/npm/fleet-reset.js +222 -0
  27. package/npm/fleet-roster.js +81 -0
  28. package/npm/launch-config.js +727 -0
  29. package/npm/preflight-guidance.js +77 -0
  30. package/npm/scenario-check.js +404 -15
  31. package/npm/scenario-handlers.js +32 -5
  32. package/npm/scenario-interactivity.js +245 -0
  33. package/npm/scenario-metrics.js +16 -2
  34. package/npm/scenario-mocks.js +62 -25
  35. package/npm/scenario-playwright.js +350 -8
  36. package/npm/utils.js +697 -26
  37. package/npm/vm-urls.js +117 -0
  38. package/package.json +8 -7
  39. package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +2 -0
  40. package/ui/dist/assets/EditorShell-tMMdarZD.js +187 -0
  41. package/ui/dist/assets/Markdown-BHBqz-Mj.js +1 -0
  42. package/ui/dist/assets/ScenarioDataPanel-DYP7pi_n.css +32 -0
  43. package/ui/dist/assets/ScenarioDataPanel-xWYGZOpo.js +5 -0
  44. package/ui/dist/assets/ScenarioDataPanelFullPage-5G6KuZgw.js +1 -0
  45. package/ui/dist/assets/ScenarioRenderer-CSdhX1Ak.js +1 -0
  46. package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +1 -0
  47. package/ui/dist/assets/index-BGqairFV.css +1 -0
  48. package/ui/dist/assets/index-BuK0lizt.js +27 -0
  49. package/ui/dist/assets/markdown-v0F0UEt3.js +37 -0
  50. package/ui/dist/assets/react-CSS0HapR.js +28 -0
  51. package/ui/dist/assets/useEvents-DggWrtHe.js +1 -0
  52. package/ui/dist/assets/xterm--24IGk-x.js +9 -0
  53. package/ui/dist/favicon.ico +0 -0
  54. package/ui/dist/favicon.png +0 -0
  55. package/ui/dist/index.html +16 -0
  56. package/npm/postinstall.js +0 -132
package/npm/cli.js CHANGED
@@ -1,14 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { spawn } = require("child_process");
4
- const { ensureBinary, uiDistDir } = require("./utils");
4
+ const { ensureBinary } = require("./utils");
5
5
 
6
6
  const binary = ensureBinary();
7
7
 
8
+ // The Rust binary embeds `ui/dist` at build time, so no `CODEYAM_EDITOR_UI_DIR`
9
+ // is required. Devs who want to override the bundle can still export the env
10
+ // var manually — the binary's resolver checks it first.
8
11
  const child = spawn(binary, process.argv.slice(2), {
9
12
  stdio: "inherit",
10
13
  cwd: process.cwd(),
11
- env: { ...process.env, CODEYAM_EDITOR_UI_DIR: uiDistDir() },
14
+ env: process.env,
12
15
  });
13
16
 
14
17
  child.on("exit", (code) => {
@@ -0,0 +1,442 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // npm/cloud-branch-switch.js — switch the whole fleet to a new branch in one command.
5
+ //
6
+ // Orchestrates:
7
+ // 1. Verify the new branch exists on origin.
8
+ // 2. Print the launchd repoint command (operator runs it; auto-mode classifier
9
+ // blocks ~/Library/LaunchAgents writes from Claude Code).
10
+ // 3. Fire the dashboard's Reconfigure action for each idle VM in the roster,
11
+ // passing the new branch as both clientBranch + editorBranch so no dashboard
12
+ // restart is needed for the operation itself.
13
+ // 4. Poll /data until all fired Reconfigure jobs reach a terminal state.
14
+ // 5. Run fixup-vm-creds on each successfully reconfigured VM (if the script exists).
15
+ //
16
+ // Usage:
17
+ // npm run cloud:branch-switch editor-improvements-N [--dry-run] [--force]
18
+ //
19
+ // Env overrides:
20
+ // URLS_FILE cloudflared URL JSON (default .codeyam/logs/cloudflared-urls.json)
21
+ // TOKEN_FILE dashboard bearer-token file
22
+ // VMS_STATE_FILE roster JSON (default .codeyam/logs/fleet-vms.json)
23
+ // DASH_PORT dashboard port for localhost fallback (default 8787)
24
+ // BRANCH_SWITCH_POLL_SEC /data poll interval in seconds (default 15)
25
+ // BRANCH_SWITCH_TIMEOUT_SEC per-job deadline (default 2400)
26
+
27
+ const { spawnSync } = require('child_process');
28
+ const fs = require('fs');
29
+ const path = require('path');
30
+ const http = require('http');
31
+ const https = require('https');
32
+
33
+ const { rootDir } = require('./utils');
34
+ // Shared roster reader — the roster file is a fleet-wide contract, so its
35
+ // read+write logic lives in one module the CLI, the dashboard, and this
36
+ // orchestrator all import (see npm/fleet-roster.js).
37
+ const { readRoster } = require('./fleet-roster');
38
+
39
+ const CANONICAL_EDITOR_REPO = 'https://github.com/codeyam-ai/codeyam-editor.git';
40
+ const DASH_PORT = Number(process.env.DASH_PORT) || 8787;
41
+ const URLS_FILE = process.env.URLS_FILE
42
+ || path.join(rootDir, '.codeyam', 'logs', 'cloudflared-urls.json');
43
+ const TOKEN_FILE = process.env.TOKEN_FILE
44
+ || path.join(rootDir, 'scripts', 'operator', 'fleet-dashboard', '.token');
45
+ const POLL_SEC = Number(process.env.BRANCH_SWITCH_POLL_SEC) || 15;
46
+ const TIMEOUT_SEC = Number(process.env.BRANCH_SWITCH_TIMEOUT_SEC) || 2400;
47
+ const FIXUP_SCRIPT = path.join(rootDir, 'scripts', 'operator', 'fixup-vm-creds.sh');
48
+
49
+ // ── Pure helpers (unit-tested in cloud-branch-switch.test.js) ──────────────
50
+
51
+ /** Parse argv slice for cloud:branch-switch. Returns { newBranch, dryRun, force }. */
52
+ function parseBranchSwitchArgs(argv) {
53
+ const args = { newBranch: null, dryRun: false, force: false };
54
+ for (const a of argv) {
55
+ if (a === '--dry-run') { args.dryRun = true; continue; }
56
+ if (a === '--force') { args.force = true; continue; }
57
+ if (a.startsWith('--')) throw new Error(`Unknown flag: ${a}`);
58
+ if (args.newBranch) {
59
+ throw new Error(
60
+ `Unexpected positional arg '${a}' (branch already set to '${args.newBranch}')`,
61
+ );
62
+ }
63
+ args.newBranch = a;
64
+ }
65
+ return args;
66
+ }
67
+
68
+ /** True for non-empty branch names composed only of safe git-ref characters. */
69
+ function isValidBranchName(branch) {
70
+ return typeof branch === 'string' && branch.length > 0
71
+ && /^[a-zA-Z0-9._/-]+$/.test(branch);
72
+ }
73
+
74
+ /** Find a VM by number in the /data vms array (which is ordered, not keyed). */
75
+ function findVm(vms, n) {
76
+ return Array.isArray(vms) ? vms.find((v) => v && v.n === n) : undefined;
77
+ }
78
+
79
+ /**
80
+ * True when a VM's active reconfigure job has reached a terminal state, OR no
81
+ * reconfigure job is present (a later job may have taken its slot, meaning the
82
+ * reconfigure completed). Never blocks indefinitely in the poller.
83
+ */
84
+ function isReconfigureTerminal(vm) {
85
+ const job = vm && vm.job;
86
+ if (!job || job.action !== 'reconfigure') return true;
87
+ return job.state === 'done' || job.state === 'error';
88
+ }
89
+
90
+ /** Extract the terminal result ('done' / 'error') for a VM's reconfigure job. */
91
+ function reconfigureResult(vm) {
92
+ const job = vm && vm.job;
93
+ if (!job || job.action !== 'reconfigure') return 'done';
94
+ return job.state === 'done' ? 'done' : 'error';
95
+ }
96
+
97
+ /** True when the /data vms array shows an active session on this VM. */
98
+ function vmHasActiveSession(vms, n) {
99
+ const vm = findVm(vms, n);
100
+ return !!(vm && vm.hasSession);
101
+ }
102
+
103
+ /** Current client branch for a VM, from cardIdentity (live wins over history). */
104
+ function vmClientBranch(vms, n) {
105
+ const vm = findVm(vms, n);
106
+ return (vm && vm.cardIdentity && vm.cardIdentity.clientBranch) || null;
107
+ }
108
+
109
+ /** Build the /action/reconfigure query string for a VM + branch. */
110
+ function buildReconfigureQuery(n, newBranch, force) {
111
+ const p = new URLSearchParams({
112
+ vm: String(n),
113
+ clientRepo: CANONICAL_EDITOR_REPO,
114
+ clientBranch: newBranch,
115
+ editorVariant: 'dev',
116
+ editorBranch: newBranch,
117
+ });
118
+ if (force) p.set('force', '1');
119
+ return p.toString();
120
+ }
121
+
122
+ // ── I/O helpers ─────────────────────────────────────────────────────────────
123
+
124
+ function readToken() {
125
+ try { return fs.readFileSync(TOKEN_FILE, 'utf8').trim(); } catch { return ''; }
126
+ }
127
+
128
+ function readDashboardBaseUrl() {
129
+ try {
130
+ const urls = JSON.parse(fs.readFileSync(URLS_FILE, 'utf8'));
131
+ const dash = urls && typeof urls.dashboard === 'string' ? urls.dashboard.trim() : '';
132
+ if (dash) return dash;
133
+ } catch { /* fall through to localhost */ }
134
+ return `http://127.0.0.1:${DASH_PORT}`;
135
+ }
136
+
137
+ function branchExistsOnOrigin(branch) {
138
+ const r = spawnSync('git', ['ls-remote', '--heads', 'origin', branch], {
139
+ encoding: 'utf8', cwd: rootDir,
140
+ });
141
+ if (r.error || r.status !== 0) return false;
142
+ return String(r.stdout || '').includes(`refs/heads/${branch}`);
143
+ }
144
+
145
+ // ── HTTP ────────────────────────────────────────────────────────────────────
146
+
147
+ function httpFetch(urlStr, opts = {}) {
148
+ return new Promise((resolve, reject) => {
149
+ const parsed = new URL(urlStr);
150
+ const lib = parsed.protocol === 'https:' ? https : http;
151
+ const reqOpts = {
152
+ hostname: parsed.hostname,
153
+ port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
154
+ path: parsed.pathname + parsed.search,
155
+ method: opts.method || 'GET',
156
+ headers: opts.headers || {},
157
+ };
158
+ const req = lib.request(reqOpts, (res) => {
159
+ let body = '';
160
+ res.on('data', (chunk) => { body += chunk; });
161
+ res.on('end', () => {
162
+ try { resolve({ status: res.statusCode, body: JSON.parse(body) }); }
163
+ catch { resolve({ status: res.statusCode, body }); }
164
+ });
165
+ });
166
+ req.setTimeout(opts.timeoutMs || 15000, () => {
167
+ req.destroy();
168
+ reject(new Error(`Timeout fetching ${urlStr}`));
169
+ });
170
+ req.on('error', reject);
171
+ req.end();
172
+ });
173
+ }
174
+
175
+ function authHeaders(tok) {
176
+ return { Authorization: `Bearer ${tok}` };
177
+ }
178
+
179
+ async function fetchDashboardData(baseUrl, tok) {
180
+ const r = await httpFetch(`${baseUrl}/data`, { headers: authHeaders(tok) });
181
+ if (r.status !== 200 || !r.body || typeof r.body !== 'object') {
182
+ throw new Error(`Dashboard /data returned HTTP ${r.status}`);
183
+ }
184
+ return r.body;
185
+ }
186
+
187
+ async function postReconfigure(baseUrl, tok, n, newBranch, force) {
188
+ const q = buildReconfigureQuery(n, newBranch, force);
189
+ return httpFetch(`${baseUrl}/action/reconfigure?${q}`, {
190
+ method: 'POST',
191
+ headers: { ...authHeaders(tok), 'Content-Length': '0' },
192
+ });
193
+ }
194
+
195
+ // ── Job poller ───────────────────────────────────────────────────────────────
196
+
197
+ function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); }
198
+
199
+ /** Poll /data until every VM in `ns` has a terminal reconfigure job state.
200
+ * Returns an object mapping each n to 'done' or 'error' or 'timeout'. */
201
+ async function waitForReconfigureJobs(baseUrl, tok, ns, pollSec, timeoutSec) {
202
+ const deadline = Date.now() + timeoutSec * 1000;
203
+ const pending = new Set(ns);
204
+ const results = {};
205
+
206
+ while (pending.size > 0) {
207
+ if (Date.now() >= deadline) {
208
+ for (const n of pending) {
209
+ results[n] = 'timeout';
210
+ process.stdout.write(` VM-${n}: TIMEOUT\n`);
211
+ }
212
+ break;
213
+ }
214
+
215
+ let data;
216
+ try {
217
+ data = await fetchDashboardData(baseUrl, tok);
218
+ } catch (e) {
219
+ process.stdout.write(` (poll error: ${e.message}; retrying in ${pollSec}s)\n`);
220
+ await sleep(pollSec * 1000);
221
+ continue;
222
+ }
223
+
224
+ for (const n of [...pending]) {
225
+ const vm = findVm(data.vms, n);
226
+ if (isReconfigureTerminal(vm)) {
227
+ results[n] = reconfigureResult(vm);
228
+ pending.delete(n);
229
+ process.stdout.write(` VM-${n}: ${results[n]}\n`);
230
+ }
231
+ }
232
+ if (pending.size > 0) {
233
+ const elapsed = Math.round((Date.now() - (deadline - timeoutSec * 1000)) / 1000);
234
+ process.stdout.write(
235
+ ` waiting (${[...pending].map((n) => `VM-${n}`).join(', ')} — +${elapsed}s)...\n`,
236
+ );
237
+ await sleep(pollSec * 1000);
238
+ }
239
+ }
240
+ return results;
241
+ }
242
+
243
+ /** Classify each VM in roster as either to-switch or skip.
244
+ * Pure: takes already-loaded /data vms array and per-VM state; no I/O. */
245
+ function classifyVmsForSwitch(roster, vms, newBranch, force) {
246
+ const toSwitch = [];
247
+ const skipped = [];
248
+ for (const n of roster) {
249
+ const currentBranch = vmClientBranch(vms, n);
250
+ if (currentBranch === newBranch) {
251
+ skipped.push({ n, reason: `already on ${newBranch}` });
252
+ continue;
253
+ }
254
+ if (vmHasActiveSession(vms, n) && !force) {
255
+ skipped.push({ n, reason: 'has active session (use --force to override)' });
256
+ continue;
257
+ }
258
+ toSwitch.push(n);
259
+ }
260
+ return { toSwitch, skipped };
261
+ }
262
+
263
+ /** Fire Reconfigure for each VM in toSwitch. Returns { fired, rejected }.
264
+ * postFn is injected for testability; defaults to postReconfigure. */
265
+ async function fireReconfigures(baseUrl, tok, toSwitch, newBranch, force, postFn = postReconfigure) {
266
+ const fired = [];
267
+ const rejected = [];
268
+ for (const n of toSwitch) {
269
+ try {
270
+ const r = await postFn(baseUrl, tok, n, newBranch, force);
271
+ if (r.body && r.body.ok === false) {
272
+ const why = r.body.msg || 'rejected by dashboard';
273
+ console.warn(` VM-${n}: rejected — ${why}`);
274
+ rejected.push({ n, reason: why });
275
+ } else {
276
+ const queued = r.body && r.body.queued ? ' (queued)' : '';
277
+ console.log(` VM-${n}: accepted${queued}`);
278
+ fired.push(n);
279
+ }
280
+ } catch (e) {
281
+ console.warn(` VM-${n}: HTTP error — ${e.message}`);
282
+ rejected.push({ n, reason: e.message });
283
+ }
284
+ }
285
+ return { fired, rejected };
286
+ }
287
+
288
+ // ── Fixup ────────────────────────────────────────────────────────────────────
289
+
290
+ function runFixupVmCreds(n) {
291
+ if (!fs.existsSync(FIXUP_SCRIPT)) return;
292
+ console.log(` fixup-vm-creds VM-${n}...`);
293
+ const r = spawnSync('bash', [FIXUP_SCRIPT, String(n)], {
294
+ stdio: 'inherit', cwd: rootDir,
295
+ });
296
+ if (r.status !== 0) {
297
+ console.warn(` WARN: fixup-vm-creds exited ${r.status} for VM-${n}`);
298
+ }
299
+ }
300
+
301
+ // ── Main ─────────────────────────────────────────────────────────────────────
302
+
303
+ async function branchSwitchMain() {
304
+ let args;
305
+ try {
306
+ args = parseBranchSwitchArgs(process.argv.slice(2));
307
+ } catch (e) {
308
+ console.error(`Error: ${e.message}`);
309
+ console.error('Usage: npm run cloud:branch-switch editor-improvements-N [--dry-run] [--force]');
310
+ process.exit(1);
311
+ }
312
+
313
+ if (!args.newBranch) {
314
+ console.error('Usage: npm run cloud:branch-switch editor-improvements-N [--dry-run] [--force]');
315
+ process.exit(1);
316
+ }
317
+ if (!isValidBranchName(args.newBranch)) {
318
+ console.error(`Error: invalid branch name '${args.newBranch}'`);
319
+ process.exit(1);
320
+ }
321
+
322
+ const dryRun = args.dryRun;
323
+ const force = args.force;
324
+ const newBranch = args.newBranch;
325
+
326
+ console.log(
327
+ `cloud:branch-switch ${newBranch}${dryRun ? ' --dry-run' : ''}${force ? ' --force' : ''}`,
328
+ );
329
+
330
+ // Step 1: verify branch exists on origin
331
+ process.stdout.write('Step 1: verifying branch exists on origin... ');
332
+ if (!branchExistsOnOrigin(newBranch)) {
333
+ console.error(`\nError: '${newBranch}' does not exist on origin.`);
334
+ process.exit(1);
335
+ }
336
+ console.log('✓');
337
+
338
+ // Step 2: print launchd repoint instruction
339
+ console.log('\nStep 2: launchd repoint (operator action — run once after the switch):');
340
+ console.log(` BRANCH=${newBranch} bash scripts/operator/install-launchd-agents.sh`);
341
+ console.log(' (auto-mode classifier blocks ~/Library/LaunchAgents writes from Claude Code)');
342
+
343
+ // Step 3: load dashboard state
344
+ const baseUrl = readDashboardBaseUrl();
345
+ const tok = readToken();
346
+ if (!tok) {
347
+ console.error(`\nError: no dashboard token at ${TOKEN_FILE}`);
348
+ process.exit(1);
349
+ }
350
+
351
+ console.log('\nStep 3: fetching dashboard /data...');
352
+ let data;
353
+ try {
354
+ data = await fetchDashboardData(baseUrl, tok);
355
+ } catch (e) {
356
+ console.error(`Error: ${e.message}`);
357
+ console.error(` Dashboard: ${baseUrl}`);
358
+ process.exit(1);
359
+ }
360
+
361
+ const roster = readRoster();
362
+ if (!roster.length) {
363
+ console.error('Error: roster is empty — no VMs to switch.');
364
+ process.exit(1);
365
+ }
366
+
367
+ const { toSwitch, skipped } = classifyVmsForSwitch(roster, data.vms, newBranch, force);
368
+
369
+ console.log('\nPlan:');
370
+ for (const n of toSwitch) {
371
+ const cur = vmClientBranch(data.vms, n) || '?';
372
+ console.log(` VM-${n}: RECONFIGURE ${cur} → ${newBranch}`);
373
+ }
374
+ for (const s of skipped) console.log(` VM-${s.n}: SKIP — ${s.reason}`);
375
+
376
+ if (!toSwitch.length) {
377
+ console.log('\nNothing to do — all VMs already at target branch or skipped.');
378
+ return;
379
+ }
380
+
381
+ if (dryRun) {
382
+ console.log('\n--dry-run: plan printed, no side effects.');
383
+ return;
384
+ }
385
+
386
+ // Step 4: fire Reconfigure for each VM
387
+ console.log(`\nStep 4: firing Reconfigure on ${toSwitch.length} VM(s)...`);
388
+ const { fired, rejected } = await fireReconfigures(baseUrl, tok, toSwitch, newBranch, force);
389
+ for (const r of rejected) skipped.push(r);
390
+
391
+ if (!fired.length) {
392
+ console.log('\nNo VMs accepted Reconfigure.');
393
+ return;
394
+ }
395
+
396
+ // Step 5: wait for jobs to complete
397
+ console.log(`\nStep 5: waiting for ${fired.length} job(s) (timeout ${TIMEOUT_SEC}s, poll ${POLL_SEC}s)...`);
398
+ const results = await waitForReconfigureJobs(baseUrl, tok, fired, POLL_SEC, TIMEOUT_SEC);
399
+
400
+ const succeeded = fired.filter((n) => results[n] === 'done');
401
+ const failed = fired.filter((n) => results[n] !== 'done');
402
+
403
+ // Step 6: fixup-vm-creds for successfully reconfigured VMs
404
+ if (succeeded.length && fs.existsSync(FIXUP_SCRIPT)) {
405
+ console.log('\nStep 6: running fixup-vm-creds on reconfigured VMs...');
406
+ for (const n of succeeded) runFixupVmCreds(n);
407
+ }
408
+
409
+ console.log('\nSummary:');
410
+ if (succeeded.length) console.log(` reconfigured: ${succeeded.map((n) => `VM-${n}`).join(', ')}`);
411
+ if (skipped.length) console.log(` skipped: ${skipped.map((s) => `VM-${s.n}`).join(', ')}`);
412
+ if (failed.length) console.log(` failed: ${failed.map((n) => `VM-${n} (${results[n]})`).join(', ')}`);
413
+
414
+ console.log(`\nDone. Remember to run:`);
415
+ console.log(` BRANCH=${newBranch} bash scripts/operator/install-launchd-agents.sh`);
416
+
417
+ if (failed.length) process.exit(1);
418
+ }
419
+
420
+ module.exports = {
421
+ parseBranchSwitchArgs,
422
+ isValidBranchName,
423
+ findVm,
424
+ isReconfigureTerminal,
425
+ reconfigureResult,
426
+ vmHasActiveSession,
427
+ vmClientBranch,
428
+ buildReconfigureQuery,
429
+ classifyVmsForSwitch,
430
+ fireReconfigures,
431
+ readRoster,
432
+ readToken,
433
+ readDashboardBaseUrl,
434
+ branchExistsOnOrigin,
435
+ };
436
+
437
+ if (require.main === module) {
438
+ branchSwitchMain().catch((e) => {
439
+ console.error(`Error: ${e.message || e}`);
440
+ process.exit(1);
441
+ });
442
+ }