@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,403 @@
1
+ // Calibration-aid commands — the static / inspection tools an engineer leans on while hand-authoring
2
+ // or fixing a flow-file. None of them mutate the doc pack; they read, check, and report:
3
+ // inspect — open the app (cached session loaded) and dump [data-testid]s for pinning locators
4
+ // lint — pure-static flow-file checks (R001…R004 + plugin-contributed rules)
5
+ // flow-tree — the extends graph, orphans, and resolution issues
6
+ // diagnose — halt context + recommendations for one step (optional live --cdp probe)
7
+ // style — init/validate docs/style.yaml; --check scans prose for jargon leaks
8
+ import { promises as fs } from "node:fs";
9
+ import * as path from "node:path";
10
+ import { LocalStorageStateCache, parseAuthStrategyFile } from "./auth.js";
11
+ import { FlowFileError, parseFlowFile, resolveFlowExtends } from "./flow-file.js";
12
+ import { buildDiagnoseReport, formatReportText, probeLive, } from "./diagnose.js";
13
+ import { formatIssuesText, lintFlow } from "./flow-lint.js";
14
+ import { buildFlowTree, formatTreeText } from "./flow-tree.js";
15
+ import { formatJargonHitsText, initStyleIfAbsent, loadStyle, scanWorkspaceForJargon, StyleError, writeStyle, } from "./style.js";
16
+ import { readPluginsLock, readWorkspacePluginsConfig } from "./plugins/lock.js";
17
+ import { resolvePlugins } from "./plugins/runtime.js";
18
+ import { launchPlaywrightSession } from "./playwright-driver.js";
19
+ import { loadWorkspaceConfig, resolveWorkspacePath } from "./workspace.js";
20
+ import { listFlowFiles, parseFlags } from "./cli-shared.js";
21
+ import { USAGE } from "./cli-usage.js";
22
+ export async function cmdInspect(args) {
23
+ const { positionals, flags } = parseFlags(args);
24
+ const workspaceDir = positionals[0];
25
+ if (!workspaceDir) {
26
+ process.stderr.write("inspect: missing <workspace-dir>\n\n" + USAGE + "\n");
27
+ return 2;
28
+ }
29
+ const wsCfg = await loadWorkspaceConfig(workspaceDir);
30
+ const cdp = typeof flags.get("cdp") === "string" ? flags.get("cdp") : undefined;
31
+ const explicitUrl = typeof flags.get("url") === "string" ? flags.get("url") : undefined;
32
+ const url = explicitUrl ?? wsCfg?.app_url;
33
+ if (!cdp && !url) {
34
+ process.stderr.write("inspect: no URL — pass --url <url>, set app_url in .docsxai.json, or use --cdp <endpoint>\n");
35
+ return 2;
36
+ }
37
+ const selector = typeof flags.get("selector") === "string" ? flags.get("selector") : undefined;
38
+ const headed = flags.get("headed") === true;
39
+ const ignoreHTTPSErrors = flags.get("ignore-https-errors") === true || !!wsCfg?.ignore_https_errors;
40
+ const waitForSel = typeof flags.get("wait-for") === "string" ? flags.get("wait-for") : undefined;
41
+ const waitMs = typeof flags.get("wait") === "string" ? Math.max(0, Number(flags.get("wait")) || 0) : 800;
42
+ let storageState;
43
+ if (!cdp) {
44
+ let role = typeof flags.get("role") === "string" ? flags.get("role") : undefined;
45
+ if (!role) {
46
+ try {
47
+ role = parseAuthStrategyFile(await fs.readFile(resolveWorkspacePath(workspaceDir, "auth", "strategy.yaml"), "utf8")).default_role;
48
+ }
49
+ catch {
50
+ role = "editor";
51
+ }
52
+ }
53
+ storageState =
54
+ (await new LocalStorageStateCache(resolveWorkspacePath(workspaceDir, ".auth")).load(role)) ??
55
+ undefined;
56
+ if (!storageState) {
57
+ process.stderr.write(`inspect: no valid cached session for role "${role}" — inspecting unauthenticated (\`docsxai capture-auth\` first if the app needs login)\n`);
58
+ }
59
+ }
60
+ let session;
61
+ try {
62
+ session = await launchPlaywrightSession({
63
+ ...(cdp
64
+ ? { connectOverCdp: cdp }
65
+ : {
66
+ ...(url ? { baseURL: url } : {}),
67
+ headed,
68
+ ignoreHTTPSErrors,
69
+ ...(storageState ? { storageState } : {}),
70
+ }),
71
+ docPackRoot: workspaceDir,
72
+ });
73
+ }
74
+ catch (e) {
75
+ const msg = e.message;
76
+ if (/Executable doesn't exist|playwright install/i.test(msg)) {
77
+ process.stderr.write("inspect: no Chromium binary — `npx playwright-core install chromium` (source checkout: `pnpm -C packages/engine exec playwright-core install chromium`)\n");
78
+ return 1;
79
+ }
80
+ process.stderr.write(`inspect: failed to launch browser: ${msg}\n`);
81
+ return 1;
82
+ }
83
+ try {
84
+ // In CDP-attach mode without an explicit --url, inspect whatever the attached browser already has open.
85
+ if (url && (!cdp || explicitUrl))
86
+ await session.page.goto(url, { waitUntil: "domcontentloaded" }).catch(() => undefined);
87
+ if (waitForSel)
88
+ await session.page.waitForSelector(waitForSel, { timeout: 30_000 }).catch(() => undefined);
89
+ else if (waitMs > 0)
90
+ await session.page.waitForTimeout(waitMs);
91
+ process.stdout.write(`inspect: ${session.page.url()}\n title: ${await session.page.title().catch(() => "(?)")}\n`);
92
+ if (selector) {
93
+ const els = await session.page.locator(selector).all();
94
+ process.stdout.write(` ${els.length} element(s) matching ${selector} (first 20):\n`);
95
+ for (const el of els.slice(0, 20)) {
96
+ const html = (await el.evaluate((e) => e.outerHTML).catch(() => ""))
97
+ .replace(/\s+/g, " ")
98
+ .slice(0, 400);
99
+ process.stdout.write(` ${html}\n`);
100
+ }
101
+ }
102
+ else {
103
+ const items = await session.page
104
+ .$$eval("[data-testid]", (els) => els.map((e) => ({
105
+ testid: e.getAttribute("data-testid") ?? "",
106
+ tag: e.tagName.toLowerCase(),
107
+ text: (e.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 60),
108
+ visible: typeof e.checkVisibility ===
109
+ "function"
110
+ ? e.checkVisibility()
111
+ : e.offsetParent != null,
112
+ })))
113
+ .catch(() => []);
114
+ process.stdout.write(` ${items.length} [data-testid] element(s) (✓ = visible) — pin these as locators:\n`);
115
+ for (const it of items) {
116
+ process.stdout.write(` ${it.visible ? "✓" : " "} [data-testid="${it.testid}"] <${it.tag}> ${it.text ? `"${it.text}"` : ""}\n`);
117
+ }
118
+ process.stdout.write(` (--selector '<css>' dumps matching elements' HTML; --url <url> for a sub-page; --wait <ms> / --wait-for '<css>' to settle a slow SPA before snapshot; --cdp <endpoint> to attach to a running Chrome instead of launching; --headed to open it)\n`);
119
+ }
120
+ if (headed && !cdp) {
121
+ process.stdout.write("inspect: browser is open — close it to exit.\n");
122
+ await new Promise((resolve) => session.browser.on("disconnected", () => resolve()));
123
+ }
124
+ return 0;
125
+ }
126
+ catch (e) {
127
+ process.stderr.write(`inspect: ${e.message}\n`);
128
+ return 1;
129
+ }
130
+ finally {
131
+ if (!headed)
132
+ await session.close();
133
+ }
134
+ }
135
+ export async function cmdLint(args) {
136
+ const { positionals, flags } = parseFlags(args);
137
+ if (!positionals[0]) {
138
+ process.stderr.write(`lint: missing <workspace-dir>\n`);
139
+ return 2;
140
+ }
141
+ const projectDir = positionals[0];
142
+ const flowFilter = typeof flags.get("flow") === "string" ? flags.get("flow") : undefined;
143
+ const format = typeof flags.get("format") === "string" ? flags.get("format") : "text";
144
+ if (format !== "text" && format !== "json") {
145
+ process.stderr.write(`lint: --format must be "text" or "json"\n`);
146
+ return 2;
147
+ }
148
+ let flowPaths;
149
+ try {
150
+ flowPaths = await listFlowFiles(projectDir);
151
+ }
152
+ catch (e) {
153
+ process.stderr.write(`lint: ${e.message}\n`);
154
+ return 2;
155
+ }
156
+ const flowsByName = new Map();
157
+ for (const p of flowPaths) {
158
+ try {
159
+ const text = await fs.readFile(p, "utf8");
160
+ const flow = parseFlowFile(text, path.basename(p));
161
+ flowsByName.set(flow.name, flow);
162
+ }
163
+ catch (e) {
164
+ const msg = e instanceof FlowFileError ? e.message : e.message;
165
+ process.stderr.write(`lint: parse error in ${p}: ${msg}\n`);
166
+ return 1;
167
+ }
168
+ }
169
+ const targets = flowFilter
170
+ ? flowsByName.has(flowFilter)
171
+ ? [flowsByName.get(flowFilter)]
172
+ : []
173
+ : Array.from(flowsByName.values());
174
+ if (flowFilter && targets.length === 0) {
175
+ process.stderr.write(`lint: flow not found: ${flowFilter}\n`);
176
+ return 2;
177
+ }
178
+ const loadFlow = (name) => {
179
+ const f = flowsByName.get(name);
180
+ if (!f)
181
+ throw new Error(`extends target not found: ${name}`);
182
+ return f;
183
+ };
184
+ // Lint-rule plugins: resolve the workspace's plugin registry and feed registered
185
+ // rules through lintFlow's extraRules. A plugin-resolution failure degrades to
186
+ // core rules with a warning — lint must stay usable while a plugin is broken.
187
+ let extraRules = [];
188
+ try {
189
+ const cfg = await readWorkspacePluginsConfig(projectDir);
190
+ if (cfg.sources.length > 0) {
191
+ const lock = await readPluginsLock(projectDir);
192
+ const registry = await resolvePlugins({
193
+ workspaceDir: projectDir,
194
+ sources: cfg.sources,
195
+ enabledCapabilities: cfg.capabilities,
196
+ lock,
197
+ });
198
+ extraRules = registry.getLintRules();
199
+ }
200
+ }
201
+ catch (e) {
202
+ process.stderr.write(`lint: plugin rules skipped — ${e.message}\n`);
203
+ }
204
+ const issues = [];
205
+ for (const flow of targets) {
206
+ const result = await lintFlow(flow, { loadFlow, extraRules });
207
+ issues.push(...result);
208
+ }
209
+ if (format === "json") {
210
+ process.stdout.write(JSON.stringify(issues, null, 2) + "\n");
211
+ }
212
+ else {
213
+ process.stdout.write(formatIssuesText(issues));
214
+ }
215
+ return issues.some((i) => i.severity === "error" || i.severity === "warning") ? 1 : 0;
216
+ }
217
+ export async function cmdFlowTree(args) {
218
+ const { positionals, flags } = parseFlags(args);
219
+ if (!positionals[0]) {
220
+ process.stderr.write(`flow-tree: missing <workspace-dir>\n`);
221
+ return 2;
222
+ }
223
+ const projectDir = positionals[0];
224
+ const format = typeof flags.get("format") === "string" ? flags.get("format") : "text";
225
+ if (format !== "text" && format !== "json") {
226
+ process.stderr.write(`flow-tree: --format must be "text" or "json"\n`);
227
+ return 2;
228
+ }
229
+ let flowPaths;
230
+ try {
231
+ flowPaths = await listFlowFiles(projectDir);
232
+ }
233
+ catch (e) {
234
+ process.stderr.write(`flow-tree: ${e.message}\n`);
235
+ return 2;
236
+ }
237
+ const flowsByName = new Map();
238
+ for (const p of flowPaths) {
239
+ try {
240
+ const text = await fs.readFile(p, "utf8");
241
+ const flow = parseFlowFile(text, path.basename(p));
242
+ flowsByName.set(flow.name, flow);
243
+ }
244
+ catch (e) {
245
+ const msg = e instanceof FlowFileError ? e.message : e.message;
246
+ process.stderr.write(`flow-tree: parse error in ${p}: ${msg}\n`);
247
+ return 1;
248
+ }
249
+ }
250
+ const tree = await buildFlowTree(flowsByName);
251
+ if (format === "json") {
252
+ process.stdout.write(JSON.stringify(tree, null, 2) + "\n");
253
+ }
254
+ else {
255
+ process.stdout.write(formatTreeText(tree));
256
+ }
257
+ return tree.issues.length > 0 || tree.orphans.length > 0 ? 1 : 0;
258
+ }
259
+ export async function cmdDiagnose(args) {
260
+ const { positionals, flags } = parseFlags(args);
261
+ if (!positionals[0]) {
262
+ process.stderr.write(`diagnose: missing <workspace-dir>\n`);
263
+ return 2;
264
+ }
265
+ const projectDir = positionals[0];
266
+ const flowName = typeof flags.get("flow") === "string" ? flags.get("flow") : undefined;
267
+ const stepId = typeof flags.get("step") === "string" ? flags.get("step") : undefined;
268
+ const cdpEndpoint = typeof flags.get("cdp") === "string" ? flags.get("cdp") : undefined;
269
+ const format = typeof flags.get("format") === "string" ? flags.get("format") : "text";
270
+ if (!flowName) {
271
+ process.stderr.write(`diagnose: --flow <name> required\n`);
272
+ return 2;
273
+ }
274
+ if (!stepId) {
275
+ process.stderr.write(`diagnose: --step <step-id> required\n`);
276
+ return 2;
277
+ }
278
+ if (format !== "text" && format !== "json") {
279
+ process.stderr.write(`diagnose: --format must be "text" or "json"\n`);
280
+ return 2;
281
+ }
282
+ // Load the flow (resolving `extends` so step lookup works against the merged step list).
283
+ const loadFlowFile = async (name) => {
284
+ const fp = resolveWorkspacePath(projectDir, "flows", `${name}.flow.yaml`);
285
+ let text;
286
+ try {
287
+ text = await fs.readFile(fp, "utf8");
288
+ }
289
+ catch {
290
+ throw new FlowFileError(`no flow named "${name}" at ${fp}`);
291
+ }
292
+ return parseFlowFile(text, fp);
293
+ };
294
+ let flow;
295
+ try {
296
+ const parsed = await loadFlowFile(flowName);
297
+ flow = parsed.extends ? await resolveFlowExtends(parsed, loadFlowFile) : parsed;
298
+ }
299
+ catch (e) {
300
+ const msg = e instanceof FlowFileError ? e.message : e.message;
301
+ process.stderr.write(`diagnose: ${msg}\n`);
302
+ return 1;
303
+ }
304
+ const step = flow.steps.find((s) => s.id === stepId);
305
+ if (!step) {
306
+ process.stderr.write(`diagnose: no step "${stepId}" in flow "${flowName}" (merged step list: ${flow.steps.map((s) => s.id).join(", ")})\n`);
307
+ return 1;
308
+ }
309
+ const resolvedSelector = step.target
310
+ ? step.target.startsWith("$")
311
+ ? (flow.locators[step.target.slice(1)] ?? step.target)
312
+ : step.target
313
+ : undefined;
314
+ const haltScreenshotAbsPath = resolveWorkspacePath(projectDir, "docs", flowName, "halts", `${stepId}.png`);
315
+ // Optional live probe via --cdp.
316
+ let liveProbe;
317
+ let liveSession;
318
+ if (cdpEndpoint && resolvedSelector) {
319
+ liveProbe = async () => {
320
+ liveSession = await launchPlaywrightSession({
321
+ connectOverCdp: cdpEndpoint,
322
+ docPackRoot: projectDir,
323
+ });
324
+ return probeLive(liveSession.driver, resolvedSelector, cdpEndpoint);
325
+ };
326
+ }
327
+ let report;
328
+ try {
329
+ report = await buildDiagnoseReport({
330
+ workspace: projectDir,
331
+ flow,
332
+ step,
333
+ ...(resolvedSelector ? { resolvedSelector } : {}),
334
+ haltScreenshotAbsPath,
335
+ ...(liveProbe ? { liveProbe } : {}),
336
+ });
337
+ }
338
+ catch (e) {
339
+ process.stderr.write(`diagnose: live probe failed: ${e.message}\n`);
340
+ return 1;
341
+ }
342
+ finally {
343
+ if (liveSession)
344
+ await liveSession.close();
345
+ }
346
+ if (format === "json") {
347
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
348
+ }
349
+ else {
350
+ process.stdout.write(formatReportText(report));
351
+ }
352
+ return 0;
353
+ }
354
+ export async function cmdStyle(args) {
355
+ const { positionals, flags } = parseFlags(args);
356
+ if (!positionals[0]) {
357
+ process.stderr.write(`style: missing <workspace-dir>\n`);
358
+ return 2;
359
+ }
360
+ const projectDir = positionals[0];
361
+ const check = flags.get("check") === true;
362
+ const format = typeof flags.get("format") === "string" ? flags.get("format") : "text";
363
+ if (format !== "text" && format !== "json") {
364
+ process.stderr.write(`style: --format must be "text" or "json"\n`);
365
+ return 2;
366
+ }
367
+ // init-if-absent (idempotent); then load + validate; then rederive JSON; then optional jargon check.
368
+ const { created } = await initStyleIfAbsent(projectDir);
369
+ let style;
370
+ try {
371
+ style = await loadStyle(projectDir);
372
+ }
373
+ catch (e) {
374
+ if (e instanceof StyleError) {
375
+ process.stderr.write(`style: ${e.message}\n`);
376
+ return 1;
377
+ }
378
+ throw e;
379
+ }
380
+ if (!style) {
381
+ // Shouldn't happen after initStyleIfAbsent, but defensive.
382
+ process.stderr.write(`style: failed to initialise style.yaml in ${projectDir}\n`);
383
+ return 1;
384
+ }
385
+ // Always rewrite to ensure derived JSON stays in sync with YAML (idempotent).
386
+ const paths = await writeStyle(projectDir, style);
387
+ let hits = [];
388
+ if (check) {
389
+ hits = await scanWorkspaceForJargon(projectDir, style);
390
+ }
391
+ if (format === "json") {
392
+ process.stdout.write(JSON.stringify({ style, paths, created, jargonLeaks: check ? hits : undefined }, null, 2) +
393
+ "\n");
394
+ }
395
+ else {
396
+ process.stdout.write(`style: ${created ? "created" : "validated"} ${path.relative(projectDir, paths.yamlPath)}; rederived ${path.relative(projectDir, paths.jsonPath)}\n`);
397
+ if (check) {
398
+ process.stdout.write("\n");
399
+ process.stdout.write(formatJargonHitsText(hits));
400
+ }
401
+ }
402
+ return check && hits.length > 0 ? 1 : 0;
403
+ }
@@ -0,0 +1,5 @@
1
+ export declare function cmdLogin(args: string[]): Promise<number>;
2
+ export declare function cmdPush(args: string[]): Promise<number>;
3
+ export declare function cmdPull(args: string[]): Promise<number>;
4
+ /** `docsxai plugins …` — forwards to the plugin-runtime CLI verbatim. */
5
+ export declare function cmdPlugins(args: string[]): Promise<number>;
@@ -0,0 +1,211 @@
1
+ // Backend / sync commands — everything that talks to `@docsxai/backend` over HTTP, plus the plugins
2
+ // subcommand delegation:
3
+ // login — validate a bearer token (or run the OAuth 2.1 + PKCE flow with --oauth)
4
+ // push — serialise the doc pack and POST it as a new revision (binds the workspace on first push)
5
+ // pull — fetch a revision's artifacts back into the workspace files
6
+ // plugins — forwards to plugins-cli (list | info | sync over the plugin runtime; no plugin code runs)
7
+ import { promises as fs } from "node:fs";
8
+ import * as path from "node:path";
9
+ import { BackendClient, BackendClientError, createBackendClient, oauthLogin, saveBackendTokenFile, } from "./backend-client.js";
10
+ import { fetchScreenshotBlobs, readDocPack, uploadScreenshotBlobs, writeDocPack, } from "./doc-pack-io.js";
11
+ import { pluginsCli } from "./plugins-cli.js";
12
+ import { loadWorkspaceConfig, resolveWorkspacePath } from "./workspace.js";
13
+ import { parseFlags } from "./cli-shared.js";
14
+ export async function cmdLogin(args) {
15
+ const { positionals, flags } = parseFlags(args);
16
+ const backendUrl = typeof flags.get("backend-url") === "string" ? flags.get("backend-url") : undefined;
17
+ if (!backendUrl) {
18
+ process.stderr.write(`login: --backend-url <url> required\n`);
19
+ return 2;
20
+ }
21
+ const oauthFlag = flags.get("oauth");
22
+ if (oauthFlag !== undefined) {
23
+ // OAuth 2.1 authorization-code + PKCE against the backend's authorization server. Tokens land
24
+ // at <workspace>/.auth/backend-token.json (mode 0600); push/pull/run pick them up from there.
25
+ // The flag parser hands `--oauth <dir>` the dir as the flag value; bare `--oauth` reads it
26
+ // from the positional.
27
+ const workspaceDir = typeof oauthFlag === "string" ? oauthFlag : positionals[0];
28
+ if (!workspaceDir) {
29
+ process.stderr.write(`login: --oauth requires a <workspace-dir> (tokens are stored at <workspace>/.auth/backend-token.json)\n`);
30
+ return 2;
31
+ }
32
+ try {
33
+ const tokens = await oauthLogin({
34
+ backendUrl,
35
+ onAuthorizeUrl: (u) => {
36
+ process.stdout.write(`login: open this URL in your browser to authorize:\n ${u}\n`);
37
+ },
38
+ });
39
+ const storedAt = await saveBackendTokenFile(workspaceDir, tokens);
40
+ process.stdout.write(`login: ok. tokens stored at ${storedAt} (access token expires ${new Date(tokens.expires_at).toISOString()})\n`);
41
+ return 0;
42
+ }
43
+ catch (e) {
44
+ process.stderr.write(`login: ${e.message}\n`);
45
+ return 1;
46
+ }
47
+ }
48
+ if (!process.env.DOCSX_TOKEN) {
49
+ process.stderr.write(`login: DOCSX_TOKEN env var not set. Export it before running: DOCSX_TOKEN=<token> docsxai login --backend-url ${backendUrl}\n`);
50
+ return 2;
51
+ }
52
+ let client;
53
+ try {
54
+ client = new BackendClient({ baseUrl: backendUrl });
55
+ }
56
+ catch (e) {
57
+ process.stderr.write(`login: ${e.message}\n`);
58
+ return 1;
59
+ }
60
+ try {
61
+ const h = await client.health();
62
+ if (!h.ok) {
63
+ process.stderr.write(`login: backend health-check returned ok=false\n`);
64
+ return 1;
65
+ }
66
+ const wss = await client.listWorkspaces();
67
+ process.stdout.write(`login: ok. ${wss.length} workspace${wss.length !== 1 ? "s" : ""} visible at ${backendUrl}\n`);
68
+ return 0;
69
+ }
70
+ catch (e) {
71
+ if (e instanceof BackendClientError) {
72
+ process.stderr.write(`login: ${e.message}\n`);
73
+ return 1;
74
+ }
75
+ throw e;
76
+ }
77
+ }
78
+ /** Ensure the workspace has a backend workspace + project to push to; create them on first push. */
79
+ async function ensureBackendBinding(client, projectDir, cfg, workspaceName) {
80
+ let wsId = cfg.backend_workspace_id;
81
+ let projectId = cfg.backend_project_id;
82
+ let createdAny = false;
83
+ if (!wsId) {
84
+ const ws = await client.createWorkspace(workspaceName);
85
+ wsId = ws.id;
86
+ createdAny = true;
87
+ }
88
+ if (!projectId) {
89
+ const proj = await client.createProject(wsId, workspaceName);
90
+ projectId = proj.id;
91
+ createdAny = true;
92
+ }
93
+ return { wsId, projectId, createdAny };
94
+ }
95
+ export async function cmdPush(args) {
96
+ const { positionals, flags } = parseFlags(args);
97
+ if (!positionals[0]) {
98
+ process.stderr.write(`push: missing <workspace-dir>\n`);
99
+ return 2;
100
+ }
101
+ const projectDir = positionals[0];
102
+ const wsCfg = await loadWorkspaceConfig(projectDir);
103
+ if (!wsCfg?.backend_url) {
104
+ process.stderr.write(`push: no backend_url in ${path.join(projectDir, ".docsxai.json")}. Set it before pushing.\n`);
105
+ return 2;
106
+ }
107
+ const kindArg = typeof flags.get("kind") === "string" ? flags.get("kind") : "calibrate";
108
+ if (kindArg !== "calibrate" && kindArg !== "run" && kindArg !== "edit") {
109
+ process.stderr.write(`push: --kind must be calibrate | run | edit (got "${kindArg}")\n`);
110
+ return 2;
111
+ }
112
+ const author = (typeof flags.get("author") === "string" ? flags.get("author") : null) ??
113
+ process.env.USER ??
114
+ "unknown";
115
+ let client;
116
+ try {
117
+ client = await createBackendClient({ baseUrl: wsCfg.backend_url, workspaceDir: projectDir });
118
+ }
119
+ catch (e) {
120
+ process.stderr.write(`push: ${e.message}\n`);
121
+ return 1;
122
+ }
123
+ try {
124
+ const binding = await ensureBackendBinding(client, projectDir, wsCfg, path.basename(path.resolve(projectDir)));
125
+ if (binding.createdAny) {
126
+ // Persist the new IDs back to .docsxai.json so subsequent push/pull don't re-create.
127
+ const updated = {
128
+ ...wsCfg,
129
+ backend_workspace_id: binding.wsId,
130
+ backend_project_id: binding.projectId,
131
+ };
132
+ await fs.writeFile(resolveWorkspacePath(projectDir, ".docsxai.json"), JSON.stringify(updated, null, 2) + "\n", "utf8");
133
+ }
134
+ const rev = await client.createRevision(binding.wsId, binding.projectId, {
135
+ kind: kindArg,
136
+ author,
137
+ });
138
+ const payloads = await readDocPack(projectDir);
139
+ if (payloads.screenshots) {
140
+ // Screenshot bytes go up as content-addressed blobs (HEAD-probed, so unchanged PNGs are
141
+ // skipped); the artifact slot carries only the sha256 manifest.
142
+ const { uploaded, skipped } = await uploadScreenshotBlobs(projectDir, payloads.screenshots, client);
143
+ process.stdout.write(`push: screenshots — ${uploaded} blob(s) uploaded, ${skipped} already on the backend\n`);
144
+ }
145
+ let pushed = 0;
146
+ for (const [key, p] of Object.entries(payloads)) {
147
+ if (p === null)
148
+ continue;
149
+ await client.putArtifact(binding.wsId, binding.projectId, rev.id, key, p);
150
+ pushed++;
151
+ }
152
+ await client.finalizeRevision(binding.wsId, binding.projectId, rev.id);
153
+ process.stdout.write(`push: revision ${rev.id} (${kindArg}, ${author}) — ${pushed} artifact slot${pushed !== 1 ? "s" : ""} uploaded, finalized\n`);
154
+ return 0;
155
+ }
156
+ catch (e) {
157
+ if (e instanceof BackendClientError) {
158
+ process.stderr.write(`push: ${e.message}\n`);
159
+ return 1;
160
+ }
161
+ throw e;
162
+ }
163
+ }
164
+ export async function cmdPull(args) {
165
+ const { positionals, flags } = parseFlags(args);
166
+ if (!positionals[0]) {
167
+ process.stderr.write(`pull: missing <workspace-dir>\n`);
168
+ return 2;
169
+ }
170
+ const projectDir = positionals[0];
171
+ const wsCfg = await loadWorkspaceConfig(projectDir);
172
+ if (!wsCfg?.backend_url || !wsCfg.backend_workspace_id || !wsCfg.backend_project_id) {
173
+ process.stderr.write(`pull: workspace isn't bound to a backend yet. Run \`push\` first (or hand-edit .docsxai.json's backend_workspace_id / backend_project_id).\n`);
174
+ return 2;
175
+ }
176
+ const revArg = typeof flags.get("rev") === "string" ? flags.get("rev") : "head";
177
+ let client;
178
+ try {
179
+ client = await createBackendClient({ baseUrl: wsCfg.backend_url, workspaceDir: projectDir });
180
+ }
181
+ catch (e) {
182
+ process.stderr.write(`pull: ${e.message}\n`);
183
+ return 1;
184
+ }
185
+ try {
186
+ const rev = await client.getRevision(wsCfg.backend_workspace_id, wsCfg.backend_project_id, revArg);
187
+ const payloads = {};
188
+ for (const artifact of rev.artifacts) {
189
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
190
+ payloads[artifact] = await client.getArtifact(wsCfg.backend_workspace_id, wsCfg.backend_project_id, rev.id, artifact);
191
+ }
192
+ // The screenshots artifact is a sha256 manifest — fetch the bytes behind it (integrity-checked).
193
+ const screenshotBytes = payloads.screenshots
194
+ ? await fetchScreenshotBlobs(payloads.screenshots, client)
195
+ : undefined;
196
+ const r = await writeDocPack(projectDir, payloads, screenshotBytes ? { screenshotBytes } : {});
197
+ process.stdout.write(`pull: revision ${rev.id} (${rev.kind}, ${rev.author}) — wrote ${r.filesWritten} file(s)\n`);
198
+ return 0;
199
+ }
200
+ catch (e) {
201
+ if (e instanceof BackendClientError) {
202
+ process.stderr.write(`pull: ${e.message}\n`);
203
+ return 1;
204
+ }
205
+ throw e;
206
+ }
207
+ }
208
+ /** `docsxai plugins …` — forwards to the plugin-runtime CLI verbatim. */
209
+ export async function cmdPlugins(args) {
210
+ return pluginsCli(args);
211
+ }
@@ -0,0 +1,5 @@
1
+ export declare function cmdRender(args: string[]): Promise<number>;
2
+ export declare function cmdZip(args: string[]): Promise<number>;
3
+ export declare function cmdExport(args: string[]): Promise<number>;
4
+ export declare function cmdBaseline(args: string[]): Promise<number>;
5
+ export declare function cmdDiff(args: string[]): Promise<number>;