skrypt-ai 0.4.2 → 0.6.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 +101 -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.d.ts +0 -4
- package/dist/autofix/index.js +10 -24
- package/dist/capture/browser.d.ts +11 -0
- package/dist/capture/browser.js +173 -0
- package/dist/capture/diff.d.ts +23 -0
- package/dist/capture/diff.js +52 -0
- package/dist/capture/index.d.ts +23 -0
- package/dist/capture/index.js +210 -0
- package/dist/capture/naming.d.ts +17 -0
- package/dist/capture/naming.js +45 -0
- package/dist/capture/parser.d.ts +15 -0
- package/dist/capture/parser.js +80 -0
- package/dist/capture/types.d.ts +57 -0
- package/dist/capture/types.js +1 -0
- package/dist/cli.js +20 -3
- package/dist/commands/autofix.js +136 -120
- package/dist/commands/cron.js +58 -47
- package/dist/commands/deploy.js +123 -102
- package/dist/commands/generate.js +125 -7
- package/dist/commands/heal.d.ts +10 -0
- package/dist/commands/heal.js +201 -0
- package/dist/commands/i18n.js +146 -111
- 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/lint.js +50 -44
- package/dist/commands/llms-txt.js +59 -49
- package/dist/commands/login.js +63 -34
- package/dist/commands/mcp.js +6 -0
- package/dist/commands/monitor.js +13 -8
- package/dist/commands/qa.d.ts +2 -0
- package/dist/commands/qa.js +43 -0
- package/dist/commands/review-pr.js +108 -92
- package/dist/commands/sdk.js +128 -122
- package/dist/commands/security.d.ts +2 -0
- package/dist/commands/security.js +109 -0
- package/dist/commands/test.js +91 -92
- package/dist/commands/version.js +104 -75
- package/dist/commands/watch.js +130 -114
- package/dist/config/types.js +2 -2
- package/dist/context-hub/index.d.ts +23 -0
- package/dist/context-hub/index.js +179 -0
- package/dist/context-hub/mappings.d.ts +8 -0
- package/dist/context-hub/mappings.js +55 -0
- package/dist/context-hub/types.d.ts +33 -0
- package/dist/context-hub/types.js +1 -0
- package/dist/generator/generator.js +39 -6
- package/dist/generator/types.d.ts +7 -0
- package/dist/generator/writer.d.ts +3 -1
- package/dist/generator/writer.js +36 -7
- 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/llm/anthropic-client.d.ts +1 -0
- package/dist/llm/anthropic-client.js +3 -1
- package/dist/llm/index.d.ts +6 -4
- package/dist/llm/index.js +76 -261
- package/dist/llm/openai-client.d.ts +1 -0
- package/dist/llm/openai-client.js +7 -2
- package/dist/plugins/index.js +7 -0
- package/dist/qa/checks.d.ts +10 -0
- package/dist/qa/checks.js +492 -0
- package/dist/qa/fixes.d.ts +30 -0
- package/dist/qa/fixes.js +277 -0
- package/dist/qa/index.d.ts +29 -0
- package/dist/qa/index.js +187 -0
- package/dist/qa/types.d.ts +24 -0
- package/dist/qa/types.js +1 -0
- package/dist/scanner/csharp.d.ts +23 -0
- package/dist/scanner/csharp.js +421 -0
- package/dist/scanner/index.js +53 -26
- package/dist/scanner/java.d.ts +39 -0
- package/dist/scanner/java.js +318 -0
- package/dist/scanner/kotlin.d.ts +23 -0
- package/dist/scanner/kotlin.js +389 -0
- package/dist/scanner/php.d.ts +57 -0
- package/dist/scanner/php.js +351 -0
- package/dist/scanner/python.js +17 -0
- package/dist/scanner/ruby.d.ts +36 -0
- package/dist/scanner/ruby.js +431 -0
- package/dist/scanner/swift.d.ts +25 -0
- package/dist/scanner/swift.js +392 -0
- package/dist/scanner/types.d.ts +1 -1
- package/dist/template/content/docs/_navigation.json +46 -0
- package/dist/template/content/docs/_sidebars.json +684 -0
- package/dist/template/content/docs/core.md +4544 -0
- package/dist/template/content/docs/index.mdx +89 -0
- package/dist/template/content/docs/integrations.md +1158 -0
- package/dist/template/content/docs/llms-full.md +403 -0
- package/dist/template/content/docs/llms.txt +4588 -0
- package/dist/template/content/docs/other.md +10379 -0
- package/dist/template/content/docs/tools.md +746 -0
- package/dist/template/content/docs/types.md +531 -0
- package/dist/template/docs.json +13 -11
- package/dist/template/mdx-components.tsx +27 -2
- package/dist/template/package.json +6 -0
- package/dist/template/public/search-index.json +1 -1
- package/dist/template/scripts/build-search-index.mjs +149 -13
- package/dist/template/src/app/api/chat/route.ts +83 -128
- package/dist/template/src/app/docs/[...slug]/page.tsx +75 -20
- package/dist/template/src/app/docs/llms-full.md +151 -4
- package/dist/template/src/app/docs/llms.txt +2464 -847
- package/dist/template/src/app/docs/page.mdx +48 -38
- package/dist/template/src/app/layout.tsx +3 -1
- package/dist/template/src/app/page.tsx +22 -8
- package/dist/template/src/components/ai-chat.tsx +73 -64
- package/dist/template/src/components/breadcrumbs.tsx +21 -23
- package/dist/template/src/components/copy-button.tsx +13 -9
- package/dist/template/src/components/copy-page-button.tsx +54 -0
- package/dist/template/src/components/docs-layout.tsx +37 -25
- package/dist/template/src/components/header.tsx +51 -10
- package/dist/template/src/components/mdx/card.tsx +17 -3
- package/dist/template/src/components/mdx/code-block.tsx +13 -9
- package/dist/template/src/components/mdx/code-group.tsx +13 -8
- package/dist/template/src/components/mdx/heading.tsx +15 -2
- package/dist/template/src/components/mdx/highlighted-code.tsx +13 -8
- package/dist/template/src/components/mdx/index.tsx +2 -0
- package/dist/template/src/components/mdx/mermaid.tsx +110 -0
- package/dist/template/src/components/mdx/screenshot.tsx +150 -0
- package/dist/template/src/components/scroll-to-hash.tsx +48 -0
- package/dist/template/src/components/sidebar.tsx +12 -18
- package/dist/template/src/components/table-of-contents.tsx +9 -0
- package/dist/template/src/lib/highlight.ts +3 -88
- package/dist/template/src/lib/navigation.ts +159 -0
- 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 +17 -6
- package/dist/utils/files.d.ts +9 -1
- package/dist/utils/files.js +59 -10
- package/dist/utils/validation.d.ts +0 -3
- package/dist/utils/validation.js +0 -26
- package/package.json +5 -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,26 @@ 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
|
+
const plan = data.plan === 'pro' ? 'pro' : 'free';
|
|
125
|
+
return {
|
|
126
|
+
valid: data.valid,
|
|
127
|
+
plan,
|
|
128
|
+
email: data.email,
|
|
129
|
+
expiresAt: typeof data.expiresAt === 'string' ? data.expiresAt : undefined,
|
|
130
|
+
error: typeof data.error === 'string' ? data.error : undefined,
|
|
131
|
+
};
|
|
40
132
|
}
|
|
41
133
|
catch {
|
|
42
134
|
return { valid: false, plan: 'free', email: '', error: 'Failed to connect to API' };
|
|
43
135
|
}
|
|
44
136
|
}
|
|
45
137
|
export async function requirePro(commandName) {
|
|
46
|
-
const config =
|
|
138
|
+
const config = await getAuthConfigAsync();
|
|
47
139
|
if (!config.apiKey) {
|
|
48
140
|
console.error(`\n ⚡ ${commandName} requires Skrypt Pro\n`);
|
|
49
141
|
console.error(' Get started:');
|
|
@@ -71,7 +163,7 @@ export async function requirePro(commandName) {
|
|
|
71
163
|
return false;
|
|
72
164
|
}
|
|
73
165
|
// Cache the result for 1 hour
|
|
74
|
-
saveAuthConfig({
|
|
166
|
+
await saveAuthConfig({
|
|
75
167
|
...config,
|
|
76
168
|
plan: result.plan,
|
|
77
169
|
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.d.ts
CHANGED
|
@@ -32,10 +32,6 @@ export interface ValidationResult {
|
|
|
32
32
|
* Auto-fix a code example using LLM
|
|
33
33
|
*/
|
|
34
34
|
export declare function autoFixExample(example: CodeExample, client: LLMClient, options?: AutoFixOptions): Promise<FixResult>;
|
|
35
|
-
/**
|
|
36
|
-
* Batch auto-fix multiple examples
|
|
37
|
-
*/
|
|
38
|
-
export declare function autoFixBatch(examples: CodeExample[], client: LLMClient, options?: AutoFixOptions): Promise<Map<number, FixResult>>;
|
|
39
35
|
/**
|
|
40
36
|
* Create a TypeScript validator using tsc
|
|
41
37
|
*/
|
package/dist/autofix/index.js
CHANGED
|
@@ -137,34 +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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
const example = examples[i];
|
|
155
|
-
if (!example)
|
|
156
|
-
continue;
|
|
157
|
-
console.log(` [${i + 1}/${examples.length}] Fixing ${example.language} example...`);
|
|
158
|
-
const result = await autoFixExample(example, client, options);
|
|
159
|
-
results.set(i, result);
|
|
160
|
-
if (result.success) {
|
|
161
|
-
console.log(` ✓ Fixed in ${result.iterations} iteration(s)`);
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
console.log(` ✗ Could not fix after ${result.iterations} iteration(s)`);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
return results;
|
|
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;
|
|
168
154
|
}
|
|
169
155
|
/**
|
|
170
156
|
* Create a TypeScript validator using tsc
|
|
@@ -212,7 +198,7 @@ export function createPythonValidator() {
|
|
|
212
198
|
const { randomUUID } = await import('crypto');
|
|
213
199
|
// Use crypto.randomUUID() to avoid timestamp collisions
|
|
214
200
|
const tempFile = join(tmpdir(), `autofix_${randomUUID()}.py`);
|
|
215
|
-
writeFileSync(tempFile, code);
|
|
201
|
+
writeFileSync(tempFile, code, { mode: 0o600 });
|
|
216
202
|
try {
|
|
217
203
|
// Use spawnSync with array args to avoid command injection
|
|
218
204
|
const result = spawnSync('python3', ['-m', 'py_compile', tempFile], {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright wrapper for screenshot capture.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imports Playwright (optional dependency).
|
|
5
|
+
* Only allows localhost URLs by default for security.
|
|
6
|
+
*/
|
|
7
|
+
import type { ScreenshotDirective, ScreenshotResult, CaptureOptions } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Capture screenshots for a batch of directives.
|
|
10
|
+
*/
|
|
11
|
+
export declare function captureScreenshots(directives: ScreenshotDirective[], options: CaptureOptions): Promise<ScreenshotResult[]>;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright wrapper for screenshot capture.
|
|
3
|
+
*
|
|
4
|
+
* Dynamically imports Playwright (optional dependency).
|
|
5
|
+
* Only allows localhost URLs by default for security.
|
|
6
|
+
*/
|
|
7
|
+
import { screenshotFilename, darkVariantFilename } from './naming.js';
|
|
8
|
+
import { computeHash } from './diff.js';
|
|
9
|
+
const ALLOWED_HOSTS = new Set(['localhost', '127.0.0.1', '::1']);
|
|
10
|
+
/**
|
|
11
|
+
* Dynamically import Playwright. Exits with clear instructions if not installed.
|
|
12
|
+
*/
|
|
13
|
+
async function ensurePlaywright() {
|
|
14
|
+
try {
|
|
15
|
+
// Dynamic import — playwright is optional
|
|
16
|
+
const mod = await Function('return import("playwright")')();
|
|
17
|
+
return mod;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
throw new Error('Playwright is required for screenshot capture. Install it: npx playwright install chromium');
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validate that a URL is on an allowed host (localhost by default).
|
|
25
|
+
*/
|
|
26
|
+
function validateUrl(url) {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = new URL(url);
|
|
29
|
+
return { valid: ALLOWED_HOSTS.has(parsed.hostname), host: parsed.hostname };
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return { valid: false, host: '' };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Capture screenshots for a batch of directives.
|
|
37
|
+
*/
|
|
38
|
+
export async function captureScreenshots(directives, options) {
|
|
39
|
+
const pw = await ensurePlaywright();
|
|
40
|
+
const results = [];
|
|
41
|
+
const browser = await pw.chromium.launch({ headless: true });
|
|
42
|
+
try {
|
|
43
|
+
// Process with concurrency limit
|
|
44
|
+
const chunks = [];
|
|
45
|
+
for (let i = 0; i < directives.length; i += options.concurrency) {
|
|
46
|
+
chunks.push(directives.slice(i, i + options.concurrency));
|
|
47
|
+
}
|
|
48
|
+
for (const chunk of chunks) {
|
|
49
|
+
const chunkResults = await Promise.all(chunk.map(directive => captureOne(browser, directive, options, pw)));
|
|
50
|
+
results.push(...chunkResults);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
await browser.close();
|
|
55
|
+
}
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Capture a single screenshot.
|
|
60
|
+
*/
|
|
61
|
+
async function captureOne(browser, directive, options, pw) {
|
|
62
|
+
const start = Date.now();
|
|
63
|
+
const filename = screenshotFilename(directive.url, directive.selector);
|
|
64
|
+
// Security check
|
|
65
|
+
const { valid, host } = validateUrl(directive.url);
|
|
66
|
+
if (!valid) {
|
|
67
|
+
return {
|
|
68
|
+
directive,
|
|
69
|
+
filename,
|
|
70
|
+
status: 'failed',
|
|
71
|
+
error: `URL host "${host}" not allowed. Only localhost, 127.0.0.1, and ::1 are permitted.`,
|
|
72
|
+
duration: Date.now() - start,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// Build context options
|
|
76
|
+
const contextOpts = {
|
|
77
|
+
viewport: options.viewport,
|
|
78
|
+
};
|
|
79
|
+
// Device emulation
|
|
80
|
+
if (options.device) {
|
|
81
|
+
const device = pw.devices[options.device];
|
|
82
|
+
if (device) {
|
|
83
|
+
contextOpts.viewport = device.viewport;
|
|
84
|
+
contextOpts.userAgent = device.userAgent;
|
|
85
|
+
contextOpts.deviceScaleFactor = device.deviceScaleFactor;
|
|
86
|
+
contextOpts.isMobile = device.isMobile;
|
|
87
|
+
contextOpts.hasTouch = device.hasTouch;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const context = await browser.newContext(contextOpts);
|
|
91
|
+
const page = await context.newPage();
|
|
92
|
+
try {
|
|
93
|
+
// Navigate
|
|
94
|
+
await page.goto(directive.url, {
|
|
95
|
+
waitUntil: 'networkidle',
|
|
96
|
+
timeout: options.timeout,
|
|
97
|
+
});
|
|
98
|
+
// Wait for settle time
|
|
99
|
+
await page.waitForTimeout(options.wait);
|
|
100
|
+
// Capture light mode screenshot
|
|
101
|
+
let buffer;
|
|
102
|
+
if (directive.selector) {
|
|
103
|
+
const locator = page.locator(directive.selector);
|
|
104
|
+
const visible = await locator.isVisible();
|
|
105
|
+
if (!visible) {
|
|
106
|
+
return {
|
|
107
|
+
directive,
|
|
108
|
+
filename,
|
|
109
|
+
status: 'failed',
|
|
110
|
+
error: `Selector "${directive.selector}" not found or not visible on ${directive.url}`,
|
|
111
|
+
duration: Date.now() - start,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
buffer = await locator.screenshot({ type: 'png' });
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
buffer = await page.screenshot({ fullPage: true, type: 'png' });
|
|
118
|
+
}
|
|
119
|
+
const hash = computeHash(buffer);
|
|
120
|
+
const result = {
|
|
121
|
+
directive,
|
|
122
|
+
filename,
|
|
123
|
+
status: 'captured',
|
|
124
|
+
hash,
|
|
125
|
+
duration: Date.now() - start,
|
|
126
|
+
};
|
|
127
|
+
// Dark mode capture
|
|
128
|
+
if (options.dark) {
|
|
129
|
+
try {
|
|
130
|
+
await page.emulateMedia({ colorScheme: 'dark' });
|
|
131
|
+
await page.evaluate(() => {
|
|
132
|
+
document.documentElement.classList.add('dark');
|
|
133
|
+
});
|
|
134
|
+
await page.waitForTimeout(500);
|
|
135
|
+
let darkBuffer;
|
|
136
|
+
if (directive.selector) {
|
|
137
|
+
darkBuffer = await page.locator(directive.selector).screenshot({ type: 'png' });
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
darkBuffer = await page.screenshot({ fullPage: true, type: 'png' });
|
|
141
|
+
}
|
|
142
|
+
result.darkFilename = darkVariantFilename(filename);
|
|
143
|
+
result._buffers = {
|
|
144
|
+
light: buffer,
|
|
145
|
+
dark: darkBuffer,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
// Dark mode capture failed — continue with light only
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Store light buffer for the orchestrator
|
|
153
|
+
if (!result._buffers) {
|
|
154
|
+
;
|
|
155
|
+
result._buffer = buffer;
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
161
|
+
return {
|
|
162
|
+
directive,
|
|
163
|
+
filename,
|
|
164
|
+
status: 'failed',
|
|
165
|
+
error: message,
|
|
166
|
+
duration: Date.now() - start,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
await page.close();
|
|
171
|
+
await context.close();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-based change detection for screenshots.
|
|
3
|
+
*
|
|
4
|
+
* Stores SHA-256 hashes in .skrypt/screenshot-hashes.json (project root).
|
|
5
|
+
* Used with --diff flag to skip re-capturing unchanged screenshots.
|
|
6
|
+
*/
|
|
7
|
+
export type HashStore = Record<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Compute SHA-256 hash of a buffer.
|
|
10
|
+
*/
|
|
11
|
+
export declare function computeHash(buffer: Buffer): string;
|
|
12
|
+
/**
|
|
13
|
+
* Load the hash store from .skrypt/screenshot-hashes.json in the project directory.
|
|
14
|
+
*/
|
|
15
|
+
export declare function loadHashStore(projectDir: string): HashStore;
|
|
16
|
+
/**
|
|
17
|
+
* Save the hash store to .skrypt/screenshot-hashes.json in the project directory.
|
|
18
|
+
*/
|
|
19
|
+
export declare function saveHashStore(projectDir: string, store: HashStore): void;
|
|
20
|
+
/**
|
|
21
|
+
* Check if a screenshot has changed based on its hash.
|
|
22
|
+
*/
|
|
23
|
+
export declare function hasChanged(filename: string, newHash: string, store: HashStore): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hash-based change detection for screenshots.
|
|
3
|
+
*
|
|
4
|
+
* Stores SHA-256 hashes in .skrypt/screenshot-hashes.json (project root).
|
|
5
|
+
* Used with --diff flag to skip re-capturing unchanged screenshots.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'crypto';
|
|
8
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
const HASH_FILE = 'screenshot-hashes.json';
|
|
11
|
+
const STORE_DIR = '.skrypt';
|
|
12
|
+
/**
|
|
13
|
+
* Compute SHA-256 hash of a buffer.
|
|
14
|
+
*/
|
|
15
|
+
export function computeHash(buffer) {
|
|
16
|
+
return createHash('sha256').update(buffer).digest('hex');
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Load the hash store from .skrypt/screenshot-hashes.json in the project directory.
|
|
20
|
+
*/
|
|
21
|
+
export function loadHashStore(projectDir) {
|
|
22
|
+
const storePath = join(projectDir, STORE_DIR, HASH_FILE);
|
|
23
|
+
if (!existsSync(storePath)) {
|
|
24
|
+
return {};
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const content = readFileSync(storePath, 'utf-8');
|
|
28
|
+
const parsed = JSON.parse(content);
|
|
29
|
+
if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Save the hash store to .skrypt/screenshot-hashes.json in the project directory.
|
|
40
|
+
*/
|
|
41
|
+
export function saveHashStore(projectDir, store) {
|
|
42
|
+
const storeDir = join(projectDir, STORE_DIR);
|
|
43
|
+
mkdirSync(storeDir, { recursive: true });
|
|
44
|
+
const storePath = join(storeDir, HASH_FILE);
|
|
45
|
+
writeFileSync(storePath, JSON.stringify(store, null, 2), 'utf-8');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if a screenshot has changed based on its hash.
|
|
49
|
+
*/
|
|
50
|
+
export function hasChanged(filename, newHash, store) {
|
|
51
|
+
return store[filename] !== newHash;
|
|
52
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot capture orchestrator.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates MDX parsing, browser capture, diff detection, and file output.
|
|
5
|
+
*/
|
|
6
|
+
import type { CaptureOptions, CaptureReport } from './types.js';
|
|
7
|
+
export type { CaptureOptions, CaptureReport } from './types.js';
|
|
8
|
+
/**
|
|
9
|
+
* Run the screenshot capture pipeline.
|
|
10
|
+
*
|
|
11
|
+
* 1. Find all MDX files in docsDir
|
|
12
|
+
* 2. Parse <Screenshot> directives from each
|
|
13
|
+
* 3. Validate base URL reachability
|
|
14
|
+
* 4. Capture screenshots via headless browser
|
|
15
|
+
* 5. Compare hashes for --diff mode
|
|
16
|
+
* 6. Write changed files to outputDir
|
|
17
|
+
* 7. Write manifest and hash store
|
|
18
|
+
*/
|
|
19
|
+
export declare function runCapture(docsDir: string, options: CaptureOptions): Promise<CaptureReport>;
|
|
20
|
+
/**
|
|
21
|
+
* Print a capture report to stdout.
|
|
22
|
+
*/
|
|
23
|
+
export declare function printCaptureReport(report: CaptureReport): void;
|