@atolis-hq/corum 0.1.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.
Files changed (39) hide show
  1. package/README.md +223 -0
  2. package/dist/src/adapters/index.js +12 -0
  3. package/dist/src/adapters/openapi/index.js +12 -0
  4. package/dist/src/adapters/openapi/mapper.js +218 -0
  5. package/dist/src/adapters/openapi/parser.js +16 -0
  6. package/dist/src/bin/corum.js +164 -0
  7. package/dist/src/cli.js +20 -0
  8. package/dist/src/graph/index.js +128 -0
  9. package/dist/src/graph/overlay.js +136 -0
  10. package/dist/src/import/config.js +39 -0
  11. package/dist/src/import/runner.js +56 -0
  12. package/dist/src/loader/cluster-loader.js +120 -0
  13. package/dist/src/loader/constants.js +32 -0
  14. package/dist/src/loader/edge-loader.js +59 -0
  15. package/dist/src/loader/fs-utils.js +20 -0
  16. package/dist/src/loader/index.js +108 -0
  17. package/dist/src/loader/pack-loader.js +99 -0
  18. package/dist/src/mcp/index.js +333 -0
  19. package/dist/src/mcp/serializers.js +68 -0
  20. package/dist/src/openapi-to-api-endpoints.js +240 -0
  21. package/dist/src/reconcile/index.js +46 -0
  22. package/dist/src/schema/index.js +16 -0
  23. package/dist/src/source/config-file.js +22 -0
  24. package/dist/src/source/config.js +71 -0
  25. package/dist/src/source/content-utils.js +13 -0
  26. package/dist/src/source/file-source.js +135 -0
  27. package/dist/src/source/git-cache.js +54 -0
  28. package/dist/src/source/git-source.js +333 -0
  29. package/dist/src/source/index.js +8 -0
  30. package/dist/src/web/server.js +557 -0
  31. package/dist/src/writer/graph-writer.js +153 -0
  32. package/package.json +36 -0
  33. package/web/app.jsx +668 -0
  34. package/web/favicon.svg +19 -0
  35. package/web/index.html +41 -0
  36. package/web/nav.js +141 -0
  37. package/web/primitives.jsx +583 -0
  38. package/web/router.js +49 -0
  39. package/web/style.css +827 -0
@@ -0,0 +1,557 @@
1
+ import express from 'express';
2
+ import path from 'node:path';
3
+ import { existsSync, readFileSync, readdirSync, watch } from 'node:fs';
4
+ import { readdir } from 'node:fs/promises';
5
+ import { fileURLToPath, pathToFileURL } from 'node:url';
6
+ import { parse as parseYaml } from 'yaml';
7
+ import { computeClusterOverlay, getClusterView, listNodes } from '../graph/index.js';
8
+ import { loadGraph, loadMultiGraph } from '../loader/index.js';
9
+ import { VALID_EDGE_TYPE_SET } from '../loader/constants.js';
10
+ import { getOwnedSections } from '../loader/pack-loader.js';
11
+ import { QueryError } from '../schema/index.js';
12
+ import { createGraphRuntimeConfig } from '../source/config.js';
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = path.dirname(__filename);
15
+ const WEB_DIR = path.join(__dirname, '..', '..', '..', 'web');
16
+ function createMultiGraphCache(source) {
17
+ let cache = null;
18
+ return {
19
+ async get() {
20
+ if (!cache)
21
+ cache = await loadMultiGraph({ source });
22
+ return cache;
23
+ },
24
+ invalidate() {
25
+ cache = null;
26
+ },
27
+ };
28
+ }
29
+ async function getGraphForRef(ref, cache, fallback) {
30
+ const multi = await cache.get();
31
+ const branch = multi.branches.find(item => item.ref === ref);
32
+ return branch?.graph ?? fallback;
33
+ }
34
+ async function getPluginFiles() {
35
+ try {
36
+ const files = await readdir(path.join(WEB_DIR, 'plugins'));
37
+ return files.filter(file => file.endsWith('.jsx'));
38
+ }
39
+ catch {
40
+ return [];
41
+ }
42
+ }
43
+ function getNavigationOwnership(graph, node) {
44
+ let match;
45
+ for (const parent of graph.nodesById.values()) {
46
+ if (parent.id === node.id)
47
+ continue;
48
+ const parentTemplate = graph.templates.get(parent.template);
49
+ if (!parentTemplate)
50
+ continue;
51
+ for (const [section, childTemplate] of Object.entries(getOwnedSections(parentTemplate))) {
52
+ if (childTemplate !== node.template)
53
+ continue;
54
+ if (!node.id.startsWith(`${parent.id}.${section}.`))
55
+ continue;
56
+ if (!match || parent.id.length > match.parentId.length) {
57
+ match = { parentId: parent.id, ownedSection: section };
58
+ }
59
+ }
60
+ }
61
+ return match;
62
+ }
63
+ function summarizeNodeForNavigation(graph, node) {
64
+ const ownership = getNavigationOwnership(graph, node);
65
+ return {
66
+ ...node,
67
+ ...(ownership ?? {}),
68
+ };
69
+ }
70
+ function resolveNodeRef(graph, node, rawValue) {
71
+ if (rawValue.startsWith('#/schemas/')) {
72
+ const name = rawValue.slice(10);
73
+ const id = `${node.id}.schemas.${name}`;
74
+ return graph.nodesById.has(id) ? { display: name, nodeId: id } : { display: name };
75
+ }
76
+ if (rawValue.startsWith('#/enums/')) {
77
+ const name = rawValue.slice(8);
78
+ const id = `${node.id}.enums.${name}`;
79
+ return graph.nodesById.has(id) ? { display: name, nodeId: id } : { display: name };
80
+ }
81
+ if (graph.nodesById.has(rawValue))
82
+ return { display: rawValue, nodeId: rawValue };
83
+ return { display: rawValue };
84
+ }
85
+ function getPropertySchemas(templateProperties) {
86
+ if (Array.isArray(templateProperties.allOf)) {
87
+ const merged = {};
88
+ for (const schema of templateProperties.allOf) {
89
+ Object.assign(merged, getPropertySchemas(schema));
90
+ }
91
+ return merged;
92
+ }
93
+ if (typeof templateProperties.properties === 'object' && templateProperties.properties !== null) {
94
+ return templateProperties.properties;
95
+ }
96
+ return {};
97
+ }
98
+ function annotateNodeRefProperties(graph, node, template) {
99
+ if (!template.properties)
100
+ return node.properties;
101
+ const propSchemas = getPropertySchemas(template.properties);
102
+ const result = { ...node.properties };
103
+ for (const [key, schema] of Object.entries(propSchemas)) {
104
+ const value = result[key];
105
+ if (value === undefined)
106
+ continue;
107
+ if (schema.format === 'node-ref' && typeof value === 'string') {
108
+ result[key] = resolveNodeRef(graph, node, value);
109
+ }
110
+ else if (schema.type === 'object' &&
111
+ typeof schema.additionalProperties === 'object' &&
112
+ schema.additionalProperties !== null &&
113
+ schema.additionalProperties.format === 'node-ref' &&
114
+ typeof value === 'object' &&
115
+ value !== null &&
116
+ !Array.isArray(value)) {
117
+ result[key] = Object.fromEntries(Object.entries(value).map(([k, v]) => typeof v === 'string' ? [k, resolveNodeRef(graph, node, v)] : [k, v]));
118
+ }
119
+ }
120
+ return result;
121
+ }
122
+ function annotateNode(graph, node) {
123
+ const template = graph.templates.get(node.template);
124
+ return template ? { ...node, properties: annotateNodeRefProperties(graph, node, template) } : node;
125
+ }
126
+ function parseIncludeEdges(value) {
127
+ if (typeof value !== 'string' || value.trim() === '')
128
+ return [];
129
+ const types = value
130
+ .split(',')
131
+ .map(item => item.trim())
132
+ .filter((item) => VALID_EDGE_TYPE_SET.has(item));
133
+ return [...new Set(types)];
134
+ }
135
+ function createReloadEvents() {
136
+ const clients = new Set();
137
+ let version = 0;
138
+ return {
139
+ subscribe(res) {
140
+ clients.add(res);
141
+ res.writeHead(200, {
142
+ 'Content-Type': 'text/event-stream',
143
+ 'Cache-Control': 'no-cache',
144
+ Connection: 'keep-alive',
145
+ });
146
+ res.write(`event: connected\ndata: ${JSON.stringify({ version })}\n\n`);
147
+ res.on('close', () => clients.delete(res));
148
+ },
149
+ notify() {
150
+ version += 1;
151
+ const payload = `event: graph-reloaded\ndata: ${JSON.stringify({ version })}\n\n`;
152
+ for (const client of clients) {
153
+ client.write(payload);
154
+ }
155
+ },
156
+ };
157
+ }
158
+ function replaceGraph(target, source) {
159
+ target.nodesById = source.nodesById;
160
+ target.edgesByFrom = source.edgesByFrom;
161
+ target.edgesByTo = source.edgesByTo;
162
+ target.templates = source.templates;
163
+ target.diagnostics = source.diagnostics;
164
+ }
165
+ function isFileWatcherEnabled(options) {
166
+ if (options.fileWatcher !== undefined)
167
+ return options.fileWatcher;
168
+ const value = process.env.CORUM_FILE_WATCHER ?? process.env.CORUM_WATCH;
169
+ return value === '1' || value === 'true' || value === 'yes';
170
+ }
171
+ function resolvePackDirs(graphPath) {
172
+ const graphYamlPath = path.join(graphPath, 'graph.yaml');
173
+ if (!existsSync(graphYamlPath)) {
174
+ return [path.resolve(graphPath, '.corum/packs')];
175
+ }
176
+ try {
177
+ const doc = parseYaml(readFileSync(graphYamlPath, 'utf-8'));
178
+ const packs = Array.isArray(doc.templatePacks) ? doc.templatePacks : [];
179
+ return packs
180
+ .filter((pack) => typeof pack === 'object' && pack !== null && typeof pack.path === 'string')
181
+ .map(pack => path.resolve(graphPath, pack.path));
182
+ }
183
+ catch {
184
+ return [];
185
+ }
186
+ }
187
+ function listDirectories(root) {
188
+ if (!existsSync(root))
189
+ return [];
190
+ const dirs = [root];
191
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
192
+ if (entry.isDirectory()) {
193
+ dirs.push(...listDirectories(path.join(root, entry.name)));
194
+ }
195
+ }
196
+ return dirs;
197
+ }
198
+ function watchRoot(root, onChange) {
199
+ if (!existsSync(root))
200
+ return [];
201
+ try {
202
+ return [watch(root, { recursive: true }, (_eventType, filename) => onChange(filename))];
203
+ }
204
+ catch {
205
+ return listDirectories(root).map(dir => watch(dir, (_eventType, filename) => onChange(filename)));
206
+ }
207
+ }
208
+ function isRelevantWatchEvent(filename) {
209
+ if (!filename)
210
+ return true;
211
+ const name = String(filename).replace(/\\/g, '/');
212
+ return name.endsWith('.yaml') || name.endsWith('.yml');
213
+ }
214
+ export function startGraphFileWatcher(graph, options) {
215
+ const { graphPath, onReload } = options;
216
+ const logger = options.logger ?? console.error;
217
+ const debounceMs = options.debounceMs ?? 150;
218
+ let watchers = [];
219
+ let timer;
220
+ let reloading = false;
221
+ function closeWatchers() {
222
+ for (const watcher of watchers)
223
+ watcher.close();
224
+ watchers = [];
225
+ }
226
+ function refreshWatchers() {
227
+ closeWatchers();
228
+ const roots = [...new Set([path.resolve(graphPath), ...resolvePackDirs(graphPath)])];
229
+ watchers = roots.flatMap(root => watchRoot(root, scheduleReload));
230
+ }
231
+ async function reload() {
232
+ if (reloading)
233
+ return;
234
+ reloading = true;
235
+ try {
236
+ const nextGraph = await loadGraph({ graphPath, strict: true });
237
+ replaceGraph(graph, nextGraph);
238
+ refreshWatchers();
239
+ onReload?.();
240
+ logger(`[corum] graph reloaded after file change`);
241
+ }
242
+ catch (err) {
243
+ logger(`[corum] graph reload failed: ${err}`);
244
+ }
245
+ finally {
246
+ reloading = false;
247
+ }
248
+ }
249
+ function scheduleReload(filename) {
250
+ if (!isRelevantWatchEvent(filename))
251
+ return;
252
+ if (timer)
253
+ clearTimeout(timer);
254
+ timer = setTimeout(() => {
255
+ timer = undefined;
256
+ void reload();
257
+ }, debounceMs);
258
+ }
259
+ refreshWatchers();
260
+ logger(`[corum] file watcher enabled`);
261
+ return () => {
262
+ if (timer)
263
+ clearTimeout(timer);
264
+ closeWatchers();
265
+ };
266
+ }
267
+ export function createApp(graph, reloadEvents = createReloadEvents(), source, multiCache, onReloadRequest) {
268
+ const app = express();
269
+ app.get('/health', (_req, res) => {
270
+ res.json({ ok: true });
271
+ });
272
+ app.get('/api/events', (_req, res) => {
273
+ reloadEvents.subscribe(res);
274
+ });
275
+ app.get('/api/templates', async (req, res) => {
276
+ const includeCore = req.query.includeCore === 'true';
277
+ let targetGraph = graph;
278
+ if (typeof req.query.ref === 'string' && multiCache) {
279
+ targetGraph = await getGraphForRef(req.query.ref, multiCache, graph);
280
+ }
281
+ const templates = [...targetGraph.templates.values()]
282
+ .filter(template => includeCore || !template.info?.core)
283
+ .sort((a, b) => a.name.localeCompare(b.name))
284
+ .map(template => ({
285
+ name: template.name,
286
+ version: template.info?.version,
287
+ core: template.info?.core ?? false,
288
+ abstract: template.info?.abstract ?? false,
289
+ extends: template.extends,
290
+ description: template.info?.description,
291
+ ui: template.ui,
292
+ }));
293
+ res.json(templates);
294
+ });
295
+ app.get('/api/nodes', async (req, res) => {
296
+ const { template, component, state, stability } = req.query;
297
+ const includeCore = req.query.includeCore === 'true';
298
+ const filter = {
299
+ template: typeof template === 'string' ? template : undefined,
300
+ component: typeof component === 'string' ? component : undefined,
301
+ state: typeof state === 'string' ? state : undefined,
302
+ stability: typeof stability === 'string' ? stability : undefined,
303
+ };
304
+ let targetGraph = graph;
305
+ if (typeof req.query.ref === 'string' && multiCache) {
306
+ targetGraph = await getGraphForRef(req.query.ref, multiCache, graph);
307
+ }
308
+ const nodes = listNodes(targetGraph, filter)
309
+ .filter(node => includeCore || !targetGraph.templates.get(node.template)?.info?.core)
310
+ .map(node => {
311
+ const ownership = summarizeNodeForNavigation(targetGraph, node);
312
+ return {
313
+ id: ownership.id,
314
+ template: ownership.template,
315
+ component: ownership.component,
316
+ state: ownership.state,
317
+ stability: ownership.stability,
318
+ parentId: ownership.parentId,
319
+ ownedSection: ownership.ownedSection,
320
+ };
321
+ });
322
+ res.json(nodes);
323
+ });
324
+ app.get('/api/cluster', async (req, res) => {
325
+ const nodeId = typeof req.query.nodeId === 'string' ? req.query.nodeId : undefined;
326
+ if (!nodeId) {
327
+ res.status(400).json({ error: 'nodeId query param required' });
328
+ return;
329
+ }
330
+ try {
331
+ let targetGraph = graph;
332
+ if (typeof req.query.ref === 'string' && multiCache) {
333
+ targetGraph = await getGraphForRef(req.query.ref, multiCache, graph);
334
+ }
335
+ const cluster = getClusterView(targetGraph, nodeId, parseIncludeEdges(req.query.includeEdges));
336
+ const rawOverlayRefs = req.query.overlayRefs;
337
+ const overlayRefs = Array.isArray(rawOverlayRefs)
338
+ ? rawOverlayRefs.filter((r) => typeof r === 'string' && r.length > 0)
339
+ : typeof rawOverlayRefs === 'string' && rawOverlayRefs.length > 0
340
+ ? [rawOverlayRefs]
341
+ : [];
342
+ let overlay = null;
343
+ if (overlayRefs.length > 0 && multiCache && typeof req.query.ref === 'string') {
344
+ const multi = await multiCache.get();
345
+ overlay = computeClusterOverlay(multi, req.query.ref, overlayRefs, nodeId);
346
+ }
347
+ res.json({
348
+ root: summarizeNodeForNavigation(targetGraph, annotateNode(targetGraph, cluster.root)),
349
+ descendants: cluster.descendants.map(child => summarizeNodeForNavigation(targetGraph, annotateNode(targetGraph, child))),
350
+ includedNodes: cluster.includedNodes.map(node => summarizeNodeForNavigation(targetGraph, annotateNode(targetGraph, node))),
351
+ edges: cluster.edges,
352
+ overlay,
353
+ });
354
+ }
355
+ catch (err) {
356
+ if (err instanceof QueryError) {
357
+ res.status(404).json({ error: err.message });
358
+ }
359
+ else {
360
+ res.status(500).json({ error: 'Internal server error' });
361
+ }
362
+ }
363
+ });
364
+ app.get('/api/plugins', (_req, res) => {
365
+ getPluginFiles()
366
+ .then(files => res.json(files))
367
+ .catch(() => res.json([]));
368
+ });
369
+ app.get('/api/branches', async (_req, res) => {
370
+ if (!source) {
371
+ res.status(501).json({ error: 'multi-branch requires a configured source' });
372
+ return;
373
+ }
374
+ try {
375
+ const multi = multiCache ? await multiCache.get() : await loadMultiGraph({ source });
376
+ res.json({
377
+ default: multi.default.ref,
378
+ branches: multi.branches.map(branch => ({
379
+ ref: branch.ref,
380
+ isDefault: branch.isDefault,
381
+ })),
382
+ results: multi.branchResults,
383
+ });
384
+ }
385
+ catch (err) {
386
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Internal server error' });
387
+ }
388
+ });
389
+ app.post('/api/reload', async (_req, res) => {
390
+ try {
391
+ await onReloadRequest?.();
392
+ res.status(202).json({ ok: true });
393
+ }
394
+ catch (err) {
395
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Internal server error' });
396
+ }
397
+ });
398
+ app.get('/api/overlay/:ref(*)', async (req, res) => {
399
+ if (!source) {
400
+ res.status(501).json({ error: 'multi-branch requires a configured source' });
401
+ return;
402
+ }
403
+ try {
404
+ const multi = multiCache ? await multiCache.get() : await loadMultiGraph({ source });
405
+ const overlay = multi.overlay(req.params.ref);
406
+ res.json({
407
+ viewingRef: overlay.viewingRef,
408
+ nodes: [...overlay.nodes.values()].map(node => ({
409
+ id: node.id,
410
+ ghostState: node.ghostState,
411
+ branches: [...node.presence.keys()],
412
+ node: node.presence.get(overlay.viewingRef) ?? [...node.presence.values()][0],
413
+ })),
414
+ });
415
+ }
416
+ catch (err) {
417
+ if (err instanceof QueryError) {
418
+ res.status(400).json({ error: err.message });
419
+ }
420
+ else {
421
+ res.status(500).json({ error: err instanceof Error ? err.message : 'Internal server error' });
422
+ }
423
+ }
424
+ });
425
+ app.use(express.static(WEB_DIR, {
426
+ setHeaders(res, filePath) {
427
+ if (filePath.endsWith('.jsx')) {
428
+ res.setHeader('Content-Type', 'text/babel; charset=utf-8');
429
+ }
430
+ },
431
+ }));
432
+ return app;
433
+ }
434
+ export function startWebServer(graph, options = {}) {
435
+ const portSource = options.port !== undefined
436
+ ? 'options.port'
437
+ : process.env.CORUM_WEB_PORT !== undefined
438
+ ? 'CORUM_WEB_PORT'
439
+ : 'default';
440
+ const graphPathSource = options.graphPath !== undefined
441
+ ? 'options.graphPath'
442
+ : process.env.CORUM_GRAPH_PATH !== undefined
443
+ ? 'CORUM_GRAPH_PATH'
444
+ : 'default';
445
+ const port = options.port ?? parseInt(process.env.CORUM_WEB_PORT ?? '3000', 10);
446
+ const graphPath = options.graphPath ?? process.env.CORUM_GRAPH_PATH ?? path.join(process.cwd(), '.corum/graph');
447
+ const logger = options.logger ?? console.error;
448
+ const reloadEvents = createReloadEvents();
449
+ const multiCache = options.source ? createMultiGraphCache(options.source) : undefined;
450
+ let reloadInFlight = null;
451
+ async function reloadState(reason) {
452
+ if (reloadInFlight)
453
+ return reloadInFlight;
454
+ reloadInFlight = (async () => {
455
+ const nextGraph = options.source
456
+ ? await loadGraph({ source: options.source, strict: true })
457
+ : await loadGraph({ graphPath, strict: true });
458
+ replaceGraph(graph, nextGraph);
459
+ multiCache?.invalidate();
460
+ reloadEvents.notify();
461
+ logger(`[corum] graph reloaded after ${reason}`);
462
+ })();
463
+ try {
464
+ await reloadInFlight;
465
+ }
466
+ finally {
467
+ reloadInFlight = null;
468
+ }
469
+ }
470
+ const app = createApp(graph, reloadEvents, options.source, multiCache, () => reloadState('manual reload'));
471
+ const stopWatcher = isFileWatcherEnabled(options)
472
+ ? startGraphFileWatcher(graph, {
473
+ graphPath,
474
+ debounceMs: options.fileWatcherDebounceMs,
475
+ logger,
476
+ onReload: () => {
477
+ multiCache?.invalidate();
478
+ reloadEvents.notify();
479
+ },
480
+ })
481
+ : undefined;
482
+ const stopPoller = startGitSourcePoller(options.source, options.gitPollSeconds, logger, () => reloadState('git poll'));
483
+ return new Promise((resolve, reject) => {
484
+ const server = app.listen(port, () => {
485
+ const addr = server.address();
486
+ if (options.port !== 0) {
487
+ // graphPath is informational only — the graph object was already loaded by the caller
488
+ logger(`[corum web] config graphPath=${graphPath} (${graphPathSource})`);
489
+ logger(`[corum web] config port=${addr.port} (${portSource})`);
490
+ logger(`[corum web] config webDir=${WEB_DIR}`);
491
+ logger(`[corum web] http://localhost:${addr.port}`);
492
+ }
493
+ resolve({
494
+ port: addr.port,
495
+ close: () => new Promise((res, rej) => {
496
+ stopWatcher?.();
497
+ stopPoller?.();
498
+ server.close(err => (err ? rej(err) : res()));
499
+ }),
500
+ });
501
+ });
502
+ server.on('error', reject);
503
+ });
504
+ }
505
+ function startGitSourcePoller(source, pollSeconds, logger, onChange) {
506
+ if (!source || !pollSeconds || !hasReloadSignature(source))
507
+ return undefined;
508
+ const pollableSource = source;
509
+ let timer;
510
+ let stopped = false;
511
+ let inFlight = false;
512
+ let lastSignature = null;
513
+ async function check() {
514
+ if (stopped || inFlight)
515
+ return;
516
+ inFlight = true;
517
+ try {
518
+ const signature = await pollableSource.reloadSignature();
519
+ if (lastSignature !== null && signature !== lastSignature) {
520
+ await onChange();
521
+ }
522
+ lastSignature = signature;
523
+ }
524
+ catch (err) {
525
+ logger(`[corum] git poll failed: ${err}`);
526
+ }
527
+ finally {
528
+ inFlight = false;
529
+ }
530
+ }
531
+ void check();
532
+ timer = setInterval(() => {
533
+ void check();
534
+ }, pollSeconds * 1000);
535
+ logger(`[corum] git poll enabled (${pollSeconds}s)`);
536
+ return () => {
537
+ stopped = true;
538
+ if (timer)
539
+ clearInterval(timer);
540
+ };
541
+ }
542
+ function hasReloadSignature(source) {
543
+ return typeof source.reloadSignature === 'function';
544
+ }
545
+ function isEntrypoint() {
546
+ return process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
547
+ }
548
+ if (isEntrypoint()) {
549
+ const config = createGraphRuntimeConfig();
550
+ const graph = await loadGraph({ source: config.source, strict: true });
551
+ await startWebServer(graph, {
552
+ graphPath: config.graphPath,
553
+ source: config.source,
554
+ gitPollSeconds: config.gitPollSeconds,
555
+ fileWatcher: config.fileWatcherGraphPath && process.argv.includes('--watch') ? true : undefined,
556
+ });
557
+ }