@olomonhelpdesk2/helpdesk 0.1.14 → 0.1.15
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/helpdesk-capture.mjs +196 -62
- package/package.json +1 -1
package/helpdesk-capture.mjs
CHANGED
|
@@ -427,7 +427,7 @@ function preflight(facts) {
|
|
|
427
427
|
name: "posture",
|
|
428
428
|
ok: true,
|
|
429
429
|
blocking: false,
|
|
430
|
-
detail: facts.clips ? "read-only for the screenshots; after them, the clips the seed command named press the controls their steps name, in the tenant it built" : "read-only: this navigates and photographs, and activates no control"
|
|
430
|
+
detail: facts.clips ? "read-only for the screenshots; after them, the clips the seed command named and the articles ask for press the controls their steps name, in the tenant it built" : "read-only: this navigates and photographs, and activates no control"
|
|
431
431
|
});
|
|
432
432
|
if (facts.rebaselineReason) {
|
|
433
433
|
checks.push({
|
|
@@ -1522,13 +1522,21 @@ function layoutManifestText(manifest) {
|
|
|
1522
1522
|
// core/capture/clips.ts
|
|
1523
1523
|
var CLIP_VIEWPORT = { width: 1280, height: 800 };
|
|
1524
1524
|
var CLIP_FPS = 10;
|
|
1525
|
-
var CLIP_MAX_MS =
|
|
1525
|
+
var CLIP_MAX_MS = 2e4;
|
|
1526
1526
|
var CLIP_MAX_STEPS = 12;
|
|
1527
1527
|
var CLIP_MAX_MP4_BYTES = 3 * 1024 * 1024;
|
|
1528
1528
|
var CLIP_MAX_GIF_BYTES = 8 * 1024 * 1024;
|
|
1529
1529
|
var CLIP_GIF_WIDTH = 960;
|
|
1530
|
-
var CLIP_ACTIONS = ["click", "fill", "select", "check"];
|
|
1531
|
-
|
|
1530
|
+
var CLIP_ACTIONS = ["click", "fill", "select", "check", "auto"];
|
|
1531
|
+
function actionForRole(role, value) {
|
|
1532
|
+
if (role === "textbox" || role === "searchbox") return "fill";
|
|
1533
|
+
if ((role === "combobox" || role === "listbox") && value) return "select";
|
|
1534
|
+
if (role === "checkbox" || role === "radio" || role === "switch") return "check";
|
|
1535
|
+
return "click";
|
|
1536
|
+
}
|
|
1537
|
+
var MAX_SEEDED_SCRIPTS = 50;
|
|
1538
|
+
var CLIP_MAX_ARTICLE_SCRIPTS = 400;
|
|
1539
|
+
var CLIP_MAX_LEAD_STEPS = 2 * CLIP_MAX_STEPS;
|
|
1532
1540
|
var MAX_TEXT2 = 200;
|
|
1533
1541
|
var MAX_TITLE = 120;
|
|
1534
1542
|
var MAX_ID = 80;
|
|
@@ -1551,21 +1559,40 @@ function readStep(value, at) {
|
|
|
1551
1559
|
const says = cleanText(step["says"]);
|
|
1552
1560
|
const given = step["value"];
|
|
1553
1561
|
if (!route || !isPathLike(route)) return `step ${at} names no screen`;
|
|
1554
|
-
if (typeof role !== "string" || !CONTROL_ROLES.includes(role)) return `step ${at} names no control role a screen lists`;
|
|
1562
|
+
if (role !== void 0 && (typeof role !== "string" || !CONTROL_ROLES.includes(role))) return `step ${at} names no control role a screen lists`;
|
|
1555
1563
|
if (!name || name.length > MAX_TEXT2) return `step ${at} names no control`;
|
|
1556
|
-
if (typeof action !== "string" || !CLIP_ACTIONS.includes(action)) return `step ${at} says neither click, fill, select nor
|
|
1564
|
+
if (typeof action !== "string" || !CLIP_ACTIONS.includes(action)) return `step ${at} says neither click, fill, select, check nor auto`;
|
|
1557
1565
|
if (!says || says.length > MAX_TEXT2) return `step ${at} has no sentence for a reader`;
|
|
1566
|
+
if (role === void 0 && action !== "auto") return `step ${at} says how to act on a control without saying what kind it is`;
|
|
1558
1567
|
const typed = action === "fill" || action === "select";
|
|
1559
|
-
|
|
1560
|
-
if (
|
|
1561
|
-
|
|
1568
|
+
const valued = typeof given === "string" && given.trim() !== "" && given.length <= MAX_TEXT2;
|
|
1569
|
+
if (typed && !valued) return `step ${at} has nothing to ${action}`;
|
|
1570
|
+
if (action !== "auto" && !typed && given !== void 0) return `step ${at} gives a value to ${action}, which takes none`;
|
|
1571
|
+
if (action === "auto" && given !== void 0 && !valued) return `step ${at} has a value nothing can type`;
|
|
1572
|
+
return {
|
|
1573
|
+
route,
|
|
1574
|
+
control: { ...role === void 0 ? {} : { role }, name },
|
|
1575
|
+
action,
|
|
1576
|
+
...valued && (typed || action === "auto") ? { value: given } : {},
|
|
1577
|
+
says
|
|
1578
|
+
};
|
|
1562
1579
|
}
|
|
1563
|
-
function
|
|
1580
|
+
function readSteps(raw) {
|
|
1581
|
+
const steps = [];
|
|
1582
|
+
for (const [index, value] of raw.entries()) {
|
|
1583
|
+
const step = readStep(value, index + 1);
|
|
1584
|
+
if (typeof step === "string") return step;
|
|
1585
|
+
steps.push(step);
|
|
1586
|
+
}
|
|
1587
|
+
return steps;
|
|
1588
|
+
}
|
|
1589
|
+
function readClipScripts(raw, limit = MAX_SEEDED_SCRIPTS) {
|
|
1564
1590
|
const scripts = [];
|
|
1565
1591
|
const refused = [];
|
|
1566
1592
|
if (raw === void 0) return { scripts, refused };
|
|
1567
1593
|
if (!Array.isArray(raw)) return { scripts, refused: [{ id: "clips", because: "clips is not a list" }] };
|
|
1568
|
-
|
|
1594
|
+
if (raw.length > limit) refused.push({ id: "clips", because: `${raw.length - limit} clip(s) past the first ${limit} were not read` });
|
|
1595
|
+
for (const value of raw.slice(0, limit)) {
|
|
1569
1596
|
const script = recordOf2(value);
|
|
1570
1597
|
const id = typeof script["id"] === "string" ? script["id"] : "";
|
|
1571
1598
|
const label = id || "a clip";
|
|
@@ -1590,21 +1617,18 @@ function readClipScripts(raw) {
|
|
|
1590
1617
|
refuse(`it has ${rawSteps.length} steps, and a clip has between 1 and ${CLIP_MAX_STEPS}`);
|
|
1591
1618
|
continue;
|
|
1592
1619
|
}
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
if (typeof step === "string") {
|
|
1598
|
-
problem = step;
|
|
1599
|
-
break;
|
|
1600
|
-
}
|
|
1601
|
-
steps.push(step);
|
|
1620
|
+
const rawBefore = script["before"];
|
|
1621
|
+
if (rawBefore !== void 0 && (!Array.isArray(rawBefore) || rawBefore.length > CLIP_MAX_LEAD_STEPS)) {
|
|
1622
|
+
refuse(`its steps before the recording are not a list of at most ${CLIP_MAX_LEAD_STEPS}`);
|
|
1623
|
+
continue;
|
|
1602
1624
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1625
|
+
const steps = readSteps(rawSteps);
|
|
1626
|
+
const before = readSteps(rawBefore ?? []);
|
|
1627
|
+
if (typeof steps === "string" || typeof before === "string") {
|
|
1628
|
+
refuse(typeof before === "string" ? `before the recording, ${before}` : steps);
|
|
1605
1629
|
continue;
|
|
1606
1630
|
}
|
|
1607
|
-
const first = steps[0];
|
|
1631
|
+
const first = before[0] ?? steps[0];
|
|
1608
1632
|
if (!isPathLike(visit) || !fillsPattern(first.route, visit.split("?")[0])) {
|
|
1609
1633
|
refuse(`it opens at ${visit || "nowhere"}, which is not ${first.route}`);
|
|
1610
1634
|
continue;
|
|
@@ -1613,17 +1637,19 @@ function readClipScripts(raw) {
|
|
|
1613
1637
|
refuse("it names no screen to land on");
|
|
1614
1638
|
continue;
|
|
1615
1639
|
}
|
|
1616
|
-
scripts.push({ id, title, visit, steps, ...lands ? { lands } : {} });
|
|
1640
|
+
scripts.push({ id, title, visit, ...before.length ? { before } : {}, steps, ...lands ? { lands } : {} });
|
|
1617
1641
|
}
|
|
1618
1642
|
return { scripts, refused };
|
|
1619
1643
|
}
|
|
1620
1644
|
function resolveStep(manifest, step) {
|
|
1621
1645
|
const name = cleanText(step.control.name).toLowerCase();
|
|
1622
1646
|
const where = manifest.screen.path;
|
|
1623
|
-
const
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
if (matches
|
|
1647
|
+
const role = step.control.role;
|
|
1648
|
+
const what = role ?? "control";
|
|
1649
|
+
const matches = manifest.controls.filter((control) => (!role || control.role === role) && control.name.toLowerCase() === name);
|
|
1650
|
+
if (matches.length === 0) return { ok: false, because: `no ${what} "${step.control.name}" on ${where}` };
|
|
1651
|
+
if (matches.length > 1) return { ok: false, because: `${matches.length} of ${what} "${step.control.name}" on ${where}, so the step does not say which` };
|
|
1652
|
+
if (matches[0].state?.disabled) return { ok: false, because: `${what} "${step.control.name}" on ${where} is disabled` };
|
|
1627
1653
|
return { ok: true, control: matches[0] };
|
|
1628
1654
|
}
|
|
1629
1655
|
function onScreen(route, location2) {
|
|
@@ -1633,19 +1659,22 @@ function clipRoutes(script) {
|
|
|
1633
1659
|
return [.../* @__PURE__ */ new Set([...script.steps.map((step) => step.route), ...script.lands ? [script.lands] : []])];
|
|
1634
1660
|
}
|
|
1635
1661
|
function clipFingerprint(script, touched) {
|
|
1662
|
+
const stepsOf = (steps) => steps.map((step) => ({
|
|
1663
|
+
route: step.route,
|
|
1664
|
+
role: step.control.role ?? null,
|
|
1665
|
+
name: step.control.name,
|
|
1666
|
+
action: step.action,
|
|
1667
|
+
value: step.value ?? null,
|
|
1668
|
+
says: step.says
|
|
1669
|
+
}));
|
|
1636
1670
|
const canonical = {
|
|
1637
1671
|
version: 1,
|
|
1638
1672
|
id: script.id,
|
|
1639
1673
|
title: script.title,
|
|
1640
1674
|
lands: script.lands ?? null,
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
name: step.control.name,
|
|
1645
|
-
action: step.action,
|
|
1646
|
-
value: step.value ?? null,
|
|
1647
|
-
says: step.says
|
|
1648
|
-
})),
|
|
1675
|
+
// Only when there are some, so a clip with none keeps the fingerprint it had before steps could come first.
|
|
1676
|
+
...script.before?.length ? { before: stepsOf(script.before) } : {},
|
|
1677
|
+
steps: stepsOf(script.steps),
|
|
1649
1678
|
touched: [...touched].map((screen) => [screen.route, screen.shotId, screen.sha256]).sort((left, right) => String(left[0]).localeCompare(String(right[0])))
|
|
1650
1679
|
};
|
|
1651
1680
|
return createHash("sha256").update(JSON.stringify(canonical)).digest("hex");
|
|
@@ -3356,6 +3385,7 @@ var MOVE_FRAMES = 6;
|
|
|
3356
3385
|
var DWELL_MS = 500;
|
|
3357
3386
|
var PRESS_MS = 200;
|
|
3358
3387
|
var CHARS_PER_FRAME = 2;
|
|
3388
|
+
var TYPING_FRAMES = 12;
|
|
3359
3389
|
var SETTLE_MS = 3e3;
|
|
3360
3390
|
var STEP_WAIT_MS = 4e3;
|
|
3361
3391
|
var CLOSING_MS = 1200;
|
|
@@ -3422,11 +3452,75 @@ async function recordClip(options, script, touched) {
|
|
|
3422
3452
|
await keep(next, FRAME_MS);
|
|
3423
3453
|
return true;
|
|
3424
3454
|
};
|
|
3455
|
+
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3456
|
+
const measureOn = async (step, where) => resolveStep(
|
|
3457
|
+
buildLayoutManifest(await layout(cover), {
|
|
3458
|
+
shotId: script.id,
|
|
3459
|
+
route: step.route,
|
|
3460
|
+
path: where.split("?")[0],
|
|
3461
|
+
variant: "desktop",
|
|
3462
|
+
image: { ...CLIP_VIEWPORT },
|
|
3463
|
+
origin: new URL(options.baseUrl).origin
|
|
3464
|
+
}),
|
|
3465
|
+
step
|
|
3466
|
+
);
|
|
3467
|
+
const steady = async () => {
|
|
3468
|
+
let previous = await page.screenshot(cover);
|
|
3469
|
+
for (let waited = 0; waited < SETTLE_MS; waited += FRAME_MS) {
|
|
3470
|
+
await pause(FRAME_MS);
|
|
3471
|
+
const next = await page.screenshot(cover);
|
|
3472
|
+
if (sameBytes(next, previous)) return;
|
|
3473
|
+
previous = next;
|
|
3474
|
+
}
|
|
3475
|
+
};
|
|
3476
|
+
const reach = async (route) => {
|
|
3477
|
+
let where = await location2();
|
|
3478
|
+
for (let waited = 0; !onScreen(route, where) && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3479
|
+
await pause(FRAME_MS);
|
|
3480
|
+
where = await location2();
|
|
3481
|
+
}
|
|
3482
|
+
if (onScreen(route, where)) {
|
|
3483
|
+
await page.quiet?.(SETTLE_MS);
|
|
3484
|
+
await steady();
|
|
3485
|
+
}
|
|
3486
|
+
return where;
|
|
3487
|
+
};
|
|
3488
|
+
const lead = async (step) => {
|
|
3489
|
+
const where = await reach(step.route);
|
|
3490
|
+
if (!onScreen(step.route, where)) return `the browser is on ${where}, not ${step.route}`;
|
|
3491
|
+
let resolution = await measureOn(step, where);
|
|
3492
|
+
for (let waited = 0; !resolution.ok && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3493
|
+
await pause(FRAME_MS);
|
|
3494
|
+
resolution = await measureOn(step, where);
|
|
3495
|
+
}
|
|
3496
|
+
if (!resolution.ok) return resolution.because;
|
|
3497
|
+
const target = centreOf(resolution.control.box);
|
|
3498
|
+
const action = step.action === "auto" ? actionForRole(resolution.control.role, step.value) : step.action;
|
|
3499
|
+
if (action === "fill") {
|
|
3500
|
+
if (!step.value) return `it types into ${resolution.control.name}, and says nothing to type`;
|
|
3501
|
+
await pointer({ kind: "focus", at: target });
|
|
3502
|
+
await type(step.value);
|
|
3503
|
+
} else if (action === "select") {
|
|
3504
|
+
await pointer({ kind: "select", at: target, value: step.value ?? "" });
|
|
3505
|
+
} else {
|
|
3506
|
+
await pointer({ kind: "click", at: target });
|
|
3507
|
+
}
|
|
3508
|
+
await page.quiet?.(SETTLE_MS);
|
|
3509
|
+
await steady();
|
|
3510
|
+
return null;
|
|
3511
|
+
};
|
|
3425
3512
|
await page.goto(new URL(script.visit, options.baseUrl).toString());
|
|
3426
3513
|
if (await page.isSignIn()) return refuse("its first screen asked for a sign-in, so this session is not the demo tenant's");
|
|
3427
3514
|
const opened = await location2();
|
|
3428
|
-
|
|
3429
|
-
|
|
3515
|
+
const performedFirst = script.before?.[0] ?? script.steps[0];
|
|
3516
|
+
if (!onScreen(performedFirst.route, opened)) return fail(`it opened on ${opened}, not ${performedFirst.route}`);
|
|
3517
|
+
for (const [index, step] of (script.before ?? []).entries()) {
|
|
3518
|
+
const problem = await lead(step);
|
|
3519
|
+
if (problem) return fail(`before the recording, step ${index + 1}: ${problem}`);
|
|
3520
|
+
}
|
|
3521
|
+
const starting = script.before?.length ? await reach(script.steps[0].route) : opened;
|
|
3522
|
+
if (!onScreen(script.steps[0].route, starting)) return fail(`once the steps before it were done, the browser is on ${starting}, not ${script.steps[0].route}`);
|
|
3523
|
+
const before = await judge(starting);
|
|
3430
3524
|
if (before) return refuse(before);
|
|
3431
3525
|
let resting = START;
|
|
3432
3526
|
let poster;
|
|
@@ -3453,26 +3547,17 @@ async function recordClip(options, script, touched) {
|
|
|
3453
3547
|
if (!onScreen(step.route, where)) return fail(`step ${index + 1}: the browser is on ${where}, not ${step.route}`);
|
|
3454
3548
|
const arrived = await judge(where);
|
|
3455
3549
|
if (arrived) return refuse(arrived);
|
|
3456
|
-
|
|
3457
|
-
buildLayoutManifest(await layout(cover), {
|
|
3458
|
-
shotId: script.id,
|
|
3459
|
-
route: step.route,
|
|
3460
|
-
path: where.split("?")[0],
|
|
3461
|
-
variant: "desktop",
|
|
3462
|
-
image: { ...CLIP_VIEWPORT },
|
|
3463
|
-
origin: new URL(options.baseUrl).origin
|
|
3464
|
-
}),
|
|
3465
|
-
step
|
|
3466
|
-
);
|
|
3467
|
-
let resolution = await measure();
|
|
3550
|
+
let resolution = await measureOn(step, where);
|
|
3468
3551
|
for (let waited = 0; !resolution.ok && waited < STEP_WAIT_MS; waited += FRAME_MS) {
|
|
3469
3552
|
await cursor(resting);
|
|
3470
3553
|
await film();
|
|
3471
|
-
resolution = await
|
|
3554
|
+
resolution = await measureOn(step, where);
|
|
3472
3555
|
}
|
|
3473
3556
|
if (!resolution.ok) return fail(`step ${index + 1}: ${resolution.because}`);
|
|
3474
3557
|
const target = centreOf(resolution.control.box);
|
|
3475
3558
|
const startedAt = at;
|
|
3559
|
+
const action = step.action === "auto" ? actionForRole(resolution.control.role, step.value) : step.action;
|
|
3560
|
+
if (action === "fill" && !step.value) return fail(`step ${index + 1}: it types into ${resolution.control.name}, and says nothing to type`);
|
|
3476
3561
|
for (const point of cursorPath(resting, target, MOVE_FRAMES)) {
|
|
3477
3562
|
await cursor(point);
|
|
3478
3563
|
await shoot(FRAME_MS);
|
|
@@ -3482,14 +3567,15 @@ async function recordClip(options, script, touched) {
|
|
|
3482
3567
|
poster ??= frames[frames.length - 1].file;
|
|
3483
3568
|
await cursor(target, true);
|
|
3484
3569
|
await shoot(PRESS_MS);
|
|
3485
|
-
if (
|
|
3570
|
+
if (action === "fill") {
|
|
3571
|
+
const value = step.value ?? "";
|
|
3486
3572
|
await pointer({ kind: "focus", at: target });
|
|
3487
3573
|
await cursor(target, false);
|
|
3488
|
-
for (const chunk of chunksOf(
|
|
3574
|
+
for (const chunk of chunksOf(value, Math.max(CHARS_PER_FRAME, Math.ceil(value.length / TYPING_FRAMES)))) {
|
|
3489
3575
|
await type(chunk);
|
|
3490
3576
|
await shoot(FRAME_MS);
|
|
3491
3577
|
}
|
|
3492
|
-
} else if (
|
|
3578
|
+
} else if (action === "select") {
|
|
3493
3579
|
await pointer({ kind: "select", at: target, value: step.value ?? "" });
|
|
3494
3580
|
} else {
|
|
3495
3581
|
await pointer({ kind: "click", at: target });
|
|
@@ -3507,9 +3593,9 @@ async function recordClip(options, script, touched) {
|
|
|
3507
3593
|
if (after) return refuse(after);
|
|
3508
3594
|
steps.push({
|
|
3509
3595
|
route: step.route,
|
|
3510
|
-
role:
|
|
3596
|
+
role: resolution.control.role,
|
|
3511
3597
|
name: resolution.control.name,
|
|
3512
|
-
action
|
|
3598
|
+
action,
|
|
3513
3599
|
says: step.says,
|
|
3514
3600
|
atMs: startedAt,
|
|
3515
3601
|
box: resolution.control.box
|
|
@@ -3559,7 +3645,7 @@ async function recordClip(options, script, touched) {
|
|
|
3559
3645
|
await rm(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
3560
3646
|
}
|
|
3561
3647
|
}
|
|
3562
|
-
var CLIP_BUDGET_MS =
|
|
3648
|
+
var CLIP_BUDGET_MS = 20 * 6e4;
|
|
3563
3649
|
async function runClips(options) {
|
|
3564
3650
|
const started = Date.now();
|
|
3565
3651
|
const outcomes = [];
|
|
@@ -4767,6 +4853,28 @@ async function writeLocalClip(dir, clip) {
|
|
|
4767
4853
|
durationMs: clip.recording.durationMs
|
|
4768
4854
|
};
|
|
4769
4855
|
}
|
|
4856
|
+
function articleClipsToRecord(raw, examples) {
|
|
4857
|
+
if (!Array.isArray(raw)) return readClipScripts(raw, CLIP_MAX_ARTICLE_SCRIPTS);
|
|
4858
|
+
const named = examples ?? { paths: /* @__PURE__ */ new Map(), refused: [] };
|
|
4859
|
+
const unopened = [];
|
|
4860
|
+
const opened = raw.map((value) => {
|
|
4861
|
+
const script = typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
|
|
4862
|
+
const visit = typeof script["visit"] === "string" ? script["visit"] : "";
|
|
4863
|
+
const resolved = resolveVisit(visit, named);
|
|
4864
|
+
if (!("needs" in resolved)) return { ...script, visit: resolved.visit };
|
|
4865
|
+
unopened.push({ id: typeof script["id"] === "string" ? script["id"] : "a clip", because: describeMissingExample(visit, resolved.needs) });
|
|
4866
|
+
return void 0;
|
|
4867
|
+
});
|
|
4868
|
+
const read = readClipScripts(
|
|
4869
|
+
opened.filter((value) => value !== void 0),
|
|
4870
|
+
CLIP_MAX_ARTICLE_SCRIPTS
|
|
4871
|
+
);
|
|
4872
|
+
return { scripts: read.scripts, refused: [...unopened, ...read.refused] };
|
|
4873
|
+
}
|
|
4874
|
+
function clipsToRecord(seeded, articles) {
|
|
4875
|
+
const named = new Set(seeded.map((script) => script.id));
|
|
4876
|
+
return [...seeded, ...articles.filter((script) => !named.has(script.id))];
|
|
4877
|
+
}
|
|
4770
4878
|
function clipForm(clip) {
|
|
4771
4879
|
const id = clip.recording.clipId;
|
|
4772
4880
|
const form = new FormData();
|
|
@@ -4784,9 +4892,18 @@ async function clipsOnDisk(dir) {
|
|
|
4784
4892
|
return /* @__PURE__ */ new Map();
|
|
4785
4893
|
}
|
|
4786
4894
|
}
|
|
4895
|
+
function unrecordedLines(refused, skipped) {
|
|
4896
|
+
const lines = [
|
|
4897
|
+
...skipped.map((piece) => ` ${piece.slug ? `${piece.slug}${piece.at ? ` at ${piece.at}` : ""}` : "The help desk"}: ${piece.because}`),
|
|
4898
|
+
...refused.map((entry) => ` ${entry.id}: ${entry.because}`)
|
|
4899
|
+
];
|
|
4900
|
+
if (!lines.length) return [];
|
|
4901
|
+
const shown = lines.slice(0, 12);
|
|
4902
|
+
return [`${lines.length} piece(s) of the articles' steps are not recorded:`, ...shown, ...lines.length > shown.length ? [` and ${lines.length - shown.length} more.`] : []];
|
|
4903
|
+
}
|
|
4787
4904
|
async function recordSeededClips(args) {
|
|
4788
4905
|
if (!args.scripts.length) {
|
|
4789
|
-
console.log("\n--clips was given, but the seed command named no clips, so none were recorded.");
|
|
4906
|
+
console.log("\n--clips was given, but the seed command named no clips and no article asks for one, so none were recorded.");
|
|
4790
4907
|
return [];
|
|
4791
4908
|
}
|
|
4792
4909
|
if (!await ffmpegAvailable()) {
|
|
@@ -4799,9 +4916,12 @@ async function recordSeededClips(args) {
|
|
|
4799
4916
|
);
|
|
4800
4917
|
const previous = args.local ? await clipsOnDisk(args.local) : /* @__PURE__ */ new Map();
|
|
4801
4918
|
const written = [];
|
|
4802
|
-
|
|
4803
|
-
|
|
4804
|
-
`
|
|
4919
|
+
const asked = args.scripts.length - args.named;
|
|
4920
|
+
console.log(
|
|
4921
|
+
`
|
|
4922
|
+
The seed command named ${args.named} clip(s)${asked ? `, and the help desk's how-to and troubleshooting articles ask for ${asked} more, one for each piece of their steps` : ""}. Each is recorded in the demo tenant the seed built when it is new or something it shows has changed.
|
|
4923
|
+
`
|
|
4924
|
+
);
|
|
4805
4925
|
const outcomes = await runClips({
|
|
4806
4926
|
baseUrl: args.baseUrl,
|
|
4807
4927
|
browser: args.browser,
|
|
@@ -5142,8 +5262,19 @@ ${unclassified.join("\n")}`);
|
|
|
5142
5262
|
Written to ${local}: ${kept.length} frame(s) and manifest.json.`);
|
|
5143
5263
|
}
|
|
5144
5264
|
if (parsed.clips) {
|
|
5265
|
+
const named = seeded?.clips ?? [];
|
|
5266
|
+
const articles = seeded ? articleClipsToRecord(orders.clipScripts, seeded.examples) : { scripts: [], refused: [] };
|
|
5267
|
+
const asked = Array.isArray(orders.clipScripts) ? orders.clipScripts.length : 0;
|
|
5268
|
+
if (!seeded && asked) {
|
|
5269
|
+
console.log(`
|
|
5270
|
+
The help desk's articles ask for ${asked} clip(s), and none is recorded: clips are recorded only in a demo tenant a --seed-command built.`);
|
|
5271
|
+
}
|
|
5272
|
+
const unrecorded = seeded ? unrecordedLines(articles.refused, Array.isArray(orders.clipsSkipped) ? orders.clipsSkipped : []) : [];
|
|
5273
|
+
if (unrecorded.length) console.log(`
|
|
5274
|
+
${unrecorded.join("\n")}`);
|
|
5145
5275
|
await recordSeededClips({
|
|
5146
|
-
scripts:
|
|
5276
|
+
scripts: clipsToRecord(named, articles.scripts),
|
|
5277
|
+
named: named.length,
|
|
5147
5278
|
browser,
|
|
5148
5279
|
baseUrl,
|
|
5149
5280
|
clearance,
|
|
@@ -5307,13 +5438,16 @@ if (runningAsCommand(import.meta.url)) {
|
|
|
5307
5438
|
);
|
|
5308
5439
|
}
|
|
5309
5440
|
export {
|
|
5441
|
+
articleClipsToRecord,
|
|
5310
5442
|
clipForm,
|
|
5443
|
+
clipsToRecord,
|
|
5311
5444
|
frameForm,
|
|
5312
5445
|
framesOnDisk,
|
|
5313
5446
|
main,
|
|
5314
5447
|
onePerScreen,
|
|
5315
5448
|
seededLast,
|
|
5316
5449
|
shotsFromRepo,
|
|
5450
|
+
unrecordedLines,
|
|
5317
5451
|
withExamples,
|
|
5318
5452
|
withPhoneShots,
|
|
5319
5453
|
writeLocalClip,
|