fraim-framework 2.0.64 → 2.0.65
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/bin/fraim-mcp.js +52 -19
- package/bin/fraim.js +23 -0
- package/dist/src/cli/commands/add-ide.js +53 -14
- package/dist/src/cli/commands/doctor.js +12 -24
- package/dist/src/cli/commands/init-project.js +0 -3
- package/dist/src/cli/commands/init.js +0 -2
- package/dist/src/cli/commands/mcp.js +65 -0
- package/dist/src/cli/commands/setup.js +17 -1
- package/dist/src/cli/commands/sync.js +173 -104
- package/dist/src/cli/setup/auto-mcp-setup.js +6 -4
- package/dist/src/cli/setup/mcp-config-generator.js +65 -41
- package/dist/src/fraim/issue-tracking/ado-provider.js +304 -0
- package/dist/src/fraim/issue-tracking/factory.js +63 -0
- package/dist/src/fraim/issue-tracking/github-provider.js +200 -0
- package/dist/src/fraim/issue-tracking/types.js +7 -0
- package/dist/src/fraim/issue-tracking-config.js +83 -0
- package/dist/src/local-mcp-server/stdio-server.js +23 -3
- package/dist/src/utils/remote-sync.js +130 -0
- package/package.json +2 -3
- package/dist/src/utils/enforcement-utils.js +0 -239
- package/dist/src/utils/validate-workflows.js +0 -101
- package/registry/scripts/cleanup-branch.ts +0 -341
- package/registry/scripts/code-quality-check.sh +0 -566
- package/registry/scripts/comprehensive-explorer.py +0 -297
- package/registry/scripts/create-git-labels.sh +0 -49
- package/registry/scripts/create-website-structure.js +0 -562
- package/registry/scripts/detect-tautological-tests.sh +0 -38
- package/registry/scripts/evaluate-code-quality.ts +0 -36
- package/registry/scripts/exec-with-timeout.ts +0 -122
- package/registry/scripts/generate-engagement-emails.ts +0 -830
- package/registry/scripts/interactive-explorer.py +0 -270
- package/registry/scripts/markdown-to-pdf.js +0 -395
- package/registry/scripts/newsletter-helpers.ts +0 -777
- package/registry/scripts/pdf-styles.css +0 -172
- package/registry/scripts/prep-issue.sh +0 -548
- package/registry/scripts/productivity/build-productivity-csv.mjs +0 -242
- package/registry/scripts/productivity/fetch-pr-details.mjs +0 -144
- package/registry/scripts/productivity/productivity-report.sh +0 -147
- package/registry/scripts/profile-server.ts +0 -426
- package/registry/scripts/run-thank-you-workflow.ts +0 -122
- package/registry/scripts/scrape-site.py +0 -302
- package/registry/scripts/send-newsletter-simple.ts +0 -102
- package/registry/scripts/send-thank-you-emails.ts +0 -57
- package/registry/scripts/validate-openapi-limits.ts +0 -366
- package/registry/scripts/validate-test-coverage.ts +0 -280
- package/registry/scripts/verify-pr-comments.sh +0 -74
- package/registry/scripts/verify-test-coverage.ts +0 -36
- package/registry/stubs/workflows/azure/cost-optimization.md +0 -11
- package/registry/stubs/workflows/bootstrap/create-architecture.md +0 -11
- package/registry/stubs/workflows/bootstrap/detect-broken-windows.md +0 -11
- package/registry/stubs/workflows/bootstrap/evaluate-code-quality.md +0 -11
- package/registry/stubs/workflows/bootstrap/verify-test-coverage.md +0 -11
- package/registry/stubs/workflows/brainstorming/blue-sky-brainstorming.md +0 -11
- package/registry/stubs/workflows/brainstorming/codebase-brainstorming.md +0 -11
- package/registry/stubs/workflows/business-development/create-business-plan.md +0 -11
- package/registry/stubs/workflows/business-development/ideate-business-opportunity.md +0 -11
- package/registry/stubs/workflows/business-development/price-product.md +0 -18
- package/registry/stubs/workflows/compliance/detect-compliance-requirements.md +0 -11
- package/registry/stubs/workflows/compliance/generate-audit-evidence.md +0 -11
- package/registry/stubs/workflows/compliance/soc2-evidence-generator.md +0 -11
- package/registry/stubs/workflows/customer-development/insight-analysis.md +0 -11
- package/registry/stubs/workflows/customer-development/insight-triage.md +0 -11
- package/registry/stubs/workflows/customer-development/interview-preparation.md +0 -11
- package/registry/stubs/workflows/customer-development/linkedin-outreach.md +0 -11
- package/registry/stubs/workflows/customer-development/strategic-brainstorming.md +0 -11
- package/registry/stubs/workflows/customer-development/thank-customers.md +0 -11
- package/registry/stubs/workflows/customer-development/user-survey-dispatch.md +0 -11
- package/registry/stubs/workflows/customer-development/users-to-target.md +0 -11
- package/registry/stubs/workflows/customer-development/weekly-newsletter.md +0 -11
- package/registry/stubs/workflows/deploy/cloud-deployment.md +0 -11
- package/registry/stubs/workflows/improve-fraim/contribute.md +0 -11
- package/registry/stubs/workflows/improve-fraim/file-issue.md +0 -11
- package/registry/stubs/workflows/learning/build-skillset.md +0 -11
- package/registry/stubs/workflows/learning/synthesize-learnings.md +0 -11
- package/registry/stubs/workflows/legal/contract-review-analysis.md +0 -11
- package/registry/stubs/workflows/legal/nda.md +0 -11
- package/registry/stubs/workflows/legal/patent-filing.md +0 -11
- package/registry/stubs/workflows/legal/saas-contract-development.md +0 -11
- package/registry/stubs/workflows/legal/trademark-filing.md +0 -11
- package/registry/stubs/workflows/marketing/content-creation.md +0 -11
- package/registry/stubs/workflows/marketing/convert-to-pdf.md +0 -11
- package/registry/stubs/workflows/marketing/create-modern-website.md +0 -11
- package/registry/stubs/workflows/marketing/domain-registration.md +0 -11
- package/registry/stubs/workflows/marketing/hbr-article.md +0 -11
- package/registry/stubs/workflows/marketing/launch-checklist.md +0 -11
- package/registry/stubs/workflows/marketing/marketing-strategy.md +0 -11
- package/registry/stubs/workflows/marketing/storytelling.md +0 -11
- package/registry/stubs/workflows/performance/analyze-performance.md +0 -11
- package/registry/stubs/workflows/product-building/design.md +0 -11
- package/registry/stubs/workflows/product-building/implement.md +0 -11
- package/registry/stubs/workflows/product-building/iterate-on-pr-comments.md +0 -11
- package/registry/stubs/workflows/product-building/prep-issue.md +0 -11
- package/registry/stubs/workflows/product-building/prototype.md +0 -11
- package/registry/stubs/workflows/product-building/resolve.md +0 -11
- package/registry/stubs/workflows/product-building/retrospect.md +0 -11
- package/registry/stubs/workflows/product-building/spec.md +0 -11
- package/registry/stubs/workflows/product-building/test.md +0 -11
- package/registry/stubs/workflows/productivity-report/productivity-report.md +0 -11
- package/registry/stubs/workflows/quality-assurance/browser-validation.md +0 -11
- package/registry/stubs/workflows/quality-assurance/iterative-improvement-cycle.md +0 -11
- package/registry/stubs/workflows/replicate/replicate-discovery.md +0 -11
- package/registry/stubs/workflows/replicate/replicate-to-issues.md +0 -11
- package/registry/stubs/workflows/reviewer/review-implementation-vs-design-spec.md +0 -11
- package/registry/stubs/workflows/reviewer/review-implementation-vs-feature-spec.md +0 -11
- package/registry/stubs/workflows/startup-credits/aws-activate-application.md +0 -11
- package/registry/stubs/workflows/startup-credits/google-cloud-application.md +0 -11
- package/registry/stubs/workflows/startup-credits/microsoft-azure-application.md +0 -11
|
@@ -1,270 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
Interactive site explorer using Playwright
|
|
4
|
-
Navigates through the site, clicks buttons, and captures screenshots
|
|
5
|
-
|
|
6
|
-
Usage:
|
|
7
|
-
python interactive_explorer.py <base_url> [--output-dir DIR] [--headless]
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import asyncio
|
|
11
|
-
from playwright.async_api import async_playwright
|
|
12
|
-
import json
|
|
13
|
-
import os
|
|
14
|
-
import sys
|
|
15
|
-
import argparse
|
|
16
|
-
from datetime import datetime
|
|
17
|
-
from pathlib import Path
|
|
18
|
-
|
|
19
|
-
class SiteInteractor:
|
|
20
|
-
def __init__(self, base_url, output_dir="."):
|
|
21
|
-
self.base_url = base_url
|
|
22
|
-
self.output_dir = Path(output_dir)
|
|
23
|
-
self.screenshots_dir = self.output_dir / "screenshots"
|
|
24
|
-
self.screenshots_dir.mkdir(parents=True, exist_ok=True)
|
|
25
|
-
|
|
26
|
-
self.use_cases = []
|
|
27
|
-
self.pages_visited = []
|
|
28
|
-
self.actions_taken = []
|
|
29
|
-
|
|
30
|
-
async def take_screenshot(self, page, name, description=""):
|
|
31
|
-
"""Take a screenshot and save it"""
|
|
32
|
-
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
33
|
-
filename = self.screenshots_dir / f"{name}_{timestamp}.png"
|
|
34
|
-
await page.screenshot(path=str(filename), full_page=True)
|
|
35
|
-
self.pages_visited.append({
|
|
36
|
-
'name': name,
|
|
37
|
-
'filename': str(filename),
|
|
38
|
-
'url': page.url,
|
|
39
|
-
'description': description,
|
|
40
|
-
'timestamp': timestamp
|
|
41
|
-
})
|
|
42
|
-
print(f" 📸 Screenshot: {filename.name} - {description}")
|
|
43
|
-
return str(filename)
|
|
44
|
-
|
|
45
|
-
async def wait_for_react(self, page):
|
|
46
|
-
"""Wait for React to finish loading"""
|
|
47
|
-
try:
|
|
48
|
-
await page.wait_for_selector('#root', state='attached', timeout=5000)
|
|
49
|
-
await page.wait_for_timeout(2000)
|
|
50
|
-
content = await page.evaluate('''() => {
|
|
51
|
-
const root = document.getElementById('root');
|
|
52
|
-
return root && root.children.length > 0;
|
|
53
|
-
}''')
|
|
54
|
-
if not content:
|
|
55
|
-
await page.wait_for_timeout(3000)
|
|
56
|
-
except Exception as e:
|
|
57
|
-
print(f" ⚠️ React loading check: {e}")
|
|
58
|
-
|
|
59
|
-
async def analyze_page_content(self, page):
|
|
60
|
-
"""Analyze the current page content"""
|
|
61
|
-
content = await page.evaluate('''() => {
|
|
62
|
-
return {
|
|
63
|
-
title: document.title,
|
|
64
|
-
url: window.location.href,
|
|
65
|
-
headings: {
|
|
66
|
-
h1: Array.from(document.querySelectorAll('h1')).map(h => h.innerText.trim()),
|
|
67
|
-
h2: Array.from(document.querySelectorAll('h2')).map(h => h.innerText.trim()),
|
|
68
|
-
h3: Array.from(document.querySelectorAll('h3')).map(h => h.innerText.trim()),
|
|
69
|
-
},
|
|
70
|
-
buttons: Array.from(document.querySelectorAll('button, [role="button"], a[href]'))
|
|
71
|
-
.filter(el => el.offsetParent !== null)
|
|
72
|
-
.map(el => el.innerText.trim())
|
|
73
|
-
.filter(text => text.length > 0),
|
|
74
|
-
links: Array.from(document.querySelectorAll('a[href]'))
|
|
75
|
-
.map(a => ({text: a.innerText.trim(), href: a.href}))
|
|
76
|
-
.filter(link => link.text.length > 0),
|
|
77
|
-
forms: Array.from(document.querySelectorAll('form')).length,
|
|
78
|
-
inputs: Array.from(document.querySelectorAll('input, textarea, select'))
|
|
79
|
-
.map(input => ({
|
|
80
|
-
type: input.type || input.tagName.toLowerCase(),
|
|
81
|
-
name: input.name || '',
|
|
82
|
-
placeholder: input.placeholder || '',
|
|
83
|
-
id: input.id || ''
|
|
84
|
-
})),
|
|
85
|
-
visible_text: document.body.innerText.substring(0, 1000)
|
|
86
|
-
};
|
|
87
|
-
}''')
|
|
88
|
-
return content
|
|
89
|
-
|
|
90
|
-
async def navigate_and_explore(self, page, max_clicks=20):
|
|
91
|
-
"""Navigate through the site and explore all functionality"""
|
|
92
|
-
print(f"\n🌐 Navigating to {self.base_url}...")
|
|
93
|
-
await page.goto(self.base_url, wait_until='networkidle', timeout=30000)
|
|
94
|
-
await self.wait_for_react(page)
|
|
95
|
-
|
|
96
|
-
# Screenshot of homepage
|
|
97
|
-
await self.take_screenshot(page, "01_homepage", "Initial homepage load")
|
|
98
|
-
|
|
99
|
-
# Analyze homepage
|
|
100
|
-
homepage_content = await self.analyze_page_content(page)
|
|
101
|
-
print(f"\n📄 Homepage Analysis:")
|
|
102
|
-
print(f" Title: {homepage_content['title']}")
|
|
103
|
-
print(f" H1: {homepage_content['headings']['h1']}")
|
|
104
|
-
print(f" Buttons found: {len(homepage_content['buttons'])}")
|
|
105
|
-
print(f" Links found: {len(homepage_content['links'])}")
|
|
106
|
-
|
|
107
|
-
# Try clicking navigation/links
|
|
108
|
-
clicked_urls = set([self.base_url])
|
|
109
|
-
click_count = 0
|
|
110
|
-
|
|
111
|
-
# Get all links
|
|
112
|
-
links = await page.query_selector_all('a[href], button, [role="button"]')
|
|
113
|
-
|
|
114
|
-
for i, link in enumerate(links[:max_clicks]):
|
|
115
|
-
try:
|
|
116
|
-
if click_count >= max_clicks:
|
|
117
|
-
break
|
|
118
|
-
|
|
119
|
-
is_visible = await link.is_visible()
|
|
120
|
-
if not is_visible:
|
|
121
|
-
continue
|
|
122
|
-
|
|
123
|
-
tag_name = await link.evaluate('el => el.tagName.toLowerCase()')
|
|
124
|
-
text = await link.inner_text()
|
|
125
|
-
href = await link.get_attribute('href') if tag_name == 'a' else None
|
|
126
|
-
|
|
127
|
-
if not text.strip() and not href:
|
|
128
|
-
continue
|
|
129
|
-
|
|
130
|
-
print(f"\n 🖱️ Clicking: {text[:50] if text else 'Link'} (tag: {tag_name})")
|
|
131
|
-
|
|
132
|
-
# Take screenshot before click
|
|
133
|
-
await self.take_screenshot(
|
|
134
|
-
page,
|
|
135
|
-
f"02_before_click_{click_count+1}",
|
|
136
|
-
f"Before clicking: {text[:30]}"
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
# Click the element
|
|
140
|
-
try:
|
|
141
|
-
if tag_name == 'a':
|
|
142
|
-
await link.click(timeout=5000)
|
|
143
|
-
else:
|
|
144
|
-
await link.click(timeout=5000)
|
|
145
|
-
|
|
146
|
-
click_count += 1
|
|
147
|
-
await page.wait_for_timeout(3000)
|
|
148
|
-
|
|
149
|
-
# Check if URL changed
|
|
150
|
-
current_url = page.url
|
|
151
|
-
if current_url not in clicked_urls:
|
|
152
|
-
clicked_urls.add(current_url)
|
|
153
|
-
await self.take_screenshot(
|
|
154
|
-
page,
|
|
155
|
-
f"03_after_click_{click_count}",
|
|
156
|
-
f"After clicking: {text[:30]} - URL: {current_url}"
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
# Analyze new page
|
|
160
|
-
new_content = await self.analyze_page_content(page)
|
|
161
|
-
print(f" 📄 New page: {new_content['title']}")
|
|
162
|
-
|
|
163
|
-
# Try to fill forms if present
|
|
164
|
-
if new_content['forms'] > 0 or len(new_content['inputs']) > 0:
|
|
165
|
-
print(f" 📝 Forms/inputs found: {len(new_content['inputs'])}")
|
|
166
|
-
await self.take_screenshot(
|
|
167
|
-
page,
|
|
168
|
-
f"04_form_page_{click_count}",
|
|
169
|
-
f"Page with forms/inputs"
|
|
170
|
-
)
|
|
171
|
-
|
|
172
|
-
except Exception as e:
|
|
173
|
-
print(f" ⚠️ Click failed: {e}")
|
|
174
|
-
try:
|
|
175
|
-
await page.keyboard.press('Escape')
|
|
176
|
-
await page.wait_for_timeout(1000)
|
|
177
|
-
except:
|
|
178
|
-
pass
|
|
179
|
-
|
|
180
|
-
except Exception as e:
|
|
181
|
-
print(f" ⚠️ Error processing link {i}: {e}")
|
|
182
|
-
continue
|
|
183
|
-
|
|
184
|
-
# Final screenshot
|
|
185
|
-
await self.take_screenshot(page, "99_final_state", "Final state after exploration")
|
|
186
|
-
|
|
187
|
-
async def identify_use_cases(self, page):
|
|
188
|
-
"""Identify use cases from the site content"""
|
|
189
|
-
print("\n🔍 Identifying use cases...")
|
|
190
|
-
|
|
191
|
-
all_text = await page.evaluate('''() => {
|
|
192
|
-
return document.body.innerText;
|
|
193
|
-
}''')
|
|
194
|
-
|
|
195
|
-
use_case_keywords = {
|
|
196
|
-
'Event Management': ['event', 'calendar', 'schedule', 'meeting', 'workshop'],
|
|
197
|
-
'Field Assessment': ['assessment', 'field', 'survey', 'evaluation', 'inspection'],
|
|
198
|
-
'Species Management': ['species', 'invasive', 'native', 'wildlife', 'plant'],
|
|
199
|
-
'Data Collection': ['form', 'submit', 'upload', 'data', 'record'],
|
|
200
|
-
'Search & Discovery': ['search', 'find', 'filter', 'browse', 'discover'],
|
|
201
|
-
'Reporting': ['report', 'download', 'export', 'generate', 'print'],
|
|
202
|
-
'User Account': ['login', 'sign in', 'register', 'profile', 'account'],
|
|
203
|
-
'Map & Location': ['map', 'location', 'coordinates', 'gps', 'area'],
|
|
204
|
-
'Education': ['learn', 'guide', 'tutorial', 'help', 'documentation'],
|
|
205
|
-
'Community': ['community', 'share', 'connect', 'network', 'collaborate']
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
identified_use_cases = []
|
|
209
|
-
text_lower = all_text.lower()
|
|
210
|
-
|
|
211
|
-
for use_case, keywords in use_case_keywords.items():
|
|
212
|
-
matches = sum(1 for keyword in keywords if keyword in text_lower)
|
|
213
|
-
if matches >= 2:
|
|
214
|
-
identified_use_cases.append({
|
|
215
|
-
'use_case': use_case,
|
|
216
|
-
'confidence': 'high' if matches >= 3 else 'medium',
|
|
217
|
-
'keywords_found': [k for k in keywords if k in text_lower]
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
self.use_cases = identified_use_cases
|
|
221
|
-
return identified_use_cases
|
|
222
|
-
|
|
223
|
-
async def run_full_analysis(self, headless=True):
|
|
224
|
-
"""Run the complete analysis"""
|
|
225
|
-
async with async_playwright() as p:
|
|
226
|
-
print("🚀 Starting browser...")
|
|
227
|
-
browser = await p.chromium.launch(headless=headless)
|
|
228
|
-
context = await browser.new_context(
|
|
229
|
-
viewport={'width': 1920, 'height': 1080}
|
|
230
|
-
)
|
|
231
|
-
page = await context.new_page()
|
|
232
|
-
|
|
233
|
-
try:
|
|
234
|
-
await self.navigate_and_explore(page)
|
|
235
|
-
use_cases = await self.identify_use_cases(page)
|
|
236
|
-
|
|
237
|
-
report = {
|
|
238
|
-
'base_url': self.base_url,
|
|
239
|
-
'analysis_date': datetime.now().isoformat(),
|
|
240
|
-
'pages_visited': self.pages_visited,
|
|
241
|
-
'use_cases': use_cases,
|
|
242
|
-
'total_screenshots': len(self.pages_visited)
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
output_path = self.output_dir / 'interaction_analysis.json'
|
|
246
|
-
with open(output_path, 'w', encoding='utf-8') as f:
|
|
247
|
-
json.dump(report, f, indent=2, ensure_ascii=False)
|
|
248
|
-
|
|
249
|
-
print(f"\n✅ Analysis complete!")
|
|
250
|
-
print(f" 📸 Screenshots: {len(self.pages_visited)}")
|
|
251
|
-
print(f" 🎯 Use cases identified: {len(use_cases)}")
|
|
252
|
-
print(f" 💾 Report saved: {output_path}")
|
|
253
|
-
|
|
254
|
-
finally:
|
|
255
|
-
await browser.close()
|
|
256
|
-
|
|
257
|
-
async def main():
|
|
258
|
-
parser = argparse.ArgumentParser(description='Interactively explore a website with Playwright')
|
|
259
|
-
parser.add_argument('url', help='Base URL of the website to explore')
|
|
260
|
-
parser.add_argument('--output-dir', default='.', help='Output directory for results (default: current directory)')
|
|
261
|
-
parser.add_argument('--headless', action='store_true', help='Run browser in headless mode')
|
|
262
|
-
parser.add_argument('--max-clicks', type=int, default=20, help='Maximum number of clicks to perform (default: 20)')
|
|
263
|
-
|
|
264
|
-
args = parser.parse_args()
|
|
265
|
-
|
|
266
|
-
interactor = SiteInteractor(args.url, args.output_dir)
|
|
267
|
-
await interactor.run_full_analysis(headless=args.headless)
|
|
268
|
-
|
|
269
|
-
if __name__ == "__main__":
|
|
270
|
-
asyncio.run(main())
|
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Markdown to PDF Converter Script
|
|
5
|
-
*
|
|
6
|
-
* This script converts markdown files to PDF using puppeteer and markdown-it.
|
|
7
|
-
* It supports various markdown features including tables, code blocks, and images.
|
|
8
|
-
*
|
|
9
|
-
* EXECUTION MODEL:
|
|
10
|
-
* - Script location: ~/.fraim/scripts/markdown-to-pdf.js
|
|
11
|
-
* - Working directory: Current project directory (process.cwd())
|
|
12
|
-
* - Input/output files: Relative to current project directory
|
|
13
|
-
* - Config: Reads from .fraim/config.json in current project directory
|
|
14
|
-
*
|
|
15
|
-
* Usage:
|
|
16
|
-
* node ~/.fraim/scripts/markdown-to-pdf.js <input.md> [output.pdf] [options]
|
|
17
|
-
*
|
|
18
|
-
* Options:
|
|
19
|
-
* --format <format> Paper format (A4, Letter, Legal, etc.) - default: A4
|
|
20
|
-
* --margin <margin> Page margins in inches - default: 0.5
|
|
21
|
-
* --css <file> Custom CSS file for styling
|
|
22
|
-
* --header <text> Header text
|
|
23
|
-
* --footer <text> Footer text
|
|
24
|
-
* --landscape Use landscape orientation
|
|
25
|
-
* --no-background Disable background graphics
|
|
26
|
-
*
|
|
27
|
-
* Examples:
|
|
28
|
-
* node ~/.fraim/scripts/markdown-to-pdf.js README.md
|
|
29
|
-
* node ~/.fraim/scripts/markdown-to-pdf.js docs/spec.md output/spec.pdf --format Letter
|
|
30
|
-
* node ~/.fraim/scripts/markdown-to-pdf.js report.md --css custom.css --header "Company Report"
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
const fs = require('fs');
|
|
34
|
-
const path = require('path');
|
|
35
|
-
|
|
36
|
-
// Get project directory (where the user is working)
|
|
37
|
-
const PROJECT_DIR = process.cwd();
|
|
38
|
-
const CONFIG_FILE = path.join(PROJECT_DIR, '.fraim', 'config.json');
|
|
39
|
-
|
|
40
|
-
// Load project configuration if available
|
|
41
|
-
function loadProjectConfig() {
|
|
42
|
-
try {
|
|
43
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
44
|
-
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8'));
|
|
45
|
-
return config;
|
|
46
|
-
}
|
|
47
|
-
} catch (error) {
|
|
48
|
-
console.warn('Warning: Could not load project config from .fraim/config.json');
|
|
49
|
-
}
|
|
50
|
-
return {};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check if required dependencies are available
|
|
54
|
-
function checkDependencies() {
|
|
55
|
-
const requiredPackages = ['puppeteer', 'markdown-it', 'markdown-it-highlightjs'];
|
|
56
|
-
const missingPackages = [];
|
|
57
|
-
|
|
58
|
-
for (const pkg of requiredPackages) {
|
|
59
|
-
try {
|
|
60
|
-
require.resolve(pkg);
|
|
61
|
-
} catch (error) {
|
|
62
|
-
missingPackages.push(pkg);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (missingPackages.length > 0) {
|
|
67
|
-
console.error('Missing required packages:', missingPackages.join(', '));
|
|
68
|
-
console.error('Install them with: npm install', missingPackages.join(' '));
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Parse command line arguments
|
|
75
|
-
function parseArgs() {
|
|
76
|
-
const args = process.argv.slice(2);
|
|
77
|
-
|
|
78
|
-
if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
|
|
79
|
-
console.log(`
|
|
80
|
-
Markdown to PDF Converter
|
|
81
|
-
|
|
82
|
-
Usage: node ~/.fraim/scripts/markdown-to-pdf.js <input.md> [output.pdf] [options]
|
|
83
|
-
|
|
84
|
-
Options:
|
|
85
|
-
--format <format> Paper format (A4, Letter, Legal, etc.) - default: A4
|
|
86
|
-
--margin <margin> Page margins in inches - default: 0.5
|
|
87
|
-
--css <file> Custom CSS file for styling
|
|
88
|
-
--header <text> Header text
|
|
89
|
-
--footer <text> Footer text
|
|
90
|
-
--landscape Use landscape orientation
|
|
91
|
-
--no-background Disable background graphics
|
|
92
|
-
--help, -h Show this help message
|
|
93
|
-
|
|
94
|
-
Examples:
|
|
95
|
-
node ~/.fraim/scripts/markdown-to-pdf.js README.md
|
|
96
|
-
node ~/.fraim/scripts/markdown-to-pdf.js docs/spec.md output/spec.pdf --format Letter
|
|
97
|
-
node ~/.fraim/scripts/markdown-to-pdf.js report.md --css custom.css --header "Company Report"
|
|
98
|
-
|
|
99
|
-
Working Directory: ${PROJECT_DIR}
|
|
100
|
-
Config File: ${CONFIG_FILE}
|
|
101
|
-
`);
|
|
102
|
-
process.exit(0);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const config = {
|
|
106
|
-
input: args[0],
|
|
107
|
-
output: null,
|
|
108
|
-
format: 'A4',
|
|
109
|
-
margin: '0.5in',
|
|
110
|
-
css: null,
|
|
111
|
-
header: null,
|
|
112
|
-
footer: null,
|
|
113
|
-
landscape: false,
|
|
114
|
-
background: true
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// Resolve input path relative to project directory
|
|
118
|
-
if (!path.isAbsolute(config.input)) {
|
|
119
|
-
config.input = path.resolve(PROJECT_DIR, config.input);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Check if second argument is output file or option
|
|
123
|
-
if (args[1] && !args[1].startsWith('--')) {
|
|
124
|
-
config.output = args[1];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Parse options
|
|
128
|
-
for (let i = 0; i < args.length; i++) {
|
|
129
|
-
const arg = args[i];
|
|
130
|
-
|
|
131
|
-
if (arg === '--format' && args[i + 1]) {
|
|
132
|
-
config.format = args[i + 1];
|
|
133
|
-
i++;
|
|
134
|
-
} else if (arg === '--margin' && args[i + 1]) {
|
|
135
|
-
config.margin = args[i + 1];
|
|
136
|
-
i++;
|
|
137
|
-
} else if (arg === '--css' && args[i + 1]) {
|
|
138
|
-
config.css = args[i + 1];
|
|
139
|
-
i++;
|
|
140
|
-
} else if (arg === '--header' && args[i + 1]) {
|
|
141
|
-
config.header = args[i + 1];
|
|
142
|
-
i++;
|
|
143
|
-
} else if (arg === '--footer' && args[i + 1]) {
|
|
144
|
-
config.footer = args[i + 1];
|
|
145
|
-
i++;
|
|
146
|
-
} else if (arg === '--landscape') {
|
|
147
|
-
config.landscape = true;
|
|
148
|
-
} else if (arg === '--no-background') {
|
|
149
|
-
config.background = false;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Set default output if not provided
|
|
154
|
-
if (!config.output) {
|
|
155
|
-
const inputPath = path.parse(config.input);
|
|
156
|
-
config.output = path.join(inputPath.dir, inputPath.name + '.pdf');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Resolve output path relative to project directory
|
|
160
|
-
if (!path.isAbsolute(config.output)) {
|
|
161
|
-
config.output = path.resolve(PROJECT_DIR, config.output);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Resolve CSS path relative to project directory if provided
|
|
165
|
-
if (config.css && !path.isAbsolute(config.css)) {
|
|
166
|
-
config.css = path.resolve(PROJECT_DIR, config.css);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
return config;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Convert markdown to HTML
|
|
173
|
-
async function markdownToHtml(markdownContent, customCss = null) {
|
|
174
|
-
const MarkdownIt = require('markdown-it');
|
|
175
|
-
const hljs = require('markdown-it-highlightjs');
|
|
176
|
-
|
|
177
|
-
const md = new MarkdownIt({
|
|
178
|
-
html: true,
|
|
179
|
-
linkify: true,
|
|
180
|
-
typographer: true,
|
|
181
|
-
breaks: false
|
|
182
|
-
}).use(hljs);
|
|
183
|
-
|
|
184
|
-
const htmlContent = md.render(markdownContent);
|
|
185
|
-
|
|
186
|
-
// Default CSS for better PDF rendering
|
|
187
|
-
const defaultCss = `
|
|
188
|
-
body {
|
|
189
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
190
|
-
line-height: 1.6;
|
|
191
|
-
color: #333;
|
|
192
|
-
max-width: none;
|
|
193
|
-
margin: 0;
|
|
194
|
-
padding: 20px;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
h1, h2, h3, h4, h5, h6 {
|
|
198
|
-
color: #2c3e50;
|
|
199
|
-
margin-top: 2em;
|
|
200
|
-
margin-bottom: 1em;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
h1 { font-size: 2.5em; border-bottom: 2px solid #3498db; padding-bottom: 0.3em; }
|
|
204
|
-
h2 { font-size: 2em; border-bottom: 1px solid #bdc3c7; padding-bottom: 0.3em; }
|
|
205
|
-
h3 { font-size: 1.5em; }
|
|
206
|
-
|
|
207
|
-
code {
|
|
208
|
-
background-color: #f8f9fa;
|
|
209
|
-
padding: 2px 4px;
|
|
210
|
-
border-radius: 3px;
|
|
211
|
-
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
pre {
|
|
215
|
-
background-color: #f8f9fa;
|
|
216
|
-
border: 1px solid #e9ecef;
|
|
217
|
-
border-radius: 6px;
|
|
218
|
-
padding: 16px;
|
|
219
|
-
overflow-x: auto;
|
|
220
|
-
margin: 1em 0;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
pre code {
|
|
224
|
-
background: none;
|
|
225
|
-
padding: 0;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
table {
|
|
229
|
-
border-collapse: collapse;
|
|
230
|
-
width: 100%;
|
|
231
|
-
margin: 1em 0;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
th, td {
|
|
235
|
-
border: 1px solid #ddd;
|
|
236
|
-
padding: 12px;
|
|
237
|
-
text-align: left;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
th {
|
|
241
|
-
background-color: #f2f2f2;
|
|
242
|
-
font-weight: bold;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
blockquote {
|
|
246
|
-
border-left: 4px solid #3498db;
|
|
247
|
-
margin: 1em 0;
|
|
248
|
-
padding-left: 1em;
|
|
249
|
-
color: #7f8c8d;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
img {
|
|
253
|
-
max-width: 100%;
|
|
254
|
-
height: auto;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
a {
|
|
258
|
-
color: #3498db;
|
|
259
|
-
text-decoration: none;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
a:hover {
|
|
263
|
-
text-decoration: underline;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
ul, ol {
|
|
267
|
-
padding-left: 2em;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
li {
|
|
271
|
-
margin: 0.5em 0;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
@media print {
|
|
275
|
-
body { margin: 0; }
|
|
276
|
-
h1, h2, h3, h4, h5, h6 { page-break-after: avoid; }
|
|
277
|
-
pre, blockquote { page-break-inside: avoid; }
|
|
278
|
-
img { page-break-inside: avoid; }
|
|
279
|
-
}
|
|
280
|
-
`;
|
|
281
|
-
|
|
282
|
-
let customCssContent = '';
|
|
283
|
-
if (customCss && fs.existsSync(customCss)) {
|
|
284
|
-
customCssContent = fs.readFileSync(customCss, 'utf8');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return `
|
|
288
|
-
<!DOCTYPE html>
|
|
289
|
-
<html>
|
|
290
|
-
<head>
|
|
291
|
-
<meta charset="utf-8">
|
|
292
|
-
<title>Converted from Markdown</title>
|
|
293
|
-
<style>${defaultCss}${customCssContent}</style>
|
|
294
|
-
</head>
|
|
295
|
-
<body>
|
|
296
|
-
${htmlContent}
|
|
297
|
-
</body>
|
|
298
|
-
</html>
|
|
299
|
-
`;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// Convert HTML to PDF using Puppeteer
|
|
303
|
-
async function htmlToPdf(html, outputPath, config) {
|
|
304
|
-
const puppeteer = require('puppeteer');
|
|
305
|
-
|
|
306
|
-
const browser = await puppeteer.launch({
|
|
307
|
-
headless: 'new',
|
|
308
|
-
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
try {
|
|
312
|
-
const page = await browser.newPage();
|
|
313
|
-
await page.setContent(html, { waitUntil: 'networkidle0' });
|
|
314
|
-
|
|
315
|
-
const pdfOptions = {
|
|
316
|
-
path: outputPath,
|
|
317
|
-
format: config.format,
|
|
318
|
-
margin: {
|
|
319
|
-
top: config.margin,
|
|
320
|
-
right: config.margin,
|
|
321
|
-
bottom: config.margin,
|
|
322
|
-
left: config.margin
|
|
323
|
-
},
|
|
324
|
-
landscape: config.landscape,
|
|
325
|
-
printBackground: config.background,
|
|
326
|
-
preferCSSPageSize: true
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
if (config.header) {
|
|
330
|
-
pdfOptions.displayHeaderFooter = true;
|
|
331
|
-
pdfOptions.headerTemplate = `<div style="font-size: 10px; width: 100%; text-align: center;">${config.header}</div>`;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (config.footer) {
|
|
335
|
-
pdfOptions.displayHeaderFooter = true;
|
|
336
|
-
pdfOptions.footerTemplate = `<div style="font-size: 10px; width: 100%; text-align: center;">${config.footer}</div>`;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
await page.pdf(pdfOptions);
|
|
340
|
-
console.log(`✅ PDF generated successfully: ${path.relative(PROJECT_DIR, outputPath)}`);
|
|
341
|
-
|
|
342
|
-
} finally {
|
|
343
|
-
await browser.close();
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Main function
|
|
348
|
-
async function main() {
|
|
349
|
-
try {
|
|
350
|
-
if (!checkDependencies()) {
|
|
351
|
-
process.exit(1);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
const config = parseArgs();
|
|
355
|
-
const projectConfig = loadProjectConfig();
|
|
356
|
-
|
|
357
|
-
console.log(`📄 Converting markdown to PDF...`);
|
|
358
|
-
console.log(` Working directory: ${PROJECT_DIR}`);
|
|
359
|
-
console.log(` Input: ${path.relative(PROJECT_DIR, config.input)}`);
|
|
360
|
-
console.log(` Output: ${path.relative(PROJECT_DIR, config.output)}`);
|
|
361
|
-
|
|
362
|
-
// Validate input file
|
|
363
|
-
if (!fs.existsSync(config.input)) {
|
|
364
|
-
console.error(`❌ Input file not found: ${path.relative(PROJECT_DIR, config.input)}`);
|
|
365
|
-
process.exit(1);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Ensure output directory exists
|
|
369
|
-
const outputDir = path.dirname(config.output);
|
|
370
|
-
if (!fs.existsSync(outputDir)) {
|
|
371
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
372
|
-
console.log(`📁 Created output directory: ${path.relative(PROJECT_DIR, outputDir)}`);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Read markdown file
|
|
376
|
-
const markdownContent = fs.readFileSync(config.input, 'utf8');
|
|
377
|
-
|
|
378
|
-
// Convert to HTML
|
|
379
|
-
const html = await markdownToHtml(markdownContent, config.css);
|
|
380
|
-
|
|
381
|
-
// Convert to PDF
|
|
382
|
-
await htmlToPdf(html, config.output, config);
|
|
383
|
-
|
|
384
|
-
} catch (error) {
|
|
385
|
-
console.error('❌ Error:', error.message);
|
|
386
|
-
process.exit(1);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Run if called directly
|
|
391
|
-
if (require.main === module) {
|
|
392
|
-
main();
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
module.exports = { markdownToHtml, htmlToPdf, parseArgs, checkDependencies };
|