skrypt-ai 0.8.0 → 0.8.1

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 (101) hide show
  1. package/dist/auth/index.js +6 -0
  2. package/dist/binding/binder.d.ts +5 -0
  3. package/dist/binding/binder.js +63 -0
  4. package/dist/binding/detector.d.ts +5 -0
  5. package/dist/binding/detector.js +51 -0
  6. package/dist/binding/doc-parser.d.ts +9 -0
  7. package/dist/binding/doc-parser.js +138 -0
  8. package/dist/binding/extractor.d.ts +14 -0
  9. package/dist/binding/extractor.js +39 -0
  10. package/dist/binding/index.d.ts +5 -0
  11. package/dist/binding/index.js +5 -0
  12. package/dist/binding/types.d.ts +74 -0
  13. package/dist/binding/types.js +1 -0
  14. package/dist/claims/extractor.d.ts +13 -0
  15. package/dist/claims/extractor.js +138 -0
  16. package/dist/claims/index.d.ts +4 -0
  17. package/dist/claims/index.js +4 -0
  18. package/dist/claims/reporter.d.ts +9 -0
  19. package/dist/claims/reporter.js +65 -0
  20. package/dist/claims/store.d.ts +13 -0
  21. package/dist/claims/store.js +51 -0
  22. package/dist/claims/types.d.ts +34 -0
  23. package/dist/claims/types.js +1 -0
  24. package/dist/cli.js +516 -56
  25. package/dist/commands/bind.d.ts +2 -0
  26. package/dist/commands/bind.js +139 -0
  27. package/dist/commands/claims.d.ts +2 -0
  28. package/dist/commands/claims.js +84 -0
  29. package/dist/commands/coverage.d.ts +2 -0
  30. package/dist/commands/coverage.js +61 -0
  31. package/dist/commands/generate/index.js +5 -0
  32. package/dist/commands/generate/scan.js +33 -14
  33. package/dist/commands/generate/write.d.ts +7 -0
  34. package/dist/commands/generate/write.js +65 -1
  35. package/dist/commands/import.js +12 -3
  36. package/dist/commands/init.js +68 -5
  37. package/dist/commands/monitor.d.ts +15 -0
  38. package/dist/commands/monitor.js +2 -2
  39. package/dist/commands/mutate.d.ts +2 -0
  40. package/dist/commands/mutate.js +177 -0
  41. package/dist/config/types.js +2 -2
  42. package/dist/coverage/calculator.d.ts +7 -0
  43. package/dist/coverage/calculator.js +86 -0
  44. package/dist/coverage/index.d.ts +3 -0
  45. package/dist/coverage/index.js +3 -0
  46. package/dist/coverage/reporter.d.ts +9 -0
  47. package/dist/coverage/reporter.js +65 -0
  48. package/dist/coverage/types.d.ts +36 -0
  49. package/dist/coverage/types.js +1 -0
  50. package/dist/generator/generator.d.ts +3 -1
  51. package/dist/generator/generator.js +137 -23
  52. package/dist/generator/mdx-serializer.js +3 -2
  53. package/dist/generator/organizer.d.ts +5 -1
  54. package/dist/generator/organizer.js +29 -14
  55. package/dist/generator/types.d.ts +6 -0
  56. package/dist/generator/writer.js +7 -2
  57. package/dist/github/org-discovery.js +5 -0
  58. package/dist/importers/mintlify.js +4 -3
  59. package/dist/llm/anthropic-client.js +1 -0
  60. package/dist/llm/index.d.ts +15 -0
  61. package/dist/llm/index.js +148 -29
  62. package/dist/llm/openai-client.js +2 -0
  63. package/dist/mutation/index.d.ts +4 -0
  64. package/dist/mutation/index.js +4 -0
  65. package/dist/mutation/mutator.d.ts +5 -0
  66. package/dist/mutation/mutator.js +101 -0
  67. package/dist/mutation/reporter.d.ts +14 -0
  68. package/dist/mutation/reporter.js +66 -0
  69. package/dist/mutation/runner.d.ts +9 -0
  70. package/dist/mutation/runner.js +70 -0
  71. package/dist/mutation/types.d.ts +51 -0
  72. package/dist/mutation/types.js +1 -0
  73. package/dist/qa/checks.d.ts +1 -0
  74. package/dist/qa/checks.js +47 -0
  75. package/dist/qa/index.js +2 -1
  76. package/dist/scanner/index.js +78 -11
  77. package/dist/scanner/typescript.js +42 -31
  78. package/dist/sentry.d.ts +3 -0
  79. package/dist/sentry.js +28 -0
  80. package/dist/template/docs.json +6 -3
  81. package/dist/template/next.config.mjs +15 -1
  82. package/dist/template/package.json +4 -3
  83. package/dist/template/public/docs-schema.json +257 -0
  84. package/dist/template/sentry.client.config.ts +12 -0
  85. package/dist/template/sentry.edge.config.ts +7 -0
  86. package/dist/template/sentry.server.config.ts +7 -0
  87. package/dist/template/src/app/docs/[...slug]/page.tsx +11 -5
  88. package/dist/template/src/app/docs/layout.tsx +2 -4
  89. package/dist/template/src/app/global-error.tsx +60 -0
  90. package/dist/template/src/app/layout.tsx +7 -16
  91. package/dist/template/src/app/page.tsx +2 -5
  92. package/dist/template/src/components/ai-chat-impl.tsx +1 -1
  93. package/dist/template/src/components/docs-layout.tsx +1 -15
  94. package/dist/template/src/components/footer.tsx +95 -19
  95. package/dist/template/src/components/header.tsx +1 -1
  96. package/dist/template/src/components/search-dialog.tsx +5 -0
  97. package/dist/template/src/instrumentation.ts +11 -0
  98. package/dist/template/src/lib/docs-config.ts +235 -0
  99. package/dist/template/src/lib/fonts.ts +3 -3
  100. package/dist/testing/runner.js +8 -1
  101. package/package.json +2 -1
@@ -0,0 +1,139 @@
1
+ import { Command } from 'commander';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
3
+ import { resolve, join } from 'path';
4
+ import { extractSymbols } from '../binding/extractor.js';
5
+ import { parseDocSections } from '../binding/doc-parser.js';
6
+ import { createBindings } from '../binding/binder.js';
7
+ import { detectStale } from '../binding/detector.js';
8
+ const BINDINGS_PATH = '.skrypt/bindings.json';
9
+ export const bindCommand = new Command('bind')
10
+ .description('Bind documentation sections to source code symbols')
11
+ .argument('[source]', 'Source code directory', '.')
12
+ .option('--docs <dir>', 'Documentation directory', './docs')
13
+ .option('--check', 'Check current bindings for stale docs')
14
+ .option('--diff', 'Show changes since last bind')
15
+ .option('--json', 'Output as JSON')
16
+ .action(async (source, options) => {
17
+ try {
18
+ const sourcePath = resolve(source);
19
+ const docsPath = resolve(options.docs || './docs');
20
+ if (!existsSync(sourcePath)) {
21
+ console.error(`Error: Source directory not found: ${sourcePath}`);
22
+ process.exit(1);
23
+ }
24
+ if (!existsSync(docsPath)) {
25
+ console.error(`Error: Docs directory not found: ${docsPath}`);
26
+ process.exit(1);
27
+ }
28
+ console.log('skrypt bind');
29
+ console.log(` source: ${sourcePath}`);
30
+ console.log(` docs: ${docsPath}`);
31
+ console.log('');
32
+ // Step 1: Extract code symbols
33
+ console.log('Extracting code symbols...');
34
+ const symbols = await extractSymbols(sourcePath);
35
+ const exported = symbols.filter(s => s.isExported);
36
+ console.log(` Found ${symbols.length} symbols (${exported.length} exported)`);
37
+ // Step 2: Parse doc sections
38
+ console.log('Parsing documentation sections...');
39
+ const sections = parseDocSections(docsPath);
40
+ console.log(` Found ${sections.length} sections across ${new Set(sections.map(s => s.filePath)).size} files`);
41
+ // Step 3: Create bindings
42
+ console.log('\nCreating bindings...');
43
+ const bindings = createBindings(symbols, sections);
44
+ console.log(` Created ${bindings.length} bindings`);
45
+ const explicit = bindings.filter(b => b.bindingType === 'explicit').length;
46
+ const reference = bindings.filter(b => b.bindingType === 'reference').length;
47
+ console.log(` Explicit (code block): ${explicit}`);
48
+ console.log(` Reference (prose): ${reference}`);
49
+ // Handle --check mode
50
+ if (options.check) {
51
+ const existing = readBindingsFile(sourcePath);
52
+ if (!existing) {
53
+ console.error('\nNo existing bindings found. Run `skrypt bind` first.');
54
+ process.exit(1);
55
+ }
56
+ const staleDocs = detectStale(existing.symbols, symbols, existing.bindings);
57
+ if (staleDocs.length === 0) {
58
+ console.log('\n\x1b[32mAll documentation is up to date.\x1b[0m');
59
+ return;
60
+ }
61
+ console.log(`\n\x1b[33m${staleDocs.length} stale doc section(s) found:\x1b[0m\n`);
62
+ for (const stale of staleDocs) {
63
+ const severityColor = stale.severity === 'high' ? '\x1b[31m' : stale.severity === 'medium' ? '\x1b[33m' : '\x1b[2m';
64
+ console.log(` ${severityColor}[${stale.severity.toUpperCase()}]\x1b[0m ${stale.docPath}`);
65
+ console.log(` Section: ${stale.section}`);
66
+ console.log(` Reason: ${stale.reason}`);
67
+ console.log('');
68
+ }
69
+ if (options.json) {
70
+ console.log(JSON.stringify({ staleDocs }, null, 2));
71
+ }
72
+ const highCount = staleDocs.filter(s => s.severity === 'high').length;
73
+ if (highCount > 0)
74
+ process.exit(1);
75
+ return;
76
+ }
77
+ // Handle --diff mode
78
+ if (options.diff) {
79
+ const existing = readBindingsFile(sourcePath);
80
+ if (!existing) {
81
+ console.log('\nNo previous bindings to diff against.');
82
+ }
83
+ else {
84
+ const staleDocs = detectStale(existing.symbols, symbols, existing.bindings);
85
+ const newBindings = bindings.length - existing.bindings.length;
86
+ console.log(`\n Previous bindings: ${existing.bindings.length}`);
87
+ console.log(` Current bindings: ${bindings.length}`);
88
+ console.log(` Net change: ${newBindings >= 0 ? '+' : ''}${newBindings}`);
89
+ console.log(` Stale sections: ${staleDocs.length}`);
90
+ }
91
+ return;
92
+ }
93
+ // Default: save bindings
94
+ const bindingsFile = {
95
+ version: 1,
96
+ generatedAt: new Date().toISOString(),
97
+ bindings,
98
+ symbols,
99
+ sections,
100
+ };
101
+ const outputPath = join(sourcePath, BINDINGS_PATH);
102
+ mkdirSync(join(sourcePath, '.skrypt'), { recursive: true });
103
+ writeFileSync(outputPath, JSON.stringify(bindingsFile, null, 2));
104
+ if (options.json) {
105
+ console.log(JSON.stringify({ bindings: bindings.length, symbols: symbols.length, sections: sections.length }, null, 2));
106
+ }
107
+ console.log(`\nBindings saved to ${outputPath}`);
108
+ console.log(`\n=== Summary ===`);
109
+ console.log(` Symbols: ${symbols.length}`);
110
+ console.log(` Sections: ${sections.length}`);
111
+ console.log(` Bindings: ${bindings.length}`);
112
+ const boundSymbols = new Set(bindings.map(b => b.codeSymbol.name));
113
+ const unboundExported = exported.filter(s => !boundSymbols.has(s.name));
114
+ if (unboundExported.length > 0) {
115
+ console.log(`\n \x1b[33mUnbound exported symbols: ${unboundExported.length}\x1b[0m`);
116
+ for (const s of unboundExported.slice(0, 10)) {
117
+ console.log(` ${s.kind.padEnd(10)} ${s.name}`);
118
+ }
119
+ if (unboundExported.length > 10) {
120
+ console.log(` ... and ${unboundExported.length - 10} more`);
121
+ }
122
+ }
123
+ }
124
+ catch (err) {
125
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
126
+ process.exit(1);
127
+ }
128
+ });
129
+ function readBindingsFile(projectDir) {
130
+ const filePath = join(projectDir, BINDINGS_PATH);
131
+ if (!existsSync(filePath))
132
+ return null;
133
+ try {
134
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const claimsCommand: Command;
@@ -0,0 +1,84 @@
1
+ import { Command } from 'commander';
2
+ import { existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { requirePro } from '../auth/index.js';
5
+ import { loadConfig, checkApiKey } from '../config/index.js';
6
+ import { createLLMClient } from '../llm/index.js';
7
+ import { extractClaimsFromDirectory } from '../claims/extractor.js';
8
+ import { readClaims, writeClaims, mergeClaims } from '../claims/store.js';
9
+ import { printClaimsReport, formatClaimsJson } from '../claims/reporter.js';
10
+ export const claimsCommand = new Command('claims')
11
+ .description('Extract testable claims from documentation')
12
+ .argument('[docs]', 'Documentation directory', './docs')
13
+ .option('--file <path>', 'Target a specific doc file')
14
+ .option('--type <type>', 'Filter by claim type (behavioral, constraint, integration, performance, error_handling)')
15
+ .option('--json', 'Output as JSON')
16
+ .option('--verify', 'Verify extracted claims (coming soon)')
17
+ .option('--provider <name>', 'LLM provider')
18
+ .option('--model <name>', 'LLM model')
19
+ .option('-c, --config <file>', 'Config file path')
20
+ .action(async (docs, options) => {
21
+ try {
22
+ if (!await requirePro('claims')) {
23
+ process.exit(1);
24
+ }
25
+ const docsPath = resolve(docs);
26
+ if (!existsSync(docsPath)) {
27
+ console.error(`Error: Docs directory not found: ${docsPath}`);
28
+ process.exit(1);
29
+ }
30
+ if (options.verify) {
31
+ console.log('\nClaim verification is coming in a future release.');
32
+ console.log('For now, claims are extracted and stored for review.\n');
33
+ }
34
+ console.log('skrypt claims');
35
+ console.log(` docs: ${docsPath}`);
36
+ if (options.file)
37
+ console.log(` file: ${options.file}`);
38
+ if (options.type)
39
+ console.log(` type: ${options.type}`);
40
+ console.log('');
41
+ // Set up LLM client
42
+ const config = loadConfig(options.config);
43
+ if (options.provider)
44
+ config.llm.provider = options.provider;
45
+ if (options.model)
46
+ config.llm.model = options.model;
47
+ const { ok, envKey } = checkApiKey(config.llm.provider);
48
+ if (!ok && envKey) {
49
+ console.error(`Error: ${envKey} required for ${config.llm.provider}`);
50
+ process.exit(1);
51
+ }
52
+ const client = createLLMClient({
53
+ provider: config.llm.provider,
54
+ model: config.llm.model,
55
+ });
56
+ // Extract claims
57
+ console.log('Extracting testable claims...');
58
+ const claims = await extractClaimsFromDirectory(docsPath, client, {
59
+ file: options.file ? resolve(options.file) : undefined,
60
+ type: options.type,
61
+ });
62
+ if (claims.length === 0) {
63
+ console.log('\nNo testable claims found.');
64
+ return;
65
+ }
66
+ // Merge with existing claims
67
+ const projectDir = resolve('.');
68
+ const existing = readClaims(projectDir);
69
+ const merged = existing ? mergeClaims(existing.claims, claims) : claims;
70
+ writeClaims(projectDir, merged);
71
+ // Output
72
+ if (options.json) {
73
+ console.log(formatClaimsJson(claims));
74
+ }
75
+ else {
76
+ printClaimsReport(claims);
77
+ console.log(` Claims saved to .skrypt/claims.json`);
78
+ }
79
+ }
80
+ catch (err) {
81
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
82
+ process.exit(1);
83
+ }
84
+ });
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const coverageCommand: Command;
@@ -0,0 +1,61 @@
1
+ import { Command } from 'commander';
2
+ import { existsSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { scanDirectory } from '../scanner/index.js';
5
+ import { parseDocumentedElements } from '../audit/doc-parser.js';
6
+ import { calculateCoverage } from '../coverage/calculator.js';
7
+ import { printCoverageReport, formatCoverageJson } from '../coverage/reporter.js';
8
+ export const coverageCommand = new Command('coverage')
9
+ .description('Measure documentation coverage across your codebase')
10
+ .argument('[source]', 'Source code directory', '.')
11
+ .option('--docs <dir>', 'Documentation directory', './docs')
12
+ .option('--include-internal', 'Include non-exported symbols')
13
+ .option('--min-coverage <percent>', 'Fail if coverage is below this threshold')
14
+ .option('--json', 'Output as JSON')
15
+ .action(async (source, options) => {
16
+ try {
17
+ const sourcePath = resolve(source);
18
+ const docsPath = resolve(options.docs || './docs');
19
+ if (!existsSync(sourcePath)) {
20
+ console.error(`Error: Source directory not found: ${sourcePath}`);
21
+ process.exit(1);
22
+ }
23
+ if (!existsSync(docsPath)) {
24
+ console.error(`Error: Docs directory not found: ${docsPath}`);
25
+ process.exit(1);
26
+ }
27
+ // Step 1: Scan source code
28
+ console.log('Scanning source code...');
29
+ const scanResult = await scanDirectory(sourcePath, {
30
+ onProgress: (current, total, file) => {
31
+ process.stdout.write(`\r [${current}/${total}] ${file.slice(-50).padStart(50)}`);
32
+ }
33
+ });
34
+ console.log('');
35
+ const codeElements = scanResult.files.flatMap(f => f.elements);
36
+ // Step 2: Parse documented elements
37
+ console.log('Parsing documentation...');
38
+ const docElements = parseDocumentedElements(docsPath);
39
+ // Step 3: Calculate coverage
40
+ const report = calculateCoverage(codeElements, docElements, options.includeInternal ?? false);
41
+ // Step 4: Output
42
+ if (options.json) {
43
+ console.log(formatCoverageJson(report));
44
+ }
45
+ else {
46
+ printCoverageReport(report);
47
+ }
48
+ // Step 5: Threshold check
49
+ if (options.minCoverage) {
50
+ const threshold = parseInt(options.minCoverage, 10);
51
+ if (!isNaN(threshold) && report.coveragePercent < threshold) {
52
+ console.error(`\nCoverage ${report.coveragePercent}% is below threshold ${threshold}%`);
53
+ process.exit(1);
54
+ }
55
+ }
56
+ }
57
+ catch (err) {
58
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
59
+ process.exit(1);
60
+ }
61
+ });
@@ -169,6 +169,11 @@ export const generateCommand = new Command('generate')
169
169
  else if (config.llm.provider !== 'ollama') {
170
170
  console.log(' routing: via Skrypt API proxy');
171
171
  }
172
+ // Quality recommendation for non-optimal models
173
+ const RECOMMENDED_MODELS = ['claude-sonnet-4-6', 'claude-sonnet-4-20250514', 'claude-opus-4-6'];
174
+ if (!RECOMMENDED_MODELS.some(m => config.llm.model.includes(m))) {
175
+ console.log(' tip: For best results, use --provider anthropic (Claude Sonnet 4 recommended)');
176
+ }
172
177
  console.log('');
173
178
  // Scan sources
174
179
  const scanResult = await scanSources(sources, {
@@ -67,6 +67,36 @@ export function findOpenAPISpec(sourcePath) {
67
67
  }
68
68
  return null;
69
69
  }
70
+ /** Cache compiled RegExp for name-based exclude patterns (avoids recompiling per element). */
71
+ const nameRegexCache = new Map();
72
+ /** ReDoS guard pattern — reject patterns with nested quantifiers. */
73
+ const REDOS_GUARD = /(\+|\*|\?)\{|\(\?[^:)]|\(\.[*+].*\)\+|\([^)]*[+*][^)]*\)[+*]/;
74
+ /** Metacharacter test — only compile regex if the pattern contains these. */
75
+ const HAS_META = /[*+?{}()|[\]\\^$.]/;
76
+ function getCachedNameRegex(namePattern) {
77
+ if (nameRegexCache.has(namePattern)) {
78
+ return nameRegexCache.get(namePattern);
79
+ }
80
+ // Only compile if pattern contains regex metacharacters
81
+ if (!HAS_META.test(namePattern)) {
82
+ nameRegexCache.set(namePattern, null);
83
+ return null;
84
+ }
85
+ // Reject patterns prone to catastrophic backtracking
86
+ if (REDOS_GUARD.test(namePattern)) {
87
+ nameRegexCache.set(namePattern, null);
88
+ return null;
89
+ }
90
+ try {
91
+ const regex = new RegExp(namePattern);
92
+ nameRegexCache.set(namePattern, regex);
93
+ return regex;
94
+ }
95
+ catch {
96
+ nameRegexCache.set(namePattern, null);
97
+ return null;
98
+ }
99
+ }
70
100
  /**
71
101
  * Check if element should be excluded based on patterns
72
102
  */
@@ -78,20 +108,9 @@ export function shouldExcludeElement(element, patterns) {
78
108
  if (element.name === namePattern) {
79
109
  return true;
80
110
  }
81
- // Only use regex if the pattern contains regex metacharacters
82
- // Reject patterns with nested quantifiers to prevent catastrophic backtracking (ReDoS)
83
- if (/[*+?{}()|[\]\\^$.]/.test(namePattern)) {
84
- if (/(\+|\*|\?)\{|\(\?[^:)]|\(\.[*+].*\)\+|\([^)]*[+*][^)]*\)[+*]/.test(namePattern)) {
85
- continue; // Skip patterns prone to catastrophic backtracking
86
- }
87
- try {
88
- if (new RegExp(namePattern).test(element.name))
89
- return true;
90
- }
91
- catch {
92
- // Invalid regex — treat as literal match (already checked above)
93
- }
94
- }
111
+ const regex = getCachedNameRegex(namePattern);
112
+ if (regex && regex.test(element.name))
113
+ return true;
95
114
  }
96
115
  // Match by file path
97
116
  else if (pattern.includes('/') || pattern.includes('*')) {
@@ -1,3 +1,4 @@
1
+ import { type Topic } from '../../generator/index.js';
1
2
  import { APIElement } from '../../scanner/index.js';
2
3
  type GeneratedDoc = Awaited<ReturnType<typeof import('../../generator/index.js').generateForElements>>[number];
3
4
  interface WriteDocsOptions {
@@ -22,4 +23,10 @@ interface WriteAssetsOptions {
22
23
  * Write post-generation assets: OpenAPI spec copy, llms.txt, AGENTS.md, manifest.
23
24
  */
24
25
  export declare function writeAssets(docs: GeneratedDoc[], allElements: APIElement[], outputPath: string, primarySourcePath: string, configOutputPath: string, filesWritten: number, options: WriteAssetsOptions): Promise<void>;
26
+ /**
27
+ * Update docs.json navigation after topic-based generation.
28
+ * Preserves all non-"API Reference" groups and replaces the
29
+ * "API Reference" group with pages derived from the generated topics.
30
+ */
31
+ export declare function updateDocsJsonNavigation(topics: Topic[], outputDir: string): void;
25
32
  export {};
@@ -1,4 +1,4 @@
1
- import { existsSync, copyFileSync, mkdirSync, writeFileSync } from 'fs';
1
+ import { existsSync, copyFileSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
2
2
  import { resolve, basename, dirname, join, relative } from 'path';
3
3
  import { groupDocsByFile, writeDocsToDirectory, writeDocsByTopic, writeLlmsTxt, generateAgentsMd } from '../../generator/index.js';
4
4
  import { writeManifest, buildManifestEntries } from '../../refresh/manifest.js';
@@ -15,6 +15,8 @@ export async function writeDocs(docs, outputPath, primarySourcePath, isMultiSour
15
15
  filesWritten = result.filesWritten;
16
16
  totalDocs = result.totalDocs;
17
17
  console.log(` topics: ${result.topics.map(t => t.name).join(', ')}`);
18
+ // Auto-update docs.json navigation with generated topics
19
+ updateDocsJsonNavigation(result.topics, outputPath);
18
20
  }
19
21
  else if (isMultiSource) {
20
22
  // Multi-source: write docs namespaced by source label
@@ -118,3 +120,65 @@ export async function writeAssets(docs, allElements, outputPath, primarySourcePa
118
120
  // Non-fatal — manifest is optional
119
121
  }
120
122
  }
123
+ /**
124
+ * Find docs.json by walking up from the output directory (max 3 levels).
125
+ * Returns the path if found, null otherwise.
126
+ */
127
+ function findDocsJson(outputDir) {
128
+ let dir = resolve(outputDir);
129
+ for (let i = 0; i < 4; i++) {
130
+ const candidate = join(dir, 'docs.json');
131
+ if (existsSync(candidate))
132
+ return candidate;
133
+ const parent = dirname(dir);
134
+ if (parent === dir)
135
+ break; // filesystem root
136
+ dir = parent;
137
+ }
138
+ return null;
139
+ }
140
+ /**
141
+ * Update docs.json navigation after topic-based generation.
142
+ * Preserves all non-"API Reference" groups and replaces the
143
+ * "API Reference" group with pages derived from the generated topics.
144
+ */
145
+ export function updateDocsJsonNavigation(topics, outputDir) {
146
+ const docsJsonPath = findDocsJson(outputDir);
147
+ if (!docsJsonPath)
148
+ return;
149
+ let docsJson;
150
+ try {
151
+ docsJson = JSON.parse(readFileSync(docsJsonPath, 'utf-8'));
152
+ }
153
+ catch {
154
+ return; // corrupted or unreadable — skip silently
155
+ }
156
+ if (!docsJson || typeof docsJson !== 'object')
157
+ return;
158
+ const navigation = Array.isArray(docsJson.navigation) ? docsJson.navigation : [];
159
+ // Build updated "API Reference" pages sorted by topic order
160
+ const apiRefPages = topics
161
+ .slice()
162
+ .sort((a, b) => a.order - b.order)
163
+ .map(t => ({ title: t.name, path: `/docs/${t.id}` }));
164
+ // Preserve all groups except "API Reference", then insert updated one
165
+ const otherGroups = navigation.filter((g) => g.group !== 'API Reference');
166
+ // Find the original API Reference group to preserve its icon
167
+ const existingApiRef = navigation.find((g) => g.group === 'API Reference');
168
+ const apiRefGroup = {
169
+ group: 'API Reference',
170
+ icon: existingApiRef?.icon ?? 'Code',
171
+ pages: apiRefPages,
172
+ };
173
+ // Re-insert API Reference at its original position (or at the end)
174
+ const originalIndex = navigation.findIndex((g) => g.group === 'API Reference');
175
+ const updated = [...otherGroups];
176
+ if (originalIndex >= 0 && originalIndex < updated.length) {
177
+ updated.splice(originalIndex, 0, apiRefGroup);
178
+ }
179
+ else {
180
+ updated.push(apiRefGroup);
181
+ }
182
+ docsJson.navigation = updated;
183
+ writeFileSync(docsJsonPath, JSON.stringify(docsJson, null, 2) + '\n', 'utf-8');
184
+ }
@@ -1,6 +1,6 @@
1
1
  import { Command } from 'commander';
2
2
  import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync } from 'fs';
3
- import { resolve, join, dirname, basename } from 'path';
3
+ import { resolve, join, dirname, basename, sep } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  import { detectFormat, isGitHubUrl, parseGitHubUrl, runImport, importFromGitHub } from '../importers/index.js';
6
6
  const __filename = fileURLToPath(import.meta.url);
@@ -102,14 +102,23 @@ function scaffoldOutput(outputDir, result) {
102
102
  mkdirSync(outputDir, { recursive: true });
103
103
  }
104
104
  // Write transformed content files
105
+ const resolvedOutputDir = resolve(outputDir) + sep;
105
106
  for (const [filePath, content] of result.files) {
106
- const outputPath = join(outputDir, filePath);
107
+ const outputPath = resolve(join(outputDir, filePath));
108
+ if (!outputPath.startsWith(resolvedOutputDir)) {
109
+ console.warn(` Skipping file outside output directory: ${filePath}`);
110
+ continue;
111
+ }
107
112
  mkdirSync(dirname(outputPath), { recursive: true });
108
113
  writeFileSync(outputPath, content);
109
114
  }
110
115
  // Copy assets
111
116
  for (const [destPath, srcPath] of result.assets) {
112
- const outputPath = join(outputDir, destPath);
117
+ const outputPath = resolve(join(outputDir, destPath));
118
+ if (!outputPath.startsWith(resolvedOutputDir)) {
119
+ console.warn(` Skipping asset outside output directory: ${destPath}`);
120
+ continue;
121
+ }
113
122
  mkdirSync(dirname(outputPath), { recursive: true });
114
123
  try {
115
124
  cpSync(srcPath, outputPath);
@@ -1,20 +1,62 @@
1
1
  import { Command } from 'commander';
2
- import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, readdirSync } from 'fs';
3
- import { join, dirname, resolve } from 'path';
2
+ import { existsSync, mkdirSync, cpSync, copyFileSync, readFileSync, writeFileSync, readdirSync } from 'fs';
3
+ import { join, dirname, resolve, extname } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
5
  const __filename = fileURLToPath(import.meta.url);
6
6
  const __dirname = dirname(__filename);
7
+ /** Returns true if the value looks like a URL (http/https/data URI). */
8
+ function isUrl(value) {
9
+ return /^https?:\/\/|^data:/.test(value);
10
+ }
11
+ /** Validate a hex color string (#RGB or #RRGGBB). */
12
+ function isValidHex(color) {
13
+ return /^#[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?$/.test(color);
14
+ }
15
+ /**
16
+ * Copy a local asset into the template's public/ directory and return the
17
+ * public-relative path (e.g. `/logo.png`). If the source is a URL, return
18
+ * it as-is.
19
+ */
20
+ function resolveAsset(value, targetDir, defaultName) {
21
+ if (isUrl(value))
22
+ return value;
23
+ const src = resolve(value);
24
+ if (!existsSync(src)) {
25
+ console.error(`Error: File not found: ${src}`);
26
+ process.exit(1);
27
+ }
28
+ const ext = extname(src);
29
+ const destName = `${defaultName}${ext}`;
30
+ const publicDir = join(targetDir, 'public');
31
+ mkdirSync(publicDir, { recursive: true });
32
+ copyFileSync(src, join(publicDir, destName));
33
+ return `/${destName}`;
34
+ }
7
35
  export const initCommand = new Command('init')
8
36
  .description('Initialize a new documentation site')
9
37
  .argument('[directory]', 'Target directory', '.')
10
38
  .option('--name <name>', 'Project name', 'my-docs')
39
+ .option('--color <hex>', 'Primary brand color (hex, e.g. "#FF5733")')
40
+ .option('--logo <url-or-path>', 'Logo URL or local file path')
41
+ .option('--favicon <url-or-path>', 'Favicon URL or local file path')
11
42
  .action(async (directory, options) => {
12
43
  try {
13
44
  const targetDir = resolve(directory);
14
45
  console.log('skrypt init');
15
46
  console.log(` directory: ${targetDir}`);
16
47
  console.log(` name: ${options.name}`);
48
+ if (options.color)
49
+ console.log(` color: ${options.color}`);
50
+ if (options.logo)
51
+ console.log(` logo: ${options.logo}`);
52
+ if (options.favicon)
53
+ console.log(` favicon: ${options.favicon}`);
17
54
  console.log('');
55
+ // Validate color if provided
56
+ if (options.color && !isValidHex(options.color)) {
57
+ console.error(`Error: Invalid hex color "${options.color}". Use format #RGB or #RRGGBB.`);
58
+ process.exit(1);
59
+ }
18
60
  // Check if directory exists and is not empty
19
61
  if (existsSync(targetDir)) {
20
62
  const files = readdirSync(targetDir);
@@ -44,15 +86,32 @@ export const initCommand = new Command('init')
44
86
  console.error('Error: Template package.json is missing or corrupted. Please reinstall skrypt.');
45
87
  process.exit(1);
46
88
  }
47
- // Update docs.json with project name
89
+ // Update docs.json with project name and branding
48
90
  const docsJsonPath = join(targetDir, 'docs.json');
49
91
  try {
50
92
  const docsJson = JSON.parse(readFileSync(docsJsonPath, 'utf-8'));
51
93
  docsJson.name = options.name;
52
94
  docsJson.description = `${options.name} documentation`;
95
+ // Branding: primary color
96
+ if (options.color) {
97
+ if (!docsJson.theme)
98
+ docsJson.theme = {};
99
+ docsJson.theme.primaryColor = options.color;
100
+ }
101
+ // Branding: logo
102
+ if (options.logo) {
103
+ docsJson.logo = resolveAsset(options.logo, targetDir, 'logo');
104
+ }
105
+ // Branding: favicon
106
+ if (options.favicon) {
107
+ docsJson.favicon = resolveAsset(options.favicon, targetDir, 'favicon');
108
+ }
53
109
  writeFileSync(docsJsonPath, JSON.stringify(docsJson, null, 2));
54
110
  }
55
- catch {
111
+ catch (e) {
112
+ // Re-throw asset resolution errors (already printed)
113
+ if (e instanceof Error && e.message.includes('ENOENT'))
114
+ throw e;
56
115
  console.error('Error: Template docs.json is missing or corrupted. Please reinstall skrypt.');
57
116
  process.exit(1);
58
117
  }
@@ -63,7 +122,11 @@ export const initCommand = new Command('init')
63
122
  console.log(' npm install');
64
123
  console.log(' npm run dev');
65
124
  console.log('');
66
- console.log('Then open http://localhost:3000 to see your docs.');
125
+ console.log('Customize your docs in docs.json:');
126
+ console.log(' - theme.primaryColor: Your brand color (hex)');
127
+ console.log(' - logo: Path to your logo image');
128
+ console.log(' - favicon: Path to your favicon');
129
+ console.log(' - fonts: { sans, mono } font families');
67
130
  console.log('');
68
131
  console.log('To generate API documentation:');
69
132
  console.log(' skrypt generate ./src -o ./content/docs');
@@ -1,2 +1,17 @@
1
1
  import { Command } from 'commander';
2
+ export interface CodeChange {
3
+ file: string;
4
+ status: 'added' | 'modified' | 'deleted';
5
+ diff: string;
6
+ isUserFacing: boolean;
7
+ }
8
+ export interface DocSuggestion {
9
+ file: string;
10
+ action: 'create' | 'update' | 'delete';
11
+ reason: string;
12
+ suggestedContent?: string;
13
+ priority: 'high' | 'medium' | 'low';
14
+ }
15
+ export declare function isCodeFile(file: string): boolean;
16
+ export declare function isUserFacingChange(file: string, diff: string): boolean;
2
17
  export declare const monitorCommand: Command;
@@ -50,11 +50,11 @@ function getGitChanges(since, repoPath) {
50
50
  }
51
51
  return changes;
52
52
  }
53
- function isCodeFile(file) {
53
+ export function isCodeFile(file) {
54
54
  const codeExtensions = ['.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs', '.rb', '.java', '.swift', '.kt'];
55
55
  return codeExtensions.some(ext => file.endsWith(ext));
56
56
  }
57
- function isUserFacingChange(file, diff) {
57
+ export function isUserFacingChange(file, diff) {
58
58
  // Check if file is in common API/public paths
59
59
  const publicPaths = ['src/api', 'lib/', 'pkg/', 'public/', 'exports/', 'index.'];
60
60
  if (publicPaths.some(p => file.includes(p)))
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const mutateCommand: Command;