skrypt-ai 0.4.2 → 0.6.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.
Files changed (159) hide show
  1. package/dist/auth/index.d.ts +13 -3
  2. package/dist/auth/index.js +101 -9
  3. package/dist/auth/keychain.d.ts +5 -0
  4. package/dist/auth/keychain.js +82 -0
  5. package/dist/auth/notices.d.ts +3 -0
  6. package/dist/auth/notices.js +42 -0
  7. package/dist/autofix/index.d.ts +0 -4
  8. package/dist/autofix/index.js +10 -24
  9. package/dist/capture/browser.d.ts +11 -0
  10. package/dist/capture/browser.js +173 -0
  11. package/dist/capture/diff.d.ts +23 -0
  12. package/dist/capture/diff.js +52 -0
  13. package/dist/capture/index.d.ts +23 -0
  14. package/dist/capture/index.js +210 -0
  15. package/dist/capture/naming.d.ts +17 -0
  16. package/dist/capture/naming.js +45 -0
  17. package/dist/capture/parser.d.ts +15 -0
  18. package/dist/capture/parser.js +80 -0
  19. package/dist/capture/types.d.ts +57 -0
  20. package/dist/capture/types.js +1 -0
  21. package/dist/cli.js +20 -3
  22. package/dist/commands/autofix.js +136 -120
  23. package/dist/commands/cron.js +58 -47
  24. package/dist/commands/deploy.js +123 -102
  25. package/dist/commands/generate.js +125 -7
  26. package/dist/commands/heal.d.ts +10 -0
  27. package/dist/commands/heal.js +201 -0
  28. package/dist/commands/i18n.js +146 -111
  29. package/dist/commands/import.d.ts +2 -0
  30. package/dist/commands/import.js +157 -0
  31. package/dist/commands/init.js +19 -7
  32. package/dist/commands/lint.js +50 -44
  33. package/dist/commands/llms-txt.js +59 -49
  34. package/dist/commands/login.js +63 -34
  35. package/dist/commands/mcp.js +6 -0
  36. package/dist/commands/monitor.js +13 -8
  37. package/dist/commands/qa.d.ts +2 -0
  38. package/dist/commands/qa.js +43 -0
  39. package/dist/commands/review-pr.js +108 -92
  40. package/dist/commands/sdk.js +128 -122
  41. package/dist/commands/security.d.ts +2 -0
  42. package/dist/commands/security.js +109 -0
  43. package/dist/commands/test.js +91 -92
  44. package/dist/commands/version.js +104 -75
  45. package/dist/commands/watch.js +130 -114
  46. package/dist/config/types.js +2 -2
  47. package/dist/context-hub/index.d.ts +23 -0
  48. package/dist/context-hub/index.js +179 -0
  49. package/dist/context-hub/mappings.d.ts +8 -0
  50. package/dist/context-hub/mappings.js +55 -0
  51. package/dist/context-hub/types.d.ts +33 -0
  52. package/dist/context-hub/types.js +1 -0
  53. package/dist/generator/generator.js +39 -6
  54. package/dist/generator/types.d.ts +7 -0
  55. package/dist/generator/writer.d.ts +3 -1
  56. package/dist/generator/writer.js +36 -7
  57. package/dist/importers/confluence.d.ts +5 -0
  58. package/dist/importers/confluence.js +137 -0
  59. package/dist/importers/detect.d.ts +20 -0
  60. package/dist/importers/detect.js +121 -0
  61. package/dist/importers/docusaurus.d.ts +5 -0
  62. package/dist/importers/docusaurus.js +279 -0
  63. package/dist/importers/gitbook.d.ts +5 -0
  64. package/dist/importers/gitbook.js +189 -0
  65. package/dist/importers/github.d.ts +8 -0
  66. package/dist/importers/github.js +99 -0
  67. package/dist/importers/index.d.ts +15 -0
  68. package/dist/importers/index.js +30 -0
  69. package/dist/importers/markdown.d.ts +6 -0
  70. package/dist/importers/markdown.js +105 -0
  71. package/dist/importers/mintlify.d.ts +5 -0
  72. package/dist/importers/mintlify.js +172 -0
  73. package/dist/importers/notion.d.ts +5 -0
  74. package/dist/importers/notion.js +174 -0
  75. package/dist/importers/readme.d.ts +5 -0
  76. package/dist/importers/readme.js +184 -0
  77. package/dist/importers/transform.d.ts +90 -0
  78. package/dist/importers/transform.js +457 -0
  79. package/dist/importers/types.d.ts +37 -0
  80. package/dist/importers/types.js +1 -0
  81. package/dist/llm/anthropic-client.d.ts +1 -0
  82. package/dist/llm/anthropic-client.js +3 -1
  83. package/dist/llm/index.d.ts +6 -4
  84. package/dist/llm/index.js +76 -261
  85. package/dist/llm/openai-client.d.ts +1 -0
  86. package/dist/llm/openai-client.js +7 -2
  87. package/dist/plugins/index.js +7 -0
  88. package/dist/qa/checks.d.ts +10 -0
  89. package/dist/qa/checks.js +492 -0
  90. package/dist/qa/fixes.d.ts +30 -0
  91. package/dist/qa/fixes.js +277 -0
  92. package/dist/qa/index.d.ts +29 -0
  93. package/dist/qa/index.js +187 -0
  94. package/dist/qa/types.d.ts +24 -0
  95. package/dist/qa/types.js +1 -0
  96. package/dist/scanner/csharp.d.ts +23 -0
  97. package/dist/scanner/csharp.js +421 -0
  98. package/dist/scanner/index.js +53 -26
  99. package/dist/scanner/java.d.ts +39 -0
  100. package/dist/scanner/java.js +318 -0
  101. package/dist/scanner/kotlin.d.ts +23 -0
  102. package/dist/scanner/kotlin.js +389 -0
  103. package/dist/scanner/php.d.ts +57 -0
  104. package/dist/scanner/php.js +351 -0
  105. package/dist/scanner/python.js +17 -0
  106. package/dist/scanner/ruby.d.ts +36 -0
  107. package/dist/scanner/ruby.js +431 -0
  108. package/dist/scanner/swift.d.ts +25 -0
  109. package/dist/scanner/swift.js +392 -0
  110. package/dist/scanner/types.d.ts +1 -1
  111. package/dist/template/content/docs/_navigation.json +46 -0
  112. package/dist/template/content/docs/_sidebars.json +684 -0
  113. package/dist/template/content/docs/core.md +4544 -0
  114. package/dist/template/content/docs/index.mdx +89 -0
  115. package/dist/template/content/docs/integrations.md +1158 -0
  116. package/dist/template/content/docs/llms-full.md +403 -0
  117. package/dist/template/content/docs/llms.txt +4588 -0
  118. package/dist/template/content/docs/other.md +10379 -0
  119. package/dist/template/content/docs/tools.md +746 -0
  120. package/dist/template/content/docs/types.md +531 -0
  121. package/dist/template/docs.json +13 -11
  122. package/dist/template/mdx-components.tsx +27 -2
  123. package/dist/template/package.json +6 -0
  124. package/dist/template/public/search-index.json +1 -1
  125. package/dist/template/scripts/build-search-index.mjs +149 -13
  126. package/dist/template/src/app/api/chat/route.ts +83 -128
  127. package/dist/template/src/app/docs/[...slug]/page.tsx +75 -20
  128. package/dist/template/src/app/docs/llms-full.md +151 -4
  129. package/dist/template/src/app/docs/llms.txt +2464 -847
  130. package/dist/template/src/app/docs/page.mdx +48 -38
  131. package/dist/template/src/app/layout.tsx +3 -1
  132. package/dist/template/src/app/page.tsx +22 -8
  133. package/dist/template/src/components/ai-chat.tsx +73 -64
  134. package/dist/template/src/components/breadcrumbs.tsx +21 -23
  135. package/dist/template/src/components/copy-button.tsx +13 -9
  136. package/dist/template/src/components/copy-page-button.tsx +54 -0
  137. package/dist/template/src/components/docs-layout.tsx +37 -25
  138. package/dist/template/src/components/header.tsx +51 -10
  139. package/dist/template/src/components/mdx/card.tsx +17 -3
  140. package/dist/template/src/components/mdx/code-block.tsx +13 -9
  141. package/dist/template/src/components/mdx/code-group.tsx +13 -8
  142. package/dist/template/src/components/mdx/heading.tsx +15 -2
  143. package/dist/template/src/components/mdx/highlighted-code.tsx +13 -8
  144. package/dist/template/src/components/mdx/index.tsx +2 -0
  145. package/dist/template/src/components/mdx/mermaid.tsx +110 -0
  146. package/dist/template/src/components/mdx/screenshot.tsx +150 -0
  147. package/dist/template/src/components/scroll-to-hash.tsx +48 -0
  148. package/dist/template/src/components/sidebar.tsx +12 -18
  149. package/dist/template/src/components/table-of-contents.tsx +9 -0
  150. package/dist/template/src/lib/highlight.ts +3 -88
  151. package/dist/template/src/lib/navigation.ts +159 -0
  152. package/dist/template/src/lib/search-types.ts +4 -1
  153. package/dist/template/src/lib/search.ts +30 -7
  154. package/dist/template/src/styles/globals.css +17 -6
  155. package/dist/utils/files.d.ts +9 -1
  156. package/dist/utils/files.js +59 -10
  157. package/dist/utils/validation.d.ts +0 -3
  158. package/dist/utils/validation.js +0 -26
  159. package/package.json +5 -1
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
- import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
3
- import { resolve, join, extname } from 'path';
2
+ import { existsSync, readFileSync, writeFileSync, readdirSync, statSync, mkdirSync } from 'fs';
3
+ import { resolve, join, extname, dirname } from 'path';
4
4
  function extractFrontmatter(content) {
5
5
  const match = content.match(/^---\n([\s\S]*?)\n---/);
6
6
  if (!match)
@@ -136,54 +136,64 @@ export const llmsTxtCommand = new Command('llms-txt')
136
136
  .option('--title <title>', 'Documentation title', 'Documentation')
137
137
  .option('--description <desc>', 'Documentation description', 'API and usage documentation')
138
138
  .action(async (docsPath, options) => {
139
- const resolvedDocs = resolve(docsPath);
140
- const outputDir = resolve(options.output || './public');
141
- console.log('skrypt llms-txt');
142
- console.log(` docs: ${resolvedDocs}`);
143
- console.log(` output: ${outputDir}`);
144
- console.log(` base-url: ${options.baseUrl}`);
145
- console.log('');
146
- // Check if docs directory exists
147
- if (!existsSync(resolvedDocs)) {
148
- console.error(`Error: Documentation directory not found: ${resolvedDocs}`);
149
- process.exit(1);
139
+ try {
140
+ const resolvedDocs = resolve(docsPath);
141
+ const outputDir = resolve(options.output || './public');
142
+ console.log('skrypt llms-txt');
143
+ console.log(` docs: ${resolvedDocs}`);
144
+ console.log(` output: ${outputDir}`);
145
+ console.log(` base-url: ${options.baseUrl}`);
146
+ console.log('');
147
+ // Check if docs directory exists
148
+ if (!existsSync(resolvedDocs)) {
149
+ console.error(`Error: Documentation directory not found: ${resolvedDocs}`);
150
+ process.exit(1);
151
+ }
152
+ // Scan documentation
153
+ console.log('Step 1: Scanning documentation...');
154
+ const docs = walkDocs(resolvedDocs);
155
+ if (docs.length === 0) {
156
+ console.error('Error: No documentation files found');
157
+ process.exit(1);
158
+ }
159
+ console.log(` Found ${docs.length} documentation files`);
160
+ // Ensure output directory exists
161
+ mkdirSync(outputDir, { recursive: true });
162
+ // Generate llms.txt (summary)
163
+ console.log('\nStep 2: Generating llms.txt...');
164
+ const llmsTxt = generateLLMsTxt(docs, {
165
+ title: options.title || 'Documentation',
166
+ description: options.description || 'API and usage documentation',
167
+ baseUrl: options.baseUrl || 'https://docs.example.com'
168
+ });
169
+ const llmsTxtPath = join(outputDir, 'llms.txt');
170
+ mkdirSync(dirname(llmsTxtPath), { recursive: true });
171
+ writeFileSync(llmsTxtPath, llmsTxt);
172
+ console.log(` Written: ${llmsTxtPath}`);
173
+ // Generate llms-full.txt (complete content)
174
+ console.log('\nStep 3: Generating llms-full.txt...');
175
+ const llmsFullTxt = generateLLMsFullTxt(docs, {
176
+ title: options.title || 'Documentation',
177
+ description: options.description || 'API and usage documentation',
178
+ baseUrl: options.baseUrl || 'https://docs.example.com'
179
+ });
180
+ const llmsFullTxtPath = join(outputDir, 'llms-full.txt');
181
+ mkdirSync(dirname(llmsFullTxtPath), { recursive: true });
182
+ writeFileSync(llmsFullTxtPath, llmsFullTxt);
183
+ console.log(` Written: ${llmsFullTxtPath}`);
184
+ // Summary
185
+ console.log('\n=== Summary ===');
186
+ console.log(` Documentation files: ${docs.length}`);
187
+ console.log(` llms.txt: ${(llmsTxt.length / 1024).toFixed(1)} KB`);
188
+ console.log(` llms-full.txt: ${(llmsFullTxt.length / 1024).toFixed(1)} KB`);
189
+ console.log('');
190
+ console.log('Add these to your robots.txt:');
191
+ console.log(' Sitemap: /llms.txt');
192
+ console.log('');
193
+ console.log('AI agents (GPTBot, ClaudeBot, etc.) will automatically discover your docs!');
150
194
  }
151
- // Scan documentation
152
- console.log('Step 1: Scanning documentation...');
153
- const docs = walkDocs(resolvedDocs);
154
- if (docs.length === 0) {
155
- console.error('Error: No documentation files found');
195
+ catch (err) {
196
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
156
197
  process.exit(1);
157
198
  }
158
- console.log(` Found ${docs.length} documentation files`);
159
- // Generate llms.txt (summary)
160
- console.log('\nStep 2: Generating llms.txt...');
161
- const llmsTxt = generateLLMsTxt(docs, {
162
- title: options.title || 'Documentation',
163
- description: options.description || 'API and usage documentation',
164
- baseUrl: options.baseUrl || 'https://docs.example.com'
165
- });
166
- const llmsTxtPath = join(outputDir, 'llms.txt');
167
- writeFileSync(llmsTxtPath, llmsTxt);
168
- console.log(` Written: ${llmsTxtPath}`);
169
- // Generate llms-full.txt (complete content)
170
- console.log('\nStep 3: Generating llms-full.txt...');
171
- const llmsFullTxt = generateLLMsFullTxt(docs, {
172
- title: options.title || 'Documentation',
173
- description: options.description || 'API and usage documentation',
174
- baseUrl: options.baseUrl || 'https://docs.example.com'
175
- });
176
- const llmsFullTxtPath = join(outputDir, 'llms-full.txt');
177
- writeFileSync(llmsFullTxtPath, llmsFullTxt);
178
- console.log(` Written: ${llmsFullTxtPath}`);
179
- // Summary
180
- console.log('\n=== Summary ===');
181
- console.log(` Documentation files: ${docs.length}`);
182
- console.log(` llms.txt: ${(llmsTxt.length / 1024).toFixed(1)} KB`);
183
- console.log(` llms-full.txt: ${(llmsFullTxt.length / 1024).toFixed(1)} KB`);
184
- console.log('');
185
- console.log('Add these to your robots.txt:');
186
- console.log(' Sitemap: /llms.txt');
187
- console.log('');
188
- console.log('AI agents (GPTBot, ClaudeBot, etc.) will automatically discover your docs!');
189
199
  });
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
- import { saveAuthConfig, getAuthConfig, clearAuth, checkPlan } from '../auth/index.js';
2
+ import { saveAuthConfig, getAuthConfigAsync, clearAuth, checkPlan, getKeyStorageMethod } from '../auth/index.js';
3
+ import { getKeychainPlatformName } from '../auth/keychain.js';
3
4
  import * as readline from 'readline';
4
5
  function prompt(question) {
5
6
  const rl = readline.createInterface({
@@ -17,49 +18,77 @@ export const loginCommand = new Command('login')
17
18
  .description('Login to Skrypt Pro')
18
19
  .option('--key <api-key>', 'API key (or set SKRYPT_API_KEY env var)')
19
20
  .action(async (options) => {
20
- console.log('skrypt login\n');
21
- let apiKey = options.key || process.env.SKRYPT_API_KEY;
22
- if (!apiKey) {
23
- console.log(' Get your API key at: https://skrypt.sh/dashboard/settings\n');
24
- apiKey = await prompt(' Enter API key: ');
25
- }
26
- if (!apiKey) {
27
- console.error(' Error: No API key provided');
28
- process.exit(1);
21
+ try {
22
+ console.log('skrypt login\n');
23
+ let apiKey = options.key || process.env.SKRYPT_API_KEY;
24
+ if (!apiKey) {
25
+ console.log(' Get your API key at: https://skrypt.sh/dashboard/settings\n');
26
+ apiKey = await prompt(' Enter API key: ');
27
+ }
28
+ if (!apiKey) {
29
+ console.error(' Error: No API key provided');
30
+ process.exit(1);
31
+ }
32
+ console.log('\n Verifying...');
33
+ const result = await checkPlan(apiKey);
34
+ if (!result.valid) {
35
+ console.error(`\n Error: ${result.error || 'Invalid API key'}`);
36
+ process.exit(1);
37
+ }
38
+ await saveAuthConfig({
39
+ apiKey,
40
+ email: result.email,
41
+ plan: result.plan,
42
+ expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString()
43
+ });
44
+ console.log(`\n ✓ Logged in as ${result.email}`);
45
+ console.log(` Plan: ${result.plan.toUpperCase()}\n`);
46
+ // Trust signals
47
+ const storageMethod = await getKeyStorageMethod();
48
+ if (storageMethod === 'keychain') {
49
+ console.log(` 🔒 API key stored in ${getKeychainPlatformName()}`);
50
+ }
51
+ else if (storageMethod === 'file') {
52
+ console.log(' 🔒 API key stored in ~/.skrypt/auth.json (0600 permissions)');
53
+ }
54
+ console.log(' Your key is hashed (SHA-256) on our servers — never stored in plaintext');
55
+ console.log('');
56
+ if (result.plan === 'free') {
57
+ console.log(' Upgrade to Pro: https://skrypt.sh/pro\n');
58
+ }
29
59
  }
30
- console.log('\n Verifying...');
31
- const result = await checkPlan(apiKey);
32
- if (!result.valid) {
33
- console.error(`\n Error: ${result.error || 'Invalid API key'}`);
60
+ catch (err) {
61
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
34
62
  process.exit(1);
35
63
  }
36
- saveAuthConfig({
37
- apiKey,
38
- email: result.email,
39
- plan: result.plan,
40
- expiresAt: new Date(Date.now() + 60 * 60 * 1000).toISOString()
41
- });
42
- console.log(`\n ✓ Logged in as ${result.email}`);
43
- console.log(` Plan: ${result.plan.toUpperCase()}\n`);
44
- if (result.plan === 'free') {
45
- console.log(' Upgrade to Pro: https://skrypt.sh/pro\n');
46
- }
47
64
  });
48
65
  export const logoutCommand = new Command('logout')
49
66
  .description('Logout from Skrypt')
50
67
  .action(async () => {
51
- clearAuth();
52
- console.log(' Logged out successfully\n');
68
+ try {
69
+ await clearAuth();
70
+ console.log(' Logged out successfully\n');
71
+ }
72
+ catch (err) {
73
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
74
+ process.exit(1);
75
+ }
53
76
  });
54
77
  export const whoamiCommand = new Command('whoami')
55
78
  .description('Show current login status')
56
79
  .action(async () => {
57
- const config = getAuthConfig();
58
- if (!config.apiKey) {
59
- console.log(' Not logged in');
60
- console.log(' Run: skrypt login\n');
61
- return;
80
+ try {
81
+ const config = await getAuthConfigAsync();
82
+ if (!config.apiKey) {
83
+ console.log(' Not logged in');
84
+ console.log(' Run: skrypt login\n');
85
+ return;
86
+ }
87
+ console.log(` Email: ${config.email || 'Unknown'}`);
88
+ console.log(` Plan: ${(config.plan || 'free').toUpperCase()}\n`);
89
+ }
90
+ catch (err) {
91
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
92
+ process.exit(1);
62
93
  }
63
- console.log(` Email: ${config.email || 'Unknown'}`);
64
- console.log(` Plan: ${(config.plan || 'free').toUpperCase()}\n`);
65
94
  });
@@ -229,6 +229,12 @@ export const mcpCommand = new Command('mcp')
229
229
  // Get specific doc
230
230
  if (url.pathname.startsWith('/doc/')) {
231
231
  const docPath = decodeURIComponent(url.pathname.slice(5));
232
+ // Prevent path traversal
233
+ if (docPath.includes('..') || docPath.startsWith('/')) {
234
+ res.writeHead(400, { 'Content-Type': 'application/json' });
235
+ res.end(JSON.stringify({ error: 'Invalid path' }));
236
+ return;
237
+ }
232
238
  const doc = docs.find(d => d.path === docPath || d.path === docPath + '.mdx' || d.path === docPath + '.md');
233
239
  if (doc) {
234
240
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { execSync, spawnSync } from 'child_process';
2
+ import { spawnSync } from 'child_process';
3
3
  import { existsSync, writeFileSync, readdirSync, statSync, mkdirSync } from 'fs';
4
4
  import { resolve, join, dirname } from 'path';
5
5
  import { createLLMClient } from '../llm/index.js';
@@ -275,16 +275,21 @@ export const monitorCommand = new Command('monitor')
275
275
  console.log('\n=== Creating GitHub PR ===\n');
276
276
  try {
277
277
  // Check if gh CLI is available
278
- execSync('gh --version', { stdio: 'ignore' });
279
- // Create branch
278
+ spawnSync('gh', ['--version'], { stdio: 'ignore' });
279
+ // Create branch (use spawnSync to avoid shell injection)
280
280
  const branchName = `docs/auto-update-${Date.now()}`;
281
- execSync(`git checkout -b ${branchName}`, { cwd: resolvedPath });
281
+ spawnSync('git', ['checkout', '-b', branchName], { cwd: resolvedPath, stdio: 'inherit' });
282
282
  // Stage and commit
283
- execSync('git add docs/', { cwd: resolvedPath });
284
- execSync(`git commit -m "docs: auto-update documentation for recent changes"`, { cwd: resolvedPath });
283
+ spawnSync('git', ['add', 'docs/'], { cwd: resolvedPath, stdio: 'inherit' });
284
+ spawnSync('git', ['commit', '-m', 'docs: auto-update documentation for recent changes'], { cwd: resolvedPath, stdio: 'inherit' });
285
285
  // Push and create PR
286
- execSync(`git push -u origin ${branchName}`, { cwd: resolvedPath });
287
- const prUrl = execSync(`gh pr create --title "docs: Auto-update documentation" --body "This PR was automatically generated by Skrypt monitor.\n\n## Changes\n${suggestions.map(s => `- ${s.action}: ${s.file}`).join('\n')}"`, { cwd: resolvedPath, encoding: 'utf-8' }).trim();
286
+ spawnSync('git', ['push', '-u', 'origin', branchName], { cwd: resolvedPath, stdio: 'inherit' });
287
+ const changesList = suggestions.map(s => `- ${s.action}: ${s.file}`).join('\n');
288
+ const prBody = `This PR was automatically generated by Skrypt monitor.\n\n## Changes\n${changesList}`;
289
+ const prResult = spawnSync('gh', ['pr', 'create', '--title', 'docs: Auto-update documentation', '--body', prBody], {
290
+ cwd: resolvedPath, encoding: 'utf-8'
291
+ });
292
+ const prUrl = (prResult.stdout || '').trim();
288
293
  console.log(`PR created: ${prUrl}`);
289
294
  }
290
295
  catch (err) {
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const qaCommand: Command;
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import { resolve } from 'path';
3
+ import { existsSync } from 'fs';
4
+ import { runQA, printQAReport, fixQAIssues, printFixReport } from '../qa/index.js';
5
+ export const qaCommand = new Command('qa')
6
+ .description('Run quality assurance checks on documentation')
7
+ .argument('<dir>', 'Documentation directory to check')
8
+ .option('--strict', 'Fail on warnings (not just errors)')
9
+ .option('--fix', 'Auto-fix safe issues before reporting')
10
+ .action((dir, options) => {
11
+ const targetDir = resolve(dir);
12
+ if (!existsSync(targetDir)) {
13
+ console.error(`Error: Directory not found: ${targetDir}`);
14
+ process.exit(1);
15
+ }
16
+ console.log('skrypt qa');
17
+ console.log(` target: ${targetDir}`);
18
+ // Auto-fix if requested
19
+ if (options.fix) {
20
+ const fixReport = fixQAIssues(targetDir);
21
+ printFixReport(fixReport);
22
+ }
23
+ const report = runQA(targetDir);
24
+ printQAReport(report);
25
+ // Show check-by-check summary
26
+ console.log('\n Checks:');
27
+ for (const check of report.checks) {
28
+ const icon = check.passed ? '✓' : '✗';
29
+ const count = check.issues.length;
30
+ const label = count > 0 ? ` (${count} issue${count > 1 ? 's' : ''})` : '';
31
+ console.log(` ${icon} ${check.check}${label}`);
32
+ }
33
+ console.log('');
34
+ if (!report.passed) {
35
+ console.log('QA failed — fix errors before deploying.\n');
36
+ process.exit(1);
37
+ }
38
+ if (options.strict && report.warnings > 0) {
39
+ console.log('QA failed (strict mode) — warnings present.\n');
40
+ process.exit(1);
41
+ }
42
+ console.log('QA passed.\n');
43
+ });
@@ -7,111 +7,127 @@ export const reviewPRCommand = new Command('review-pr')
7
7
  .option('--dry-run', 'Show issues without posting comments')
8
8
  .option('--token <token>', 'GitHub token (or use GITHUB_TOKEN env var)')
9
9
  .action(async (prUrl, options) => {
10
- // Parse PR URL
11
- const urlMatch = prUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
12
- if (!urlMatch) {
13
- console.error('Error: Invalid PR URL format');
14
- console.error('Expected: https://github.com/owner/repo/pull/123');
15
- process.exit(1);
16
- }
17
- const owner = urlMatch[1];
18
- const repo = urlMatch[2];
19
- const pullNumber = urlMatch[3];
20
- if (!owner || !repo || !pullNumber) {
21
- console.error('Error: Could not parse owner/repo/PR number from URL');
22
- process.exit(1);
23
- }
24
- console.log('skrypt review-pr');
25
- console.log(` repo: ${owner}/${repo}`);
26
- console.log(` PR: #${pullNumber}`);
27
- console.log('');
28
- const token = options.token || process.env.GITHUB_TOKEN;
29
- if (!token) {
30
- console.error('Error: GITHUB_TOKEN environment variable or --token required');
31
- process.exit(1);
32
- }
33
- // Analyze the PR
34
- console.log('Analyzing PR for documentation issues...');
35
- const config = {
36
- owner,
37
- repo,
38
- pullNumber: parseInt(pullNumber),
39
- token,
40
- };
41
- let issues;
42
10
  try {
43
- issues = await analyzePRForDocs(config);
44
- }
45
- catch (err) {
46
- const message = err instanceof Error ? err.message : String(err);
47
- console.error(`Error analyzing PR: ${message}`);
48
- process.exit(1);
49
- }
50
- if (issues.length === 0) {
51
- console.log('\n✓ No documentation issues found!');
52
- process.exit(0);
53
- }
54
- console.log(`\nFound ${issues.length} issue(s):`);
55
- console.log('');
56
- // Group by severity
57
- const errors = issues.filter(i => i.severity === 'error');
58
- const warnings = issues.filter(i => i.severity === 'warning');
59
- const infos = issues.filter(i => i.severity === 'info');
60
- if (errors.length > 0) {
61
- console.log(`❌ Errors (${errors.length}):`);
62
- for (const issue of errors) {
63
- console.log(` ${issue.file}: ${issue.message}`);
11
+ // Parse PR URL
12
+ const urlMatch = prUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
13
+ if (!urlMatch) {
14
+ console.error('Error: Invalid PR URL format');
15
+ console.error('Expected: https://github.com/owner/repo/pull/123');
16
+ process.exit(1);
64
17
  }
65
- }
66
- if (warnings.length > 0) {
67
- console.log(`⚠️ Warnings (${warnings.length}):`);
68
- for (const issue of warnings) {
69
- console.log(` ${issue.file}: ${issue.message}`);
18
+ const owner = urlMatch[1];
19
+ const repo = urlMatch[2];
20
+ const pullNumber = urlMatch[3];
21
+ if (!owner || !repo || !pullNumber) {
22
+ console.error('Error: Could not parse owner/repo/PR number from URL');
23
+ process.exit(1);
70
24
  }
71
- }
72
- if (infos.length > 0) {
73
- console.log(`ℹ️ Suggestions (${infos.length}):`);
74
- for (const issue of infos) {
75
- console.log(` ${issue.file}: ${issue.message}`);
25
+ // Validate owner/repo contain only safe characters (prevent API path injection)
26
+ const safeNamePattern = /^[a-zA-Z0-9._-]+$/;
27
+ if (!safeNamePattern.test(owner) || !safeNamePattern.test(repo)) {
28
+ console.error('Error: Invalid characters in owner/repo name');
29
+ process.exit(1);
76
30
  }
77
- }
78
- if (options.dryRun) {
79
- console.log('\n[dry run - no comments posted]');
80
- process.exit(0);
81
- }
82
- // Post comments
83
- console.log('\nPosting comments to PR...');
84
- if (options.inline) {
85
- const inlineIssues = issues.filter(i => i.line);
86
- const generalIssues = issues.filter(i => !i.line);
87
- if (inlineIssues.length > 0) {
88
- const inlineResults = await postInlineComments(config, inlineIssues);
89
- const success = inlineResults.filter(r => r.success).length;
90
- console.log(` Posted ${success}/${inlineIssues.length} inline comments`);
31
+ console.log('skrypt review-pr');
32
+ console.log(` repo: ${owner}/${repo}`);
33
+ console.log(` PR: #${pullNumber}`);
34
+ console.log('');
35
+ if (options.token) {
36
+ console.warn(' Warning: Passing tokens via CLI arguments exposes them in shell history.');
37
+ console.warn(' Prefer: GITHUB_TOKEN env var\n');
91
38
  }
92
- if (generalIssues.length > 0) {
93
- const result = await postPRComment(config, generalIssues);
94
- if (result.success) {
95
- console.log(` Posted summary comment`);
39
+ const token = options.token || process.env.GITHUB_TOKEN;
40
+ if (!token) {
41
+ console.error('Error: GITHUB_TOKEN environment variable or --token required');
42
+ process.exit(1);
43
+ }
44
+ // Analyze the PR
45
+ console.log('Analyzing PR for documentation issues...');
46
+ const config = {
47
+ owner,
48
+ repo,
49
+ pullNumber: parseInt(pullNumber),
50
+ token,
51
+ };
52
+ let issues;
53
+ try {
54
+ issues = await analyzePRForDocs(config);
55
+ }
56
+ catch (err) {
57
+ const message = err instanceof Error ? err.message : String(err);
58
+ console.error(`Error analyzing PR: ${message}`);
59
+ process.exit(1);
60
+ }
61
+ if (issues.length === 0) {
62
+ console.log('\n✓ No documentation issues found!');
63
+ process.exit(0);
64
+ }
65
+ console.log(`\nFound ${issues.length} issue(s):`);
66
+ console.log('');
67
+ // Group by severity
68
+ const errors = issues.filter(i => i.severity === 'error');
69
+ const warnings = issues.filter(i => i.severity === 'warning');
70
+ const infos = issues.filter(i => i.severity === 'info');
71
+ if (errors.length > 0) {
72
+ console.log(`❌ Errors (${errors.length}):`);
73
+ for (const issue of errors) {
74
+ console.log(` ${issue.file}: ${issue.message}`);
96
75
  }
97
- else {
98
- console.error(` Failed to post summary: ${result.error}`);
76
+ }
77
+ if (warnings.length > 0) {
78
+ console.log(`⚠️ Warnings (${warnings.length}):`);
79
+ for (const issue of warnings) {
80
+ console.log(` ${issue.file}: ${issue.message}`);
99
81
  }
100
82
  }
101
- }
102
- else {
103
- const result = await postPRComment(config, issues);
104
- if (result.success) {
105
- console.log(`✓ Comment posted successfully`);
83
+ if (infos.length > 0) {
84
+ console.log(`ℹ️ Suggestions (${infos.length}):`);
85
+ for (const issue of infos) {
86
+ console.log(` ${issue.file}: ${issue.message}`);
87
+ }
88
+ }
89
+ if (options.dryRun) {
90
+ console.log('\n[dry run - no comments posted]');
91
+ process.exit(0);
92
+ }
93
+ // Post comments
94
+ console.log('\nPosting comments to PR...');
95
+ if (options.inline) {
96
+ const inlineIssues = issues.filter(i => i.line);
97
+ const generalIssues = issues.filter(i => !i.line);
98
+ if (inlineIssues.length > 0) {
99
+ const inlineResults = await postInlineComments(config, inlineIssues);
100
+ const success = inlineResults.filter(r => r.success).length;
101
+ console.log(` Posted ${success}/${inlineIssues.length} inline comments`);
102
+ }
103
+ if (generalIssues.length > 0) {
104
+ const result = await postPRComment(config, generalIssues);
105
+ if (result.success) {
106
+ console.log(` Posted summary comment`);
107
+ }
108
+ else {
109
+ console.error(` Failed to post summary: ${result.error}`);
110
+ }
111
+ }
106
112
  }
107
113
  else {
108
- console.error(`✗ Failed to post comment: ${result.error}`);
114
+ const result = await postPRComment(config, issues);
115
+ if (result.success) {
116
+ console.log(`✓ Comment posted successfully`);
117
+ }
118
+ else {
119
+ console.error(`✗ Failed to post comment: ${result.error}`);
120
+ process.exit(1);
121
+ }
122
+ }
123
+ console.log('\nDone!');
124
+ // Exit with error code if there are errors
125
+ if (errors.length > 0) {
109
126
  process.exit(1);
110
127
  }
111
128
  }
112
- console.log('\nDone!');
113
- // Exit with error code if there are errors
114
- if (errors.length > 0) {
129
+ catch (err) {
130
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
115
131
  process.exit(1);
116
132
  }
117
133
  });