@olomonhelpdesk2/helpdesk 0.1.0 → 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 +80 -41
- package/helpdesk-seed.mjs +22 -0
- package/helpdesk.mjs +19 -4
- 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,17 +1996,51 @@ 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
|
-
)
|
|
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
|
+
}
|
|
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
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// adapters/invoked-as-command.ts
|
|
2035
|
+
import { realpathSync } from "node:fs";
|
|
2036
|
+
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
2037
|
+
function runningAsCommand(moduleUrl) {
|
|
2038
|
+
const invoked = process.argv[1];
|
|
2039
|
+
if (!invoked) return false;
|
|
2040
|
+
try {
|
|
2041
|
+
return moduleUrl === pathToFileURL2(realpathSync(invoked)).href;
|
|
2001
2042
|
} catch {
|
|
2002
|
-
|
|
2003
|
-
"Capture needs Playwright. Install it beside this command with `npm i -D playwright-core` and make sure a Chrome is available (set CAPTURE_CHROME to its path if it is not on the default one)."
|
|
2004
|
-
);
|
|
2043
|
+
return false;
|
|
2005
2044
|
}
|
|
2006
2045
|
}
|
|
2007
2046
|
|
|
@@ -2032,7 +2071,7 @@ function looksLikeIdentifier(segment) {
|
|
|
2032
2071
|
return /\d/.test(segment) && segment.length >= 3;
|
|
2033
2072
|
}
|
|
2034
2073
|
function templatise(paths) {
|
|
2035
|
-
const split = paths.map((
|
|
2074
|
+
const split = paths.map((path3) => path3.split("/").filter(Boolean));
|
|
2036
2075
|
const depth2 = Math.max(0, ...split.map((segments) => segments.length));
|
|
2037
2076
|
const parameters = /* @__PURE__ */ new Set();
|
|
2038
2077
|
const prefixOf = (segments, upTo) => {
|
|
@@ -2135,9 +2174,9 @@ var CassetteIndex = class {
|
|
|
2135
2174
|
return void 0;
|
|
2136
2175
|
}
|
|
2137
2176
|
};
|
|
2138
|
-
function matchesTemplate(template,
|
|
2177
|
+
function matchesTemplate(template, path3) {
|
|
2139
2178
|
const wanted = template.split("/").filter(Boolean);
|
|
2140
|
-
const actual =
|
|
2179
|
+
const actual = path3.split("/").filter(Boolean);
|
|
2141
2180
|
if (wanted.length !== actual.length) return false;
|
|
2142
2181
|
return wanted.every((segment, index) => segment.startsWith(":") || segment === actual[index]);
|
|
2143
2182
|
}
|
|
@@ -2168,10 +2207,10 @@ function shut(server) {
|
|
|
2168
2207
|
});
|
|
2169
2208
|
}
|
|
2170
2209
|
function splitUrl(url) {
|
|
2171
|
-
const [
|
|
2210
|
+
const [path3, search] = url.split("?");
|
|
2172
2211
|
const query = {};
|
|
2173
2212
|
for (const [name, value] of new URLSearchParams(search ?? "")) query[name] = value;
|
|
2174
|
-
return { path:
|
|
2213
|
+
return { path: path3 ?? "/", query };
|
|
2175
2214
|
}
|
|
2176
2215
|
async function replayFrom(options) {
|
|
2177
2216
|
const index = new CassetteIndex(options.cassette);
|
|
@@ -2179,14 +2218,14 @@ async function replayFrom(options) {
|
|
|
2179
2218
|
const served = /* @__PURE__ */ new Set();
|
|
2180
2219
|
const server = createServer((incoming, outgoing) => {
|
|
2181
2220
|
void (async () => {
|
|
2182
|
-
const { path:
|
|
2221
|
+
const { path: path3, query } = splitUrl(incoming.url ?? "/");
|
|
2183
2222
|
await bodyOf(incoming);
|
|
2184
|
-
const found = index.find({ method: incoming.method ?? "GET", path:
|
|
2223
|
+
const found = index.find({ method: incoming.method ?? "GET", path: path3, query });
|
|
2185
2224
|
if (!found) {
|
|
2186
2225
|
const from = incoming.headers["referer"] ?? incoming.headers["x-seed-page"];
|
|
2187
2226
|
const miss = {
|
|
2188
2227
|
method: incoming.method ?? "GET",
|
|
2189
|
-
path:
|
|
2228
|
+
path: path3,
|
|
2190
2229
|
...Object.keys(query).length ? { query } : {},
|
|
2191
2230
|
...typeof from === "string" ? { from: pageOf(from) } : {}
|
|
2192
2231
|
};
|
|
@@ -2422,8 +2461,8 @@ function gapsFrom(misses, cassette) {
|
|
|
2422
2461
|
const templates = templatise([...known, ...misses.map((miss) => miss.path)]);
|
|
2423
2462
|
const grouped = /* @__PURE__ */ new Map();
|
|
2424
2463
|
for (const miss of misses) {
|
|
2425
|
-
const
|
|
2426
|
-
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 } : {} });
|
|
2427
2466
|
const existing = grouped.get(key);
|
|
2428
2467
|
if (existing) {
|
|
2429
2468
|
existing.asked += 1;
|
|
@@ -2432,7 +2471,7 @@ function gapsFrom(misses, cassette) {
|
|
|
2432
2471
|
}
|
|
2433
2472
|
grouped.set(key, {
|
|
2434
2473
|
method: miss.method.toUpperCase(),
|
|
2435
|
-
path:
|
|
2474
|
+
path: path3,
|
|
2436
2475
|
asked: 1,
|
|
2437
2476
|
broke: miss.from ? [miss.from] : [],
|
|
2438
2477
|
example: miss.path
|
|
@@ -2502,7 +2541,7 @@ function coverageReport(found, total) {
|
|
|
2502
2541
|
|
|
2503
2542
|
// adapters/capture-bin.ts
|
|
2504
2543
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2505
|
-
import
|
|
2544
|
+
import path2 from "node:path";
|
|
2506
2545
|
|
|
2507
2546
|
// adapters/map-cli.ts
|
|
2508
2547
|
import { execFileSync } from "node:child_process";
|
|
@@ -2515,14 +2554,14 @@ var ROUTE_GROUP = /^\((.+)\)$/;
|
|
|
2515
2554
|
var DYNAMIC = /^\[{1,2}(\.{3})?(.+?)\]{1,2}$/;
|
|
2516
2555
|
var PARALLEL = /^@/;
|
|
2517
2556
|
var INTERCEPTING = /^\(\.+\)/;
|
|
2518
|
-
function classify(
|
|
2519
|
-
if (
|
|
2557
|
+
function classify(path3) {
|
|
2558
|
+
if (path3 === "/api" || path3.startsWith("/api/")) {
|
|
2520
2559
|
return { skipped: "not-html", note: "an API route, not a screen" };
|
|
2521
2560
|
}
|
|
2522
|
-
if (/(^|\/)(admin|internal|debug|_dev)(\/|$)/.test(
|
|
2561
|
+
if (/(^|\/)(admin|internal|debug|_dev)(\/|$)/.test(path3)) {
|
|
2523
2562
|
return { skipped: "internal-tooling", note: "internal tooling" };
|
|
2524
2563
|
}
|
|
2525
|
-
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)) {
|
|
2526
2565
|
return { skipped: "unauthenticated-flow", note: "an authentication step" };
|
|
2527
2566
|
}
|
|
2528
2567
|
return void 0;
|
|
@@ -2626,8 +2665,8 @@ function enclosingLayouts(pageFile, layouts) {
|
|
|
2626
2665
|
const dir = pageFile.slice(0, pageFile.lastIndexOf("/"));
|
|
2627
2666
|
return layouts.filter((layout) => dir.startsWith(layout.slice(0, layout.lastIndexOf("/")))).sort((a, b) => a.length - b.length);
|
|
2628
2667
|
}
|
|
2629
|
-
function depth(
|
|
2630
|
-
return
|
|
2668
|
+
function depth(path3) {
|
|
2669
|
+
return path3 === "/" ? 0 : path3.split("/").filter(Boolean).length;
|
|
2631
2670
|
}
|
|
2632
2671
|
|
|
2633
2672
|
// core/discovery/article-plan.ts
|
|
@@ -2664,8 +2703,8 @@ var ENTRY_POINT = /* @__PURE__ */ new Set([
|
|
|
2664
2703
|
"integration",
|
|
2665
2704
|
"integrations"
|
|
2666
2705
|
]);
|
|
2667
|
-
function hasSegment(
|
|
2668
|
-
return
|
|
2706
|
+
function hasSegment(path3, vocabulary) {
|
|
2707
|
+
return path3.split("/").filter(Boolean).some((segment) => vocabulary.has(segment.toLowerCase()));
|
|
2669
2708
|
}
|
|
2670
2709
|
function score(route, signals) {
|
|
2671
2710
|
const because = [];
|
|
@@ -2704,8 +2743,8 @@ function tierFor(rank, count) {
|
|
|
2704
2743
|
if (rank <= Math.max(16, Math.ceil(count * 0.6))) return "supporting";
|
|
2705
2744
|
return "minor";
|
|
2706
2745
|
}
|
|
2707
|
-
function slugFor(
|
|
2708
|
-
const segments =
|
|
2746
|
+
function slugFor(path3) {
|
|
2747
|
+
const segments = path3.split("/").filter(Boolean).filter((segment) => !segment.startsWith("["));
|
|
2709
2748
|
return segments.length ? segments.join("/") : "overview";
|
|
2710
2749
|
}
|
|
2711
2750
|
function titleFor(route) {
|
|
@@ -2764,7 +2803,7 @@ function buildArticlePlan(options) {
|
|
|
2764
2803
|
let covered = 0;
|
|
2765
2804
|
const needs = [...merged.entries()].map(([slug, group], index) => {
|
|
2766
2805
|
const rank = index + 1;
|
|
2767
|
-
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);
|
|
2768
2807
|
const already = existing.has(slug) || Boolean(claimed);
|
|
2769
2808
|
if (already) covered += 1;
|
|
2770
2809
|
return {
|
|
@@ -2808,7 +2847,7 @@ function buildStoryboard(options) {
|
|
|
2808
2847
|
const beneath = options.routes.filter(
|
|
2809
2848
|
(route) => !covered.has(route.path) && !route.skipped && [...covered].some((base) => base !== "/" && route.path.startsWith(`${base}/`))
|
|
2810
2849
|
);
|
|
2811
|
-
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));
|
|
2812
2851
|
const limit = options.maxSteps ?? 5;
|
|
2813
2852
|
const steps = ordered.slice(0, limit).map((route, index) => ({
|
|
2814
2853
|
route: route.path,
|
|
@@ -2883,7 +2922,7 @@ function shotsFromRepo(repo, previous) {
|
|
|
2883
2922
|
}
|
|
2884
2923
|
async function previousFrames(out) {
|
|
2885
2924
|
try {
|
|
2886
|
-
const manifest = JSON.parse(await readFile(
|
|
2925
|
+
const manifest = JSON.parse(await readFile(path2.join(out, "manifest.json"), "utf8"));
|
|
2887
2926
|
return new Map((manifest.frames ?? []).map((frame) => [frame.id, frame.sha256]));
|
|
2888
2927
|
} catch {
|
|
2889
2928
|
return /* @__PURE__ */ new Map();
|
|
@@ -2948,7 +2987,7 @@ Approve one on the plan page, then run this again.`
|
|
|
2948
2987
|
appUrl: baseUrl,
|
|
2949
2988
|
seedCommand: parsed.seedCommand,
|
|
2950
2989
|
...parsed.appCommand ? { appCommand: parsed.appCommand } : {},
|
|
2951
|
-
sessionOut: parsed.session ??
|
|
2990
|
+
sessionOut: parsed.session ?? path2.join(process.cwd(), "helpdesk-demo-session.json"),
|
|
2952
2991
|
onLog: (line) => console.log(line)
|
|
2953
2992
|
});
|
|
2954
2993
|
if (seeded.sessionPath) parsed.session = seeded.sessionPath;
|
|
@@ -3058,7 +3097,7 @@ Approve one on the plan page, then run this again.`
|
|
|
3058
3097
|
async send(frame) {
|
|
3059
3098
|
if (local) {
|
|
3060
3099
|
const file = `${frame.shot.id}.png`;
|
|
3061
|
-
await writeFile(
|
|
3100
|
+
await writeFile(path2.join(local, file), frame.image);
|
|
3062
3101
|
frames.push({
|
|
3063
3102
|
id: frame.shot.id,
|
|
3064
3103
|
route: frame.shot.route,
|
|
@@ -3121,7 +3160,7 @@ ${report.captured} photographed, ${report.unchanged} unchanged, ${report.skipped
|
|
|
3121
3160
|
});
|
|
3122
3161
|
}
|
|
3123
3162
|
await writeFile(
|
|
3124
|
-
|
|
3163
|
+
path2.join(local, "manifest.json"),
|
|
3125
3164
|
`${JSON.stringify(
|
|
3126
3165
|
{
|
|
3127
3166
|
schema: "olomon.capture-manifest/1",
|
|
@@ -3261,7 +3300,7 @@ The session will be saved to ${sessionPath} and sent nowhere.
|
|
|
3261
3300
|
Saved ${sessionPath}. Pass it to a capture run with --session ${sessionPath}.`);
|
|
3262
3301
|
return 0;
|
|
3263
3302
|
}
|
|
3264
|
-
if (import.meta.url
|
|
3303
|
+
if (runningAsCommand(import.meta.url)) {
|
|
3265
3304
|
main(process.argv.slice(2)).then(
|
|
3266
3305
|
(code) => process.exit(code),
|
|
3267
3306
|
(error) => {
|
package/helpdesk-seed.mjs
CHANGED
|
@@ -10,6 +10,19 @@ function helpdeskCommand(command, args = []) {
|
|
|
10
10
|
return parts.join(" ");
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
// adapters/invoked-as-command.ts
|
|
14
|
+
import { realpathSync } from "node:fs";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
function runningAsCommand(moduleUrl) {
|
|
17
|
+
const invoked = process.argv[1];
|
|
18
|
+
if (!invoked) return false;
|
|
19
|
+
try {
|
|
20
|
+
return moduleUrl === pathToFileURL(realpathSync(invoked)).href;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
// core/seed/cassette.ts
|
|
14
27
|
var CASSETTE_SCHEMA = "olomon.seed-cassette/1";
|
|
15
28
|
var DROP_HEADERS = /* @__PURE__ */ new Set([
|
|
@@ -1367,6 +1380,15 @@ async function routes(args) {
|
|
|
1367
1380
|
function missFile(cassette) {
|
|
1368
1381
|
return cassette.replace(/\.json$/, "") + ".misses.json";
|
|
1369
1382
|
}
|
|
1383
|
+
if (runningAsCommand(import.meta.url)) {
|
|
1384
|
+
main(process.argv.slice(2)).then(
|
|
1385
|
+
(code) => process.exit(code),
|
|
1386
|
+
(error) => {
|
|
1387
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1388
|
+
process.exit(1);
|
|
1389
|
+
}
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1370
1392
|
export {
|
|
1371
1393
|
carriedReport,
|
|
1372
1394
|
main
|
package/helpdesk.mjs
CHANGED
|
@@ -1067,6 +1067,19 @@ function originOf(cwd) {
|
|
|
1067
1067
|
}
|
|
1068
1068
|
}
|
|
1069
1069
|
|
|
1070
|
+
// adapters/invoked-as-command.ts
|
|
1071
|
+
import { realpathSync } from "node:fs";
|
|
1072
|
+
import { pathToFileURL } from "node:url";
|
|
1073
|
+
function runningAsCommand(moduleUrl) {
|
|
1074
|
+
const invoked = process.argv[1];
|
|
1075
|
+
if (!invoked) return false;
|
|
1076
|
+
try {
|
|
1077
|
+
return moduleUrl === pathToFileURL(realpathSync(invoked)).href;
|
|
1078
|
+
} catch {
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1070
1083
|
// adapters/map-bin.ts
|
|
1071
1084
|
function arg(name) {
|
|
1072
1085
|
const index = process.argv.indexOf(`--${name}`);
|
|
@@ -1091,7 +1104,9 @@ async function main() {
|
|
|
1091
1104
|
});
|
|
1092
1105
|
console.log(result.report);
|
|
1093
1106
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1107
|
+
if (runningAsCommand(import.meta.url)) {
|
|
1108
|
+
main().catch((error) => {
|
|
1109
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
});
|
|
1112
|
+
}
|