@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,280 @@
|
|
|
1
|
+
// Doc-pack ops — the commands that package, render, snapshot, and compare the produced doc pack.
|
|
2
|
+
// They read the rendered artifacts (flows/docs/screenshots) and emit something derived; none of
|
|
3
|
+
// them drive a live browser:
|
|
4
|
+
// render — build the static viewer by spawning the docsxai-viewer bin
|
|
5
|
+
// zip — deterministic single-archive hand-off bundle
|
|
6
|
+
// export — project the doc pack to ADF (Confluence) or self-contained Playwright specs
|
|
7
|
+
// baseline — snapshot the doc pack into .baseline/ as the "before" for diff
|
|
8
|
+
// diff — drift report against a baseline (the after-vs-before)
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { promises as fs } from "node:fs";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { FlowFileError } from "./flow-file.js";
|
|
13
|
+
import { diffDocPacks, formatDriftReportMarkdown, formatDriftReportText, severityAtLeast, } from "./diff.js";
|
|
14
|
+
import { projectDocPackToAdf } from "./export/adf.js";
|
|
15
|
+
import { exportWorkspaceFlowsAsPlaywrightTests } from "./export/playwright-test.js";
|
|
16
|
+
import { formatViewerBinFailure, resolveViewerBin } from "./viewer-bin.js";
|
|
17
|
+
import { ZipError, zipDocPack } from "./zip.js";
|
|
18
|
+
import { resolveWorkspacePath } from "./workspace.js";
|
|
19
|
+
import { parseFlags } from "./cli-shared.js";
|
|
20
|
+
import { USAGE } from "./cli-usage.js";
|
|
21
|
+
export async function cmdRender(args) {
|
|
22
|
+
const { positionals } = parseFlags(args);
|
|
23
|
+
const projectDir = positionals[0];
|
|
24
|
+
if (!projectDir) {
|
|
25
|
+
process.stderr.write("render: missing <project-dir>\n\n" + USAGE + "\n");
|
|
26
|
+
return 2;
|
|
27
|
+
}
|
|
28
|
+
const docsDir = resolveWorkspacePath(projectDir, "docs");
|
|
29
|
+
const outDir = resolveWorkspacePath(projectDir, ".viewer");
|
|
30
|
+
// The viewer is its own package/bin; spawn it so the engine doesn't depend on it at build time.
|
|
31
|
+
const viewerBin = await resolveViewerBin();
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const child = spawn(viewerBin.command, [...viewerBin.prefixArgs, "build", docsDir, outDir], {
|
|
34
|
+
stdio: "inherit",
|
|
35
|
+
});
|
|
36
|
+
child.on("error", (e) => {
|
|
37
|
+
if (e.code === "ENOENT") {
|
|
38
|
+
process.stderr.write(`render: ${formatViewerBinFailure(viewerBin)}\n`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
process.stderr.write(`render: ${e.message}\n`);
|
|
42
|
+
}
|
|
43
|
+
resolve(1);
|
|
44
|
+
});
|
|
45
|
+
child.on("exit", (code) => {
|
|
46
|
+
if ((code ?? 1) === 0) {
|
|
47
|
+
process.stdout.write(`render: open ${path.join(outDir, "index.html")} (the index links the flows; each flow page shows the screenshots — hover a pulsing halo to read its callout)\n`);
|
|
48
|
+
}
|
|
49
|
+
resolve(code ?? 1);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export async function cmdZip(args) {
|
|
54
|
+
const { positionals, flags } = parseFlags(args);
|
|
55
|
+
if (!positionals[0]) {
|
|
56
|
+
process.stderr.write(`zip: missing <workspace-dir>\n`);
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
59
|
+
const projectDir = positionals[0];
|
|
60
|
+
const output = typeof flags.get("out") === "string"
|
|
61
|
+
? flags.get("out")
|
|
62
|
+
: path.join(process.cwd(), `${path.basename(path.resolve(projectDir))}.zip`);
|
|
63
|
+
const includeViewer = flags.get("include-viewer") === true;
|
|
64
|
+
try {
|
|
65
|
+
const r = await zipDocPack({ workspace: projectDir, output, includeViewer });
|
|
66
|
+
const kb = (r.bytes / 1024).toFixed(1);
|
|
67
|
+
process.stdout.write(`zip: wrote ${r.output} (${r.entries.length} entries, ${kb} KB)\n`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
if (e instanceof ZipError) {
|
|
72
|
+
process.stderr.write(`zip: ${e.message}\n`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
throw e;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
export async function cmdExport(args) {
|
|
79
|
+
const [format, ...rest] = args;
|
|
80
|
+
switch (format) {
|
|
81
|
+
case "adf":
|
|
82
|
+
return cmdExportAdf(rest);
|
|
83
|
+
case "playwright":
|
|
84
|
+
return cmdExportPlaywright(rest);
|
|
85
|
+
default:
|
|
86
|
+
process.stderr.write(`export: unknown format "${format ?? ""}" — supported: adf, playwright\n`);
|
|
87
|
+
return 2;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function cmdExportAdf(rest) {
|
|
91
|
+
const { positionals, flags } = parseFlags(rest);
|
|
92
|
+
if (!positionals[0]) {
|
|
93
|
+
process.stderr.write(`export adf: missing <workspace-dir>\n`);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
const projectDir = positionals[0];
|
|
97
|
+
const modeFlag = flags.get("mode");
|
|
98
|
+
let mode = "single";
|
|
99
|
+
if (typeof modeFlag === "string") {
|
|
100
|
+
if (modeFlag !== "single" && modeFlag !== "page-tree") {
|
|
101
|
+
process.stderr.write(`export adf: --mode must be "single" or "page-tree" (got "${modeFlag}")\n`);
|
|
102
|
+
return 2;
|
|
103
|
+
}
|
|
104
|
+
mode = modeFlag;
|
|
105
|
+
}
|
|
106
|
+
const flow = flags.get("flow");
|
|
107
|
+
const title = flags.get("title");
|
|
108
|
+
try {
|
|
109
|
+
const projection = await projectDocPackToAdf({
|
|
110
|
+
workspaceDir: projectDir,
|
|
111
|
+
...(typeof flow === "string" ? { flows: [flow] } : {}),
|
|
112
|
+
options: { mode, ...(typeof title === "string" ? { title } : {}) },
|
|
113
|
+
});
|
|
114
|
+
// The agentic-path artifact: a host agent hands these files to the Atlassian MCP (or the
|
|
115
|
+
// confluence publisher plugin consumes them). Default destination is inside the workspace.
|
|
116
|
+
const outFlag = flags.get("out");
|
|
117
|
+
const outDir = typeof outFlag === "string"
|
|
118
|
+
? path.resolve(outFlag)
|
|
119
|
+
: resolveWorkspacePath(projectDir, ".export", "adf");
|
|
120
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
121
|
+
const projectionPath = path.join(outDir, "projection.json");
|
|
122
|
+
const attachmentsPath = path.join(outDir, "attachments.json");
|
|
123
|
+
await fs.writeFile(projectionPath, JSON.stringify(projection, null, 2) + "\n", "utf8");
|
|
124
|
+
const manifest = projection.documents.flatMap((d) => d.attachments.map((a) => ({ section: d.section, ...a })));
|
|
125
|
+
await fs.writeFile(attachmentsPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
126
|
+
process.stdout.write(`export adf: wrote ${projectionPath} (${projection.documents.length} document${projection.documents.length === 1 ? "" : "s"}, mode ${projection.mode}) + ${attachmentsPath} (${manifest.length} attachment${manifest.length === 1 ? "" : "s"})\n`);
|
|
127
|
+
for (const w of projection.warnings)
|
|
128
|
+
process.stderr.write(`export adf: warning: ${w}\n`);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
catch (e) {
|
|
132
|
+
process.stderr.write(`export adf: ${e.message}\n`);
|
|
133
|
+
return 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function cmdExportPlaywright(rest) {
|
|
137
|
+
const { positionals, flags } = parseFlags(rest);
|
|
138
|
+
if (!positionals[0]) {
|
|
139
|
+
process.stderr.write(`export playwright: missing <workspace-dir>\n`);
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
const projectDir = positionals[0];
|
|
143
|
+
const flow = flags.get("flow");
|
|
144
|
+
const outFlag = flags.get("out");
|
|
145
|
+
const outDir = typeof outFlag === "string"
|
|
146
|
+
? path.resolve(outFlag)
|
|
147
|
+
: resolveWorkspacePath(projectDir, ".export", "tests");
|
|
148
|
+
try {
|
|
149
|
+
const specs = await exportWorkspaceFlowsAsPlaywrightTests({
|
|
150
|
+
workspaceDir: projectDir,
|
|
151
|
+
...(typeof flow === "string" ? { flows: [flow] } : {}),
|
|
152
|
+
});
|
|
153
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
154
|
+
for (const spec of specs) {
|
|
155
|
+
await fs.writeFile(path.join(outDir, spec.fileName), spec.content, "utf8");
|
|
156
|
+
}
|
|
157
|
+
process.stdout.write(`export playwright: wrote ${specs.length} spec${specs.length === 1 ? "" : "s"} to ${outDir}\n`);
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
if (e instanceof FlowFileError) {
|
|
162
|
+
process.stderr.write(`export playwright: ${e.message}\n`);
|
|
163
|
+
return 1;
|
|
164
|
+
}
|
|
165
|
+
process.stderr.write(`export playwright: ${e.message}\n`);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Copy `src` → `dest` if `src` exists. Returns 1 if copied, 0 if absent. */
|
|
170
|
+
async function copyIfExists(src, dest) {
|
|
171
|
+
try {
|
|
172
|
+
await fs.access(src);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
178
|
+
await fs.copyFile(src, dest);
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
export async function cmdBaseline(args) {
|
|
182
|
+
const { positionals, flags } = parseFlags(args);
|
|
183
|
+
if (!positionals[0]) {
|
|
184
|
+
process.stderr.write(`baseline: missing <workspace-dir>\n`);
|
|
185
|
+
return 2;
|
|
186
|
+
}
|
|
187
|
+
const projectDir = positionals[0];
|
|
188
|
+
const outFlag = flags.get("out");
|
|
189
|
+
const outDir = typeof outFlag === "string"
|
|
190
|
+
? path.resolve(outFlag)
|
|
191
|
+
: resolveWorkspacePath(projectDir, ".baseline");
|
|
192
|
+
try {
|
|
193
|
+
// Refresh = replace: a baseline is a derived snapshot, so the previous one is dropped whole
|
|
194
|
+
// rather than merged (a stale leftover file would read as drift).
|
|
195
|
+
await fs.rm(path.join(outDir, "flows"), { recursive: true, force: true });
|
|
196
|
+
await fs.rm(path.join(outDir, "docs"), { recursive: true, force: true });
|
|
197
|
+
let copied = 0;
|
|
198
|
+
const flowsDir = resolveWorkspacePath(projectDir, "flows");
|
|
199
|
+
for (const entry of (await fs.readdir(flowsDir).catch(() => [])).sort()) {
|
|
200
|
+
if (!entry.endsWith(".flow.yaml"))
|
|
201
|
+
continue;
|
|
202
|
+
copied += await copyIfExists(path.join(flowsDir, entry), path.join(outDir, "flows", entry));
|
|
203
|
+
}
|
|
204
|
+
const docsDir = resolveWorkspacePath(projectDir, "docs");
|
|
205
|
+
copied += await copyIfExists(path.join(docsDir, "locators.yaml"), path.join(outDir, "docs", "locators.yaml"));
|
|
206
|
+
const docEntries = await fs.readdir(docsDir, { withFileTypes: true }).catch(() => []);
|
|
207
|
+
for (const entry of docEntries.sort((x, y) => x.name.localeCompare(y.name))) {
|
|
208
|
+
if (!entry.isDirectory())
|
|
209
|
+
continue;
|
|
210
|
+
const flowDir = path.join(docsDir, entry.name);
|
|
211
|
+
const outFlowDir = path.join(outDir, "docs", entry.name);
|
|
212
|
+
for (const f of (await fs.readdir(flowDir).catch(() => [])).sort()) {
|
|
213
|
+
if (f.endsWith(".md") || f === "annotations.json") {
|
|
214
|
+
copied += await copyIfExists(path.join(flowDir, f), path.join(outFlowDir, f));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const shotsDir = path.join(flowDir, "screenshots");
|
|
218
|
+
for (const f of (await fs.readdir(shotsDir).catch(() => [])).sort()) {
|
|
219
|
+
if (!f.endsWith(".png"))
|
|
220
|
+
continue;
|
|
221
|
+
copied += await copyIfExists(path.join(shotsDir, f), path.join(outFlowDir, "screenshots", f));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
process.stdout.write(`baseline: snapshotted ${copied} file${copied === 1 ? "" : "s"} to ${outDir}\n`);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
process.stderr.write(`baseline: ${e.message}\n`);
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
export async function cmdDiff(args) {
|
|
233
|
+
const { positionals, flags } = parseFlags(args);
|
|
234
|
+
if (!positionals[0]) {
|
|
235
|
+
process.stderr.write(`diff: missing <workspace-dir>\n`);
|
|
236
|
+
return 2;
|
|
237
|
+
}
|
|
238
|
+
const projectDir = positionals[0];
|
|
239
|
+
const againstFlag = flags.get("against");
|
|
240
|
+
const againstDir = typeof againstFlag === "string"
|
|
241
|
+
? path.resolve(againstFlag)
|
|
242
|
+
: resolveWorkspacePath(projectDir, ".baseline");
|
|
243
|
+
const format = typeof flags.get("format") === "string" ? flags.get("format") : "text";
|
|
244
|
+
if (format !== "json" && format !== "md" && format !== "text") {
|
|
245
|
+
process.stderr.write(`diff: --format must be json | md | text (got "${format}")\n`);
|
|
246
|
+
return 2;
|
|
247
|
+
}
|
|
248
|
+
const failOnFlag = flags.get("fail-on");
|
|
249
|
+
let failOn = null;
|
|
250
|
+
if (failOnFlag !== undefined) {
|
|
251
|
+
if (failOnFlag !== "warn" && failOnFlag !== "fail") {
|
|
252
|
+
process.stderr.write(`diff: --fail-on must be warn | fail (got "${String(failOnFlag)}")\n`);
|
|
253
|
+
return 2;
|
|
254
|
+
}
|
|
255
|
+
failOn = failOnFlag;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
await fs.access(path.join(againstDir, "flows"));
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
process.stderr.write(`diff: no baseline at ${againstDir} — run \`docsxai baseline ${projectDir}\` first (or pass --against <dir>)\n`);
|
|
262
|
+
return 2;
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const report = await diffDocPacks(againstDir, projectDir);
|
|
266
|
+
if (format === "json")
|
|
267
|
+
process.stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
268
|
+
else if (format === "md")
|
|
269
|
+
process.stdout.write(formatDriftReportMarkdown(report));
|
|
270
|
+
else
|
|
271
|
+
process.stdout.write(formatDriftReportText(report));
|
|
272
|
+
if (failOn !== null && severityAtLeast(report.summary.severity, failOn))
|
|
273
|
+
return 1;
|
|
274
|
+
return 0;
|
|
275
|
+
}
|
|
276
|
+
catch (e) {
|
|
277
|
+
process.stderr.write(`diff: ${e.message}\n`);
|
|
278
|
+
return 1;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare function cmdRun(args: string[]): Promise<number>;
|
|
2
|
+
export declare function cmdCaptureAuth(args: string[]): Promise<number>;
|
|
3
|
+
export declare function cmdInit(args: string[]): Promise<number>;
|
|
4
|
+
export declare function cmdCalibrate(args: string[]): Promise<number>;
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// Runtime / session commands — the ones that stand up a workspace or drive a live browser:
|
|
2
|
+
// init — scaffold a workspace
|
|
3
|
+
// capture-auth — run a role's auth strategy and cache the session
|
|
4
|
+
// calibrate — write flows/<name>.flow.yaml from a structured flow-guide
|
|
5
|
+
// run — execute the workspace's flows against the live app, emitting annotations
|
|
6
|
+
//
|
|
7
|
+
// These reach into the engine's heavier subsystems (Playwright, auth, the flow runtime); the
|
|
8
|
+
// thinner static/aid commands live in cli-commands-authoring.ts.
|
|
9
|
+
import { promises as fs } from "node:fs";
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { LocalStorageStateCache, makeStrategy, parseAuthStrategyFile, resolveCredsEnv, resolveStateCache, } from "./auth.js";
|
|
12
|
+
import { calibrate } from "./calibrate.js";
|
|
13
|
+
import { FlowFileError, parseFlowFile, resolveFlowExtends } from "./flow-file.js";
|
|
14
|
+
import { recordRunHistory } from "./backend-client.js";
|
|
15
|
+
import { runFlow } from "./flow-runtime.js";
|
|
16
|
+
import { launchPlaywrightSession } from "./playwright-driver.js";
|
|
17
|
+
import { PlaywrightInstrumentedBrowser } from "./playwright-instrumented-browser.js";
|
|
18
|
+
import { initWorkspace, loadWorkspaceConfig, resolveWorkspacePath, resolveWorkspacePathReal, } from "./workspace.js";
|
|
19
|
+
import { listFlowFiles, parseFlags } from "./cli-shared.js";
|
|
20
|
+
import { USAGE } from "./cli-usage.js";
|
|
21
|
+
async function loadAuthStorageState(projectDir) {
|
|
22
|
+
const descriptorPath = resolveWorkspacePath(projectDir, "auth", "strategy.yaml");
|
|
23
|
+
let text;
|
|
24
|
+
try {
|
|
25
|
+
text = await fs.readFile(descriptorPath, "utf8");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined; // no auth configured — run with a fresh context
|
|
29
|
+
}
|
|
30
|
+
const descriptor = parseAuthStrategyFile(text, descriptorPath);
|
|
31
|
+
const role = descriptor.default_role;
|
|
32
|
+
const cache = await resolveStateCache(descriptor.roles[role], projectDir);
|
|
33
|
+
const state = await cache.load(role);
|
|
34
|
+
if (!state) {
|
|
35
|
+
throw new Error(`auth/strategy.yaml configures role "${role}" but no valid cached session was found at ${path.join(projectDir, ".auth", role + ".json")}.\n` +
|
|
36
|
+
`Capture one first (calibration's auth step, e.g. the manual-capture flow).`);
|
|
37
|
+
}
|
|
38
|
+
return state;
|
|
39
|
+
}
|
|
40
|
+
export async function cmdRun(args) {
|
|
41
|
+
const { positionals, flags } = parseFlags(args);
|
|
42
|
+
if (!positionals[0]) {
|
|
43
|
+
process.stderr.write("run: missing <project-dir>\n\n" + USAGE + "\n");
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
const projectDir = positionals[0];
|
|
47
|
+
const onlyFlow = typeof flags.get("flow") === "string" ? flags.get("flow") : undefined;
|
|
48
|
+
const stopAfter = typeof flags.get("stop-after") === "string" ? flags.get("stop-after") : undefined;
|
|
49
|
+
const startFrom = typeof flags.get("start-from") === "string" ? flags.get("start-from") : undefined;
|
|
50
|
+
const cdpEndpoint = typeof flags.get("cdp") === "string" ? flags.get("cdp") : undefined;
|
|
51
|
+
const pause = flags.get("pause") === true;
|
|
52
|
+
const headed = flags.get("headed") === true || pause; // --pause implies --headed
|
|
53
|
+
if (startFrom && !onlyFlow) {
|
|
54
|
+
process.stderr.write(`run: --start-from requires --flow <name> (single-flow calibration aid)\n`);
|
|
55
|
+
return 2;
|
|
56
|
+
}
|
|
57
|
+
const wsCfg = await loadWorkspaceConfig(projectDir);
|
|
58
|
+
const baseURL = (typeof flags.get("base-url") === "string" ? flags.get("base-url") : undefined) ??
|
|
59
|
+
wsCfg?.app_url;
|
|
60
|
+
const ignoreHTTPSErrors = flags.get("ignore-https-errors") === true || !!wsCfg?.ignore_https_errors;
|
|
61
|
+
let flowPaths;
|
|
62
|
+
try {
|
|
63
|
+
flowPaths = await listFlowFiles(projectDir);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
process.stderr.write(`run: ${e.message}\n`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
const loadFlowFile = async (name) => {
|
|
70
|
+
const fp = resolveWorkspacePath(projectDir, "flows", `${name}.flow.yaml`);
|
|
71
|
+
let text;
|
|
72
|
+
try {
|
|
73
|
+
text = await fs.readFile(fp, "utf8");
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
throw new FlowFileError(`\`extends\`: no flow named "${name}" at ${fp}`);
|
|
77
|
+
}
|
|
78
|
+
return parseFlowFile(text, fp);
|
|
79
|
+
};
|
|
80
|
+
const flows = [];
|
|
81
|
+
for (const fp of flowPaths) {
|
|
82
|
+
let flow;
|
|
83
|
+
try {
|
|
84
|
+
const parsed = parseFlowFile(await fs.readFile(fp, "utf8"), fp);
|
|
85
|
+
flow = parsed.extends ? await resolveFlowExtends(parsed, loadFlowFile) : parsed;
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
if (e instanceof FlowFileError) {
|
|
89
|
+
process.stderr.write(`run: ${e.message}\n`);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
throw e;
|
|
93
|
+
}
|
|
94
|
+
if (!onlyFlow || flow.name === onlyFlow)
|
|
95
|
+
flows.push(flow);
|
|
96
|
+
}
|
|
97
|
+
if (flows.length === 0) {
|
|
98
|
+
process.stderr.write(onlyFlow
|
|
99
|
+
? `run: no flow named "${onlyFlow}"\n`
|
|
100
|
+
: `run: no flow-files in ${projectDir}/flows\n`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
let storageState;
|
|
104
|
+
try {
|
|
105
|
+
storageState = await loadAuthStorageState(projectDir);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
process.stderr.write(`run: ${e.message}\n`);
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
// Parallel runners: each flow gets its own Playwright session, so flows are isolated and can run together.
|
|
112
|
+
// `--concurrency N` (default 1) caps how many run at once. `--pause` / `--stop-after` force concurrency=1
|
|
113
|
+
// (they're single-flow calibration aids; mixing them with parallelism would be chaos).
|
|
114
|
+
const concurrencyRaw = typeof flags.get("concurrency") === "string" ? Number(flags.get("concurrency")) : 1;
|
|
115
|
+
const requestedConcurrency = Math.max(1, Math.floor(concurrencyRaw || 1));
|
|
116
|
+
const forceSingle = pause || stopAfter || startFrom || cdpEndpoint;
|
|
117
|
+
const concurrency = forceSingle ? 1 : requestedConcurrency;
|
|
118
|
+
if (forceSingle && requestedConcurrency > 1) {
|
|
119
|
+
process.stderr.write(`run: --pause / --stop-after / --start-from / --cdp force --concurrency 1 (ignoring --concurrency ${requestedConcurrency})\n`);
|
|
120
|
+
}
|
|
121
|
+
const tag = concurrency > 1 ? (name) => `run [${name}]: ` : () => "run: ";
|
|
122
|
+
async function runOne(flow) {
|
|
123
|
+
const name = flow.name;
|
|
124
|
+
let session;
|
|
125
|
+
try {
|
|
126
|
+
// When attaching to an existing Chrome via --cdp, the operator owns its auth state — don't
|
|
127
|
+
// load cached `storageState` over it (would replace cookies). When launching fresh, do.
|
|
128
|
+
session = await launchPlaywrightSession({
|
|
129
|
+
baseURL,
|
|
130
|
+
headed,
|
|
131
|
+
ignoreHTTPSErrors,
|
|
132
|
+
...(cdpEndpoint ? { connectOverCdp: cdpEndpoint } : { storageState }),
|
|
133
|
+
...(flow.environment ? { environment: flow.environment } : {}),
|
|
134
|
+
docPackRoot: projectDir,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (e) {
|
|
138
|
+
const msg = e.message;
|
|
139
|
+
if (/Executable doesn't exist|browserType\.launch|playwright install/i.test(msg)) {
|
|
140
|
+
process.stderr.write(`${tag(name)}no Chromium binary found. Install one: npx playwright-core install chromium (source checkout: pnpm -C packages/engine exec playwright-core install chromium)\n`);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
process.stderr.write(`${tag(name)}failed to launch browser: ${msg}\n`);
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const result = await runFlow(flow, session.driver, {
|
|
149
|
+
resolveLocator: (n) => flow.locators[n],
|
|
150
|
+
...(stopAfter ? { stopAfter } : {}),
|
|
151
|
+
...(startFrom ? { startFrom } : {}),
|
|
152
|
+
});
|
|
153
|
+
await fs.mkdir(resolveWorkspacePath(projectDir, "docs", flow.name), { recursive: true });
|
|
154
|
+
// Flow names come from the flow-files — resolve the write target symlink-aware.
|
|
155
|
+
const annotationsPath = await resolveWorkspacePathReal(projectDir, "docs", flow.name, "annotations.json");
|
|
156
|
+
// With `startFrom`, only the post-startFrom steps emit annotations — merge them into the
|
|
157
|
+
// existing file (if any) by step id so the prior steps' annotations stay in place. Same
|
|
158
|
+
// story for screenshots (they live as separate PNGs and are simply not re-captured).
|
|
159
|
+
let toWrite = result.annotations;
|
|
160
|
+
if (startFrom) {
|
|
161
|
+
try {
|
|
162
|
+
const existingText = await fs.readFile(annotationsPath, "utf8");
|
|
163
|
+
const existing = JSON.parse(existingText);
|
|
164
|
+
const newStepIds = new Set(result.annotations.annotations.map((a) => a.step));
|
|
165
|
+
const merged = [
|
|
166
|
+
...existing.annotations.filter((a) => !newStepIds.has(a.step)),
|
|
167
|
+
...result.annotations.annotations,
|
|
168
|
+
];
|
|
169
|
+
toWrite = { ...result.annotations, annotations: merged };
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// No existing file (or unreadable) — just write what we have.
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await fs.writeFile(annotationsPath, JSON.stringify(toWrite, null, 2) + "\n", "utf8");
|
|
176
|
+
process.stdout.write(`${tag(name)}${name} — ${result.steps.length} step(s) executed, ${result.annotations.annotations.length} annotation(s) ${startFrom ? "merged" : "written"}\n`);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
process.stderr.write(`${tag(name)}${e.message}\n`);
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
if (pause) {
|
|
185
|
+
process.stdout.write("run: --pause — browser is open at the last step run; close it to exit.\n");
|
|
186
|
+
await new Promise((resolve) => session.browser.on("disconnected", () => resolve()));
|
|
187
|
+
}
|
|
188
|
+
await session.close();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
let idx = 0;
|
|
192
|
+
let anyFailed = false;
|
|
193
|
+
let okCount = 0;
|
|
194
|
+
const startedAt = Date.now();
|
|
195
|
+
async function worker() {
|
|
196
|
+
while (idx < flows.length) {
|
|
197
|
+
const flow = flows[idx++];
|
|
198
|
+
if (await runOne(flow))
|
|
199
|
+
okCount++;
|
|
200
|
+
else
|
|
201
|
+
anyFailed = true;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const workers = Math.min(concurrency, flows.length);
|
|
205
|
+
if (workers > 1)
|
|
206
|
+
process.stdout.write(`run: running ${flows.length} flows with ${workers} parallel worker(s)…\n`);
|
|
207
|
+
await Promise.all(Array.from({ length: workers }, () => worker()));
|
|
208
|
+
// Backend-bound workspaces get a run record appended; offline-tolerant (warn, never fail the run).
|
|
209
|
+
const history = await recordRunHistory({
|
|
210
|
+
workspaceDir: projectDir,
|
|
211
|
+
config: wsCfg ?? {},
|
|
212
|
+
ok: !anyFailed,
|
|
213
|
+
durationMs: Date.now() - startedAt,
|
|
214
|
+
summary: `${okCount}/${flows.length} flows ok`,
|
|
215
|
+
});
|
|
216
|
+
if (history.warning)
|
|
217
|
+
process.stderr.write(`run: warning — ${history.warning}\n`);
|
|
218
|
+
return anyFailed ? 1 : 0;
|
|
219
|
+
}
|
|
220
|
+
export async function cmdCaptureAuth(args) {
|
|
221
|
+
const { positionals, flags } = parseFlags(args);
|
|
222
|
+
const projectDir = positionals[0];
|
|
223
|
+
if (!projectDir) {
|
|
224
|
+
process.stderr.write("capture-auth: missing <project-dir>\n\n" + USAGE + "\n");
|
|
225
|
+
return 2;
|
|
226
|
+
}
|
|
227
|
+
const wsCfg = await loadWorkspaceConfig(projectDir);
|
|
228
|
+
const baseURL = (typeof flags.get("base-url") === "string" ? flags.get("base-url") : undefined) ??
|
|
229
|
+
wsCfg?.app_url;
|
|
230
|
+
if (!baseURL) {
|
|
231
|
+
process.stderr.write("capture-auth: --base-url <url> is required (or set app_url in the workspace's .docsxai.json)\n");
|
|
232
|
+
return 2;
|
|
233
|
+
}
|
|
234
|
+
const headless = flags.get("headless") === true;
|
|
235
|
+
const ignoreHTTPSErrors = flags.get("ignore-https-errors") === true || !!wsCfg?.ignore_https_errors;
|
|
236
|
+
const authCookie = typeof flags.get("auth-cookie") === "string" ? flags.get("auth-cookie") : undefined;
|
|
237
|
+
const cdp = typeof flags.get("cdp") === "string" ? flags.get("cdp") : undefined;
|
|
238
|
+
const fresh = flags.get("fresh") === true;
|
|
239
|
+
// Persistent Chrome profile under the workspace — re-running capture-auth reuses the login. (Not when attaching, or with --fresh.)
|
|
240
|
+
const profileDir = fresh || cdp ? undefined : resolveWorkspacePath(projectDir, ".auth", "chrome-profile");
|
|
241
|
+
const descriptorPath = resolveWorkspacePath(projectDir, "auth", "strategy.yaml");
|
|
242
|
+
let descriptorText;
|
|
243
|
+
try {
|
|
244
|
+
descriptorText = await fs.readFile(descriptorPath, "utf8");
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
process.stderr.write(`capture-auth: no auth descriptor at ${descriptorPath}\n`);
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
let role;
|
|
251
|
+
let roleAuth;
|
|
252
|
+
try {
|
|
253
|
+
const descriptor = parseAuthStrategyFile(descriptorText, descriptorPath);
|
|
254
|
+
role =
|
|
255
|
+
typeof flags.get("role") === "string"
|
|
256
|
+
? flags.get("role")
|
|
257
|
+
: descriptor.default_role;
|
|
258
|
+
const ra = descriptor.roles[role];
|
|
259
|
+
if (!ra) {
|
|
260
|
+
process.stderr.write(`capture-auth: role "${role}" not in ${descriptorPath}\n`);
|
|
261
|
+
return 1;
|
|
262
|
+
}
|
|
263
|
+
roleAuth = ra;
|
|
264
|
+
}
|
|
265
|
+
catch (e) {
|
|
266
|
+
process.stderr.write(`capture-auth: ${e.message}\n`);
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
let creds;
|
|
270
|
+
try {
|
|
271
|
+
creds = resolveCredsEnv(roleAuth);
|
|
272
|
+
}
|
|
273
|
+
catch (e) {
|
|
274
|
+
process.stderr.write(`capture-auth: ${e.message}\n`);
|
|
275
|
+
return 1;
|
|
276
|
+
}
|
|
277
|
+
let strategy;
|
|
278
|
+
try {
|
|
279
|
+
strategy = makeStrategy(roleAuth, {
|
|
280
|
+
instrumentedBrowser: () => new PlaywrightInstrumentedBrowser({
|
|
281
|
+
headless,
|
|
282
|
+
ignoreHTTPSErrors,
|
|
283
|
+
...(cdp ? { connectOverCdp: cdp } : {}),
|
|
284
|
+
...(profileDir ? { profileDir } : {}),
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
process.stderr.write(`capture-auth: ${e.message}\n`);
|
|
290
|
+
return 1;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
process.stdout.write(`capture-auth: launching browser for role "${role}" (${roleAuth.strategy})${cdp ? ` — attaching to ${cdp}` : profileDir ? " — reusing saved profile if present" : " — fresh profile"}; log in if prompted, then trigger capture…\n`);
|
|
294
|
+
const result = await strategy.authenticate({ creds, options: roleAuth.options, baseURL, role });
|
|
295
|
+
const cookies = result.storageState.cookies ?? [];
|
|
296
|
+
process.stdout.write(`capture-auth: captured ${cookies.length} cookie(s)${cookies.length ? " (newest expiry first):" : ""}\n`);
|
|
297
|
+
for (const c of [...cookies].sort((a, b) => (b.expires || 0) - (a.expires || 0))) {
|
|
298
|
+
const exp = c.expires && c.expires > 0 ? new Date(c.expires * 1000).toISOString() : "(session)";
|
|
299
|
+
process.stdout.write(` ${c.name} @${c.domain} expires ${exp}\n`);
|
|
300
|
+
}
|
|
301
|
+
const { expiresAt, source } = await new LocalStorageStateCache(resolveWorkspacePath(projectDir, ".auth")).save(role, result, roleAuth, Date.now(), authCookie ? { authCookie } : {});
|
|
302
|
+
process.stdout.write(`capture-auth: cached ${role} → ${path.join(projectDir, ".auth", role + ".json")}\n` +
|
|
303
|
+
` expires ${new Date(expiresAt).toISOString()} (from ${source}; re-run when it lapses)\n`);
|
|
304
|
+
if (!/^auth-cookie/.test(source)) {
|
|
305
|
+
process.stdout.write(` tip: pick the app's auth/session cookie from the list above and set 'cache.auth_cookie: <name>' in\n` +
|
|
306
|
+
` ${path.join(projectDir, "auth", "strategy.yaml")} (or pass --auth-cookie <name>) so the cache tracks its real expiry.\n`);
|
|
307
|
+
}
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
catch (e) {
|
|
311
|
+
process.stderr.write(`capture-auth: ${e.message}\n`);
|
|
312
|
+
return 1;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
export async function cmdInit(args) {
|
|
316
|
+
const { positionals, flags } = parseFlags(args);
|
|
317
|
+
const persistTmp = flags.get("persist") === "tmp";
|
|
318
|
+
const dir = positionals[0];
|
|
319
|
+
if (!persistTmp && !dir) {
|
|
320
|
+
process.stderr.write("init: missing <workspace-dir> (or use --persist tmp)\n\n" + USAGE + "\n");
|
|
321
|
+
return 2;
|
|
322
|
+
}
|
|
323
|
+
const str = (k) => typeof flags.get(k) === "string" ? flags.get(k) : undefined;
|
|
324
|
+
const auth = str("auth");
|
|
325
|
+
if (auth !== undefined && auth !== "manual-capture" && auth !== "none") {
|
|
326
|
+
process.stderr.write("init: --auth must be 'manual-capture' or 'none'\n");
|
|
327
|
+
return 2;
|
|
328
|
+
}
|
|
329
|
+
const trigger = str("capture-trigger");
|
|
330
|
+
if (trigger !== undefined && trigger !== "console" && trigger !== "button") {
|
|
331
|
+
process.stderr.write("init: --capture-trigger must be 'console' or 'button'\n");
|
|
332
|
+
return 2;
|
|
333
|
+
}
|
|
334
|
+
const appUrl = str("app-url");
|
|
335
|
+
const role = str("role");
|
|
336
|
+
const ttl = str("ttl");
|
|
337
|
+
const authCookie = str("auth-cookie");
|
|
338
|
+
try {
|
|
339
|
+
const r = await initWorkspace({
|
|
340
|
+
...(dir ? { dir } : {}),
|
|
341
|
+
persistTmp,
|
|
342
|
+
...(appUrl ? { appUrl } : {}),
|
|
343
|
+
...(auth ? { auth } : {}),
|
|
344
|
+
...(role ? { role } : {}),
|
|
345
|
+
...(ttl ? { ttl } : {}),
|
|
346
|
+
...(trigger ? { captureTrigger: trigger } : {}),
|
|
347
|
+
...(authCookie ? { authCookie } : {}),
|
|
348
|
+
ignoreHttpsErrors: flags.get("ignore-https-errors") === true,
|
|
349
|
+
force: flags.get("force") === true,
|
|
350
|
+
});
|
|
351
|
+
process.stdout.write(`init: workspace ${r.ephemeral ? "(ephemeral) " : ""}at ${r.dir}\n created: ${r.created.join(", ")}\n`);
|
|
352
|
+
process.stdout.write(` next: ${appUrl ? "" : "(set app_url in .docsxai.json, then) "}docsxai capture-auth ${r.dir} → …calibrate… → docsxai run ${r.dir} → docsxai render ${r.dir}\n`);
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
process.stderr.write(`init: ${e.message}\n`);
|
|
357
|
+
return 1;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
export async function cmdCalibrate(args) {
|
|
361
|
+
const { positionals, flags } = parseFlags(args);
|
|
362
|
+
const workspaceDir = positionals[0];
|
|
363
|
+
if (!workspaceDir) {
|
|
364
|
+
process.stderr.write("calibrate: missing <workspace-dir>\n\n" + USAGE + "\n");
|
|
365
|
+
return 2;
|
|
366
|
+
}
|
|
367
|
+
const from = typeof flags.get("from") === "string" ? flags.get("from") : undefined;
|
|
368
|
+
if (!from) {
|
|
369
|
+
process.stderr.write("calibrate: --from <flow.md|.yaml> is required (the structured flow-guide)\n");
|
|
370
|
+
return 2;
|
|
371
|
+
}
|
|
372
|
+
const flowName = typeof flags.get("name") === "string" ? flags.get("name") : undefined;
|
|
373
|
+
let text;
|
|
374
|
+
try {
|
|
375
|
+
text = await fs.readFile(from, "utf8");
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
process.stderr.write(`calibrate: cannot read ${from}\n`);
|
|
379
|
+
return 1;
|
|
380
|
+
}
|
|
381
|
+
try {
|
|
382
|
+
const r = await calibrate({
|
|
383
|
+
workspaceDir,
|
|
384
|
+
fromText: text,
|
|
385
|
+
fromSource: from,
|
|
386
|
+
...(flowName ? { flowName } : {}),
|
|
387
|
+
});
|
|
388
|
+
process.stdout.write(`calibrate: wrote ${r.flowFilePath} (${r.flow.steps.length} steps)\n`);
|
|
389
|
+
if (r.wroteStyle)
|
|
390
|
+
process.stdout.write(`calibrate: wrote default ${r.stylePath}\n`);
|
|
391
|
+
process.stdout.write(` next: docsxai run ${workspaceDir} (then: docsxai render ${workspaceDir})\n`);
|
|
392
|
+
return 0;
|
|
393
|
+
}
|
|
394
|
+
catch (e) {
|
|
395
|
+
process.stderr.write(`calibrate: ${e.message}\n`);
|
|
396
|
+
return 1;
|
|
397
|
+
}
|
|
398
|
+
}
|