helpshelf 0.1.0 → 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/dist/app-crawler-LPVXXFOR.js +6 -0
- package/dist/chunk-DT7ACGIP.js +212 -0
- package/dist/index.js +141 -271
- package/package.json +3 -3
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// src/crawler/app-crawler.ts
|
|
2
|
+
import { chromium } from "playwright-core";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
async function crawlApp(options) {
|
|
6
|
+
const { url, email, password, snapshot, screenshotDir, maxPages, onProgress } = options;
|
|
7
|
+
const possiblePaths = [
|
|
8
|
+
process.env.CHROME_PATH,
|
|
9
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
10
|
+
`${process.env.HOME}/Library/Caches/ms-playwright/chromium-1208/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing`
|
|
11
|
+
].filter(Boolean);
|
|
12
|
+
let execPath;
|
|
13
|
+
for (const p of possiblePaths) {
|
|
14
|
+
if (existsSync(p)) {
|
|
15
|
+
execPath = p;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const browser = await chromium.launch({
|
|
20
|
+
headless: true,
|
|
21
|
+
...execPath ? { executablePath: execPath } : {}
|
|
22
|
+
});
|
|
23
|
+
const context = await browser.newContext({
|
|
24
|
+
viewport: { width: 1280, height: 800 },
|
|
25
|
+
deviceScaleFactor: 2
|
|
26
|
+
// Retina screenshots
|
|
27
|
+
});
|
|
28
|
+
const page = await context.newPage();
|
|
29
|
+
const pages = [];
|
|
30
|
+
const visited = /* @__PURE__ */ new Set();
|
|
31
|
+
let screenshotCount = 0;
|
|
32
|
+
try {
|
|
33
|
+
onProgress("Navigating to app...");
|
|
34
|
+
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
35
|
+
await page.waitForTimeout(1e3);
|
|
36
|
+
if (email && password) {
|
|
37
|
+
onProgress("Logging in...");
|
|
38
|
+
const loggedIn = await attemptLogin(page, email, password, url);
|
|
39
|
+
if (!loggedIn) {
|
|
40
|
+
onProgress("Login failed \u2014 continuing as anonymous");
|
|
41
|
+
} else {
|
|
42
|
+
onProgress("Logged in successfully");
|
|
43
|
+
await page.waitForTimeout(2e3);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
onProgress("Discovering navigation...");
|
|
47
|
+
const navigation = await discoverNavigation(page);
|
|
48
|
+
const urlsToVisit = buildUrlList(page.url(), navigation, snapshot, maxPages);
|
|
49
|
+
onProgress(`Found ${urlsToVisit.length} pages to document`);
|
|
50
|
+
for (let i = 0; i < urlsToVisit.length; i++) {
|
|
51
|
+
const targetUrl = urlsToVisit[i];
|
|
52
|
+
const path = new URL(targetUrl).pathname;
|
|
53
|
+
if (visited.has(path)) continue;
|
|
54
|
+
visited.add(path);
|
|
55
|
+
onProgress(`[${i + 1}/${urlsToVisit.length}] Capturing ${path}`);
|
|
56
|
+
try {
|
|
57
|
+
await page.goto(targetUrl, { waitUntil: "networkidle", timeout: 15e3 });
|
|
58
|
+
await page.waitForTimeout(800);
|
|
59
|
+
const screenshotName = pathToFilename(path) + ".png";
|
|
60
|
+
const screenshotPath = join(screenshotDir, screenshotName);
|
|
61
|
+
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
62
|
+
screenshotCount++;
|
|
63
|
+
const capture = await extractPageInfo(page, path, screenshotName);
|
|
64
|
+
pages.push(capture);
|
|
65
|
+
} catch (err) {
|
|
66
|
+
onProgress(` \u26A0 Skipped ${path}: ${err instanceof Error ? err.message : "timeout"}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const appName = await page.title() || snapshot?.project.name || null;
|
|
70
|
+
return {
|
|
71
|
+
baseUrl: url,
|
|
72
|
+
pages,
|
|
73
|
+
totalScreenshots: screenshotCount,
|
|
74
|
+
appName,
|
|
75
|
+
navigation
|
|
76
|
+
};
|
|
77
|
+
} finally {
|
|
78
|
+
await browser.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function attemptLogin(page, email, password, baseUrl) {
|
|
82
|
+
const currentUrl = page.url();
|
|
83
|
+
const isLoginPage = currentUrl.includes("login") || currentUrl.includes("signin") || currentUrl.includes("auth");
|
|
84
|
+
if (!isLoginPage) {
|
|
85
|
+
for (const loginPath of ["/login", "/signin", "/auth/login", "/auth/signin"]) {
|
|
86
|
+
try {
|
|
87
|
+
await page.goto(new URL(loginPath, baseUrl).toString(), { waitUntil: "networkidle", timeout: 1e4 });
|
|
88
|
+
if (page.url().includes("login") || page.url().includes("signin") || page.url().includes("auth")) break;
|
|
89
|
+
} catch {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const emailInput = await page.$('input[type="email"], input[name="email"], input[placeholder*="email" i], input[placeholder*="username" i]');
|
|
96
|
+
const passwordInput = await page.$('input[type="password"]');
|
|
97
|
+
if (!emailInput || !passwordInput) return false;
|
|
98
|
+
await emailInput.fill(email);
|
|
99
|
+
await passwordInput.fill(password);
|
|
100
|
+
const submitBtn = await page.$('button[type="submit"], button:has-text("Sign in"), button:has-text("Log in"), button:has-text("Login"), button:has-text("Sign In")');
|
|
101
|
+
if (submitBtn) {
|
|
102
|
+
await submitBtn.click();
|
|
103
|
+
await page.waitForNavigation({ waitUntil: "networkidle", timeout: 1e4 }).catch(() => {
|
|
104
|
+
});
|
|
105
|
+
await page.waitForTimeout(2e3);
|
|
106
|
+
}
|
|
107
|
+
const afterUrl = page.url();
|
|
108
|
+
return !afterUrl.includes("login") && !afterUrl.includes("signin");
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function discoverNavigation(page) {
|
|
114
|
+
const sections = [];
|
|
115
|
+
try {
|
|
116
|
+
const navLinks = await page.$$eval(
|
|
117
|
+
'nav a[href], aside a[href], [role="navigation"] a[href]',
|
|
118
|
+
(anchors) => anchors.map((a) => ({
|
|
119
|
+
text: a.textContent?.trim() || "",
|
|
120
|
+
href: a.getAttribute("href") || ""
|
|
121
|
+
})).filter((l) => l.text && l.href && !l.href.startsWith("#") && !l.href.startsWith("http"))
|
|
122
|
+
);
|
|
123
|
+
if (navLinks.length > 0) {
|
|
124
|
+
sections.push({
|
|
125
|
+
label: "Navigation",
|
|
126
|
+
links: navLinks
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
} catch {
|
|
130
|
+
}
|
|
131
|
+
return { sections };
|
|
132
|
+
}
|
|
133
|
+
function buildUrlList(currentUrl, navigation, snapshot, maxPages) {
|
|
134
|
+
const baseUrl = new URL(currentUrl).origin;
|
|
135
|
+
const urls = /* @__PURE__ */ new Set();
|
|
136
|
+
urls.add(currentUrl);
|
|
137
|
+
for (const section of navigation.sections) {
|
|
138
|
+
for (const link of section.links) {
|
|
139
|
+
try {
|
|
140
|
+
const fullUrl = new URL(link.href, baseUrl).toString();
|
|
141
|
+
urls.add(fullUrl);
|
|
142
|
+
} catch {
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (snapshot) {
|
|
147
|
+
const pagePaths = /* @__PURE__ */ new Set();
|
|
148
|
+
for (const route of snapshot.apiRoutes) {
|
|
149
|
+
if (route.path.startsWith("/api/")) continue;
|
|
150
|
+
pagePaths.add(route.path);
|
|
151
|
+
}
|
|
152
|
+
for (const path of pagePaths) {
|
|
153
|
+
try {
|
|
154
|
+
urls.add(new URL(path, baseUrl).toString());
|
|
155
|
+
} catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return Array.from(urls).slice(0, maxPages);
|
|
160
|
+
}
|
|
161
|
+
async function extractPageInfo(page, path, screenshot) {
|
|
162
|
+
const title = await page.title();
|
|
163
|
+
const headings = await page.$$eval(
|
|
164
|
+
"h1, h2, h3",
|
|
165
|
+
(els) => els.map((el) => el.textContent?.trim() || "").filter(Boolean).slice(0, 10)
|
|
166
|
+
);
|
|
167
|
+
const elements = await page.$$eval(
|
|
168
|
+
"button:visible, a:visible, input:visible, select:visible",
|
|
169
|
+
(els) => els.slice(0, 30).map((el) => ({
|
|
170
|
+
type: el.tagName.toLowerCase() === "button" ? "button" : el.tagName.toLowerCase() === "a" ? "link" : el.tagName.toLowerCase() === "input" ? "input" : el.tagName.toLowerCase() === "select" ? "select" : "button",
|
|
171
|
+
text: el.textContent?.trim().slice(0, 100) || "",
|
|
172
|
+
ariaLabel: el.getAttribute("aria-label")
|
|
173
|
+
}))
|
|
174
|
+
);
|
|
175
|
+
const navItems = await page.$$eval(
|
|
176
|
+
"nav a, aside a",
|
|
177
|
+
(els) => els.map((el) => el.textContent?.trim() || "").filter(Boolean)
|
|
178
|
+
);
|
|
179
|
+
const category = detectCategory(path);
|
|
180
|
+
return {
|
|
181
|
+
url: page.url(),
|
|
182
|
+
path,
|
|
183
|
+
title,
|
|
184
|
+
screenshot,
|
|
185
|
+
headings,
|
|
186
|
+
elements,
|
|
187
|
+
navItems,
|
|
188
|
+
category
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function detectCategory(path) {
|
|
192
|
+
const p = path.toLowerCase();
|
|
193
|
+
if (p.includes("setting")) return "Settings";
|
|
194
|
+
if (p.includes("billing") || p.includes("plan") || p.includes("subscription")) return "Billing";
|
|
195
|
+
if (p.includes("team") || p.includes("member") || p.includes("invite")) return "Team Management";
|
|
196
|
+
if (p.includes("analytic") || p.includes("stat") || p.includes("report")) return "Analytics";
|
|
197
|
+
if (p.includes("content") || p.includes("article") || p.includes("doc")) return "Content Management";
|
|
198
|
+
if (p.includes("integrat") || p.includes("connect") || p.includes("source")) return "Integrations";
|
|
199
|
+
if (p.includes("widget") || p.includes("floatie") || p.includes("preview")) return "Widget";
|
|
200
|
+
if (p.includes("announce")) return "Announcements";
|
|
201
|
+
if (p.includes("help") || p.includes("center")) return "Help Center";
|
|
202
|
+
if (p.includes("login") || p.includes("signup") || p.includes("auth")) return "Authentication";
|
|
203
|
+
if (p === "/" || p.includes("dashboard") || p.includes("overview")) return "Getting Started";
|
|
204
|
+
return "Features";
|
|
205
|
+
}
|
|
206
|
+
function pathToFilename(path) {
|
|
207
|
+
return (path || "home").replace(/^\//, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "") || "home";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
crawlApp
|
|
212
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
6
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
7
|
-
});
|
|
2
|
+
import {
|
|
3
|
+
crawlApp
|
|
4
|
+
} from "./chunk-DT7ACGIP.js";
|
|
8
5
|
|
|
9
6
|
// src/index.ts
|
|
10
7
|
import { Command } from "commander";
|
|
11
8
|
|
|
12
9
|
// src/commands/scan.ts
|
|
13
|
-
import { resolve, relative as relative4 } from "path";
|
|
10
|
+
import { resolve, join as join8, relative as relative4 } from "path";
|
|
14
11
|
import { writeFileSync, existsSync as existsSync8, mkdirSync } from "fs";
|
|
15
12
|
import chalk from "chalk";
|
|
16
13
|
import ora from "ora";
|
|
17
14
|
|
|
18
15
|
// src/scanners/project.ts
|
|
19
16
|
import { join, basename } from "path";
|
|
20
|
-
import {
|
|
17
|
+
import { existsSync } from "fs";
|
|
18
|
+
|
|
19
|
+
// src/utils/fs.ts
|
|
20
|
+
import { readFileSync } from "fs";
|
|
21
|
+
function readFileSafe(path, encoding = "utf-8") {
|
|
22
|
+
try {
|
|
23
|
+
return readFileSync(path, encoding);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/scanners/project.ts
|
|
21
30
|
function scanProjectMeta(dir) {
|
|
22
31
|
const pkgPath = join(dir, "package.json");
|
|
23
32
|
if (existsSync(pkgPath)) {
|
|
24
33
|
try {
|
|
25
|
-
const pkg = JSON.parse(
|
|
34
|
+
const pkg = JSON.parse(readFileSafe(pkgPath) || "{}");
|
|
26
35
|
return {
|
|
27
36
|
name: pkg.name || basename(dir),
|
|
28
37
|
description: pkg.description || null,
|
|
@@ -37,7 +46,7 @@ function scanProjectMeta(dir) {
|
|
|
37
46
|
const pyprojectPath = join(dir, "pyproject.toml");
|
|
38
47
|
if (existsSync(pyprojectPath)) {
|
|
39
48
|
try {
|
|
40
|
-
const content =
|
|
49
|
+
const content = readFileSafe(pyprojectPath) || "";
|
|
41
50
|
const nameMatch = content.match(/^name\s*=\s*"([^"]+)"/m);
|
|
42
51
|
const versionMatch = content.match(/^version\s*=\s*"([^"]+)"/m);
|
|
43
52
|
const descMatch = content.match(/^description\s*=\s*"([^"]+)"/m);
|
|
@@ -64,7 +73,7 @@ function scanProjectMeta(dir) {
|
|
|
64
73
|
|
|
65
74
|
// src/scanners/stack.ts
|
|
66
75
|
import { join as join2 } from "path";
|
|
67
|
-
import {
|
|
76
|
+
import { existsSync as existsSync2 } from "fs";
|
|
68
77
|
function scanStack(dir) {
|
|
69
78
|
const result = {
|
|
70
79
|
framework: null,
|
|
@@ -101,7 +110,7 @@ function scanStack(dir) {
|
|
|
101
110
|
const pkgPath = join2(dir, "package.json");
|
|
102
111
|
if (existsSync2(pkgPath)) {
|
|
103
112
|
try {
|
|
104
|
-
const pkg = JSON.parse(
|
|
113
|
+
const pkg = JSON.parse(readFileSafe(pkgPath) || "{}");
|
|
105
114
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
106
115
|
if (allDeps["next"]) {
|
|
107
116
|
result.framework = "next";
|
|
@@ -161,7 +170,7 @@ function scanStack(dir) {
|
|
|
161
170
|
if (result.language === "python") {
|
|
162
171
|
const reqPath = join2(dir, "requirements.txt");
|
|
163
172
|
if (existsSync2(reqPath)) {
|
|
164
|
-
const reqs =
|
|
173
|
+
const reqs = (readFileSafe(reqPath) || "").toLowerCase();
|
|
165
174
|
if (reqs.includes("django")) {
|
|
166
175
|
result.framework = "django";
|
|
167
176
|
result.technologies.push("Django");
|
|
@@ -180,7 +189,7 @@ function scanStack(dir) {
|
|
|
180
189
|
|
|
181
190
|
// src/scanners/readme.ts
|
|
182
191
|
import { join as join3 } from "path";
|
|
183
|
-
import {
|
|
192
|
+
import { existsSync as existsSync3 } from "fs";
|
|
184
193
|
var README_NAMES = ["README.md", "readme.md", "README.MD", "Readme.md", "README", "readme"];
|
|
185
194
|
function scanReadme(dir) {
|
|
186
195
|
let readmePath = null;
|
|
@@ -192,7 +201,8 @@ function scanReadme(dir) {
|
|
|
192
201
|
}
|
|
193
202
|
}
|
|
194
203
|
if (!readmePath) return null;
|
|
195
|
-
const content =
|
|
204
|
+
const content = readFileSafe(readmePath);
|
|
205
|
+
if (!content) return null;
|
|
196
206
|
return parseMarkdownSections(content);
|
|
197
207
|
}
|
|
198
208
|
function parseMarkdownSections(markdown) {
|
|
@@ -235,7 +245,7 @@ function parseMarkdownSections(markdown) {
|
|
|
235
245
|
|
|
236
246
|
// src/scanners/api-routes.ts
|
|
237
247
|
import { join as join4, relative } from "path";
|
|
238
|
-
import {
|
|
248
|
+
import { existsSync as existsSync4, readdirSync, statSync } from "fs";
|
|
239
249
|
function scanApiRoutes(dir, stack) {
|
|
240
250
|
const routes = [];
|
|
241
251
|
if (stack.framework === "next") {
|
|
@@ -267,7 +277,8 @@ function scanNextAppRoutes(projectDir, appDir, routes) {
|
|
|
267
277
|
if (!filePath.includes("/api/")) return;
|
|
268
278
|
const relPath = relative(appDir, filePath);
|
|
269
279
|
const routePath = "/" + relPath.replace(/\/route\.(ts|js)$/, "").replace(/\[([^\]]+)\]/g, ":$1");
|
|
270
|
-
const content =
|
|
280
|
+
const content = readFileSafe(filePath);
|
|
281
|
+
if (!content) return;
|
|
271
282
|
const methods = extractHttpMethods(content);
|
|
272
283
|
const description = extractRouteDescription(content);
|
|
273
284
|
for (const method of methods) {
|
|
@@ -287,7 +298,8 @@ function scanNextPagesRoutes(projectDir, pagesDir, routes) {
|
|
|
287
298
|
if (!filePath.match(/\.(ts|js|tsx|jsx)$/)) return;
|
|
288
299
|
const relPath = relative(apiDir, filePath);
|
|
289
300
|
const routePath = "/api/" + relPath.replace(/\.(ts|js|tsx|jsx)$/, "").replace(/\[([^\]]+)\]/g, ":$1").replace(/\/index$/, "");
|
|
290
|
-
const content =
|
|
301
|
+
const content = readFileSafe(filePath);
|
|
302
|
+
if (!content) return;
|
|
291
303
|
const description = extractRouteDescription(content);
|
|
292
304
|
routes.push({
|
|
293
305
|
method: "ALL",
|
|
@@ -305,7 +317,8 @@ function scanExpressRoutes(dir, routes) {
|
|
|
305
317
|
if (!existsSync4(fullPath)) continue;
|
|
306
318
|
walkDir(fullPath, (filePath) => {
|
|
307
319
|
if (!filePath.match(/\.(ts|js)$/)) return;
|
|
308
|
-
const content =
|
|
320
|
+
const content = readFileSafe(filePath);
|
|
321
|
+
if (!content) return;
|
|
309
322
|
let match;
|
|
310
323
|
while ((match = routePattern.exec(content)) !== null) {
|
|
311
324
|
routes.push({
|
|
@@ -361,7 +374,7 @@ function walkDir(dir, callback) {
|
|
|
361
374
|
|
|
362
375
|
// src/scanners/env-vars.ts
|
|
363
376
|
import { join as join5, relative as relative2 } from "path";
|
|
364
|
-
import {
|
|
377
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2 } from "fs";
|
|
365
378
|
var PUBLIC_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "NUXT_PUBLIC_", "EXPO_PUBLIC_"];
|
|
366
379
|
function scanEnvVars(dir) {
|
|
367
380
|
const envMap = /* @__PURE__ */ new Map();
|
|
@@ -371,7 +384,8 @@ function scanEnvVars(dir) {
|
|
|
371
384
|
const fullPath = join5(dir, srcDir);
|
|
372
385
|
if (!existsSync5(fullPath)) continue;
|
|
373
386
|
walkSourceFiles(fullPath, (filePath) => {
|
|
374
|
-
const content =
|
|
387
|
+
const content = readFileSafe(filePath);
|
|
388
|
+
if (!content) return;
|
|
375
389
|
const relFile = relative2(dir, filePath);
|
|
376
390
|
const processEnvPattern = /process\.env\.([A-Z][A-Z0-9_]*)/g;
|
|
377
391
|
let match;
|
|
@@ -392,7 +406,8 @@ function scanEnvVars(dir) {
|
|
|
392
406
|
for (const config of rootConfigs) {
|
|
393
407
|
const configPath = join5(dir, config);
|
|
394
408
|
if (!existsSync5(configPath)) continue;
|
|
395
|
-
const content =
|
|
409
|
+
const content = readFileSafe(configPath);
|
|
410
|
+
if (!content) continue;
|
|
396
411
|
const relFile = config;
|
|
397
412
|
const processEnvPattern = /process\.env\.([A-Z][A-Z0-9_]*)/g;
|
|
398
413
|
let match;
|
|
@@ -434,7 +449,9 @@ function parseEnvExample(dir) {
|
|
|
434
449
|
for (const name of candidates) {
|
|
435
450
|
const path = join5(dir, name);
|
|
436
451
|
if (!existsSync5(path)) continue;
|
|
437
|
-
const
|
|
452
|
+
const content = readFileSafe(path);
|
|
453
|
+
if (!content) continue;
|
|
454
|
+
const lines = content.split("\n");
|
|
438
455
|
let lastComment = "";
|
|
439
456
|
for (const line of lines) {
|
|
440
457
|
const trimmed = line.trim();
|
|
@@ -472,7 +489,7 @@ function walkSourceFiles(dir, callback) {
|
|
|
472
489
|
|
|
473
490
|
// src/scanners/dependencies.ts
|
|
474
491
|
import { join as join6 } from "path";
|
|
475
|
-
import {
|
|
492
|
+
import { existsSync as existsSync6 } from "fs";
|
|
476
493
|
var CATEGORY_MAP = {
|
|
477
494
|
// Frameworks
|
|
478
495
|
"next": "framework",
|
|
@@ -570,7 +587,7 @@ function scanDependencies(dir) {
|
|
|
570
587
|
const pkgPath = join6(dir, "package.json");
|
|
571
588
|
if (!existsSync6(pkgPath)) return [];
|
|
572
589
|
try {
|
|
573
|
-
const pkg = JSON.parse(
|
|
590
|
+
const pkg = JSON.parse(readFileSafe(pkgPath) || "{}");
|
|
574
591
|
const deps = [];
|
|
575
592
|
if (pkg.dependencies) {
|
|
576
593
|
for (const [name, version] of Object.entries(pkg.dependencies)) {
|
|
@@ -919,34 +936,55 @@ async function scanCommand(options) {
|
|
|
919
936
|
let readme = null;
|
|
920
937
|
if (options.readme !== false) {
|
|
921
938
|
spinner.start("Parsing README...");
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
939
|
+
try {
|
|
940
|
+
readme = scanReadme(projectDir);
|
|
941
|
+
if (readme) {
|
|
942
|
+
spinner.succeed(`README: ${chalk.cyan(readme.length + " sections")}`);
|
|
943
|
+
} else {
|
|
944
|
+
spinner.warn("No README found");
|
|
945
|
+
}
|
|
946
|
+
} catch {
|
|
947
|
+
spinner.warn("Could not read README (file may be on iCloud or network drive)");
|
|
927
948
|
}
|
|
928
949
|
}
|
|
929
950
|
let apiRoutes = [];
|
|
930
951
|
if (options.api !== false) {
|
|
931
952
|
spinner.start("Detecting API routes...");
|
|
932
|
-
|
|
933
|
-
|
|
953
|
+
try {
|
|
954
|
+
apiRoutes = scanApiRoutes(projectDir, stack);
|
|
955
|
+
spinner.succeed(`API routes: ${chalk.cyan(apiRoutes.length + " endpoints")}`);
|
|
956
|
+
} catch {
|
|
957
|
+
spinner.warn("Could not scan API routes");
|
|
958
|
+
}
|
|
934
959
|
}
|
|
935
960
|
let envVars = [];
|
|
936
961
|
if (options.env !== false) {
|
|
937
962
|
spinner.start("Scanning environment variables...");
|
|
938
|
-
|
|
939
|
-
|
|
963
|
+
try {
|
|
964
|
+
envVars = scanEnvVars(projectDir);
|
|
965
|
+
spinner.succeed(`Env vars: ${chalk.cyan(envVars.length + " variables")}`);
|
|
966
|
+
} catch {
|
|
967
|
+
spinner.warn("Could not scan environment variables");
|
|
968
|
+
}
|
|
940
969
|
}
|
|
941
970
|
let dependencies = [];
|
|
942
971
|
if (options.deps !== false) {
|
|
943
972
|
spinner.start("Analyzing dependencies...");
|
|
944
|
-
|
|
945
|
-
|
|
973
|
+
try {
|
|
974
|
+
dependencies = scanDependencies(projectDir);
|
|
975
|
+
spinner.succeed(`Dependencies: ${chalk.cyan(dependencies.length + " packages")}`);
|
|
976
|
+
} catch {
|
|
977
|
+
spinner.warn("Could not analyze dependencies");
|
|
978
|
+
}
|
|
946
979
|
}
|
|
947
980
|
spinner.start("Mapping file structure...");
|
|
948
|
-
|
|
949
|
-
|
|
981
|
+
let structure = { topLevel: [], sourceFileCount: 0, keyDirs: [] };
|
|
982
|
+
try {
|
|
983
|
+
structure = scanFileStructure(projectDir);
|
|
984
|
+
spinner.succeed(`Structure: ${chalk.cyan(structure.sourceFileCount + " source files")}`);
|
|
985
|
+
} catch {
|
|
986
|
+
spinner.warn("Could not map file structure");
|
|
987
|
+
}
|
|
950
988
|
spinner.start("Generating FAQ items...");
|
|
951
989
|
const faqs = generateFaqs({ project, stack, readme, apiRoutes, envVars, dependencies });
|
|
952
990
|
spinner.succeed(`FAQs: ${chalk.cyan(faqs.length + " items generated")}`);
|
|
@@ -990,6 +1028,50 @@ async function scanCommand(options) {
|
|
|
990
1028
|
log(` ${chalk.dim("Snapshot:")} ${chalk.underline(relative4(process.cwd(), outputPath))}`);
|
|
991
1029
|
log(` ${chalk.dim("Docs template:")} ${chalk.underline(relative4(process.cwd(), templatePath))}`);
|
|
992
1030
|
log(` ${chalk.dim("Duration:")} ${scanDurationMs}ms`);
|
|
1031
|
+
if (options.url) {
|
|
1032
|
+
log();
|
|
1033
|
+
log(chalk.bold("\u{1F4F8} Capturing screenshots..."));
|
|
1034
|
+
try {
|
|
1035
|
+
const { crawlApp: crawlApp2 } = await import("./app-crawler-LPVXXFOR.js");
|
|
1036
|
+
const screenshotDir = resolve(outputDir, "screenshots");
|
|
1037
|
+
mkdirSync(screenshotDir, { recursive: true });
|
|
1038
|
+
const maxPages = parseInt(options.maxPages || "20", 10);
|
|
1039
|
+
const screenshotSpinner = ora("Launching browser...").start();
|
|
1040
|
+
const result = await crawlApp2({
|
|
1041
|
+
url: options.url,
|
|
1042
|
+
email: options.email,
|
|
1043
|
+
password: options.password,
|
|
1044
|
+
snapshot,
|
|
1045
|
+
screenshotDir,
|
|
1046
|
+
maxPages,
|
|
1047
|
+
onProgress: (msg) => {
|
|
1048
|
+
screenshotSpinner.text = msg;
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
screenshotSpinner.succeed(`Captured ${result.totalScreenshots} screenshots from ${result.pages.length} pages`);
|
|
1052
|
+
const manifest = {
|
|
1053
|
+
version: 1,
|
|
1054
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1055
|
+
baseUrl: result.baseUrl,
|
|
1056
|
+
pages: result.pages.map((p) => ({
|
|
1057
|
+
path: p.path,
|
|
1058
|
+
title: p.title,
|
|
1059
|
+
screenshot: p.screenshot,
|
|
1060
|
+
category: p.category,
|
|
1061
|
+
headings: p.headings,
|
|
1062
|
+
elements: p.elements.length,
|
|
1063
|
+
buttons: p.elements.filter((e) => e.type === "button").map((e) => e.text).filter(Boolean),
|
|
1064
|
+
inputs: p.elements.filter((e) => e.type === "input").length
|
|
1065
|
+
})),
|
|
1066
|
+
navigation: result.navigation
|
|
1067
|
+
};
|
|
1068
|
+
writeFileSync(join8(screenshotDir, "manifest.json"), JSON.stringify(manifest, null, 2));
|
|
1069
|
+
log(` ${chalk.dim("Screenshots:")} ${chalk.underline(relative4(process.cwd(), screenshotDir))}`);
|
|
1070
|
+
} catch (err) {
|
|
1071
|
+
log(chalk.yellow(` \u26A0 Screenshots skipped: ${err instanceof Error ? err.message : "browser not available"}`));
|
|
1072
|
+
log(chalk.dim(" Install Playwright with: npx playwright install chromium"));
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
993
1075
|
log();
|
|
994
1076
|
log(` ${chalk.dim("Next steps:")}`);
|
|
995
1077
|
log(` 1. Write docs to ${chalk.cyan(".helpshelf/docs/")} (or let your AI agent write them using the template)`);
|
|
@@ -1106,7 +1188,7 @@ GUIDELINES:
|
|
|
1106
1188
|
|
|
1107
1189
|
// src/commands/push.ts
|
|
1108
1190
|
import { resolve as resolve2, join as join9, basename as basename2 } from "path";
|
|
1109
|
-
import { readFileSync as
|
|
1191
|
+
import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync9, readdirSync as readdirSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
1110
1192
|
import chalk2 from "chalk";
|
|
1111
1193
|
import ora2 from "ora";
|
|
1112
1194
|
async function pushCommand(options) {
|
|
@@ -1217,10 +1299,10 @@ async function pushCommand(options) {
|
|
|
1217
1299
|
}
|
|
1218
1300
|
function readDocsDirectory(dir) {
|
|
1219
1301
|
if (!existsSync9(dir)) return [];
|
|
1220
|
-
const files =
|
|
1302
|
+
const files = readdirSync4(dir).filter((f) => f.endsWith(".md"));
|
|
1221
1303
|
const articles = [];
|
|
1222
1304
|
for (const file of files) {
|
|
1223
|
-
const content =
|
|
1305
|
+
const content = readFileSync2(join9(dir, file), "utf-8");
|
|
1224
1306
|
const slug = basename2(file, ".md");
|
|
1225
1307
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
1226
1308
|
const title = frontmatter.title || slug.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
@@ -1278,22 +1360,21 @@ function loadConfig() {
|
|
|
1278
1360
|
try {
|
|
1279
1361
|
const configPath = resolve2(".helpshelf/config.json");
|
|
1280
1362
|
if (!existsSync9(configPath)) return null;
|
|
1281
|
-
return JSON.parse(
|
|
1363
|
+
return JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
1282
1364
|
} catch {
|
|
1283
1365
|
return null;
|
|
1284
1366
|
}
|
|
1285
1367
|
}
|
|
1286
1368
|
function saveConfig(config) {
|
|
1287
1369
|
try {
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
ws(resolve2(".helpshelf/config.json"), JSON.stringify(config, null, 2));
|
|
1370
|
+
mkdirSync2(resolve2(".helpshelf"), { recursive: true });
|
|
1371
|
+
writeFileSync2(resolve2(".helpshelf/config.json"), JSON.stringify(config, null, 2));
|
|
1291
1372
|
} catch {
|
|
1292
1373
|
}
|
|
1293
1374
|
}
|
|
1294
1375
|
|
|
1295
1376
|
// src/commands/init.ts
|
|
1296
|
-
import { writeFileSync as
|
|
1377
|
+
import { writeFileSync as writeFileSync3, readFileSync as readFileSync3, existsSync as existsSync10, mkdirSync as mkdirSync3 } from "fs";
|
|
1297
1378
|
import { resolve as resolve3, join as join10 } from "path";
|
|
1298
1379
|
import chalk3 from "chalk";
|
|
1299
1380
|
async function initCommand() {
|
|
@@ -1303,12 +1384,12 @@ async function initCommand() {
|
|
|
1303
1384
|
console.log(chalk3.bold("\u{1F680} Initializing HelpShelf"));
|
|
1304
1385
|
console.log();
|
|
1305
1386
|
if (!existsSync10(helpshelfDir)) {
|
|
1306
|
-
|
|
1387
|
+
mkdirSync3(helpshelfDir, { recursive: true });
|
|
1307
1388
|
console.log(chalk3.green(" \u2713"), "Created .helpshelf/ directory");
|
|
1308
1389
|
}
|
|
1309
1390
|
const configPath = join10(helpshelfDir, "config.json");
|
|
1310
1391
|
if (!existsSync10(configPath)) {
|
|
1311
|
-
|
|
1392
|
+
writeFileSync3(configPath, JSON.stringify({
|
|
1312
1393
|
siteHash: "",
|
|
1313
1394
|
apiKey: "",
|
|
1314
1395
|
autoSync: false
|
|
@@ -1317,9 +1398,9 @@ async function initCommand() {
|
|
|
1317
1398
|
}
|
|
1318
1399
|
const gitignorePath = join10(dir, ".gitignore");
|
|
1319
1400
|
if (existsSync10(gitignorePath)) {
|
|
1320
|
-
const gitignore =
|
|
1401
|
+
const gitignore = readFileSync3(gitignorePath, "utf-8");
|
|
1321
1402
|
if (!gitignore.includes(".helpshelf/snapshot.json")) {
|
|
1322
|
-
|
|
1403
|
+
writeFileSync3(gitignorePath, gitignore.trimEnd() + "\n\n# HelpShelf\n.helpshelf/snapshot.json\n");
|
|
1323
1404
|
console.log(chalk3.green(" \u2713"), "Added .helpshelf/snapshot.json to .gitignore");
|
|
1324
1405
|
}
|
|
1325
1406
|
}
|
|
@@ -1332,238 +1413,27 @@ async function initCommand() {
|
|
|
1332
1413
|
}
|
|
1333
1414
|
|
|
1334
1415
|
// src/commands/screenshot.ts
|
|
1335
|
-
import { resolve as
|
|
1336
|
-
import { readFileSync as
|
|
1416
|
+
import { resolve as resolve4, join as join11 } from "path";
|
|
1417
|
+
import { readFileSync as readFileSync4, existsSync as existsSync11, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
|
|
1337
1418
|
import chalk4 from "chalk";
|
|
1338
1419
|
import ora3 from "ora";
|
|
1339
|
-
|
|
1340
|
-
// src/crawler/app-crawler.ts
|
|
1341
|
-
import { chromium } from "playwright-core";
|
|
1342
|
-
import { join as join11 } from "path";
|
|
1343
|
-
import { existsSync as existsSync11 } from "fs";
|
|
1344
|
-
async function crawlApp(options) {
|
|
1345
|
-
const { url, email, password, snapshot, screenshotDir, maxPages, onProgress } = options;
|
|
1346
|
-
const possiblePaths = [
|
|
1347
|
-
process.env.CHROME_PATH,
|
|
1348
|
-
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
1349
|
-
`${process.env.HOME}/Library/Caches/ms-playwright/chromium-1208/chrome-mac-arm64/Google Chrome for Testing.app/Contents/MacOS/Google Chrome for Testing`
|
|
1350
|
-
].filter(Boolean);
|
|
1351
|
-
let execPath;
|
|
1352
|
-
for (const p of possiblePaths) {
|
|
1353
|
-
if (existsSync11(p)) {
|
|
1354
|
-
execPath = p;
|
|
1355
|
-
break;
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
const browser = await chromium.launch({
|
|
1359
|
-
headless: true,
|
|
1360
|
-
...execPath ? { executablePath: execPath } : {}
|
|
1361
|
-
});
|
|
1362
|
-
const context = await browser.newContext({
|
|
1363
|
-
viewport: { width: 1280, height: 800 },
|
|
1364
|
-
deviceScaleFactor: 2
|
|
1365
|
-
// Retina screenshots
|
|
1366
|
-
});
|
|
1367
|
-
const page = await context.newPage();
|
|
1368
|
-
const pages = [];
|
|
1369
|
-
const visited = /* @__PURE__ */ new Set();
|
|
1370
|
-
let screenshotCount = 0;
|
|
1371
|
-
try {
|
|
1372
|
-
onProgress("Navigating to app...");
|
|
1373
|
-
await page.goto(url, { waitUntil: "networkidle", timeout: 3e4 });
|
|
1374
|
-
await page.waitForTimeout(1e3);
|
|
1375
|
-
if (email && password) {
|
|
1376
|
-
onProgress("Logging in...");
|
|
1377
|
-
const loggedIn = await attemptLogin(page, email, password, url);
|
|
1378
|
-
if (!loggedIn) {
|
|
1379
|
-
onProgress("Login failed \u2014 continuing as anonymous");
|
|
1380
|
-
} else {
|
|
1381
|
-
onProgress("Logged in successfully");
|
|
1382
|
-
await page.waitForTimeout(2e3);
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
onProgress("Discovering navigation...");
|
|
1386
|
-
const navigation = await discoverNavigation(page);
|
|
1387
|
-
const urlsToVisit = buildUrlList(page.url(), navigation, snapshot, maxPages);
|
|
1388
|
-
onProgress(`Found ${urlsToVisit.length} pages to document`);
|
|
1389
|
-
for (let i = 0; i < urlsToVisit.length; i++) {
|
|
1390
|
-
const targetUrl = urlsToVisit[i];
|
|
1391
|
-
const path = new URL(targetUrl).pathname;
|
|
1392
|
-
if (visited.has(path)) continue;
|
|
1393
|
-
visited.add(path);
|
|
1394
|
-
onProgress(`[${i + 1}/${urlsToVisit.length}] Capturing ${path}`);
|
|
1395
|
-
try {
|
|
1396
|
-
await page.goto(targetUrl, { waitUntil: "networkidle", timeout: 15e3 });
|
|
1397
|
-
await page.waitForTimeout(800);
|
|
1398
|
-
const screenshotName = pathToFilename(path) + ".png";
|
|
1399
|
-
const screenshotPath = join11(screenshotDir, screenshotName);
|
|
1400
|
-
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
1401
|
-
screenshotCount++;
|
|
1402
|
-
const capture = await extractPageInfo(page, path, screenshotName);
|
|
1403
|
-
pages.push(capture);
|
|
1404
|
-
} catch (err) {
|
|
1405
|
-
onProgress(` \u26A0 Skipped ${path}: ${err instanceof Error ? err.message : "timeout"}`);
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
const appName = await page.title() || snapshot?.project.name || null;
|
|
1409
|
-
return {
|
|
1410
|
-
baseUrl: url,
|
|
1411
|
-
pages,
|
|
1412
|
-
totalScreenshots: screenshotCount,
|
|
1413
|
-
appName,
|
|
1414
|
-
navigation
|
|
1415
|
-
};
|
|
1416
|
-
} finally {
|
|
1417
|
-
await browser.close();
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
async function attemptLogin(page, email, password, baseUrl) {
|
|
1421
|
-
const currentUrl = page.url();
|
|
1422
|
-
const isLoginPage = currentUrl.includes("login") || currentUrl.includes("signin") || currentUrl.includes("auth");
|
|
1423
|
-
if (!isLoginPage) {
|
|
1424
|
-
for (const loginPath of ["/login", "/signin", "/auth/login", "/auth/signin"]) {
|
|
1425
|
-
try {
|
|
1426
|
-
await page.goto(new URL(loginPath, baseUrl).toString(), { waitUntil: "networkidle", timeout: 1e4 });
|
|
1427
|
-
if (page.url().includes("login") || page.url().includes("signin") || page.url().includes("auth")) break;
|
|
1428
|
-
} catch {
|
|
1429
|
-
continue;
|
|
1430
|
-
}
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
try {
|
|
1434
|
-
const emailInput = await page.$('input[type="email"], input[name="email"], input[placeholder*="email" i], input[placeholder*="username" i]');
|
|
1435
|
-
const passwordInput = await page.$('input[type="password"]');
|
|
1436
|
-
if (!emailInput || !passwordInput) return false;
|
|
1437
|
-
await emailInput.fill(email);
|
|
1438
|
-
await passwordInput.fill(password);
|
|
1439
|
-
const submitBtn = await page.$('button[type="submit"], button:has-text("Sign in"), button:has-text("Log in"), button:has-text("Login"), button:has-text("Sign In")');
|
|
1440
|
-
if (submitBtn) {
|
|
1441
|
-
await submitBtn.click();
|
|
1442
|
-
await page.waitForNavigation({ waitUntil: "networkidle", timeout: 1e4 }).catch(() => {
|
|
1443
|
-
});
|
|
1444
|
-
await page.waitForTimeout(2e3);
|
|
1445
|
-
}
|
|
1446
|
-
const afterUrl = page.url();
|
|
1447
|
-
return !afterUrl.includes("login") && !afterUrl.includes("signin");
|
|
1448
|
-
} catch {
|
|
1449
|
-
return false;
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
async function discoverNavigation(page) {
|
|
1453
|
-
const sections = [];
|
|
1454
|
-
try {
|
|
1455
|
-
const navLinks = await page.$$eval(
|
|
1456
|
-
'nav a[href], aside a[href], [role="navigation"] a[href]',
|
|
1457
|
-
(anchors) => anchors.map((a) => ({
|
|
1458
|
-
text: a.textContent?.trim() || "",
|
|
1459
|
-
href: a.getAttribute("href") || ""
|
|
1460
|
-
})).filter((l) => l.text && l.href && !l.href.startsWith("#") && !l.href.startsWith("http"))
|
|
1461
|
-
);
|
|
1462
|
-
if (navLinks.length > 0) {
|
|
1463
|
-
sections.push({
|
|
1464
|
-
label: "Navigation",
|
|
1465
|
-
links: navLinks
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
} catch {
|
|
1469
|
-
}
|
|
1470
|
-
return { sections };
|
|
1471
|
-
}
|
|
1472
|
-
function buildUrlList(currentUrl, navigation, snapshot, maxPages) {
|
|
1473
|
-
const baseUrl = new URL(currentUrl).origin;
|
|
1474
|
-
const urls = /* @__PURE__ */ new Set();
|
|
1475
|
-
urls.add(currentUrl);
|
|
1476
|
-
for (const section of navigation.sections) {
|
|
1477
|
-
for (const link of section.links) {
|
|
1478
|
-
try {
|
|
1479
|
-
const fullUrl = new URL(link.href, baseUrl).toString();
|
|
1480
|
-
urls.add(fullUrl);
|
|
1481
|
-
} catch {
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
if (snapshot) {
|
|
1486
|
-
const pagePaths = /* @__PURE__ */ new Set();
|
|
1487
|
-
for (const route of snapshot.apiRoutes) {
|
|
1488
|
-
if (route.path.startsWith("/api/")) continue;
|
|
1489
|
-
pagePaths.add(route.path);
|
|
1490
|
-
}
|
|
1491
|
-
for (const path of pagePaths) {
|
|
1492
|
-
try {
|
|
1493
|
-
urls.add(new URL(path, baseUrl).toString());
|
|
1494
|
-
} catch {
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
return Array.from(urls).slice(0, maxPages);
|
|
1499
|
-
}
|
|
1500
|
-
async function extractPageInfo(page, path, screenshot) {
|
|
1501
|
-
const title = await page.title();
|
|
1502
|
-
const headings = await page.$$eval(
|
|
1503
|
-
"h1, h2, h3",
|
|
1504
|
-
(els) => els.map((el) => el.textContent?.trim() || "").filter(Boolean).slice(0, 10)
|
|
1505
|
-
);
|
|
1506
|
-
const elements = await page.$$eval(
|
|
1507
|
-
"button:visible, a:visible, input:visible, select:visible",
|
|
1508
|
-
(els) => els.slice(0, 30).map((el) => ({
|
|
1509
|
-
type: el.tagName.toLowerCase() === "button" ? "button" : el.tagName.toLowerCase() === "a" ? "link" : el.tagName.toLowerCase() === "input" ? "input" : el.tagName.toLowerCase() === "select" ? "select" : "button",
|
|
1510
|
-
text: el.textContent?.trim().slice(0, 100) || "",
|
|
1511
|
-
ariaLabel: el.getAttribute("aria-label")
|
|
1512
|
-
}))
|
|
1513
|
-
);
|
|
1514
|
-
const navItems = await page.$$eval(
|
|
1515
|
-
"nav a, aside a",
|
|
1516
|
-
(els) => els.map((el) => el.textContent?.trim() || "").filter(Boolean)
|
|
1517
|
-
);
|
|
1518
|
-
const category = detectCategory(path);
|
|
1519
|
-
return {
|
|
1520
|
-
url: page.url(),
|
|
1521
|
-
path,
|
|
1522
|
-
title,
|
|
1523
|
-
screenshot,
|
|
1524
|
-
headings,
|
|
1525
|
-
elements,
|
|
1526
|
-
navItems,
|
|
1527
|
-
category
|
|
1528
|
-
};
|
|
1529
|
-
}
|
|
1530
|
-
function detectCategory(path) {
|
|
1531
|
-
const p = path.toLowerCase();
|
|
1532
|
-
if (p.includes("setting")) return "Settings";
|
|
1533
|
-
if (p.includes("billing") || p.includes("plan") || p.includes("subscription")) return "Billing";
|
|
1534
|
-
if (p.includes("team") || p.includes("member") || p.includes("invite")) return "Team Management";
|
|
1535
|
-
if (p.includes("analytic") || p.includes("stat") || p.includes("report")) return "Analytics";
|
|
1536
|
-
if (p.includes("content") || p.includes("article") || p.includes("doc")) return "Content Management";
|
|
1537
|
-
if (p.includes("integrat") || p.includes("connect") || p.includes("source")) return "Integrations";
|
|
1538
|
-
if (p.includes("widget") || p.includes("floatie") || p.includes("preview")) return "Widget";
|
|
1539
|
-
if (p.includes("announce")) return "Announcements";
|
|
1540
|
-
if (p.includes("help") || p.includes("center")) return "Help Center";
|
|
1541
|
-
if (p.includes("login") || p.includes("signup") || p.includes("auth")) return "Authentication";
|
|
1542
|
-
if (p === "/" || p.includes("dashboard") || p.includes("overview")) return "Getting Started";
|
|
1543
|
-
return "Features";
|
|
1544
|
-
}
|
|
1545
|
-
function pathToFilename(path) {
|
|
1546
|
-
return (path || "home").replace(/^\//, "").replace(/\//g, "-").replace(/[^a-zA-Z0-9-]/g, "") || "home";
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
// src/commands/screenshot.ts
|
|
1550
1420
|
async function screenshotCommand(options) {
|
|
1551
1421
|
console.log();
|
|
1552
1422
|
console.log(chalk4.bold("\u{1F4F8} HelpShelf Screenshot"));
|
|
1553
1423
|
console.log(chalk4.dim(` Target: ${options.url}`));
|
|
1554
1424
|
console.log();
|
|
1555
1425
|
let snapshot = null;
|
|
1556
|
-
const snapshotPath =
|
|
1557
|
-
if (
|
|
1558
|
-
snapshot = JSON.parse(
|
|
1426
|
+
const snapshotPath = resolve4(options.snapshot || ".helpshelf/snapshot.json");
|
|
1427
|
+
if (existsSync11(snapshotPath)) {
|
|
1428
|
+
snapshot = JSON.parse(readFileSync4(snapshotPath, "utf-8"));
|
|
1559
1429
|
console.log(chalk4.dim(` Using scan data: ${snapshot.apiRoutes.length} routes`));
|
|
1560
1430
|
console.log();
|
|
1561
1431
|
} else {
|
|
1562
1432
|
console.log(chalk4.dim(' No snapshot found \u2014 run "helpshelf scan" first for better route coverage'));
|
|
1563
1433
|
console.log();
|
|
1564
1434
|
}
|
|
1565
|
-
const outputDir =
|
|
1566
|
-
|
|
1435
|
+
const outputDir = resolve4(options.output || ".helpshelf/screenshots");
|
|
1436
|
+
mkdirSync4(outputDir, { recursive: true });
|
|
1567
1437
|
const maxPages = parseInt(options.maxPages || "20", 10);
|
|
1568
1438
|
const spinner = ora3("Launching browser...").start();
|
|
1569
1439
|
try {
|
|
@@ -1596,14 +1466,14 @@ async function screenshotCommand(options) {
|
|
|
1596
1466
|
navigation: result.navigation
|
|
1597
1467
|
};
|
|
1598
1468
|
writeFileSync4(
|
|
1599
|
-
|
|
1469
|
+
join11(outputDir, "manifest.json"),
|
|
1600
1470
|
JSON.stringify(manifest, null, 2)
|
|
1601
1471
|
);
|
|
1602
1472
|
console.log();
|
|
1603
1473
|
console.log(chalk4.green.bold("\u2705 Screenshots captured!"));
|
|
1604
1474
|
console.log();
|
|
1605
1475
|
console.log(` ${chalk4.dim("Output:")} ${outputDir}`);
|
|
1606
|
-
console.log(` ${chalk4.dim("Manifest:")} ${
|
|
1476
|
+
console.log(` ${chalk4.dim("Manifest:")} ${join11(outputDir, "manifest.json")}`);
|
|
1607
1477
|
console.log();
|
|
1608
1478
|
console.log(` ${chalk4.dim("Pages:")}`);
|
|
1609
1479
|
for (const page of result.pages) {
|
|
@@ -1622,7 +1492,7 @@ async function screenshotCommand(options) {
|
|
|
1622
1492
|
// src/index.ts
|
|
1623
1493
|
var program = new Command();
|
|
1624
1494
|
program.name("helpshelf").description("AI-agent toolkit for auto-generating support docs").version("0.1.0");
|
|
1625
|
-
program.command("scan").description("Scan your codebase \u2192 snapshot.json + docs-template.json").option("-d, --dir <path>", "Project directory to scan", ".").option("-o, --output <path>", "Output file path", ".helpshelf/snapshot.json").option("--no-readme", "Skip README parsing").option("--no-deps", "Skip dependency analysis").option("--no-api", "Skip API route detection").option("--no-env", "Skip environment variable detection").option("--verbose", "Show detailed scan output").option("--quiet", "Suppress output (for programmatic use)").action(scanCommand);
|
|
1495
|
+
program.command("scan").description("Scan your codebase \u2192 snapshot.json + docs-template.json").option("-d, --dir <path>", "Project directory to scan", ".").option("-o, --output <path>", "Output file path", ".helpshelf/snapshot.json").option("--no-readme", "Skip README parsing").option("--no-deps", "Skip dependency analysis").option("--no-api", "Skip API route detection").option("--no-env", "Skip environment variable detection").option("--verbose", "Show detailed scan output").option("--quiet", "Suppress output (for programmatic use)").option("-u, --url <url>", "Also capture screenshots from a running app (e.g., http://localhost:3000)").option("-e, --email <email>", "Login email for screenshot auth").option("-p, --password <password>", "Login password for screenshot auth").option("-m, --max-pages <n>", "Max pages to screenshot", "20").action(scanCommand);
|
|
1626
1496
|
program.command("screenshot").description("Capture screenshots of your running app's pages").requiredOption("-u, --url <url>", "URL of your running app (e.g., http://localhost:3000)").option("-e, --email <email>", "Login email for authenticated pages").option("-p, --password <password>", "Login password").option("-s, --snapshot <path>", "Path to scan snapshot (for route intelligence)", ".helpshelf/snapshot.json").option("-o, --output <path>", "Output directory for screenshots", ".helpshelf/screenshots").option("-m, --max-pages <n>", "Maximum pages to capture", "20").action(screenshotCommand);
|
|
1627
1497
|
program.command("push").description("Push docs to HelpShelf (creates account if needed)").option("--email <email>", "HelpShelf account email").option("--domain <domain>", "Your app domain (for help center URL)").option("--docs-dir <path>", "Directory containing markdown docs", ".helpshelf/docs").option("--site <hash>", "Existing HelpShelf site hash").option("--api-key <key>", "Existing HelpShelf API key").option("--api-url <url>", "HelpShelf API URL", "https://app.helpshelf.com").option("--dry-run", "Preview what would be pushed without sending").action(pushCommand);
|
|
1628
1498
|
program.command("init").description("Initialize HelpShelf in your project").action(initCommand);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "helpshelf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Scan your codebase and auto-generate support docs for HelpShelf",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "tsup src/index.ts --format esm --dts --clean --external playwright-core",
|
|
13
|
+
"build": "tsup src/index.ts --format esm --dts --clean --platform node --external playwright-core",
|
|
14
14
|
"dev": "tsup src/index.ts --format esm --watch",
|
|
15
15
|
"lint": "eslint src/",
|
|
16
16
|
"type-check": "tsc --noEmit",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"chalk": "^5.3.0",
|
|
22
22
|
"commander": "^12.1.0",
|
|
23
|
-
"glob": "
|
|
23
|
+
"glob": "10",
|
|
24
24
|
"ignore": "^6.0.2",
|
|
25
25
|
"ora": "^8.1.0",
|
|
26
26
|
"playwright-core": "^1.58.1"
|