@docsxai/engine 0.2.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 (129) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +130 -0
  3. package/dist/auth/api-login.d.ts +69 -0
  4. package/dist/auth/api-login.js +95 -0
  5. package/dist/auth/browser-session.d.ts +28 -0
  6. package/dist/auth/browser-session.js +43 -0
  7. package/dist/auth/cookie-jar.d.ts +58 -0
  8. package/dist/auth/cookie-jar.js +212 -0
  9. package/dist/auth/email-otp.d.ts +210 -0
  10. package/dist/auth/email-otp.js +166 -0
  11. package/dist/auth/http-basic.d.ts +5 -0
  12. package/dist/auth/http-basic.js +17 -0
  13. package/dist/auth/index.d.ts +47 -0
  14. package/dist/auth/index.js +137 -0
  15. package/dist/auth/jwt-injection.d.ts +153 -0
  16. package/dist/auth/jwt-injection.js +136 -0
  17. package/dist/auth/manual-capture.d.ts +35 -0
  18. package/dist/auth/manual-capture.js +30 -0
  19. package/dist/auth/mtls.d.ts +15 -0
  20. package/dist/auth/mtls.js +53 -0
  21. package/dist/auth/pat-header.d.ts +19 -0
  22. package/dist/auth/pat-header.js +34 -0
  23. package/dist/auth/storage-state-cache.d.ts +38 -0
  24. package/dist/auth/storage-state-cache.js +143 -0
  25. package/dist/auth/test-backdoor.d.ts +25 -0
  26. package/dist/auth/test-backdoor.js +51 -0
  27. package/dist/auth/totp.d.ts +39 -0
  28. package/dist/auth/totp.js +108 -0
  29. package/dist/auth/types.d.ts +86 -0
  30. package/dist/auth/types.js +57 -0
  31. package/dist/auth/ui-form.d.ts +204 -0
  32. package/dist/auth/ui-form.js +153 -0
  33. package/dist/auth/webauthn.d.ts +88 -0
  34. package/dist/auth/webauthn.js +67 -0
  35. package/dist/auth.d.ts +1 -0
  36. package/dist/auth.js +3 -0
  37. package/dist/backend-client-contracts.d.ts +88 -0
  38. package/dist/backend-client-contracts.js +19 -0
  39. package/dist/backend-client-oauth-login.d.ts +7 -0
  40. package/dist/backend-client-oauth-login.js +90 -0
  41. package/dist/backend-client-state-cache.d.ts +73 -0
  42. package/dist/backend-client-state-cache.js +185 -0
  43. package/dist/backend-client-token.d.ts +18 -0
  44. package/dist/backend-client-token.js +94 -0
  45. package/dist/backend-client-transport.d.ts +66 -0
  46. package/dist/backend-client-transport.js +181 -0
  47. package/dist/backend-client.d.ts +5 -0
  48. package/dist/backend-client.js +18 -0
  49. package/dist/calibrate.d.ts +31 -0
  50. package/dist/calibrate.js +68 -0
  51. package/dist/cli-commands-authoring.d.ts +5 -0
  52. package/dist/cli-commands-authoring.js +403 -0
  53. package/dist/cli-commands-backend.d.ts +5 -0
  54. package/dist/cli-commands-backend.js +211 -0
  55. package/dist/cli-commands-docpack.d.ts +5 -0
  56. package/dist/cli-commands-docpack.js +280 -0
  57. package/dist/cli-commands-session.d.ts +4 -0
  58. package/dist/cli-commands-session.js +398 -0
  59. package/dist/cli-shared.d.ts +5 -0
  60. package/dist/cli-shared.js +45 -0
  61. package/dist/cli-usage.d.ts +1 -0
  62. package/dist/cli-usage.js +137 -0
  63. package/dist/cli.d.ts +2 -0
  64. package/dist/cli.js +77 -0
  65. package/dist/diagnose.d.ts +50 -0
  66. package/dist/diagnose.js +168 -0
  67. package/dist/diff-compute.d.ts +13 -0
  68. package/dist/diff-compute.js +378 -0
  69. package/dist/diff-report.d.ts +7 -0
  70. package/dist/diff-report.js +125 -0
  71. package/dist/diff-types.d.ts +125 -0
  72. package/dist/diff-types.js +15 -0
  73. package/dist/diff.d.ts +3 -0
  74. package/dist/diff.js +16 -0
  75. package/dist/doc-pack-io.d.ts +30 -0
  76. package/dist/doc-pack-io.js +182 -0
  77. package/dist/doc-pack.d.ts +1814 -0
  78. package/dist/doc-pack.js +328 -0
  79. package/dist/doctor-checks-plugins.d.ts +2 -0
  80. package/dist/doctor-checks-plugins.js +136 -0
  81. package/dist/doctor-checks.d.ts +56 -0
  82. package/dist/doctor-checks.js +367 -0
  83. package/dist/doctor.d.ts +7 -0
  84. package/dist/doctor.js +62 -0
  85. package/dist/export/adf.d.ts +57 -0
  86. package/dist/export/adf.js +323 -0
  87. package/dist/export/playwright-test.d.ts +26 -0
  88. package/dist/export/playwright-test.js +221 -0
  89. package/dist/flow-file.d.ts +21 -0
  90. package/dist/flow-file.js +180 -0
  91. package/dist/flow-lint.d.ts +24 -0
  92. package/dist/flow-lint.js +203 -0
  93. package/dist/flow-runtime.d.ts +113 -0
  94. package/dist/flow-runtime.js +273 -0
  95. package/dist/flow-tree.d.ts +19 -0
  96. package/dist/flow-tree.js +104 -0
  97. package/dist/index.d.ts +27 -0
  98. package/dist/index.js +31 -0
  99. package/dist/playwright-driver.d.ts +105 -0
  100. package/dist/playwright-driver.js +363 -0
  101. package/dist/playwright-instrumented-browser.d.ts +51 -0
  102. package/dist/playwright-instrumented-browser.js +189 -0
  103. package/dist/plugins/load.d.ts +22 -0
  104. package/dist/plugins/load.js +99 -0
  105. package/dist/plugins/lock.d.ts +40 -0
  106. package/dist/plugins/lock.js +122 -0
  107. package/dist/plugins/manifest.d.ts +70 -0
  108. package/dist/plugins/manifest.js +115 -0
  109. package/dist/plugins/plan.d.ts +51 -0
  110. package/dist/plugins/plan.js +279 -0
  111. package/dist/plugins/registry.d.ts +59 -0
  112. package/dist/plugins/registry.js +71 -0
  113. package/dist/plugins/runtime.d.ts +7 -0
  114. package/dist/plugins/runtime.js +27 -0
  115. package/dist/plugins/types.d.ts +58 -0
  116. package/dist/plugins/types.js +4 -0
  117. package/dist/plugins-cli.d.ts +1 -0
  118. package/dist/plugins-cli.js +191 -0
  119. package/dist/redact.d.ts +16 -0
  120. package/dist/redact.js +72 -0
  121. package/dist/style.d.ts +46 -0
  122. package/dist/style.js +151 -0
  123. package/dist/viewer-bin.d.ts +20 -0
  124. package/dist/viewer-bin.js +97 -0
  125. package/dist/workspace.d.ts +60 -0
  126. package/dist/workspace.js +172 -0
  127. package/dist/zip.d.ts +17 -0
  128. package/dist/zip.js +113 -0
  129. package/package.json +64 -0
@@ -0,0 +1,328 @@
1
+ // Doc-pack schema — the artifacts a calibration run produces and an execution run consumes.
2
+ //
3
+ // Layout on disk:
4
+ // <project>/flows/<flow>.flow.yaml — flow-file (source of truth for execution)
5
+ // <project>/docs/<flow>/<step>.md — step write-ups (user-facing prose)
6
+ // <project>/docs/<flow>/screenshots/<step>.png
7
+ // <project>/docs/<flow>/annotations.json — per-step annotation records (this module's AnnotationsFile)
8
+ // <project>/docs/style.yaml + style.json — style artifact (canonical + derived)
9
+ // <project>/docs/locators.yaml — locator manifest (one canonical locator per step)
10
+ // <project>/auth/strategy.yaml — target-site auth-strategy descriptor
11
+ //
12
+ // Runtime validation is done with zod; the exported TS types are inferred from the schemas
13
+ // so the two never drift.
14
+ import { z } from "zod";
15
+ // ---------------------------------------------------------------------------
16
+ // Shared
17
+ // ---------------------------------------------------------------------------
18
+ export const ArrowStyle = z.enum([
19
+ "top-left",
20
+ "top-right",
21
+ "bottom-left",
22
+ "bottom-right",
23
+ "top",
24
+ "bottom",
25
+ "left",
26
+ "right",
27
+ ]);
28
+ /** A locator reference (`$play_button`) resolved against a flow-file's `locators` map, or an inline selector. */
29
+ export const LocatorRef = z.string().min(1);
30
+ // ---------------------------------------------------------------------------
31
+ // Flow-file (`<flow>.flow.yaml`)
32
+ // ---------------------------------------------------------------------------
33
+ export const ActionType = z.enum([
34
+ "navigate",
35
+ "click",
36
+ "fill",
37
+ "upload",
38
+ "press",
39
+ "hover",
40
+ "select",
41
+ "check",
42
+ "uncheck",
43
+ "wait",
44
+ ]);
45
+ /**
46
+ * What to wait for after a step's action settles. `network_idle` / `element_stable` / `load` are named
47
+ * primitives; `{ selector }` waits for an element to appear (Playwright's default timeout, ~30s) — give it
48
+ * `timeout_ms` to override that (e.g. waiting on a multi-minute backend op that mounts a "done" element);
49
+ * `{ timeout_ms }` alone is a blind sleep (last resort — for animations, not state).
50
+ */
51
+ export const WaitSpec = z.union([
52
+ z.enum(["network_idle", "element_stable", "load"]),
53
+ z.object({ timeout_ms: z.number().int().positive() }).strict(),
54
+ z.object({ selector: LocatorRef, timeout_ms: z.number().int().positive().optional() }).strict(),
55
+ ]);
56
+ /** Post-step success criterion. Execution halts if it fails (no selector fallbacks — drift is a signal). */
57
+ export const SuccessSpec = z.union([
58
+ z.object({ visible: LocatorRef }).strict(),
59
+ z.object({ hidden: LocatorRef }).strict(),
60
+ z.object({ url_matches: z.string().min(1) }).strict(),
61
+ z
62
+ .object({ text_contains: z.object({ selector: LocatorRef, text: z.string() }).strict() })
63
+ .strict(),
64
+ ]);
65
+ export const NudgeOffset = z.object({ x: z.number(), y: z.number() }).strict();
66
+ // ---------------------------------------------------------------------------
67
+ // Execution environment (`environment:` block)
68
+ // ---------------------------------------------------------------------------
69
+ export const ViewportSize = z
70
+ .object({ width: z.number().int().positive(), height: z.number().int().positive() })
71
+ .strict();
72
+ export const ViewportPreset = z.enum(["desktop", "tablet", "mobile"]);
73
+ /** Named viewport presets — `desktop` 1440×900, `tablet` 834×1112, `mobile` 390×844. */
74
+ export const VIEWPORT_PRESETS = {
75
+ desktop: { width: 1440, height: 900 },
76
+ tablet: { width: 834, height: 1112 },
77
+ mobile: { width: 390, height: 844 },
78
+ };
79
+ /**
80
+ * Deterministic execution environment for a flow. All fields optional; applied at browser-context
81
+ * creation (so the whole flow runs under them). With `extends`, the child flow's `environment`
82
+ * wins per-key over the parent's (a child can pin just `viewport` and inherit the parent's clock).
83
+ */
84
+ export const EnvironmentSpec = z
85
+ .object({
86
+ /** ISO-8601 instant the page clock is frozen at — `new Date()` etc. return this for the whole run. */
87
+ clock: z.string().datetime({ offset: true, local: true }).optional(),
88
+ /** BCP-47 language tag (e.g. `en-GB`). */
89
+ locale: z
90
+ .string()
91
+ .regex(/^[A-Za-z]{2,3}(-[A-Za-z0-9]+)*$/, "must be a BCP-47 language tag (e.g. en-GB)")
92
+ .optional(),
93
+ /** IANA timezone (e.g. `Europe/Amsterdam`). */
94
+ timezone: z.string().min(1).optional(),
95
+ /** `{ width, height }` in CSS pixels, or a named preset — see {@link VIEWPORT_PRESETS}. */
96
+ viewport: z.union([ViewportPreset, ViewportSize]).optional(),
97
+ color_scheme: z.enum(["light", "dark"]).optional(),
98
+ reduced_motion: z.boolean().optional(),
99
+ })
100
+ .strict();
101
+ // ---------------------------------------------------------------------------
102
+ // Redactions (`redactions:` — flow-level and per-step)
103
+ // ---------------------------------------------------------------------------
104
+ export const RedactionStyle = z.enum(["box", "pixelate"]);
105
+ /** A fixed rectangle in CSS pixels (viewport coordinates), scaled to device pixels at capture time. */
106
+ export const RedactionRegion = z
107
+ .object({
108
+ x: z.number().min(0),
109
+ y: z.number().min(0),
110
+ width: z.number().positive(),
111
+ height: z.number().positive(),
112
+ })
113
+ .strict();
114
+ /**
115
+ * One area to mask on every screenshot it applies to (step shots *and* halt shots): either an
116
+ * element (locator ref / inline selector, resolved to its bounding box at capture time) or a fixed
117
+ * `region`. Default `style` is `box` (solid #000 fill); `pixelate` is a 16-px mosaic. A selector
118
+ * that matches nothing at capture time is skipped with a stderr warning — redacting an absent
119
+ * element is vacuously satisfied, never a halt. Flow-level `redactions` apply to every step;
120
+ * per-step `redactions` are additive.
121
+ */
122
+ export const RedactionSpec = z.union([
123
+ z.object({ selector: LocatorRef, style: RedactionStyle.optional() }).strict(),
124
+ z.object({ region: RedactionRegion, style: RedactionStyle.optional() }).strict(),
125
+ ]);
126
+ export const StepAnnotation = z
127
+ .object({
128
+ copy: z.string().min(1),
129
+ arrow: ArrowStyle.optional(),
130
+ /**
131
+ * Optional pixel offset applied to the callout + arrow after Popper-like placement. The halo (which
132
+ * highlights the target element) stays on the target. Use this when two annotations on the same
133
+ * screenshot would otherwise overlap each other — nudge one aside so both are readable.
134
+ * Image-space pixels; small values (5–40 px in either direction) typically suffice.
135
+ */
136
+ nudge: NudgeOffset.optional(),
137
+ /**
138
+ * Optional override: the locator to anchor the halo/arrow to. Default = the step's `target`. Use this on
139
+ * a step whose action *transitions the UI* — the action target vanishes (gets unmounted / replaced) and
140
+ * a *different* element is what you want to highlight in the resulting state. Point this at the
141
+ * surviving / appearing element.
142
+ */
143
+ target: LocatorRef.optional(),
144
+ })
145
+ .strict();
146
+ export const Step = z
147
+ .object({
148
+ id: z.string().min(1),
149
+ action: ActionType,
150
+ /**
151
+ * Best-effort step: if the action / `wait_for` / `success` check throws (target absent, wait timed
152
+ * out, etc.), **skip this step and continue** instead of halting the flow. For conditionally-present
153
+ * UI — a confirmation modal that sometimes appears, a first-run tooltip, a cookie banner. A skipped
154
+ * optional step emits no screenshot / annotation (same as a step skipped by `--start-from`). Prefer
155
+ * this over a permissive comma-selector that no-ops on one branch.
156
+ */
157
+ optional: z.boolean().optional(),
158
+ /** Locator ref (`$name`) or inline selector. Optional for actions like `navigate` (uses `value`) or `wait`. */
159
+ target: LocatorRef.optional(),
160
+ /** Action payload: text for `fill`, file path for `upload`, key for `press`, path/URL for `navigate`, option for `select`. */
161
+ value: z.string().optional(),
162
+ wait_for: WaitSpec.optional(),
163
+ success: SuccessSpec.optional(),
164
+ /** Single call-out on this step's screenshot. Shorthand for a one-element `annotations` array. */
165
+ annotation: StepAnnotation.optional(),
166
+ /**
167
+ * Multiple call-outs on the same screenshot — rendered as numbered badges (1, 2, …) so the reader sees
168
+ * up front that there's more than one thing to look at without having to hover everything. Each entry
169
+ * has its own `target` (defaults to the step's `target`) and `copy` / `arrow` — see {@link StepAnnotation}.
170
+ * Mutually exclusive with `annotation`.
171
+ */
172
+ annotations: z.array(StepAnnotation).min(1).optional(),
173
+ /** Extra redactions for this step's screenshots, additive on top of the flow-level list. */
174
+ redactions: z.array(RedactionSpec).min(1).optional(),
175
+ })
176
+ .strict()
177
+ .refine((s) => !(s.annotation && s.annotations), {
178
+ message: "step has both `annotation` and `annotations`; use one (`annotations: [...]` for the multi-callout form)",
179
+ path: ["annotations"],
180
+ });
181
+ /** A precondition the flow assumes (e.g. `{ logged_in_as: "editor" }`, `{ feature_flag: "recap.enabled" }`). */
182
+ export const Prerequisite = z.record(z.string(), z.union([z.string(), z.boolean()]));
183
+ export const FlowFile = z
184
+ .object({
185
+ name: z.string().min(1),
186
+ /**
187
+ * Name of another flow whose steps run *first* (composition). The parent's `locators` + `prerequisites`
188
+ * are merged in (this flow wins on collisions); step ids must be unique across the merge. Chains allowed
189
+ * (A extends B extends C); cycles are rejected. Resolved at run time against `flows/<name>.flow.yaml`.
190
+ * Typical use: factor out a shared preamble (Library → open a video → editor) so dependent flows don't
191
+ * re-walk it every run. (`run --stop-after` operates on the merged step list.)
192
+ */
193
+ extends: z.string().min(1).optional(),
194
+ /**
195
+ * Deterministic execution environment (frozen clock, locale, timezone, viewport, color scheme,
196
+ * reduced motion). With `extends`, merged per-key — this flow's keys win over the parent's.
197
+ */
198
+ environment: EnvironmentSpec.optional(),
199
+ /** Areas masked on every screenshot this flow produces (incl. halt shots). See {@link RedactionSpec}. */
200
+ redactions: z.array(RedactionSpec).min(1).optional(),
201
+ prerequisites: z.array(Prerequisite).default([]),
202
+ /** Named canonical locators referenced from steps as `$name`. One per name; no fallback lists. */
203
+ locators: z.record(z.string(), z.string()).default({}),
204
+ steps: z.array(Step).min(1),
205
+ })
206
+ .strict();
207
+ // ---------------------------------------------------------------------------
208
+ // Annotations (`<flow>/annotations.json`)
209
+ // ---------------------------------------------------------------------------
210
+ export const BoundingBox = z
211
+ .object({ x: z.number(), y: z.number(), width: z.number(), height: z.number() })
212
+ .strict();
213
+ export const AnnotationRecord = z
214
+ .object({
215
+ step: z.string().min(1),
216
+ selector: z.string().min(1),
217
+ bounding_box: BoundingBox.optional(),
218
+ copy: z.string().min(1),
219
+ arrow_style: ArrowStyle.optional(),
220
+ /** Optional pixel offset applied to the callout + arrow at render time — see {@link NudgeOffset}. */
221
+ nudge: NudgeOffset.optional(),
222
+ /** 1-based index of this annotation *within its step's screenshot* — set only when the step has > 1 annotation, so the viewer can render a numbered badge. Absent → render as a plain (un-numbered) halo. */
223
+ index: z.number().int().positive().optional(),
224
+ })
225
+ .strict();
226
+ export const AnnotationsFile = z
227
+ .object({
228
+ schema: z.literal("docsxai/annotations@1"),
229
+ flow: z.string().min(1),
230
+ annotations: z.array(AnnotationRecord),
231
+ })
232
+ .strict();
233
+ // ---------------------------------------------------------------------------
234
+ // Style artifact (`style.yaml` canonical → `style.json` derived)
235
+ // ---------------------------------------------------------------------------
236
+ export const StyleArtifact = z
237
+ .object({
238
+ schema: z.literal("docsxai/style@1"),
239
+ voice: z.record(z.string(), z.unknown()).optional(),
240
+ structure: z.record(z.string(), z.unknown()).optional(),
241
+ terminology: z.record(z.string(), z.string()).optional(),
242
+ visual: z.record(z.string(), z.unknown()).optional(),
243
+ localisation: z.record(z.string(), z.unknown()).optional(),
244
+ /** Categories of testing-jargon the commit stage must strip from user-facing prose. */
245
+ pruning_rules: z.array(z.string()).optional(),
246
+ })
247
+ .strict();
248
+ // ---------------------------------------------------------------------------
249
+ // Locator manifest (`locators.yaml`)
250
+ // ---------------------------------------------------------------------------
251
+ export const LocatorManifest = z
252
+ .object({
253
+ schema: z.literal("docsxai/locators@1"),
254
+ /** flow name → locator name → canonical selector. One per name; no fallbacks. */
255
+ flows: z.record(z.string(), z.record(z.string(), z.string())),
256
+ })
257
+ .strict();
258
+ // ---------------------------------------------------------------------------
259
+ // Auth-strategy descriptor (`auth/strategy.yaml`)
260
+ // ---------------------------------------------------------------------------
261
+ export const StrategyName = z.enum([
262
+ "api-login",
263
+ "jwt-injection",
264
+ "ui-form",
265
+ "http-basic",
266
+ "mtls",
267
+ "pat-header",
268
+ "email-otp",
269
+ "totp",
270
+ "webauthn",
271
+ "manual-capture",
272
+ "test-backdoor",
273
+ ]);
274
+ /** `session` = use the captured session's own lifetime; otherwise a duration string (`30m`, `1h`) or ms number. */
275
+ export const CacheTtl = z.union([
276
+ z.literal("session"),
277
+ z.string().regex(/^\d+(ms|s|m|h)$/),
278
+ z.number().int().positive(),
279
+ ]);
280
+ export const RoleAuth = z
281
+ .object({
282
+ strategy: StrategyName,
283
+ /** Env-var *names* holding credentials — never the values. May be `{}` (e.g. `manual-capture` needs none). */
284
+ creds_env: z.record(z.string(), z.string()).default({}),
285
+ options: z.record(z.string(), z.unknown()).default({}),
286
+ cache: z
287
+ .object({
288
+ enabled: z.boolean().default(false),
289
+ store: z.enum(["local", "backend"]).default("local"),
290
+ /** Fallback expiry when no `auth_cookie` is set/found: a duration, or `session` (→ a 1h default). */
291
+ ttl: CacheTtl.default("session"),
292
+ /**
293
+ * Name of the app's actual auth/session cookie. When set, the cached session's `expiresAt` is *that*
294
+ * cookie's expiry — the real bound — rather than the `ttl` guess. Identify it from the captured jar
295
+ * (`capture-auth` prints it): it's on the app's domain, long-lived (not an ephemeral IdP scratch
296
+ * cookie), e.g. `AppSession.Production` / `.AspNetCore.Cookies` / `session`. Optional.
297
+ */
298
+ auth_cookie: z.string().min(1).optional(),
299
+ })
300
+ .strict()
301
+ .default({ enabled: false, store: "local", ttl: "session" }),
302
+ })
303
+ .strict();
304
+ export const AuthStrategyDescriptor = z
305
+ .object({
306
+ schema: z.literal("docsxai/auth-strategy@1"),
307
+ default_role: z.string().min(1),
308
+ roles: z.record(z.string(), RoleAuth),
309
+ })
310
+ .strict()
311
+ .refine((d) => d.default_role in d.roles, {
312
+ message: "default_role must be one of the keys in roles",
313
+ path: ["default_role"],
314
+ });
315
+ // ---------------------------------------------------------------------------
316
+ // Revision metadata (linear immutable revisions per project)
317
+ // ---------------------------------------------------------------------------
318
+ export const RevisionKind = z.enum(["calibrate", "run", "edit"]);
319
+ export const RevisionMeta = z
320
+ .object({
321
+ rev_id: z.string().min(1),
322
+ parent_rev_id: z.string().min(1).nullable(),
323
+ kind: RevisionKind,
324
+ author: z.string().min(1),
325
+ /** ISO-8601 timestamp. */
326
+ timestamp: z.string().min(1),
327
+ })
328
+ .strict();
@@ -0,0 +1,2 @@
1
+ import type { DoctorCheck } from "./doctor-checks.js";
2
+ export declare function checkPlugins(workspaceDir: string): Promise<DoctorCheck[]>;
@@ -0,0 +1,136 @@
1
+ // docsxai doctor - the plugin-runtime probe. Split out of doctor-checks.ts: the
2
+ // plugin config / lock / manifest checking is a distinct, sizeable concern from
3
+ // the environment and workspace-content probes, with its own dependency set
4
+ // (the plugin runtime + lock + manifest domain).
5
+ import { promises as fs } from "node:fs";
6
+ import { isApiVersionCompatible, RUNTIME_API_VERSION } from "./plugins/manifest.js";
7
+ import { PLUGINS_LOCK_FILE, PluginsConfigError, PluginsLockError, readPluginsLock, readWorkspacePluginsConfig, verifyLock, } from "./plugins/lock.js";
8
+ import { resolvePluginSources } from "./plugins/runtime.js";
9
+ import { WORKSPACE_CONFIG_FILE } from "./workspace.js";
10
+ export async function checkPlugins(workspaceDir) {
11
+ let cfg;
12
+ try {
13
+ cfg = await readWorkspacePluginsConfig(workspaceDir);
14
+ }
15
+ catch (e) {
16
+ if (e instanceof PluginsConfigError) {
17
+ return [
18
+ {
19
+ name: "plugins",
20
+ ok: false,
21
+ detail: e.message,
22
+ fix: `fix the "plugins" / "plugin_capabilities" keys in ${WORKSPACE_CONFIG_FILE}`,
23
+ },
24
+ ];
25
+ }
26
+ throw e;
27
+ }
28
+ if (cfg.sources.length === 0) {
29
+ return [
30
+ {
31
+ name: "plugins",
32
+ ok: true,
33
+ info: true,
34
+ detail: `no plugins configured (add a "plugins" array to ${WORKSPACE_CONFIG_FILE})`,
35
+ },
36
+ ];
37
+ }
38
+ let lock;
39
+ try {
40
+ lock = await readPluginsLock(workspaceDir);
41
+ }
42
+ catch (e) {
43
+ if (e instanceof PluginsLockError) {
44
+ return [
45
+ {
46
+ name: "plugins",
47
+ ok: false,
48
+ detail: e.message,
49
+ fix: `re-pin: docsxai plugins sync ${workspaceDir}`,
50
+ },
51
+ ];
52
+ }
53
+ throw e;
54
+ }
55
+ const checks = [];
56
+ if (!lock) {
57
+ checks.push({
58
+ name: "plugins",
59
+ ok: false,
60
+ detail: `${PLUGINS_LOCK_FILE} missing (${cfg.sources.length} plugin(s) configured — no reproducibility pin)`,
61
+ fix: `docsxai plugins sync ${workspaceDir}`,
62
+ });
63
+ }
64
+ const resolutions = await resolvePluginSources(workspaceDir, cfg.sources);
65
+ const enabled = new Set(cfg.capabilities);
66
+ const namespaceOwner = new Map();
67
+ for (const r of resolutions) {
68
+ if (!r.ok) {
69
+ checks.push({
70
+ name: "plugins",
71
+ ok: false,
72
+ detail: `${r.record.name}: ${r.record.statusReason ?? "unresolvable"}`,
73
+ fix: `fix the source entry in ${WORKSPACE_CONFIG_FILE} (or install the package), then \`docsxai plugins sync\``,
74
+ });
75
+ continue;
76
+ }
77
+ const c = r.candidate;
78
+ const ns = c.manifest.namespace;
79
+ const issues = [];
80
+ if (!isApiVersionCompatible(c.manifest.apiVersion)) {
81
+ issues.push({
82
+ detail: `${c.name} apiVersion "${c.manifest.apiVersion}" incompatible with runtime apiVersion "${RUNTIME_API_VERSION}"`,
83
+ fix: "upgrade the plugin or the engine (same major, plugin minor ≤ runtime)",
84
+ });
85
+ }
86
+ const prior = namespaceOwner.get(ns);
87
+ if (prior) {
88
+ issues.push({
89
+ detail: `${c.name} namespace "${ns}" already claimed by ${prior} — neither will load`,
90
+ fix: "namespaces are unique across the configured set; rename one",
91
+ });
92
+ }
93
+ else {
94
+ namespaceOwner.set(ns, c.name);
95
+ }
96
+ let lockNote = "no lock";
97
+ if (lock) {
98
+ let bytes;
99
+ try {
100
+ bytes = await fs.readFile(c.registerPath);
101
+ }
102
+ catch {
103
+ bytes = null;
104
+ }
105
+ const mismatch = verifyLock(lock, ns, bytes);
106
+ if (mismatch) {
107
+ issues.push({
108
+ detail: mismatch,
109
+ fix: `after auditing the change: docsxai plugins sync ${workspaceDir}`,
110
+ });
111
+ }
112
+ else {
113
+ lockNote = "lock ok";
114
+ }
115
+ }
116
+ const missingCaps = c.manifest.capabilities.filter((cap) => !enabled.has(cap));
117
+ if (missingCaps.length > 0) {
118
+ issues.push({
119
+ detail: `${c.name} declares capability(ies) [${missingCaps.join(", ")}] not enabled for this workspace`,
120
+ fix: `opt in via "plugin_capabilities" in ${WORKSPACE_CONFIG_FILE}`,
121
+ });
122
+ }
123
+ if (issues.length > 0) {
124
+ for (const i of issues)
125
+ checks.push({ name: "plugins", ok: false, ...i });
126
+ }
127
+ else {
128
+ checks.push({
129
+ name: "plugins",
130
+ ok: true,
131
+ detail: `${c.name}@${c.version} (ns=${ns}, ${c.manifest.kinds.join(",")}, ${lockNote})`,
132
+ });
133
+ }
134
+ }
135
+ return checks;
136
+ }
@@ -0,0 +1,56 @@
1
+ export interface DoctorCheck {
2
+ name: string;
3
+ ok: boolean;
4
+ detail: string;
5
+ fix?: string;
6
+ /** Informational row — printed with − instead of ✓/✗; never fails doctor. */
7
+ info?: boolean;
8
+ }
9
+ export interface DoctorOptions {
10
+ /** Workspace dir to inspect. Default: the current working directory. */
11
+ workspaceDir?: string;
12
+ /** Env source (tests inject). Default `process.env`. */
13
+ env?: NodeJS.ProcessEnv;
14
+ /** Node version under test (tests inject). Default `process.versions.node`. */
15
+ nodeVersion?: string;
16
+ /** Chromium probe override (tests inject; the real probe asks playwright-core). */
17
+ chromiumProbe?: () => Promise<{
18
+ ok: boolean;
19
+ detail: string;
20
+ }>;
21
+ /** fetch override for the backend health probe (tests inject). */
22
+ fetchImpl?: typeof globalThis.fetch;
23
+ /** Clock override for session-freshness checks (tests inject). */
24
+ now?: number;
25
+ }
26
+ /** Layer-3 default probe: does playwright-core have a cached Chromium binary?
27
+ * Synchronous - the sole work is the sanctioned `chromiumExecutablePath()`
28
+ * helper (a sync `executablePath()` + `existsSync`), no IO to await. */
29
+ export declare function probeChromium(): {
30
+ ok: boolean;
31
+ detail: string;
32
+ };
33
+ export declare function checkNode(nodeVersion: string): DoctorCheck;
34
+ export declare function checkChromium(probe: () => {
35
+ ok: boolean;
36
+ detail: string;
37
+ } | Promise<{
38
+ ok: boolean;
39
+ detail: string;
40
+ }>): Promise<DoctorCheck>;
41
+ interface WorkspaceProbe {
42
+ check: DoctorCheck;
43
+ /** Parsed `.docsxai.json` when valid (doctor reads the raw JSON — schema-checked here). */
44
+ config: Record<string, unknown> | null;
45
+ /** True when the dir looks like a workspace at all (config file or flows/ present). */
46
+ present: boolean;
47
+ }
48
+ export declare function checkWorkspace(workspaceDir: string): Promise<WorkspaceProbe>;
49
+ export declare function checkFlows(workspaceDir: string): Promise<DoctorCheck>;
50
+ export declare function checkAuth(workspaceDir: string, now: number): Promise<DoctorCheck[]>;
51
+ export declare function checkBackend(workspaceDir: string, config: Record<string, unknown> | null, env: NodeJS.ProcessEnv, fetchImpl: typeof globalThis.fetch): Promise<DoctorCheck>;
52
+ export declare function checkViewer(env: NodeJS.ProcessEnv, resolveFrom?: string[]): Promise<DoctorCheck>;
53
+ /** DOCSX_* env vars the docsxai packages read (engine + backend). */
54
+ export declare const KNOWN_DOCSX_ENV_VARS: ReadonlyArray<string>;
55
+ export declare function checkEnv(env: NodeJS.ProcessEnv): DoctorCheck[];
56
+ export {};