diff --git a/Makefile b/Makefile
index c6cd6b2..d5a2dfe 100755
--- a/Makefile
+++ b/Makefile
@@ -5,7 +5,7 @@
# Make these targets phony (they don't create files with these names)
.PHONY: all setup dev clean clean-all build build-win build-linux \
build-mac build-mac-arm build-mac-universal \
- test css css-watch lint format validate qa setup-hooks sonar \
+ test css css-watch lint lint-md format validate qa docs-screenshots setup-hooks sonar \
security gitleaks sbom renovate renovate-local mend-scan \
icons sample-logo release
@@ -59,6 +59,9 @@ css-watch: setup-scripts
lint: setup-scripts
@node scripts/index.js lint
+lint-md: setup-scripts
+ @node scripts/index.js lint-md
+
format: setup-scripts
@node scripts/index.js format
@@ -68,6 +71,9 @@ validate: setup-scripts
qa: setup-scripts
@node scripts/index.js qa
+docs-screenshots: setup-scripts
+ @node scripts/index.js docs-screenshots
+
setup-hooks: setup-scripts
@node scripts/index.js hooks
diff --git a/README.md b/README.md
index 43fa5e3..7bc5011 100755
--- a/README.md
+++ b/README.md
@@ -8,7 +8,47 @@ A desktop app to prepare code repositories for AI workflows.
- File filtering with custom patterns and `.gitignore` support
- Token counting support for selected files
- Processed output ready to copy/export for AI tools
+- Export format selector: Markdown or XML
- Cross-platform support (Windows, macOS, Linux)
+- UI panel screenshots: `docs/APP_VIEWS.md`
+
+## Processed Output Example
+
+
+
+Full sample files:
+
+- Markdown: [`docs/examples/output-markdown.md`](docs/examples/output-markdown.md)
+- XML: [`docs/examples/output.xml`](docs/examples/output.xml)
+
+### Markdown export example
+
+````md
+# Repository Analysis
+
+## src/App.tsx
+
+```ts
+export function App() {
+ return Hello AI Code Fusion;
+}
+```
+
+Tokens: 120
+````
+
+### XML export example
+
+```xml
+
+
+ Hello AI Code Fusion;
+}
+ ]]>
+
+```
## Download Release
diff --git a/assets/ai_code_fusion_1.jpg b/assets/ai_code_fusion_1.jpg
deleted file mode 100755
index d27d5a0..0000000
Binary files a/assets/ai_code_fusion_1.jpg and /dev/null differ
diff --git a/assets/ai_code_fusion_2.jpg b/assets/ai_code_fusion_2.jpg
deleted file mode 100755
index c5029da..0000000
Binary files a/assets/ai_code_fusion_2.jpg and /dev/null differ
diff --git a/assets/ai_code_fusion_3.jpg b/assets/ai_code_fusion_3.jpg
deleted file mode 100755
index 74bccc9..0000000
Binary files a/assets/ai_code_fusion_3.jpg and /dev/null differ
diff --git a/assets/ai_code_fusion_4.jpg b/assets/ai_code_fusion_4.jpg
deleted file mode 100755
index 41fb1f5..0000000
Binary files a/assets/ai_code_fusion_4.jpg and /dev/null differ
diff --git a/docs/APP_VIEWS.md b/docs/APP_VIEWS.md
new file mode 100644
index 0000000..d53fdbf
--- /dev/null
+++ b/docs/APP_VIEWS.md
@@ -0,0 +1,31 @@
+# App Views
+
+This page shows up-to-date screenshots for the main app panels.
+
+## Start Panel (Config)
+
+
+
+## Select Files Panel
+
+
+
+## Select Files Panel (With Selection)
+
+
+
+## Select Files Panel (Resized)
+
+
+
+## Processed Output Panel
+
+
+
+## Refresh Screenshots
+
+```bash
+npm run docs:screenshots
+```
+
+This command runs the Playwright capture flow and updates screenshots in `docs/images/`.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index a808b2d..934d93b 100755
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -46,9 +46,11 @@ make build-mac
# Quality
make test
make lint
+make lint-md
make format
make validate
make qa
+make docs-screenshots
# Security / dependency automation
make security
diff --git a/docs/examples/output-markdown.md b/docs/examples/output-markdown.md
new file mode 100644
index 0000000..1971ed8
--- /dev/null
+++ b/docs/examples/output-markdown.md
@@ -0,0 +1,23 @@
+# Repository Analysis
+
+## src/App.tsx
+
+```ts
+export function App() {
+ return Hello AI Code Fusion;
+}
+```
+
+Tokens: 120
+
+## src/features/feature-24/ui/Feature24Panel.tsx
+
+```ts
+export function Feature24Panel() {
+ return ;
+}
+```
+
+Tokens: 240
+
+--END--
diff --git a/docs/examples/output.xml b/docs/examples/output.xml
new file mode 100644
index 0000000..5dce688
--- /dev/null
+++ b/docs/examples/output.xml
@@ -0,0 +1,13 @@
+
+
+ Hello AI Code Fusion;
+}
+ ]]>
+ Panel content;
+}
+ ]]>
+
diff --git a/docs/images/app-config-panel.png b/docs/images/app-config-panel.png
new file mode 100644
index 0000000..6b37786
Binary files /dev/null and b/docs/images/app-config-panel.png differ
diff --git a/docs/images/app-processed-panel.png b/docs/images/app-processed-panel.png
new file mode 100644
index 0000000..b1d72d9
Binary files /dev/null and b/docs/images/app-processed-panel.png differ
diff --git a/docs/images/app-select-panel-resized.png b/docs/images/app-select-panel-resized.png
new file mode 100644
index 0000000..a045102
Binary files /dev/null and b/docs/images/app-select-panel-resized.png differ
diff --git a/docs/images/app-select-panel-selected.png b/docs/images/app-select-panel-selected.png
new file mode 100644
index 0000000..7b6be90
Binary files /dev/null and b/docs/images/app-select-panel-selected.png differ
diff --git a/docs/images/app-select-panel.png b/docs/images/app-select-panel.png
new file mode 100644
index 0000000..dcc12f2
Binary files /dev/null and b/docs/images/app-select-panel.png differ
diff --git a/package.json b/package.json
index 12f4110..28ac626 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,8 @@
"predev": "npm run build:ts && node scripts/clean-dev-assets.js",
"dev": "node scripts/index.js dev",
"clear-assets": "rimraf src/renderer/bundle.js src/renderer/bundle.js.map src/renderer/bundle.js.LICENSE.txt src/renderer/output.css",
- "lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint src tests --ext .js,.jsx,.ts,.tsx --cache",
+ "lint": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint src tests --ext .js,.jsx,.ts,.tsx --cache && npm run lint:md",
+ "lint:md": "node scripts/lint-markdown-links.js",
"lint:tests": "cross-env ESLINT_USE_FLAT_CONFIG=false eslint tests --ext .js,.jsx,.ts,.tsx --cache",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,md,html,css}\"",
"test": "jest --config jest.config.js --passWithNoTests",
@@ -33,6 +34,8 @@
"qa": "node scripts/index.js qa",
"preqa:screenshot": "npm run build:ts",
"qa:screenshot": "node scripts/capture-ui-screenshot.js",
+ "predocs:screenshots": "npm run build:ts && npm run build:webpack",
+ "docs:screenshots": "node scripts/generate-doc-screenshots.js",
"security": "node scripts/index.js security",
"gitleaks": "node scripts/index.js gitleaks",
"gitleaks:staged": "node scripts/index.js gitleaks-staged",
diff --git a/scripts/capture-ui-screenshot.js b/scripts/capture-ui-screenshot.js
index 875e302..7b967ab 100644
--- a/scripts/capture-ui-screenshot.js
+++ b/scripts/capture-ui-screenshot.js
@@ -7,7 +7,8 @@ const { chromium } = require('playwright');
const ROOT_DIR = path.join(__dirname, '..');
const RENDERER_DIR = path.join(ROOT_DIR, 'src', 'renderer');
-const SCREENSHOT_DIR = path.join(ROOT_DIR, 'dist', 'qa', 'screenshots');
+const DEFAULT_SCREENSHOT_DIR = path.join('dist', 'qa', 'screenshots');
+const SCREENSHOT_DIR = resolveOutputDirectory(process.env.UI_SCREENSHOT_DIR);
const PORT = Number(process.env.UI_SCREENSHOT_PORT || 4173);
const DEFAULT_SCREENSHOT_NAME = `ui-${process.platform}-${process.arch}.png`;
const FIXED_MTIME = 1700000000000;
@@ -58,6 +59,21 @@ function sanitizeScreenshotName(nameCandidate) {
return withExtension;
}
+function resolveOutputDirectory(dirCandidate) {
+ const rawDir =
+ typeof dirCandidate === 'string' && dirCandidate.trim()
+ ? dirCandidate.trim()
+ : DEFAULT_SCREENSHOT_DIR;
+ const absoluteDir = path.resolve(ROOT_DIR, rawDir);
+ const relativeToRoot = path.relative(ROOT_DIR, absoluteDir);
+
+ if (relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
+ throw new Error(`Invalid screenshot directory: ${rawDir}`);
+ }
+
+ return absoluteDir;
+}
+
function resolveOutputPath(fileName) {
const targetPath = path.resolve(SCREENSHOT_DIR, fileName);
const relativeToRoot = path.relative(SCREENSHOT_DIR, targetPath);
@@ -313,12 +329,14 @@ const SCREENSHOTS = {
sourceTab: resolveOutputPath(`${SCREENSHOT_BASE_NAME}-source.png`),
sourceSelected: resolveOutputPath(`${SCREENSHOT_BASE_NAME}-source-selected.png`),
sourceSelectedResized: resolveOutputPath(`${SCREENSHOT_BASE_NAME}-source-selected-resized.png`),
+ processedTab: resolveOutputPath(`${SCREENSHOT_BASE_NAME}-processed.png`),
};
const UI_SELECTORS = {
appRoot: '#app',
configTab: '[data-tab="config"]',
sourceTab: '[data-tab="source"]',
+ processedTabActive: '[data-tab="processed"][aria-selected="true"]',
secretScanningToggle: '#enable-secret-scanning',
suspiciousFilesToggle: '#exclude-suspicious-files',
sourceFolderExpandButton: 'button[aria-label="Expand folder src"]',
@@ -331,6 +349,7 @@ const UI_SELECTORS = {
refreshFileListButton: 'button[title="Refresh the file list"]',
fileTreeScrollContainer: '.file-tree .overflow-auto',
processSelectedFilesButton: '[data-testid="process-selected-files-button"]',
+ processedContent: '#processed-content',
};
async function setupMockElectronApi(page) {
@@ -358,20 +377,65 @@ async function setupMockElectronApi(page) {
const tree = excludeSensitiveFiles ? mockFilteredDirectoryTree : mockDirectoryTree;
return cloneTree(tree);
},
- analyzeRepository: async () => ({
- totalFiles: 0,
- totalTokens: 0,
- files: [],
- }),
- processRepository: async () => ({
- content: '',
- stats: {
- totalFiles: 0,
- totalTokens: 0,
+ analyzeRepository: async (options = {}) => {
+ const selectedFilePaths = Array.isArray(options?.selectedFiles) ? options.selectedFiles : [];
+ const filesInfo = selectedFilePaths.map((filePath, index) => {
+ const normalizedPath = String(filePath);
+ const relativePath = normalizedPath.startsWith(`${mockRootPath}/`)
+ ? normalizedPath.slice(mockRootPath.length + 1)
+ : normalizedPath;
+ return {
+ path: relativePath,
+ tokens: 120 * (index + 1),
+ isBinary: false,
+ };
+ });
+
+ return {
+ totalFiles: filesInfo.length,
+ totalTokens: filesInfo.reduce((sum, file) => sum + file.tokens, 0),
+ filesInfo,
+ };
+ },
+ processRepository: async (options = {}) => {
+ const inputFilesInfo = Array.isArray(options?.filesInfo) ? options.filesInfo : [];
+ const filesInfo = inputFilesInfo.map((file, index) => ({
+ path: String(file?.path || `src/file-${index + 1}.ts`),
+ tokens:
+ Number.isFinite(file?.tokens) && Number(file.tokens) > 0 ? Number(file.tokens) : 120 * (index + 1),
+ isBinary: false,
+ }));
+ const totalTokens = filesInfo.reduce((sum, file) => sum + file.tokens, 0);
+ const exportFormat = options?.options?.exportFormat === 'xml' ? 'xml' : 'markdown';
+ const content =
+ exportFormat === 'xml'
+ ? [
+ '',
+ ``,
+ ...filesInfo.map(
+ (file) =>
+ ` `
+ ),
+ '',
+ ].join('\n')
+ : [
+ '# Repository Analysis',
+ '',
+ ...filesInfo.map(
+ (file) => `## ${file.path}\n\n\`\`\`ts\n// Preview for ${file.path}\n\`\`\`\nTokens: ${file.tokens}\n`
+ ),
+ '--END--',
+ ].join('\n');
+
+ return {
+ content,
+ exportFormat,
+ totalTokens,
+ processedFiles: filesInfo.length,
skippedFiles: 0,
- processedFiles: 0,
- },
- }),
+ filesInfo,
+ };
+ },
countFilesTokens: async (options) => {
const filePaths = Array.isArray(options?.filePaths) ? options.filePaths : [];
const results = {};
@@ -569,6 +633,33 @@ async function captureAppStateScreenshots(page) {
await runStep('Capture resized screenshot with deep tree expanded', async () => {
await page.screenshot({ path: SCREENSHOTS.sourceSelectedResized, fullPage: true });
});
+
+ await runStep('Return to desktop viewport before processing', async () => {
+ await page.setViewportSize({ width: 1440, height: 900 });
+ });
+
+ await runStep('Wait for process button to be enabled', async () => {
+ await page.waitForFunction((selector) => {
+ const button = document.querySelector(selector);
+ if (!(button instanceof HTMLButtonElement)) {
+ return false;
+ }
+ return !button.disabled && /process selected files/i.test(button.textContent || '');
+ }, UI_SELECTORS.processSelectedFilesButton);
+ });
+
+ await runStep('Process selected files', async () => {
+ await page.locator(UI_SELECTORS.processSelectedFilesButton).first().click();
+ });
+
+ await runStep('Wait for processed panel to render', async () => {
+ await page.waitForSelector(UI_SELECTORS.processedTabActive, { timeout: 10000 });
+ await page.waitForSelector(UI_SELECTORS.processedContent, { timeout: 10000 });
+ });
+
+ await runStep('Capture processed tab screenshot', async () => {
+ await page.screenshot({ path: SCREENSHOTS.processedTab, fullPage: true });
+ });
}
async function captureScreenshot() {
diff --git a/scripts/generate-doc-screenshots.js b/scripts/generate-doc-screenshots.js
new file mode 100755
index 0000000..07b4221
--- /dev/null
+++ b/scripts/generate-doc-screenshots.js
@@ -0,0 +1,62 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const ROOT_DIR = path.join(__dirname, '..');
+const CAPTURE_SCRIPT_PATH = path.join(__dirname, 'capture-ui-screenshot.js');
+const TEMP_SCREENSHOT_DIR = path.join(ROOT_DIR, 'dist', 'docs', 'screenshots');
+const DOCS_IMAGE_DIR = path.join(ROOT_DIR, 'docs', 'images');
+const TEMP_BASE_NAME = 'docs-panels';
+
+const screenshotMap = [
+ { from: `${TEMP_BASE_NAME}.png`, to: 'app-config-panel.png' },
+ { from: `${TEMP_BASE_NAME}-source.png`, to: 'app-select-panel.png' },
+ { from: `${TEMP_BASE_NAME}-source-selected.png`, to: 'app-select-panel-selected.png' },
+ { from: `${TEMP_BASE_NAME}-source-selected-resized.png`, to: 'app-select-panel-resized.png' },
+ { from: `${TEMP_BASE_NAME}-processed.png`, to: 'app-processed-panel.png' },
+];
+
+function fail(message) {
+ console.error(`Failed to generate docs screenshots: ${message}`);
+ process.exit(1);
+}
+
+function runCaptureScript() {
+ const captureEnv = {
+ ...process.env,
+ UI_SCREENSHOT_DIR: path.relative(ROOT_DIR, TEMP_SCREENSHOT_DIR),
+ UI_SCREENSHOT_NAME: `${TEMP_BASE_NAME}.png`,
+ UI_SCREENSHOT_PORT: process.env.UI_SCREENSHOT_PORT || '4174',
+ };
+
+ const result = spawnSync(process.execPath, [CAPTURE_SCRIPT_PATH], {
+ cwd: ROOT_DIR,
+ env: captureEnv,
+ stdio: 'inherit',
+ });
+
+ if (result.status !== 0) {
+ fail(`capture script exited with code ${result.status}`);
+ }
+}
+
+function copyScreenshotsToDocs() {
+ fs.mkdirSync(DOCS_IMAGE_DIR, { recursive: true });
+
+ for (const { from, to } of screenshotMap) {
+ const sourcePath = path.join(TEMP_SCREENSHOT_DIR, from);
+ const targetPath = path.join(DOCS_IMAGE_DIR, to);
+
+ if (!fs.existsSync(sourcePath)) {
+ fail(`missing screenshot ${sourcePath}`);
+ }
+
+ fs.copyFileSync(sourcePath, targetPath);
+ console.log(`Updated docs screenshot: ${targetPath}`);
+ }
+}
+
+runCaptureScript();
+copyScreenshotsToDocs();
diff --git a/scripts/index.js b/scripts/index.js
index 7dca026..b1a8cfd 100755
--- a/scripts/index.js
+++ b/scripts/index.js
@@ -114,6 +114,12 @@ async function executeCommand() {
console.log('Linting completed successfully');
break;
+ case 'lint:md':
+ case 'lint-md':
+ await utils.runNpmScript('lint:md');
+ console.log('Markdown linting completed successfully');
+ break;
+
case 'format':
await utils.runNpmScript('format');
console.log('Formatting completed successfully');
@@ -134,6 +140,12 @@ async function executeCommand() {
console.log('QA checks completed successfully');
break;
+ case 'docs-screenshots':
+ case 'docs:screenshots':
+ await utils.runNpmScript('docs:screenshots');
+ console.log('Docs screenshots refreshed successfully');
+ break;
+
// Security automation commands
case 'security':
await security.runSecurity();
diff --git a/scripts/lib/utils.js b/scripts/lib/utils.js
index d937a92..e318108 100755
--- a/scripts/lib/utils.js
+++ b/scripts/lib/utils.js
@@ -226,9 +226,11 @@ function printHelp() {
console.log(' test - Run tests');
console.log(' test:watch - Run tests in watch mode');
console.log(' lint - Run linter');
+ console.log(' lint:md - Validate markdown links, image paths, and no decorative icons');
console.log(' format - Format code');
console.log(' validate - Run all code quality checks');
console.log(' qa - Run lint + tests + security checks');
+ console.log(' docs-screenshots - Refresh docs UI screenshots');
console.log(' security - Run security checks (gitleaks + sbom)');
console.log(' gitleaks - Run gitleaks secret scan');
console.log(' sbom - Generate CycloneDX SBOM');
diff --git a/scripts/lint-markdown-links.js b/scripts/lint-markdown-links.js
new file mode 100755
index 0000000..52dedc6
--- /dev/null
+++ b/scripts/lint-markdown-links.js
@@ -0,0 +1,195 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
+
+const ROOT_DIR = path.join(__dirname, '..');
+
+const IGNORED_PROTOCOLS = ['http://', 'https://', 'mailto:', 'tel:', 'data:', 'javascript:'];
+const DECORATIVE_ICON_PATTERN = /\p{Extended_Pictographic}/u;
+
+function ensureError(error) {
+ if (error instanceof Error) {
+ return error;
+ }
+
+ return new Error(String(error));
+}
+
+function getMarkdownFiles() {
+ try {
+ const output = execSync('git ls-files "*.md"', {
+ cwd: ROOT_DIR,
+ encoding: 'utf8',
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+
+ return output
+ .split(/\r?\n/)
+ .map((line) => line.trim())
+ .filter(Boolean)
+ .map((filePath) => path.join(ROOT_DIR, filePath));
+ } catch (error) {
+ throw new Error(`Unable to list markdown files: ${ensureError(error).message}`);
+ }
+}
+
+function isExternalTarget(target) {
+ const normalizedTarget = target.toLowerCase();
+ return IGNORED_PROTOCOLS.some((protocol) => normalizedTarget.startsWith(protocol));
+}
+
+function normalizeTarget(rawTarget) {
+ if (!rawTarget) {
+ return '';
+ }
+
+ let target = rawTarget.trim();
+
+ if (target.startsWith('<') && target.endsWith('>')) {
+ target = target.slice(1, -1).trim();
+ }
+
+ const titleSuffixMatch = target.match(/^([^\s]+)\s+["'(].*$/);
+ if (titleSuffixMatch) {
+ target = titleSuffixMatch[1];
+ }
+
+ return target;
+}
+
+function resolveTargetPath(markdownFilePath, target) {
+ const [pathWithoutAnchor] = target.split('#');
+
+ if (!pathWithoutAnchor || isExternalTarget(pathWithoutAnchor) || pathWithoutAnchor.startsWith('#')) {
+ return null;
+ }
+
+ if (path.isAbsolute(pathWithoutAnchor)) {
+ return path.resolve(ROOT_DIR, `.${pathWithoutAnchor}`);
+ }
+
+ return path.resolve(path.dirname(markdownFilePath), pathWithoutAnchor);
+}
+
+function extractTargetsFromLine(line) {
+ const targets = [];
+
+ const markdownLinkPattern = /!?\[[^\]]*\]\(([^)]+)\)/g;
+ let linkMatch;
+ while ((linkMatch = markdownLinkPattern.exec(line)) !== null) {
+ targets.push(linkMatch[1]);
+ }
+
+ const htmlImagePattern = /
]*\bsrc=["']([^"']+)["'][^>]*>/gi;
+ let imageMatch;
+ while ((imageMatch = htmlImagePattern.exec(line)) !== null) {
+ targets.push(imageMatch[1]);
+ }
+
+ return targets;
+}
+
+function lintMarkdownFile(markdownFilePath) {
+ const content = fs.readFileSync(markdownFilePath, 'utf8');
+ const lines = content.split(/\r?\n/);
+ const errors = [];
+
+ let inFenceBlock = false;
+ let fenceMarker = '';
+
+ for (let index = 0; index < lines.length; index += 1) {
+ const line = lines[index];
+ const fenceMatch = line.match(/^\s*(```|~~~)/);
+
+ if (fenceMatch) {
+ const currentFenceMarker = fenceMatch[1];
+ if (!inFenceBlock) {
+ inFenceBlock = true;
+ fenceMarker = currentFenceMarker;
+ } else if (currentFenceMarker === fenceMarker) {
+ inFenceBlock = false;
+ fenceMarker = '';
+ }
+ continue;
+ }
+
+ if (inFenceBlock) {
+ continue;
+ }
+
+ if (DECORATIVE_ICON_PATTERN.test(line)) {
+ errors.push({
+ kind: 'decorative-icon',
+ filePath: markdownFilePath,
+ lineNumber: index + 1,
+ lineText: line.trim(),
+ });
+ }
+
+ const rawTargets = extractTargetsFromLine(line);
+ for (const rawTarget of rawTargets) {
+ const normalizedTarget = normalizeTarget(rawTarget);
+ if (!normalizedTarget || isExternalTarget(normalizedTarget) || normalizedTarget.startsWith('#')) {
+ continue;
+ }
+
+ const resolvedTargetPath = resolveTargetPath(markdownFilePath, normalizedTarget);
+ if (!resolvedTargetPath) {
+ continue;
+ }
+
+ if (!fs.existsSync(resolvedTargetPath)) {
+ errors.push({
+ kind: 'missing-target',
+ filePath: markdownFilePath,
+ lineNumber: index + 1,
+ target: normalizedTarget,
+ resolvedPath: resolvedTargetPath,
+ });
+ }
+ }
+ }
+
+ return errors;
+}
+
+function run() {
+ const markdownFiles = getMarkdownFiles();
+ let linkCount = 0;
+ const allErrors = [];
+
+ for (const markdownFilePath of markdownFiles) {
+ const fileErrors = lintMarkdownFile(markdownFilePath);
+ allErrors.push(...fileErrors);
+
+ const content = fs.readFileSync(markdownFilePath, 'utf8');
+ const lines = content.split(/\r?\n/);
+ for (const line of lines) {
+ linkCount += extractTargetsFromLine(line).length;
+ }
+ }
+
+ if (allErrors.length > 0) {
+ console.error('Markdown docs lint failed:\n');
+ for (const error of allErrors) {
+ const relativeFilePath = path.relative(ROOT_DIR, error.filePath);
+
+ if (error.kind === 'decorative-icon') {
+ console.error(`- ${relativeFilePath}:${error.lineNumber} -> decorative icon found: ${error.lineText}`);
+ continue;
+ }
+
+ const relativeResolvedPath = path.relative(ROOT_DIR, error.resolvedPath);
+ console.error(`- ${relativeFilePath}:${error.lineNumber} -> ${error.target} (missing: ${relativeResolvedPath})`);
+ }
+ process.exit(1);
+ }
+
+ console.log(
+ `Markdown docs lint passed: ${markdownFiles.length} markdown files checked, ${linkCount} links/images scanned, no decorative icons found.`
+ );
+}
+
+run();
diff --git a/tests/catalog.md b/tests/catalog.md
index cee4a83..e513c6b 100644
--- a/tests/catalog.md
+++ b/tests/catalog.md
@@ -6,7 +6,9 @@ Purpose: quick map of what is covered, why it exists, and which command to run.
- Full tests: `npm test -- --runInBand`
- Lint: `npm run lint`
+- Markdown docs lint (links/images/icons): `npm run lint:md`
- UI screenshot gate: `npm run qa:screenshot`
+- Docs screenshots: `npm run docs:screenshots`
## Unit Tests
@@ -37,9 +39,14 @@ Purpose: quick map of what is covered, why it exists, and which command to run.
## Visual Regression Signal
-| Command | Primary Target | Key Use Cases |
-| ----------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- |
-| `npm run qa:screenshot` | `scripts/capture-ui-screenshot.js` + renderer UI | Cross-OS UI sanity, resized layout checks, deep file-tree selection visibility, secret-filter toggle behavior |
+| Command | Primary Target | Key Use Cases |
+| -------------------------- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
+| `npm run qa:screenshot` | `scripts/capture-ui-screenshot.js` + renderer UI | Cross-OS UI sanity, resized layout checks, deep file-tree selection visibility, secret-filter toggle behavior |
+| `npm run docs:screenshots` | `scripts/generate-doc-screenshots.js` + renderer UI | Refresh tracked screenshots for Config/Select/Processed panels in `docs/APP_VIEWS.md` |
+
+## Manual UI Doc Test
+
+- `tests/manual/docs-ui-screenshots.md`
## Change-to-Test Mapping
diff --git a/tests/manual/docs-ui-screenshots.md b/tests/manual/docs-ui-screenshots.md
new file mode 100644
index 0000000..484ea18
--- /dev/null
+++ b/tests/manual/docs-ui-screenshots.md
@@ -0,0 +1,29 @@
+# Manual Test: Docs UI Screenshots
+
+Purpose: regenerate and verify documentation screenshots for the main app views.
+
+## Command
+
+```bash
+npm run docs:screenshots
+```
+
+## Expected Outputs
+
+The command should update these files:
+
+- `docs/images/app-config-panel.png`
+- `docs/images/app-select-panel.png`
+- `docs/images/app-select-panel-selected.png`
+- `docs/images/app-select-panel-resized.png`
+- `docs/images/app-processed-panel.png`
+
+## Verification Checklist
+
+1. Open `docs/APP_VIEWS.md`.
+2. Confirm all five images render.
+3. Confirm screenshots reflect current UI labels:
+ - `Start`
+ - `Select Files`
+ - `Processed Output`
+4. Confirm `Processed Output` screenshot shows content and file/token summary.