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.
- package/dist/auth/index.js +6 -0
- package/dist/binding/binder.d.ts +5 -0
- package/dist/binding/binder.js +63 -0
- package/dist/binding/detector.d.ts +5 -0
- package/dist/binding/detector.js +51 -0
- package/dist/binding/doc-parser.d.ts +9 -0
- package/dist/binding/doc-parser.js +138 -0
- package/dist/binding/extractor.d.ts +14 -0
- package/dist/binding/extractor.js +39 -0
- package/dist/binding/index.d.ts +5 -0
- package/dist/binding/index.js +5 -0
- package/dist/binding/types.d.ts +74 -0
- package/dist/binding/types.js +1 -0
- package/dist/claims/extractor.d.ts +13 -0
- package/dist/claims/extractor.js +138 -0
- package/dist/claims/index.d.ts +4 -0
- package/dist/claims/index.js +4 -0
- package/dist/claims/reporter.d.ts +9 -0
- package/dist/claims/reporter.js +65 -0
- package/dist/claims/store.d.ts +13 -0
- package/dist/claims/store.js +51 -0
- package/dist/claims/types.d.ts +34 -0
- package/dist/claims/types.js +1 -0
- package/dist/cli.js +516 -56
- package/dist/commands/bind.d.ts +2 -0
- package/dist/commands/bind.js +139 -0
- package/dist/commands/claims.d.ts +2 -0
- package/dist/commands/claims.js +84 -0
- package/dist/commands/coverage.d.ts +2 -0
- package/dist/commands/coverage.js +61 -0
- package/dist/commands/generate/index.js +5 -0
- package/dist/commands/generate/scan.js +33 -14
- package/dist/commands/generate/write.d.ts +7 -0
- package/dist/commands/generate/write.js +65 -1
- package/dist/commands/import.js +12 -3
- package/dist/commands/init.js +68 -5
- package/dist/commands/monitor.d.ts +15 -0
- package/dist/commands/monitor.js +2 -2
- package/dist/commands/mutate.d.ts +2 -0
- package/dist/commands/mutate.js +177 -0
- package/dist/config/types.js +2 -2
- package/dist/coverage/calculator.d.ts +7 -0
- package/dist/coverage/calculator.js +86 -0
- package/dist/coverage/index.d.ts +3 -0
- package/dist/coverage/index.js +3 -0
- package/dist/coverage/reporter.d.ts +9 -0
- package/dist/coverage/reporter.js +65 -0
- package/dist/coverage/types.d.ts +36 -0
- package/dist/coverage/types.js +1 -0
- package/dist/generator/generator.d.ts +3 -1
- package/dist/generator/generator.js +137 -23
- package/dist/generator/mdx-serializer.js +3 -2
- package/dist/generator/organizer.d.ts +5 -1
- package/dist/generator/organizer.js +29 -14
- package/dist/generator/types.d.ts +6 -0
- package/dist/generator/writer.js +7 -2
- package/dist/github/org-discovery.js +5 -0
- package/dist/importers/mintlify.js +4 -3
- package/dist/llm/anthropic-client.js +1 -0
- package/dist/llm/index.d.ts +15 -0
- package/dist/llm/index.js +148 -29
- package/dist/llm/openai-client.js +2 -0
- package/dist/mutation/index.d.ts +4 -0
- package/dist/mutation/index.js +4 -0
- package/dist/mutation/mutator.d.ts +5 -0
- package/dist/mutation/mutator.js +101 -0
- package/dist/mutation/reporter.d.ts +14 -0
- package/dist/mutation/reporter.js +66 -0
- package/dist/mutation/runner.d.ts +9 -0
- package/dist/mutation/runner.js +70 -0
- package/dist/mutation/types.d.ts +51 -0
- package/dist/mutation/types.js +1 -0
- package/dist/qa/checks.d.ts +1 -0
- package/dist/qa/checks.js +47 -0
- package/dist/qa/index.js +2 -1
- package/dist/scanner/index.js +78 -11
- package/dist/scanner/typescript.js +42 -31
- package/dist/sentry.d.ts +3 -0
- package/dist/sentry.js +28 -0
- package/dist/template/docs.json +6 -3
- package/dist/template/next.config.mjs +15 -1
- package/dist/template/package.json +4 -3
- package/dist/template/public/docs-schema.json +257 -0
- package/dist/template/sentry.client.config.ts +12 -0
- package/dist/template/sentry.edge.config.ts +7 -0
- package/dist/template/sentry.server.config.ts +7 -0
- package/dist/template/src/app/docs/[...slug]/page.tsx +11 -5
- package/dist/template/src/app/docs/layout.tsx +2 -4
- package/dist/template/src/app/global-error.tsx +60 -0
- package/dist/template/src/app/layout.tsx +7 -16
- package/dist/template/src/app/page.tsx +2 -5
- package/dist/template/src/components/ai-chat-impl.tsx +1 -1
- package/dist/template/src/components/docs-layout.tsx +1 -15
- package/dist/template/src/components/footer.tsx +95 -19
- package/dist/template/src/components/header.tsx +1 -1
- package/dist/template/src/components/search-dialog.tsx +5 -0
- package/dist/template/src/instrumentation.ts +11 -0
- package/dist/template/src/lib/docs-config.ts +235 -0
- package/dist/template/src/lib/fonts.ts +3 -3
- package/dist/testing/runner.js +8 -1
- 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,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,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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
}
|
package/dist/commands/import.js
CHANGED
|
@@ -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);
|
package/dist/commands/init.js
CHANGED
|
@@ -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('
|
|
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;
|
package/dist/commands/monitor.js
CHANGED
|
@@ -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)))
|