@hcgstudio/ogma 0.0.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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +39 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +155 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/defineOgmaReview.d.ts +3 -0
  8. package/dist/defineOgmaReview.d.ts.map +1 -0
  9. package/dist/defineOgmaReview.js +4 -0
  10. package/dist/defineOgmaReview.js.map +1 -0
  11. package/dist/index.d.ts +5 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +4 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/node/project.d.ts +18 -0
  16. package/dist/node/project.d.ts.map +1 -0
  17. package/dist/node/project.js +104 -0
  18. package/dist/node/project.js.map +1 -0
  19. package/dist/node/server.d.ts +21 -0
  20. package/dist/node/server.d.ts.map +1 -0
  21. package/dist/node/server.js +476 -0
  22. package/dist/node/server.js.map +1 -0
  23. package/dist/node/templates.d.ts +5 -0
  24. package/dist/node/templates.d.ts.map +1 -0
  25. package/dist/node/templates.js +145 -0
  26. package/dist/node/templates.js.map +1 -0
  27. package/dist/types.d.ts +100 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +2 -0
  30. package/dist/types.js.map +1 -0
  31. package/dist/viewer/OgmaReviewApp.d.ts +7 -0
  32. package/dist/viewer/OgmaReviewApp.d.ts.map +1 -0
  33. package/dist/viewer/OgmaReviewApp.js +387 -0
  34. package/dist/viewer/OgmaReviewApp.js.map +1 -0
  35. package/dist/viewer/client.d.ts +2 -0
  36. package/dist/viewer/client.d.ts.map +1 -0
  37. package/dist/viewer/client.js +13 -0
  38. package/dist/viewer/client.js.map +1 -0
  39. package/dist/viewer/defaultReview.d.ts +4 -0
  40. package/dist/viewer/defaultReview.d.ts.map +1 -0
  41. package/dist/viewer/defaultReview.js +55 -0
  42. package/dist/viewer/defaultReview.js.map +1 -0
  43. package/dist/viewer/normalizeReviewModule.d.ts +3 -0
  44. package/dist/viewer/normalizeReviewModule.d.ts.map +1 -0
  45. package/dist/viewer/normalizeReviewModule.js +58 -0
  46. package/dist/viewer/normalizeReviewModule.js.map +1 -0
  47. package/package.json +47 -0
  48. package/src/cli.ts +194 -0
  49. package/src/defineOgmaReview.ts +5 -0
  50. package/src/index.ts +17 -0
  51. package/src/node/project.ts +143 -0
  52. package/src/node/server.ts +598 -0
  53. package/src/node/templates.ts +148 -0
  54. package/src/types.ts +111 -0
  55. package/src/viewer/OgmaReviewApp.tsx +1099 -0
  56. package/src/viewer/client.tsx +18 -0
  57. package/src/viewer/defaultReview.tsx +168 -0
  58. package/src/viewer/normalizeReviewModule.ts +87 -0
  59. package/src/viewer/styles.css +1140 -0
  60. package/src/viewer/virtual.d.ts +11 -0
@@ -0,0 +1,598 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createRequire } from 'node:module';
3
+ import { readFile, writeFile } from 'node:fs/promises';
4
+ import type { IncomingMessage, ServerResponse } from 'node:http';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { createServer, type Plugin, type ViteDevServer } from 'vite';
8
+ import type {
9
+ OgmaAnnotation,
10
+ OgmaAnnotationStatus,
11
+ OgmaClientConfig,
12
+ OgmaFeedbackExport,
13
+ OgmaReviewSession,
14
+ OgmaServerStatus,
15
+ OgmaSessionHistoryEntry,
16
+ OgmaViewportSnapshot
17
+ } from '../types.js';
18
+ import { ensureOgmaProject, type OgmaResolvedProject } from './project.js';
19
+ import { DEFAULT_SKILL_URL } from './templates.js';
20
+
21
+ type NextFunction = (error?: unknown) => void;
22
+
23
+ export interface OgmaStartOptions {
24
+ cwd: string;
25
+ host: string;
26
+ open: boolean;
27
+ packageRoot: string;
28
+ port: number;
29
+ review?: string;
30
+ skillUrl?: string;
31
+ }
32
+
33
+ interface PackageManifest {
34
+ version?: string;
35
+ }
36
+
37
+ function toPosixPath(value: string) {
38
+ return value.split(path.sep).join('/');
39
+ }
40
+
41
+ function fsImportSpecifier(value: string) {
42
+ return `/@fs/${toPosixPath(value)}`;
43
+ }
44
+
45
+ function isObject(value: unknown): value is Record<string, unknown> {
46
+ return typeof value === 'object' && value !== null;
47
+ }
48
+
49
+ function nowIso() {
50
+ return new Date().toISOString();
51
+ }
52
+
53
+ function emptySession(): OgmaReviewSession {
54
+ return {
55
+ reviewId: 'ogma-review',
56
+ annotations: [],
57
+ updatedAt: nowIso()
58
+ };
59
+ }
60
+
61
+ async function readPackageVersion(packageRoot: string) {
62
+ try {
63
+ const manifest = JSON.parse(
64
+ await readFile(path.join(packageRoot, 'package.json'), 'utf8')
65
+ ) as PackageManifest;
66
+
67
+ return manifest.version ?? '0.0.0';
68
+ } catch {
69
+ return '0.0.0';
70
+ }
71
+ }
72
+
73
+ async function readRequestBody(request: IncomingMessage) {
74
+ const chunks: Buffer[] = [];
75
+
76
+ for await (const chunk of request) {
77
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
78
+ }
79
+
80
+ return Buffer.concat(chunks).toString('utf8');
81
+ }
82
+
83
+ function sendJson(response: ServerResponse, statusCode: number, value: unknown) {
84
+ response.statusCode = statusCode;
85
+ response.setHeader('content-type', 'application/json; charset=utf-8');
86
+ response.end(`${JSON.stringify(value, null, 2)}\n`);
87
+ }
88
+
89
+ function sendText(response: ServerResponse, statusCode: number, value: string, contentType = 'text/plain') {
90
+ response.statusCode = statusCode;
91
+ response.setHeader('content-type', `${contentType}; charset=utf-8`);
92
+ response.end(value);
93
+ }
94
+
95
+ function isStatus(value: unknown): value is OgmaAnnotationStatus {
96
+ return value === 'open' || value === 'queued' || value === 'addressed';
97
+ }
98
+
99
+ function sanitizeAnnotation(value: unknown): OgmaAnnotation | null {
100
+ if (!isObject(value)) {
101
+ return null;
102
+ }
103
+
104
+ const id = typeof value.id === 'string' ? value.id : '';
105
+ const screenId = typeof value.screenId === 'string' ? value.screenId : '';
106
+ const statusValue = isStatus(value.status) ? value.status : 'open';
107
+
108
+ if (!id || !screenId) {
109
+ return null;
110
+ }
111
+
112
+ return {
113
+ id,
114
+ screenId,
115
+ x: typeof value.x === 'number' ? value.x : 50,
116
+ y: typeof value.y === 'number' ? value.y : 50,
117
+ title: typeof value.title === 'string' ? value.title : 'Untitled feedback',
118
+ detail: typeof value.detail === 'string' ? value.detail : '',
119
+ status: statusValue,
120
+ action: typeof value.action === 'string' ? value.action : 'Update the referenced JSX screen.',
121
+ createdAt: typeof value.createdAt === 'string' ? value.createdAt : nowIso(),
122
+ updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : nowIso()
123
+ };
124
+ }
125
+
126
+ function sanitizeSession(value: unknown): OgmaReviewSession {
127
+ if (!isObject(value)) {
128
+ return emptySession();
129
+ }
130
+
131
+ const annotations = Array.isArray(value.annotations)
132
+ ? value.annotations
133
+ .map((annotation) => sanitizeAnnotation(annotation))
134
+ .filter((annotation): annotation is OgmaAnnotation => annotation !== null)
135
+ : [];
136
+
137
+ return {
138
+ reviewId: typeof value.reviewId === 'string' ? value.reviewId : 'ogma-review',
139
+ annotations,
140
+ updatedAt: typeof value.updatedAt === 'string' ? value.updatedAt : nowIso()
141
+ };
142
+ }
143
+
144
+ async function readSession(project: OgmaResolvedProject) {
145
+ try {
146
+ return sanitizeSession(JSON.parse(await readFile(project.sessionPath, 'utf8')));
147
+ } catch {
148
+ const session = emptySession();
149
+ await writeSession(project, session);
150
+ return session;
151
+ }
152
+ }
153
+
154
+ async function writeSession(project: OgmaResolvedProject, session: OgmaReviewSession) {
155
+ await writeFile(project.sessionPath, `${JSON.stringify(session, null, 2)}\n`, 'utf8');
156
+ await appendHistory(project, session);
157
+ }
158
+
159
+ function countByStatus(annotations: OgmaAnnotation[]): Record<OgmaAnnotationStatus, number> {
160
+ return {
161
+ addressed: annotations.filter((annotation) => annotation.status === 'addressed').length,
162
+ open: annotations.filter((annotation) => annotation.status === 'open').length,
163
+ queued: annotations.filter((annotation) => annotation.status === 'queued').length
164
+ };
165
+ }
166
+
167
+ async function readHistory(project: OgmaResolvedProject): Promise<OgmaSessionHistoryEntry[]> {
168
+ try {
169
+ const value = JSON.parse(await readFile(project.historyPath, 'utf8'));
170
+ return Array.isArray(value) ? (value as OgmaSessionHistoryEntry[]) : [];
171
+ } catch {
172
+ return [];
173
+ }
174
+ }
175
+
176
+ async function appendHistory(project: OgmaResolvedProject, session: OgmaReviewSession) {
177
+ const history = await readHistory(project);
178
+ const entry: OgmaSessionHistoryEntry = {
179
+ id: `${Date.now()}`,
180
+ annotationCount: session.annotations.length,
181
+ counts: countByStatus(session.annotations),
182
+ reviewId: session.reviewId,
183
+ updatedAt: session.updatedAt
184
+ };
185
+
186
+ history.push(entry);
187
+ await writeFile(project.historyPath, `${JSON.stringify(history.slice(-50), null, 2)}\n`, 'utf8');
188
+ }
189
+
190
+ function buildFeedbackExport(
191
+ session: OgmaReviewSession,
192
+ reviewUrl: string
193
+ ): OgmaFeedbackExport {
194
+ return {
195
+ reviewId: session.reviewId,
196
+ generatedAt: nowIso(),
197
+ reviewUrl,
198
+ annotations: session.annotations.map((annotation) => ({
199
+ id: annotation.id,
200
+ screenId: annotation.screenId,
201
+ title: annotation.title,
202
+ detail: annotation.detail,
203
+ status: annotation.status,
204
+ action: annotation.action,
205
+ location: {
206
+ x: annotation.x,
207
+ y: annotation.y
208
+ }
209
+ }))
210
+ };
211
+ }
212
+
213
+ function sessionFromFeedbackExport(value: unknown): OgmaReviewSession {
214
+ if (!isObject(value) || !Array.isArray(value.annotations)) {
215
+ return emptySession();
216
+ }
217
+
218
+ const timestamp = nowIso();
219
+ const annotations = value.annotations
220
+ .map((item) => {
221
+ if (!isObject(item)) {
222
+ return null;
223
+ }
224
+
225
+ return sanitizeAnnotation({
226
+ id: item.id,
227
+ screenId: item.screenId,
228
+ x: isObject(item.location) && typeof item.location.x === 'number' ? item.location.x : 50,
229
+ y: isObject(item.location) && typeof item.location.y === 'number' ? item.location.y : 50,
230
+ title: item.title,
231
+ detail: item.detail,
232
+ status: item.status,
233
+ action: item.action,
234
+ createdAt: timestamp,
235
+ updatedAt: timestamp
236
+ });
237
+ })
238
+ .filter((annotation): annotation is OgmaAnnotation => annotation !== null);
239
+
240
+ return {
241
+ reviewId: typeof value.reviewId === 'string' ? value.reviewId : 'ogma-review',
242
+ annotations,
243
+ updatedAt: timestamp
244
+ };
245
+ }
246
+
247
+ function sanitizeSnapshot(value: unknown, reviewUrl: string): OgmaViewportSnapshot {
248
+ const timestamp = nowIso();
249
+
250
+ if (!isObject(value)) {
251
+ return {
252
+ id: `${Date.now()}`,
253
+ annotations: [],
254
+ createdAt: timestamp,
255
+ reviewId: 'ogma-review',
256
+ reviewUrl,
257
+ screenId: 'unknown',
258
+ viewportMode: 'desktop'
259
+ };
260
+ }
261
+
262
+ const annotations = Array.isArray(value.annotations)
263
+ ? value.annotations
264
+ .map((annotation) => sanitizeAnnotation(annotation))
265
+ .filter((annotation): annotation is OgmaAnnotation => annotation !== null)
266
+ : [];
267
+
268
+ return {
269
+ id: typeof value.id === 'string' ? value.id : `${Date.now()}`,
270
+ annotations,
271
+ createdAt: typeof value.createdAt === 'string' ? value.createdAt : timestamp,
272
+ reviewId: typeof value.reviewId === 'string' ? value.reviewId : 'ogma-review',
273
+ reviewUrl,
274
+ screenId: typeof value.screenId === 'string' ? value.screenId : 'unknown',
275
+ viewportMode: typeof value.viewportMode === 'string' ? value.viewportMode : 'desktop'
276
+ };
277
+ }
278
+
279
+ function createIndexHtml() {
280
+ return `<!doctype html>
281
+ <html lang="en">
282
+ <head>
283
+ <meta charset="UTF-8" />
284
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
285
+ <title>Ogma Review</title>
286
+ </head>
287
+ <body>
288
+ <div id="ogma-root"></div>
289
+ <script type="module" src="/src/viewer/client.tsx"></script>
290
+ </body>
291
+ </html>
292
+ `;
293
+ }
294
+
295
+ function createRuntimePlugin({
296
+ config,
297
+ packageRoot,
298
+ project,
299
+ status
300
+ }: {
301
+ config: OgmaClientConfig;
302
+ packageRoot: string;
303
+ project: OgmaResolvedProject;
304
+ status: OgmaServerStatus;
305
+ }): Plugin {
306
+ const sourceRoot = path.join(packageRoot, 'src');
307
+ const designModuleId = 'virtual:ogma-design';
308
+ const configModuleId = 'virtual:ogma-config';
309
+ const resolvedDesignModuleId = `\0${designModuleId}`;
310
+ const resolvedConfigModuleId = `\0${configModuleId}`;
311
+
312
+ return {
313
+ name: 'ogma-runtime',
314
+ resolveId(id) {
315
+ if (id === designModuleId) {
316
+ return resolvedDesignModuleId;
317
+ }
318
+
319
+ if (id === configModuleId) {
320
+ return resolvedConfigModuleId;
321
+ }
322
+
323
+ return null;
324
+ },
325
+ load(id) {
326
+ if (id === resolvedDesignModuleId) {
327
+ const reviewImport = fsImportSpecifier(project.designEntry);
328
+ const notesImport = `${fsImportSpecifier(project.notesPath)}?raw`;
329
+ const normalizerImport = fsImportSpecifier(path.join(sourceRoot, 'viewer/normalizeReviewModule.ts'));
330
+
331
+ return [
332
+ `import * as reviewModule from ${JSON.stringify(reviewImport)};`,
333
+ `import productNotes from ${JSON.stringify(notesImport)};`,
334
+ `import { normalizeReviewModule } from ${JSON.stringify(normalizerImport)};`,
335
+ 'export const review = normalizeReviewModule(reviewModule, productNotes);'
336
+ ].join('\n');
337
+ }
338
+
339
+ if (id === resolvedConfigModuleId) {
340
+ return `export const runtimeConfig = ${JSON.stringify(config, null, 2)};`;
341
+ }
342
+
343
+ return null;
344
+ },
345
+ configureServer(server) {
346
+ server.middlewares.use(async (request, response, next: NextFunction) => {
347
+ try {
348
+ await handleRequest({ next, project, request, response, server, status });
349
+ } catch (error) {
350
+ sendJson(response, 500, {
351
+ error: error instanceof Error ? error.message : String(error)
352
+ });
353
+ }
354
+ });
355
+ }
356
+ };
357
+ }
358
+
359
+ async function handleRequest({
360
+ next,
361
+ project,
362
+ request,
363
+ response,
364
+ server,
365
+ status
366
+ }: {
367
+ next: NextFunction;
368
+ project: OgmaResolvedProject;
369
+ request: IncomingMessage;
370
+ response: ServerResponse;
371
+ server: ViteDevServer;
372
+ status: OgmaServerStatus;
373
+ }) {
374
+ const url = new URL(request.url ?? '/', 'http://ogma.local');
375
+
376
+ if (request.method === 'GET' && (url.pathname === '/' || url.pathname === '/review')) {
377
+ const html = await server.transformIndexHtml(url.pathname, createIndexHtml());
378
+ sendText(response, 200, html, 'text/html');
379
+ return;
380
+ }
381
+
382
+ if (!url.pathname.startsWith('/api/ogma')) {
383
+ next();
384
+ return;
385
+ }
386
+
387
+ if (request.method === 'GET' && url.pathname === '/api/ogma/status') {
388
+ sendJson(response, 200, status);
389
+ return;
390
+ }
391
+
392
+ if (request.method === 'GET' && url.pathname === '/api/ogma/session') {
393
+ sendJson(response, 200, await readSession(project));
394
+ return;
395
+ }
396
+
397
+ if (request.method === 'PUT' && url.pathname === '/api/ogma/session') {
398
+ const session = sanitizeSession(JSON.parse(await readRequestBody(request)));
399
+ await writeSession(project, session);
400
+ sendJson(response, 200, session);
401
+ return;
402
+ }
403
+
404
+ if (request.method === 'GET' && url.pathname === '/api/ogma/feedback') {
405
+ sendJson(response, 200, buildFeedbackExport(await readSession(project), status.reviewUrl));
406
+ return;
407
+ }
408
+
409
+ if (request.method === 'PUT' && url.pathname === '/api/ogma/feedback') {
410
+ const session = sessionFromFeedbackExport(JSON.parse(await readRequestBody(request)));
411
+ await writeSession(project, session);
412
+ sendJson(response, 200, session);
413
+ return;
414
+ }
415
+
416
+ if (request.method === 'POST' && url.pathname === '/api/ogma/feedback/export') {
417
+ const exportData = buildFeedbackExport(await readSession(project), status.reviewUrl);
418
+ await writeFile(project.feedbackPath, `${JSON.stringify(exportData, null, 2)}\n`, 'utf8');
419
+ sendJson(response, 200, {
420
+ path: project.feedbackPath,
421
+ feedback: exportData
422
+ });
423
+ return;
424
+ }
425
+
426
+ if (request.method === 'GET' && url.pathname === '/api/ogma/history') {
427
+ sendJson(response, 200, await readHistory(project));
428
+ return;
429
+ }
430
+
431
+ if (request.method === 'POST' && url.pathname === '/api/ogma/snapshots') {
432
+ const snapshot = sanitizeSnapshot(JSON.parse(await readRequestBody(request)), status.reviewUrl);
433
+ const fileName = `${snapshot.createdAt.replace(/[^0-9a-z]/gi, '-')}-${snapshot.screenId}.json`;
434
+ const snapshotPath = path.join(project.snapshotsDir, fileName);
435
+
436
+ await writeFile(snapshotPath, `${JSON.stringify(snapshot, null, 2)}\n`, 'utf8');
437
+ sendJson(response, 200, {
438
+ path: snapshotPath,
439
+ snapshot
440
+ });
441
+ return;
442
+ }
443
+
444
+ sendJson(response, 404, {
445
+ error: `Unknown Ogma API route: ${request.method ?? 'GET'} ${url.pathname}`
446
+ });
447
+ }
448
+
449
+ function openBrowser(url: string) {
450
+ const command =
451
+ process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
452
+ const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
453
+ const child = spawn(command, args, {
454
+ detached: true,
455
+ stdio: 'ignore'
456
+ });
457
+
458
+ child.unref();
459
+ }
460
+
461
+ function resolveReviewUrl(server: ViteDevServer) {
462
+ const localUrl = server.resolvedUrls?.local[0] ?? 'http://localhost:4317/';
463
+ return new URL('/review', localUrl).toString();
464
+ }
465
+
466
+ export function getPackageRootFromImportMeta(metaUrl: string) {
467
+ return path.dirname(path.dirname(fileURLToPath(metaUrl)));
468
+ }
469
+
470
+ export async function startOgmaServer(options: OgmaStartOptions) {
471
+ const project = await ensureOgmaProject({
472
+ cwd: options.cwd,
473
+ review: options.review
474
+ });
475
+ const requireFromPackage = createRequire(path.join(options.packageRoot, 'package.json'));
476
+ const version = await readPackageVersion(options.packageRoot);
477
+ const startedAt = nowIso();
478
+ const runtimeConfig: OgmaClientConfig = {
479
+ cwd: options.cwd,
480
+ dataDir: project.dataDir,
481
+ defaultDesignDir: project.defaultDesignDir,
482
+ reviewUrl: `http://localhost:${options.port}/review`,
483
+ skillUrl: options.skillUrl ?? DEFAULT_SKILL_URL,
484
+ serverStartedAt: startedAt
485
+ };
486
+ const status: OgmaServerStatus = {
487
+ packageName: '@hcgstudio/ogma',
488
+ version,
489
+ cwd: options.cwd,
490
+ dataDir: project.dataDir,
491
+ designEntry: project.designEntry,
492
+ historyPath: project.historyPath,
493
+ notesPath: project.notesPath,
494
+ reviewUrl: runtimeConfig.reviewUrl,
495
+ skillUrl: runtimeConfig.skillUrl,
496
+ snapshotsDir: project.snapshotsDir,
497
+ serverStartedAt: startedAt
498
+ };
499
+ const server = await createServer({
500
+ appType: 'custom',
501
+ clearScreen: false,
502
+ plugins: [
503
+ createRuntimePlugin({
504
+ config: runtimeConfig,
505
+ packageRoot: options.packageRoot,
506
+ project,
507
+ status
508
+ })
509
+ ],
510
+ resolve: {
511
+ alias: [
512
+ {
513
+ find: '@hcgstudio/ogma/styles.css',
514
+ replacement: path.join(options.packageRoot, 'src/viewer/styles.css')
515
+ },
516
+ {
517
+ find: '@hcgstudio/ogma',
518
+ replacement: path.join(options.packageRoot, 'src/index.ts')
519
+ },
520
+ {
521
+ find: 'react/jsx-runtime',
522
+ replacement: requireFromPackage.resolve('react/jsx-runtime')
523
+ },
524
+ {
525
+ find: 'react/jsx-dev-runtime',
526
+ replacement: requireFromPackage.resolve('react/jsx-dev-runtime')
527
+ },
528
+ {
529
+ find: 'react-dom/client',
530
+ replacement: requireFromPackage.resolve('react-dom/client')
531
+ },
532
+ {
533
+ find: 'react',
534
+ replacement: requireFromPackage.resolve('react')
535
+ },
536
+ {
537
+ find: 'lucide-react',
538
+ replacement: requireFromPackage.resolve('lucide-react')
539
+ }
540
+ ],
541
+ dedupe: ['@hcgstudio/ogma', 'react', 'react-dom', 'lucide-react']
542
+ },
543
+ root: options.packageRoot,
544
+ server: {
545
+ fs: {
546
+ allow: [options.cwd, options.packageRoot, path.dirname(project.designEntry), project.dataDir]
547
+ },
548
+ host: options.host,
549
+ port: options.port,
550
+ strictPort: false
551
+ }
552
+ });
553
+
554
+ await server.listen();
555
+
556
+ runtimeConfig.reviewUrl = resolveReviewUrl(server);
557
+ status.reviewUrl = runtimeConfig.reviewUrl;
558
+
559
+ console.log('');
560
+ console.log('Ogma review server ready');
561
+ console.log(` Review URL: ${status.reviewUrl}`);
562
+ console.log(` Skill URL: ${status.skillUrl}`);
563
+ console.log(` Designs: ${project.designEntry}`);
564
+ console.log(` Notes: ${project.notesPath}`);
565
+ console.log(` Feedback: ${project.sessionPath}`);
566
+ console.log(` History: ${project.historyPath}`);
567
+ console.log('');
568
+
569
+ if (options.open) {
570
+ openBrowser(status.reviewUrl);
571
+ }
572
+
573
+ return {
574
+ project,
575
+ reviewUrl: status.reviewUrl,
576
+ server,
577
+ status
578
+ };
579
+ }
580
+
581
+ export async function assertPackageInstalledFromCwd(cwd: string) {
582
+ const require = createRequire(path.join(cwd, 'package.json'));
583
+ const missing: string[] = [];
584
+
585
+ for (const specifier of ['react', 'react-dom/client', 'vite']) {
586
+ try {
587
+ require.resolve(specifier);
588
+ } catch {
589
+ missing.push(specifier);
590
+ }
591
+ }
592
+
593
+ if (missing.length > 0) {
594
+ throw new Error(
595
+ `Missing runtime dependencies: ${missing.join(', ')}. Run npm install -D @hcgstudio/ogma react react-dom vite.`
596
+ );
597
+ }
598
+ }