skrypt-ai 0.4.2 → 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 (51) 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/generator/writer.js +12 -3
  18. package/dist/importers/confluence.d.ts +5 -0
  19. package/dist/importers/confluence.js +137 -0
  20. package/dist/importers/detect.d.ts +20 -0
  21. package/dist/importers/detect.js +121 -0
  22. package/dist/importers/docusaurus.d.ts +5 -0
  23. package/dist/importers/docusaurus.js +279 -0
  24. package/dist/importers/gitbook.d.ts +5 -0
  25. package/dist/importers/gitbook.js +189 -0
  26. package/dist/importers/github.d.ts +8 -0
  27. package/dist/importers/github.js +99 -0
  28. package/dist/importers/index.d.ts +15 -0
  29. package/dist/importers/index.js +30 -0
  30. package/dist/importers/markdown.d.ts +6 -0
  31. package/dist/importers/markdown.js +105 -0
  32. package/dist/importers/mintlify.d.ts +5 -0
  33. package/dist/importers/mintlify.js +172 -0
  34. package/dist/importers/notion.d.ts +5 -0
  35. package/dist/importers/notion.js +174 -0
  36. package/dist/importers/readme.d.ts +5 -0
  37. package/dist/importers/readme.js +184 -0
  38. package/dist/importers/transform.d.ts +90 -0
  39. package/dist/importers/transform.js +457 -0
  40. package/dist/importers/types.d.ts +37 -0
  41. package/dist/importers/types.js +1 -0
  42. package/dist/plugins/index.js +7 -0
  43. package/dist/scanner/index.js +37 -24
  44. package/dist/scanner/python.js +17 -0
  45. package/dist/template/public/search-index.json +1 -1
  46. package/dist/template/scripts/build-search-index.mjs +67 -9
  47. package/dist/template/src/lib/search-types.ts +4 -1
  48. package/dist/template/src/lib/search.ts +30 -7
  49. package/dist/utils/files.d.ts +9 -1
  50. package/dist/utils/files.js +59 -10
  51. package/package.json +4 -1
@@ -1,4 +1,4 @@
1
- interface AuthConfig {
1
+ export interface AuthConfig {
2
2
  apiKey?: string;
3
3
  email?: string;
4
4
  plan?: 'free' | 'pro';
@@ -11,9 +11,19 @@ interface PlanCheckResponse {
11
11
  expiresAt?: string;
12
12
  error?: string;
13
13
  }
14
+ /**
15
+ * Sync auth config reader — checks env var and auth file only (no keychain).
16
+ * Use getAuthConfigAsync() when keychain access is needed.
17
+ */
14
18
  export declare function getAuthConfig(): AuthConfig;
15
- export declare function saveAuthConfig(config: AuthConfig): void;
16
- export declare function clearAuth(): void;
19
+ /**
20
+ * Async auth config reader — checks env var → keychain → auth file.
21
+ * This is the preferred method for all command actions.
22
+ */
23
+ export declare function getAuthConfigAsync(): Promise<AuthConfig>;
24
+ export declare function saveAuthConfig(config: AuthConfig): Promise<void>;
25
+ export declare function clearAuth(): Promise<void>;
26
+ export declare function getKeyStorageMethod(): Promise<'keychain' | 'file' | 'env' | 'none'>;
17
27
  export declare function checkPlan(apiKey: string): Promise<PlanCheckResponse>;
18
28
  export declare function requirePro(commandName: string): Promise<boolean>;
19
29
  export declare const PRO_COMMANDS: string[];
@@ -1,10 +1,57 @@
1
- import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs';
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync, unlinkSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
- const CONFIG_DIR = join(homedir(), '.skrypt');
4
+ import { keychainStore, keychainRetrieve, keychainDelete } from './keychain.js';
5
+ const homeDir = homedir();
6
+ if (!homeDir) {
7
+ throw new Error('Could not determine home directory');
8
+ }
9
+ const CONFIG_DIR = join(homeDir, '.skrypt');
5
10
  const AUTH_FILE = join(CONFIG_DIR, 'auth.json');
6
11
  const API_BASE = process.env.SKRYPT_API_URL || 'https://api.skrypt.sh';
12
+ /**
13
+ * Sync auth config reader — checks env var and auth file only (no keychain).
14
+ * Use getAuthConfigAsync() when keychain access is needed.
15
+ */
7
16
  export function getAuthConfig() {
17
+ if (process.env.SKRYPT_API_KEY) {
18
+ const fileMeta = readAuthFile();
19
+ return {
20
+ apiKey: process.env.SKRYPT_API_KEY,
21
+ email: fileMeta.email,
22
+ plan: fileMeta.plan,
23
+ expiresAt: fileMeta.expiresAt,
24
+ };
25
+ }
26
+ return readAuthFile();
27
+ }
28
+ /**
29
+ * Async auth config reader — checks env var → keychain → auth file.
30
+ * This is the preferred method for all command actions.
31
+ */
32
+ export async function getAuthConfigAsync() {
33
+ if (process.env.SKRYPT_API_KEY) {
34
+ const fileMeta = readAuthFile();
35
+ return {
36
+ apiKey: process.env.SKRYPT_API_KEY,
37
+ email: fileMeta.email,
38
+ plan: fileMeta.plan,
39
+ expiresAt: fileMeta.expiresAt,
40
+ };
41
+ }
42
+ const keychainKey = await keychainRetrieve();
43
+ if (keychainKey) {
44
+ const fileMeta = readAuthFile();
45
+ return {
46
+ apiKey: keychainKey,
47
+ email: fileMeta.email,
48
+ plan: fileMeta.plan,
49
+ expiresAt: fileMeta.expiresAt,
50
+ };
51
+ }
52
+ return readAuthFile();
53
+ }
54
+ function readAuthFile() {
8
55
  if (!existsSync(AUTH_FILE)) {
9
56
  return {};
10
57
  }
@@ -15,16 +62,49 @@ export function getAuthConfig() {
15
62
  return {};
16
63
  }
17
64
  }
18
- export function saveAuthConfig(config) {
65
+ export async function saveAuthConfig(config) {
19
66
  mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
20
- writeFileSync(AUTH_FILE, JSON.stringify(config, null, 2));
67
+ let keyInKeychain = false;
68
+ if (config.apiKey) {
69
+ keyInKeychain = await keychainStore(config.apiKey);
70
+ }
71
+ // Write metadata to file (omit key if stored in keychain)
72
+ const fileConfig = {
73
+ email: config.email,
74
+ plan: config.plan,
75
+ expiresAt: config.expiresAt,
76
+ };
77
+ if (!keyInKeychain && config.apiKey) {
78
+ fileConfig.apiKey = config.apiKey;
79
+ }
80
+ // Write with restrictive permissions from the start
81
+ const content = JSON.stringify(fileConfig, null, 2);
82
+ writeFileSync(AUTH_FILE, content, { mode: 0o600 });
21
83
  chmodSync(AUTH_FILE, 0o600);
22
84
  }
23
- export function clearAuth() {
85
+ export async function clearAuth() {
86
+ await keychainDelete();
24
87
  if (existsSync(AUTH_FILE)) {
25
- writeFileSync(AUTH_FILE, '{}');
88
+ try {
89
+ unlinkSync(AUTH_FILE);
90
+ }
91
+ catch {
92
+ // Fallback: overwrite with empty
93
+ writeFileSync(AUTH_FILE, '{}', { mode: 0o600 });
94
+ }
26
95
  }
27
96
  }
97
+ export async function getKeyStorageMethod() {
98
+ if (process.env.SKRYPT_API_KEY)
99
+ return 'env';
100
+ const keychainKey = await keychainRetrieve();
101
+ if (keychainKey)
102
+ return 'keychain';
103
+ const fileConfig = readAuthFile();
104
+ if (fileConfig.apiKey)
105
+ return 'file';
106
+ return 'none';
107
+ }
28
108
  export async function checkPlan(apiKey) {
29
109
  try {
30
110
  const response = await fetch(`${API_BASE}/v1/plan`, {
@@ -36,14 +116,19 @@ export async function checkPlan(apiKey) {
36
116
  if (!response.ok) {
37
117
  return { valid: false, plan: 'free', email: '', error: 'Invalid API key' };
38
118
  }
39
- return await response.json();
119
+ const data = await response.json();
120
+ // Validate response shape
121
+ if (typeof data.valid !== 'boolean' || typeof data.email !== 'string') {
122
+ return { valid: false, plan: 'free', email: '', error: 'Invalid API response' };
123
+ }
124
+ return data;
40
125
  }
41
126
  catch {
42
127
  return { valid: false, plan: 'free', email: '', error: 'Failed to connect to API' };
43
128
  }
44
129
  }
45
130
  export async function requirePro(commandName) {
46
- const config = getAuthConfig();
131
+ const config = await getAuthConfigAsync();
47
132
  if (!config.apiKey) {
48
133
  console.error(`\n ⚡ ${commandName} requires Skrypt Pro\n`);
49
134
  console.error(' Get started:');
@@ -71,7 +156,7 @@ export async function requirePro(commandName) {
71
156
  return false;
72
157
  }
73
158
  // Cache the result for 1 hour
74
- saveAuthConfig({
159
+ await saveAuthConfig({
75
160
  ...config,
76
161
  plan: result.plan,
77
162
  email: result.email,
@@ -0,0 +1,5 @@
1
+ export declare function keychainAvailable(): Promise<boolean>;
2
+ export declare function keychainStore(key: string): Promise<boolean>;
3
+ export declare function keychainRetrieve(): Promise<string | null>;
4
+ export declare function keychainDelete(): Promise<boolean>;
5
+ export declare function getKeychainPlatformName(): string;
@@ -0,0 +1,82 @@
1
+ const SERVICE_NAME = 'skrypt';
2
+ const ACCOUNT_NAME = 'api-key';
3
+ let keyringLoadPromise = null;
4
+ async function loadKeyring() {
5
+ // Use a shared promise to prevent race conditions — only one import attempt
6
+ if (keyringLoadPromise)
7
+ return keyringLoadPromise;
8
+ keyringLoadPromise = doLoadKeyring();
9
+ return keyringLoadPromise;
10
+ }
11
+ async function doLoadKeyring() {
12
+ try {
13
+ // Dynamic import — @napi-rs/keyring is an optional dependency.
14
+ // Using indirect import to avoid TypeScript compile error for missing module.
15
+ const moduleName = '@napi-rs/keyring';
16
+ const mod = await import(/* webpackIgnore: true */ moduleName);
17
+ return mod;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ export async function keychainAvailable() {
24
+ const mod = await loadKeyring();
25
+ if (!mod)
26
+ return false;
27
+ try {
28
+ const entry = new mod.Entry(SERVICE_NAME, ACCOUNT_NAME);
29
+ // Actually test that keychain is functional by attempting a read
30
+ entry.getPassword();
31
+ return true;
32
+ }
33
+ catch {
34
+ return false;
35
+ }
36
+ }
37
+ export async function keychainStore(key) {
38
+ const mod = await loadKeyring();
39
+ if (!mod)
40
+ return false;
41
+ try {
42
+ const entry = new mod.Entry(SERVICE_NAME, ACCOUNT_NAME);
43
+ entry.setPassword(key);
44
+ return true;
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ export async function keychainRetrieve() {
51
+ const mod = await loadKeyring();
52
+ if (!mod)
53
+ return null;
54
+ try {
55
+ const entry = new mod.Entry(SERVICE_NAME, ACCOUNT_NAME);
56
+ const password = entry.getPassword();
57
+ return password || null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ export async function keychainDelete() {
64
+ const mod = await loadKeyring();
65
+ if (!mod)
66
+ return false;
67
+ try {
68
+ const entry = new mod.Entry(SERVICE_NAME, ACCOUNT_NAME);
69
+ entry.deletePassword();
70
+ return true;
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ export function getKeychainPlatformName() {
77
+ switch (process.platform) {
78
+ case 'darwin': return 'macOS Keychain';
79
+ case 'win32': return 'Windows Credential Manager';
80
+ default: return 'system keyring (libsecret)';
81
+ }
82
+ }
@@ -0,0 +1,3 @@
1
+ export declare function hasSeenNotice(id: string): boolean;
2
+ export declare function markNoticeSeen(id: string): void;
3
+ export declare function showSecurityNotice(): void;
@@ -0,0 +1,42 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ const CONFIG_DIR = join(homedir(), '.skrypt');
5
+ const NOTICES_FILE = join(CONFIG_DIR, 'notices.json');
6
+ function loadNotices() {
7
+ if (!existsSync(NOTICES_FILE))
8
+ return { seen: {} };
9
+ try {
10
+ return JSON.parse(readFileSync(NOTICES_FILE, 'utf-8'));
11
+ }
12
+ catch {
13
+ return { seen: {} };
14
+ }
15
+ }
16
+ function saveNotices(state) {
17
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
18
+ writeFileSync(NOTICES_FILE, JSON.stringify(state, null, 2));
19
+ }
20
+ export function hasSeenNotice(id) {
21
+ const state = loadNotices();
22
+ return id in state.seen;
23
+ }
24
+ export function markNoticeSeen(id) {
25
+ const state = loadNotices();
26
+ state.seen[id] = new Date().toISOString();
27
+ saveNotices(state);
28
+ }
29
+ export function showSecurityNotice() {
30
+ if (hasSeenNotice('security-v1'))
31
+ return;
32
+ console.log('');
33
+ console.log(' \x1b[36m🔒 Security Notice\x1b[0m');
34
+ console.log('');
35
+ console.log(' Your API keys are stored locally and never sent to Skrypt.');
36
+ console.log(' When using your own keys (OPENAI_API_KEY, etc.), LLM calls');
37
+ console.log(' go directly to the provider — they never touch our servers.');
38
+ console.log('');
39
+ console.log(' Run \x1b[1mskrypt security\x1b[0m for full details.');
40
+ console.log('');
41
+ markNoticeSeen('security-v1');
42
+ }
@@ -137,13 +137,20 @@ function extractCode(response, language) {
137
137
  const genericMatch = response.match(/```\w*\n([\s\S]*?)\n```/);
138
138
  if (genericMatch?.[1])
139
139
  return genericMatch[1].trim();
140
- // Try to find code without block
140
+ // Try to find code without block — only accept if it looks like actual code
141
141
  const lines = response.split('\n');
142
142
  const codeLines = lines.filter(line => {
143
143
  const trimmed = line.trim();
144
144
  return trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('//');
145
145
  });
146
- return codeLines.length > 0 ? codeLines.join('\n') : null;
146
+ if (codeLines.length === 0)
147
+ return null;
148
+ const joined = codeLines.join('\n');
149
+ // Require at least some code-like tokens to avoid returning prose as code
150
+ const codeTokens = /[=(){}[\];:,]|def |class |function |const |let |var |import |from |return /;
151
+ if (!codeTokens.test(joined))
152
+ return null;
153
+ return joined;
147
154
  }
148
155
  /**
149
156
  * Batch auto-fix multiple examples
@@ -212,7 +219,7 @@ export function createPythonValidator() {
212
219
  const { randomUUID } = await import('crypto');
213
220
  // Use crypto.randomUUID() to avoid timestamp collisions
214
221
  const tempFile = join(tmpdir(), `autofix_${randomUUID()}.py`);
215
- writeFileSync(tempFile, code);
222
+ writeFileSync(tempFile, code, { mode: 0o600 });
216
223
  try {
217
224
  // Use spawnSync with array args to avoid command injection
218
225
  const result = spawnSync('python3', ['-m', 'py_compile', tempFile], {
package/dist/cli.js CHANGED
@@ -18,6 +18,8 @@ import { loginCommand, logoutCommand, whoamiCommand } from './commands/login.js'
18
18
  import { cronCommand } from './commands/cron.js';
19
19
  import { deployCommand } from './commands/deploy.js';
20
20
  import { testCommand } from './commands/test.js';
21
+ import { securityCommand } from './commands/security.js';
22
+ import { importCommand } from './commands/import.js';
21
23
  import { createRequire } from 'module';
22
24
  process.on('uncaughtException', (err) => {
23
25
  console.error('\x1b[31mFatal error:\x1b[0m', err.message);
@@ -33,6 +35,16 @@ process.on('unhandledRejection', (reason) => {
33
35
  });
34
36
  const require = createRequire(import.meta.url);
35
37
  const { version: VERSION } = require('../package.json');
38
+ function semverNewer(latest, current) {
39
+ const parse = (v) => v.split('.').map(Number);
40
+ const [lMaj, lMin, lPatch] = parse(latest);
41
+ const [cMaj, cMin, cPatch] = parse(current);
42
+ if (lMaj !== cMaj)
43
+ return lMaj > cMaj;
44
+ if (lMin !== cMin)
45
+ return lMin > cMin;
46
+ return lPatch > cPatch;
47
+ }
36
48
  async function checkForUpdates() {
37
49
  try {
38
50
  const res = await fetch('https://registry.npmjs.org/skrypt-ai/latest', {
@@ -41,9 +53,8 @@ async function checkForUpdates() {
41
53
  if (res.ok) {
42
54
  const data = await res.json();
43
55
  const version = data.version;
44
- if (typeof version === 'string' && /^\d+\.\d+\.\d+/.test(version) && version !== VERSION) {
45
- const latest = version;
46
- console.log(`\n Update available: ${VERSION} → ${latest}`);
56
+ if (typeof version === 'string' && /^\d+\.\d+\.\d+$/.test(version) && semverNewer(version, VERSION)) {
57
+ console.log(`\n Update available: ${VERSION} → ${version}`);
47
58
  console.log(` Run: npm install -g skrypt-ai@latest\n`);
48
59
  }
49
60
  }
@@ -80,4 +91,6 @@ program.addCommand(whoamiCommand);
80
91
  program.addCommand(cronCommand);
81
92
  program.addCommand(deployCommand);
82
93
  program.addCommand(testCommand);
94
+ program.addCommand(securityCommand);
95
+ program.addCommand(importCommand);
83
96
  program.parse();
@@ -6,6 +6,7 @@ import { DEFAULT_MODELS } from '../config/types.js';
6
6
  import { scanDirectory } from '../scanner/index.js';
7
7
  import { createLLMClient } from '../llm/index.js';
8
8
  import { generateForElements, groupDocsByFile, writeDocsToDirectory, writeDocsByTopic, writeLlmsTxt } from '../generator/index.js';
9
+ import { showSecurityNotice } from '../auth/notices.js';
9
10
  /**
10
11
  * Read .skryptignore patterns from source directory
11
12
  */
@@ -60,9 +61,23 @@ function shouldExcludeElement(element, patterns) {
60
61
  // Match by name
61
62
  if (pattern.startsWith('name:')) {
62
63
  const namePattern = pattern.slice(5);
63
- if (element.name === namePattern || element.name.match(new RegExp(namePattern))) {
64
+ if (element.name === namePattern) {
64
65
  return true;
65
66
  }
67
+ // Only use regex if the pattern contains regex metacharacters
68
+ // Reject patterns with nested quantifiers to prevent catastrophic backtracking (ReDoS)
69
+ if (/[*+?{}()|[\]\\^$.]/.test(namePattern)) {
70
+ if (/(\+|\*|\?)\{|\(\?[^:)]|\(\.[*+].*\)\+|\([^)]*[+*][^)]*\)[+*]/.test(namePattern)) {
71
+ continue; // Skip patterns prone to catastrophic backtracking
72
+ }
73
+ try {
74
+ if (new RegExp(namePattern).test(element.name))
75
+ return true;
76
+ }
77
+ catch {
78
+ // Invalid regex — treat as literal match (already checked above)
79
+ }
80
+ }
66
81
  }
67
82
  // Match by file path
68
83
  else if (pattern.includes('/') || pattern.includes('*')) {
@@ -112,6 +127,11 @@ export const generateCommand = new Command('generate')
112
127
  if (options.output)
113
128
  config.output.path = options.output;
114
129
  if (options.provider) {
130
+ const validProviders = ['deepseek', 'openai', 'anthropic', 'google', 'ollama', 'openrouter'];
131
+ if (!validProviders.includes(options.provider)) {
132
+ console.error(`Error: Unknown provider "${options.provider}". Valid: ${validProviders.join(', ')}`);
133
+ process.exit(1);
134
+ }
115
135
  config.llm.provider = options.provider;
116
136
  // Use provider's default model unless explicitly specified
117
137
  if (!options.model) {
@@ -137,6 +157,8 @@ export const generateCommand = new Command('generate')
137
157
  process.exit(1);
138
158
  }
139
159
  }
160
+ // First-run security notice
161
+ showSecurityNotice();
140
162
  console.log('skrypt generate');
141
163
  console.log(` source: ${config.source.path}`);
142
164
  console.log(` output: ${config.output.path}`);
@@ -145,6 +167,20 @@ export const generateCommand = new Command('generate')
145
167
  if (config.llm.baseUrl) {
146
168
  console.log(` base url: ${config.llm.baseUrl}`);
147
169
  }
170
+ // Routing transparency
171
+ const providerEnvKeys = {
172
+ openai: 'OPENAI_API_KEY',
173
+ anthropic: 'ANTHROPIC_API_KEY',
174
+ google: 'GOOGLE_API_KEY',
175
+ deepseek: 'DEEPSEEK_API_KEY',
176
+ };
177
+ const providerKey = providerEnvKeys[config.llm.provider];
178
+ if (providerKey && process.env[providerKey]) {
179
+ console.log(` routing: direct to ${config.llm.provider} (BYOK — your key never touches Skrypt)`);
180
+ }
181
+ else if (config.llm.provider !== 'ollama') {
182
+ console.log(' routing: via Skrypt API proxy');
183
+ }
148
184
  console.log('');
149
185
  // Check source exists
150
186
  const sourcePath = resolve(config.source.path);
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare const importCommand: Command;
@@ -0,0 +1,157 @@
1
+ import { Command } from 'commander';
2
+ import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync } from 'fs';
3
+ import { resolve, join, dirname, basename } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { detectFormat, isGitHubUrl, parseGitHubUrl, runImport, importFromGitHub } from '../importers/index.js';
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ export const importCommand = new Command('import')
9
+ .description('Import documentation from another platform')
10
+ .argument('<source>', 'Local directory or GitHub URL')
11
+ .option('--from <format>', 'Source format (mintlify, docusaurus, gitbook, readme, notion, confluence, markdown)')
12
+ .option('-o, --output <dir>', 'Output directory')
13
+ .option('--dry-run', 'Show what would be imported without writing files')
14
+ .option('--name <name>', 'Project name')
15
+ .action(async (source, options) => {
16
+ try {
17
+ console.log('skrypt import\n');
18
+ // Validate --from flag
19
+ const VALID_FORMATS = ['mintlify', 'docusaurus', 'gitbook', 'readme', 'notion', 'confluence', 'markdown'];
20
+ if (options.from && !VALID_FORMATS.includes(options.from)) {
21
+ console.error(` Error: Unknown format "${options.from}". Valid formats: ${VALID_FORMATS.join(', ')}`);
22
+ process.exit(1);
23
+ }
24
+ let result;
25
+ if (isGitHubUrl(source)) {
26
+ // GitHub URL import
27
+ const { owner, repo, path, ref } = parseGitHubUrl(source);
28
+ console.log(` Source: github.com/${owner}/${repo}/${path} (${ref})`);
29
+ result = await importFromGitHub(owner, repo, path, ref, {
30
+ format: options.from,
31
+ name: options.name,
32
+ });
33
+ }
34
+ else {
35
+ // Local directory import
36
+ const sourceDir = resolve(source);
37
+ if (!existsSync(sourceDir)) {
38
+ console.error(` Error: Source directory not found: ${sourceDir}`);
39
+ process.exit(1);
40
+ }
41
+ const format = options.from || detectFormat(sourceDir);
42
+ console.log(` Source: ${sourceDir} (${format})`);
43
+ result = runImport(sourceDir, format, options.name);
44
+ }
45
+ // Determine output directory
46
+ const outputDir = resolve(options.output || `${basename(source).replace(/[^a-zA-Z0-9-_]/g, '-')}-docs`);
47
+ console.log(` Output: ${outputDir}`);
48
+ console.log('');
49
+ // Print migration report
50
+ printReport(result);
51
+ if (options.dryRun) {
52
+ console.log(' [dry run — no files written]\n');
53
+ return;
54
+ }
55
+ // Scaffold output directory
56
+ scaffoldOutput(outputDir, result);
57
+ console.log(' Next steps:');
58
+ console.log(` cd ${outputDir} && npm install && npm run dev\n`);
59
+ }
60
+ catch (err) {
61
+ const message = err instanceof Error ? err.message : String(err);
62
+ console.error(` Error: ${message}`);
63
+ process.exit(1);
64
+ }
65
+ });
66
+ function printReport(result) {
67
+ console.log(' Imported:');
68
+ console.log(` ${result.stats.pages} pages across ${result.stats.groups} groups`);
69
+ const { transforms } = result.stats;
70
+ if (transforms.callouts > 0)
71
+ console.log(` ${transforms.callouts} callouts mapped`);
72
+ if (transforms.tabs > 0)
73
+ console.log(` ${transforms.tabs} tabs mapped`);
74
+ if (transforms.codeGroups > 0)
75
+ console.log(` ${transforms.codeGroups} code groups mapped`);
76
+ if (transforms.steps > 0)
77
+ console.log(` ${transforms.steps} steps mapped`);
78
+ if (transforms.accordions > 0)
79
+ console.log(` ${transforms.accordions} accordions mapped`);
80
+ if (transforms.images > 0)
81
+ console.log(` ${transforms.images} images copied`);
82
+ if (result.warnings.length > 0) {
83
+ console.log('');
84
+ console.log(' Warnings:');
85
+ for (const w of result.warnings.slice(0, 10)) {
86
+ console.log(` - ${w}`);
87
+ }
88
+ if (result.warnings.length > 10) {
89
+ console.log(` ... and ${result.warnings.length - 10} more`);
90
+ }
91
+ }
92
+ console.log('');
93
+ }
94
+ function scaffoldOutput(outputDir, result) {
95
+ // Copy template
96
+ const templateDir = join(__dirname, '..', 'template');
97
+ if (existsSync(templateDir)) {
98
+ mkdirSync(outputDir, { recursive: true });
99
+ cpSync(templateDir, outputDir, { recursive: true });
100
+ }
101
+ else {
102
+ mkdirSync(outputDir, { recursive: true });
103
+ }
104
+ // Write transformed content files
105
+ for (const [filePath, content] of result.files) {
106
+ const outputPath = join(outputDir, filePath);
107
+ mkdirSync(dirname(outputPath), { recursive: true });
108
+ writeFileSync(outputPath, content);
109
+ }
110
+ // Copy assets
111
+ for (const [destPath, srcPath] of result.assets) {
112
+ const outputPath = join(outputDir, destPath);
113
+ mkdirSync(dirname(outputPath), { recursive: true });
114
+ try {
115
+ cpSync(srcPath, outputPath);
116
+ }
117
+ catch {
118
+ // Image may not exist (e.g. CDN URL)
119
+ }
120
+ }
121
+ // Generate docs.json navigation
122
+ const docsJson = {
123
+ name: result.name,
124
+ description: result.description,
125
+ theme: {
126
+ primaryColor: '#3b82f6',
127
+ accentColor: '#8b5cf6',
128
+ font: 'Inter',
129
+ },
130
+ navigation: result.navigation.map(group => ({
131
+ group: group.group,
132
+ pages: group.pages.map(page => ({
133
+ title: page.title,
134
+ path: `/docs/${page.slug}`,
135
+ })),
136
+ })),
137
+ footer: {
138
+ links: [],
139
+ },
140
+ };
141
+ writeFileSync(join(outputDir, 'docs.json'), JSON.stringify(docsJson, null, 2));
142
+ // Update package.json name if template exists
143
+ const pkgPath = join(outputDir, 'package.json');
144
+ if (existsSync(pkgPath)) {
145
+ try {
146
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
147
+ pkg.name = result.name.toLowerCase().replace(/[^a-z0-9-]/g, '-');
148
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
149
+ }
150
+ catch { /* skip */ }
151
+ }
152
+ console.log(` ✓ Wrote ${result.files.size} pages to ${outputDir}`);
153
+ if (result.assets.size > 0) {
154
+ console.log(` ✓ Copied ${result.assets.size} assets`);
155
+ }
156
+ console.log('');
157
+ }
@@ -35,15 +35,27 @@ export const initCommand = new Command('init')
35
35
  cpSync(templateDir, targetDir, { recursive: true });
36
36
  // Update package.json with project name
37
37
  const packageJsonPath = join(targetDir, 'package.json');
38
- const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
39
- packageJson.name = options.name;
40
- writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
38
+ try {
39
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
40
+ packageJson.name = options.name;
41
+ writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
42
+ }
43
+ catch {
44
+ console.error('Error: Template package.json is missing or corrupted. Please reinstall skrypt.');
45
+ process.exit(1);
46
+ }
41
47
  // Update docs.json with project name
42
48
  const docsJsonPath = join(targetDir, 'docs.json');
43
- const docsJson = JSON.parse(readFileSync(docsJsonPath, 'utf-8'));
44
- docsJson.name = options.name;
45
- docsJson.description = `${options.name} documentation`;
46
- writeFileSync(docsJsonPath, JSON.stringify(docsJson, null, 2));
49
+ try {
50
+ const docsJson = JSON.parse(readFileSync(docsJsonPath, 'utf-8'));
51
+ docsJson.name = options.name;
52
+ docsJson.description = `${options.name} documentation`;
53
+ writeFileSync(docsJsonPath, JSON.stringify(docsJson, null, 2));
54
+ }
55
+ catch {
56
+ console.error('Error: Template docs.json is missing or corrupted. Please reinstall skrypt.');
57
+ process.exit(1);
58
+ }
47
59
  console.log('');
48
60
  console.log('Done! Next steps:');
49
61
  console.log('');