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.
Files changed (61) hide show
  1. package/dist/auth/index.d.ts +13 -3
  2. package/dist/auth/index.js +94 -9
  3. package/dist/auth/keychain.d.ts +5 -0
  4. package/dist/auth/keychain.js +82 -0
  5. package/dist/auth/notices.d.ts +3 -0
  6. package/dist/auth/notices.js +42 -0
  7. package/dist/autofix/index.js +10 -3
  8. package/dist/cli.js +16 -3
  9. package/dist/commands/generate.js +37 -1
  10. package/dist/commands/import.d.ts +2 -0
  11. package/dist/commands/import.js +157 -0
  12. package/dist/commands/init.js +19 -7
  13. package/dist/commands/login.js +15 -4
  14. package/dist/commands/review-pr.js +10 -0
  15. package/dist/commands/security.d.ts +2 -0
  16. package/dist/commands/security.js +103 -0
  17. package/dist/config/loader.js +2 -2
  18. package/dist/generator/writer.js +12 -3
  19. package/dist/importers/confluence.d.ts +5 -0
  20. package/dist/importers/confluence.js +137 -0
  21. package/dist/importers/detect.d.ts +20 -0
  22. package/dist/importers/detect.js +121 -0
  23. package/dist/importers/docusaurus.d.ts +5 -0
  24. package/dist/importers/docusaurus.js +279 -0
  25. package/dist/importers/gitbook.d.ts +5 -0
  26. package/dist/importers/gitbook.js +189 -0
  27. package/dist/importers/github.d.ts +8 -0
  28. package/dist/importers/github.js +99 -0
  29. package/dist/importers/index.d.ts +15 -0
  30. package/dist/importers/index.js +30 -0
  31. package/dist/importers/markdown.d.ts +6 -0
  32. package/dist/importers/markdown.js +105 -0
  33. package/dist/importers/mintlify.d.ts +5 -0
  34. package/dist/importers/mintlify.js +172 -0
  35. package/dist/importers/notion.d.ts +5 -0
  36. package/dist/importers/notion.js +174 -0
  37. package/dist/importers/readme.d.ts +5 -0
  38. package/dist/importers/readme.js +184 -0
  39. package/dist/importers/transform.d.ts +90 -0
  40. package/dist/importers/transform.js +457 -0
  41. package/dist/importers/types.d.ts +37 -0
  42. package/dist/importers/types.js +1 -0
  43. package/dist/plugins/index.js +7 -0
  44. package/dist/scanner/index.js +37 -24
  45. package/dist/scanner/python.js +17 -0
  46. package/dist/template/public/search-index.json +1 -1
  47. package/dist/template/scripts/build-search-index.mjs +67 -9
  48. package/dist/template/src/components/mdx/dark-image.tsx +56 -0
  49. package/dist/template/src/components/mdx/frame.tsx +64 -0
  50. package/dist/template/src/components/mdx/highlighted-code.tsx +145 -31
  51. package/dist/template/src/components/mdx/index.tsx +4 -0
  52. package/dist/template/src/components/mdx/link-preview.tsx +119 -0
  53. package/dist/template/src/components/mdx/tooltip.tsx +101 -0
  54. package/dist/template/src/components/syntax-theme-selector.tsx +167 -20
  55. package/dist/template/src/lib/search-types.ts +4 -1
  56. package/dist/template/src/lib/search.ts +30 -7
  57. package/dist/template/src/styles/globals.css +39 -0
  58. package/dist/utils/files.d.ts +9 -1
  59. package/dist/utils/files.js +59 -10
  60. package/dist/utils/validation.js +1 -1
  61. package/package.json +4 -1
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
- import { saveAuthConfig, getAuthConfig, clearAuth, checkPlan } from '../auth/index.js';
2
+ import { saveAuthConfig, getAuthConfigAsync, clearAuth, checkPlan, getKeyStorageMethod } from '../auth/index.js';
3
+ import { getKeychainPlatformName } from '../auth/keychain.js';
3
4
  import * as readline from 'readline';
4
5
  function prompt(question) {
5
6
  const rl = readline.createInterface({
@@ -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 = getAuthConfig();
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,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const securityCommand: Command;
@@ -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
+ });
@@ -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}. ` +
@@ -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: "${topic.name}"
179
- description: "${topic.description || `API reference for ${topic.name}`}"
187
+ title: "${safeTitle}"
188
+ description: "${safeDesc}"
180
189
  ---
181
190
 
182
191
  `;
@@ -0,0 +1,5 @@
1
+ import type { ImportResult } from './types.js';
2
+ /**
3
+ * Import Confluence HTML space export.
4
+ */
5
+ export declare function importConfluence(dir: string, name?: string): ImportResult;
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import type { ImportResult } from './types.js';
2
+ /**
3
+ * Import Docusaurus documentation.
4
+ */
5
+ export declare function importDocusaurus(dir: string, name?: string): ImportResult;