@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.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +155 -0
- package/dist/cli.js.map +1 -0
- package/dist/defineOgmaReview.d.ts +3 -0
- package/dist/defineOgmaReview.d.ts.map +1 -0
- package/dist/defineOgmaReview.js +4 -0
- package/dist/defineOgmaReview.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/node/project.d.ts +18 -0
- package/dist/node/project.d.ts.map +1 -0
- package/dist/node/project.js +104 -0
- package/dist/node/project.js.map +1 -0
- package/dist/node/server.d.ts +21 -0
- package/dist/node/server.d.ts.map +1 -0
- package/dist/node/server.js +476 -0
- package/dist/node/server.js.map +1 -0
- package/dist/node/templates.d.ts +5 -0
- package/dist/node/templates.d.ts.map +1 -0
- package/dist/node/templates.js +145 -0
- package/dist/node/templates.js.map +1 -0
- package/dist/types.d.ts +100 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/viewer/OgmaReviewApp.d.ts +7 -0
- package/dist/viewer/OgmaReviewApp.d.ts.map +1 -0
- package/dist/viewer/OgmaReviewApp.js +387 -0
- package/dist/viewer/OgmaReviewApp.js.map +1 -0
- package/dist/viewer/client.d.ts +2 -0
- package/dist/viewer/client.d.ts.map +1 -0
- package/dist/viewer/client.js +13 -0
- package/dist/viewer/client.js.map +1 -0
- package/dist/viewer/defaultReview.d.ts +4 -0
- package/dist/viewer/defaultReview.d.ts.map +1 -0
- package/dist/viewer/defaultReview.js +55 -0
- package/dist/viewer/defaultReview.js.map +1 -0
- package/dist/viewer/normalizeReviewModule.d.ts +3 -0
- package/dist/viewer/normalizeReviewModule.d.ts.map +1 -0
- package/dist/viewer/normalizeReviewModule.js +58 -0
- package/dist/viewer/normalizeReviewModule.js.map +1 -0
- package/package.json +47 -0
- package/src/cli.ts +194 -0
- package/src/defineOgmaReview.ts +5 -0
- package/src/index.ts +17 -0
- package/src/node/project.ts +143 -0
- package/src/node/server.ts +598 -0
- package/src/node/templates.ts +148 -0
- package/src/types.ts +111 -0
- package/src/viewer/OgmaReviewApp.tsx +1099 -0
- package/src/viewer/client.tsx +18 -0
- package/src/viewer/defaultReview.tsx +168 -0
- package/src/viewer/normalizeReviewModule.ts +87 -0
- package/src/viewer/styles.css +1140 -0
- 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
|
+
}
|