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.
Files changed (107) hide show
  1. package/bin/fraim-mcp.js +52 -19
  2. package/bin/fraim.js +23 -0
  3. package/dist/src/cli/commands/add-ide.js +53 -14
  4. package/dist/src/cli/commands/doctor.js +12 -24
  5. package/dist/src/cli/commands/init-project.js +0 -3
  6. package/dist/src/cli/commands/init.js +0 -2
  7. package/dist/src/cli/commands/mcp.js +65 -0
  8. package/dist/src/cli/commands/setup.js +17 -1
  9. package/dist/src/cli/commands/sync.js +173 -104
  10. package/dist/src/cli/setup/auto-mcp-setup.js +6 -4
  11. package/dist/src/cli/setup/mcp-config-generator.js +65 -41
  12. package/dist/src/fraim/issue-tracking/ado-provider.js +304 -0
  13. package/dist/src/fraim/issue-tracking/factory.js +63 -0
  14. package/dist/src/fraim/issue-tracking/github-provider.js +200 -0
  15. package/dist/src/fraim/issue-tracking/types.js +7 -0
  16. package/dist/src/fraim/issue-tracking-config.js +83 -0
  17. package/dist/src/local-mcp-server/stdio-server.js +23 -3
  18. package/dist/src/utils/remote-sync.js +130 -0
  19. package/package.json +2 -3
  20. package/dist/src/utils/enforcement-utils.js +0 -239
  21. package/dist/src/utils/validate-workflows.js +0 -101
  22. package/registry/scripts/cleanup-branch.ts +0 -341
  23. package/registry/scripts/code-quality-check.sh +0 -566
  24. package/registry/scripts/comprehensive-explorer.py +0 -297
  25. package/registry/scripts/create-git-labels.sh +0 -49
  26. package/registry/scripts/create-website-structure.js +0 -562
  27. package/registry/scripts/detect-tautological-tests.sh +0 -38
  28. package/registry/scripts/evaluate-code-quality.ts +0 -36
  29. package/registry/scripts/exec-with-timeout.ts +0 -122
  30. package/registry/scripts/generate-engagement-emails.ts +0 -830
  31. package/registry/scripts/interactive-explorer.py +0 -270
  32. package/registry/scripts/markdown-to-pdf.js +0 -395
  33. package/registry/scripts/newsletter-helpers.ts +0 -777
  34. package/registry/scripts/pdf-styles.css +0 -172
  35. package/registry/scripts/prep-issue.sh +0 -548
  36. package/registry/scripts/productivity/build-productivity-csv.mjs +0 -242
  37. package/registry/scripts/productivity/fetch-pr-details.mjs +0 -144
  38. package/registry/scripts/productivity/productivity-report.sh +0 -147
  39. package/registry/scripts/profile-server.ts +0 -426
  40. package/registry/scripts/run-thank-you-workflow.ts +0 -122
  41. package/registry/scripts/scrape-site.py +0 -302
  42. package/registry/scripts/send-newsletter-simple.ts +0 -102
  43. package/registry/scripts/send-thank-you-emails.ts +0 -57
  44. package/registry/scripts/validate-openapi-limits.ts +0 -366
  45. package/registry/scripts/validate-test-coverage.ts +0 -280
  46. package/registry/scripts/verify-pr-comments.sh +0 -74
  47. package/registry/scripts/verify-test-coverage.ts +0 -36
  48. package/registry/stubs/workflows/azure/cost-optimization.md +0 -11
  49. package/registry/stubs/workflows/bootstrap/create-architecture.md +0 -11
  50. package/registry/stubs/workflows/bootstrap/detect-broken-windows.md +0 -11
  51. package/registry/stubs/workflows/bootstrap/evaluate-code-quality.md +0 -11
  52. package/registry/stubs/workflows/bootstrap/verify-test-coverage.md +0 -11
  53. package/registry/stubs/workflows/brainstorming/blue-sky-brainstorming.md +0 -11
  54. package/registry/stubs/workflows/brainstorming/codebase-brainstorming.md +0 -11
  55. package/registry/stubs/workflows/business-development/create-business-plan.md +0 -11
  56. package/registry/stubs/workflows/business-development/ideate-business-opportunity.md +0 -11
  57. package/registry/stubs/workflows/business-development/price-product.md +0 -18
  58. package/registry/stubs/workflows/compliance/detect-compliance-requirements.md +0 -11
  59. package/registry/stubs/workflows/compliance/generate-audit-evidence.md +0 -11
  60. package/registry/stubs/workflows/compliance/soc2-evidence-generator.md +0 -11
  61. package/registry/stubs/workflows/customer-development/insight-analysis.md +0 -11
  62. package/registry/stubs/workflows/customer-development/insight-triage.md +0 -11
  63. package/registry/stubs/workflows/customer-development/interview-preparation.md +0 -11
  64. package/registry/stubs/workflows/customer-development/linkedin-outreach.md +0 -11
  65. package/registry/stubs/workflows/customer-development/strategic-brainstorming.md +0 -11
  66. package/registry/stubs/workflows/customer-development/thank-customers.md +0 -11
  67. package/registry/stubs/workflows/customer-development/user-survey-dispatch.md +0 -11
  68. package/registry/stubs/workflows/customer-development/users-to-target.md +0 -11
  69. package/registry/stubs/workflows/customer-development/weekly-newsletter.md +0 -11
  70. package/registry/stubs/workflows/deploy/cloud-deployment.md +0 -11
  71. package/registry/stubs/workflows/improve-fraim/contribute.md +0 -11
  72. package/registry/stubs/workflows/improve-fraim/file-issue.md +0 -11
  73. package/registry/stubs/workflows/learning/build-skillset.md +0 -11
  74. package/registry/stubs/workflows/learning/synthesize-learnings.md +0 -11
  75. package/registry/stubs/workflows/legal/contract-review-analysis.md +0 -11
  76. package/registry/stubs/workflows/legal/nda.md +0 -11
  77. package/registry/stubs/workflows/legal/patent-filing.md +0 -11
  78. package/registry/stubs/workflows/legal/saas-contract-development.md +0 -11
  79. package/registry/stubs/workflows/legal/trademark-filing.md +0 -11
  80. package/registry/stubs/workflows/marketing/content-creation.md +0 -11
  81. package/registry/stubs/workflows/marketing/convert-to-pdf.md +0 -11
  82. package/registry/stubs/workflows/marketing/create-modern-website.md +0 -11
  83. package/registry/stubs/workflows/marketing/domain-registration.md +0 -11
  84. package/registry/stubs/workflows/marketing/hbr-article.md +0 -11
  85. package/registry/stubs/workflows/marketing/launch-checklist.md +0 -11
  86. package/registry/stubs/workflows/marketing/marketing-strategy.md +0 -11
  87. package/registry/stubs/workflows/marketing/storytelling.md +0 -11
  88. package/registry/stubs/workflows/performance/analyze-performance.md +0 -11
  89. package/registry/stubs/workflows/product-building/design.md +0 -11
  90. package/registry/stubs/workflows/product-building/implement.md +0 -11
  91. package/registry/stubs/workflows/product-building/iterate-on-pr-comments.md +0 -11
  92. package/registry/stubs/workflows/product-building/prep-issue.md +0 -11
  93. package/registry/stubs/workflows/product-building/prototype.md +0 -11
  94. package/registry/stubs/workflows/product-building/resolve.md +0 -11
  95. package/registry/stubs/workflows/product-building/retrospect.md +0 -11
  96. package/registry/stubs/workflows/product-building/spec.md +0 -11
  97. package/registry/stubs/workflows/product-building/test.md +0 -11
  98. package/registry/stubs/workflows/productivity-report/productivity-report.md +0 -11
  99. package/registry/stubs/workflows/quality-assurance/browser-validation.md +0 -11
  100. package/registry/stubs/workflows/quality-assurance/iterative-improvement-cycle.md +0 -11
  101. package/registry/stubs/workflows/replicate/replicate-discovery.md +0 -11
  102. package/registry/stubs/workflows/replicate/replicate-to-issues.md +0 -11
  103. package/registry/stubs/workflows/reviewer/review-implementation-vs-design-spec.md +0 -11
  104. package/registry/stubs/workflows/reviewer/review-implementation-vs-feature-spec.md +0 -11
  105. package/registry/stubs/workflows/startup-credits/aws-activate-application.md +0 -11
  106. package/registry/stubs/workflows/startup-credits/google-cloud-application.md +0 -11
  107. 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 };