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.
- 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/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/lib/search-types.ts +4 -1
- package/dist/template/src/lib/search.ts +30 -7
- package/dist/utils/files.d.ts +9 -1
- package/dist/utils/files.js +59 -10
- package/package.json +4 -1
package/dist/auth/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
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[];
|
package/dist/auth/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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,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
|
+
}
|
package/dist/autofix/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
45
|
-
|
|
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
|
|
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,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
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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('');
|