skrypt-ai 0.4.1 → 0.5.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.
- package/dist/auth/index.d.ts +13 -3
- package/dist/auth/index.js +94 -9
- package/dist/auth/keychain.d.ts +5 -0
- package/dist/auth/keychain.js +82 -0
- package/dist/auth/notices.d.ts +3 -0
- package/dist/auth/notices.js +42 -0
- package/dist/autofix/index.js +10 -3
- package/dist/cli.js +16 -3
- package/dist/commands/generate.js +37 -1
- package/dist/commands/import.d.ts +2 -0
- package/dist/commands/import.js +157 -0
- package/dist/commands/init.js +19 -7
- package/dist/commands/login.js +15 -4
- package/dist/commands/review-pr.js +10 -0
- package/dist/commands/security.d.ts +2 -0
- package/dist/commands/security.js +103 -0
- package/dist/config/loader.js +2 -2
- package/dist/generator/writer.js +12 -3
- package/dist/importers/confluence.d.ts +5 -0
- package/dist/importers/confluence.js +137 -0
- package/dist/importers/detect.d.ts +20 -0
- package/dist/importers/detect.js +121 -0
- package/dist/importers/docusaurus.d.ts +5 -0
- package/dist/importers/docusaurus.js +279 -0
- package/dist/importers/gitbook.d.ts +5 -0
- package/dist/importers/gitbook.js +189 -0
- package/dist/importers/github.d.ts +8 -0
- package/dist/importers/github.js +99 -0
- package/dist/importers/index.d.ts +15 -0
- package/dist/importers/index.js +30 -0
- package/dist/importers/markdown.d.ts +6 -0
- package/dist/importers/markdown.js +105 -0
- package/dist/importers/mintlify.d.ts +5 -0
- package/dist/importers/mintlify.js +172 -0
- package/dist/importers/notion.d.ts +5 -0
- package/dist/importers/notion.js +174 -0
- package/dist/importers/readme.d.ts +5 -0
- package/dist/importers/readme.js +184 -0
- package/dist/importers/transform.d.ts +90 -0
- package/dist/importers/transform.js +457 -0
- package/dist/importers/types.d.ts +37 -0
- package/dist/importers/types.js +1 -0
- package/dist/plugins/index.js +7 -0
- package/dist/scanner/index.js +37 -24
- package/dist/scanner/python.js +17 -0
- package/dist/template/public/search-index.json +1 -1
- package/dist/template/scripts/build-search-index.mjs +67 -9
- package/dist/template/src/components/mdx/dark-image.tsx +56 -0
- package/dist/template/src/components/mdx/frame.tsx +64 -0
- package/dist/template/src/components/mdx/highlighted-code.tsx +145 -31
- package/dist/template/src/components/mdx/index.tsx +4 -0
- package/dist/template/src/components/mdx/link-preview.tsx +119 -0
- package/dist/template/src/components/mdx/tooltip.tsx +101 -0
- package/dist/template/src/components/syntax-theme-selector.tsx +167 -20
- package/dist/template/src/lib/search-types.ts +4 -1
- package/dist/template/src/lib/search.ts +30 -7
- package/dist/template/src/styles/globals.css +39 -0
- package/dist/utils/files.d.ts +9 -1
- package/dist/utils/files.js +59 -10
- package/dist/utils/validation.js +1 -1
- package/package.json +4 -1
package/dist/commands/login.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
|
-
import { saveAuthConfig,
|
|
2
|
+
import { saveAuthConfig, getAuthConfigAsync, clearAuth, checkPlan, getKeyStorageMethod } from '../auth/index.js';
|
|
3
|
+
import { getKeychainPlatformName } from '../auth/keychain.js';
|
|
3
4
|
import * as readline from 'readline';
|
|
4
5
|
function prompt(question) {
|
|
5
6
|
const rl = readline.createInterface({
|
|
@@ -33,7 +34,7 @@ export const loginCommand = new Command('login')
|
|
|
33
34
|
console.error(`\n Error: ${result.error || 'Invalid API key'}`);
|
|
34
35
|
process.exit(1);
|
|
35
36
|
}
|
|
36
|
-
saveAuthConfig({
|
|
37
|
+
await saveAuthConfig({
|
|
37
38
|
apiKey,
|
|
38
39
|
email: result.email,
|
|
39
40
|
plan: result.plan,
|
|
@@ -41,6 +42,16 @@ export const loginCommand = new Command('login')
|
|
|
41
42
|
});
|
|
42
43
|
console.log(`\n ✓ Logged in as ${result.email}`);
|
|
43
44
|
console.log(` Plan: ${result.plan.toUpperCase()}\n`);
|
|
45
|
+
// Trust signals
|
|
46
|
+
const storageMethod = await getKeyStorageMethod();
|
|
47
|
+
if (storageMethod === 'keychain') {
|
|
48
|
+
console.log(` 🔒 API key stored in ${getKeychainPlatformName()}`);
|
|
49
|
+
}
|
|
50
|
+
else if (storageMethod === 'file') {
|
|
51
|
+
console.log(' 🔒 API key stored in ~/.skrypt/auth.json (0600 permissions)');
|
|
52
|
+
}
|
|
53
|
+
console.log(' Your key is hashed (SHA-256) on our servers — never stored in plaintext');
|
|
54
|
+
console.log('');
|
|
44
55
|
if (result.plan === 'free') {
|
|
45
56
|
console.log(' Upgrade to Pro: https://skrypt.sh/pro\n');
|
|
46
57
|
}
|
|
@@ -48,13 +59,13 @@ export const loginCommand = new Command('login')
|
|
|
48
59
|
export const logoutCommand = new Command('logout')
|
|
49
60
|
.description('Logout from Skrypt')
|
|
50
61
|
.action(async () => {
|
|
51
|
-
clearAuth();
|
|
62
|
+
await clearAuth();
|
|
52
63
|
console.log(' Logged out successfully\n');
|
|
53
64
|
});
|
|
54
65
|
export const whoamiCommand = new Command('whoami')
|
|
55
66
|
.description('Show current login status')
|
|
56
67
|
.action(async () => {
|
|
57
|
-
const config =
|
|
68
|
+
const config = await getAuthConfigAsync();
|
|
58
69
|
if (!config.apiKey) {
|
|
59
70
|
console.log(' Not logged in');
|
|
60
71
|
console.log(' Run: skrypt login\n');
|
|
@@ -21,10 +21,20 @@ export const reviewPRCommand = new Command('review-pr')
|
|
|
21
21
|
console.error('Error: Could not parse owner/repo/PR number from URL');
|
|
22
22
|
process.exit(1);
|
|
23
23
|
}
|
|
24
|
+
// Validate owner/repo contain only safe characters (prevent API path injection)
|
|
25
|
+
const safeNamePattern = /^[a-zA-Z0-9._-]+$/;
|
|
26
|
+
if (!safeNamePattern.test(owner) || !safeNamePattern.test(repo)) {
|
|
27
|
+
console.error('Error: Invalid characters in owner/repo name');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
24
30
|
console.log('skrypt review-pr');
|
|
25
31
|
console.log(` repo: ${owner}/${repo}`);
|
|
26
32
|
console.log(` PR: #${pullNumber}`);
|
|
27
33
|
console.log('');
|
|
34
|
+
if (options.token) {
|
|
35
|
+
console.warn(' Warning: Passing tokens via CLI arguments exposes them in shell history.');
|
|
36
|
+
console.warn(' Prefer: GITHUB_TOKEN env var\n');
|
|
37
|
+
}
|
|
28
38
|
const token = options.token || process.env.GITHUB_TOKEN;
|
|
29
39
|
if (!token) {
|
|
30
40
|
console.error('Error: GITHUB_TOKEN environment variable or --token required');
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { existsSync, statSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import { getAuthConfigAsync, getKeyStorageMethod } from '../auth/index.js';
|
|
6
|
+
import { keychainAvailable, getKeychainPlatformName } from '../auth/keychain.js';
|
|
7
|
+
export const securityCommand = new Command('security')
|
|
8
|
+
.description('Show security and key storage details')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
console.log('skrypt security\n');
|
|
11
|
+
const configDir = join(homedir(), '.skrypt');
|
|
12
|
+
const authFile = join(configDir, 'auth.json');
|
|
13
|
+
// 1. Key Storage
|
|
14
|
+
console.log(' \x1b[1mKey Storage\x1b[0m');
|
|
15
|
+
const method = await getKeyStorageMethod();
|
|
16
|
+
const hasKeychain = await keychainAvailable();
|
|
17
|
+
if (method === 'env') {
|
|
18
|
+
console.log(' ✓ Using SKRYPT_API_KEY environment variable');
|
|
19
|
+
}
|
|
20
|
+
else if (method === 'keychain') {
|
|
21
|
+
console.log(` ✓ API key stored in ${getKeychainPlatformName()}`);
|
|
22
|
+
console.log(' Hardware-backed encryption (Secure Enclave on macOS)');
|
|
23
|
+
}
|
|
24
|
+
else if (method === 'file') {
|
|
25
|
+
console.log(` ● API key stored in ${authFile}`);
|
|
26
|
+
console.log(' File permissions: 0600 (owner read/write only)');
|
|
27
|
+
if (!hasKeychain) {
|
|
28
|
+
console.log(' \x1b[33mTip: Install @napi-rs/keyring for OS keychain storage\x1b[0m');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
console.log(' ○ No API key configured');
|
|
33
|
+
console.log(' Run: skrypt login');
|
|
34
|
+
}
|
|
35
|
+
console.log('');
|
|
36
|
+
// 2. Data Flow
|
|
37
|
+
console.log(' \x1b[1mData Flow\x1b[0m');
|
|
38
|
+
const envKeys = {
|
|
39
|
+
OPENAI_API_KEY: !!process.env.OPENAI_API_KEY,
|
|
40
|
+
ANTHROPIC_API_KEY: !!process.env.ANTHROPIC_API_KEY,
|
|
41
|
+
GOOGLE_API_KEY: !!process.env.GOOGLE_API_KEY,
|
|
42
|
+
DEEPSEEK_API_KEY: !!process.env.DEEPSEEK_API_KEY,
|
|
43
|
+
};
|
|
44
|
+
const hasBYOK = Object.values(envKeys).some(Boolean);
|
|
45
|
+
if (hasBYOK) {
|
|
46
|
+
const providers = Object.entries(envKeys)
|
|
47
|
+
.filter(([, v]) => v)
|
|
48
|
+
.map(([k]) => k.replace('_API_KEY', '').toLowerCase());
|
|
49
|
+
console.log(` ✓ BYOK mode: LLM calls go directly to ${providers.join(', ')}`);
|
|
50
|
+
console.log(' Your keys never touch Skrypt servers');
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const config = await getAuthConfigAsync();
|
|
54
|
+
if (config.apiKey) {
|
|
55
|
+
console.log(' ● Proxy mode: LLM calls routed through Skrypt API');
|
|
56
|
+
console.log(' Your Skrypt key authenticates requests');
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
console.log(' ○ No provider keys or Skrypt key configured');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
console.log('');
|
|
63
|
+
// 3. Server-Side Security
|
|
64
|
+
console.log(' \x1b[1mServer-Side Security\x1b[0m');
|
|
65
|
+
console.log(' • API keys hashed with SHA-256 — never stored in plaintext');
|
|
66
|
+
console.log(' • Encrypted at rest with AES-256 via AWS KMS');
|
|
67
|
+
console.log(' • TLS 1.3 for all API communication');
|
|
68
|
+
console.log('');
|
|
69
|
+
// 4. Permissions
|
|
70
|
+
console.log(' \x1b[1mPermissions\x1b[0m');
|
|
71
|
+
if (existsSync(configDir)) {
|
|
72
|
+
try {
|
|
73
|
+
const dirStat = statSync(configDir);
|
|
74
|
+
const dirMode = (dirStat.mode & 0o777).toString(8);
|
|
75
|
+
console.log(` ~/.skrypt/ ${dirMode} (${dirMode === '700' ? '✓' : '⚠ expected 700'})`);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
console.log(' ~/.skrypt/ unable to read');
|
|
79
|
+
}
|
|
80
|
+
if (existsSync(authFile)) {
|
|
81
|
+
try {
|
|
82
|
+
const fileStat = statSync(authFile);
|
|
83
|
+
const fileMode = (fileStat.mode & 0o777).toString(8);
|
|
84
|
+
console.log(` ~/.skrypt/auth.json ${fileMode} (${fileMode === '600' ? '✓' : '⚠ expected 600'})`);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
console.log(' ~/.skrypt/auth.json unable to read');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
console.log(' ~/.skrypt/ does not exist yet');
|
|
93
|
+
}
|
|
94
|
+
console.log('');
|
|
95
|
+
// 5. Environment
|
|
96
|
+
console.log(' \x1b[1mEnvironment\x1b[0m');
|
|
97
|
+
console.log(` Platform: ${process.platform}`);
|
|
98
|
+
console.log(` OS keychain: ${hasKeychain ? '✓ available' : '✗ not available'}`);
|
|
99
|
+
console.log(` SKRYPT_API_KEY: ${process.env.SKRYPT_API_KEY ? '✓ set' : '○ not set'}`);
|
|
100
|
+
console.log(` OPENAI_API_KEY: ${process.env.OPENAI_API_KEY ? '✓ set' : '○ not set'}`);
|
|
101
|
+
console.log(` ANTHROPIC_API_KEY: ${process.env.ANTHROPIC_API_KEY ? '✓ set' : '○ not set'}`);
|
|
102
|
+
console.log('');
|
|
103
|
+
});
|
package/dist/config/loader.js
CHANGED
|
@@ -36,7 +36,7 @@ function parseConfigFile(filepath) {
|
|
|
36
36
|
}
|
|
37
37
|
catch (err) {
|
|
38
38
|
throw new Error(`Could not read config file: ${filepath}. ` +
|
|
39
|
-
(err instanceof Error ? err.message : String(err)));
|
|
39
|
+
(err instanceof Error ? err.message : String(err)), { cause: err });
|
|
40
40
|
}
|
|
41
41
|
let parsed;
|
|
42
42
|
try {
|
|
@@ -45,7 +45,7 @@ function parseConfigFile(filepath) {
|
|
|
45
45
|
catch (err) {
|
|
46
46
|
throw new Error(`Config file has invalid YAML: ${filepath}. ` +
|
|
47
47
|
`Please check the syntax and try again. ` +
|
|
48
|
-
(err instanceof Error ? err.message : String(err)));
|
|
48
|
+
(err instanceof Error ? err.message : String(err)), { cause: err });
|
|
49
49
|
}
|
|
50
50
|
if (parsed === null || parsed === undefined || typeof parsed !== 'object') {
|
|
51
51
|
throw new Error(`Config file is empty or not a valid YAML object: ${filepath}. ` +
|
package/dist/generator/writer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdir, writeFile } from 'fs/promises';
|
|
2
|
-
import { dirname, join, basename, relative } from 'path';
|
|
2
|
+
import { dirname, join, basename, relative, resolve, sep } from 'path';
|
|
3
3
|
import { formatAsMarkdown } from './generator.js';
|
|
4
4
|
import { organizeByTopic, detectCrossReferences, getCrossRefsForElement } from './organizer.js';
|
|
5
5
|
import { slugify } from '../utils/files.js';
|
|
@@ -88,6 +88,12 @@ export async function writeDocsToDirectory(results, outputDir, sourceDir) {
|
|
|
88
88
|
}
|
|
89
89
|
const docFileName = relPath.replace(/\.[^.]+$/, '.md');
|
|
90
90
|
const outputPath = join(outputDir, docFileName);
|
|
91
|
+
// Double-check resolved path stays within output directory
|
|
92
|
+
const resolvedOutput = resolve(outputPath);
|
|
93
|
+
if (!resolvedOutput.startsWith(resolve(outputDir) + sep) && resolvedOutput !== resolve(outputDir)) {
|
|
94
|
+
console.warn(`Skipping file outside output directory: ${result.filePath}`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
91
97
|
// Create subdirectories if needed
|
|
92
98
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
93
99
|
// Generate markdown content
|
|
@@ -174,9 +180,12 @@ export async function writeDocsByTopic(docs, outputDir) {
|
|
|
174
180
|
*/
|
|
175
181
|
function formatTopicMarkdown(topic, allCrossRefs) {
|
|
176
182
|
// MDX frontmatter (standard across doc platforms)
|
|
183
|
+
const safeTitle = topic.name.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ');
|
|
184
|
+
const rawDesc = topic.description || `API reference for ${topic.name}`;
|
|
185
|
+
const safeDesc = rawDesc.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, ' ');
|
|
177
186
|
let content = `---
|
|
178
|
-
title: "${
|
|
179
|
-
description: "${
|
|
187
|
+
title: "${safeTitle}"
|
|
188
|
+
description: "${safeDesc}"
|
|
180
189
|
---
|
|
181
190
|
|
|
182
191
|
`;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
+
import { join, relative, basename, extname } from 'path';
|
|
3
|
+
import { transformConfluenceCallouts, transformConfluenceHtml, normalizeFrontmatter, } from './transform.js';
|
|
4
|
+
/**
|
|
5
|
+
* Import Confluence HTML space export.
|
|
6
|
+
*/
|
|
7
|
+
export function importConfluence(dir, name) {
|
|
8
|
+
const result = createEmptyResult(name);
|
|
9
|
+
const stats = { callouts: 0, tabs: 0, codeGroups: 0, steps: 0, accordions: 0, images: 0, other: 0 };
|
|
10
|
+
// Find all HTML files
|
|
11
|
+
const htmlFiles = findHtmlFiles(dir);
|
|
12
|
+
if (htmlFiles.length === 0) {
|
|
13
|
+
result.warnings.push('No .html files found in Confluence export');
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
const pages = [];
|
|
17
|
+
for (const filePath of htmlFiles) {
|
|
18
|
+
// Skip index.html (usually a listing page)
|
|
19
|
+
if (basename(filePath) === 'index.html')
|
|
20
|
+
continue;
|
|
21
|
+
const page = processConfluencePage(dir, filePath, stats, result);
|
|
22
|
+
if (page)
|
|
23
|
+
pages.push(page);
|
|
24
|
+
}
|
|
25
|
+
if (pages.length > 0) {
|
|
26
|
+
result.navigation.push({ group: name || 'Documentation', pages });
|
|
27
|
+
}
|
|
28
|
+
// Copy attachments
|
|
29
|
+
const attachmentsDir = join(dir, 'attachments');
|
|
30
|
+
if (existsSync(attachmentsDir)) {
|
|
31
|
+
collectAttachments(attachmentsDir, result, stats);
|
|
32
|
+
}
|
|
33
|
+
result.stats = {
|
|
34
|
+
pages: result.files.size,
|
|
35
|
+
groups: result.navigation.length,
|
|
36
|
+
transforms: stats,
|
|
37
|
+
};
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
function findHtmlFiles(dir) {
|
|
41
|
+
const files = [];
|
|
42
|
+
try {
|
|
43
|
+
for (const entry of readdirSync(dir)) {
|
|
44
|
+
const fullPath = join(dir, entry);
|
|
45
|
+
try {
|
|
46
|
+
const stat = statSync(fullPath);
|
|
47
|
+
if (stat.isFile() && extname(entry) === '.html') {
|
|
48
|
+
files.push(fullPath);
|
|
49
|
+
}
|
|
50
|
+
else if (stat.isDirectory() && !entry.startsWith('.') && entry !== 'attachments') {
|
|
51
|
+
files.push(...findHtmlFiles(fullPath));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch { /* skip */ }
|
|
60
|
+
return files;
|
|
61
|
+
}
|
|
62
|
+
function processConfluencePage(dir, filePath, stats, result) {
|
|
63
|
+
const rawContent = readFileSync(filePath, 'utf-8');
|
|
64
|
+
let content = rawContent;
|
|
65
|
+
// Extract body content
|
|
66
|
+
const bodyMatch = content.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
|
67
|
+
if (bodyMatch)
|
|
68
|
+
content = bodyMatch[1];
|
|
69
|
+
// Extract title from <title> tag or <h1> (use rawContent to search full HTML)
|
|
70
|
+
let title = '';
|
|
71
|
+
const titleMatch = rawContent.match(/<title>([^<]+)<\/title>/i);
|
|
72
|
+
if (titleMatch)
|
|
73
|
+
title = titleMatch[1].trim();
|
|
74
|
+
if (!title) {
|
|
75
|
+
const h1Match = content.match(/<h1[^>]*>([\s\S]*?)<\/h1>/i);
|
|
76
|
+
if (h1Match)
|
|
77
|
+
title = h1Match[1].replace(/<[^>]+>/g, '').trim();
|
|
78
|
+
}
|
|
79
|
+
if (!title)
|
|
80
|
+
title = basename(filePath, '.html').replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
81
|
+
const originalContent = content;
|
|
82
|
+
// Apply transforms: callouts first, then general HTML conversion
|
|
83
|
+
content = transformConfluenceCallouts(content);
|
|
84
|
+
content = transformConfluenceHtml(content);
|
|
85
|
+
content = normalizeFrontmatter(content, { title });
|
|
86
|
+
stats.callouts += countNew(originalContent, content, '<Callout');
|
|
87
|
+
stats.accordions += countNew(originalContent, content, '<Accordion');
|
|
88
|
+
const rel = relative(dir, filePath);
|
|
89
|
+
const slug = basename(filePath, '.html').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
90
|
+
const outputPath = `content/docs/${slug}.mdx`;
|
|
91
|
+
result.files.set(outputPath, content);
|
|
92
|
+
return { title, slug, sourcePath: rel, content };
|
|
93
|
+
}
|
|
94
|
+
function collectAttachments(attachmentsDir, result, stats) {
|
|
95
|
+
const imgExts = new Set(['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp']);
|
|
96
|
+
function walk(dir) {
|
|
97
|
+
try {
|
|
98
|
+
for (const entry of readdirSync(dir)) {
|
|
99
|
+
const fullPath = join(dir, entry);
|
|
100
|
+
try {
|
|
101
|
+
const stat = statSync(fullPath);
|
|
102
|
+
if (stat.isDirectory()) {
|
|
103
|
+
walk(fullPath);
|
|
104
|
+
}
|
|
105
|
+
else if (imgExts.has(extname(entry).toLowerCase())) {
|
|
106
|
+
const dest = `public/images/${entry}`;
|
|
107
|
+
result.assets.set(dest, fullPath);
|
|
108
|
+
stats.images++;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch { /* skip */ }
|
|
117
|
+
}
|
|
118
|
+
walk(attachmentsDir);
|
|
119
|
+
}
|
|
120
|
+
function countNew(original, transformed, marker) {
|
|
121
|
+
const escaped = marker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
122
|
+
const origCount = (original.match(new RegExp(escaped, 'g')) || []).length;
|
|
123
|
+
const newCount = (transformed.match(new RegExp(escaped, 'g')) || []).length;
|
|
124
|
+
return Math.max(0, newCount - origCount);
|
|
125
|
+
}
|
|
126
|
+
function createEmptyResult(name) {
|
|
127
|
+
return {
|
|
128
|
+
navigation: [],
|
|
129
|
+
name: name || 'Documentation',
|
|
130
|
+
description: 'Imported from Confluence',
|
|
131
|
+
files: new Map(),
|
|
132
|
+
assets: new Map(),
|
|
133
|
+
warnings: [],
|
|
134
|
+
stats: { pages: 0, groups: 0, transforms: { callouts: 0, tabs: 0, codeGroups: 0, steps: 0, accordions: 0, images: 0, other: 0 } },
|
|
135
|
+
sourceFormat: 'confluence',
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ImportFormat } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Auto-detect documentation format from a directory by checking marker files.
|
|
4
|
+
* First match wins (priority order).
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectFormat(dir: string): ImportFormat;
|
|
7
|
+
/**
|
|
8
|
+
* Check if a string is a GitHub URL
|
|
9
|
+
*/
|
|
10
|
+
export declare function isGitHubUrl(input: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Parse a GitHub URL into components
|
|
13
|
+
* Supports: https://github.com/owner/repo/tree/branch/path
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseGitHubUrl(url: string): {
|
|
16
|
+
owner: string;
|
|
17
|
+
repo: string;
|
|
18
|
+
path: string;
|
|
19
|
+
ref: string;
|
|
20
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { join, extname } from 'path';
|
|
3
|
+
/**
|
|
4
|
+
* Auto-detect documentation format from a directory by checking marker files.
|
|
5
|
+
* First match wins (priority order).
|
|
6
|
+
*/
|
|
7
|
+
export function detectFormat(dir) {
|
|
8
|
+
// 1. Mintlify: mint.json or docs.json with Mintlify structure
|
|
9
|
+
if (existsSync(join(dir, 'mint.json')))
|
|
10
|
+
return 'mintlify';
|
|
11
|
+
if (existsSync(join(dir, 'docs.json'))) {
|
|
12
|
+
try {
|
|
13
|
+
const content = JSON.parse(readFileSync(join(dir, 'docs.json'), 'utf-8'));
|
|
14
|
+
// Mintlify docs.json has navigation with group/pages AND one of: anchors, tabs, topbarCtaButton
|
|
15
|
+
if (content.navigation && Array.isArray(content.navigation) &&
|
|
16
|
+
(content.anchors || content.tabs || content.topbarCtaButton || content.topbarLinks)) {
|
|
17
|
+
return 'mintlify';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch { /* not mintlify */ }
|
|
21
|
+
}
|
|
22
|
+
// 2. Docusaurus: docusaurus.config.js or .ts
|
|
23
|
+
if (existsSync(join(dir, 'docusaurus.config.js')) || existsSync(join(dir, 'docusaurus.config.ts'))) {
|
|
24
|
+
return 'docusaurus';
|
|
25
|
+
}
|
|
26
|
+
// 3. GitBook: SUMMARY.md or .gitbook.yaml
|
|
27
|
+
if (existsSync(join(dir, 'SUMMARY.md')) || existsSync(join(dir, '.gitbook.yaml'))) {
|
|
28
|
+
return 'gitbook';
|
|
29
|
+
}
|
|
30
|
+
// 4. ReadMe: files with [block: syntax or _order.yaml
|
|
31
|
+
if (hasReadmeMarkers(dir))
|
|
32
|
+
return 'readme';
|
|
33
|
+
// 5. Notion: files with <aside> + 32-char hex UUID filenames
|
|
34
|
+
if (hasNotionMarkers(dir))
|
|
35
|
+
return 'notion';
|
|
36
|
+
// 6. Confluence: .html files with <ac:structured-macro
|
|
37
|
+
if (hasConfluenceMarkers(dir))
|
|
38
|
+
return 'confluence';
|
|
39
|
+
// 7. MkDocs (handled by markdown importer)
|
|
40
|
+
if (existsSync(join(dir, 'mkdocs.yml')))
|
|
41
|
+
return 'markdown';
|
|
42
|
+
// 8. Fallback: any .md/.mdx files present
|
|
43
|
+
return 'markdown';
|
|
44
|
+
}
|
|
45
|
+
function hasReadmeMarkers(dir) {
|
|
46
|
+
try {
|
|
47
|
+
const entries = readdirSync(dir, { recursive: true });
|
|
48
|
+
for (const entry of entries) {
|
|
49
|
+
const fullPath = join(dir, entry);
|
|
50
|
+
if (entry === '_order.yaml' || entry.endsWith('/_order.yaml'))
|
|
51
|
+
return true;
|
|
52
|
+
if (extname(entry) === '.md') {
|
|
53
|
+
try {
|
|
54
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
55
|
+
if (content.includes('[block:'))
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch { /* skip */ }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch { /* skip */ }
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
function hasNotionMarkers(dir) {
|
|
66
|
+
const UUID_PATTERN = /[0-9a-f]{32}/;
|
|
67
|
+
try {
|
|
68
|
+
const entries = readdirSync(dir);
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (extname(entry) === '.md' && UUID_PATTERN.test(entry)) {
|
|
71
|
+
try {
|
|
72
|
+
const content = readFileSync(join(dir, entry), 'utf-8');
|
|
73
|
+
if (content.includes('<aside>'))
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
catch { /* skip */ }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch { /* skip */ }
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
function hasConfluenceMarkers(dir) {
|
|
84
|
+
try {
|
|
85
|
+
const entries = readdirSync(dir);
|
|
86
|
+
for (const entry of entries) {
|
|
87
|
+
if (extname(entry) === '.html') {
|
|
88
|
+
try {
|
|
89
|
+
const content = readFileSync(join(dir, entry), 'utf-8');
|
|
90
|
+
if (content.includes('<ac:structured-macro'))
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
catch { /* skip */ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch { /* skip */ }
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if a string is a GitHub URL
|
|
102
|
+
*/
|
|
103
|
+
export function isGitHubUrl(input) {
|
|
104
|
+
return /^https?:\/\/(www\.)?github\.com\//.test(input);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Parse a GitHub URL into components
|
|
108
|
+
* Supports: https://github.com/owner/repo/tree/branch/path
|
|
109
|
+
*/
|
|
110
|
+
export function parseGitHubUrl(url) {
|
|
111
|
+
const match = url.match(/^https?:\/\/(www\.)?github\.com\/([^/]+)\/([^/]+)(?:\/tree\/([^/]+)(?:\/(.*))?)?/);
|
|
112
|
+
if (!match) {
|
|
113
|
+
throw new Error(`Invalid GitHub URL: ${url}`);
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
owner: match[2],
|
|
117
|
+
repo: match[3],
|
|
118
|
+
ref: match[4] || 'main',
|
|
119
|
+
path: match[5] || '',
|
|
120
|
+
};
|
|
121
|
+
}
|