@olomonhelpdesk2/helpdesk 0.1.1 → 0.1.2
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 +69 -43
- package/package.json +1 -1
package/helpdesk-capture.mjs
CHANGED
|
@@ -65,8 +65,8 @@ function actionFor(outcome) {
|
|
|
65
65
|
return `${outcome.route} asked for ${outcome.missingEndpoint ?? "data"}, which is not in the recording. Visit this screen while recording, then run this again.`;
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
function subjectOf(
|
|
69
|
-
const segments =
|
|
68
|
+
function subjectOf(path3) {
|
|
69
|
+
const segments = path3.split("/").filter(Boolean).filter((segment) => !/^(api|v\d+)$/.test(segment));
|
|
70
70
|
while (segments.length > 1 && looksLikeAnId(segments[segments.length - 1])) segments.pop();
|
|
71
71
|
const last = segments[segments.length - 1] ?? "record";
|
|
72
72
|
return last.replace(/[-_]/g, " ").replace(/s$/, "");
|
|
@@ -172,11 +172,11 @@ function canonicalisation(asked, landed) {
|
|
|
172
172
|
function samePlace(asked, landed) {
|
|
173
173
|
return stripQuery(asked) === stripQuery(landed);
|
|
174
174
|
}
|
|
175
|
-
function stripQuery(
|
|
176
|
-
return
|
|
175
|
+
function stripQuery(path3) {
|
|
176
|
+
return path3.split(/[?#]/)[0] ?? path3;
|
|
177
177
|
}
|
|
178
|
-
function normalise(
|
|
179
|
-
return stripQuery(
|
|
178
|
+
function normalise(path3) {
|
|
179
|
+
return stripQuery(path3).toLowerCase().replace(/\/index(\.html?)?$/, "/").replace(/\/+$/, "").replace(/^$/, "/");
|
|
180
180
|
}
|
|
181
181
|
function firstSentence(text2) {
|
|
182
182
|
const sentence = text2.trim().split(/(?<=[.!?])\s|\n/)[0] ?? text2;
|
|
@@ -304,6 +304,11 @@ function attestation(run) {
|
|
|
304
304
|
].filter(Boolean).join("\n");
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
+
// adapters/capture-cli.ts
|
|
308
|
+
import { createRequire } from "node:module";
|
|
309
|
+
import path from "node:path";
|
|
310
|
+
import { pathToFileURL } from "node:url";
|
|
311
|
+
|
|
307
312
|
// core/capture/scrub.ts
|
|
308
313
|
var BRANDS = [
|
|
309
314
|
"Chase",
|
|
@@ -1991,28 +1996,49 @@ async function captureSignIn(options) {
|
|
|
1991
1996
|
await browser.close();
|
|
1992
1997
|
}
|
|
1993
1998
|
}
|
|
1999
|
+
async function resolveFrom(directory, specifier) {
|
|
2000
|
+
const resolve = createRequire(path.join(directory, "package.json"));
|
|
2001
|
+
return import(
|
|
2002
|
+
/* webpackIgnore: true */
|
|
2003
|
+
pathToFileURL(resolve.resolve(specifier)).href
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
1994
2006
|
async function loadPlaywright() {
|
|
1995
2007
|
const specifier = "playwright-core";
|
|
1996
|
-
|
|
1997
|
-
|
|
2008
|
+
const given = process.env["CAPTURE_PLAYWRIGHT"];
|
|
2009
|
+
const sources = [
|
|
2010
|
+
...given ? [() => resolveFrom(given, specifier)] : [],
|
|
2011
|
+
// Beside this file, which covers a local devDependency and a bundled copy.
|
|
2012
|
+
() => import(
|
|
1998
2013
|
/* webpackIgnore: true */
|
|
1999
2014
|
specifier
|
|
2000
|
-
)
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
)
|
|
2015
|
+
),
|
|
2016
|
+
// From the repository being photographed, which covers an npx invocation
|
|
2017
|
+
// in a project that already has Playwright. Many do: the capture rig
|
|
2018
|
+
// detector exists because so many repositories already drive a browser.
|
|
2019
|
+
() => resolveFrom(process.cwd(), specifier)
|
|
2020
|
+
];
|
|
2021
|
+
for (const source of sources) {
|
|
2022
|
+
try {
|
|
2023
|
+
const loaded = await source();
|
|
2024
|
+
const chromium = loaded.chromium ?? loaded.default?.chromium;
|
|
2025
|
+
if (chromium) return { chromium };
|
|
2026
|
+
} catch {
|
|
2027
|
+
}
|
|
2005
2028
|
}
|
|
2029
|
+
throw new Error(
|
|
2030
|
+
"Capture needs Playwright. Install it in the repository you are photographing with `npm i -D playwright-core`, or point CAPTURE_PLAYWRIGHT at a copy, and make sure a Chrome is available (set CAPTURE_CHROME to its path if it is not on the default one)."
|
|
2031
|
+
);
|
|
2006
2032
|
}
|
|
2007
2033
|
|
|
2008
2034
|
// adapters/invoked-as-command.ts
|
|
2009
2035
|
import { realpathSync } from "node:fs";
|
|
2010
|
-
import { pathToFileURL } from "node:url";
|
|
2036
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
2011
2037
|
function runningAsCommand(moduleUrl) {
|
|
2012
2038
|
const invoked = process.argv[1];
|
|
2013
2039
|
if (!invoked) return false;
|
|
2014
2040
|
try {
|
|
2015
|
-
return moduleUrl ===
|
|
2041
|
+
return moduleUrl === pathToFileURL2(realpathSync(invoked)).href;
|
|
2016
2042
|
} catch {
|
|
2017
2043
|
return false;
|
|
2018
2044
|
}
|
|
@@ -2045,7 +2071,7 @@ function looksLikeIdentifier(segment) {
|
|
|
2045
2071
|
return /\d/.test(segment) && segment.length >= 3;
|
|
2046
2072
|
}
|
|
2047
2073
|
function templatise(paths) {
|
|
2048
|
-
const split = paths.map((
|
|
2074
|
+
const split = paths.map((path3) => path3.split("/").filter(Boolean));
|
|
2049
2075
|
const depth2 = Math.max(0, ...split.map((segments) => segments.length));
|
|
2050
2076
|
const parameters = /* @__PURE__ */ new Set();
|
|
2051
2077
|
const prefixOf = (segments, upTo) => {
|
|
@@ -2148,9 +2174,9 @@ var CassetteIndex = class {
|
|
|
2148
2174
|
return void 0;
|
|
2149
2175
|
}
|
|
2150
2176
|
};
|
|
2151
|
-
function matchesTemplate(template,
|
|
2177
|
+
function matchesTemplate(template, path3) {
|
|
2152
2178
|
const wanted = template.split("/").filter(Boolean);
|
|
2153
|
-
const actual =
|
|
2179
|
+
const actual = path3.split("/").filter(Boolean);
|
|
2154
2180
|
if (wanted.length !== actual.length) return false;
|
|
2155
2181
|
return wanted.every((segment, index) => segment.startsWith(":") || segment === actual[index]);
|
|
2156
2182
|
}
|
|
@@ -2181,10 +2207,10 @@ function shut(server) {
|
|
|
2181
2207
|
});
|
|
2182
2208
|
}
|
|
2183
2209
|
function splitUrl(url) {
|
|
2184
|
-
const [
|
|
2210
|
+
const [path3, search] = url.split("?");
|
|
2185
2211
|
const query = {};
|
|
2186
2212
|
for (const [name, value] of new URLSearchParams(search ?? "")) query[name] = value;
|
|
2187
|
-
return { path:
|
|
2213
|
+
return { path: path3 ?? "/", query };
|
|
2188
2214
|
}
|
|
2189
2215
|
async function replayFrom(options) {
|
|
2190
2216
|
const index = new CassetteIndex(options.cassette);
|
|
@@ -2192,14 +2218,14 @@ async function replayFrom(options) {
|
|
|
2192
2218
|
const served = /* @__PURE__ */ new Set();
|
|
2193
2219
|
const server = createServer((incoming, outgoing) => {
|
|
2194
2220
|
void (async () => {
|
|
2195
|
-
const { path:
|
|
2221
|
+
const { path: path3, query } = splitUrl(incoming.url ?? "/");
|
|
2196
2222
|
await bodyOf(incoming);
|
|
2197
|
-
const found = index.find({ method: incoming.method ?? "GET", path:
|
|
2223
|
+
const found = index.find({ method: incoming.method ?? "GET", path: path3, query });
|
|
2198
2224
|
if (!found) {
|
|
2199
2225
|
const from = incoming.headers["referer"] ?? incoming.headers["x-seed-page"];
|
|
2200
2226
|
const miss = {
|
|
2201
2227
|
method: incoming.method ?? "GET",
|
|
2202
|
-
path:
|
|
2228
|
+
path: path3,
|
|
2203
2229
|
...Object.keys(query).length ? { query } : {},
|
|
2204
2230
|
...typeof from === "string" ? { from: pageOf(from) } : {}
|
|
2205
2231
|
};
|
|
@@ -2435,8 +2461,8 @@ function gapsFrom(misses, cassette) {
|
|
|
2435
2461
|
const templates = templatise([...known, ...misses.map((miss) => miss.path)]);
|
|
2436
2462
|
const grouped = /* @__PURE__ */ new Map();
|
|
2437
2463
|
for (const miss of misses) {
|
|
2438
|
-
const
|
|
2439
|
-
const key = interactionKey({ method: miss.method, path:
|
|
2464
|
+
const path3 = templates.get(miss.path) ?? miss.path;
|
|
2465
|
+
const key = interactionKey({ method: miss.method, path: path3, ...miss.query ? { query: miss.query } : {} });
|
|
2440
2466
|
const existing = grouped.get(key);
|
|
2441
2467
|
if (existing) {
|
|
2442
2468
|
existing.asked += 1;
|
|
@@ -2445,7 +2471,7 @@ function gapsFrom(misses, cassette) {
|
|
|
2445
2471
|
}
|
|
2446
2472
|
grouped.set(key, {
|
|
2447
2473
|
method: miss.method.toUpperCase(),
|
|
2448
|
-
path:
|
|
2474
|
+
path: path3,
|
|
2449
2475
|
asked: 1,
|
|
2450
2476
|
broke: miss.from ? [miss.from] : [],
|
|
2451
2477
|
example: miss.path
|
|
@@ -2515,7 +2541,7 @@ function coverageReport(found, total) {
|
|
|
2515
2541
|
|
|
2516
2542
|
// adapters/capture-bin.ts
|
|
2517
2543
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2518
|
-
import
|
|
2544
|
+
import path2 from "node:path";
|
|
2519
2545
|
|
|
2520
2546
|
// adapters/map-cli.ts
|
|
2521
2547
|
import { execFileSync } from "node:child_process";
|
|
@@ -2528,14 +2554,14 @@ var ROUTE_GROUP = /^\((.+)\)$/;
|
|
|
2528
2554
|
var DYNAMIC = /^\[{1,2}(\.{3})?(.+?)\]{1,2}$/;
|
|
2529
2555
|
var PARALLEL = /^@/;
|
|
2530
2556
|
var INTERCEPTING = /^\(\.+\)/;
|
|
2531
|
-
function classify(
|
|
2532
|
-
if (
|
|
2557
|
+
function classify(path3) {
|
|
2558
|
+
if (path3 === "/api" || path3.startsWith("/api/")) {
|
|
2533
2559
|
return { skipped: "not-html", note: "an API route, not a screen" };
|
|
2534
2560
|
}
|
|
2535
|
-
if (/(^|\/)(admin|internal|debug|_dev)(\/|$)/.test(
|
|
2561
|
+
if (/(^|\/)(admin|internal|debug|_dev)(\/|$)/.test(path3)) {
|
|
2536
2562
|
return { skipped: "internal-tooling", note: "internal tooling" };
|
|
2537
2563
|
}
|
|
2538
|
-
if (/(^|\/)(login|logout|signin|sign-in|signup|sign-up|auth|verify|reset-password)(\/|$)/.test(
|
|
2564
|
+
if (/(^|\/)(login|logout|signin|sign-in|signup|sign-up|auth|verify|reset-password)(\/|$)/.test(path3)) {
|
|
2539
2565
|
return { skipped: "unauthenticated-flow", note: "an authentication step" };
|
|
2540
2566
|
}
|
|
2541
2567
|
return void 0;
|
|
@@ -2639,8 +2665,8 @@ function enclosingLayouts(pageFile, layouts) {
|
|
|
2639
2665
|
const dir = pageFile.slice(0, pageFile.lastIndexOf("/"));
|
|
2640
2666
|
return layouts.filter((layout) => dir.startsWith(layout.slice(0, layout.lastIndexOf("/")))).sort((a, b) => a.length - b.length);
|
|
2641
2667
|
}
|
|
2642
|
-
function depth(
|
|
2643
|
-
return
|
|
2668
|
+
function depth(path3) {
|
|
2669
|
+
return path3 === "/" ? 0 : path3.split("/").filter(Boolean).length;
|
|
2644
2670
|
}
|
|
2645
2671
|
|
|
2646
2672
|
// core/discovery/article-plan.ts
|
|
@@ -2677,8 +2703,8 @@ var ENTRY_POINT = /* @__PURE__ */ new Set([
|
|
|
2677
2703
|
"integration",
|
|
2678
2704
|
"integrations"
|
|
2679
2705
|
]);
|
|
2680
|
-
function hasSegment(
|
|
2681
|
-
return
|
|
2706
|
+
function hasSegment(path3, vocabulary) {
|
|
2707
|
+
return path3.split("/").filter(Boolean).some((segment) => vocabulary.has(segment.toLowerCase()));
|
|
2682
2708
|
}
|
|
2683
2709
|
function score(route, signals) {
|
|
2684
2710
|
const because = [];
|
|
@@ -2717,8 +2743,8 @@ function tierFor(rank, count) {
|
|
|
2717
2743
|
if (rank <= Math.max(16, Math.ceil(count * 0.6))) return "supporting";
|
|
2718
2744
|
return "minor";
|
|
2719
2745
|
}
|
|
2720
|
-
function slugFor(
|
|
2721
|
-
const segments =
|
|
2746
|
+
function slugFor(path3) {
|
|
2747
|
+
const segments = path3.split("/").filter(Boolean).filter((segment) => !segment.startsWith("["));
|
|
2722
2748
|
return segments.length ? segments.join("/") : "overview";
|
|
2723
2749
|
}
|
|
2724
2750
|
function titleFor(route) {
|
|
@@ -2777,7 +2803,7 @@ function buildArticlePlan(options) {
|
|
|
2777
2803
|
let covered = 0;
|
|
2778
2804
|
const needs = [...merged.entries()].map(([slug, group], index) => {
|
|
2779
2805
|
const rank = index + 1;
|
|
2780
|
-
const claimed = [group.entry.route.path, ...group.routes.slice(1) ?? []].map((
|
|
2806
|
+
const claimed = [group.entry.route.path, ...group.routes.slice(1) ?? []].map((path3) => options.documented?.[path3]).find(Boolean);
|
|
2781
2807
|
const already = existing.has(slug) || Boolean(claimed);
|
|
2782
2808
|
if (already) covered += 1;
|
|
2783
2809
|
return {
|
|
@@ -2821,7 +2847,7 @@ function buildStoryboard(options) {
|
|
|
2821
2847
|
const beneath = options.routes.filter(
|
|
2822
2848
|
(route) => !covered.has(route.path) && !route.skipped && [...covered].some((base) => base !== "/" && route.path.startsWith(`${base}/`))
|
|
2823
2849
|
);
|
|
2824
|
-
const ordered = [...covered].map((
|
|
2850
|
+
const ordered = [...covered].map((path3) => byPath.get(path3)).filter((route) => Boolean(route) && !route.skipped).sort((a, b) => a.path.length - b.path.length).concat(beneath.sort((a, b) => a.path.length - b.path.length));
|
|
2825
2851
|
const limit = options.maxSteps ?? 5;
|
|
2826
2852
|
const steps = ordered.slice(0, limit).map((route, index) => ({
|
|
2827
2853
|
route: route.path,
|
|
@@ -2896,7 +2922,7 @@ function shotsFromRepo(repo, previous) {
|
|
|
2896
2922
|
}
|
|
2897
2923
|
async function previousFrames(out) {
|
|
2898
2924
|
try {
|
|
2899
|
-
const manifest = JSON.parse(await readFile(
|
|
2925
|
+
const manifest = JSON.parse(await readFile(path2.join(out, "manifest.json"), "utf8"));
|
|
2900
2926
|
return new Map((manifest.frames ?? []).map((frame) => [frame.id, frame.sha256]));
|
|
2901
2927
|
} catch {
|
|
2902
2928
|
return /* @__PURE__ */ new Map();
|
|
@@ -2961,7 +2987,7 @@ Approve one on the plan page, then run this again.`
|
|
|
2961
2987
|
appUrl: baseUrl,
|
|
2962
2988
|
seedCommand: parsed.seedCommand,
|
|
2963
2989
|
...parsed.appCommand ? { appCommand: parsed.appCommand } : {},
|
|
2964
|
-
sessionOut: parsed.session ??
|
|
2990
|
+
sessionOut: parsed.session ?? path2.join(process.cwd(), "helpdesk-demo-session.json"),
|
|
2965
2991
|
onLog: (line) => console.log(line)
|
|
2966
2992
|
});
|
|
2967
2993
|
if (seeded.sessionPath) parsed.session = seeded.sessionPath;
|
|
@@ -3071,7 +3097,7 @@ Approve one on the plan page, then run this again.`
|
|
|
3071
3097
|
async send(frame) {
|
|
3072
3098
|
if (local) {
|
|
3073
3099
|
const file = `${frame.shot.id}.png`;
|
|
3074
|
-
await writeFile(
|
|
3100
|
+
await writeFile(path2.join(local, file), frame.image);
|
|
3075
3101
|
frames.push({
|
|
3076
3102
|
id: frame.shot.id,
|
|
3077
3103
|
route: frame.shot.route,
|
|
@@ -3134,7 +3160,7 @@ ${report.captured} photographed, ${report.unchanged} unchanged, ${report.skipped
|
|
|
3134
3160
|
});
|
|
3135
3161
|
}
|
|
3136
3162
|
await writeFile(
|
|
3137
|
-
|
|
3163
|
+
path2.join(local, "manifest.json"),
|
|
3138
3164
|
`${JSON.stringify(
|
|
3139
3165
|
{
|
|
3140
3166
|
schema: "olomon.capture-manifest/1",
|