@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.
- package/LICENSE +202 -0
- package/README.md +130 -0
- package/dist/auth/api-login.d.ts +69 -0
- package/dist/auth/api-login.js +95 -0
- package/dist/auth/browser-session.d.ts +28 -0
- package/dist/auth/browser-session.js +43 -0
- package/dist/auth/cookie-jar.d.ts +58 -0
- package/dist/auth/cookie-jar.js +212 -0
- package/dist/auth/email-otp.d.ts +210 -0
- package/dist/auth/email-otp.js +166 -0
- package/dist/auth/http-basic.d.ts +5 -0
- package/dist/auth/http-basic.js +17 -0
- package/dist/auth/index.d.ts +47 -0
- package/dist/auth/index.js +137 -0
- package/dist/auth/jwt-injection.d.ts +153 -0
- package/dist/auth/jwt-injection.js +136 -0
- package/dist/auth/manual-capture.d.ts +35 -0
- package/dist/auth/manual-capture.js +30 -0
- package/dist/auth/mtls.d.ts +15 -0
- package/dist/auth/mtls.js +53 -0
- package/dist/auth/pat-header.d.ts +19 -0
- package/dist/auth/pat-header.js +34 -0
- package/dist/auth/storage-state-cache.d.ts +38 -0
- package/dist/auth/storage-state-cache.js +143 -0
- package/dist/auth/test-backdoor.d.ts +25 -0
- package/dist/auth/test-backdoor.js +51 -0
- package/dist/auth/totp.d.ts +39 -0
- package/dist/auth/totp.js +108 -0
- package/dist/auth/types.d.ts +86 -0
- package/dist/auth/types.js +57 -0
- package/dist/auth/ui-form.d.ts +204 -0
- package/dist/auth/ui-form.js +153 -0
- package/dist/auth/webauthn.d.ts +88 -0
- package/dist/auth/webauthn.js +67 -0
- package/dist/auth.d.ts +1 -0
- package/dist/auth.js +3 -0
- package/dist/backend-client-contracts.d.ts +88 -0
- package/dist/backend-client-contracts.js +19 -0
- package/dist/backend-client-oauth-login.d.ts +7 -0
- package/dist/backend-client-oauth-login.js +90 -0
- package/dist/backend-client-state-cache.d.ts +73 -0
- package/dist/backend-client-state-cache.js +185 -0
- package/dist/backend-client-token.d.ts +18 -0
- package/dist/backend-client-token.js +94 -0
- package/dist/backend-client-transport.d.ts +66 -0
- package/dist/backend-client-transport.js +181 -0
- package/dist/backend-client.d.ts +5 -0
- package/dist/backend-client.js +18 -0
- package/dist/calibrate.d.ts +31 -0
- package/dist/calibrate.js +68 -0
- package/dist/cli-commands-authoring.d.ts +5 -0
- package/dist/cli-commands-authoring.js +403 -0
- package/dist/cli-commands-backend.d.ts +5 -0
- package/dist/cli-commands-backend.js +211 -0
- package/dist/cli-commands-docpack.d.ts +5 -0
- package/dist/cli-commands-docpack.js +280 -0
- package/dist/cli-commands-session.d.ts +4 -0
- package/dist/cli-commands-session.js +398 -0
- package/dist/cli-shared.d.ts +5 -0
- package/dist/cli-shared.js +45 -0
- package/dist/cli-usage.d.ts +1 -0
- package/dist/cli-usage.js +137 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +77 -0
- package/dist/diagnose.d.ts +50 -0
- package/dist/diagnose.js +168 -0
- package/dist/diff-compute.d.ts +13 -0
- package/dist/diff-compute.js +378 -0
- package/dist/diff-report.d.ts +7 -0
- package/dist/diff-report.js +125 -0
- package/dist/diff-types.d.ts +125 -0
- package/dist/diff-types.js +15 -0
- package/dist/diff.d.ts +3 -0
- package/dist/diff.js +16 -0
- package/dist/doc-pack-io.d.ts +30 -0
- package/dist/doc-pack-io.js +182 -0
- package/dist/doc-pack.d.ts +1814 -0
- package/dist/doc-pack.js +328 -0
- package/dist/doctor-checks-plugins.d.ts +2 -0
- package/dist/doctor-checks-plugins.js +136 -0
- package/dist/doctor-checks.d.ts +56 -0
- package/dist/doctor-checks.js +367 -0
- package/dist/doctor.d.ts +7 -0
- package/dist/doctor.js +62 -0
- package/dist/export/adf.d.ts +57 -0
- package/dist/export/adf.js +323 -0
- package/dist/export/playwright-test.d.ts +26 -0
- package/dist/export/playwright-test.js +221 -0
- package/dist/flow-file.d.ts +21 -0
- package/dist/flow-file.js +180 -0
- package/dist/flow-lint.d.ts +24 -0
- package/dist/flow-lint.js +203 -0
- package/dist/flow-runtime.d.ts +113 -0
- package/dist/flow-runtime.js +273 -0
- package/dist/flow-tree.d.ts +19 -0
- package/dist/flow-tree.js +104 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +31 -0
- package/dist/playwright-driver.d.ts +105 -0
- package/dist/playwright-driver.js +363 -0
- package/dist/playwright-instrumented-browser.d.ts +51 -0
- package/dist/playwright-instrumented-browser.js +189 -0
- package/dist/plugins/load.d.ts +22 -0
- package/dist/plugins/load.js +99 -0
- package/dist/plugins/lock.d.ts +40 -0
- package/dist/plugins/lock.js +122 -0
- package/dist/plugins/manifest.d.ts +70 -0
- package/dist/plugins/manifest.js +115 -0
- package/dist/plugins/plan.d.ts +51 -0
- package/dist/plugins/plan.js +279 -0
- package/dist/plugins/registry.d.ts +59 -0
- package/dist/plugins/registry.js +71 -0
- package/dist/plugins/runtime.d.ts +7 -0
- package/dist/plugins/runtime.js +27 -0
- package/dist/plugins/types.d.ts +58 -0
- package/dist/plugins/types.js +4 -0
- package/dist/plugins-cli.d.ts +1 -0
- package/dist/plugins-cli.js +191 -0
- package/dist/redact.d.ts +16 -0
- package/dist/redact.js +72 -0
- package/dist/style.d.ts +46 -0
- package/dist/style.js +151 -0
- package/dist/viewer-bin.d.ts +20 -0
- package/dist/viewer-bin.js +97 -0
- package/dist/workspace.d.ts +60 -0
- package/dist/workspace.js +172 -0
- package/dist/zip.d.ts +17 -0
- package/dist/zip.js +113 -0
- package/package.json +64 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// `docsxai doctor` — the per-check probe catalogue.
|
|
2
|
+
//
|
|
3
|
+
// One pure(ish) function per row of the doctor checklist, each over injectable inputs (a workspace
|
|
4
|
+
// dir, an env source, a fake backend, a mocked chromium probe) so per-state coverage is cheap.
|
|
5
|
+
// `doctor.ts` orchestrates these into the printed checklist. Pure inspection throughout: no flow
|
|
6
|
+
// runs, no plugin code execution, no browser launch — the only network touch is the opt-in GET of
|
|
7
|
+
// the configured backend's /v1/health.
|
|
8
|
+
import { existsSync, promises as fs } from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { parseAuthStrategyFile } from "./auth.js";
|
|
11
|
+
import { FlowFileError, parseFlowFile } from "./flow-file.js";
|
|
12
|
+
import { chromiumExecutablePath } from "./playwright-driver.js";
|
|
13
|
+
import { resolveViewerBin, VIEWER_BIN_ENV, VIEWER_BIN_NAME, VIEWER_PACKAGE } from "./viewer-bin.js";
|
|
14
|
+
import { resolveWorkspacePath, WORKSPACE_CONFIG_FILE } from "./workspace.js";
|
|
15
|
+
const CHROMIUM_FIX = "npx playwright-core install chromium (source checkout: pnpm -C packages/engine exec playwright-core install chromium)";
|
|
16
|
+
/** Layer-3 default probe: does playwright-core have a cached Chromium binary?
|
|
17
|
+
* Synchronous - the sole work is the sanctioned `chromiumExecutablePath()`
|
|
18
|
+
* helper (a sync `executablePath()` + `existsSync`), no IO to await. */
|
|
19
|
+
export function probeChromium() {
|
|
20
|
+
try {
|
|
21
|
+
// Goes through playwright-driver's helper — the one sanctioned playwright-core entry point —
|
|
22
|
+
// so doctor doesn't open a second import site for the SDK.
|
|
23
|
+
const p = chromiumExecutablePath();
|
|
24
|
+
if (p)
|
|
25
|
+
return { ok: true, detail: p };
|
|
26
|
+
return { ok: false, detail: "playwright-core has no Chromium binary cached" };
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
return { ok: false, detail: e.message };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function readTextIfExists(p) {
|
|
33
|
+
try {
|
|
34
|
+
return await fs.readFile(p, "utf8");
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Individual checks (exported for per-state unit tests)
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
export function checkNode(nodeVersion) {
|
|
44
|
+
const major = Number(nodeVersion.split(".")[0]);
|
|
45
|
+
if (Number.isFinite(major) && major >= 20) {
|
|
46
|
+
return { name: "node", ok: true, detail: `v${nodeVersion} (>= 20 required)` };
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
name: "node",
|
|
50
|
+
ok: false,
|
|
51
|
+
detail: `v${nodeVersion} — the engine requires Node >= 20`,
|
|
52
|
+
fix: "upgrade Node (https://nodejs.org); the engine's `engines` field pins >= 20",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function checkChromium(probe) {
|
|
56
|
+
const r = await probe();
|
|
57
|
+
return r.ok
|
|
58
|
+
? { name: "chromium", ok: true, detail: r.detail }
|
|
59
|
+
: { name: "chromium", ok: false, detail: r.detail, fix: CHROMIUM_FIX };
|
|
60
|
+
}
|
|
61
|
+
export async function checkWorkspace(workspaceDir) {
|
|
62
|
+
const configPath = path.join(path.resolve(workspaceDir), WORKSPACE_CONFIG_FILE);
|
|
63
|
+
const text = await readTextIfExists(configPath);
|
|
64
|
+
const flowsDirExists = existsSync(path.join(path.resolve(workspaceDir), "flows"));
|
|
65
|
+
if (text === null) {
|
|
66
|
+
return {
|
|
67
|
+
check: {
|
|
68
|
+
name: "workspace",
|
|
69
|
+
ok: true,
|
|
70
|
+
info: true,
|
|
71
|
+
detail: flowsDirExists
|
|
72
|
+
? `flows/ present but no ${WORKSPACE_CONFIG_FILE} at ${workspaceDir} — \`docsxai init\` writes one`
|
|
73
|
+
: `no ${WORKSPACE_CONFIG_FILE} at ${workspaceDir} — not a docsxai workspace (pass <workspace-dir> or run doctor from one); workspace checks skipped`,
|
|
74
|
+
},
|
|
75
|
+
config: null,
|
|
76
|
+
present: flowsDirExists,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
raw = JSON.parse(text);
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
return {
|
|
85
|
+
check: {
|
|
86
|
+
name: "workspace",
|
|
87
|
+
ok: false,
|
|
88
|
+
detail: `${configPath} is not valid JSON: ${e.message}`,
|
|
89
|
+
fix: `fix the JSON (or re-scaffold with \`docsxai init ${workspaceDir} --force\`)`,
|
|
90
|
+
},
|
|
91
|
+
config: null,
|
|
92
|
+
present: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const cfg = raw;
|
|
96
|
+
if (cfg?.schema !== "docsxai/workspace@1") {
|
|
97
|
+
return {
|
|
98
|
+
check: {
|
|
99
|
+
name: "workspace",
|
|
100
|
+
ok: false,
|
|
101
|
+
detail: `${configPath} has schema "${String(cfg?.schema)}" — expected "docsxai/workspace@1"`,
|
|
102
|
+
fix: `set "schema": "docsxai/workspace@1" (or re-scaffold with \`docsxai init\`)`,
|
|
103
|
+
},
|
|
104
|
+
config: null,
|
|
105
|
+
present: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const appUrl = typeof cfg.app_url === "string" ? cfg.app_url : undefined;
|
|
109
|
+
return {
|
|
110
|
+
check: {
|
|
111
|
+
name: "workspace",
|
|
112
|
+
ok: true,
|
|
113
|
+
detail: `${configPath} (docsxai/workspace@1${appUrl ? `, app_url ${appUrl}` : ", no app_url"})`,
|
|
114
|
+
},
|
|
115
|
+
config: cfg,
|
|
116
|
+
present: true,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
export async function checkFlows(workspaceDir) {
|
|
120
|
+
const flowsDir = path.join(path.resolve(workspaceDir), "flows");
|
|
121
|
+
let entries;
|
|
122
|
+
try {
|
|
123
|
+
entries = (await fs.readdir(flowsDir)).filter((e) => e.endsWith(".flow.yaml")).sort();
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return {
|
|
127
|
+
name: "flows",
|
|
128
|
+
ok: false,
|
|
129
|
+
detail: `no flows/ directory at ${flowsDir}`,
|
|
130
|
+
fix: `\`docsxai init\` scaffolds it; flows live at flows/<name>.flow.yaml`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
if (entries.length === 0) {
|
|
134
|
+
return {
|
|
135
|
+
name: "flows",
|
|
136
|
+
ok: true,
|
|
137
|
+
info: true,
|
|
138
|
+
detail: "flows/ is empty — calibrate one, or hand-author flows/<name>.flow.yaml",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const p = path.join(flowsDir, entry);
|
|
143
|
+
try {
|
|
144
|
+
parseFlowFile(await fs.readFile(p, "utf8"), entry);
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
const msg = e instanceof FlowFileError ? e.message : e.message;
|
|
148
|
+
return {
|
|
149
|
+
name: "flows",
|
|
150
|
+
ok: false,
|
|
151
|
+
detail: `${entry} does not parse: ${msg}`,
|
|
152
|
+
fix: `fix the flow-file (then \`docsxai lint ${workspaceDir}\` for the full static report)`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
name: "flows",
|
|
158
|
+
ok: true,
|
|
159
|
+
detail: `${entries.length} flow-file(s) parse`,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export async function checkAuth(workspaceDir, now) {
|
|
163
|
+
const descriptorPath = resolveWorkspacePath(workspaceDir, "auth", "strategy.yaml");
|
|
164
|
+
const text = await readTextIfExists(descriptorPath);
|
|
165
|
+
if (text === null) {
|
|
166
|
+
return [
|
|
167
|
+
{
|
|
168
|
+
name: "auth",
|
|
169
|
+
ok: true,
|
|
170
|
+
info: true,
|
|
171
|
+
detail: "no auth/strategy.yaml — flows run with a fresh, unauthenticated context",
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
let descriptor;
|
|
176
|
+
try {
|
|
177
|
+
descriptor = parseAuthStrategyFile(text, descriptorPath);
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
return [
|
|
181
|
+
{
|
|
182
|
+
name: "auth",
|
|
183
|
+
ok: false,
|
|
184
|
+
detail: e.message.replace(/\n\s*/g, " "),
|
|
185
|
+
fix: "fix auth/strategy.yaml against the descriptor schema (docsxai/auth-strategy@1)",
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
const role = descriptor.default_role;
|
|
190
|
+
const roles = Object.keys(descriptor.roles).join(", ");
|
|
191
|
+
const checks = [
|
|
192
|
+
{
|
|
193
|
+
name: "auth",
|
|
194
|
+
ok: true,
|
|
195
|
+
detail: `auth/strategy.yaml ok — role(s) ${roles} (default "${role}", strategy ${descriptor.roles[role].strategy})`,
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
// Cached-session freshness for the default role (the one `run` loads).
|
|
199
|
+
const sessionPath = resolveWorkspacePath(workspaceDir, ".auth", `${role}.json`);
|
|
200
|
+
const sessionText = await readTextIfExists(sessionPath);
|
|
201
|
+
if (sessionText === null) {
|
|
202
|
+
checks.push({
|
|
203
|
+
name: "auth",
|
|
204
|
+
ok: true,
|
|
205
|
+
info: true,
|
|
206
|
+
detail: `no cached session for role "${role}" — \`docsxai capture-auth ${workspaceDir}\` before \`run\``,
|
|
207
|
+
});
|
|
208
|
+
return checks;
|
|
209
|
+
}
|
|
210
|
+
let expiresAt;
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(sessionText);
|
|
213
|
+
expiresAt = typeof parsed.expiresAt === "number" ? parsed.expiresAt : undefined;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
expiresAt = undefined;
|
|
217
|
+
}
|
|
218
|
+
if (expiresAt === undefined) {
|
|
219
|
+
checks.push({
|
|
220
|
+
name: "auth",
|
|
221
|
+
ok: false,
|
|
222
|
+
detail: `cached session ${sessionPath} is corrupt (no expiresAt)`,
|
|
223
|
+
fix: `re-capture: docsxai capture-auth ${workspaceDir}`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else if (expiresAt <= now) {
|
|
227
|
+
checks.push({
|
|
228
|
+
name: "auth",
|
|
229
|
+
ok: false,
|
|
230
|
+
detail: `cached session for role "${role}" expired ${new Date(expiresAt).toISOString()}`,
|
|
231
|
+
fix: `re-capture: docsxai capture-auth ${workspaceDir}`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
checks.push({
|
|
236
|
+
name: "auth",
|
|
237
|
+
ok: true,
|
|
238
|
+
detail: `cached session for role "${role}" valid until ${new Date(expiresAt).toISOString()}`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return checks;
|
|
242
|
+
}
|
|
243
|
+
export async function checkBackend(workspaceDir, config, env, fetchImpl) {
|
|
244
|
+
const backendUrl = typeof config?.backend_url === "string" ? config.backend_url : undefined;
|
|
245
|
+
if (!backendUrl) {
|
|
246
|
+
return {
|
|
247
|
+
name: "backend",
|
|
248
|
+
ok: true,
|
|
249
|
+
info: true,
|
|
250
|
+
detail: "no backend_url configured — the workspace operates fully locally",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// Token presence is a note, not a gate — /v1/health is unauthenticated.
|
|
254
|
+
const tokenNote = env.DOCSX_TOKEN
|
|
255
|
+
? "token: DOCSX_TOKEN set"
|
|
256
|
+
: existsSync(path.join(path.resolve(workspaceDir), ".auth", "backend-token.json"))
|
|
257
|
+
? "token: OAuth tokens at .auth/backend-token.json"
|
|
258
|
+
: "no token — push/pull need DOCSX_TOKEN or `docsxai login --oauth`";
|
|
259
|
+
const base = backendUrl.replace(/\/+$/, "");
|
|
260
|
+
try {
|
|
261
|
+
const res = await fetchImpl(`${base}/v1/health`, { signal: AbortSignal.timeout(2000) });
|
|
262
|
+
if (!res.ok) {
|
|
263
|
+
return {
|
|
264
|
+
name: "backend",
|
|
265
|
+
ok: false,
|
|
266
|
+
detail: `${base}/v1/health → HTTP ${res.status}`,
|
|
267
|
+
fix: "the backend answered but is unhealthy — check its logs",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return { name: "backend", ok: true, detail: `${base}/v1/health ok (${tokenNote})` };
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
return {
|
|
274
|
+
name: "backend",
|
|
275
|
+
ok: false,
|
|
276
|
+
detail: `${base}/v1/health unreachable (${e.message})`,
|
|
277
|
+
fix: "start the backend (or fix backend_url in .docsxai.json)",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
export async function checkViewer(env, resolveFrom) {
|
|
282
|
+
const resolution = await resolveViewerBin({
|
|
283
|
+
env: env,
|
|
284
|
+
...(resolveFrom ? { resolveFrom } : {}),
|
|
285
|
+
});
|
|
286
|
+
if (resolution.source === "env") {
|
|
287
|
+
return {
|
|
288
|
+
name: "viewer",
|
|
289
|
+
ok: true,
|
|
290
|
+
detail: `$${VIEWER_BIN_ENV} → ${resolution.prefixArgs[0] ?? resolution.command} (layer 1: env override)`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (resolution.source === "package") {
|
|
294
|
+
return {
|
|
295
|
+
name: "viewer",
|
|
296
|
+
ok: true,
|
|
297
|
+
detail: `${VIEWER_PACKAGE} installed next to the engine → ${resolution.prefixArgs[0]} (layer 2: installed package)`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
// Layer 3 is "trust PATH" — resolveViewerBin doesn't verify it, so doctor does.
|
|
301
|
+
for (const dir of (env.PATH ?? "").split(path.delimiter)) {
|
|
302
|
+
if (!dir)
|
|
303
|
+
continue;
|
|
304
|
+
const candidate = path.join(dir, VIEWER_BIN_NAME);
|
|
305
|
+
if (existsSync(candidate)) {
|
|
306
|
+
return {
|
|
307
|
+
name: "viewer",
|
|
308
|
+
ok: true,
|
|
309
|
+
detail: `\`${VIEWER_BIN_NAME}\` on PATH → ${candidate} (layer 3: PATH)`,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
name: "viewer",
|
|
315
|
+
ok: false,
|
|
316
|
+
detail: `\`${VIEWER_BIN_NAME}\` not resolvable — tried $${VIEWER_BIN_ENV}, the installed ${VIEWER_PACKAGE} package, and PATH`,
|
|
317
|
+
fix: `install ${VIEWER_PACKAGE} next to the engine (a global \`docsxai\` install ships it), or point ${VIEWER_BIN_ENV} at its bin script`,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
/** DOCSX_* env vars the docsxai packages read (engine + backend). */
|
|
321
|
+
export const KNOWN_DOCSX_ENV_VARS = [
|
|
322
|
+
"DOCSX_TOKEN",
|
|
323
|
+
"DOCSX_CACHE_KEY",
|
|
324
|
+
"DOCSX_VIEWER_BIN",
|
|
325
|
+
"DOCSX_ENGINE_BIN",
|
|
326
|
+
"DOCSX_DATA_DIR",
|
|
327
|
+
"DOCSX_OAUTH_AUTO_APPROVE",
|
|
328
|
+
"DOCSX_WEBHOOK_SECRET",
|
|
329
|
+
];
|
|
330
|
+
export function checkEnv(env) {
|
|
331
|
+
const set = Object.keys(env)
|
|
332
|
+
.filter((k) => k.startsWith("DOCSX_"))
|
|
333
|
+
.sort();
|
|
334
|
+
if (set.length === 0) {
|
|
335
|
+
return [
|
|
336
|
+
{ name: "env", ok: true, info: true, detail: "no DOCSX_* variables set (defaults apply)" },
|
|
337
|
+
];
|
|
338
|
+
}
|
|
339
|
+
const checks = [];
|
|
340
|
+
const known = set.filter((k) => KNOWN_DOCSX_ENV_VARS.includes(k));
|
|
341
|
+
if (known.length > 0) {
|
|
342
|
+
checks.push({ name: "env", ok: true, detail: `set: ${known.join(", ")}` });
|
|
343
|
+
}
|
|
344
|
+
const cacheKey = env.DOCSX_CACHE_KEY;
|
|
345
|
+
if (cacheKey) {
|
|
346
|
+
const decoded = Buffer.from(cacheKey, "base64");
|
|
347
|
+
if (decoded.length !== 32) {
|
|
348
|
+
checks.push({
|
|
349
|
+
name: "env",
|
|
350
|
+
ok: false,
|
|
351
|
+
detail: `DOCSX_CACHE_KEY must decode to exactly 32 bytes (got ${decoded.length})`,
|
|
352
|
+
fix: "regenerate: openssl rand -base64 32",
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
for (const k of set) {
|
|
357
|
+
if (!KNOWN_DOCSX_ENV_VARS.includes(k)) {
|
|
358
|
+
checks.push({
|
|
359
|
+
name: "env",
|
|
360
|
+
ok: false,
|
|
361
|
+
detail: `${k} is not a DOCSX_* variable any docsxai package reads (typo?)`,
|
|
362
|
+
fix: `unset it or fix the spelling — known: ${KNOWN_DOCSX_ENV_VARS.join(", ")}`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return checks;
|
|
367
|
+
}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type DoctorCheck, type DoctorOptions } from "./doctor-checks.js";
|
|
2
|
+
export * from "./doctor-checks.js";
|
|
3
|
+
export * from "./doctor-checks-plugins.js";
|
|
4
|
+
export declare function buildDoctorChecks(opts?: DoctorOptions): Promise<DoctorCheck[]>;
|
|
5
|
+
export declare function formatDoctorChecks(checks: DoctorCheck[]): string;
|
|
6
|
+
/** `docsxai doctor [<workspace-dir>]` — returns the process exit code. */
|
|
7
|
+
export declare function runDoctor(args: string[]): Promise<number>;
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// `docsxai doctor` — environment + workspace health-check.
|
|
2
|
+
//
|
|
3
|
+
// Prints a ✓/✗ checklist with a one-line fix per failing check (− marks purely
|
|
4
|
+
// informational rows; they never fail doctor). Exits 0 iff everything passes,
|
|
5
|
+
// 1 otherwise. Pure inspection throughout: no flow runs, no plugin code
|
|
6
|
+
// execution, no browser launch — the only network touch is an opt-in GET of
|
|
7
|
+
// the configured backend's /v1/health.
|
|
8
|
+
//
|
|
9
|
+
// The per-check probe catalogue lives in ./doctor-checks.js; this file owns the
|
|
10
|
+
// checklist assembly + CLI entry and re-exports the checks' public surface so
|
|
11
|
+
// every doctor symbol stays importable from ./doctor.js.
|
|
12
|
+
import { checkAuth, checkBackend, checkChromium, checkEnv, checkFlows, checkNode, checkViewer, checkWorkspace, probeChromium, } from "./doctor-checks.js";
|
|
13
|
+
import { checkPlugins } from "./doctor-checks-plugins.js";
|
|
14
|
+
// Re-export the probe catalogue's public surface so importers keep reaching it
|
|
15
|
+
// through ./doctor.js (its original home) — no importer or test changes. The
|
|
16
|
+
// plugin-runtime probe lives in its own sibling but is re-exported here too.
|
|
17
|
+
export * from "./doctor-checks.js";
|
|
18
|
+
export * from "./doctor-checks-plugins.js";
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Assembly + CLI entry
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
export async function buildDoctorChecks(opts = {}) {
|
|
23
|
+
const env = opts.env ?? process.env;
|
|
24
|
+
const now = opts.now ?? Date.now();
|
|
25
|
+
const workspaceDir = opts.workspaceDir ?? process.cwd();
|
|
26
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
27
|
+
const checks = [];
|
|
28
|
+
checks.push(checkNode(opts.nodeVersion ?? process.versions.node));
|
|
29
|
+
checks.push(await checkChromium(opts.chromiumProbe ?? probeChromium));
|
|
30
|
+
const ws = await checkWorkspace(workspaceDir);
|
|
31
|
+
checks.push(ws.check);
|
|
32
|
+
if (ws.present) {
|
|
33
|
+
checks.push(await checkFlows(workspaceDir));
|
|
34
|
+
checks.push(...(await checkAuth(workspaceDir, now)));
|
|
35
|
+
checks.push(await checkBackend(workspaceDir, ws.config, env, fetchImpl));
|
|
36
|
+
checks.push(...(await checkPlugins(workspaceDir)));
|
|
37
|
+
}
|
|
38
|
+
checks.push(await checkViewer(env));
|
|
39
|
+
checks.push(...checkEnv(env));
|
|
40
|
+
return checks;
|
|
41
|
+
}
|
|
42
|
+
export function formatDoctorChecks(checks) {
|
|
43
|
+
let out = "docsxai doctor — environment & workspace health\n\n";
|
|
44
|
+
let allOk = true;
|
|
45
|
+
for (const c of checks) {
|
|
46
|
+
if (!c.ok)
|
|
47
|
+
allOk = false;
|
|
48
|
+
const glyph = c.info ? "−" : c.ok ? "✓" : "✗";
|
|
49
|
+
out += ` ${glyph} ${c.name.padEnd(10)} ${c.detail}\n`;
|
|
50
|
+
if (!c.ok && c.fix)
|
|
51
|
+
out += ` fix: ${c.fix}\n`;
|
|
52
|
+
}
|
|
53
|
+
out += `\n${allOk ? "all checks passed" : "fix the ✗ items above"}\n`;
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
/** `docsxai doctor [<workspace-dir>]` — returns the process exit code. */
|
|
57
|
+
export async function runDoctor(args) {
|
|
58
|
+
const positionals = args.filter((a) => !a.startsWith("--"));
|
|
59
|
+
const checks = await buildDoctorChecks(positionals[0] ? { workspaceDir: positionals[0] } : {});
|
|
60
|
+
process.stdout.write(formatDoctorChecks(checks));
|
|
61
|
+
return checks.every((c) => c.ok) ? 0 : 1;
|
|
62
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export interface AdfMark {
|
|
2
|
+
type: string;
|
|
3
|
+
attrs?: Record<string, unknown>;
|
|
4
|
+
}
|
|
5
|
+
export interface AdfNode {
|
|
6
|
+
type: string;
|
|
7
|
+
attrs?: Record<string, unknown>;
|
|
8
|
+
marks?: AdfMark[];
|
|
9
|
+
content?: AdfNode[];
|
|
10
|
+
text?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface AdfDoc {
|
|
13
|
+
version: 1;
|
|
14
|
+
type: "doc";
|
|
15
|
+
content: AdfNode[];
|
|
16
|
+
}
|
|
17
|
+
export type AdfExportMode = "single" | "page-tree";
|
|
18
|
+
export interface AdfAttachment {
|
|
19
|
+
/** Unique-within-document upload name, `<flow>--<step>.png`. */
|
|
20
|
+
fileName: string;
|
|
21
|
+
/** Absolute path of the source PNG inside the workspace. */
|
|
22
|
+
sourcePath: string;
|
|
23
|
+
/** sha256 (hex) of the source bytes — the publisher's skip-unchanged key. */
|
|
24
|
+
sha256: string;
|
|
25
|
+
}
|
|
26
|
+
export interface AdfDocument {
|
|
27
|
+
/** Page-identity key: `project` for the consolidated/parent page, the flow name for children. */
|
|
28
|
+
section: string;
|
|
29
|
+
title: string;
|
|
30
|
+
adf: AdfDoc;
|
|
31
|
+
attachments: AdfAttachment[];
|
|
32
|
+
}
|
|
33
|
+
export interface AdfProjection {
|
|
34
|
+
schema: "docsxai/adf-projection@1";
|
|
35
|
+
mode: AdfExportMode;
|
|
36
|
+
documents: AdfDocument[];
|
|
37
|
+
warnings: string[];
|
|
38
|
+
}
|
|
39
|
+
export interface AdfExportOptions {
|
|
40
|
+
mode?: AdfExportMode;
|
|
41
|
+
/** Title of the consolidated page (`single`) / the parent page (`page-tree`). Default: "Site documentation". */
|
|
42
|
+
title?: string;
|
|
43
|
+
}
|
|
44
|
+
/** Inline markdown → ADF text nodes, accumulating marks through nesting (bold inside link, …). */
|
|
45
|
+
export declare function inlineMarkdownToAdf(source: string, marks?: AdfMark[]): AdfNode[];
|
|
46
|
+
/** Block-level markdown (subset) → ADF block nodes. */
|
|
47
|
+
export declare function markdownToAdf(markdown: string): AdfNode[];
|
|
48
|
+
/**
|
|
49
|
+
* Project a workspace's doc pack into Confluence-ready ADF documents. Pure file → JSON
|
|
50
|
+
* transform: deterministic for a given doc pack, performs no HTTP, and never writes.
|
|
51
|
+
*/
|
|
52
|
+
export declare function projectDocPackToAdf(opts: {
|
|
53
|
+
workspaceDir: string;
|
|
54
|
+
/** Restrict to these flow names (default: every `flows/*.flow.yaml`). Unknown names throw. */
|
|
55
|
+
flows?: string[];
|
|
56
|
+
options?: AdfExportOptions;
|
|
57
|
+
}): Promise<AdfProjection>;
|