@checkstack/ui 0.5.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/package.json +8 -15
  3. package/src/components/CodeEditor/CodeEditor.tsx +14 -420
  4. package/src/components/CodeEditor/MonacoEditor.tsx +530 -0
  5. package/src/components/CodeEditor/generateTypeDefinitions.ts +169 -0
  6. package/src/components/CodeEditor/index.ts +4 -3
  7. package/src/components/CodeEditor/templateUtils.test.ts +87 -0
  8. package/src/components/CodeEditor/templateUtils.ts +81 -0
  9. package/src/components/DynamicForm/FormField.tsx +13 -7
  10. package/src/components/DynamicForm/MultiTypeEditorField.tsx +33 -0
  11. package/src/components/DynamicForm/utils.ts +3 -0
  12. package/src/components/StatusUpdateTimeline.tsx +6 -6
  13. package/src/components/Tabs.tsx +1 -0
  14. package/src/components/CodeEditor/languageSupport/enterBehavior.test.ts +0 -173
  15. package/src/components/CodeEditor/languageSupport/enterBehavior.ts +0 -35
  16. package/src/components/CodeEditor/languageSupport/index.ts +0 -22
  17. package/src/components/CodeEditor/languageSupport/json-utils.ts +0 -117
  18. package/src/components/CodeEditor/languageSupport/json.test.ts +0 -274
  19. package/src/components/CodeEditor/languageSupport/json.ts +0 -139
  20. package/src/components/CodeEditor/languageSupport/markdown-utils.ts +0 -65
  21. package/src/components/CodeEditor/languageSupport/markdown.test.ts +0 -245
  22. package/src/components/CodeEditor/languageSupport/markdown.ts +0 -134
  23. package/src/components/CodeEditor/languageSupport/types.ts +0 -48
  24. package/src/components/CodeEditor/languageSupport/xml-utils.ts +0 -94
  25. package/src/components/CodeEditor/languageSupport/xml.test.ts +0 -239
  26. package/src/components/CodeEditor/languageSupport/xml.ts +0 -116
  27. package/src/components/CodeEditor/languageSupport/yaml-utils.ts +0 -101
  28. package/src/components/CodeEditor/languageSupport/yaml.test.ts +0 -203
  29. package/src/components/CodeEditor/languageSupport/yaml.ts +0 -120
@@ -0,0 +1,530 @@
1
+ import React from "react";
2
+ import Editor, {
3
+ loader,
4
+ type OnMount,
5
+ type Monaco,
6
+ } from "@monaco-editor/react";
7
+ import type { editor, Position } from "monaco-editor";
8
+ import { detectOpenTemplate, detectAutoClosedBraces } from "./templateUtils";
9
+
10
+ // Configure Monaco to use self-hosted files (for air-gapped environments)
11
+ // The @monaco-editor/react package will load from node_modules by default
12
+ loader.config({
13
+ "vs/nls": { availableLanguages: { "*": "en" } },
14
+ });
15
+
16
+ export type CodeEditorLanguage =
17
+ | "json"
18
+ | "yaml"
19
+ | "xml"
20
+ | "markdown"
21
+ | "javascript"
22
+ | "typescript"
23
+ | "shell";
24
+
25
+ /**
26
+ * A single payload property available for templating
27
+ */
28
+ export interface TemplateProperty {
29
+ /** Full path to the property, e.g., "payload.incident.title" */
30
+ path: string;
31
+ /** Type of the property, e.g., "string", "number", "boolean" */
32
+ type: string;
33
+ /** Optional description of the property */
34
+ description?: string;
35
+ }
36
+
37
+ export interface CodeEditorProps {
38
+ /** Unique identifier for the editor */
39
+ id?: string;
40
+ /** Current value of the editor */
41
+ value: string;
42
+ /** Callback when the value changes */
43
+ onChange: (value: string) => void;
44
+ /** Language for syntax highlighting */
45
+ language?: CodeEditorLanguage;
46
+ /** Minimum height of the editor */
47
+ minHeight?: string;
48
+ /** Whether the editor is read-only */
49
+ readOnly?: boolean;
50
+ /** Placeholder text when empty */
51
+ placeholder?: string;
52
+ /**
53
+ * TypeScript type definitions to inject for IntelliSense.
54
+ * Generated from JSON schemas for context-aware autocomplete.
55
+ */
56
+ typeDefinitions?: string;
57
+ /**
58
+ * Optional template properties for autocomplete.
59
+ * When provided, typing "{{" triggers autocomplete with available template variables.
60
+ */
61
+ templateProperties?: TemplateProperty[];
62
+ }
63
+
64
+ // Map language names to Monaco language IDs
65
+ const languageMap: Record<string, string> = {
66
+ json: "json",
67
+ yaml: "yaml",
68
+ xml: "xml",
69
+ markdown: "markdown",
70
+ javascript: "javascript",
71
+ typescript: "typescript",
72
+ shell: "shell",
73
+ };
74
+
75
+ // Track if we've registered the json-template language
76
+ let jsonTemplateLanguageRegistered = false;
77
+
78
+ /**
79
+ * Default type definitions for backend TypeScript/JavaScript editors.
80
+ * Provides console and fetch APIs without DOM types.
81
+ * Used when no custom typeDefinitions are provided to the editor.
82
+ */
83
+ const DEFAULT_BACKEND_TYPE_DEFINITIONS = `
84
+ /** Expected return type for healthcheck scripts */
85
+ interface HealthCheckScriptResult {
86
+ /** Whether the health check passed */
87
+ success: boolean;
88
+ /** Optional status message */
89
+ message?: string;
90
+ /** Optional numeric value for metrics */
91
+ value?: number;
92
+ }
93
+
94
+ /** Console for logging */
95
+ declare const console: {
96
+ /** Log an info message */
97
+ log(...args: unknown[]): void;
98
+ /** Log a warning message */
99
+ warn(...args: unknown[]): void;
100
+ /** Log an error message */
101
+ error(...args: unknown[]): void;
102
+ /** Log an info message */
103
+ info(...args: unknown[]): void;
104
+ };
105
+
106
+ /** HTTP Request configuration */
107
+ interface RequestInit {
108
+ method?: string;
109
+ headers?: Record<string, string> | Headers;
110
+ body?: string | FormData | URLSearchParams;
111
+ mode?: 'cors' | 'no-cors' | 'same-origin';
112
+ credentials?: 'omit' | 'same-origin' | 'include';
113
+ cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache';
114
+ redirect?: 'follow' | 'error' | 'manual';
115
+ signal?: AbortSignal;
116
+ }
117
+
118
+ /** HTTP Response */
119
+ interface Response {
120
+ readonly ok: boolean;
121
+ readonly status: number;
122
+ readonly statusText: string;
123
+ readonly headers: Headers;
124
+ readonly url: string;
125
+ readonly redirected: boolean;
126
+ json(): Promise<unknown>;
127
+ text(): Promise<string>;
128
+ blob(): Promise<Blob>;
129
+ arrayBuffer(): Promise<ArrayBuffer>;
130
+ clone(): Response;
131
+ }
132
+
133
+ /** HTTP Headers */
134
+ interface Headers {
135
+ get(name: string): string | null;
136
+ has(name: string): boolean;
137
+ set(name: string, value: string): void;
138
+ append(name: string, value: string): void;
139
+ delete(name: string): void;
140
+ forEach(callback: (value: string, key: string) => void): void;
141
+ }
142
+
143
+ /** Fetch API for making HTTP requests */
144
+ declare function fetch(input: string | URL, init?: RequestInit): Promise<Response>;
145
+ `;
146
+
147
+ /**
148
+ * A code editor component with syntax highlighting, IntelliSense, and template autocomplete.
149
+ * Uses Monaco Editor (VS Code's editor) for full TypeScript/JavaScript language support.
150
+ */
151
+ export const CodeEditor: React.FC<CodeEditorProps> = ({
152
+ id,
153
+ value,
154
+ onChange,
155
+ language = "json",
156
+ minHeight = "100px",
157
+ readOnly = false,
158
+ placeholder,
159
+ typeDefinitions,
160
+ templateProperties,
161
+ }) => {
162
+ const editorRef = React.useRef<editor.IStandaloneCodeEditor | null>(null);
163
+ const monacoRef = React.useRef<Monaco | null>(null);
164
+ const disposablesRef = React.useRef<{ dispose: () => void }[]>([]);
165
+ // Track when editor is mounted to re-trigger useEffects
166
+ const [isEditorMounted, setIsEditorMounted] = React.useState(false);
167
+
168
+ // Register json-template language BEFORE editor creates the model
169
+ const handleEditorWillMount = (monaco: Monaco) => {
170
+ if (!jsonTemplateLanguageRegistered) {
171
+ monaco.languages.register({ id: "json-template" });
172
+ // Use JSON syntax highlighting
173
+ monaco.languages.setLanguageConfiguration("json-template", {
174
+ comments: {
175
+ lineComment: "//",
176
+ blockComment: ["/*", "*/"],
177
+ },
178
+ brackets: [
179
+ ["{", "}"],
180
+ ["[", "]"],
181
+ ],
182
+ autoClosingPairs: [
183
+ { open: "{", close: "}" },
184
+ { open: "[", close: "]" },
185
+ { open: '"', close: '"' },
186
+ ],
187
+ });
188
+ // Custom tokenizer that highlights {{...}} templates specially
189
+ monaco.languages.setMonarchTokensProvider("json-template", {
190
+ tokenizer: {
191
+ root: [
192
+ [/\{\{[^}]*\}\}/, "variable"], // Template syntax - highlight specially
193
+ [/"([^"\\]|\\.)*$/, "string.invalid"], // Unterminated string
194
+ [/"/, "string", "@string"],
195
+ [/[{}[\]]/, "delimiter.bracket"],
196
+ [/-?\d+\.?\d*([eE][-+]?\d+)?/, "number"],
197
+ [/true|false/, "keyword"],
198
+ [/null/, "keyword"],
199
+ [/[,:]/, "delimiter"],
200
+ [/\s+/, "white"],
201
+ ],
202
+ string: [
203
+ [/[^\\"]+/, "string"],
204
+ [/\\./, "string.escape"],
205
+ [/"/, "string", "@pop"],
206
+ ],
207
+ },
208
+ });
209
+ jsonTemplateLanguageRegistered = true;
210
+ }
211
+ };
212
+
213
+ // Handle editor mount
214
+ const handleEditorDidMount: OnMount = (editor, monaco) => {
215
+ editorRef.current = editor;
216
+ monacoRef.current = monaco;
217
+ setIsEditorMounted(true);
218
+
219
+ // Configure TypeScript/JavaScript to exclude DOM types for backend scripts
220
+ // This prevents Web API autocompletions (AudioContext, Canvas, etc.)
221
+ if (language === "typescript" || language === "javascript") {
222
+ const defaults =
223
+ language === "typescript"
224
+ ? monaco.languages.typescript.typescriptDefaults
225
+ : monaco.languages.typescript.javascriptDefaults;
226
+
227
+ // Configure compiler options to exclude DOM types but keep ES libs
228
+ // This gives us Promise, Array, etc. but not AudioContext, Canvas, etc.
229
+ defaults.setCompilerOptions({
230
+ target: monaco.languages.typescript.ScriptTarget.ESNext,
231
+ module: monaco.languages.typescript.ModuleKind.ESNext,
232
+ lib: ["esnext"], // Include ES libs but NOT DOM
233
+ allowNonTsExtensions: true,
234
+ noEmit: true,
235
+ strict: true,
236
+ });
237
+
238
+ // Suppress certain diagnostics that don't apply to our script context
239
+ // - 1108: "A 'return' statement can only be used within a function body"
240
+ // Our scripts are wrapped in async function at runtime, so return is valid
241
+ defaults.setDiagnosticsOptions({
242
+ diagnosticCodesToIgnore: [1108],
243
+ });
244
+
245
+ // Disable fetching default lib content (prevents DOM types loading)
246
+ defaults.setEagerModelSync(true);
247
+
248
+ // Add custom type definitions if provided, otherwise add minimal defaults
249
+ const definitions = typeDefinitions ?? DEFAULT_BACKEND_TYPE_DEFINITIONS;
250
+ const lib = defaults.addExtraLib(definitions, "file:///context.d.ts");
251
+ disposablesRef.current.push(lib);
252
+ }
253
+
254
+ // Handle validation for template syntax in JSON
255
+ // Since we're using json-template language (no built-in validation),
256
+ // we run our own validation with preprocessed content.
257
+ if (
258
+ templateProperties &&
259
+ templateProperties.length > 0 &&
260
+ language === "json"
261
+ ) {
262
+ const model = editor.getModel();
263
+ if (model) {
264
+ // Custom validation function
265
+ const runCustomValidation = () => {
266
+ const content = model.getValue();
267
+
268
+ // Replace {{...}} templates with valid JSON strings of same length
269
+ // Using same length ensures error positions map correctly
270
+ const preprocessed = content.replaceAll(
271
+ /\{\{[^}]*\}\}/g,
272
+ (match) => `"${"_".repeat(Math.max(0, match.length - 2))}"`,
273
+ );
274
+
275
+ // Try to parse the preprocessed JSON
276
+ const markers: editor.IMarkerData[] = [];
277
+ try {
278
+ JSON.parse(preprocessed);
279
+ } catch (error) {
280
+ if (error instanceof SyntaxError) {
281
+ // Extract position from error message (varies by browser)
282
+ const message = error.message;
283
+ // Try to find line/column info - JSON.parse errors are at position
284
+ const posMatch = message.match(/position (\d+)/i);
285
+
286
+ if (posMatch) {
287
+ const pos = Number.parseInt(posMatch[1], 10);
288
+ const position = model.getPositionAt(pos);
289
+ markers.push({
290
+ severity: monaco.MarkerSeverity.Error,
291
+ message: message,
292
+ startLineNumber: position.lineNumber,
293
+ startColumn: position.column,
294
+ endLineNumber: position.lineNumber,
295
+ endColumn: position.column + 1,
296
+ });
297
+ } else {
298
+ // Fallback: put error at start
299
+ markers.push({
300
+ severity: monaco.MarkerSeverity.Error,
301
+ message: message,
302
+ startLineNumber: 1,
303
+ startColumn: 1,
304
+ endLineNumber: 1,
305
+ endColumn: 2,
306
+ });
307
+ }
308
+ }
309
+ }
310
+
311
+ monaco.editor.setModelMarkers(model, "json-template", markers);
312
+ };
313
+
314
+ // Run validation on content changes
315
+ const contentListener = model.onDidChangeContent(() => {
316
+ runCustomValidation();
317
+ });
318
+
319
+ // Run initial validation
320
+ runCustomValidation();
321
+
322
+ disposablesRef.current.push(contentListener, {
323
+ dispose: () => {
324
+ // Re-enable Monaco's JSON validation when unmounted
325
+ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
326
+ validate: true,
327
+ });
328
+ },
329
+ });
330
+ }
331
+ }
332
+ };
333
+
334
+ // Track the template provider separately so we can update it
335
+ const templateProviderRef = React.useRef<{ dispose: () => void } | undefined>(
336
+ undefined,
337
+ );
338
+
339
+ // Cleanup on unmount
340
+ React.useEffect(() => {
341
+ return () => {
342
+ for (const d of disposablesRef.current) {
343
+ d.dispose();
344
+ }
345
+ disposablesRef.current = [];
346
+ if (templateProviderRef.current) {
347
+ templateProviderRef.current.dispose();
348
+ }
349
+ };
350
+ }, []);
351
+
352
+ // Update template completion provider when templateProperties changes
353
+ React.useEffect(() => {
354
+ if (!monacoRef.current) return;
355
+
356
+ if (templateProviderRef.current) {
357
+ templateProviderRef.current.dispose();
358
+ }
359
+
360
+ if (!templateProperties || templateProperties.length === 0) return;
361
+
362
+ const monaco = monacoRef.current;
363
+
364
+ // Compute effective language - same logic as used for Editor
365
+ const hasTemplates = templateProperties && templateProperties.length > 0;
366
+ const providerLanguage =
367
+ hasTemplates && language === "json"
368
+ ? "json-template"
369
+ : (languageMap[language] ?? language);
370
+
371
+ const provider = monaco.languages.registerCompletionItemProvider(
372
+ providerLanguage,
373
+ {
374
+ triggerCharacters: ["{"],
375
+ provideCompletionItems: (
376
+ model: editor.ITextModel,
377
+ position: Position,
378
+ ) => {
379
+ // Get full content and cursor offset for utility functions
380
+ const content = model.getValue();
381
+ const cursorOffset = model.getOffsetAt(position);
382
+
383
+ // Use tested utility to detect if we're in an open template
384
+ const openTemplate = detectOpenTemplate({ content, cursorOffset });
385
+
386
+ if (!openTemplate.isInTemplate) {
387
+ return { suggestions: [] };
388
+ }
389
+
390
+ const query = openTemplate.query.toLowerCase();
391
+ const startColumn = openTemplate.startColumn;
392
+
393
+ // Check if Monaco auto-closed with }} after cursor using tested utility
394
+ const autoClosedBraces = detectAutoClosedBraces({
395
+ content,
396
+ cursorOffset,
397
+ });
398
+ const endColumn = position.column + autoClosedBraces;
399
+
400
+ const suggestions = templateProperties
401
+ .filter(
402
+ (prop) => query === "" || prop.path.toLowerCase().includes(query),
403
+ )
404
+ .map((prop, index) => ({
405
+ label: `{{${prop.path}}}`,
406
+ kind: monaco.languages.CompletionItemKind.Variable,
407
+ detail: prop.type,
408
+ documentation: prop.description,
409
+ insertText: `{{${prop.path}}}`,
410
+ // Use sortText starting with space to appear before other completions
411
+ sortText: ` ${String(index).padStart(4, "0")}`,
412
+ // Use the query as filterText so it matches what user typed
413
+ filterText: `{{${query}${prop.path}`,
414
+ preselect: index === 0, // Preselect first item
415
+ range: {
416
+ startLineNumber: position.lineNumber,
417
+ startColumn: startColumn,
418
+ endLineNumber: position.lineNumber,
419
+ endColumn: endColumn,
420
+ },
421
+ }));
422
+
423
+ return { suggestions, incomplete: false };
424
+ },
425
+ },
426
+ );
427
+
428
+ templateProviderRef.current = provider;
429
+
430
+ return () => {
431
+ provider.dispose();
432
+ };
433
+ }, [templateProperties, language, isEditorMounted]);
434
+
435
+ // Update type definitions when they change
436
+ React.useEffect(() => {
437
+ if (!monacoRef.current) return;
438
+ if (language !== "typescript" && language !== "javascript") return;
439
+
440
+ const monaco = monacoRef.current;
441
+ const defaults =
442
+ language === "typescript"
443
+ ? monaco.languages.typescript.typescriptDefaults
444
+ : monaco.languages.typescript.javascriptDefaults;
445
+
446
+ // Configure compiler options to exclude DOM types but keep ES libs
447
+ defaults.setCompilerOptions({
448
+ target: monaco.languages.typescript.ScriptTarget.ESNext,
449
+ module: monaco.languages.typescript.ModuleKind.ESNext,
450
+ lib: ["esnext"], // Include ES libs but NOT DOM
451
+ allowNonTsExtensions: true,
452
+ noEmit: true,
453
+ strict: true,
454
+ });
455
+
456
+ // Suppress certain diagnostics that don't apply to our script context
457
+ defaults.setDiagnosticsOptions({
458
+ diagnosticCodesToIgnore: [1108], // "A 'return' statement can only be used within a function body"
459
+ });
460
+
461
+ // Add type definitions (custom or default)
462
+ const definitions = typeDefinitions ?? DEFAULT_BACKEND_TYPE_DEFINITIONS;
463
+ const lib = defaults.addExtraLib(definitions, "file:///context.d.ts");
464
+ disposablesRef.current.push(lib);
465
+
466
+ return () => {
467
+ lib.dispose();
468
+ };
469
+ }, [typeDefinitions, language]);
470
+
471
+ // Calculate height from minHeight
472
+ const heightValue = minHeight.replace("px", "");
473
+ const numericHeight = Number.parseInt(heightValue, 10) || 100;
474
+
475
+ // Compute effective language - use json-template when we have templates for JSON
476
+ const hasTemplates = templateProperties && templateProperties.length > 0;
477
+ const effectiveLanguage =
478
+ hasTemplates && language === "json"
479
+ ? "json-template"
480
+ : (languageMap[language] ?? language);
481
+
482
+ return (
483
+ <div
484
+ id={id}
485
+ className="w-full rounded-md border border-input bg-background font-mono text-sm focus-within:ring-2 focus-within:ring-ring focus-within:border-transparent transition-all box-border overflow-visible"
486
+ style={{ minHeight }}
487
+ >
488
+ <Editor
489
+ height={`${Math.max(numericHeight, 100)}px`}
490
+ language={effectiveLanguage}
491
+ value={value}
492
+ onChange={(newValue) => onChange(newValue ?? "")}
493
+ beforeMount={handleEditorWillMount}
494
+ onMount={handleEditorDidMount}
495
+ options={{
496
+ readOnly,
497
+ minimap: { enabled: false },
498
+ lineNumbers: "on",
499
+ lineNumbersMinChars: 3,
500
+ folding: false,
501
+ wordWrap: "on",
502
+ scrollBeyondLastLine: false,
503
+ fontSize: 14,
504
+ fontFamily: "ui-monospace, monospace",
505
+ tabSize: 2,
506
+ automaticLayout: true,
507
+ scrollbar: {
508
+ vertical: "auto",
509
+ horizontal: "auto",
510
+ verticalScrollbarSize: 10,
511
+ horizontalScrollbarSize: 10,
512
+ },
513
+ overviewRulerLanes: 0,
514
+ hideCursorInOverviewRuler: true,
515
+ overviewRulerBorder: false,
516
+ renderLineHighlight: "none",
517
+ contextmenu: false,
518
+ // Placeholder support via aria-label
519
+ ariaLabel: placeholder ?? "Code editor",
520
+ }}
521
+ theme="vs-dark"
522
+ loading={
523
+ <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
524
+ Loading editor...
525
+ </div>
526
+ }
527
+ />
528
+ </div>
529
+ );
530
+ };
@@ -0,0 +1,169 @@
1
+ import type { JsonSchemaProperty } from "../DynamicForm/types";
2
+
3
+ /**
4
+ * Options for generating TypeScript type definitions from JSON Schema.
5
+ */
6
+ export interface GenerateTypesOptions {
7
+ /** For integration scripts - the event payload schema */
8
+ eventPayloadSchema?: JsonSchemaProperty;
9
+
10
+ /** For healthcheck scripts - the collector config schema */
11
+ collectorConfigSchema?: JsonSchemaProperty;
12
+ }
13
+
14
+ /**
15
+ * Generate TypeScript type definitions from JSON Schema.
16
+ * These are injected into Monaco Editor for real IntelliSense.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const typeDefinitions = generateTypeDefinitions({
21
+ * eventPayloadSchema: eventSchema,
22
+ * });
23
+ * <CodeEditor typeDefinitions={typeDefinitions} language="typescript" />
24
+ * ```
25
+ */
26
+ export function generateTypeDefinitions(options: GenerateTypesOptions): string {
27
+ const lines: string[] = [];
28
+
29
+ // Integration script context (event-based)
30
+ if (options.eventPayloadSchema) {
31
+ const payloadType = jsonSchemaToTypeScript(options.eventPayloadSchema);
32
+
33
+ lines.push(`
34
+ /** Expected return type for integration scripts */
35
+ interface IntegrationScriptResult {
36
+ /** Optional external ID for tracking (e.g., ticket number, message ID) */
37
+ id?: string;
38
+ /** Optional external ID alias */
39
+ externalId?: string;
40
+ }
41
+
42
+ /** Event being delivered to this integration script */
43
+ declare const context: {
44
+ /** Event information */
45
+ readonly event: {
46
+ /** Fully qualified event ID (e.g., "incident.created") */
47
+ readonly eventId: string;
48
+ /** Event payload data */
49
+ readonly payload: ${payloadType};
50
+ /** ISO timestamp when the event occurred */
51
+ readonly timestamp: string;
52
+ /** Unique delivery ID for this delivery attempt */
53
+ readonly deliveryId: string;
54
+ };
55
+ /** Subscription that triggered this delivery */
56
+ readonly subscription: {
57
+ /** Subscription ID */
58
+ readonly id: string;
59
+ /** Subscription name */
60
+ readonly name: string;
61
+ };
62
+ };
63
+ `);
64
+ }
65
+
66
+ // Healthcheck script context (config-based)
67
+ if (options.collectorConfigSchema) {
68
+ const configType = jsonSchemaToTypeScript(options.collectorConfigSchema);
69
+
70
+ lines.push(`
71
+ /** Expected return type for healthcheck scripts */
72
+ interface HealthCheckScriptResult {
73
+ /** Whether the health check passed */
74
+ success: boolean;
75
+ /** Optional status message */
76
+ message?: string;
77
+ /** Optional numeric value for metrics */
78
+ value?: number;
79
+ }
80
+
81
+ /** Context available in healthcheck inline scripts */
82
+ declare const context: {
83
+ /** Collector configuration */
84
+ readonly config: ${configType};
85
+ };
86
+ `);
87
+ }
88
+
89
+ // Always include console and fetch
90
+ lines.push(`
91
+ /** Console for logging (logs appear in delivery events) */
92
+ declare const console: {
93
+ /** Log an info message */
94
+ log(...args: unknown[]): void;
95
+ /** Log a warning message */
96
+ warn(...args: unknown[]): void;
97
+ /** Log an error message */
98
+ error(...args: unknown[]): void;
99
+ /** Log an info message */
100
+ info(...args: unknown[]): void;
101
+ };
102
+
103
+ /** Fetch API for making HTTP requests */
104
+ declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
105
+ `);
106
+
107
+ return lines.join("\n");
108
+ }
109
+
110
+ /**
111
+ * Convert a JSON Schema to a TypeScript type string.
112
+ * Handles objects, arrays, primitives, and enums.
113
+ */
114
+ function jsonSchemaToTypeScript(
115
+ schema: JsonSchemaProperty,
116
+ indent: number = 0,
117
+ ): string {
118
+ const pad = " ".repeat(indent);
119
+
120
+ // Object type with properties
121
+ if (schema.type === "object" && schema.properties) {
122
+ const props = Object.entries(schema.properties)
123
+ .map(([key, propSchema]) => {
124
+ const optional = schema.required?.includes(key) ? "" : "?";
125
+ const type = jsonSchemaToTypeScript(propSchema, indent + 1);
126
+ const description = propSchema.description
127
+ ? `/** ${propSchema.description} */\n${pad} `
128
+ : "";
129
+ return `${description}readonly ${key}${optional}: ${type};`;
130
+ })
131
+ .map((line) =>
132
+ line
133
+ .split("\n")
134
+ .map((l) => `${pad} ${l}`)
135
+ .join("\n"),
136
+ )
137
+ .join("\n");
138
+ return `{\n${props}\n${pad}}`;
139
+ }
140
+
141
+ // Array type
142
+ if (schema.type === "array" && schema.items) {
143
+ const itemType = jsonSchemaToTypeScript(schema.items, indent);
144
+ return `readonly ${itemType}[]`;
145
+ }
146
+
147
+ // Enum type
148
+ if (schema.enum) {
149
+ return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
150
+ }
151
+
152
+ // Primitive types
153
+ if (schema.type === "string") return "string";
154
+ if (schema.type === "number" || schema.type === "integer") return "number";
155
+ if (schema.type === "boolean") return "boolean";
156
+ if (schema.type === "null") return "null";
157
+
158
+ // Record/dictionary type
159
+ if (schema.type === "object" && schema.additionalProperties) {
160
+ const valueType =
161
+ typeof schema.additionalProperties === "object"
162
+ ? jsonSchemaToTypeScript(schema.additionalProperties, indent)
163
+ : "unknown";
164
+ return `Record<string, ${valueType}>`;
165
+ }
166
+
167
+ // Fallback
168
+ return "unknown";
169
+ }
@@ -5,6 +5,7 @@ export {
5
5
  type TemplateProperty,
6
6
  } from "./CodeEditor";
7
7
 
8
- // Re-export language support for testing and extensibility
9
- export { isValidJsonTemplatePosition } from "./languageSupport";
10
- export type { LanguageSupport, DecorationRange } from "./languageSupport";
8
+ export {
9
+ generateTypeDefinitions,
10
+ type GenerateTypesOptions,
11
+ } from "./generateTypeDefinitions";