@checkstack/ui 1.9.0 → 1.11.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 (52) hide show
  1. package/CHANGELOG.md +417 -0
  2. package/package.json +15 -7
  3. package/scripts/generate-stdlib-types.ts +2 -2
  4. package/src/components/ActionCard.tsx +221 -0
  5. package/src/components/CodeEditor/CodeEditor.tsx +51 -9
  6. package/src/components/CodeEditor/TypefoxEditor.tsx +868 -0
  7. package/src/components/CodeEditor/bracketKeyGroups.test.ts +120 -0
  8. package/src/components/CodeEditor/bracketKeyGroups.ts +205 -0
  9. package/src/components/CodeEditor/generateTypeDefinitions.ts +4 -4
  10. package/src/components/CodeEditor/index.ts +2 -0
  11. package/src/components/CodeEditor/scriptContext.test.ts +41 -0
  12. package/src/components/CodeEditor/scriptContext.ts +76 -1
  13. package/src/components/CodeEditor/templateValidation.ts +51 -0
  14. package/src/components/CodeEditor/types.ts +109 -0
  15. package/src/components/CodeEditor/validateJsonTemplate.test.ts +61 -0
  16. package/src/components/CodeEditor/validateJsonTemplate.ts +26 -0
  17. package/src/components/CodeEditor/validateXmlTemplate.test.ts +34 -0
  18. package/src/components/CodeEditor/validateXmlTemplate.ts +35 -0
  19. package/src/components/CodeEditor/validateYamlTemplate.test.ts +39 -0
  20. package/src/components/CodeEditor/validateYamlTemplate.ts +28 -0
  21. package/src/components/DynamicForm/DynamicForm.tsx +2 -0
  22. package/src/components/DynamicForm/FormField.tsx +29 -9
  23. package/src/components/DynamicForm/KeyValueEditor.tsx +2 -169
  24. package/src/components/DynamicForm/MultiTypeEditorField.tsx +16 -7
  25. package/src/components/DynamicForm/types.ts +11 -0
  26. package/src/components/ListEmptyState.tsx +51 -0
  27. package/src/components/QueryErrorState.tsx +64 -0
  28. package/src/components/ResponsiveTable.tsx +92 -0
  29. package/src/components/Skeleton.tsx +39 -0
  30. package/src/components/TemplateInput.tsx +104 -0
  31. package/src/components/TemplateInputToggle.tsx +111 -0
  32. package/src/components/TemplateValueInput.test.ts +98 -0
  33. package/src/components/TemplateValueInput.tsx +470 -0
  34. package/src/components/VariablePicker.tsx +271 -0
  35. package/src/hooks/useInitOnceForKey.test.ts +27 -0
  36. package/src/hooks/useInitOnceForKey.ts +21 -18
  37. package/src/index.ts +10 -0
  38. package/src/utils/toastTemplates.test.ts +82 -0
  39. package/src/utils/toastTemplates.ts +47 -0
  40. package/stories/ActionCard.stories.tsx +62 -0
  41. package/stories/Alert.stories.tsx +5 -5
  42. package/stories/ListEmptyState.stories.tsx +48 -0
  43. package/stories/QueryErrorState.stories.tsx +40 -0
  44. package/stories/ResponsiveTable.stories.tsx +93 -0
  45. package/stories/Skeleton.stories.tsx +53 -0
  46. package/stories/TemplateInputToggle.stories.tsx +77 -0
  47. package/stories/TemplateValueInput.stories.tsx +65 -0
  48. package/stories/VariablePicker.stories.tsx +109 -0
  49. package/stories/toastTemplates.stories.tsx +60 -0
  50. package/src/components/CodeEditor/MonacoEditor.tsx +0 -616
  51. package/src/components/CodeEditor/monacoStdlib.ts +0 -62
  52. package/src/components/CodeEditor/monacoWorkers.ts +0 -118
@@ -0,0 +1,120 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { extractBracketKeyGroups } from "./bracketKeyGroups";
3
+
4
+ describe("extractBracketKeyGroups", () => {
5
+ it("returns [] when there is no `declare const context`", () => {
6
+ expect(
7
+ extractBracketKeyGroups({ typeDefinitions: "interface Foo { x: string }" }),
8
+ ).toEqual([]);
9
+ });
10
+
11
+ it("returns [] when no property keys are non-identifiers", () => {
12
+ const typeDefinitions = `declare const context: {
13
+ event: { payload: { repository: string; count: number } };
14
+ };`;
15
+ expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([]);
16
+ });
17
+
18
+ it("extracts a non-identifier key and its dotted parent path", () => {
19
+ const typeDefinitions = `declare const context: {
20
+ artifacts: {
21
+ "integration-jira.issue": { key: string; summary: string };
22
+ };
23
+ };`;
24
+ expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
25
+ { objectExpression: "context.artifacts", keys: ["integration-jira.issue"] },
26
+ ]);
27
+ });
28
+
29
+ it("groups multiple non-identifier keys under the same parent", () => {
30
+ const typeDefinitions = `declare const context: {
31
+ artifacts: {
32
+ "integration-jira.issue": { key: string };
33
+ "integration-github.pr": { number: number };
34
+ valid: { ok: boolean };
35
+ };
36
+ };`;
37
+ expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
38
+ {
39
+ objectExpression: "context.artifacts",
40
+ keys: ["integration-jira.issue", "integration-github.pr"],
41
+ },
42
+ ]);
43
+ });
44
+
45
+ it("does NOT treat string-literal union *values* as keys", () => {
46
+ const typeDefinitions = `declare const context: {
47
+ artifacts: {
48
+ "a-b": { status: "open" | "closed"; url: "https://x" };
49
+ };
50
+ };`;
51
+ expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
52
+ { objectExpression: "context.artifacts", keys: ["a-b"] },
53
+ ]);
54
+ });
55
+
56
+ it("ignores JSDoc and line comments (including braces inside them)", () => {
57
+ const typeDefinitions = `declare const context: {
58
+ /** an object like { not: a real brace } */
59
+ artifacts: {
60
+ // "comment-key": should be ignored
61
+ "real-key": { x: string };
62
+ };
63
+ };`;
64
+ expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
65
+ { objectExpression: "context.artifacts", keys: ["real-key"] },
66
+ ]);
67
+ });
68
+
69
+ it("handles `readonly` and optional `?` modifiers and deeper paths", () => {
70
+ const typeDefinitions = `declare const context: {
71
+ readonly event: {
72
+ readonly meta?: {
73
+ "x-trace.id": { v: string };
74
+ };
75
+ };
76
+ };`;
77
+ expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
78
+ { objectExpression: "context.event.meta", keys: ["x-trace.id"] },
79
+ ]);
80
+ });
81
+
82
+ it("skips non-identifier keys nested under a non-identifier parent", () => {
83
+ // `context["a-b"]` is not a dotted objectExpression, so the inner key has
84
+ // no addressable parent and must be skipped.
85
+ const typeDefinitions = `declare const context: {
86
+ "a-b": {
87
+ "c-d": { x: string };
88
+ };
89
+ };`;
90
+ expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
91
+ { objectExpression: "context", keys: ["a-b"] },
92
+ ]);
93
+ });
94
+
95
+ it("extracts OPTIONAL quoted keys (`\"key\"?:`) - the real generated shape", () => {
96
+ // Mirrors generateAutomationContextTypes output for upstream artifacts:
97
+ // `readonly artifacts: { readonly "integration-jira.issue"?: {...}; }`.
98
+ const typeDefinitions = `declare const context: {
99
+ readonly artifacts: {
100
+ readonly "integration-jira.issue"?: { key: string; url?: string };
101
+ readonly "integration-github.pr"?: { number: number };
102
+ };
103
+ };`;
104
+ expect(extractBracketKeyGroups({ typeDefinitions })).toEqual([
105
+ {
106
+ objectExpression: "context.artifacts",
107
+ keys: ["integration-jira.issue", "integration-github.pr"],
108
+ },
109
+ ]);
110
+ });
111
+
112
+ it("respects a custom root name", () => {
113
+ const typeDefinitions = `declare const scope: {
114
+ vars: { "weird key": { x: string } };
115
+ };`;
116
+ expect(
117
+ extractBracketKeyGroups({ typeDefinitions, rootName: "scope" }),
118
+ ).toEqual([{ objectExpression: "scope.vars", keys: ["weird key"] }]);
119
+ });
120
+ });
@@ -0,0 +1,205 @@
1
+ // Stage 2b (monaco-editor -> @typefox/monaco-editor-react migration).
2
+ //
3
+ // The standalone TS worker calls `getCompletionsAtPosition` with no options
4
+ // (tsWorker.js: `getCompletionsAtPosition(fileName, position, void 0)`), so it
5
+ // never surfaces object members whose keys are not valid JS identifiers (e.g.
6
+ // artifact ids like `integration-jira.issue`). The built-in SuggestAdapter also
7
+ // hardcodes `insertText: entry.name` and can't be overridden, so a custom TS
8
+ // worker is not viable. Instead we register our own completion provider that
9
+ // offers those keys as `["key"]` bracket completions.
10
+ //
11
+ // To stay TYPE-DRIVEN (and avoid threading a separate `dottedKeyCompletions`
12
+ // prop), this module derives the groups straight from the injected
13
+ // `context.d.ts`: it finds object members whose property name is a quoted,
14
+ // non-identifier string and reports the dotted access path to their parent
15
+ // object plus the list of such keys.
16
+
17
+ /** A parent object plus the non-identifier keys reachable under it. */
18
+ export interface BracketKeyGroup {
19
+ /** Member-access expression preceding the dot, e.g. `context.artifacts`. */
20
+ objectExpression: string;
21
+ /** Keys to offer; each is inserted as `["<key>"]`. */
22
+ keys: string[];
23
+ }
24
+
25
+ const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/;
26
+
27
+ const isIdentifier = (name: string): boolean => IDENTIFIER_RE.test(name);
28
+
29
+ const isWhitespace = (ch: string): boolean =>
30
+ ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
31
+
32
+ /**
33
+ * Extract bracket-notation completion groups from generated TS type
34
+ * definitions. Walks the `declare const <rootName>: { ... }` object literal,
35
+ * tracking the dotted path as it descends, and records every property whose
36
+ * name is a quoted non-identifier string.
37
+ *
38
+ * The walk is a small purpose-built scanner (not a full TS parser): it is only
39
+ * ever fed our own generated `context.d.ts`, whose shape is a plain nested
40
+ * object type. It correctly ignores string-literal type *values* (e.g.
41
+ * `status: "open" | "closed"`), JSDoc/line comments, and the `readonly` /
42
+ * optional (`?`) modifiers. Keys nested under a non-identifier parent are
43
+ * skipped because no dotted `objectExpression` can address them.
44
+ */
45
+ export const extractBracketKeyGroups = ({
46
+ typeDefinitions,
47
+ rootName = "context",
48
+ }: {
49
+ typeDefinitions: string;
50
+ rootName?: string;
51
+ }): BracketKeyGroup[] => {
52
+ const src = typeDefinitions;
53
+ // Locate `declare const <rootName>` then the opening `{` of its type.
54
+ const declMatch = new RegExp(
55
+ String.raw`declare\s+const\s+${rootName}\b`,
56
+ ).exec(src);
57
+ if (!declMatch) {
58
+ return [];
59
+ }
60
+ let i = src.indexOf("{", declMatch.index + declMatch[0].length);
61
+ if (i === -1) {
62
+ return [];
63
+ }
64
+
65
+ // Insertion-ordered accumulation so output is stable for tests/snapshots.
66
+ const groups = new Map<string, string[]>();
67
+ const addKey = (objectExpression: string, key: string): void => {
68
+ const existing = groups.get(objectExpression);
69
+ if (existing) {
70
+ if (!existing.includes(key)) {
71
+ existing.push(key);
72
+ }
73
+ } else {
74
+ groups.set(objectExpression, [key]);
75
+ }
76
+ };
77
+
78
+ // path[k] / dottable[k] describe the object open at brace depth k+1.
79
+ const path: string[] = [rootName];
80
+ const dottable: boolean[] = [true];
81
+ let depth = 1; // we consume the root `{` below
82
+ i += 1; // step past the root object's opening brace
83
+
84
+ // The property name most recently parsed, awaiting its value. Used to label
85
+ // the object we descend into on the next `{`.
86
+ let pendingName: { value: string; identifier: boolean } | null = null;
87
+
88
+ const peekNonWs = (from: number): number => {
89
+ let j = from;
90
+ while (j < src.length && isWhitespace(src[j] ?? "")) {
91
+ j += 1;
92
+ }
93
+ return j;
94
+ };
95
+
96
+ while (i < src.length && depth > 0) {
97
+ const ch = src[i] ?? "";
98
+
99
+ // Comments.
100
+ if (ch === "/" && src[i + 1] === "*") {
101
+ const end = src.indexOf("*/", i + 2);
102
+ i = end === -1 ? src.length : end + 2;
103
+ continue;
104
+ }
105
+ if (ch === "/" && src[i + 1] === "/") {
106
+ const nl = src.indexOf("\n", i + 2);
107
+ i = nl === -1 ? src.length : nl + 1;
108
+ continue;
109
+ }
110
+
111
+ if (isWhitespace(ch)) {
112
+ i += 1;
113
+ continue;
114
+ }
115
+
116
+ // String token: either a property name (if followed by `:`) or a
117
+ // type-position literal (e.g. a union member) which we ignore.
118
+ if (ch === '"' || ch === "'") {
119
+ const quote = ch;
120
+ let j = i + 1;
121
+ let str = "";
122
+ while (j < src.length && src[j] !== quote) {
123
+ if (src[j] === "\\") {
124
+ str += src[j + 1] ?? "";
125
+ j += 2;
126
+ continue;
127
+ }
128
+ str += src[j];
129
+ j += 1;
130
+ }
131
+ const afterStr = j + 1; // past closing quote
132
+ // A quoted property name may be optional: `"key"?: ...` (this is exactly
133
+ // how generated artifact keys look, e.g. `"integration-jira.issue"?:`).
134
+ let colon = peekNonWs(afterStr);
135
+ if (src[colon] === "?") {
136
+ colon = peekNonWs(colon + 1);
137
+ }
138
+ if (src[colon] === ":") {
139
+ // Quoted property name. Record under the current object when every
140
+ // ancestor segment is dot-addressable.
141
+ if (dottable.every(Boolean) && !isIdentifier(str)) {
142
+ addKey(path.join("."), str);
143
+ }
144
+ pendingName = { value: str, identifier: isIdentifier(str) };
145
+ i = colon + 1;
146
+ } else {
147
+ // Type-position string literal - not a key.
148
+ i = afterStr;
149
+ }
150
+ continue;
151
+ }
152
+
153
+ // Identifier: a property name (if followed by `:`, allowing an optional
154
+ // `?`) or a type keyword / modifier we skip.
155
+ if (/[A-Za-z_$]/.test(ch)) {
156
+ let j = i;
157
+ while (j < src.length && /[\w$]/.test(src[j] ?? "")) {
158
+ j += 1;
159
+ }
160
+ const word = src.slice(i, j);
161
+ let after = peekNonWs(j);
162
+ if (src[after] === "?") {
163
+ after = peekNonWs(after + 1);
164
+ }
165
+ if (src[after] === ":") {
166
+ pendingName = { value: word, identifier: true };
167
+ i = after + 1;
168
+ } else {
169
+ // `readonly`, `string`, `interface`, etc. - not a key for us.
170
+ i = j;
171
+ }
172
+ continue;
173
+ }
174
+
175
+ if (ch === "{") {
176
+ depth += 1;
177
+ path.push(pendingName?.value ?? "");
178
+ dottable.push(pendingName?.identifier ?? false);
179
+ pendingName = null;
180
+ i += 1;
181
+ continue;
182
+ }
183
+
184
+ if (ch === "}") {
185
+ depth -= 1;
186
+ path.pop();
187
+ dottable.pop();
188
+ pendingName = null;
189
+ i += 1;
190
+ continue;
191
+ }
192
+
193
+ // End of a property's value (e.g. `: string;`) - the pending name had a
194
+ // non-object type, so it never labels a descent.
195
+ if (ch === ";" || ch === ",") {
196
+ pendingName = null;
197
+ }
198
+ i += 1;
199
+ }
200
+
201
+ return [...groups].map(([objectExpression, keys]) => ({
202
+ objectExpression,
203
+ keys,
204
+ }));
205
+ };
@@ -89,10 +89,10 @@ declare const context: {
89
89
  `);
90
90
  }
91
91
 
92
- // We deliberately don't redeclare `console`, `fetch`, the Node stdlib,
93
- // or the Bun globals here MonacoEditor mounts the bundled upstream
94
- // `@types/node` + `bun-types` declarations into Monaco's virtual
95
- // filesystem via `ensureMonacoStdlib`, so all of that is already in scope.
92
+ // We deliberately don't redeclare `console`, `fetch`, the Node stdlib, or
93
+ // the Bun globals here: the editor mounts the bundled upstream `@types/node`
94
+ // + `bun-types` declarations into the TypeScript service, so all of that is
95
+ // already in scope.
96
96
  return lines.join("\n");
97
97
  }
98
98
 
@@ -4,6 +4,7 @@ export {
4
4
  type CodeEditorLanguage,
5
5
  type TemplateProperty,
6
6
  type ShellEnvVar,
7
+ type EditorMarker,
7
8
  } from "./CodeEditor";
8
9
 
9
10
  export {
@@ -12,6 +13,7 @@ export {
12
13
  } from "./generateTypeDefinitions";
13
14
 
14
15
  export {
16
+ customShellEnvVars,
15
17
  healthcheckScriptContext,
16
18
  integrationScriptContext,
17
19
  type ScriptEditorContext,
@@ -52,6 +52,15 @@ describe("healthcheckScriptContext", () => {
52
52
  expect(ctx.typeDefinitions).toContain("HealthCheckScriptContext");
53
53
  });
54
54
 
55
+ it("exposes `context.check` and `context.system` run-context metadata", () => {
56
+ // The runner injects check/system metadata alongside config, so the
57
+ // editor must type them or `context.system.name` would error.
58
+ const ctx = healthcheckScriptContext({});
59
+ expect(ctx.typeDefinitions).toContain("readonly check: {");
60
+ expect(ctx.typeDefinitions).toContain("readonly system: {");
61
+ expect(ctx.typeDefinitions).toContain("readonly intervalSeconds: number");
62
+ });
63
+
55
64
  it("types the `defineHealthCheck` callback parameter from the schema (not `unknown`)", () => {
56
65
  // Regression guard: the previous version had `(ctx: unknown) => …`,
57
66
  // so `ctx.config.host` produced "'ctx' is of type 'unknown'". The
@@ -95,10 +104,42 @@ describe("healthcheckScriptContext", () => {
95
104
  expect(names).toContain("PATH");
96
105
  expect(names).toContain("HOME");
97
106
  expect(names).toContain("TZ");
107
+ // Run-context vars the shell collector injects are suggested too.
108
+ expect(names).toContain("CHECKSTACK_CHECK_NAME");
109
+ expect(names).toContain("CHECKSTACK_SYSTEM_NAME");
110
+ expect(names).toContain("CHECKSTACK_CHECK_INTERVAL_SECONDS");
98
111
  // Integration-only vars must NOT leak into the healthcheck context.
99
112
  expect(names).not.toContain("EVENT_ID");
100
113
  expect(names).not.toContain("PAYLOAD_TITLE");
101
114
  });
115
+
116
+ it("surfaces the user's custom Env (JSON) keys as shell completions", () => {
117
+ const ctx = healthcheckScriptContext({
118
+ customEnv: { API_TOKEN: "secret", "not-an-ident": "x" },
119
+ });
120
+ const names = ctx.shellEnvVars.map((v) => v.name);
121
+ // Valid shell identifier from the user's env is suggested...
122
+ expect(names).toContain("API_TOKEN");
123
+ // ...alongside the whitelist + reserved run-context vars.
124
+ expect(names).toContain("PATH");
125
+ expect(names).toContain("CHECKSTACK_SYSTEM_NAME");
126
+ // Keys that aren't valid `$NAME` identifiers are dropped.
127
+ expect(names).not.toContain("not-an-ident");
128
+ // The user's own var must be ordered ahead of the whitelist + run-context
129
+ // vars so it's not buried at the bottom of the suggest list.
130
+ expect(names.indexOf("API_TOKEN")).toBeLessThan(names.indexOf("PATH"));
131
+ expect(names.indexOf("API_TOKEN")).toBeLessThan(
132
+ names.indexOf("CHECKSTACK_SYSTEM_NAME"),
133
+ );
134
+ });
135
+
136
+ it("ignores a non-object customEnv without throwing", () => {
137
+ const names = healthcheckScriptContext({
138
+ customEnv: "not an object",
139
+ }).shellEnvVars.map((v) => v.name);
140
+ expect(names).toContain("PATH");
141
+ expect(names).toContain("CHECKSTACK_CHECK_ID");
142
+ });
102
143
  });
103
144
 
104
145
  describe("integrationScriptContext", () => {
@@ -89,6 +89,22 @@ interface HealthCheckScriptResult {
89
89
  interface HealthCheckScriptContext {
90
90
  /** Strongly-typed collector configuration. */
91
91
  readonly config: ${configType};
92
+ /** Metadata about the health check this run is for. */
93
+ readonly check: {
94
+ /** The health check configuration id. */
95
+ readonly id: string;
96
+ /** The health check's display name (falls back to the id). */
97
+ readonly name: string;
98
+ /** The configured run interval, in seconds. */
99
+ readonly intervalSeconds: number;
100
+ };
101
+ /** Metadata about the system this check runs for. */
102
+ readonly system: {
103
+ /** The system id. */
104
+ readonly id: string;
105
+ /** The system's display name (falls back to the id). */
106
+ readonly name: string;
107
+ };
92
108
  }
93
109
 
94
110
  /**
@@ -328,6 +344,51 @@ const SAFE_SHELL_VARS: ShellEnvVar[] = [
328
344
  { name: "SHELL", description: "User's login shell." },
329
345
  ];
330
346
 
347
+ /**
348
+ * Run-context vars the satellite injects into every shell health-check
349
+ * run, describing the check + system it's for. Mirrors the reserved
350
+ * `CHECKSTACK_*` keys set by `healthcheck-script-backend`'s shell
351
+ * collector. User-supplied `env` values override these.
352
+ */
353
+ const HEALTHCHECK_RUN_CONTEXT_VARS: ShellEnvVar[] = [
354
+ { name: "CHECKSTACK_CHECK_ID", description: "This health check's configuration id." },
355
+ {
356
+ name: "CHECKSTACK_CHECK_NAME",
357
+ description: "This health check's display name (falls back to the id).",
358
+ },
359
+ {
360
+ name: "CHECKSTACK_CHECK_INTERVAL_SECONDS",
361
+ description: "The configured run interval, in seconds.",
362
+ },
363
+ { name: "CHECKSTACK_SYSTEM_ID", description: "The id of the system being checked." },
364
+ {
365
+ name: "CHECKSTACK_SYSTEM_NAME",
366
+ description: "The system's display name (falls back to the id).",
367
+ },
368
+ ];
369
+
370
+ /** A valid POSIX shell identifier — only these can be referenced as `$NAME`. */
371
+ const SHELL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
372
+
373
+ /**
374
+ * Turn a user's custom `env` object (e.g. a script action's or health
375
+ * check's "Env (JSON)" field) into `$`-completion hints. Defensive about
376
+ * the loosely-typed form value: non-objects yield nothing, and only keys
377
+ * that are valid shell identifiers are surfaced (a `$my-var` completion
378
+ * wouldn't be usable). Exported so editors that build their shell env
379
+ * suggestions outside the `*ScriptContext` helpers (e.g. the automation
380
+ * action editor) can merge custom env keys the same way.
381
+ */
382
+ export function customShellEnvVars(env: unknown): ShellEnvVar[] {
383
+ if (typeof env !== "object" || env === null || Array.isArray(env)) return [];
384
+ return Object.keys(env)
385
+ .filter((name) => SHELL_IDENTIFIER.test(name))
386
+ .map((name) => ({
387
+ name,
388
+ description: "Custom variable from this check's Env (JSON) field.",
389
+ }));
390
+ }
391
+
331
392
  /**
332
393
  * Vars injected by the integration platform on every delivery. Per-
333
394
  * payload-field `PAYLOAD_*` vars are appended at call time based on the
@@ -406,6 +467,13 @@ function flattenSchemaToEnvVars(
406
467
  */
407
468
  export function healthcheckScriptContext(input: {
408
469
  collectorConfigSchema?: JsonSchemaProperty;
470
+ /**
471
+ * The collector's current `env` value (the "Env (JSON)" field). Its
472
+ * keys are surfaced as `$`-completions so a user's own declared vars
473
+ * autocomplete alongside the whitelist + reserved run-context vars.
474
+ * Typed `unknown` because it comes from the loosely-typed form value.
475
+ */
476
+ customEnv?: unknown;
409
477
  }): ScriptEditorContext {
410
478
  const configType = input.collectorConfigSchema
411
479
  ? jsonSchemaToTypeScript(input.collectorConfigSchema)
@@ -418,7 +486,14 @@ export function healthcheckScriptContext(input: {
418
486
  javascript: HEALTHCHECK_INLINE_TS_STARTER,
419
487
  shell: HEALTHCHECK_SHELL_STARTER,
420
488
  },
421
- shellEnvVars: SAFE_SHELL_VARS,
489
+ // Most-relevant-first: the user's own declared env, then this check's
490
+ // run-context metadata, then the generic OS whitelist (the suggest list
491
+ // is ordered by insertion index, so order here is what the user sees).
492
+ shellEnvVars: [
493
+ ...customShellEnvVars(input.customEnv),
494
+ ...HEALTHCHECK_RUN_CONTEXT_VARS,
495
+ ...SAFE_SHELL_VARS,
496
+ ],
422
497
  };
423
498
  }
424
499
 
@@ -0,0 +1,51 @@
1
+ // Shared bits for template-aware language validation (monaco -> @typefox
2
+ // migration). Markup editors (json / yaml / xml) hold a TEMPLATE that renders
3
+ // to the target language, not the language itself: a value can be templated in
4
+ // any position, including unquoted ones (e.g. a number `timeout: {{x}}`). So we
5
+ // validate "is this valid once the templates are filled in?" - substitute each
6
+ // `{{ ... }}` with a same-length neutral value, then parse with a real parser.
7
+ //
8
+ // Per-language validators live in validate{Json,Yaml,Xml}Template.ts and all
9
+ // return TemplateDiagnostic[]; the editor maps offsets to monaco markers.
10
+
11
+ export interface TemplateDiagnostic {
12
+ /** 0-based character offset into the ORIGINAL text. */
13
+ offset: number;
14
+ /** Length of the offending span (>= 1). */
15
+ length: number;
16
+ /** Human-readable message. */
17
+ message: string;
18
+ }
19
+
20
+ // `[^{}]*` (not `[^}]*`) so an unclosed `{{` can't swallow up to a later `}}`.
21
+ const TEMPLATE_PATTERN = /\{\{[^{}]*\}\}/g;
22
+
23
+ /**
24
+ * Replace each `{{ ... }}` with `0` padded to the same length with spaces - a
25
+ * neutral value token, valid in value positions (a number followed by
26
+ * whitespace) and harmless inside strings/text. Same length keeps byte offsets
27
+ * identical, so parser offsets map 1:1 onto the original text.
28
+ */
29
+ export const substituteTemplates = (text: string): string =>
30
+ text.replaceAll(
31
+ TEMPLATE_PATTERN,
32
+ (match) => `0${" ".repeat(match.length - 1)}`,
33
+ );
34
+
35
+ /** Convert a 1-based line/column (some parsers report this) to a 0-based offset. */
36
+ export const lineColumnToOffset = ({
37
+ text,
38
+ line,
39
+ column,
40
+ }: {
41
+ text: string;
42
+ line: number;
43
+ column: number;
44
+ }): number => {
45
+ const lines = text.split("\n");
46
+ let offset = 0;
47
+ for (let index = 0; index < line - 1 && index < lines.length; index += 1) {
48
+ offset += (lines[index]?.length ?? 0) + 1; // +1 for the newline
49
+ }
50
+ return offset + Math.max(column - 1, 0);
51
+ };
@@ -0,0 +1,109 @@
1
+ // Shared, pure type definitions for the CodeEditor public API.
2
+ //
3
+ // NOTE: this file MUST stay free of monaco / @codingame / vite imports so that
4
+ // SSR/test/node paths can reference these types (and the package barrel can
5
+ // re-export them) without pulling in the browser-only editor stack.
6
+
7
+ export type CodeEditorLanguage =
8
+ | "json"
9
+ | "yaml"
10
+ | "xml"
11
+ | "markdown"
12
+ | "javascript"
13
+ | "typescript"
14
+ | "shell";
15
+
16
+ /**
17
+ * A single payload property available for templating. Used by both the
18
+ * Monaco-backed `CodeEditor` and the lightweight single-line
19
+ * `TemplateValueInput` for the simple "insert a `{{ path }}` reference" flow.
20
+ */
21
+ export interface TemplateProperty {
22
+ /** Full canonical path to the property, e.g., "trigger.payload.title". */
23
+ path: string;
24
+ /**
25
+ * Runtime-parseable `{{ }}` insertion text, e.g.
26
+ * `artifacts["integration-jira.issue"].issueKey`. When present this is
27
+ * what gets inserted; consumers fall back to `path` when it's absent.
28
+ */
29
+ templateRef?: string;
30
+ /** Type label rendered on the right, e.g. "string", "number". */
31
+ type: string;
32
+ /** Optional description of the property. */
33
+ description?: string;
34
+ /**
35
+ * Known discrete values for this field, when the schema declares an `enum`.
36
+ * Consumed by the template-completion provider to suggest concrete values
37
+ * after a comparator (e.g. `"low"` / `"high"` for a severity field).
38
+ */
39
+ enumValues?: Array<string | number | boolean>;
40
+ }
41
+
42
+ /**
43
+ * A single environment variable available to shell scripts. Surfaced as a
44
+ * completion item after the user types `$` or `${` in shell mode.
45
+ */
46
+ export interface ShellEnvVar {
47
+ /** Variable name without the leading `$`, e.g. `EVENT_ID`. */
48
+ name: string;
49
+ /** Optional description shown in the completion popup. */
50
+ description?: string;
51
+ /** Optional example value shown in the completion item detail. */
52
+ example?: string;
53
+ }
54
+
55
+ /**
56
+ * An externally-supplied diagnostic to render as an inline squiggle. Positions
57
+ * are 1-based line/column (the editor's convention). Callers compute these from
58
+ * their own validation (e.g. mapping a definition issue path back to a YAML
59
+ * node range) and pass them via `markers`.
60
+ */
61
+ export interface EditorMarker {
62
+ startLineNumber: number;
63
+ startColumn: number;
64
+ endLineNumber: number;
65
+ endColumn: number;
66
+ message: string;
67
+ severity?: "error" | "warning" | "info";
68
+ }
69
+
70
+ export interface CodeEditorProps {
71
+ /** Unique identifier for the editor. */
72
+ id?: string;
73
+ /** Current value of the editor. */
74
+ value: string;
75
+ /** Callback when the value changes. */
76
+ onChange: (value: string) => void;
77
+ /** Language for syntax highlighting. */
78
+ language?: CodeEditorLanguage;
79
+ /** Minimum height of the editor, as a CSS length (e.g. "240px"). */
80
+ minHeight?: string;
81
+ /** Whether the editor is read-only. */
82
+ readOnly?: boolean;
83
+ /** Placeholder text when empty. */
84
+ placeholder?: string;
85
+ /**
86
+ * TypeScript type definitions to inject for IntelliSense (the `context.d.ts`).
87
+ * Generated from JSON schemas for context-aware autocomplete. For
88
+ * `typescript` / `javascript` editors, non-identifier object keys in these
89
+ * definitions (e.g. artifact ids) are offered as `["key"]` bracket
90
+ * completions automatically.
91
+ */
92
+ typeDefinitions?: string;
93
+ /**
94
+ * Optional template properties for autocomplete. When provided, typing "{{"
95
+ * triggers autocomplete with the available template variables.
96
+ */
97
+ templateProperties?: TemplateProperty[];
98
+ /**
99
+ * Optional environment-variable hints for shell mode. When provided and
100
+ * `language === "shell"`, they autocomplete after `$` and `${`.
101
+ */
102
+ shellEnvVars?: ShellEnvVar[];
103
+ /**
104
+ * Externally-computed diagnostics rendered as inline squiggles (under a
105
+ * dedicated marker owner, so they coexist with the editor's own language
106
+ * diagnostics).
107
+ */
108
+ markers?: EditorMarker[];
109
+ }