@documentation-robotics/cli 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.
- package/PYTHON_CLI_PARITY.md +265 -0
- package/PYTHON_CLI_STRUCTURE_UPDATE.md +246 -0
- package/README.md +926 -0
- package/biome.json +42 -0
- package/bunfig.toml +3 -0
- package/dist/ai/claude-client.js +71 -0
- package/dist/ai/claude-client.js.map +7 -0
- package/dist/ai/context-provider.js +129 -0
- package/dist/ai/context-provider.js.map +7 -0
- package/dist/ai/tools.js +231 -0
- package/dist/ai/tools.js.map +7 -0
- package/dist/cli.js +224 -0
- package/dist/cli.js.map +7 -0
- package/dist/commands/add.js +51 -0
- package/dist/commands/add.js.map +7 -0
- package/dist/commands/changeset.js +174 -0
- package/dist/commands/changeset.js.map +7 -0
- package/dist/commands/chat.js +111 -0
- package/dist/commands/chat.js.map +7 -0
- package/dist/commands/conformance.js +132 -0
- package/dist/commands/conformance.js.map +7 -0
- package/dist/commands/delete.js +31 -0
- package/dist/commands/delete.js.map +7 -0
- package/dist/commands/element.js +28 -0
- package/dist/commands/element.js.map +7 -0
- package/dist/commands/export.js +65 -0
- package/dist/commands/export.js.map +7 -0
- package/dist/commands/info.js +40 -0
- package/dist/commands/info.js.map +7 -0
- package/dist/commands/init.js +45 -0
- package/dist/commands/init.js.map +7 -0
- package/dist/commands/list.js +32 -0
- package/dist/commands/list.js.map +7 -0
- package/dist/commands/migrate.js +36 -0
- package/dist/commands/migrate.js.map +7 -0
- package/dist/commands/project.js +74 -0
- package/dist/commands/project.js.map +7 -0
- package/dist/commands/relationship.js +118 -0
- package/dist/commands/relationship.js.map +7 -0
- package/dist/commands/search.js +48 -0
- package/dist/commands/search.js.map +7 -0
- package/dist/commands/show.js +38 -0
- package/dist/commands/show.js.map +7 -0
- package/dist/commands/trace.js +60 -0
- package/dist/commands/trace.js.map +7 -0
- package/dist/commands/update.js +37 -0
- package/dist/commands/update.js.map +7 -0
- package/dist/commands/upgrade.js +152 -0
- package/dist/commands/upgrade.js.map +7 -0
- package/dist/commands/validate.js +71 -0
- package/dist/commands/validate.js.map +7 -0
- package/dist/commands/version.js +20 -0
- package/dist/commands/version.js.map +7 -0
- package/dist/commands/visualize.js +51 -0
- package/dist/commands/visualize.js.map +7 -0
- package/dist/core/active-changeset.js +52 -0
- package/dist/core/active-changeset.js.map +7 -0
- package/dist/core/changeset.js +244 -0
- package/dist/core/changeset.js.map +7 -0
- package/dist/core/dependency-tracker.js +322 -0
- package/dist/core/dependency-tracker.js.map +7 -0
- package/dist/core/element.js +65 -0
- package/dist/core/element.js.map +7 -0
- package/dist/core/layer.js +75 -0
- package/dist/core/layer.js.map +7 -0
- package/dist/core/manifest.js +57 -0
- package/dist/core/manifest.js.map +7 -0
- package/dist/core/migration-registry.js +131 -0
- package/dist/core/migration-registry.js.map +7 -0
- package/dist/core/model.js +455 -0
- package/dist/core/model.js.map +7 -0
- package/dist/core/projection-engine.js +288 -0
- package/dist/core/projection-engine.js.map +7 -0
- package/dist/core/reference-registry.js +188 -0
- package/dist/core/reference-registry.js.map +7 -0
- package/dist/core/relationship-registry.js +114 -0
- package/dist/core/relationship-registry.js.map +7 -0
- package/dist/core/relationships.js +78 -0
- package/dist/core/relationships.js.map +7 -0
- package/dist/export/archimate-exporter.js +111 -0
- package/dist/export/archimate-exporter.js.map +7 -0
- package/dist/export/export-manager.js +63 -0
- package/dist/export/export-manager.js.map +7 -0
- package/dist/export/graphml-exporter.js +39 -0
- package/dist/export/graphml-exporter.js.map +7 -0
- package/dist/export/index.js +17 -0
- package/dist/export/index.js.map +7 -0
- package/dist/export/json-schema-exporter.js +60 -0
- package/dist/export/json-schema-exporter.js.map +7 -0
- package/dist/export/markdown-exporter.js +99 -0
- package/dist/export/markdown-exporter.js.map +7 -0
- package/dist/export/openapi-exporter.js +85 -0
- package/dist/export/openapi-exporter.js.map +7 -0
- package/dist/export/plantuml-exporter.js +60 -0
- package/dist/export/plantuml-exporter.js.map +7 -0
- package/dist/export/types.js +36 -0
- package/dist/export/types.js.map +7 -0
- package/dist/schemas/bundled/01-motivation-layer.schema.json +942 -0
- package/dist/schemas/bundled/02-business-layer.schema.json +1293 -0
- package/dist/schemas/bundled/03-security-layer.schema.json +2177 -0
- package/dist/schemas/bundled/04-application-layer.schema.json +1328 -0
- package/dist/schemas/bundled/05-technology-layer.schema.json +1465 -0
- package/dist/schemas/bundled/06-api-layer.schema.json +1743 -0
- package/dist/schemas/bundled/07-data-model-layer.schema.json +1253 -0
- package/dist/schemas/bundled/08-datastore-layer.schema.json +892 -0
- package/dist/schemas/bundled/09-ux-layer.schema.json +1506 -0
- package/dist/schemas/bundled/10-navigation-layer.schema.json +843 -0
- package/dist/schemas/bundled/11-apm-observability-layer.schema.json +1430 -0
- package/dist/schemas/bundled/12-testing-layer.schema.json +1091 -0
- package/dist/schemas/bundled/common/layer-extensions.schema.json +266 -0
- package/dist/schemas/bundled/common/predicates.schema.json +493 -0
- package/dist/schemas/bundled/common/relationships.schema.json +791 -0
- package/dist/schemas/bundled/common/source-references.schema.json +71 -0
- package/dist/schemas/bundled/link-registry.json +1839 -0
- package/dist/schemas/bundled/link-registry.schema.json +339 -0
- package/dist/schemas/bundled/relationship-catalog.json +1302 -0
- package/dist/schemas/bundled/relationship-type.schema.json +221 -0
- package/dist/server/server.js +1207 -0
- package/dist/server/server.js.map +7 -0
- package/dist/server/telemetry-middleware.js +24 -0
- package/dist/server/telemetry-middleware.js.map +7 -0
- package/dist/telemetry/console-interceptor.js +34 -0
- package/dist/telemetry/console-interceptor.js.map +7 -0
- package/dist/telemetry/index.js +54 -0
- package/dist/telemetry/index.js.map +7 -0
- package/dist/telemetry/resilient-exporter.js +41 -0
- package/dist/telemetry/resilient-exporter.js.map +7 -0
- package/dist/telemetry/resilient-log-exporter.js +41 -0
- package/dist/telemetry/resilient-log-exporter.js.map +7 -0
- package/dist/telemetry/test-instrumentation.js +32 -0
- package/dist/telemetry/test-instrumentation.js.map +7 -0
- package/dist/types/index.js +1 -0
- package/dist/types/index.js.map +7 -0
- package/dist/utils/element-utils.js +11 -0
- package/dist/utils/element-utils.js.map +7 -0
- package/dist/utils/errors.js +105 -0
- package/dist/utils/errors.js.map +7 -0
- package/dist/utils/file-io.js +69 -0
- package/dist/utils/file-io.js.map +7 -0
- package/dist/utils/globals.js +28 -0
- package/dist/utils/globals.js.map +7 -0
- package/dist/utils/model-path.js +22 -0
- package/dist/utils/model-path.js.map +7 -0
- package/dist/utils/project-paths.js +45 -0
- package/dist/utils/project-paths.js.map +7 -0
- package/dist/utils/spec-installer.js +85 -0
- package/dist/utils/spec-installer.js.map +7 -0
- package/dist/utils/spec-version.js +33 -0
- package/dist/utils/spec-version.js.map +7 -0
- package/dist/validators/index.js +15 -0
- package/dist/validators/index.js.map +7 -0
- package/dist/validators/naming-validator.js +82 -0
- package/dist/validators/naming-validator.js.map +7 -0
- package/dist/validators/reference-validator.js +106 -0
- package/dist/validators/reference-validator.js.map +7 -0
- package/dist/validators/schema-validator.js +148 -0
- package/dist/validators/schema-validator.js.map +7 -0
- package/dist/validators/semantic-validator.js +80 -0
- package/dist/validators/semantic-validator.js.map +7 -0
- package/dist/validators/types.js +48 -0
- package/dist/validators/types.js.map +7 -0
- package/dist/validators/validator.js +67 -0
- package/dist/validators/validator.js.map +7 -0
- package/docs/TEST-INSTRUMENTATION.md +405 -0
- package/docs/telemetry.md +427 -0
- package/package.json +70 -0
- package/test-debug.js +33 -0
- package/test-special.mjs +46 -0
- package/test_runner.ts +7 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/server/server.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Visualization Server\n * HTTP server with WebSocket support for real-time model updates\n */\n\nimport { Hono } from 'hono';\nimport { upgradeWebSocket, websocket } from 'hono/bun';\nimport { cors } from 'hono/cors';\nimport { serve } from 'bun';\nimport { Model } from '../core/model.js';\nimport { Element } from '../core/element.js';\nimport { telemetryMiddleware } from './telemetry-middleware.js';\n\ninterface WSMessage {\n type: 'subscribe' | 'annotate' | 'ping';\n topics?: string[];\n annotation?: {\n elementId: string;\n author: string;\n text: string;\n timestamp: string;\n };\n jsonrpc?: string; // For JSON-RPC 2.0 messages\n method?: string;\n params?: any;\n id?: string | number;\n result?: any;\n error?: {\n code: number;\n message: string;\n data?: any;\n };\n}\n\ninterface ClientAnnotation {\n id: string;\n elementId: string;\n author: string;\n content: string;\n createdAt: string;\n updatedAt?: string;\n tags?: string[];\n}\n\ninterface Changeset {\n metadata: {\n id: string;\n name: string;\n description?: string;\n type: 'feature' | 'bugfix' | 'exploration';\n status: 'active' | 'applied' | 'abandoned';\n created_at: string;\n updated_at?: string;\n workflow?: string;\n summary: {\n elements_added: number;\n elements_updated: number;\n elements_deleted: number;\n };\n };\n changes: {\n version: string;\n changes: Array<{\n timestamp: string;\n operation: 'add' | 'update' | 'delete';\n element_id: string;\n layer: string;\n element_type: string;\n data?: any;\n before?: any;\n after?: any;\n }>;\n };\n}\n\n// Type for WebSocket context from Hono/Bun\ntype HonoWSContext = any;\n\nexport interface VisualizationServerOptions {\n authEnabled?: boolean;\n authToken?: string;\n}\n\n/**\n * Visualization Server class\n */\nexport class VisualizationServer {\n private app: Hono;\n private model: Model;\n private _server?: ReturnType<typeof serve>;\n private clients: Set<HonoWSContext> = new Set();\n private watcher?: any;\n private annotations: Map<string, ClientAnnotation> = new Map(); // annotationId -> annotation\n private annotationsByElement: Map<string, Set<string>> = new Map(); // elementId -> Set<annotationId>\n private changesets: Map<string, Changeset> = new Map(); // changesetId -> changeset\n private authToken: string;\n private authEnabled: boolean = true; // Enabled by default for security\n private activeChatProcesses: Map<string, any> = new Map(); // conversationId -> Bun.spawn process\n private chatConversationCounter: number = 0;\n\n constructor(model: Model, options?: VisualizationServerOptions) {\n this.app = new Hono();\n this.model = model;\n\n // Auth configuration (CLI options override environment variables)\n this.authEnabled = options?.authEnabled ?? (process.env.DR_AUTH_ENABLED !== 'false');\n this.authToken = options?.authToken || process.env.DR_AUTH_TOKEN || this.generateAuthToken();\n\n // Add CORS middleware\n this.app.use('/*', cors());\n\n // Add telemetry middleware to instrument all HTTP requests\n this.app.use('/*', telemetryMiddleware);\n\n // Add authentication middleware (except for health endpoint and root)\n this.app.use('/api/*', async (c, next) => {\n if (!this.authEnabled) {\n return next();\n }\n\n // Check for token in Authorization header or query parameter\n const authHeader = c.req.header('Authorization');\n const queryToken = c.req.query('token');\n\n let providedToken: string | null = null;\n\n if (authHeader && authHeader.startsWith('Bearer ')) {\n providedToken = authHeader.substring(7);\n } else if (queryToken) {\n providedToken = queryToken;\n }\n\n if (!providedToken) {\n return c.json({ error: 'Authentication required. Please provide a valid token.' }, 401);\n }\n\n if (providedToken !== this.authToken) {\n return c.json({ error: 'Invalid authentication token' }, 403);\n }\n\n return next();\n });\n\n this.setupRoutes();\n\n // Log auth token if enabled\n if (this.authEnabled && process.env.VERBOSE) {\n console.log(`[Auth] Authentication enabled. Token: ${this.authToken}`);\n }\n }\n\n /**\n * Generate a random auth token\n */\n private generateAuthToken(): string {\n return `dr-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;\n }\n\n /**\n * Setup HTTP routes and WebSocket handler\n */\n private setupRoutes(): void {\n // Static viewer HTML at root\n this.app.get('/', (c) => {\n return c.html(this.getViewerHTML());\n });\n\n // Health check endpoint\n this.app.get('/health', (c) => {\n return c.json({\n status: 'ok',\n version: '0.1.0'\n });\n });\n\n // REST API endpoints\n\n // Get full model\n this.app.get('/api/model', async (c) => {\n try {\n const modelData = await this.serializeModel();\n return c.json(modelData);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n });\n\n // Get specific layer\n this.app.get('/api/layers/:name', async (c) => {\n try {\n const layerName = c.req.param('name');\n const layer = await this.model.getLayer(layerName);\n\n if (!layer) {\n return c.json({ error: 'Layer not found' }, 404);\n }\n\n const elements = layer\n .listElements()\n .map((e) => e.toJSON());\n\n return c.json({\n name: layerName,\n elements,\n elementCount: elements.length,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n });\n\n // Get specific element\n this.app.get('/api/elements/:id', async (c) => {\n try {\n const elementId = c.req.param('id');\n const element = await this.findElement(elementId);\n\n if (!element) {\n return c.json({ error: 'Element not found' }, 404);\n }\n\n return c.json({\n ...element.toJSON(),\n annotations: this.annotations.get(elementId) || [],\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n });\n\n // Get JSON Schema specifications\n this.app.get('/api/spec', async (c) => {\n try {\n const schemas = await this.loadSchemas();\n return c.json({\n version: '0.1.0',\n type: 'schema-collection',\n description: 'JSON Schema definitions from dr CLI',\n source: 'dr-cli',\n schemas,\n schemaCount: Object.keys(schemas).length,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n });\n\n // Get link registry\n this.app.get('/api/link-registry', async (c) => {\n try {\n const linkRegistry = await this.loadLinkRegistry();\n return c.json(linkRegistry);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 404);\n }\n });\n\n // Annotations API (spec-compliant routes)\n\n // Get all annotations (optionally filtered by elementId)\n this.app.get('/api/annotations', (c) => {\n const elementId = c.req.query('elementId');\n\n if (elementId) {\n // Filter by element\n const annotationIds = this.annotationsByElement.get(elementId) || new Set();\n const annotations = Array.from(annotationIds)\n .map(id => this.annotations.get(id))\n .filter((a): a is ClientAnnotation => a !== undefined);\n\n return c.json({ annotations });\n } else {\n // Return all annotations\n const annotations = Array.from(this.annotations.values());\n return c.json({ annotations });\n }\n });\n\n // Create annotation\n this.app.post('/api/annotations', async (c) => {\n try {\n const body = await c.req.json();\n\n // Validate required fields\n if (!body.elementId || !body.content || !body.author) {\n return c.json({\n error: 'Missing required fields: elementId, content, author'\n }, 400);\n }\n\n // Verify element exists\n const element = await this.findElement(body.elementId);\n if (!element) {\n return c.json({ error: 'Element not found' }, 404);\n }\n\n // Create annotation\n const annotation: ClientAnnotation = {\n id: this.generateAnnotationId(),\n elementId: String(body.elementId),\n author: String(body.author),\n content: String(body.content),\n createdAt: new Date().toISOString(),\n tags: body.tags || [],\n };\n\n // Store annotation\n this.annotations.set(annotation.id, annotation);\n if (!this.annotationsByElement.has(annotation.elementId)) {\n this.annotationsByElement.set(annotation.elementId, new Set());\n }\n this.annotationsByElement.get(annotation.elementId)!.add(annotation.id);\n\n // Broadcast to all clients\n await this.broadcastMessage({\n type: 'annotation.added',\n annotationId: annotation.id,\n elementId: annotation.elementId,\n timestamp: annotation.createdAt,\n });\n\n return c.json(annotation, 201);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n });\n\n // Update annotation\n this.app.put('/api/annotations/:annotationId', async (c) => {\n try {\n const annotationId = c.req.param('annotationId');\n const annotation = this.annotations.get(annotationId);\n\n if (!annotation) {\n return c.json({ error: 'Annotation not found' }, 404);\n }\n\n const body = await c.req.json();\n\n // Update fields\n if (body.content !== undefined) {\n annotation.content = String(body.content);\n }\n if (body.tags !== undefined) {\n annotation.tags = body.tags;\n }\n annotation.updatedAt = new Date().toISOString();\n\n // Broadcast to all clients\n await this.broadcastMessage({\n type: 'annotation.updated',\n annotationId: annotation.id,\n timestamp: annotation.updatedAt,\n });\n\n return c.json(annotation);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n });\n\n // Delete annotation\n this.app.delete('/api/annotations/:annotationId', async (c) => {\n try {\n const annotationId = c.req.param('annotationId');\n const annotation = this.annotations.get(annotationId);\n\n if (!annotation) {\n return c.json({ error: 'Annotation not found' }, 404);\n }\n\n // Remove from storage\n this.annotations.delete(annotationId);\n const elementAnnotations = this.annotationsByElement.get(annotation.elementId);\n if (elementAnnotations) {\n elementAnnotations.delete(annotationId);\n }\n\n // Broadcast to all clients\n await this.broadcastMessage({\n type: 'annotation.deleted',\n annotationId,\n timestamp: new Date().toISOString(),\n });\n\n return c.body(null, 204);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n });\n\n // Changesets API\n\n // Get all changesets\n this.app.get('/api/changesets', (c) => {\n const changesets: Record<string, any> = {};\n\n for (const [id, changeset] of this.changesets) {\n changesets[id] = {\n name: changeset.metadata.name,\n status: changeset.metadata.status,\n type: changeset.metadata.type,\n created_at: changeset.metadata.created_at,\n elements_count: changeset.changes.changes.length,\n };\n }\n\n return c.json({\n version: '1.0.0',\n changesets,\n });\n });\n\n // Get specific changeset\n this.app.get('/api/changesets/:changesetId', (c) => {\n const changesetId = c.req.param('changesetId');\n const changeset = this.changesets.get(changesetId);\n\n if (!changeset) {\n return c.json({ error: 'Changeset not found' }, 404);\n }\n\n return c.json(changeset);\n });\n\n // Deprecated: Get annotations for an element (use /api/annotations?elementId=xyz instead)\n this.app.get('/api/elements/:id/annotations', (c) => {\n const elementId = c.req.param('id');\n const annotationIds = this.annotationsByElement.get(elementId) || new Set();\n const annotations = Array.from(annotationIds)\n .map(id => this.annotations.get(id))\n .filter((a): a is ClientAnnotation => a !== undefined);\n\n return c.json({ elementId, annotations });\n });\n\n // Deprecated: Post annotation to element (use POST /api/annotations instead)\n this.app.post('/api/elements/:id/annotations', async (c) => {\n try {\n const elementId = c.req.param('id');\n\n // Verify element exists\n const element = await this.findElement(elementId);\n if (!element) {\n return c.json({ error: 'Element not found' }, 404);\n }\n\n const body = await c.req.json();\n\n // Support both 'text' and 'content' for backward compatibility\n const content = (body.content || body.text) && String(body.content || body.text).trim();\n if (!content) {\n return c.json({ error: 'Annotation content cannot be empty' }, 400);\n }\n\n const author = body.author ? String(body.author).trim() : 'Anonymous';\n\n const annotation: ClientAnnotation = {\n id: this.generateAnnotationId(),\n elementId,\n author,\n content,\n createdAt: new Date().toISOString(),\n tags: body.tags || [],\n };\n\n // Store annotation\n this.annotations.set(annotation.id, annotation);\n if (!this.annotationsByElement.has(elementId)) {\n this.annotationsByElement.set(elementId, new Set());\n }\n this.annotationsByElement.get(elementId)!.add(annotation.id);\n\n // Broadcast to all clients\n await this.broadcastMessage({\n type: 'annotation.added',\n annotationId: annotation.id,\n elementId: annotation.elementId,\n timestamp: annotation.createdAt,\n });\n\n return c.json({ success: true, annotation });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n });\n\n // WebSocket endpoint\n this.app.get(\n '/ws',\n async (c, next) => {\n // Check WebSocket auth if enabled\n if (this.authEnabled) {\n const queryToken = c.req.query('token');\n\n if (!queryToken) {\n return c.json({ error: 'Authentication required. Provide token as query parameter: ?token=YOUR_TOKEN' }, 401);\n }\n\n if (queryToken !== this.authToken) {\n return c.json({ error: 'Invalid authentication token' }, 403);\n }\n }\n\n return next();\n },\n upgradeWebSocket(() => ({\n onOpen: (_evt, ws) => {\n this.clients.add(ws);\n\n // Send connected message per spec\n ws.send(JSON.stringify({\n type: 'connected',\n version: '0.1.0',\n timestamp: new Date().toISOString(),\n }));\n\n if (process.env.VERBOSE) {\n console.log(`[WebSocket] Client connected (total: ${this.clients.size})`);\n }\n },\n\n onClose: (_evt, ws) => {\n this.clients.delete(ws);\n if (process.env.VERBOSE) {\n console.log(\n `[WebSocket] Client disconnected (total: ${this.clients.size})`\n );\n }\n },\n\n onMessage: async (message, ws) => {\n try {\n // Extract the actual message data\n // In Hono/Bun, message is a MessageEvent with a data property\n const rawData = (message as any).data ?? message;\n\n // Handle different message types (string, Buffer, ArrayBuffer)\n let msgStr: string;\n if (typeof rawData === 'string') {\n msgStr = rawData;\n } else if (rawData instanceof Buffer || rawData instanceof Uint8Array) {\n msgStr = new TextDecoder().decode(rawData);\n } else {\n msgStr = String(rawData);\n }\n const data = JSON.parse(msgStr) as WSMessage;\n\n // Check if it's a JSON-RPC message\n if (data.jsonrpc === '2.0') {\n await this.handleJSONRPCMessage(ws, data);\n } else {\n await this.handleWSMessage(ws, data);\n }\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n if (process.env.DEBUG) {\n console.error(`[WebSocket] Error handling message: ${errorMsg}`);\n }\n ws.send(\n JSON.stringify({\n type: 'error',\n message: errorMsg,\n })\n );\n }\n },\n\n onError: (error) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`[WebSocket] Error: ${message}`);\n },\n }))\n );\n }\n\n /**\n * Serialize model to JSON for client consumption\n */\n private async serializeModel(): Promise<any> {\n const layers: any = {};\n\n for (const [name, layer] of this.model.layers) {\n const elements = layer\n .listElements()\n .map((e) => {\n const annotationIds = this.annotationsByElement.get(e.id) || new Set();\n const annotations = Array.from(annotationIds)\n .map(id => this.annotations.get(id))\n .filter((a): a is ClientAnnotation => a !== undefined);\n\n return {\n ...e.toJSON(),\n annotations,\n };\n });\n\n layers[name] = {\n name,\n elements,\n elementCount: elements.length,\n };\n }\n\n return {\n manifest: this.model.manifest.toJSON(),\n layers,\n totalElements: Array.from(this.model.layers.values()).reduce(\n (sum, layer) => sum + layer.listElements().length,\n 0\n ),\n };\n }\n\n /**\n * Generate unique annotation ID\n */\n private generateAnnotationId(): string {\n return `ann-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;\n }\n\n /**\n * Broadcast message to all connected WebSocket clients\n */\n private async broadcastMessage(message: any): Promise<void> {\n const messageStr = JSON.stringify(message);\n\n let failureCount = 0;\n for (const client of this.clients) {\n try {\n client.send(messageStr);\n } catch (error) {\n failureCount++;\n const msg = error instanceof Error ? error.message : String(error);\n if (process.env.VERBOSE) {\n console.warn(`[WebSocket] Failed to send message to client: ${msg}`);\n }\n }\n }\n\n if (failureCount > 0 && process.env.VERBOSE) {\n console.warn(`[WebSocket] Failed to send message to ${failureCount}/${this.clients.size} clients`);\n }\n }\n\n /**\n * Find an element across all layers\n */\n private async findElement(id: string): Promise<Element | null> {\n for (const layer of this.model.layers.values()) {\n const element = layer.getElement(id);\n if (element) return element;\n }\n return null;\n }\n\n /**\n * Handle WebSocket messages\n */\n private async handleWSMessage(ws: HonoWSContext, data: WSMessage): Promise<void> {\n switch (data.type) {\n case 'subscribe':\n // Send subscribed confirmation\n const topics = data.topics || ['model', 'annotations'];\n ws.send(JSON.stringify({\n type: 'subscribed',\n topics,\n timestamp: new Date().toISOString(),\n }));\n\n // Send initial model state\n const modelData = await this.serializeModel();\n ws.send(\n JSON.stringify({\n type: 'model',\n data: modelData,\n timestamp: new Date().toISOString(),\n })\n );\n break;\n\n case 'ping':\n // Respond with pong\n ws.send(JSON.stringify({\n type: 'pong',\n timestamp: new Date().toISOString(),\n }));\n break;\n\n case 'annotate':\n if (data.annotation) {\n await this.broadcastMessage({\n type: 'annotation',\n data: data.annotation,\n });\n }\n break;\n\n default:\n ws.send(\n JSON.stringify({\n type: 'error',\n message: `Unknown message type: ${data.type}`,\n })\n );\n }\n }\n\n /**\n * Handle JSON-RPC 2.0 messages for chat functionality\n */\n private async handleJSONRPCMessage(ws: HonoWSContext, data: WSMessage): Promise<void> {\n const { method, params, id } = data;\n\n // Helper to send JSON-RPC response\n const sendResponse = (result: any) => {\n ws.send(JSON.stringify({\n jsonrpc: '2.0',\n result,\n id,\n }));\n };\n\n // Helper to send JSON-RPC error\n const sendError = (code: number, message: string, data?: any) => {\n ws.send(JSON.stringify({\n jsonrpc: '2.0',\n error: { code, message, data },\n id,\n }));\n };\n\n try {\n switch (method) {\n case 'chat.status':\n // Check if Claude Code CLI is available\n const claudeAvailable = await this.checkClaudeCodeCLI();\n sendResponse({\n sdk_available: claudeAvailable,\n sdk_version: claudeAvailable ? 'claude-code-cli' : null,\n error_message: claudeAvailable ? null : 'Claude Code CLI not found in PATH'\n });\n break;\n\n case 'chat.send':\n // Validate params\n if (!params || !params.message) {\n sendError(-32602, 'Message cannot be empty');\n return;\n }\n\n // Check if Claude Code is available\n if (!await this.checkClaudeCodeCLI()) {\n sendError(-32001, 'Claude Code CLI not available. Install Claude Code to enable chat.');\n return;\n }\n\n // Generate conversation ID\n const conversationId = `conv-${++this.chatConversationCounter}-${Date.now()}`;\n\n // Launch Claude Code with DR chat agent\n await this.launchClaudeCodeChat(ws, conversationId, params.message, id);\n break;\n\n case 'chat.cancel':\n // Find and cancel any active conversation\n let cancelled = false;\n let cancelledConvId = null;\n\n for (const [convId, process] of this.activeChatProcesses) {\n try {\n process.kill();\n this.activeChatProcesses.delete(convId);\n cancelled = true;\n cancelledConvId = convId;\n break;\n } catch (error) {\n // Process already terminated\n }\n }\n\n sendResponse({\n cancelled,\n conversation_id: cancelledConvId\n });\n break;\n\n default:\n sendError(-32601, `Method not found: ${method}`);\n }\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n sendError(-32603, 'Internal error', errorMsg);\n }\n }\n\n /**\n * Check if Claude Code CLI is available\n */\n private async checkClaudeCodeCLI(): Promise<boolean> {\n try {\n const result = Bun.spawnSync({\n cmd: ['which', 'claude'],\n stdout: 'pipe',\n stderr: 'pipe',\n });\n return result.exitCode === 0;\n } catch {\n return false;\n }\n }\n\n /**\n * Launch Claude Code CLI with dr-architect agent and stream responses\n */\n private async launchClaudeCodeChat(\n ws: HonoWSContext,\n conversationId: string,\n message: string,\n requestId: string | number | undefined\n ): Promise<void> {\n try {\n // Launch claude with dr-architect agent for comprehensive DR expertise\n const proc = Bun.spawn({\n cmd: [\n 'claude',\n '--agent', 'dr-architect',\n '--print',\n '--dangerously-skip-permissions',\n '--verbose',\n '--output-format', 'stream-json',\n ],\n cwd: this.model.rootPath,\n stdin: 'pipe',\n stdout: 'pipe',\n stderr: 'pipe',\n });\n\n // Store active process\n this.activeChatProcesses.set(conversationId, proc);\n\n // Send user message via stdin (like Python prototype)\n proc.stdin?.write(new TextEncoder().encode(message));\n proc.stdin?.end();\n\n // Stream stdout (JSON events)\n const stdoutReader = proc.stdout.getReader();\n const decoder = new TextDecoder();\n\n const streamOutput = async () => {\n let accumulatedText = '';\n let buffer = '';\n\n try {\n while (true) {\n const { done, value } = await stdoutReader.read();\n if (done) break;\n\n const chunk = decoder.decode(value, { stream: true });\n buffer += chunk;\n\n // Process complete lines (JSON events)\n const lines = buffer.split('\\n');\n buffer = lines.pop() || '';\n\n for (const line of lines) {\n if (!line.trim()) continue;\n\n try {\n const event = JSON.parse(line);\n\n if (event.type === 'assistant') {\n // Extract content blocks from assistant message\n const content = event.message?.content || [];\n for (const block of content) {\n if (block.type === 'text') {\n accumulatedText += block.text;\n // Send text chunk notification\n ws.send(JSON.stringify({\n jsonrpc: '2.0',\n method: 'chat.response.chunk',\n params: {\n conversation_id: conversationId,\n content: block.text,\n is_final: false,\n timestamp: new Date().toISOString(),\n },\n }));\n } else if (block.type === 'tool_use') {\n // Send tool invocation notification\n ws.send(JSON.stringify({\n jsonrpc: '2.0',\n method: 'chat.tool.invoke',\n params: {\n conversation_id: conversationId,\n tool_name: block.name,\n tool_input: block.input,\n timestamp: new Date().toISOString(),\n },\n }));\n }\n }\n } else if (event.type === 'result') {\n // Tool result\n ws.send(JSON.stringify({\n jsonrpc: '2.0',\n method: 'chat.tool.result',\n params: {\n conversation_id: conversationId,\n result: event.result,\n timestamp: new Date().toISOString(),\n },\n }));\n }\n } catch {\n // Non-JSON line, send as raw text chunk\n ws.send(JSON.stringify({\n jsonrpc: '2.0',\n method: 'chat.response.chunk',\n params: {\n conversation_id: conversationId,\n content: line + '\\n',\n is_final: false,\n timestamp: new Date().toISOString(),\n },\n }));\n }\n }\n }\n\n // Process remaining buffer\n if (buffer.trim()) {\n ws.send(JSON.stringify({\n jsonrpc: '2.0',\n method: 'chat.response.chunk',\n params: {\n conversation_id: conversationId,\n content: buffer,\n is_final: true,\n timestamp: new Date().toISOString(),\n },\n }));\n }\n\n // Wait for process to complete\n const exitCode = await proc.exited;\n\n // Clean up\n this.activeChatProcesses.delete(conversationId);\n\n // Send completion response\n ws.send(JSON.stringify({\n jsonrpc: '2.0',\n result: {\n conversation_id: conversationId,\n status: exitCode === 0 ? 'complete' : 'error',\n exit_code: exitCode,\n full_response: accumulatedText,\n timestamp: new Date().toISOString(),\n },\n id: requestId,\n }));\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n\n ws.send(JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: `Chat failed: ${errorMsg}`,\n },\n id: requestId,\n }));\n\n this.activeChatProcesses.delete(conversationId);\n\n try {\n proc.kill();\n } catch {\n // Process may already be terminated\n }\n }\n };\n\n // Start streaming in background\n streamOutput();\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n\n ws.send(JSON.stringify({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: `Failed to launch Claude Code: ${errorMsg}`,\n },\n id: requestId,\n }));\n }\n }\n\n\n /**\n * Setup file watcher for model changes\n */\n private setupFileWatcher(): void {\n const drPath = `${this.model.rootPath}/.dr`;\n\n // Use Bun's global watch API (cast to any due to type definitions)\n const bunWatch = (globalThis as any).Bun?.watch;\n if (!bunWatch) {\n console.warn('[Watcher] Bun.watch not available, file watching disabled');\n return;\n }\n\n this.watcher = bunWatch(drPath, {\n recursive: true,\n onChange: async (_event: string, path: string) => {\n if (process.env.VERBOSE) {\n console.log(`[Watcher] File changed: ${path}`);\n }\n\n try {\n // Reload model\n this.model = await Model.load(this.model.rootPath, { lazyLoad: false });\n\n // Broadcast update to all clients\n const modelData = await this.serializeModel();\n const message = JSON.stringify({\n type: 'model.updated',\n data: modelData,\n timestamp: new Date().toISOString(),\n });\n\n for (const client of this.clients) {\n try {\n client.send(message);\n } catch (error) {\n if (process.env.DEBUG) {\n const msg = error instanceof Error ? error.message : String(error);\n console.debug(`[Watcher] Failed to send update: ${msg}`);\n }\n }\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`[Watcher] Failed to reload model: ${message}`);\n }\n },\n });\n }\n\n /**\n * Load all JSON schemas from bundled directory\n */\n private async loadSchemas(): Promise<Record<string, any>> {\n const schemasPath = new URL('../schemas/bundled/', import.meta.url).pathname;\n const schemas: Record<string, any> = {};\n\n try {\n const fs = await import('fs/promises');\n const path = await import('path');\n\n const fileList = await fs.readdir(schemasPath);\n\n for (const file of fileList) {\n if (file.endsWith('.schema.json') || file.endsWith('.json')) {\n const filePath = path.join(schemasPath, file);\n const content = await fs.readFile(filePath, 'utf-8');\n schemas[file] = JSON.parse(content);\n }\n }\n\n return schemas;\n } catch (error) {\n console.error('Failed to load schemas:', error);\n throw new Error('Failed to load schema files');\n }\n }\n\n /**\n * Load link registry from bundled directory\n */\n private async loadLinkRegistry(): Promise<any> {\n const linkRegistryPath = new URL('../schemas/bundled/link-registry.json', import.meta.url).pathname;\n\n try {\n const fs = await import('fs/promises');\n const content = await fs.readFile(linkRegistryPath, 'utf-8');\n return JSON.parse(content);\n } catch (error) {\n console.error('Failed to load link registry:', error);\n throw new Error('Link registry not found');\n }\n }\n\n /**\n * Get viewer HTML\n */\n private getViewerHTML(): string {\n return `<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Documentation Robotics Viewer</title>\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n background: #f5f5f5;\n color: #333;\n }\n\n header {\n background: white;\n border-bottom: 1px solid #e0e0e0;\n padding: 20px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n }\n\n .header-content {\n max-width: 1400px;\n margin: 0 auto;\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n h1 {\n font-size: 24px;\n font-weight: 600;\n }\n\n .status-badge {\n display: inline-block;\n padding: 6px 12px;\n border-radius: 4px;\n font-size: 12px;\n font-weight: 500;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .status-badge.connected {\n background: #4caf50;\n color: white;\n }\n\n .status-badge.disconnected {\n background: #f44336;\n color: white;\n }\n\n .container {\n max-width: 1400px;\n margin: 0 auto;\n padding: 20px;\n }\n\n .content {\n display: grid;\n grid-template-columns: 300px 1fr;\n gap: 20px;\n height: calc(100vh - 100px);\n }\n\n .sidebar {\n background: white;\n border-radius: 4px;\n overflow-y: auto;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n padding: 16px;\n }\n\n .main-panel {\n background: white;\n border-radius: 4px;\n overflow-y: auto;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n padding: 20px;\n }\n\n .layer-group {\n margin-bottom: 16px;\n }\n\n .layer-header {\n font-weight: 600;\n padding: 8px 0;\n border-bottom: 2px solid #e0e0e0;\n margin-bottom: 8px;\n cursor: pointer;\n user-select: none;\n display: flex;\n align-items: center;\n }\n\n .layer-header:hover {\n color: #2196F3;\n }\n\n .layer-header::before {\n content: '\u25B6';\n margin-right: 8px;\n display: inline-block;\n transition: transform 0.2s;\n }\n\n .layer-header.expanded::before {\n transform: rotate(90deg);\n }\n\n .layer-elements {\n margin-left: 16px;\n display: none;\n }\n\n .layer-elements.expanded {\n display: block;\n }\n\n .element-item {\n padding: 8px;\n margin-bottom: 4px;\n background: #f9f9f9;\n border-left: 3px solid #e0e0e0;\n cursor: pointer;\n border-radius: 2px;\n transition: all 0.2s;\n font-size: 13px;\n }\n\n .element-item:hover {\n background: #f0f0f0;\n border-left-color: #2196F3;\n }\n\n .element-item.selected {\n background: #e3f2fd;\n border-left-color: #2196F3;\n font-weight: 500;\n }\n\n .element-details {\n min-height: 300px;\n }\n\n .detail-header {\n border-bottom: 2px solid #e0e0e0;\n padding-bottom: 12px;\n margin-bottom: 16px;\n }\n\n .detail-header h2 {\n font-size: 18px;\n margin-bottom: 4px;\n }\n\n .detail-header .element-type {\n font-size: 12px;\n color: #999;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n }\n\n .detail-section {\n margin-bottom: 16px;\n }\n\n .detail-section h3 {\n font-size: 13px;\n font-weight: 600;\n text-transform: uppercase;\n color: #999;\n margin-bottom: 8px;\n letter-spacing: 0.5px;\n }\n\n .detail-value {\n font-size: 14px;\n line-height: 1.6;\n color: #333;\n word-break: break-word;\n }\n\n .annotations-list {\n max-height: 200px;\n overflow-y: auto;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n padding: 8px;\n }\n\n .annotation-item {\n padding: 8px;\n margin-bottom: 8px;\n background: #fffde7;\n border-left: 3px solid #fbc02d;\n border-radius: 2px;\n font-size: 12px;\n }\n\n .annotation-item .author {\n font-weight: 600;\n color: #666;\n }\n\n .annotation-item .text {\n margin: 4px 0 0 0;\n color: #333;\n }\n\n .annotation-item .timestamp {\n font-size: 11px;\n color: #999;\n margin-top: 4px;\n }\n\n .annotation-form {\n display: grid;\n gap: 8px;\n margin-top: 12px;\n }\n\n .annotation-form input,\n .annotation-form textarea {\n padding: 8px;\n border: 1px solid #e0e0e0;\n border-radius: 4px;\n font-family: inherit;\n font-size: 12px;\n }\n\n .annotation-form textarea {\n resize: vertical;\n min-height: 60px;\n }\n\n .annotation-form button {\n padding: 8px 12px;\n background: #2196F3;\n color: white;\n border: none;\n border-radius: 4px;\n font-weight: 500;\n cursor: pointer;\n transition: background 0.2s;\n }\n\n .annotation-form button:hover {\n background: #1976D2;\n }\n\n .loading {\n text-align: center;\n padding: 40px;\n color: #999;\n }\n\n .loading::after {\n content: '';\n display: inline-block;\n width: 16px;\n height: 16px;\n border: 2px solid #f3f3f3;\n border-top: 2px solid #2196F3;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n margin-left: 8px;\n }\n\n @keyframes spin {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n }\n\n .stats {\n font-size: 12px;\n color: #999;\n margin-bottom: 16px;\n padding-bottom: 12px;\n border-bottom: 1px solid #e0e0e0;\n }\n </style>\n</head>\n<body>\n <header>\n <div class=\"header-content\">\n <div>\n <h1>\uD83D\uDCDA Documentation Robotics Viewer</h1>\n </div>\n <div class=\"status-badge connected\" id=\"status\">\u25CF Connected</div>\n </div>\n </header>\n\n <div class=\"container\">\n <div class=\"content\">\n <div class=\"sidebar\">\n <div class=\"stats\" id=\"stats\">Loading...</div>\n <div id=\"model-tree\">\n <div class=\"loading\">Loading model...</div>\n </div>\n </div>\n\n <div class=\"main-panel\">\n <div id=\"element-details\">\n <div class=\"loading\">Select an element to view details</div>\n </div>\n </div>\n </div>\n </div>\n\n <script>\n let currentModel = null;\n let selectedElementId = null;\n\n const statusEl = document.getElementById('status');\n const modelTreeEl = document.getElementById('model-tree');\n const detailsEl = document.getElementById('element-details');\n const statsEl = document.getElementById('stats');\n\n /**\n * Escape HTML special characters to prevent XSS\n * @param {string} text - Text to escape\n * @returns {string} HTML-escaped text\n */\n function escapeHtml(text) {\n if (!text) return '';\n const div = document.createElement('div');\n div.textContent = text;\n return div.innerHTML;\n }\n\n const ws = new WebSocket(\\`ws://\\${window.location.host}/ws\\`);\n\n ws.addEventListener('open', () => {\n console.log('WebSocket connected');\n updateStatus(true);\n ws.send(JSON.stringify({ type: 'subscribe' }));\n });\n\n ws.addEventListener('message', (event) => {\n try {\n const message = JSON.parse(event.data);\n\n if (message.type === 'model' || message.type === 'model-update') {\n currentModel = message.data;\n renderModel();\n } else if (message.type === 'annotation') {\n // Update annotations for the element\n renderSelectedElement();\n } else if (message.type === 'error') {\n console.error('WebSocket error:', message.message);\n }\n } catch (error) {\n console.error('Failed to parse message:', error);\n }\n });\n\n ws.addEventListener('close', () => {\n console.log('WebSocket disconnected');\n updateStatus(false);\n });\n\n ws.addEventListener('error', (error) => {\n console.error('WebSocket error:', error);\n updateStatus(false);\n });\n\n function updateStatus(connected) {\n if (connected) {\n statusEl.className = 'status-badge connected';\n statusEl.textContent = '\u25CF Connected';\n } else {\n statusEl.className = 'status-badge disconnected';\n statusEl.textContent = '\u25CF Disconnected';\n }\n }\n\n function renderModel() {\n if (!currentModel) return;\n\n // Update stats\n const layerCount = Object.keys(currentModel.layers).length;\n const elementCount = currentModel.totalElements || 0;\n statsEl.textContent = \\`\\${layerCount} layers \u00B7 \\${elementCount} elements\\`;\n\n // Render layers and elements\n modelTreeEl.innerHTML = '';\n const layers = Object.entries(currentModel.layers)\n .sort((a, b) => a[0].localeCompare(b[0]));\n\n for (const [layerName, layerData] of layers) {\n const layerDiv = document.createElement('div');\n layerDiv.className = 'layer-group';\n\n const headerDiv = document.createElement('div');\n headerDiv.className = 'layer-header';\n headerDiv.textContent = layerName;\n headerDiv.addEventListener('click', () => {\n headerDiv.classList.toggle('expanded');\n elementsDiv.classList.toggle('expanded');\n });\n\n const elementsDiv = document.createElement('div');\n elementsDiv.className = 'layer-elements';\n\n for (const element of layerData.elements) {\n const elemDiv = document.createElement('div');\n elemDiv.className = 'element-item';\n if (element.id === selectedElementId) {\n elemDiv.classList.add('selected');\n }\n elemDiv.innerHTML = \\`\\${escapeHtml(element.id)}<br><span style=\"font-size: 11px; color: #999;\">\\${escapeHtml(element.name)}</span>\\`;\n elemDiv.addEventListener('click', () => {\n selectedElementId = element.id;\n document.querySelectorAll('.element-item').forEach(el => el.classList.remove('selected'));\n elemDiv.classList.add('selected');\n renderSelectedElement();\n });\n\n elementsDiv.appendChild(elemDiv);\n }\n\n layerDiv.appendChild(headerDiv);\n layerDiv.appendChild(elementsDiv);\n modelTreeEl.appendChild(layerDiv);\n }\n }\n\n function renderSelectedElement() {\n if (!selectedElementId || !currentModel) {\n detailsEl.innerHTML = '<div class=\"loading\">Select an element to view details</div>';\n return;\n }\n\n // Find element in model\n let element = null;\n for (const layer of Object.values(currentModel.layers)) {\n element = layer.elements.find(e => e.id === selectedElementId);\n if (element) break;\n }\n\n if (!element) {\n detailsEl.innerHTML = '<div class=\"loading\">Element not found</div>';\n return;\n }\n\n let html = \\`\n <div class=\"detail-header\">\n <div class=\"element-type\">\\${escapeHtml(element.type)}</div>\n <h2>\\${escapeHtml(element.name)}</h2>\n <div style=\"font-size: 12px; color: #666; margin-top: 4px;\"><code>\\${escapeHtml(element.id)}</code></div>\n </div>\n \\`;\n\n if (element.description) {\n html += \\`\n <div class=\"detail-section\">\n <h3>Description</h3>\n <div class=\"detail-value\">\\${escapeHtml(element.description)}</div>\n </div>\n \\`;\n }\n\n if (element.properties && Object.keys(element.properties).length > 0) {\n html += \\`\n <div class=\"detail-section\">\n <h3>Properties</h3>\n <div class=\"detail-value\"><pre>\\${escapeHtml(JSON.stringify(element.properties, null, 2))}</pre></div>\n </div>\n \\`;\n }\n\n if (element.annotations && element.annotations.length > 0) {\n html += \\`\n <div class=\"detail-section\">\n <h3>Annotations</h3>\n <div class=\"annotations-list\">\n \\${element.annotations.map(ann => \\`\n <div class=\"annotation-item\">\n <div class=\"author\">\\${escapeHtml(ann.author)}</div>\n <div class=\"text\">\\${escapeHtml(ann.text)}</div>\n <div class=\"timestamp\">\\${new Date(ann.timestamp).toLocaleString()}</div>\n </div>\n \\`).join('')}\n </div>\n </div>\n \\`;\n }\n\n html += \\`\n <div class=\"detail-section\">\n <h3>Add Annotation</h3>\n <form class=\"annotation-form\" onsubmit=\"addAnnotation(event, '\\${escapeHtml(element.id)}')\">\n <input type=\"text\" placeholder=\"Author name\" id=\"ann-author\" required>\n <textarea placeholder=\"Annotation text\" id=\"ann-text\" required></textarea>\n <button type=\"submit\">Add Annotation</button>\n </form>\n </div>\n \\`;\n\n detailsEl.innerHTML = html;\n }\n\n function addAnnotation(event, elementId) {\n event.preventDefault();\n\n const author = document.getElementById('ann-author').value;\n const text = document.getElementById('ann-text').value;\n\n if (!author || !text) return;\n\n // Send to server\n fetch(\\`/api/elements/\\${elementId}/annotations\\`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ author, text })\n })\n .then(r => r.json())\n .then(data => {\n if (data.success) {\n document.getElementById('ann-author').value = '';\n document.getElementById('ann-text').value = '';\n }\n })\n .catch(err => console.error('Failed to add annotation:', err));\n }\n </script>\n</body>\n</html>`;\n }\n\n /**\n * Start the server\n */\n async start(port: number = 8080): Promise<void> {\n this.setupFileWatcher();\n\n this._server = serve({\n port,\n fetch: this.app.fetch,\n websocket, // Add WebSocket handler from hono/bun\n });\n\n console.log(\n `\u2713 Visualization server running at http://localhost:${port}`\n );\n }\n\n /**\n * Get the current auth token\n */\n getAuthToken(): string {\n return this.authToken;\n }\n\n /**\n * Check if authentication is enabled\n */\n isAuthEnabled(): boolean {\n return this.authEnabled;\n }\n\n /**\n * Stop the server\n */\n stop(): void {\n // Close all WebSocket connections\n for (const client of this.clients) {\n try {\n client.close();\n } catch (error) {\n // Ignore errors on close\n }\n }\n this.clients.clear();\n\n // Stop file watcher\n if (this.watcher) {\n this.watcher.close?.();\n }\n\n // Stop the server\n if (this._server) {\n this._server.stop();\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["data", "process"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const isTelemetryEnabled = !1;
|
|
2
|
+
async function telemetryMiddleware(c, next) {
|
|
3
|
+
return next();
|
|
4
|
+
try {
|
|
5
|
+
if (await next(), span && "setAttribute" in span) {
|
|
6
|
+
const statusCode = c.res.status, durationMs = Date.now() - requestStartTime;
|
|
7
|
+
span.setAttribute("http.status_code", statusCode), span.setAttribute("http.duration_ms", durationMs), statusCode >= 500 ? span.setStatus({ code: 2, message: "Server Error" }) : statusCode >= 400 ? span.setStatus({ code: 2, message: "Client Error" }) : span.setStatus({ code: 1 }), await emitLog(statusCode >= 400 ? SeverityNumber.ERROR : SeverityNumber.INFO, "Request completed", {
|
|
8
|
+
"http.method": method,
|
|
9
|
+
"http.route": route,
|
|
10
|
+
"http.target": path,
|
|
11
|
+
"http.status_code": statusCode,
|
|
12
|
+
duration_ms: durationMs
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
} catch (error) {
|
|
16
|
+
throw span && "recordException" in span && (span.recordException(error), span.setStatus({ code: 2, message: error.message })), error;
|
|
17
|
+
} finally {
|
|
18
|
+
endSpan(span);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export {
|
|
22
|
+
telemetryMiddleware
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=telemetry-middleware.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/server/telemetry-middleware.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * OpenTelemetry middleware for Hono server\n * Instruments HTTP requests with spans and semantic attributes\n */\n\nimport type { Context, Next } from 'hono';\n\n// Conditional imports based on telemetry flag\ndeclare const TELEMETRY_ENABLED: boolean | undefined;\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== 'undefined' ? TELEMETRY_ENABLED : false;\n\n/**\n * Hono middleware that creates a span for each HTTP request\n * Captures HTTP semantic attributes according to OpenTelemetry conventions:\n * - http.method: HTTP request method\n * - http.route: HTTP route pattern\n * - http.status_code: HTTP response status code\n * - http.url: Full request URL\n * - http.user_agent: User agent string\n *\n * Also emits logs at request start and completion per SigNoz integration guide\n * for request lifecycle tracking.\n */\nexport async function telemetryMiddleware(c: Context, next: Next): Promise<void> {\n if (!isTelemetryEnabled) {\n return next();\n }\n\n // Dynamic import for tree-shaking\n const { startSpan, endSpan, emitLog } = await import('../telemetry/index.js');\n const { SeverityNumber } = await import('@opentelemetry/api-logs');\n\n const method = c.req.method;\n const path = c.req.path;\n const route = c.req.routePath || path; // Use route pattern if available\n const requestStartTime = Date.now();\n\n // Create span for the HTTP request\n const span = startSpan('http.server.request', {\n 'http.method': method,\n 'http.route': route,\n 'http.url': c.req.url,\n 'http.target': path,\n 'http.user_agent': c.req.header('user-agent') || '',\n 'http.scheme': new URL(c.req.url).protocol.replace(':', ''),\n });\n\n // Log request received (per SigNoz integration guide: request lifecycle logging)\n await emitLog(SeverityNumber.INFO, 'Request received', {\n 'http.method': method,\n 'http.route': route,\n 'http.target': path,\n });\n\n try {\n // Process the request\n await next();\n\n // Add response attributes after request is processed\n if (span && 'setAttribute' in span) {\n const statusCode = c.res.status;\n const durationMs = Date.now() - requestStartTime;\n\n (span as any).setAttribute('http.status_code', statusCode);\n (span as any).setAttribute('http.duration_ms', durationMs);\n\n // Set span status based on HTTP status code\n if (statusCode >= 500) {\n (span as any).setStatus({ code: 2, message: 'Server Error' }); // ERROR\n } else if (statusCode >= 400) {\n (span as any).setStatus({ code: 2, message: 'Client Error' }); // ERROR\n } else {\n (span as any).setStatus({ code: 1 }); // OK\n }\n\n // Log request completed (per SigNoz integration guide: request lifecycle logging)\n await emitLog(\n statusCode >= 400 ? SeverityNumber.ERROR : SeverityNumber.INFO,\n 'Request completed',\n {\n 'http.method': method,\n 'http.route': route,\n 'http.target': path,\n 'http.status_code': statusCode,\n 'duration_ms': durationMs,\n }\n );\n }\n } catch (error) {\n // Record exception in span\n if (span && 'recordException' in span) {\n (span as any).recordException(error as Error);\n (span as any).setStatus({ code: 2, message: (error as Error).message }); // ERROR\n }\n throw error;\n } finally {\n // End the span\n endSpan(span);\n }\n}\n"],
|
|
5
|
+
"mappings": "AASA,MAAM,qBAAgE;AActE,eAAsB,oBAAoB,GAAY,MAAU;AAE5D,SAAO,KAAI;AA6Bb,MAAI;AAKF,QAHA,MAAM,KAAI,GAGN,QAAQ,kBAAkB,MAAM;AAClC,YAAM,aAAa,EAAE,IAAI,QACnB,aAAa,KAAK,IAAG,IAAK;AAE/B,WAAa,aAAa,oBAAoB,UAAU,GACxD,KAAa,aAAa,oBAAoB,UAAU,GAGrD,cAAc,MACf,KAAa,UAAU,EAAE,MAAM,GAAG,SAAS,eAAc,CAAE,IACnD,cAAc,MACtB,KAAa,UAAU,EAAE,MAAM,GAAG,SAAS,eAAc,CAAE,IAE3D,KAAa,UAAU,EAAE,MAAM,EAAC,CAAE,GAIrC,MAAM,QACJ,cAAc,MAAM,eAAe,QAAQ,eAAe,MAC1D,qBACA;QACE,eAAe;QACf,cAAc;QACd,eAAe;QACf,oBAAoB;QACpB,aAAe;OAChB;IAEL;EACF,SAAS,OAAO;AAEd,UAAI,QAAQ,qBAAqB,SAC9B,KAAa,gBAAgB,KAAc,GAC3C,KAAa,UAAU,EAAE,MAAM,GAAG,SAAU,MAAgB,QAAO,CAAE,IAElE;EACR;AAEE,YAAQ,IAAI;EACd;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const isTelemetryEnabled = !1, originalConsole = {
|
|
2
|
+
log: console.log,
|
|
3
|
+
error: console.error,
|
|
4
|
+
warn: console.warn,
|
|
5
|
+
debug: console.debug
|
|
6
|
+
};
|
|
7
|
+
function formatArgs(args) {
|
|
8
|
+
return args.map((arg) => {
|
|
9
|
+
if (typeof arg == "object" && arg !== null)
|
|
10
|
+
try {
|
|
11
|
+
return JSON.stringify(arg);
|
|
12
|
+
} catch {
|
|
13
|
+
return String(arg);
|
|
14
|
+
}
|
|
15
|
+
return String(arg);
|
|
16
|
+
}).join(" ");
|
|
17
|
+
}
|
|
18
|
+
function extractStackTrace(args) {
|
|
19
|
+
const errorArg = args.find((arg) => arg instanceof Error);
|
|
20
|
+
return errorArg ? {
|
|
21
|
+
"error.message": errorArg.message,
|
|
22
|
+
"error.stack": errorArg.stack || ""
|
|
23
|
+
} : {};
|
|
24
|
+
}
|
|
25
|
+
async function installConsoleInterceptor() {
|
|
26
|
+
}
|
|
27
|
+
function uninstallConsoleInterceptor() {
|
|
28
|
+
console.log = originalConsole.log, console.error = originalConsole.error, console.warn = originalConsole.warn, console.debug = originalConsole.debug;
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
installConsoleInterceptor,
|
|
32
|
+
uninstallConsoleInterceptor
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=console-interceptor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/telemetry/console-interceptor.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Console method interceptor for OpenTelemetry log emission.\n *\n * Wraps console.log, console.error, console.warn, and console.debug to emit\n * OpenTelemetry log records while preserving original console behavior.\n *\n * Log records inherit trace context from active spans and include stack traces\n * for errors. This module is only active when TELEMETRY_ENABLED is true.\n *\n * Pattern: All telemetry function calls must be directly guarded by\n * `if (TELEMETRY_ENABLED)` checks in calling code to enable tree-shaking\n * and zero-cost abstractions in production builds.\n */\n\n// Fallback for runtime environments where TELEMETRY_ENABLED is not defined by esbuild\ndeclare const TELEMETRY_ENABLED: boolean | undefined;\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== 'undefined' ? TELEMETRY_ENABLED : false;\n\n// Store original console methods before any wrapping\nconst originalConsole = {\n log: console.log,\n error: console.error,\n warn: console.warn,\n debug: console.debug,\n};\n\n/**\n * Format console arguments into a single string.\n * Converts objects to JSON representation for readability in logs.\n */\nfunction formatArgs(args: any[]): string {\n return args\n .map((arg) => {\n if (typeof arg === 'object' && arg !== null) {\n try {\n return JSON.stringify(arg);\n } catch {\n return String(arg);\n }\n }\n return String(arg);\n })\n .join(' ');\n}\n\n/**\n * Extract error stack trace and message from console arguments.\n * Returns attributes suitable for log record enrichment.\n */\nfunction extractStackTrace(args: any[]): Record<string, string> {\n const errorArg = args.find((arg) => arg instanceof Error);\n if (errorArg) {\n return {\n 'error.message': errorArg.message,\n 'error.stack': errorArg.stack || '',\n };\n }\n return {};\n}\n\n/**\n * Install console interceptor to emit OpenTelemetry log records.\n *\n * Maps console methods to OpenTelemetry severity levels:\n * - console.debug() \u2192 DEBUG\n * - console.log() \u2192 INFO\n * - console.warn() \u2192 WARN\n * - console.error() \u2192 ERROR\n *\n * Each log record includes the active trace and span identifiers\n * from the current OpenTelemetry context (if available).\n *\n * Original console behavior is preserved - all console output still appears.\n *\n * No-op when TELEMETRY_ENABLED is false.\n */\nexport async function installConsoleInterceptor(): Promise<void> {\n if (!isTelemetryEnabled) return;\n\n const [{ SeverityNumber }, { emitLog }] = await Promise.all([\n import('@opentelemetry/api-logs'),\n import('./index.js')\n ]);\n\n console.log = (...args: any[]): void => {\n emitLog(SeverityNumber.INFO, formatArgs(args));\n originalConsole.log.apply(console, args);\n };\n\n console.error = (...args: any[]): void => {\n const attrs = extractStackTrace(args);\n emitLog(SeverityNumber.ERROR, formatArgs(args), attrs);\n originalConsole.error.apply(console, args);\n };\n\n console.warn = (...args: any[]): void => {\n emitLog(SeverityNumber.WARN, formatArgs(args));\n originalConsole.warn.apply(console, args);\n };\n\n console.debug = (...args: any[]): void => {\n emitLog(SeverityNumber.DEBUG, formatArgs(args));\n originalConsole.debug.apply(console, args);\n };\n}\n\n/**\n * Uninstall console interceptor and restore original console methods.\n *\n * Useful for testing to prevent state pollution between tests.\n * Safe to call when telemetry is disabled (no-op).\n */\nexport function uninstallConsoleInterceptor(): void {\n console.log = originalConsole.log;\n console.error = originalConsole.error;\n console.warn = originalConsole.warn;\n console.debug = originalConsole.debug;\n}\n"],
|
|
5
|
+
"mappings": "AAgBA,MAAM,qBAAgE,IAGhE,kBAAkB;EACtB,KAAK,QAAQ;EACb,OAAO,QAAQ;EACf,MAAM,QAAQ;EACd,OAAO,QAAQ;;AAOjB,SAAS,WAAW,MAAW;AAC7B,SAAO,KACJ,IAAI,CAAC,QAAO;AACX,QAAI,OAAO,OAAQ,YAAY,QAAQ;AACrC,UAAI;AACF,eAAO,KAAK,UAAU,GAAG;MAC3B,QAAQ;AACN,eAAO,OAAO,GAAG;MACnB;AAEF,WAAO,OAAO,GAAG;EACnB,CAAC,EACA,KAAK,GAAG;AACb;AAMA,SAAS,kBAAkB,MAAW;AACpC,QAAM,WAAW,KAAK,KAAK,CAAC,QAAQ,eAAe,KAAK;AACxD,SAAI,WACK;IACL,iBAAiB,SAAS;IAC1B,eAAe,SAAS,SAAS;MAG9B,CAAA;AACT;AAkBA,eAAsB,4BAAyB;AA4B/C;AAQM,SAAU,8BAA2B;AACzC,UAAQ,MAAM,gBAAgB,KAC9B,QAAQ,QAAQ,gBAAgB,OAChC,QAAQ,OAAO,gBAAgB,MAC/B,QAAQ,QAAQ,gBAAgB;AAClC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const isTelemetryEnabled = !1;
|
|
2
|
+
let sdk = null, tracer = null, loggerProvider = null, logger = null, cachedContext = null, cachedTrace = null;
|
|
3
|
+
async function initTelemetry() {
|
|
4
|
+
}
|
|
5
|
+
function startSpan(name, attributes) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
async function startActiveSpan(name, fn, attributes) {
|
|
9
|
+
{
|
|
10
|
+
const noopSpan = {
|
|
11
|
+
end: () => {
|
|
12
|
+
},
|
|
13
|
+
setAttribute: () => noopSpan,
|
|
14
|
+
setAttributes: () => noopSpan,
|
|
15
|
+
addEvent: () => noopSpan,
|
|
16
|
+
setStatus: () => noopSpan,
|
|
17
|
+
updateName: () => noopSpan,
|
|
18
|
+
isRecording: () => !1,
|
|
19
|
+
recordException: () => {
|
|
20
|
+
},
|
|
21
|
+
spanContext: () => ({
|
|
22
|
+
traceId: "",
|
|
23
|
+
spanId: "",
|
|
24
|
+
traceFlags: 0
|
|
25
|
+
})
|
|
26
|
+
};
|
|
27
|
+
return fn(noopSpan);
|
|
28
|
+
}
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
tracer.startActiveSpan(name, { attributes }, async (span) => {
|
|
31
|
+
try {
|
|
32
|
+
const result = await fn(span);
|
|
33
|
+
span.end(), resolve(result);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
span.recordException(error), span.end(), reject(error);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function endSpan(span) {
|
|
41
|
+
}
|
|
42
|
+
function emitLog(severity, message, attributes) {
|
|
43
|
+
}
|
|
44
|
+
async function shutdownTelemetry() {
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
emitLog,
|
|
48
|
+
endSpan,
|
|
49
|
+
initTelemetry,
|
|
50
|
+
shutdownTelemetry,
|
|
51
|
+
startActiveSpan,
|
|
52
|
+
startSpan
|
|
53
|
+
};
|
|
54
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/telemetry/index.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * OpenTelemetry instrumentation module.\n *\n * This module wraps OpenTelemetry SDK initialization and provides span creation utilities.\n * It is completely no-op when TELEMETRY_ENABLED is false, allowing complete elimination\n * of telemetry code from production bundles via dead code elimination.\n *\n * Pattern: All telemetry function calls must be directly guarded by `if (TELEMETRY_ENABLED)` checks\n * in calling code to enable tree-shaking and zero-cost abstractions in production builds.\n */\n\nimport type { Span } from '@opentelemetry/api';\nimport type { NodeSDK } from '@opentelemetry/sdk-node';\nimport type { Tracer } from '@opentelemetry/api';\nimport type { Logger as OTelLogger } from '@opentelemetry/api-logs';\n\n// Fallback for runtime environments where TELEMETRY_ENABLED is not defined by esbuild\n// This ensures tests and non-bundled code don't crash\ndeclare const TELEMETRY_ENABLED: boolean | undefined;\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== 'undefined' ? TELEMETRY_ENABLED : false;\n\n// Module-level state: SDK and tracer instances\n// Only initialized when TELEMETRY_ENABLED is true\nlet sdk: NodeSDK | null = null;\nlet tracer: Tracer | null = null;\nlet loggerProvider: any = null; // LoggerProvider type\nlet logger: OTelLogger | null = null;\n\n// Cache OpenTelemetry API imports for synchronous access\nlet cachedContext: any = null;\nlet cachedTrace: any = null;\n\n/**\n * Initialize OpenTelemetry SDK. Must be called before creating spans.\n * No-op when TELEMETRY_ENABLED is false.\n *\n * Initializes both TracerProvider and LoggerProvider with:\n * - Service name: \"dr-cli\"\n * - Service version: from package.json\n * - Project name: from model manifest (if available)\n * - Processors: SimpleSpanProcessor and SimpleLogRecordProcessor\n * - Exporters: ResilientOTLPExporter with circuit-breaker pattern\n *\n * The exporters target http://localhost:4318/v1/traces and /v1/logs by default,\n * configurable via OTEL_EXPORTER_OTLP_ENDPOINT environment variable.\n * Log endpoint can be overridden with OTEL_EXPORTER_OTLP_LOGS_ENDPOINT.\n */\nexport async function initTelemetry(): Promise<void> {\n if (isTelemetryEnabled) {\n // Dynamic imports ensure tree-shaking when TELEMETRY_ENABLED is false\n // These imports are completely eliminated from production builds\n const [\n { NodeSDK },\n { SimpleSpanProcessor },\n { SimpleLogRecordProcessor },\n otelApi,\n { Resource },\n { LoggerProvider },\n { ResilientOTLPExporter },\n { ResilientLogExporter },\n ] = await Promise.all([\n import('@opentelemetry/sdk-node'),\n import('@opentelemetry/sdk-trace-base'),\n import('@opentelemetry/sdk-logs'),\n import('@opentelemetry/api'),\n import('@opentelemetry/resources'),\n import('@opentelemetry/sdk-logs'),\n import('./resilient-exporter.js'),\n import('./resilient-log-exporter.js'),\n ]);\n\n // Cache API imports for synchronous access in other functions\n const { trace, context } = otelApi;\n cachedTrace = trace;\n cachedContext = context;\n\n // Attempt to get CLI version from package.json\n let cliVersion = '0.1.0';\n try {\n const pkg = await import('../../package.json', { assert: { type: 'json' } });\n cliVersion = pkg.default.version || '0.1.0';\n } catch {\n // Use default version if package.json not accessible\n }\n\n // Attempt to load project name from model manifest\n let projectName = 'unknown';\n try {\n // Try to load manifest - this is a best-effort operation\n // If it fails, we just use 'unknown'\n const [{ readFileSync, existsSync }, { parse }] = await Promise.all([\n import('fs'),\n import('yaml'),\n ]);\n\n // Resolve model root and load manifest\n const possiblePaths = [\n `${process.cwd()}/documentation-robotics/model/manifest.yaml`,\n `${process.cwd()}/model/manifest.yaml`,\n ];\n\n for (const manifestPath of possiblePaths) {\n try {\n if (existsSync(manifestPath)) {\n const content = readFileSync(manifestPath, 'utf-8');\n const data = parse(content);\n projectName = data.project?.name || 'unknown';\n break;\n }\n } catch {\n // Continue to next path\n }\n }\n } catch {\n // No manifest found or error loading - use 'unknown'\n }\n\n // Create resource with service attributes\n const resource = new Resource({\n 'service.name': process.env.OTEL_SERVICE_NAME || 'dr-cli',\n 'service.version': cliVersion,\n 'dr.project.name': projectName,\n });\n\n // Create trace exporter with circuit-breaker pattern\n const traceExporter = new ResilientOTLPExporter({\n url:\n process.env.OTEL_EXPORTER_OTLP_ENDPOINT ||\n 'http://localhost:4318/v1/traces',\n timeoutMillis: 500,\n });\n\n // Initialize NodeSDK with SimpleSpanProcessor for immediate export\n const nodeSdk = new NodeSDK({\n resource,\n spanProcessor: new SimpleSpanProcessor(traceExporter),\n });\n\n // Start the SDK\n nodeSdk.start();\n sdk = nodeSdk;\n\n // Get tracer instance for span creation\n tracer = trace.getTracer('dr-cli', cliVersion);\n\n // Create log exporter with circuit-breaker pattern\n const logEndpoint =\n process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ||\n process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.replace(/\\/v1\\/traces$/, '/v1/logs') ||\n 'http://localhost:4318/v1/logs';\n\n const logExporter = new ResilientLogExporter({\n url: logEndpoint,\n timeoutMillis: 500,\n });\n\n // Initialize LoggerProvider\n loggerProvider = new LoggerProvider({\n resource,\n });\n loggerProvider.addLogRecordProcessor(\n new SimpleLogRecordProcessor(logExporter)\n );\n\n // Get logger instance for log emission\n logger = loggerProvider.getLogger('dr-cli', cliVersion);\n }\n}\n\n/**\n * Create and start a new span with the given name and optional attributes.\n * Returns null when telemetry is disabled.\n *\n * This function respects the active context - if there's an active span,\n * the new span will be created as a child of it. This enables proper\n * trace hierarchy.\n *\n * IMPORTANT: This function does NOT set the span as active in the context.\n * For automatic context propagation, use `startActiveSpan()` instead.\n *\n * Must be called within an `if (TELEMETRY_ENABLED)` guard in calling code:\n *\n * ```typescript\n * if (TELEMETRY_ENABLED) {\n * const span = startSpan('operation', { 'attr.key': 'value' });\n * try {\n * // ... operation ...\n * } finally {\n * endSpan(span);\n * }\n * }\n * ```\n */\nexport function startSpan(\n name: string,\n attributes?: Record<string, any>\n): Span | null {\n if (isTelemetryEnabled && tracer && cachedContext) {\n // Start span with current context to respect parent-child relationships\n return tracer.startSpan(name, { attributes }, cachedContext.active());\n }\n return null;\n}\n\n/**\n * Execute a function with an active span that properly propagates context.\n * Child spans created within the callback will automatically be linked to this span.\n *\n * Returns the result of the callback function.\n * Returns the callback result directly when telemetry is disabled.\n *\n * ```typescript\n * const result = await startActiveSpan('operation', async (span) => {\n * // Child spans here will be properly linked\n * return doWork();\n * }, { 'attr.key': 'value' });\n * ```\n */\nexport async function startActiveSpan<T>(\n name: string,\n fn: (span: Span) => Promise<T>,\n attributes?: Record<string, any>\n): Promise<T> {\n if (!isTelemetryEnabled || !tracer) {\n // Create a no-op span for the callback\n const noopSpan = {\n end: () => {},\n setAttribute: () => noopSpan,\n setAttributes: () => noopSpan,\n addEvent: () => noopSpan,\n setStatus: () => noopSpan,\n updateName: () => noopSpan,\n isRecording: () => false,\n recordException: () => {},\n spanContext: () => ({\n traceId: '',\n spanId: '',\n traceFlags: 0,\n }),\n } as any;\n return fn(noopSpan);\n }\n\n // Use tracer.startActiveSpan for proper context propagation\n return new Promise<T>((resolve, reject) => {\n tracer!.startActiveSpan(name, { attributes }, async (span) => {\n try {\n const result = await fn(span);\n span.end();\n resolve(result);\n } catch (error) {\n span.recordException(error as Error);\n span.end();\n reject(error);\n }\n });\n });\n}\n\n/**\n * End a span. Safe to call with null (no-op).\n *\n * Should be called in a finally block to ensure proper cleanup:\n *\n * ```typescript\n * const span = startSpan('operation');\n * try {\n * // ... operation ...\n * } finally {\n * endSpan(span);\n * }\n * ```\n */\nexport function endSpan(span: Span | null): void {\n if (isTelemetryEnabled && span) {\n span.end();\n }\n}\n\n/**\n * Emit a log record with optional severity level and attributes.\n * Automatically correlates logs with active spans by attaching traceId and spanId.\n * Returns without emitting if telemetry is disabled.\n *\n * ```typescript\n * import { SeverityNumber } from '@opentelemetry/api-logs';\n * emitLog(SeverityNumber.INFO, 'User logged in', { 'user.id': '12345' });\n * ```\n */\nexport function emitLog(\n severity: number,\n message: string,\n attributes?: Record<string, any>\n): void {\n if (!isTelemetryEnabled || !logger || !cachedTrace) {\n return;\n }\n\n const span = cachedTrace.getActiveSpan();\n const context = span?.spanContext();\n\n // Map severity number to text representation\n // Reference: https://opentelemetry.io/docs/reference/specification/logs/data-model/#severity_number\n const severityMap: Record<number, string> = {\n 0: 'UNSPECIFIED',\n 1: 'TRACE',\n 2: 'TRACE2',\n 3: 'TRACE3',\n 4: 'TRACE4',\n 5: 'DEBUG',\n 6: 'DEBUG2',\n 7: 'DEBUG3',\n 8: 'DEBUG4',\n 9: 'INFO',\n 10: 'INFO2',\n 11: 'INFO3',\n 12: 'INFO4',\n 13: 'WARN',\n 14: 'WARN2',\n 15: 'WARN3',\n 16: 'WARN4',\n 17: 'ERROR',\n 18: 'ERROR2',\n 19: 'ERROR3',\n 20: 'ERROR4',\n 21: 'FATAL',\n 22: 'FATAL2',\n 23: 'FATAL3',\n 24: 'FATAL4',\n };\n\n const severityText = severityMap[severity] || 'UNSPECIFIED';\n\n logger.emit({\n severityNumber: severity,\n severityText,\n body: message,\n attributes: {\n ...attributes,\n ...(context && {\n 'trace.id': context.traceId,\n 'span.id': context.spanId,\n }),\n },\n });\n}\n\n/**\n * Shutdown the OpenTelemetry SDK gracefully.\n * Should be called on process exit to ensure all pending spans and logs are exported.\n *\n * Ignores shutdown failures to avoid blocking process exit.\n */\nexport async function shutdownTelemetry(): Promise<void> {\n if (isTelemetryEnabled) {\n try {\n await sdk?.shutdown();\n } catch {\n // Silently ignore shutdown failures - don't block process exit\n }\n\n try {\n await loggerProvider?.shutdown();\n } catch {\n // Silently ignore shutdown failures - don't block process exit\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAmBA,MAAM,qBAAgE;AAItE,IAAI,MAAsB,MACtB,SAAwB,MACxB,iBAAsB,MACtB,SAA4B,MAG5B,gBAAqB,MACrB,cAAmB;AAiBvB,eAAsB,gBAAa;AAwHnC;AA0BM,SAAU,UACd,MACA,YAAgC;AAMhC,SAAO;AACT;AAgBA,eAAsB,gBACpB,MACA,IACA,YAAgC;AAEI;AAElC,UAAM,WAAW;MACf,KAAK,MAAK;MAAE;MACZ,cAAc,MAAM;MACpB,eAAe,MAAM;MACrB,UAAU,MAAM;MAChB,WAAW,MAAM;MACjB,YAAY,MAAM;MAClB,aAAa,MAAM;MACnB,iBAAiB,MAAK;MAAE;MACxB,aAAa,OAAO;QAClB,SAAS;QACT,QAAQ;QACR,YAAY;;;AAGhB,WAAO,GAAG,QAAQ;EACpB;AAGA,SAAO,IAAI,QAAW,CAAC,SAAS,WAAU;AACxC,WAAQ,gBAAgB,MAAM,EAAE,WAAU,GAAI,OAAO,SAAQ;AAC3D,UAAI;AACF,cAAM,SAAS,MAAM,GAAG,IAAI;AAC5B,aAAK,IAAG,GACR,QAAQ,MAAM;MAChB,SAAS,OAAO;AACd,aAAK,gBAAgB,KAAc,GACnC,KAAK,IAAG,GACR,OAAO,KAAK;MACd;IACF,CAAC;EACH,CAAC;AACH;AAgBM,SAAU,QAAQ,MAAiB;AAIzC;AAYM,SAAU,QACd,UACA,SACA,YAAgC;AAqDlC;AAQA,eAAsB,oBAAiB;AAcvC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ExportResultCode } from "@opentelemetry/core";
|
|
2
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
3
|
+
class ResilientOTLPExporter {
|
|
4
|
+
delegate;
|
|
5
|
+
retryAfter = 0;
|
|
6
|
+
// Circuit-breaker timestamp
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.delegate = new OTLPTraceExporter({
|
|
9
|
+
...config,
|
|
10
|
+
url: config?.url || "http://localhost:4318/v1/traces",
|
|
11
|
+
timeoutMillis: config?.timeoutMillis ?? 5e3
|
|
12
|
+
// 5s timeout to allow for network latency
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export(spans, resultCallback) {
|
|
16
|
+
if (Date.now() < this.retryAfter) {
|
|
17
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this.delegate.export(spans, (result) => {
|
|
21
|
+
result.code === ExportResultCode.FAILED ? (this.retryAfter = Date.now() + 3e4, resultCallback({ code: ExportResultCode.SUCCESS })) : (this.retryAfter = 0, resultCallback(result));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async forceFlush() {
|
|
25
|
+
if (!(Date.now() < this.retryAfter))
|
|
26
|
+
try {
|
|
27
|
+
await this.delegate.forceFlush?.();
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async shutdown() {
|
|
32
|
+
try {
|
|
33
|
+
await this.delegate.shutdown();
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
ResilientOTLPExporter
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=resilient-exporter.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/telemetry/resilient-exporter.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Resilient OTLP exporter with circuit-breaker pattern.\n * Gracefully handles missing OTLP collectors by silently discarding spans\n * instead of blocking CLI execution.\n */\n\nimport type {\n SpanExporter,\n ReadableSpan,\n} from '@opentelemetry/sdk-trace-base';\nimport type { ExportResult } from '@opentelemetry/core';\nimport { ExportResultCode } from '@opentelemetry/core';\nimport { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';\n\n/**\n * OTLP exporter with circuit-breaker pattern for graceful failure.\n * Silently discards spans when collector is unavailable instead of blocking.\n *\n * Circuit breaker behavior:\n * - Initial state: Attempts export to configured OTLP endpoint\n * - On failure: Sets 30-second backoff window, silently discards spans\n * - Backoff expired: Retries export, resets on success or extends on failure\n * - Timeout: 5000ms timeout to accommodate network latency\n */\nexport class ResilientOTLPExporter implements SpanExporter {\n private delegate: OTLPTraceExporter;\n private retryAfter = 0; // Circuit-breaker timestamp\n\n constructor(\n config?: Record<string, unknown> & {\n url?: string;\n timeoutMillis?: number;\n }\n ) {\n this.delegate = new OTLPTraceExporter({\n ...config,\n url: config?.url || 'http://localhost:4318/v1/traces',\n timeoutMillis: config?.timeoutMillis ?? 5000, // 5s timeout to allow for network latency\n });\n }\n\n export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {\n // Circuit breaker: skip export if recently failed.\n // Boundary: when Date.now() === this.retryAfter, retry immediately (intended behavior).\n if (Date.now() < this.retryAfter) {\n // Pretend success and silently discard spans\n resultCallback({ code: ExportResultCode.SUCCESS });\n return;\n }\n\n this.delegate.export(spans, (result: ExportResult) => {\n if (result.code === ExportResultCode.FAILED) {\n // Set 30-second backoff window after first failure\n this.retryAfter = Date.now() + 30000;\n // Report success to SDK so it doesn't queue/retry internally\n // The backoff mechanism prevents further export attempts\n resultCallback({ code: ExportResultCode.SUCCESS });\n } else {\n // Clear backoff on successful export\n this.retryAfter = 0;\n resultCallback(result);\n }\n });\n }\n\n async forceFlush(): Promise<void> {\n // Circuit breaker open: skip flush to avoid wasting resources\n if (Date.now() < this.retryAfter) {\n return;\n }\n\n try {\n await this.delegate.forceFlush?.();\n } catch {\n // Silently ignore flush failures - don't block shutdown\n }\n }\n\n async shutdown(): Promise<void> {\n try {\n await this.delegate.shutdown();\n } catch {\n // Silently ignore shutdown failures - don't let telemetry block process exit\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAWA,SAAS,wBAAwB;AACjC,SAAS,yBAAyB;AAY5B,MAAO,sBAAqB;EACxB;EACA,aAAa;;EAErB,YACE,QAGC;AAED,SAAK,WAAW,IAAI,kBAAkB;MACpC,GAAG;MACH,KAAK,QAAQ,OAAO;MACpB,eAAe,QAAQ,iBAAiB;;KACzC;EACH;EAEA,OAAO,OAAuB,gBAA8C;AAG1E,QAAI,KAAK,IAAG,IAAK,KAAK,YAAY;AAEhC,qBAAe,EAAE,MAAM,iBAAiB,QAAO,CAAE;AACjD;IACF;AAEA,SAAK,SAAS,OAAO,OAAO,CAAC,WAAwB;AACjD,MAAI,OAAO,SAAS,iBAAiB,UAEnC,KAAK,aAAa,KAAK,IAAG,IAAK,KAG/B,eAAe,EAAE,MAAM,iBAAiB,QAAO,CAAE,MAGjD,KAAK,aAAa,GAClB,eAAe,MAAM;IAEzB,CAAC;EACL;EAEA,MAAM,aAAU;AAEd,QAAI,OAAK,IAAG,IAAK,KAAK;AAItB,UAAI;AACF,cAAM,KAAK,SAAS,aAAY;MAClC,QAAQ;MAER;EACF;EAEA,MAAM,WAAQ;AACZ,QAAI;AACF,YAAM,KAAK,SAAS,SAAQ;IAC9B,QAAQ;IAER;EACF;;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ExportResultCode } from "@opentelemetry/core";
|
|
2
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
3
|
+
class ResilientLogExporter {
|
|
4
|
+
delegate;
|
|
5
|
+
retryAfter = 0;
|
|
6
|
+
// Circuit-breaker timestamp
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.delegate = new OTLPLogExporter({
|
|
9
|
+
...config,
|
|
10
|
+
url: config?.url || "http://localhost:4318/v1/logs",
|
|
11
|
+
timeoutMillis: config?.timeoutMillis ?? 5e3
|
|
12
|
+
// 5s timeout to allow for network latency
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
export(records, resultCallback) {
|
|
16
|
+
if (Date.now() < this.retryAfter) {
|
|
17
|
+
resultCallback({ code: ExportResultCode.SUCCESS });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
this.delegate.export(records, (result) => {
|
|
21
|
+
result.code === ExportResultCode.FAILED ? (this.retryAfter = Date.now() + 3e4, resultCallback({ code: ExportResultCode.SUCCESS })) : (this.retryAfter = 0, resultCallback(result));
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
async forceFlush() {
|
|
25
|
+
if (!(Date.now() < this.retryAfter))
|
|
26
|
+
try {
|
|
27
|
+
await this.delegate.forceFlush?.();
|
|
28
|
+
} catch {
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
async shutdown() {
|
|
32
|
+
try {
|
|
33
|
+
await this.delegate.shutdown();
|
|
34
|
+
} catch {
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
ResilientLogExporter
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=resilient-log-exporter.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/telemetry/resilient-log-exporter.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Resilient OTLP log exporter with circuit-breaker pattern.\n * Gracefully handles missing OTLP collectors by silently discarding log records\n * instead of blocking CLI execution.\n */\n\nimport type {\n LogRecordExporter,\n ReadableLogRecord,\n} from '@opentelemetry/sdk-logs';\nimport type { ExportResult } from '@opentelemetry/core';\nimport { ExportResultCode } from '@opentelemetry/core';\nimport { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';\n\n/**\n * OTLP log exporter with circuit-breaker pattern for graceful failure.\n * Silently discards log records when collector is unavailable instead of blocking.\n *\n * Circuit breaker behavior:\n * - Initial state: Attempts export to configured OTLP endpoint\n * - On failure: Sets 30-second backoff window, silently discards log records\n * - Backoff expired: Retries export, resets on success or extends on failure\n * - Timeout: 5000ms timeout to accommodate network latency\n */\nexport class ResilientLogExporter implements LogRecordExporter {\n private delegate: OTLPLogExporter;\n private retryAfter = 0; // Circuit-breaker timestamp\n\n constructor(\n config?: Record<string, unknown> & {\n url?: string;\n timeoutMillis?: number;\n }\n ) {\n this.delegate = new OTLPLogExporter({\n ...config,\n url: config?.url || 'http://localhost:4318/v1/logs',\n timeoutMillis: config?.timeoutMillis ?? 5000, // 5s timeout to allow for network latency\n });\n }\n\n export(\n records: ReadableLogRecord[],\n resultCallback: (result: ExportResult) => void\n ): void {\n // Circuit breaker: skip export if recently failed.\n // Boundary: when Date.now() === this.retryAfter, retry immediately (intended behavior).\n if (Date.now() < this.retryAfter) {\n // Pretend success and silently discard log records\n resultCallback({ code: ExportResultCode.SUCCESS });\n return;\n }\n\n this.delegate.export(records, (result: ExportResult) => {\n if (result.code === ExportResultCode.FAILED) {\n // Set 30-second backoff window after first failure\n this.retryAfter = Date.now() + 30000;\n // Report success to SDK so it doesn't queue/retry internally\n // The backoff mechanism prevents further export attempts\n resultCallback({ code: ExportResultCode.SUCCESS });\n } else {\n // Clear backoff on successful export\n this.retryAfter = 0;\n resultCallback(result);\n }\n });\n }\n\n async forceFlush(): Promise<void> {\n // Circuit breaker open: skip flush to avoid wasting resources\n if (Date.now() < this.retryAfter) {\n return;\n }\n\n try {\n await this.delegate.forceFlush?.();\n } catch {\n // Silently ignore flush failures - don't block shutdown\n }\n }\n\n async shutdown(): Promise<void> {\n try {\n await this.delegate.shutdown();\n } catch {\n // Silently ignore shutdown failures - don't let telemetry block process exit\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAWA,SAAS,wBAAwB;AACjC,SAAS,uBAAuB;AAY1B,MAAO,qBAAoB;EACvB;EACA,aAAa;;EAErB,YACE,QAGC;AAED,SAAK,WAAW,IAAI,gBAAgB;MAClC,GAAG;MACH,KAAK,QAAQ,OAAO;MACpB,eAAe,QAAQ,iBAAiB;;KACzC;EACH;EAEA,OACE,SACA,gBAA8C;AAI9C,QAAI,KAAK,IAAG,IAAK,KAAK,YAAY;AAEhC,qBAAe,EAAE,MAAM,iBAAiB,QAAO,CAAE;AACjD;IACF;AAEA,SAAK,SAAS,OAAO,SAAS,CAAC,WAAwB;AACrD,MAAI,OAAO,SAAS,iBAAiB,UAEnC,KAAK,aAAa,KAAK,IAAG,IAAK,KAG/B,eAAe,EAAE,MAAM,iBAAiB,QAAO,CAAE,MAGjD,KAAK,aAAa,GAClB,eAAe,MAAM;IAEzB,CAAC;EACH;EAEA,MAAM,aAAU;AAEd,QAAI,OAAK,IAAG,IAAK,KAAK;AAItB,UAAI;AACF,cAAM,KAAK,SAAS,aAAY;MAClC,QAAQ;MAER;EACF;EAEA,MAAM,WAAQ;AACZ,QAAI;AACF,YAAM,KAAK,SAAS,SAAQ;IAC9B,QAAQ;IAER;EACF;;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const isTelemetryEnabled = !1, testContext = {
|
|
2
|
+
fileSpan: null,
|
|
3
|
+
currentTestFile: "unknown"
|
|
4
|
+
};
|
|
5
|
+
function startTestFileSpan(filePath) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
function endTestFileSpan() {
|
|
9
|
+
}
|
|
10
|
+
function createTestCaseSpan(testName, suiteName) {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function recordTestResult(span, status, error) {
|
|
14
|
+
}
|
|
15
|
+
function instrumentTest(testName, testFn, suiteName) {
|
|
16
|
+
return async () => {
|
|
17
|
+
const span = createTestCaseSpan(testName, suiteName);
|
|
18
|
+
try {
|
|
19
|
+
await testFn();
|
|
20
|
+
} catch (error) {
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
createTestCaseSpan,
|
|
27
|
+
endTestFileSpan,
|
|
28
|
+
instrumentTest,
|
|
29
|
+
recordTestResult,
|
|
30
|
+
startTestFileSpan
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=test-instrumentation.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/telemetry/test-instrumentation.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Test instrumentation utilities for OpenTelemetry.\n *\n * Provides utilities for creating spans with test-specific attributes to enable\n * filtering test telemetry in observability platforms like SigNoz.\n *\n * Each test file execution creates a parent span with `test.file` and\n * `test.framework=bun` attributes. Each test case execution creates a child span\n * with `test.name` (test case name) and `test.suite` (describe block name) attributes.\n *\n * Failed test spans include `test.error.message` and `test.error.stack` attributes.\n * All test spans inherit `dr.project.name` from resource attributes.\n *\n * Pattern: All telemetry function calls must be directly guarded by\n * `if (TELEMETRY_ENABLED)` checks in calling code to enable tree-shaking\n * and zero-cost abstractions in production builds.\n */\n\nimport type { Span } from '@opentelemetry/api';\n\n// Fallback for runtime environments where TELEMETRY_ENABLED is not defined by esbuild\ndeclare const TELEMETRY_ENABLED: boolean | undefined;\nconst isTelemetryEnabled = typeof TELEMETRY_ENABLED !== 'undefined' ? TELEMETRY_ENABLED : false;\n\ninterface TestSpanContext {\n fileSpan: Span | null;\n currentTestFile: string;\n}\n\nconst testContext: TestSpanContext = {\n fileSpan: null,\n currentTestFile: 'unknown',\n};\n\n/**\n * Start a file-level span for test suite execution.\n * Call this at the beginning of each test file (e.g., in beforeAll hook).\n *\n * Returns the span object for later cleanup, or null when telemetry is disabled.\n *\n * @param filePath - The path to the test file\n * @returns The started span, or null if telemetry is disabled\n *\n * @example\n * ```typescript\n * import { beforeAll, afterAll } from 'bun:test';\n * import { startTestFileSpan, endTestFileSpan } from '../../src/telemetry/test-instrumentation.js';\n *\n * beforeAll(() => {\n * startTestFileSpan('tests/unit/example.test.ts');\n * });\n *\n * afterAll(() => {\n * endTestFileSpan();\n * });\n * ```\n */\nexport function startTestFileSpan(filePath: string): Span | null {\n if (!isTelemetryEnabled) return null;\n\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const { startSpan } = require('./index.js');\n\n testContext.currentTestFile = filePath;\n testContext.fileSpan = startSpan('test.file', {\n 'test.file': filePath,\n 'test.framework': 'bun',\n });\n return testContext.fileSpan;\n}\n\n/**\n * End the file-level span.\n * Call this at the end of each test file (e.g., in afterAll hook).\n *\n * No-op when telemetry is disabled.\n */\nexport function endTestFileSpan(): void {\n if (!isTelemetryEnabled) return;\n\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const { endSpan } = require('./index.js');\n\n if (testContext.fileSpan) {\n endSpan(testContext.fileSpan);\n testContext.fileSpan = null;\n }\n}\n\n/**\n * Create a span for a test case execution.\n *\n * Should be called at the start of each test case. The returned span should be\n * passed to recordTestResult() after the test completes to record the result\n * and end the span.\n *\n * @param testName - The name of the test case\n * @param suiteName - Optional describe block name (suite name)\n * @returns The started span, or null if telemetry is disabled\n *\n * @example\n * ```typescript\n * const span = createTestCaseSpan('should validate input', 'ValidationTests');\n * try {\n * // test code here\n * recordTestResult(span, 'pass');\n * } catch (error) {\n * recordTestResult(span, 'fail', error);\n * throw error;\n * }\n * ```\n */\nexport function createTestCaseSpan(\n testName: string,\n suiteName?: string\n): Span | null {\n if (!isTelemetryEnabled) return null;\n\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const { startSpan } = require('./index.js');\n\n return startSpan('test.case', {\n 'test.name': testName,\n 'test.suite': suiteName || '',\n 'test.file': testContext.currentTestFile,\n });\n}\n\n/**\n * Record test result and end the test case span.\n *\n * Sets the `test.status` attribute and, for failed tests, records the error\n * message and stack trace. The span is ended after recording the result.\n *\n * Safe to call with null span (no-op).\n *\n * @param span - The test case span (from createTestCaseSpan)\n * @param status - Test result status: 'pass', 'fail', or 'skip'\n * @param error - Optional Error object for failed tests\n *\n * @example\n * ```typescript\n * const span = createTestCaseSpan('my test');\n * try {\n * // test logic\n * recordTestResult(span, 'pass');\n * } catch (error) {\n * recordTestResult(span, 'fail', error as Error);\n * throw error; // Re-throw to fail the test\n * }\n * ```\n */\nexport function recordTestResult(\n span: Span | null,\n status: 'pass' | 'fail' | 'skip',\n error?: Error\n): void {\n if (!isTelemetryEnabled || !span) return;\n\n span.setAttribute('test.status', status);\n\n if (error) {\n span.setAttribute('test.error.message', error.message);\n span.setAttribute('test.error.stack', error.stack || '');\n span.recordException(error);\n }\n\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const { endSpan } = require('./index.js');\n endSpan(span);\n}\n\n/**\n * Helper for wrapping Bun test cases with instrumentation.\n *\n * This helper wraps a test function to automatically create a test case span,\n * execute the test, record the result, and end the span. Failures are recorded\n * and re-thrown to properly fail the test.\n *\n * @param testName - The name of the test case\n * @param testFn - The test function (async or sync)\n * @param suiteName - Optional describe block name for organization\n * @returns A wrapped test function that handles instrumentation\n *\n * @example\n * ```typescript\n * import { test, describe } from 'bun:test';\n * import { instrumentTest } from '../../src/telemetry/test-instrumentation.js';\n *\n * describe('MyComponent', () => {\n * test('should work', instrumentTest('should work', async () => {\n * // Test logic here\n * }, 'MyComponent'));\n *\n * test('should handle errors', instrumentTest('should handle errors', async () => {\n * // More test logic\n * }, 'MyComponent'));\n * });\n * ```\n */\nexport function instrumentTest(\n testName: string,\n testFn: () => void | Promise<void>,\n suiteName?: string\n): () => Promise<void> {\n return async () => {\n const span = createTestCaseSpan(testName, suiteName);\n try {\n await testFn();\n recordTestResult(span, 'pass');\n } catch (error) {\n recordTestResult(span, 'fail', error as Error);\n throw error; // Re-throw to fail the test\n }\n };\n}\n"],
|
|
5
|
+
"mappings": "AAsBA,MAAM,qBAAgE,IAOhE,cAA+B;EACnC,UAAU;EACV,iBAAiB;;AA0Bb,SAAU,kBAAkB,UAAgB;AACvB,SAAO;AAWlC;AAQM,SAAU,kBAAe;AAU/B;AAyBM,SAAU,mBACd,UACA,WAAkB;AAEO,SAAO;AAUlC;AA0BM,SAAU,iBACd,MACA,QACA,OAAa;AAef;AA8BM,SAAU,eACd,UACA,QACA,WAAkB;AAElB,SAAO,YAAW;AAChB,UAAM,OAAO,mBAAmB,UAAU,SAAS;AACnD,QAAI;AACF,YAAM,OAAM;IAEd,SAAS,OAAO;AACd,YACM;IACR;EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
async function findElementLayer(model, elementId) {
|
|
2
|
+
for (const layerName of model.getLayerNames()) {
|
|
3
|
+
const layer = await model.getLayer(layerName);
|
|
4
|
+
if (layer && layer.getElement(elementId))
|
|
5
|
+
return layerName;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export {
|
|
9
|
+
findElementLayer
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=element-utils.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/utils/element-utils.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Element utility functions shared across commands\n */\n\nimport { Model } from '../core/model.js';\n\n/**\n * Find which layer contains an element by ID\n */\nexport async function findElementLayer(\n model: Model,\n elementId: string\n): Promise<string | undefined> {\n for (const layerName of model.getLayerNames()) {\n const layer = await model.getLayer(layerName);\n if (layer && layer.getElement(elementId)) {\n return layerName;\n }\n }\n return undefined;\n}\n"],
|
|
5
|
+
"mappings": "AASA,eAAsB,iBACpB,OACA,WAAiB;AAEjB,aAAW,aAAa,MAAM,cAAa,GAAI;AAC7C,UAAM,QAAQ,MAAM,MAAM,SAAS,SAAS;AAC5C,QAAI,SAAS,MAAM,WAAW,SAAS;AACrC,aAAO;EAEX;AAEF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|