skrypt-ai 0.3.3 → 0.4.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 (97) hide show
  1. package/README.md +1 -1
  2. package/dist/auth/index.d.ts +0 -1
  3. package/dist/auth/index.js +3 -5
  4. package/dist/autofix/index.js +15 -3
  5. package/dist/cli.js +19 -4
  6. package/dist/commands/check-links.js +164 -174
  7. package/dist/commands/deploy.js +5 -2
  8. package/dist/commands/generate.js +206 -199
  9. package/dist/commands/i18n.js +3 -20
  10. package/dist/commands/init.js +47 -40
  11. package/dist/commands/lint.js +3 -20
  12. package/dist/commands/mcp.js +125 -122
  13. package/dist/commands/monitor.js +125 -108
  14. package/dist/commands/review-pr.js +1 -1
  15. package/dist/commands/sdk.js +1 -1
  16. package/dist/config/loader.js +21 -2
  17. package/dist/generator/organizer.d.ts +3 -0
  18. package/dist/generator/organizer.js +4 -9
  19. package/dist/generator/writer.js +2 -10
  20. package/dist/github/pr-comments.js +21 -8
  21. package/dist/plugins/index.js +1 -0
  22. package/dist/scanner/index.js +8 -2
  23. package/dist/template/docs.json +2 -1
  24. package/dist/template/next.config.mjs +3 -1
  25. package/dist/template/package.json +17 -14
  26. package/dist/template/public/favicon.svg +4 -0
  27. package/dist/template/public/search-index.json +1 -1
  28. package/dist/template/scripts/build-search-index.mjs +120 -25
  29. package/dist/template/src/app/api/chat/route.ts +11 -3
  30. package/dist/template/src/app/docs/README.md +28 -0
  31. package/dist/template/src/app/docs/[...slug]/page.tsx +141 -14
  32. package/dist/template/src/app/docs/auth/page.mdx +589 -0
  33. package/dist/template/src/app/docs/autofix/page.mdx +624 -0
  34. package/dist/template/src/app/docs/cli/page.mdx +217 -0
  35. package/dist/template/src/app/docs/config/page.mdx +428 -0
  36. package/dist/template/src/app/docs/configuration/page.mdx +86 -0
  37. package/dist/template/src/app/docs/deployment/page.mdx +112 -0
  38. package/dist/template/src/app/docs/error.tsx +20 -0
  39. package/dist/template/src/app/docs/generator/generator.md +504 -0
  40. package/dist/template/src/app/docs/generator/organizer.md +779 -0
  41. package/dist/template/src/app/docs/generator/page.mdx +613 -0
  42. package/dist/template/src/app/docs/github/page.mdx +502 -0
  43. package/dist/template/src/app/docs/llm/anthropic-client.md +549 -0
  44. package/dist/template/src/app/docs/llm/index.md +471 -0
  45. package/dist/template/src/app/docs/llm/page.mdx +428 -0
  46. package/dist/template/src/app/docs/llms-full.md +256 -0
  47. package/dist/template/src/app/docs/llms.txt +2971 -0
  48. package/dist/template/src/app/docs/not-found.tsx +23 -0
  49. package/dist/template/src/app/docs/page.mdx +0 -3
  50. package/dist/template/src/app/docs/plugins/page.mdx +1793 -0
  51. package/dist/template/src/app/docs/pro/page.mdx +121 -0
  52. package/dist/template/src/app/docs/quickstart/page.mdx +93 -0
  53. package/dist/template/src/app/docs/scanner/content-type.md +599 -0
  54. package/dist/template/src/app/docs/scanner/index.md +212 -0
  55. package/dist/template/src/app/docs/scanner/page.mdx +307 -0
  56. package/dist/template/src/app/docs/scanner/python.md +469 -0
  57. package/dist/template/src/app/docs/scanner/python_parser.md +1056 -0
  58. package/dist/template/src/app/docs/scanner/rust.md +325 -0
  59. package/dist/template/src/app/docs/scanner/typescript.md +201 -0
  60. package/dist/template/src/app/error.tsx +3 -3
  61. package/dist/template/src/app/icon.tsx +29 -0
  62. package/dist/template/src/app/layout.tsx +57 -7
  63. package/dist/template/src/app/not-found.tsx +35 -0
  64. package/dist/template/src/app/page.tsx +95 -11
  65. package/dist/template/src/components/ai-chat.tsx +26 -21
  66. package/dist/template/src/components/breadcrumbs.tsx +56 -12
  67. package/dist/template/src/components/copy-button.tsx +17 -3
  68. package/dist/template/src/components/docs-layout.tsx +202 -8
  69. package/dist/template/src/components/feedback.tsx +4 -2
  70. package/dist/template/src/components/footer.tsx +42 -0
  71. package/dist/template/src/components/header.tsx +56 -20
  72. package/dist/template/src/components/mdx/accordion.tsx +17 -13
  73. package/dist/template/src/components/mdx/callout.tsx +50 -37
  74. package/dist/template/src/components/mdx/card.tsx +24 -12
  75. package/dist/template/src/components/mdx/code-block.tsx +17 -3
  76. package/dist/template/src/components/mdx/code-group.tsx +78 -18
  77. package/dist/template/src/components/mdx/code-playground.tsx +3 -0
  78. package/dist/template/src/components/mdx/go-playground.tsx +3 -0
  79. package/dist/template/src/components/mdx/highlighted-code.tsx +178 -38
  80. package/dist/template/src/components/mdx/python-playground.tsx +2 -0
  81. package/dist/template/src/components/mdx/steps.tsx +6 -6
  82. package/dist/template/src/components/mdx/tabs.tsx +76 -8
  83. package/dist/template/src/components/page-header.tsx +19 -0
  84. package/dist/template/src/components/scroll-to-top.tsx +33 -0
  85. package/dist/template/src/components/search-dialog.tsx +251 -57
  86. package/dist/template/src/components/sidebar.tsx +137 -77
  87. package/dist/template/src/components/table-of-contents.tsx +29 -13
  88. package/dist/template/src/lib/highlight.ts +90 -31
  89. package/dist/template/src/lib/search.ts +14 -4
  90. package/dist/template/src/lib/theme-utils.ts +140 -0
  91. package/dist/template/src/styles/globals.css +397 -84
  92. package/dist/template/src/types/remark-gfm.d.ts +2 -0
  93. package/dist/utils/files.d.ts +9 -0
  94. package/dist/utils/files.js +33 -0
  95. package/dist/utils/validation.d.ts +4 -0
  96. package/dist/utils/validation.js +38 -0
  97. package/package.json +1 -4
@@ -1,26 +1,9 @@
1
1
  import { Command } from 'commander';
2
- import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
3
- import { resolve, join, extname, relative } from 'path';
2
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
3
+ import { resolve, relative } from 'path';
4
+ import { findMdxFiles } from '../utils/files.js';
4
5
  import { createServer } from 'http';
5
6
  import { requirePro } from '../auth/index.js';
6
- function findMdxFiles(dir) {
7
- const files = [];
8
- function walk(currentDir) {
9
- const entries = readdirSync(currentDir);
10
- for (const entry of entries) {
11
- const fullPath = join(currentDir, entry);
12
- const stat = statSync(fullPath);
13
- if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'node_modules') {
14
- walk(fullPath);
15
- }
16
- else if (stat.isFile() && (extname(entry) === '.mdx' || extname(entry) === '.md')) {
17
- files.push(fullPath);
18
- }
19
- }
20
- }
21
- walk(dir);
22
- return files;
23
- }
24
7
  function parseDocFile(filePath, basePath) {
25
8
  const content = readFileSync(filePath, 'utf-8');
26
9
  const relPath = relative(basePath, filePath);
@@ -166,116 +149,136 @@ export const mcpCommand = new Command('mcp')
166
149
  .option('-p, --port <port>', 'Server port', '3100')
167
150
  .option('-h, --host <host>', 'Server host', 'localhost')
168
151
  .option('-o, --output <file>', 'Output manifest to file instead of starting server')
152
+ .option('-t, --token <token>', 'Bearer token for authenticating MCP requests')
169
153
  .action(async (path, options) => {
170
- // Pro feature - requires subscription
171
- if (!await requirePro('mcp')) {
172
- process.exit(1);
173
- }
174
- const targetPath = resolve(path);
175
- if (!existsSync(targetPath)) {
176
- console.error(`Error: Path not found: ${targetPath}`);
177
- process.exit(1);
178
- }
179
- // Find and parse all docs
180
- const files = findMdxFiles(targetPath);
181
- if (files.length === 0) {
182
- console.error('No .md or .mdx files found.');
183
- process.exit(1);
184
- }
185
- const docs = files.map(f => parseDocFile(f, targetPath));
186
- const projectName = targetPath.split('/').pop() || 'docs';
187
- // Output mode: write manifest to file
188
- if (options.output) {
189
- const manifest = generateMcpManifest(docs, projectName);
190
- const outputPath = resolve(options.output);
191
- writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
192
- console.log(`MCP manifest written to ${outputPath}`);
193
- process.exit(0);
194
- }
195
- // Server mode
196
- const port = parseInt(options.port) || 3100;
197
- const host = options.host || 'localhost';
198
- console.log('skrypt mcp');
199
- console.log(` docs: ${files.length} files`);
200
- console.log(` server: http://${host}:${port}`);
201
- console.log('');
202
- const manifest = generateMcpManifest(docs, projectName);
203
- const server = createServer((req, res) => {
204
- const url = new URL(req.url || '/', `http://${host}:${port}`);
205
- // CORS headers
206
- res.setHeader('Access-Control-Allow-Origin', '*');
207
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
208
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
209
- if (req.method === 'OPTIONS') {
210
- res.writeHead(204);
211
- res.end();
212
- return;
154
+ try {
155
+ // Pro feature - requires subscription
156
+ if (!await requirePro('mcp')) {
157
+ process.exit(1);
158
+ }
159
+ const targetPath = resolve(path);
160
+ if (!existsSync(targetPath)) {
161
+ console.error(`Error: Path not found: ${targetPath}`);
162
+ process.exit(1);
163
+ }
164
+ // Find and parse all docs
165
+ const files = findMdxFiles(targetPath);
166
+ if (files.length === 0) {
167
+ console.error('No .md or .mdx files found.');
168
+ process.exit(1);
213
169
  }
214
- // MCP manifest
215
- if (url.pathname === '/' || url.pathname === '/manifest') {
216
- res.writeHead(200, { 'Content-Type': 'application/json' });
217
- res.end(JSON.stringify(manifest, null, 2));
218
- return;
170
+ const docs = files.map(f => parseDocFile(f, targetPath));
171
+ const projectName = targetPath.split('/').pop() || 'docs';
172
+ // Output mode: write manifest to file
173
+ if (options.output) {
174
+ const manifest = generateMcpManifest(docs, projectName);
175
+ const outputPath = resolve(options.output);
176
+ writeFileSync(outputPath, JSON.stringify(manifest, null, 2));
177
+ console.log(`MCP manifest written to ${outputPath}`);
178
+ process.exit(0);
219
179
  }
220
- // List docs
221
- if (url.pathname === '/docs' || url.pathname === '/list') {
222
- res.writeHead(200, { 'Content-Type': 'application/json' });
223
- res.end(JSON.stringify({
224
- docs: docs.map(d => ({
180
+ // Server mode
181
+ const port = Number(options.port) || 3100;
182
+ const host = options.host || 'localhost';
183
+ console.log('skrypt mcp');
184
+ console.log(` docs: ${files.length} files`);
185
+ console.log(` server: http://${host}:${port}`);
186
+ if (!options.token) {
187
+ console.warn('Warning: MCP server running without authentication. Use --token to secure.');
188
+ }
189
+ console.log('');
190
+ const manifest = generateMcpManifest(docs, projectName);
191
+ const server = createServer((req, res) => {
192
+ const url = new URL(req.url || '/', `http://${host}:${port}`);
193
+ // CORS headers
194
+ res.setHeader('Access-Control-Allow-Origin', `http://localhost:${port}`);
195
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
196
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
197
+ if (req.method === 'OPTIONS') {
198
+ res.writeHead(204);
199
+ res.end();
200
+ return;
201
+ }
202
+ // Token authentication
203
+ if (options.token) {
204
+ const authHeader = req.headers['authorization'];
205
+ if (authHeader !== `Bearer ${options.token}`) {
206
+ res.writeHead(401, { 'Content-Type': 'application/json' });
207
+ res.end(JSON.stringify({ error: 'Unauthorized' }));
208
+ return;
209
+ }
210
+ }
211
+ // MCP manifest
212
+ if (url.pathname === '/' || url.pathname === '/manifest') {
213
+ res.writeHead(200, { 'Content-Type': 'application/json' });
214
+ res.end(JSON.stringify(manifest, null, 2));
215
+ return;
216
+ }
217
+ // List docs
218
+ if (url.pathname === '/docs' || url.pathname === '/list') {
219
+ res.writeHead(200, { 'Content-Type': 'application/json' });
220
+ res.end(JSON.stringify({
221
+ docs: docs.map(d => ({
222
+ path: d.path,
223
+ title: d.title,
224
+ description: d.description,
225
+ })),
226
+ }));
227
+ return;
228
+ }
229
+ // Get specific doc
230
+ if (url.pathname.startsWith('/doc/')) {
231
+ const docPath = decodeURIComponent(url.pathname.slice(5));
232
+ const doc = docs.find(d => d.path === docPath || d.path === docPath + '.mdx' || d.path === docPath + '.md');
233
+ if (doc) {
234
+ res.writeHead(200, { 'Content-Type': 'application/json' });
235
+ res.end(JSON.stringify(doc));
236
+ return;
237
+ }
238
+ res.writeHead(404, { 'Content-Type': 'application/json' });
239
+ res.end(JSON.stringify({ error: 'Document not found' }));
240
+ return;
241
+ }
242
+ // Search docs
243
+ if (url.pathname === '/search') {
244
+ const query = url.searchParams.get('q')?.toLowerCase() || '';
245
+ const results = docs
246
+ .filter(d => d.title.toLowerCase().includes(query) ||
247
+ d.description.toLowerCase().includes(query) ||
248
+ d.content.toLowerCase().includes(query))
249
+ .map(d => ({
225
250
  path: d.path,
226
251
  title: d.title,
227
252
  description: d.description,
228
- })),
229
- }));
230
- return;
231
- }
232
- // Get specific doc
233
- if (url.pathname.startsWith('/doc/')) {
234
- const docPath = decodeURIComponent(url.pathname.slice(5));
235
- const doc = docs.find(d => d.path === docPath || d.path === docPath + '.mdx' || d.path === docPath + '.md');
236
- if (doc) {
253
+ score: ((d.title.toLowerCase().includes(query) ? 10 : 0) +
254
+ (d.description.toLowerCase().includes(query) ? 5 : 0) +
255
+ (d.content.toLowerCase().includes(query) ? 1 : 0)),
256
+ }))
257
+ .sort((a, b) => b.score - a.score)
258
+ .slice(0, 10);
237
259
  res.writeHead(200, { 'Content-Type': 'application/json' });
238
- res.end(JSON.stringify(doc));
260
+ res.end(JSON.stringify({ query, results }));
239
261
  return;
240
262
  }
263
+ // 404
241
264
  res.writeHead(404, { 'Content-Type': 'application/json' });
242
- res.end(JSON.stringify({ error: 'Document not found' }));
243
- return;
244
- }
245
- // Search docs
246
- if (url.pathname === '/search') {
247
- const query = url.searchParams.get('q')?.toLowerCase() || '';
248
- const results = docs
249
- .filter(d => d.title.toLowerCase().includes(query) ||
250
- d.description.toLowerCase().includes(query) ||
251
- d.content.toLowerCase().includes(query))
252
- .map(d => ({
253
- path: d.path,
254
- title: d.title,
255
- description: d.description,
256
- score: ((d.title.toLowerCase().includes(query) ? 10 : 0) +
257
- (d.description.toLowerCase().includes(query) ? 5 : 0) +
258
- (d.content.toLowerCase().includes(query) ? 1 : 0)),
259
- }))
260
- .sort((a, b) => b.score - a.score)
261
- .slice(0, 10);
262
- res.writeHead(200, { 'Content-Type': 'application/json' });
263
- res.end(JSON.stringify({ query, results }));
264
- return;
265
- }
266
- // 404
267
- res.writeHead(404, { 'Content-Type': 'application/json' });
268
- res.end(JSON.stringify({ error: 'Not found' }));
269
- });
270
- server.listen(port, host, () => {
271
- console.log(`MCP server running at http://${host}:${port}`);
272
- console.log('');
273
- console.log('Endpoints:');
274
- console.log(` GET / - MCP manifest`);
275
- console.log(` GET /docs - List all documents`);
276
- console.log(` GET /doc/:path - Get document content`);
277
- console.log(` GET /search?q= - Search documents`);
278
- console.log('');
279
- console.log('Press Ctrl+C to stop');
280
- });
265
+ res.end(JSON.stringify({ error: 'Not found' }));
266
+ });
267
+ server.listen(port, host, () => {
268
+ console.log(`MCP server running at http://${host}:${port}`);
269
+ console.log('');
270
+ console.log('Endpoints:');
271
+ console.log(` GET / - MCP manifest`);
272
+ console.log(` GET /docs - List all documents`);
273
+ console.log(` GET /doc/:path - Get document content`);
274
+ console.log(` GET /search?q= - Search documents`);
275
+ console.log('');
276
+ console.log('Press Ctrl+C to stop');
277
+ });
278
+ }
279
+ catch (err) {
280
+ const message = err instanceof Error ? err.message : String(err);
281
+ console.error(`Error: ${message}`);
282
+ process.exit(1);
283
+ }
281
284
  });
@@ -1,5 +1,5 @@
1
1
  import { Command } from 'commander';
2
- import { execSync } from 'child_process';
2
+ import { execSync, 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';
@@ -9,7 +9,14 @@ function getGitChanges(since, repoPath) {
9
9
  const changes = [];
10
10
  try {
11
11
  // Get list of changed files
12
- const diffFiles = execSync(`git diff --name-status ${since}`, { cwd: repoPath, encoding: 'utf-8' }).trim();
12
+ const diffResult = spawnSync('git', ['diff', '--name-status', since], {
13
+ cwd: repoPath, encoding: 'utf-8'
14
+ });
15
+ if (diffResult.status !== 0) {
16
+ console.error('Failed to get git diff:', diffResult.stderr);
17
+ return [];
18
+ }
19
+ const diffFiles = diffResult.stdout.toString().trim();
13
20
  if (!diffFiles)
14
21
  return [];
15
22
  for (const line of diffFiles.split('\n')) {
@@ -22,7 +29,10 @@ function getGitChanges(since, repoPath) {
22
29
  // Get the actual diff
23
30
  let diff = '';
24
31
  try {
25
- diff = execSync(`git diff ${since} -- "${file}"`, { cwd: repoPath, encoding: 'utf-8', maxBuffer: 1024 * 1024 }).slice(0, 5000); // Limit diff size
32
+ const fileDiffResult = spawnSync('git', ['diff', since, '--', file], {
33
+ cwd: repoPath, encoding: 'utf-8', maxBuffer: 1024 * 1024
34
+ });
35
+ diff = fileDiffResult.stdout.toString().slice(0, 5000); // Limit diff size
26
36
  }
27
37
  catch {
28
38
  // File might be binary or too large
@@ -178,114 +188,121 @@ export const monitorCommand = new Command('monitor')
178
188
  .option('--create-pr', 'Create a GitHub PR with changes')
179
189
  .option('--dry-run', 'Show what would be done without making changes')
180
190
  .action(async (repoPath, options) => {
181
- // Pro feature - requires subscription
182
- if (!await requirePro('monitor')) {
183
- process.exit(1);
184
- }
185
- const resolvedPath = resolve(repoPath);
186
- console.log('skrypt monitor');
187
- console.log(` repo: ${resolvedPath}`);
188
- console.log(` since: ${options.since}`);
189
- console.log('');
190
- // Load config
191
- const config = loadConfig();
192
- if (options.provider) {
193
- config.llm.provider = options.provider;
194
- }
195
- // Check API key
196
- const { ok, envKey } = checkApiKey(config.llm.provider);
197
- if (!ok && envKey) {
198
- console.error(`Error: ${envKey} required for ${config.llm.provider}`);
199
- process.exit(1);
200
- }
201
- // Step 1: Get git changes
202
- console.log('Step 1: Analyzing git changes...');
203
- const changes = getGitChanges(options.since || 'HEAD~10', resolvedPath);
204
- if (changes.length === 0) {
205
- console.log(' No code changes found.');
206
- process.exit(0);
207
- }
208
- const userFacing = changes.filter(c => c.isUserFacing);
209
- console.log(` Found ${changes.length} changed files (${userFacing.length} user-facing)`);
210
- if (userFacing.length === 0) {
211
- console.log(' No user-facing changes detected. No documentation updates needed.');
212
- process.exit(0);
213
- }
214
- // Step 2: Find existing docs
215
- console.log('\nStep 2: Scanning existing documentation...');
216
- const docsPath = join(resolvedPath, 'docs');
217
- const existingDocs = findExistingDocs(docsPath);
218
- console.log(` Found ${existingDocs.length} existing doc files`);
219
- // Step 3: Analyze with AI
220
- console.log('\nStep 3: Analyzing changes with AI...');
221
- const client = createLLMClient({
222
- provider: config.llm.provider,
223
- model: config.llm.model,
224
- });
225
- const suggestions = await analyzeChangesWithAI(changes, existingDocs, client);
226
- if (suggestions.length === 0) {
227
- console.log(' No documentation updates suggested.');
228
- process.exit(0);
229
- }
230
- console.log(`\n=== Documentation Suggestions (${suggestions.length}) ===\n`);
231
- for (const s of suggestions) {
232
- const icon = s.action === 'create' ? '➕' : s.action === 'update' ? '📝' : '🗑️';
233
- const priority = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢';
234
- console.log(`${icon} ${priority} ${s.file}`);
235
- console.log(` Action: ${s.action.toUpperCase()}`);
236
- console.log(` Reason: ${s.reason}`);
191
+ try {
192
+ // Pro feature - requires subscription
193
+ if (!await requirePro('monitor')) {
194
+ process.exit(1);
195
+ }
196
+ const resolvedPath = resolve(repoPath);
197
+ console.log('skrypt monitor');
198
+ console.log(` repo: ${resolvedPath}`);
199
+ console.log(` since: ${options.since}`);
237
200
  console.log('');
238
- }
239
- // Output to file if requested
240
- if (options.output) {
241
- const outputPath = resolve(options.output);
242
- writeFileSync(outputPath, JSON.stringify({ suggestions, changes: userFacing }, null, 2));
243
- console.log(`Suggestions saved to: ${outputPath}`);
244
- }
245
- // Auto-fix if requested
246
- if (options.autoFix && !options.dryRun) {
247
- console.log('\n=== Auto-generating documentation ===\n');
248
- for (const suggestion of suggestions.filter(s => s.action !== 'delete')) {
249
- const change = changes.find(c => suggestion.reason.includes(c.file));
250
- if (!change)
251
- continue;
252
- console.log(`Generating: ${suggestion.file}...`);
253
- const content = await generateDocContent(suggestion, change, client);
254
- if (content) {
255
- const docPath = join(docsPath, suggestion.file);
256
- mkdirSync(dirname(docPath), { recursive: true });
257
- writeFileSync(docPath, content);
258
- console.log(` ✓ Written: ${docPath}`);
259
- }
201
+ // Load config
202
+ const config = loadConfig();
203
+ if (options.provider) {
204
+ config.llm.provider = options.provider;
260
205
  }
261
- }
262
- // Create PR if requested
263
- if (options.createPr && !options.dryRun) {
264
- console.log('\n=== Creating GitHub PR ===\n');
265
- try {
266
- // Check if gh CLI is available
267
- execSync('gh --version', { stdio: 'ignore' });
268
- // Create branch
269
- const branchName = `docs/auto-update-${Date.now()}`;
270
- execSync(`git checkout -b ${branchName}`, { cwd: resolvedPath });
271
- // Stage and commit
272
- execSync('git add docs/', { cwd: resolvedPath });
273
- execSync(`git commit -m "docs: auto-update documentation for recent changes"`, { cwd: resolvedPath });
274
- // Push and create PR
275
- execSync(`git push -u origin ${branchName}`, { cwd: resolvedPath });
276
- 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();
277
- console.log(`PR created: ${prUrl}`);
206
+ // Check API key
207
+ const { ok, envKey } = checkApiKey(config.llm.provider);
208
+ if (!ok && envKey) {
209
+ console.error(`Error: ${envKey} required for ${config.llm.provider}`);
210
+ process.exit(1);
211
+ }
212
+ // Step 1: Get git changes
213
+ console.log('Step 1: Analyzing git changes...');
214
+ const changes = getGitChanges(options.since || 'HEAD~10', resolvedPath);
215
+ if (changes.length === 0) {
216
+ console.log(' No code changes found.');
217
+ process.exit(0);
218
+ }
219
+ const userFacing = changes.filter(c => c.isUserFacing);
220
+ console.log(` Found ${changes.length} changed files (${userFacing.length} user-facing)`);
221
+ if (userFacing.length === 0) {
222
+ console.log(' No user-facing changes detected. No documentation updates needed.');
223
+ process.exit(0);
224
+ }
225
+ // Step 2: Find existing docs
226
+ console.log('\nStep 2: Scanning existing documentation...');
227
+ const docsPath = join(resolvedPath, 'docs');
228
+ const existingDocs = findExistingDocs(docsPath);
229
+ console.log(` Found ${existingDocs.length} existing doc files`);
230
+ // Step 3: Analyze with AI
231
+ console.log('\nStep 3: Analyzing changes with AI...');
232
+ const client = createLLMClient({
233
+ provider: config.llm.provider,
234
+ model: config.llm.model,
235
+ });
236
+ const suggestions = await analyzeChangesWithAI(changes, existingDocs, client);
237
+ if (suggestions.length === 0) {
238
+ console.log(' No documentation updates suggested.');
239
+ process.exit(0);
240
+ }
241
+ console.log(`\n=== Documentation Suggestions (${suggestions.length}) ===\n`);
242
+ for (const s of suggestions) {
243
+ const icon = s.action === 'create' ? '➕' : s.action === 'update' ? '📝' : '🗑️';
244
+ const priority = s.priority === 'high' ? '🔴' : s.priority === 'medium' ? '🟡' : '🟢';
245
+ console.log(`${icon} ${priority} ${s.file}`);
246
+ console.log(` Action: ${s.action.toUpperCase()}`);
247
+ console.log(` Reason: ${s.reason}`);
248
+ console.log('');
249
+ }
250
+ // Output to file if requested
251
+ if (options.output) {
252
+ const outputPath = resolve(options.output);
253
+ writeFileSync(outputPath, JSON.stringify({ suggestions, changes: userFacing }, null, 2));
254
+ console.log(`Suggestions saved to: ${outputPath}`);
278
255
  }
279
- catch (err) {
280
- console.error('Failed to create PR:', err);
256
+ // Auto-fix if requested
257
+ if (options.autoFix && !options.dryRun) {
258
+ console.log('\n=== Auto-generating documentation ===\n');
259
+ for (const suggestion of suggestions.filter(s => s.action !== 'delete')) {
260
+ const change = changes.find(c => suggestion.reason.includes(c.file));
261
+ if (!change)
262
+ continue;
263
+ console.log(`Generating: ${suggestion.file}...`);
264
+ const content = await generateDocContent(suggestion, change, client);
265
+ if (content) {
266
+ const docPath = join(docsPath, suggestion.file);
267
+ mkdirSync(dirname(docPath), { recursive: true });
268
+ writeFileSync(docPath, content);
269
+ console.log(` ✓ Written: ${docPath}`);
270
+ }
271
+ }
272
+ }
273
+ // Create PR if requested
274
+ if (options.createPr && !options.dryRun) {
275
+ console.log('\n=== Creating GitHub PR ===\n');
276
+ try {
277
+ // Check if gh CLI is available
278
+ execSync('gh --version', { stdio: 'ignore' });
279
+ // Create branch
280
+ const branchName = `docs/auto-update-${Date.now()}`;
281
+ execSync(`git checkout -b ${branchName}`, { cwd: resolvedPath });
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 });
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();
288
+ console.log(`PR created: ${prUrl}`);
289
+ }
290
+ catch (err) {
291
+ console.error('Failed to create PR:', err);
292
+ }
281
293
  }
294
+ // Summary
295
+ console.log('\n=== Summary ===');
296
+ console.log(` Changes analyzed: ${changes.length}`);
297
+ console.log(` User-facing: ${userFacing.length}`);
298
+ console.log(` Suggestions: ${suggestions.length}`);
299
+ console.log(` Create: ${suggestions.filter(s => s.action === 'create').length}`);
300
+ console.log(` Update: ${suggestions.filter(s => s.action === 'update').length}`);
301
+ console.log(` Delete: ${suggestions.filter(s => s.action === 'delete').length}`);
302
+ }
303
+ catch (err) {
304
+ const message = err instanceof Error ? err.message : String(err);
305
+ console.error(`Error: ${message}`);
306
+ process.exit(1);
282
307
  }
283
- // Summary
284
- console.log('\n=== Summary ===');
285
- console.log(` Changes analyzed: ${changes.length}`);
286
- console.log(` User-facing: ${userFacing.length}`);
287
- console.log(` Suggestions: ${suggestions.length}`);
288
- console.log(` Create: ${suggestions.filter(s => s.action === 'create').length}`);
289
- console.log(` Update: ${suggestions.filter(s => s.action === 'update').length}`);
290
- console.log(` Delete: ${suggestions.filter(s => s.action === 'delete').length}`);
291
308
  });
@@ -8,7 +8,7 @@ export const reviewPRCommand = new Command('review-pr')
8
8
  .option('--token <token>', 'GitHub token (or use GITHUB_TOKEN env var)')
9
9
  .action(async (prUrl, options) => {
10
10
  // Parse PR URL
11
- const urlMatch = prUrl.match(/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
11
+ const urlMatch = prUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/);
12
12
  if (!urlMatch) {
13
13
  console.error('Error: Invalid PR URL format');
14
14
  console.error('Expected: https://github.com/owner/repo/pull/123');
@@ -115,7 +115,7 @@ async function parseOpenAPISpec(content) {
115
115
  // Try YAML - dynamic import to avoid require()
116
116
  try {
117
117
  const yaml = await import('js-yaml');
118
- return yaml.load(content);
118
+ return yaml.load(content, { schema: yaml.JSON_SCHEMA });
119
119
  }
120
120
  catch {
121
121
  throw new Error('Failed to parse spec. For YAML files, install js-yaml: npm i js-yaml');
@@ -30,8 +30,27 @@ export function loadConfig(configPath) {
30
30
  return DEFAULT_CONFIG;
31
31
  }
32
32
  function parseConfigFile(filepath) {
33
- const content = readFileSync(filepath, 'utf-8');
34
- const parsed = yaml.load(content);
33
+ let content;
34
+ try {
35
+ content = readFileSync(filepath, 'utf-8');
36
+ }
37
+ catch (err) {
38
+ throw new Error(`Could not read config file: ${filepath}. ` +
39
+ (err instanceof Error ? err.message : String(err)));
40
+ }
41
+ let parsed;
42
+ try {
43
+ parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA });
44
+ }
45
+ catch (err) {
46
+ throw new Error(`Config file has invalid YAML: ${filepath}. ` +
47
+ `Please check the syntax and try again. ` +
48
+ (err instanceof Error ? err.message : String(err)));
49
+ }
50
+ if (parsed === null || parsed === undefined || typeof parsed !== 'object') {
51
+ throw new Error(`Config file is empty or not a valid YAML object: ${filepath}. ` +
52
+ `Run 'skrypt init' to generate a valid config.`);
53
+ }
35
54
  // Merge with defaults
36
55
  return mergeConfig(DEFAULT_CONFIG, parsed);
37
56
  }
@@ -17,13 +17,16 @@ export declare function detectCrossReferences(docs: GeneratedDoc[]): CrossRefere
17
17
  export declare function getCrossRefsForElement(elementName: string, allRefs: CrossReference[]): CrossReference[];
18
18
  /**
19
19
  * Build navigation structure from topics
20
+ * @internal
20
21
  */
21
22
  export declare function buildNavigation(topics: Topic[]): NavigationItem[];
22
23
  /**
23
24
  * Generate a sidebar configuration (works with multiple doc platforms)
25
+ * @internal
24
26
  */
25
27
  export declare function generateSidebarConfig(topics: Topic[]): object;
26
28
  /**
27
29
  * Merge user config with defaults
30
+ * @internal
28
31
  */
29
32
  export declare function mergeTopicConfig(userConfig: Partial<TopicConfig>, defaults?: TopicConfig): TopicConfig;
@@ -1,3 +1,4 @@
1
+ import { slugify } from '../utils/files.js';
1
2
  /**
2
3
  * Default topic configuration with common patterns
3
4
  */
@@ -173,6 +174,7 @@ export function getCrossRefsForElement(elementName, allRefs) {
173
174
  }
174
175
  /**
175
176
  * Build navigation structure from topics
177
+ * @internal
176
178
  */
177
179
  export function buildNavigation(topics) {
178
180
  return topics.map(topic => ({
@@ -186,6 +188,7 @@ export function buildNavigation(topics) {
186
188
  }
187
189
  /**
188
190
  * Generate a sidebar configuration (works with multiple doc platforms)
191
+ * @internal
189
192
  */
190
193
  export function generateSidebarConfig(topics) {
191
194
  return {
@@ -203,17 +206,9 @@ function titleCase(str) {
203
206
  .replace(/-/g, ' ')
204
207
  .replace(/\b\w/g, c => c.toUpperCase());
205
208
  }
206
- /**
207
- * Convert string to URL-safe slug
208
- */
209
- function slugify(str) {
210
- return str
211
- .toLowerCase()
212
- .replace(/[^a-z0-9]+/g, '-')
213
- .replace(/^-|-$/g, '');
214
- }
215
209
  /**
216
210
  * Merge user config with defaults
211
+ * @internal
217
212
  */
218
213
  export function mergeTopicConfig(userConfig, defaults = DEFAULT_TOPIC_CONFIG) {
219
214
  return {