PluginManager
Manages documentation build plugins
autoFixBatch
Batch-fixes broken code examples
autoFixExample
Repairs broken code examples
checkPlan
Verifies API key and plan
clearAuth
Wipes all stored credentials
createLLMClient
Initializes typed LLM client
createPythonValidator
Validates Python code syntax
createTypeScriptValidator
Validates TypeScript code strings
definePlugin
Defines typed plugin config
fixCodeSample
Auto-repairs broken code samples
generateDocumentation
Generates docs via LLM client
getAuthConfig
Retrieves auth config synchronously
getAuthConfigAsync
Retrieves auth config asynchronously
getKeyStorageMethod
Detects API key storage location
requirePro
Gates commands behind Pro plan
runImport
Imports docs by detected format
saveAuthConfig
Persists auth config to disk
scanDirectory
Scans directory for API elements
scanFile
Extracts metadata from source file
constructor
Initializes plugin manager instance
loadPlugins
Loads plugins from config file
onAfterGenerate
Runs post-generation plugin hooks
onAfterScan
Runs post-scan plugin hooks
onAfterWrite
Runs post-write plugin hooks
onBeforeGenerate
Runs pre-generation plugin hooks
onBeforeScan
Runs pre-scan plugin hooks
onBeforeWrite
Runs pre-write plugin hooks
onInit
Triggers plugin initialization hook
register
Registers plugin with manager
runHook
Executes named hook across plugins
transformContent
Pipes content through plugin chain
PluginManager
class PluginManager
Use this to load, manage, and execute documentation plugins that transform source files into output during the Skrypt build process.
PluginManager orchestrates the full plugin lifecycle — registering plugins, running them in sequence, and providing each plugin with shared context (paths, config, and a logger).
Constructor Parameters
| Name | Type | Required | Description |
|---|---|---|---|
sourcePath | string | Yes | Absolute or relative path to the source files directory |
outputPath | string | Yes | Absolute or relative path where generated docs will be written |
config | Record<string, unknown> | No | Arbitrary config values passed to every plugin via context (defaults to {}) |
Plugin Context
Every registered plugin receives a shared context object:
| Field | Type | Description |
|---|---|---|
sourcePath | string | The source path passed to the constructor |
outputPath | string | The output path passed to the constructor |
config | Record<string, unknown> | The config object passed to the constructor |
logger.info | (msg: string) => void | Logs an info message prefixed with [plugin] |
logger.warn | (msg: string) => void | Logs a warning prefixed with [plugin] |
logger.error | (msg: string) => void | Logs an error prefixed with [plugin] |
Key Behaviors
- Plugins are stored and executed in registration order
- Each plugin receives the same shared
PluginContextinstance - Logger output is prefixed with
[plugin]for easy filtering in build logs - Config defaults to an empty object if not provided, so plugins should handle missing config keys gracefully
Example
// --- Inline types (no external imports needed) ---
type PluginContext = {
sourcePath: string
outputPath: string
config: Record<string, unknown>
logger: {
info: (msg: string) => void
warn: (msg: string) => void
error: (msg: string) => void
}
}
type SkryptPlugin = {
name: string
run: (context: PluginContext) => Promise<void>
}
// --- Self-contained PluginManager implementation ---
class PluginManager {
private plugins: SkryptPlugin[] = []
private context: PluginContext
constructor(
sourcePath: string,
outputPath: string,
config: Record<string, unknown> = {}
) {
this.context = {
sourcePath,
outputPath,
config,
logger: {
info: (msg) => console.log(`[plugin] INFO: ${msg}`),
warn: (msg) => console.log(`[plugin] WARN: ${msg}`),
error: (msg) => console.log(`[plugin] ERROR: ${msg}`),
},
}
}
register(plugin: SkryptPlugin): void {
this.plugins.push(plugin)
this.context.logger.info(`Registered plugin: ${plugin.name}`)
}
async runAll(): Promise<void> {
for (const plugin of this.plugins) {
this.context.logger.info(`Running plugin: ${plugin.name}`)
await plugin.run(this.context)
}
}
}
// --- Example plugins ---
const markdownPlugin: SkryptPlugin = {
name: 'markdown-renderer',
run: async (ctx) => {
const outputFormat = ctx.config.outputFormat ?? 'markdown'
ctx.logger.info(`Rendering docs from "${ctx.sourcePath}" as ${outputFormat}`)
// Simulate async file processing
await new Promise((res) => setTimeout(res, 10))
ctx.logger.info(`Wrote output to "${ctx.outputPath}"`)
},
}
const lintPlugin: SkryptPlugin = {
name: 'doc-linter',
run: async (ctx) => {
const strict = ctx.config.strict ?? false
ctx.logger.info(`Linting docs (strict=${strict})`)
if (!strict) {
ctx.logger.warn('Strict mode is off — some issues may be skipped')
}
},
}
// --- Main usage ---
async function main() {
try {
const manager = new PluginManager(
'./src',
'./docs/output',
{
outputFormat: 'html',
strict: true,
}
)
manager.register(markdownPlugin)
manager.register(lintPlugin)
await manager.runAll()
console.log('\n✅ All plugins completed successfully.')
// Expected output:
// [plugin] INFO: Registered plugin: markdown-renderer
// [plugin] INFO: Registered plugin: doc-linter
// [plugin] INFO: Running plugin: markdown-renderer
// [plugin] INFO: Rendering docs from "./src" as html
// [plugin] INFO: Wrote output to "./docs/output"
// [plugin] INFO: Running plugin: doc-linter
// [plugin] INFO: Linting docs (strict=true)
// ✅ All plugins completed successfully.
} catch (error) {
console.error('Plugin pipeline failed:', error)
process.exit(1)
}
}
main()
Related
autoFixBatch
async function autoFixBatch(examples: CodeExample[], client: LLMClient, options: AutoFixOptions = {}): Promise<Map<number, FixResult>>
Use this to automatically fix multiple broken code examples in a single batch operation, getting back a map of results indexed by position.
This is the batch version of autoFix — ideal when you have a collection of code snippets that need validation and repair, such as processing documentation examples, test fixtures, or user-submitted code samples all at once.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
examples | CodeExample[] | ✅ | Array of code examples to fix. Each example contains the code string and metadata. |
client | LLMClient | ✅ | LLM client instance used to generate fixes for broken examples. |
options | AutoFixOptions | ❌ | Configuration options such as max retry attempts, timeout, and fix strategy. Defaults to {}. |
Returns
Returns Promise<Map<number, FixResult>> — a Map where:
- Key: the index of the example in the input array (0-based)
- Value: a
FixResultobject containing:success: boolean— whether the fix was applied successfullyfixedCode?: string— the repaired code (present whensuccessistrue)error?: string— description of what went wrong (present whensuccessisfalse)attempts?: number— how many fix attempts were made
Examples that could not be fixed will still appear in the map with success: false, so you can always check all inputs by iterating the original array indices.
Example
// ─── Inline types (do NOT import from skrypt) ───────────────────────────────
type CodeExample = {
code: string
language?: string
filename?: string
}
type FixResult = {
success: boolean
fixedCode?: string
error?: string
attempts?: number
}
type AutoFixOptions = {
maxAttempts?: number
timeoutMs?: number
verbose?: boolean
}
type LLMClient = {
complete: (prompt: string) => Promise<string>
}
// ─── Simulated autoFixBatch implementation ────────────────────────────────────
async function autoFixBatch(
examples: CodeExample[],
client: LLMClient,
options: AutoFixOptions = {}
): Promise<Map<number, FixResult>> {
const { maxAttempts = 3, verbose = false } = options
const results = new Map<number, FixResult>()
for (let i = 0; i < examples.length; i++) {
const example = examples[i]
// Simulate a basic syntax check (real impl would use TypeScript compiler)
const hasSyntaxError = example.code.includes('???') || example.code.includes('BROKEN')
if (!hasSyntaxError) {
results.set(i, {
success: true,
fixedCode: example.code,
attempts: 1,
})
if (verbose) console.log(`[${i}] ✅ No fix needed`)
continue
}
try {
// Ask the LLM to fix the broken code
const prompt = `Fix this broken ${example.language ?? 'TypeScript'} code:\n\n${example.code}`
const fixedCode = await client.complete(prompt)
results.set(i, {
success: true,
fixedCode,
attempts: maxAttempts,
})
if (verbose) console.log(`[${i}] 🔧 Fixed after ${maxAttempts} attempt(s)`)
} catch (err) {
results.set(i, {
success: false,
error: err instanceof Error ? err.message : 'Unknown error',
attempts: maxAttempts,
})
if (verbose) console.log(`[${i}] ❌ Could not fix`)
}
}
return results
}
// ─── Mock LLM client ──────────────────────────────────────────────────────────
function createMockLLMClient(apiKey: string): LLMClient {
return {
complete: async (prompt: string): Promise<string> => {
// Simulate network latency
await new Promise((r) => setTimeout(r, 50))
// Simulate the LLM returning a corrected snippet
if (prompt.includes('BROKEN')) {
return `const greet = (name: string): string => \`Hello, \${name}!\``
}
throw new Error('LLM could not determine a fix')
},
}
}
// ─── Main usage ───────────────────────────────────────────────────────────────
const examples: CodeExample[] = [
{
// Valid example — should pass through unchanged
code: `const add = (a: number, b: number): number => a + b`,
language: 'typescript',
filename: 'math.ts',
},
{
// Broken example — LLM will fix it
code: `const greet = BROKEN (name: string) => \`Hello \${name}\``,
language: 'typescript',
filename: 'greet.ts',
},
{
// Unfixable example — LLM will throw
code: `const mystery = ???`,
language: 'typescript',
filename: 'mystery.ts',
},
]
async function main() {
const apiKey = process.env.LLM_API_KEY || 'sk-demo-key-12345'
const client = createMockLLMClient(apiKey)
try {
const results = await autoFixBatch(examples, client, {
maxAttempts: 2,
verbose: true,
})
console.log('\n── Batch Fix Results ──────────────────────────────')
for (const [index, result] of results) {
const label = examples[index].filename ?? `example[${index}]`
if (result.success) {
console.log(`\n✅ ${label} (${result.attempts} attempt(s))`)
console.log(' Fixed code:', result.fixedCode)
} else {
console.log(`\n❌ ${label} — ${result.error}`)
}
}
// Summarise
const successCount = [...results.values()].filter((r) => r.success).length
console.log(`\n── Summary: ${successCount}/${examples.length} fixed successfully ──`)
// Expected output:
// [0] ✅ No fix needed
// [1] 🔧 Fixed after 2 attempt(s)
// [2] ❌ Could not fix
//
// ✅ math.ts (1 attempt(s)) → original code unchanged
// ✅ greet.ts (2 attempt(s)) → LLM-repaired code
// ❌ mystery.ts → LLM could not determine a fix
//
// Summary: 2/3 fixed successfully
} catch (error) {
console.error('Batch fix failed unexpectedly:', error)
process.exit(1)
}
}
main()
autoFixExample
async function autoFixExample(example: CodeExample, client: LLMClient, options: AutoFixOptions = {}): Promise<FixResult>
Use this to automatically repair broken or invalid code examples by iteratively applying LLM-powered fixes until the code compiles and runs correctly.
This function takes a code example that may contain errors, submits it to an LLM client for analysis and correction, and retries up to a configurable number of times — returning the fixed code along with metadata about what changed and how many attempts it took.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
example | CodeExample | ✅ | The code example to fix, including source code, language, and any known error context |
client | LLMClient | ✅ | An LLM client instance used to generate fixes (e.g., OpenAI, Anthropic wrapper) |
options | AutoFixOptions | ❌ | Configuration options such as maxIterations (default: 3) to control retry behavior |
Returns
Returns a Promise<FixResult> that resolves with:
| Field | Type | Description |
|---|---|---|
success | boolean | Whether the example was successfully fixed |
fixedCode | string | null | The corrected source code, or null if fixing failed |
iterations | number | How many LLM fix attempts were made |
changes | string[] | A list of descriptions of what was changed |
error | string | undefined | Error message if fixing ultimately failed after all iterations |
Example
// ---- Inline type definitions (no external imports needed) ----
type CodeExample = {
code: string
language: string
errorMessage?: string
}
type AutoFixOptions = {
maxIterations?: number
verbose?: boolean
}
type FixResult = {
success: boolean
fixedCode: string | null
iterations: number
changes: string[]
error?: string
}
type LLMClient = {
complete: (prompt: string) => Promise<string>
}
// ---- Simulated autoFixExample implementation ----
async function autoFixExample(
example: CodeExample,
client: LLMClient,
options: AutoFixOptions = {}
): Promise<FixResult> {
const maxIterations = options.maxIterations ?? 3
const changes: string[] = []
let currentCode = example.code
let lastError = example.errorMessage
for (let i = 0; i < maxIterations; i++) {
const prompt = [
`Fix the following ${example.language} code.`,
lastError ? `Error: ${lastError}` : '',
`Code:\n${currentCode}`,
'Return only the corrected code, no explanation.',
]
.filter(Boolean)
.join('\n')
try {
const response = await client.complete(prompt)
if (options.verbose) {
console.log(`[Iteration ${i + 1}] LLM response received`)
}
// Simulate detecting a change
if (response !== currentCode) {
changes.push(`Iteration ${i + 1}: Applied LLM suggestion`)
currentCode = response
lastError = undefined // Assume fixed after each iteration
}
// Simulate a basic validation check (replace with real TS compile check)
const hasObviousError = currentCode.includes('SYNTAX_ERROR')
if (!hasObviousError) {
return {
success: true,
fixedCode: currentCode,
iterations: i + 1,
changes,
}
}
} catch (err) {
return {
success: false,
fixedCode: null,
iterations: i + 1,
changes,
error: err instanceof Error ? err.message : String(err),
}
}
}
return {
success: false,
fixedCode: null,
iterations: maxIterations,
changes,
error: `Could not fix example after ${maxIterations} iterations`,
}
}
// ---- Simulated LLM client ----
function createMockLLMClient(apiKey: string): LLMClient {
return {
complete: async (prompt: string): Promise<string> => {
// Simulate network latency
await new Promise((resolve) => setTimeout(resolve, 50))
// Simulate fixing a missing semicolon and undefined variable
if (prompt.includes('const x = undefinedVar')) {
return `const x = 42;\nconsole.log(x);`
}
return prompt.split('Code:\n')[1] ?? ''
},
}
}
// ---- Main usage ----
const brokenExample: CodeExample = {
code: `const x = undefinedVar\nconsole.log(x)`,
language: 'typescript',
errorMessage: "ReferenceError: undefinedVar is not defined",
}
async function main() {
const client = createMockLLMClient(
process.env.LLM_API_KEY || 'sk-your-api-key-here'
)
try {
const result = await autoFixExample(brokenExample, client, {
maxIterations: 3,
verbose: true,
})
if (result.success) {
console.log('✅ Fix succeeded!')
console.log('Fixed code:\n', result.fixedCode)
console.log('Iterations used:', result.iterations)
console.log('Changes applied:', result.changes)
// Output:
// ✅ Fix succeeded!
// Fixed code:
// const x = 42;
// console.log(x);
// Iterations used: 1
// Changes applied: [ 'Iteration 1: Applied LLM suggestion' ]
} else {
console.warn('❌ Could not fix example:', result.error)
console.log('Attempts made:', result.iterations)
}
} catch (error) {
console.error('Unexpected failure during autoFixExample:', error)
}
}
main()
checkPlan
async function checkPlan(apiKey: string): Promise<PlanCheckResponse>
Use this to verify an API key's validity and retrieve the associated subscription plan details before making API calls or gating features by plan tier.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | The Bearer token used to authenticate against the plan endpoint |
Returns
Returns a Promise<PlanCheckResponse> that resolves with the plan details for the provided API key.
| Scenario | Result |
|---|---|
| Valid API key | Resolves with plan info (e.g., tier, limits, expiry) |
| Invalid / expired key | Rejects or resolves with an error/unauthorized response |
| Network failure | Rejects with a fetch error |
Example
// Inline types — no external imports needed
type PlanCheckResponse = {
plan: 'free' | 'pro' | 'enterprise'
valid: boolean
requestsPerMonth: number
expiresAt: string | null
error?: string
}
// Simulated implementation matching the real function's behavior
async function checkPlan(apiKey: string): Promise<PlanCheckResponse> {
const API_BASE = 'https://api.supermemory.ai'
const response = await fetch(`${API_BASE}/v1/plan`, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
})
if (!response.ok) {
throw new Error(`Plan check failed: ${response.status} ${response.statusText}`)
}
return response.json() as Promise<PlanCheckResponse>
}
// --- Usage ---
async function main() {
const apiKey = process.env.SUPERMEMORY_API_KEY || 'sm_your_api_key_here'
try {
const planInfo = await checkPlan(apiKey)
if (!planInfo.valid) {
console.error('API key is invalid or expired.')
process.exit(1)
}
console.log('Plan details:', planInfo)
// Expected output:
// Plan details: {
// plan: 'pro',
// valid: true,
// requestsPerMonth: 50000,
// expiresAt: '2025-12-31T00:00:00.000Z'
// }
// Gate features by plan tier
if (planInfo.plan === 'enterprise') {
console.log('Enterprise features unlocked.')
} else if (planInfo.plan === 'pro') {
console.log(`Pro plan active — ${planInfo.requestsPerMonth.toLocaleString()} requests/month.`)
} else {
console.log('Free tier — consider upgrading for higher limits.')
}
} catch (error) {
console.error('Failed to check plan:', error instanceof Error ? error.message : error)
// Handle invalid keys or network issues gracefully
}
}
main()
clearAuth
async function clearAuth(): Promise<void>
Use this to completely sign out a user by wiping all stored authentication credentials — both from the system keychain and any local auth config file.
This is the "logout" operation. It removes credentials from two places:
- System keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager)
- Local auth file on disk (typically
~/.config/yourapp/auth.jsonor similar)
After calling this, any subsequent authenticated requests will fail until the user logs in again.
Parameters
This function takes no parameters.
Returns
| Type | Description |
|---|---|
Promise<void> | Resolves when all credentials have been cleared. Rejects if a critical error occurs during cleanup. |
Note: If the auth file does not exist, the function completes silently without error — making it safe to call even when the user is already logged out.
Example
import { existsSync, unlinkSync, writeFileSync, mkdirSync } from 'fs'
import { join } from 'path'
import { homedir } from 'os'
// --- Inline simulation of keychain operations ---
const inMemoryKeychain: Record<string, string> = {
'myapp/auth': 'super-secret-token-abc123'
}
async function keychainDelete(): Promise<void> {
delete inMemoryKeychain['myapp/auth']
console.log('[keychain] Credentials removed from system keychain')
}
// --- Inline simulation of clearAuth ---
const AUTH_FILE = join(homedir(), '.config', 'myapp-example', 'auth.json')
async function clearAuth(): Promise<void> {
// Step 1: Remove from system keychain
await keychainDelete()
// Step 2: Remove local auth file if it exists
if (existsSync(AUTH_FILE)) {
try {
unlinkSync(AUTH_FILE)
console.log(`[auth] Removed auth file: ${AUTH_FILE}`)
} catch (err) {
console.warn(`[auth] Could not remove auth file: ${(err as Error).message}`)
}
} else {
console.log('[auth] No auth file found — nothing to remove')
}
}
// --- Setup: write a fake auth file to demonstrate deletion ---
function setupFakeAuthFile(): void {
const dir = join(homedir(), '.config', 'myapp-example')
mkdirSync(dir, { recursive: true })
writeFileSync(
AUTH_FILE,
JSON.stringify({ token: 'super-secret-token-abc123', userId: 'usr_9f3kd82' }, null, 2),
{ mode: 0o600 }
)
console.log(`[setup] Created fake auth file at: ${AUTH_FILE}`)
}
// --- Main: demonstrate clearAuth ---
async function main() {
try {
setupFakeAuthFile()
console.log('\nBefore clearAuth:')
console.log(' Auth file exists:', existsSync(AUTH_FILE))
console.log(' Keychain entry exists:', 'myapp/auth' in inMemoryKeychain)
console.log('\nCalling clearAuth()...')
await clearAuth()
console.log('\nAfter clearAuth:')
console.log(' Auth file exists:', existsSync(AUTH_FILE))
console.log(' Keychain entry exists:', 'myapp/auth' in inMemoryKeychain)
// Expected output:
// Before clearAuth:
// Auth file exists: true
// Keychain entry exists: true
// Calling clearAuth()...
// [keychain] Credentials removed from system keychain
// [auth] Removed auth file: /home/user/.config/myapp-example/auth.json
// After clearAuth:
// Auth file exists: false
// Keychain entry exists: false
} catch (error) {
console.error('Failed to clear auth:', (error as Error).message)
process.exit(1)
}
}
main()
Related
createLLMClient
function createLLMClient(config: {
provider: LLMProvider
model: string
baseUrl?: string
timeout?: number
maxRetries?: number
}): LLMClient
Use this to initialize a typed LLM client for a specific AI provider (OpenAI, Anthropic, etc.) with built-in retry and timeout handling.
This is your entry point for all LLM interactions — call it once at startup, then reuse the returned client across your application to send prompts and receive completions.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
config.provider | LLMProvider | ✅ Yes | The AI provider to use. Accepted values: "openai", "anthropic", "azure" |
config.model | string | ✅ Yes | The model identifier to target (e.g. "gpt-4o", "claude-3-5-sonnet-20241022") |
config.baseUrl | string | No | Override the default API endpoint. Useful for proxies, local models, or Azure deployments |
config.timeout | number | No | Request timeout in milliseconds. Defaults to 30000 (30s) |
config.maxRetries | number | No | Number of times to retry on transient failures. Defaults to 2 |
Returns
Returns an LLMClient instance with methods to send chat/completion requests to the configured provider. The client is pre-configured with your credentials (read from environment variables), timeout, and retry logic.
Credentials: The client automatically reads the appropriate API key from your environment (e.g.
OPENAI_API_KEY,ANTHROPIC_API_KEY) based on theprovidervalue.
Example
// --- Inline types (mirrors the real library's shape) ---
type LLMProvider = "openai" | "anthropic" | "azure"
interface Message {
role: "user" | "assistant" | "system"
content: string
}
interface CompletionResponse {
id: string
content: string
model: string
usage: { promptTokens: number; completionTokens: number; totalTokens: number }
}
interface LLMClient {
complete(messages: Message[]): Promise<CompletionResponse>
provider: LLMProvider
model: string
}
interface LLMClientConfig {
provider: LLMProvider
model: string
baseUrl?: string
timeout?: number
maxRetries?: number
}
// --- Simulated createLLMClient (self-contained, no external imports) ---
function createLLMClient(config: LLMClientConfig): LLMClient {
const { provider, model, timeout = 30000, maxRetries = 2 } = config
const apiKeyMap: Record<LLMProvider, string> = {
openai: process.env.OPENAI_API_KEY || "sk-your-openai-key",
anthropic: process.env.ANTHROPIC_API_KEY || "sk-ant-your-anthropic-key",
azure: process.env.AZURE_OPENAI_API_KEY || "your-azure-key",
}
const apiKey = apiKeyMap[provider]
if (!apiKey) {
throw new Error(`No API key found for provider: ${provider}`)
}
console.log(`[createLLMClient] Initialized ${provider} client`)
console.log(` Model: ${model}`)
console.log(` Base URL: ${config.baseUrl ?? "(default)"}`)
console.log(` Timeout: ${timeout}ms`)
console.log(` Max Retries: ${maxRetries}`)
return {
provider,
model,
async complete(messages: Message[]): Promise<CompletionResponse> {
// Simulated response — real client would call the provider's API here
return {
id: "chatcmpl-abc123",
content: `Simulated response from ${provider}/${model} to: "${messages.at(-1)?.content}"`,
model,
usage: { promptTokens: 12, completionTokens: 28, totalTokens: 40 },
}
},
}
}
// --- Usage ---
async function main() {
try {
// Basic OpenAI setup
const openaiClient = createLLMClient({
provider: "openai",
model: "gpt-4o",
timeout: 15000,
maxRetries: 3,
})
const response = await openaiClient.complete([
{ role: "system", content: "You are a helpful assistant." },
{ role: "user", content: "What is the capital of France?" },
])
console.log("\n--- Completion Response ---")
console.log("ID: ", response.id)
console.log("Content: ", response.content)
console.log("Tokens: ", response.usage.totalTokens)
// Output:
// ID: chatcmpl-abc123
// Content: Simulated response from openai/gpt-4o to: "What is the capital of France?"
// Tokens: 40
// Anthropic with a custom proxy base URL
const anthropicClient = createLLMClient({
provider: "anthropic",
model: "claude-3-5-sonnet-20241022",
baseUrl: "https://my-proxy.internal/anthropic",
timeout: 20000,
})
console.log(`\nAnthropic client ready: ${anthropicClient.provider}/${anthropicClient.model}`)
// Output: Anthropic client ready: anthropic/claude-3-5-sonnet-20241022
} catch (error) {
console.error("Failed to create or use LLM client:", error)
process.exit(1)
}
}
main()
createPythonValidator
function createPythonValidator(): (code: string) => Promise<ValidationResult>
Use this to validate Python code syntax before executing or storing it — catches syntax errors without running the code.
createPythonValidator returns a reusable validator function that writes code to a temporary file and checks it with python3 -m py_compile. Requires python3 to be available in your system PATH.
Parameters
The factory function takes no parameters. The returned validator accepts:
| Name | Type | Required | Description |
|---|---|---|---|
code | string | ✅ | The Python source code string to validate |
Returns
createPythonValidator() → (code: string) => Promise<ValidationResult>
Returns a validator function. Each call to that validator returns a Promise<ValidationResult>:
| Scenario | Result shape |
|---|---|
| Valid Python syntax | { valid: true, errors: [] } |
| Syntax error detected | { valid: false, errors: [{ message: string, line?: number }] } |
python3 not found in PATH | { valid: false, errors: [{ message: 'python3 not found...' }] } |
Requirement:
python3must be installed and accessible viaPATH. The validator usespython3 -m py_compileinternally and cleans up any temporary files after each check.
Example
import { spawnSync } from 'child_process'
import { writeFileSync, unlinkSync } from 'fs'
import { join } from 'path'
import { tmpdir } from 'os'
// Inline the ValidationResult type
type ValidationError = {
message: string
line?: number
}
type ValidationResult = {
valid: boolean
errors: ValidationError[]
}
// Self-contained implementation of createPythonValidator
function createPythonValidator(): (code: string) => Promise<ValidationResult> {
return async (code: string): Promise<ValidationResult> => {
const tmpFile = join(tmpdir(), `py_validate_${Date.now()}.py`)
try {
writeFileSync(tmpFile, code, 'utf8')
const result = spawnSync('python3', ['-m', 'py_compile', tmpFile], {
encoding: 'utf8',
timeout: 10_000,
})
if (result.error) {
// python3 binary not found or failed to spawn
return {
valid: false,
errors: [{ message: `python3 not found or failed to run: ${result.error.message}` }],
}
}
if (result.status !== 0) {
// Parse error output like: " File "/tmp/py_validate.py", line 2\n ..."
const stderr = result.stderr || ''
const lineMatch = stderr.match(/line (\d+)/)
const line = lineMatch ? parseInt(lineMatch[1], 10) : undefined
// Strip the temp file path from the message for cleaner output
const cleanMessage = stderr.replace(tmpFile, '<code>').trim()
return {
valid: false,
errors: [{ message: cleanMessage, line }],
}
}
return { valid: true, errors: [] }
} finally {
// Always clean up the temp file
try {
unlinkSync(tmpFile)
} catch {
// Ignore cleanup errors
}
}
}
}
// --- Usage Example ---
async function main() {
const validate = createPythonValidator()
// ✅ Valid Python code
const validCode = `
def greet(name: str) -> str:
return f"Hello, {name}!"
print(greet("world"))
`.trim()
const validResult = await validate(validCode)
console.log('Valid code result:', validResult)
// Output: { valid: true, errors: [] }
// ❌ Invalid Python code (missing colon after def)
const invalidCode = `
def greet(name)
return "Hello, " + name
`.trim()
const invalidResult = await validate(invalidCode)
console.log('Invalid code result:', invalidResult)
// Output: { valid: false, errors: [{ message: "...: invalid syntax (<code>, line 1)", line: 1 }] }
// Reuse the same validator for multiple checks (efficient)
const snippets = [
'x = [i**2 for i in range(10)]', // valid
'if True print("oops")', // invalid - missing colon
'import os\nprint(os.getcwd())', // valid
]
console.log('\nBatch validation:')
for (const snippet of snippets) {
const { valid, errors } = await validate(snippet)
console.log(` "${snippet.slice(0, 30)}..." → ${valid ? '✅ valid' : `❌ ${errors[0]?.message}`}`)
}
}
main().catch(console.error)
createTypeScriptValidator
function createTypeScriptValidator(): (code: string) => Promise<ValidationResult>
Use this to validate TypeScript code strings at runtime — perfect for checking LLM-generated code, user-submitted snippets, or dynamically built TypeScript before execution.
Returns a validator function you call with any TypeScript code string. The validator uses the TypeScript compiler (tsc) under the hood to catch type errors, syntax issues, and compilation failures.
Parameters
This factory function takes no parameters.
Returns
| Value | Type | Description |
|---|---|---|
validator | (code: string) => Promise<ValidationResult> | A reusable async function that accepts a TypeScript code string and resolves to a ValidationResult |
ValidationResult Shape
| Field | Type | Description |
|---|---|---|
valid | boolean | true if the code compiled without errors |
errors | string[] | List of compiler error messages; empty array when valid is true |
Behavior
- Valid code → resolves with
{ valid: true, errors: [] } - Invalid code → resolves with
{ valid: false, errors: ["TS2345: ...", ...] } - Runtime failure (e.g.
typescriptnot installed) → resolves with{ valid: false, errors: ["<internal error message>"] }
Tip: Call
createTypeScriptValidator()once and reuse the returned function across multiple validations to avoid repeated setup overhead.
Example
import { transpileModule, type TranspileOptions } from 'typescript'
// --- Inline types (mirrors the real ValidationResult) ---
interface ValidationResult {
valid: boolean
errors: string[]
}
// --- Self-contained implementation mirroring the real function ---
function createTypeScriptValidator(): (code: string) => Promise<ValidationResult> {
return async (code: string): Promise<ValidationResult> => {
try {
const options: TranspileOptions = {
compilerOptions: {
strict: true,
noImplicitAny: true,
},
reportDiagnostics: true,
}
const result = transpileModule(code, options)
const errors =
result.diagnostics
?.filter((d) => d.messageText)
.map((d) =>
typeof d.messageText === 'string'
? d.messageText
: d.messageText.messageText
) ?? []
return {
valid: errors.length === 0,
errors,
}
} catch (error) {
return {
valid: false,
errors: [error instanceof Error ? error.message : String(error)],
}
}
}
}
// --- Usage ---
const validateTypeScript = createTypeScriptValidator()
async function main() {
try {
// ✅ Valid TypeScript
const validCode = `
const greet = (name: string): string => {
return \`Hello, \${name}!\`
}
console.log(greet('world'))
`
const validResult = await validateTypeScript(validCode)
console.log('Valid code result:', validResult)
// Output: { valid: true, errors: [] }
// ❌ Invalid TypeScript — wrong argument type
const invalidCode = `
const add = (a: number, b: number): number => a + b
const result: number = add("not", "numbers")
`
const invalidResult = await validateTypeScript(invalidCode)
console.log('Invalid code result:', invalidResult)
// Output: { valid: false, errors: ["Argument of type 'string' is not assignable..."] }
// ❌ Syntax error
const brokenCode = `
function broken( {
return 42
`
const brokenResult = await validateTypeScript(brokenCode)
console.log('Broken code result:', brokenResult)
// Output: { valid: false, errors: ["',' expected.", ...] }
} catch (error) {
console.error('Unexpected failure:', error)
}
}
main()
definePlugin
function definePlugin(plugin: SkryptPlugin): SkryptPlugin
Use this to define a typed plugin configuration object for the Skrypt plugin system with full type safety and IDE autocompletion.
This is an identity function that acts as a type helper — it takes your plugin definition and returns it unchanged, but ensures your object conforms to the SkryptPlugin interface at compile time. Think of it as the Skrypt equivalent of Vue's defineComponent or Vite's defineConfig.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
plugin | SkryptPlugin | ✅ | The plugin configuration object to validate and return |
Returns
Returns the same SkryptPlugin object passed in, unchanged. The value is identical to the input — the benefit is purely type-level validation and editor tooling support.
When to use this vs a plain object literal:
- Use
definePlugin(...)when you want TypeScript to catch missing/invalid fields at the definition site - Use
definePlugin(...)when you want autocomplete on plugin properties in your editor - A plain object works at runtime, but you lose type checking on the shape
Example
// Inline the SkryptPlugin type (do not import from skrypt)
type PluginHookContext = {
cwd: string
config: Record<string, unknown>
}
type SkryptPlugin = {
name: string
version?: string
description?: string
setup?: (context: PluginHookContext) => void | Promise<void>
teardown?: () => void | Promise<void>
hooks?: {
beforeBuild?: () => void | Promise<void>
afterBuild?: () => void | Promise<void>
}
}
// Identity function — validates shape at compile time, returns plugin unchanged
function definePlugin(plugin: SkryptPlugin): SkryptPlugin {
return plugin
}
// --- Define a plugin using the helper ---
const myAnalyticsPlugin = definePlugin({
name: 'analytics-reporter',
version: '1.2.0',
description: 'Reports build metrics to an analytics endpoint',
setup: async (context) => {
console.log(`[analytics-reporter] Initializing in: ${context.cwd}`)
console.log(`[analytics-reporter] Config:`, context.config)
},
hooks: {
beforeBuild: async () => {
console.log('[analytics-reporter] Build starting — recording timestamp')
},
afterBuild: async () => {
const endpoint = process.env.ANALYTICS_URL || 'https://analytics.example.com/build'
console.log(`[analytics-reporter] Sending metrics to ${endpoint}`)
},
},
teardown: async () => {
console.log('[analytics-reporter] Cleaning up resources')
},
})
// --- Simulate plugin lifecycle ---
async function runPluginLifecycle(plugin: SkryptPlugin) {
try {
console.log(`\nLoading plugin: "${plugin.name}" v${plugin.version ?? 'unknown'}`)
console.log(`Description: ${plugin.description ?? 'N/A'}`)
const context: PluginHookContext = {
cwd: process.cwd(),
config: { outputDir: 'dist', minify: true },
}
await plugin.setup?.(context)
await plugin.hooks?.beforeBuild?.()
await plugin.hooks?.afterBuild?.()
await plugin.teardown?.()
console.log(`\nPlugin "${plugin.name}" lifecycle complete.`)
// Output:
// Loading plugin: "analytics-reporter" v1.2.0
// Description: Reports build metrics to an analytics endpoint
// [analytics-reporter] Initializing in: /your/project
// [analytics-reporter] Config: { outputDir: 'dist', minify: true }
// [analytics-reporter] Build starting — recording timestamp
// [analytics-reporter] Sending metrics to https://analytics.example.com/build
// [analytics-reporter] Cleaning up resources
// Plugin "analytics-reporter" lifecycle complete.
} catch (error) {
console.error(`Plugin "${plugin.name}" failed:`, error)
}
}
runPluginLifecycle(myAnalyticsPlugin)
fixCodeSample
async function fixCodeSample(client: LLMClient, code: string, error: string, context: string, iteration: number = 1): Promise<string>
Use this to automatically repair broken code samples by sending them to an LLM with the error context and receiving a corrected version. Ideal for documentation pipelines, code generation workflows, or CI systems that validate and self-heal code examples.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
client | LLMClient | ✅ | An initialized LLM client instance used to generate the fix |
code | string | ✅ | The broken code sample that needs to be repaired |
error | string | ✅ | The error message or stack trace produced by the broken code |
context | string | ✅ | Additional context about what the code is supposed to do (e.g., function docs, expected behavior) |
iteration | number | ❌ | Which fix attempt this is (default: 1). Used to apply progressively smarter strategies on repeated failures |
Returns
Returns a Promise<string> containing the corrected code sample. The returned string is the fixed code only — ready to replace the original broken sample.
Notes
- The
iterationparameter enables escalating fix strategies: pass2or3on retry loops to signal that simpler fixes have already been attempted - The
contextparameter significantly improves fix quality — include the function signature, docstring, and expected output - The returned code may still need validation; use this inside a retry loop with a code runner for best results
Example
// ── Inline types (do not import from skrypt) ──────────────────────────────
interface LLMMessage {
role: 'system' | 'user' | 'assistant'
content: string
}
interface LLMClient {
complete(messages: LLMMessage[]): Promise<string>
}
// ── Simulated LLM client (replace with your real client in production) ───────
function createMockLLMClient(apiKey: string): LLMClient {
return {
async complete(messages: LLMMessage[]): Promise<string> {
// In production this would call OpenAI / Anthropic / etc.
console.log(`[LLMClient] Sending ${messages.length} messages to API...`)
console.log(`[LLMClient] Using API key: ${apiKey.slice(0, 8)}...`)
// Simulate a fixed version of the broken code
return `
async function fetchUser(userId: string) {
try {
const response = await fetch(\`https://api.example.com/users/\${userId}\`)
if (!response.ok) throw new Error(\`HTTP error: \${response.status}\`)
const data = await response.json()
console.log('User:', data)
return data
} catch (error) {
console.error('Failed to fetch user:', error)
throw error
}
}
fetchUser('user_abc123')
`.trim()
},
}
}
// ── Inline implementation of fixCodeSample ───────────────────────────────────
async function fixCodeSample(
client: LLMClient,
code: string,
error: string,
context: string,
iteration: number = 1
): Promise<string> {
const strategyHint =
iteration === 1
? 'Fix the specific error shown.'
: iteration === 2
? 'The previous fix did not work. Try a different approach and simplify the code.'
: 'Multiple fixes have failed. Rewrite the example from scratch using only the context provided.'
const messages: LLMMessage[] = [
{
role: 'system',
content:
'You are an expert code repair assistant. Return ONLY the corrected code with no explanation, markdown fences, or commentary.',
},
{
role: 'user',
content: `
## Context
${context}
## Broken Code
\`\`\`
${code}
\`\`\`
## Error
\`\`\`
${error}
\`\`\`
## Instructions
Fix attempt #${iteration}. ${strategyHint}
Return only the corrected code.
`.trim(),
},
]
const fixed = await client.complete(messages)
return fixed.trim()
}
// ── Example usage ─────────────────────────────────────────────────────────────
const brokenCode = `
async function fetchUser(userId: string) {
const response = await fetch('https://api.example.com/users/' + userId)
const data = response.json() // ← missing await
console.log('User:', data)
return data
}
fetchUser('user_abc123')
`.trim()
const errorMessage = `
TypeError: data.name is undefined
at fetchUser (example.ts:4:22)
`.trim()
const contextDescription = `
fetchUser(userId: string): Promise<User>
Fetches a user object from the REST API by ID.
Expected output: logs the user object and returns it.
The function must handle HTTP errors and use proper async/await.
`.trim()
async function main() {
const apiKey = process.env.OPENAI_API_KEY || 'sk-your-api-key-here'
const client = createMockLLMClient(apiKey)
try {
console.log('=== Attempt 1 ===')
const fixedCode = await fixCodeSample(
client,
brokenCode,
errorMessage,
contextDescription,
1 // first attempt — targeted fix
)
console.log('\nFixed code:\n')
console.log(fixedCode)
// Simulate a retry with escalated strategy
console.log('\n=== Attempt 2 (retry with different strategy) ===')
const fixedCodeRetry = await fixCodeSample(
client,
brokenCode,
errorMessage,
contextDescription,
2 // second attempt — broader rewrite
)
console.log('\nFixed code (retry):\n')
console.log(fixedCodeRetry)
// Expected output:
// A corrected version of fetchUser with proper `await response.json()`
// and error handling, ready to replace the broken sample.
} catch (error) {
console.error('fixCodeSample failed:', error)
process.exit(1)
}
}
main()
generateDocumentation
async function generateDocumentation(client: LLMClient, element: ElementContext, options?: { multiLanguage?: boolean }): Promise<GeneratedDocResult>
Use this to automatically generate documentation for a code element (function, class, method, etc.) using an LLM client, with optional multi-language support.
This is the primary entry point for producing structured documentation output from parsed code context. Pass in your configured LLM client and a description of the code element, and receive a ready-to-use documentation result.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
client | LLMClient | ✅ Yes | A configured LLM client instance used to generate the documentation |
element | ElementContext | ✅ Yes | Context object describing the code element to document (name, signature, source, etc.) |
options | { multiLanguage?: boolean } | ❌ No | Optional settings. multiLanguage defaults to true — when enabled, generates code examples in multiple languages |
Returns
Returns a Promise<GeneratedDocResult> that resolves to a structured documentation object. The exact shape depends on the element type, but typically includes:
| Field | Description |
|---|---|
markdown | The generated documentation in Markdown format |
code | A self-contained code example |
summary | A short plain-text summary of the element |
Rejects with an error if the LLM client fails or the element context is malformed.
Example
// ─── Inline type definitions (no external imports needed) ───────────────────
type LLMMessage = { role: 'user' | 'assistant' | 'system'; content: string }
interface LLMClient {
complete(messages: LLMMessage[]): Promise<string>
}
interface ElementContext {
name: string
signature: string
language: string
sourceCode: string
docstring?: string
}
interface GeneratedDocResult {
markdown: string
code: string
summary: string
}
// ─── Simulated generateDocumentation (mirrors real behavior) ─────────────────
async function generateDocumentation(
client: LLMClient,
element: ElementContext,
options?: { multiLanguage?: boolean }
): Promise<GeneratedDocResult> {
const useMultiLang = options?.multiLanguage ?? true
const prompt = [
`Generate documentation for the following ${element.language} code element.`,
`Name: ${element.name}`,
`Signature: ${element.signature}`,
useMultiLang
? 'Include code examples in multiple languages (TypeScript, Python, Go).'
: 'Include a code example in the original language only.',
'',
'Source:',
element.sourceCode,
element.docstring ? `\nExisting docstring: ${element.docstring}` : '',
].join('\n')
const raw = await client.complete([{ role: 'user', content: prompt }])
// In the real implementation, the response is parsed into structured fields.
// Here we simulate that parsing step.
return {
markdown: `## \`${element.name}\`\n\n${raw}`,
code: `// Example usage of ${element.name}\nconst result = await ${element.name}(client, element)`,
summary: `Generates documentation for \`${element.name}\` using an LLM.`,
}
}
// ─── Simulated LLM client (replace with a real client in production) ──────────
function createMockLLMClient(apiKey: string): LLMClient {
console.log(`LLM client initialized (key: ${apiKey.slice(0, 8)}...)`)
return {
async complete(messages: LLMMessage[]): Promise<string> {
// Simulate an LLM response
const userMessage = messages.find((m) => m.role === 'user')?.content ?? ''
return [
'Use this to calculate the total price including tax.',
'',
'**Parameters:** `price: number`, `taxRate: number`',
'**Returns:** `number` — the final price after tax is applied.',
'',
userMessage.includes('multiple languages')
? '```typescript\nconst total = calculateTotal(100, 0.08) // 108\n```'
: '```python\ntotal = calculate_total(100, 0.08) # 108.0\n```',
].join('\n')
},
}
}
// ─── Main usage example ───────────────────────────────────────────────────────
async function main() {
const apiKey = process.env.OPENAI_API_KEY || 'sk-your-api-key-here'
const client = createMockLLMClient(apiKey)
const element: ElementContext = {
name: 'calculateTotal',
signature: 'function calculateTotal(price: number, taxRate: number): number',
language: 'TypeScript',
sourceCode: `
function calculateTotal(price: number, taxRate: number): number {
return price + price * taxRate
}`.trim(),
docstring: 'Calculates the total price after applying tax.',
}
try {
// Generate docs with multi-language examples (default behavior)
const result = await generateDocumentation(client, element, {
multiLanguage: true,
})
console.log('=== Generated Documentation ===')
console.log('\n[Markdown]\n', result.markdown)
console.log('\n[Code Example]\n', result.code)
console.log('\n[Summary]\n', result.summary)
// Generate docs with single-language example
const singleLang = await generateDocumentation(client, element, {
multiLanguage: false,
})
console.log('\n=== Single-Language Mode ===')
console.log('[Markdown]\n', singleLang.markdown)
// Expected output shape:
// {
// markdown: "## `calculateTotal`\n\nUse this to calculate...",
// code: "// Example usage of calculateTotal\nconst result = ...",
// summary: "Generates documentation for `calculateTotal` using an LLM."
// }
} catch (error) {
if (error instanceof Error) {
console.error('Documentation generation failed:', error.message)
} else {
console.error('Unexpected error:', error)
}
process.exit(1)
}
}
main()
getAuthConfig
function getAuthConfig(): AuthConfig
Use this to synchronously retrieve the current authentication configuration from environment variables or a local auth file — without touching the keychain.
This is the fast, synchronous option for reading auth state. It checks SKRYPT_API_KEY env var first, then falls back to a local auth file. When you need keychain access (e.g., in interactive CLI flows), use getAuthConfigAsync() instead.
Common use cases:
- Reading auth config at startup in a script or server process
- Checking if a user is authenticated before making API calls
- CI/CD environments where the API key is injected via environment variable
Parameters
This function takes no parameters.
Returns
Returns an AuthConfig object:
| Field | Type | Always Present | Description |
|---|---|---|---|
apiKey | string | undefined | No | The API key, sourced from SKRYPT_API_KEY env var or auth file |
email | string | undefined | No | The user's email address, sourced from the auth file |
Returns an empty/partial AuthConfig (with undefined fields) if no credentials are found — it does not throw.
Source Priority
| Priority | Source | Notes |
|---|---|---|
| 1st | SKRYPT_API_KEY env var | Always checked first |
| 2nd | Local auth file | ~/.skrypt/auth.json or similar |
| ❌ | Keychain | Never accessed — use getAuthConfigAsync() for this |
Example
// Inline types — no external imports needed
type AuthConfig = {
apiKey: string | undefined
email: string | undefined
}
// Simulate the auth file structure stored on disk
type AuthFileMeta = {
apiKey?: string
email?: string
}
// Simulate reading a local auth file (e.g., ~/.skrypt/auth.json)
function readAuthFile(): AuthFileMeta {
// In real usage, this reads from the filesystem
// Here we simulate a stored auth file with example data
return {
apiKey: 'sk_file_abc123xyz',
email: 'developer@example.com',
}
}
// Self-contained implementation of getAuthConfig
function getAuthConfig(): AuthConfig {
// Priority 1: environment variable
if (process.env.SKRYPT_API_KEY) {
const fileMeta = readAuthFile()
return {
apiKey: process.env.SKRYPT_API_KEY,
email: fileMeta.email,
}
}
// Priority 2: local auth file
const fileMeta = readAuthFile()
if (fileMeta.apiKey) {
return {
apiKey: fileMeta.apiKey,
email: fileMeta.email,
}
}
// No credentials found — return empty config (does not throw)
return {
apiKey: undefined,
email: undefined,
}
}
// --- Usage ---
// Scenario 1: API key set via environment variable (common in CI/CD)
process.env.SKRYPT_API_KEY = process.env.SKRYPT_API_KEY || 'sk_live_9f8e7d6c5b4a'
const config = getAuthConfig()
if (!config.apiKey) {
console.error('Not authenticated. Run `skrypt login` or set SKRYPT_API_KEY.')
process.exit(1)
}
console.log('Auth config loaded:')
console.log(` API Key: ${config.apiKey}`)
console.log(` Email: ${config.email ?? '(not set)'}`)
// Output:
// Auth config loaded:
// API Key: sk_live_9f8e7d6c5b4a
// Email: developer@example.com
// Scenario 2: No env var — falls back to auth file
delete process.env.SKRYPT_API_KEY
const configFromFile = getAuthConfig()
console.log('\nFallback to auth file:')
console.log(` API Key: ${configFromFile.apiKey}`)
console.log(` Email: ${configFromFile.email}`)
// Output:
// Fallback to auth file:
// API Key: sk_file_abc123xyz
// Email: developer@example.com
// Scenario 3: No credentials anywhere
function getAuthConfigEmpty(): AuthConfig {
return { apiKey: undefined, email: undefined }
}
const empty = getAuthConfigEmpty()
console.log('\nNo credentials found:', empty.apiKey ?? 'unauthenticated')
// Output:
// No credentials found: unauthenticated
Related
getAuthConfigAsync
async function getAuthConfigAsync(): Promise<AuthConfig>
Use this to retrieve the current user's authentication configuration in any CLI command action. It automatically resolves credentials by checking three sources in priority order: environment variable → system keychain → auth file on disk.
This is the preferred method for reading auth in all command handlers — it handles all credential sources transparently so you don't need to check each one manually.
Returns
| Condition | Result |
|---|---|
SKRYPT_API_KEY env var is set | Returns AuthConfig with API key from env + email from auth file |
| Env var not set, keychain has credentials | Returns AuthConfig from keychain |
| Neither env nor keychain, auth file exists | Returns AuthConfig read from auth file |
| No credentials found anywhere | Throws or returns empty/null config |
AuthConfig Shape
| Field | Type | Description |
|---|---|---|
apiKey | string | The API key used to authenticate requests |
email | string | undefined | The user's email address, if stored |
Resolution Priority
SKRYPT_API_KEY (env var)
↓ not found
System Keychain
↓ not found
~/.config/skrypt/auth (file)
Example
// Inline types — no external imports needed
type AuthConfig = {
apiKey: string
email?: string
}
type AuthFileMeta = {
email?: string
apiKey?: string
}
// --- Simulated dependencies (stand-ins for fs, keychain, etc.) ---
const mockAuthFile: AuthFileMeta = {
email: 'jane@example.com',
apiKey: 'file-key-abc123',
}
const mockKeychain: AuthFileMeta = {
email: 'jane@example.com',
apiKey: 'keychain-key-xyz789',
}
function readAuthFile(): AuthFileMeta {
// In real usage: reads from ~/.config/skrypt/auth
return mockAuthFile
}
async function keychainRetrieve(): Promise<AuthFileMeta | null> {
// In real usage: reads from OS keychain (macOS Keychain, etc.)
return mockKeychain
}
// --- Self-contained implementation of getAuthConfigAsync ---
async function getAuthConfigAsync(): Promise<AuthConfig> {
// Priority 1: Environment variable
if (process.env.SKRYPT_API_KEY) {
const fileMeta = readAuthFile()
return {
apiKey: process.env.SKRYPT_API_KEY,
email: fileMeta.email,
}
}
// Priority 2: System keychain
const keychainMeta = await keychainRetrieve()
if (keychainMeta?.apiKey) {
return {
apiKey: keychainMeta.apiKey,
email: keychainMeta.email,
}
}
// Priority 3: Auth file on disk
const fileMeta = readAuthFile()
if (fileMeta?.apiKey) {
return {
apiKey: fileMeta.apiKey,
email: fileMeta.email,
}
}
throw new Error(
'No credentials found. Run `skrypt login` or set SKRYPT_API_KEY.'
)
}
// --- Usage example ---
async function main() {
try {
// Scenario A: env var is set (highest priority)
process.env.SKRYPT_API_KEY = process.env.SKRYPT_API_KEY || 'env-key-sk_live_9f3kd82'
const authConfig = await getAuthConfigAsync()
console.log('Resolved auth config:')
console.log(' API Key:', authConfig.apiKey)
console.log(' Email: ', authConfig.email ?? '(not stored)')
// Output:
// API Key: env-key-sk_live_9f3kd82
// Email: jane@example.com
// Scenario B: no env var — falls through to keychain
delete process.env.SKRYPT_API_KEY
const authFromKeychain = await getAuthConfigAsync()
console.log('\nWithout env var (keychain fallback):')
console.log(' API Key:', authFromKeychain.apiKey)
// Output:
// API Key: keychain-key-xyz789
} catch (error) {
if (error instanceof Error) {
console.error('Auth error:', error.message)
// Handle: prompt user to run `skrypt login`
}
}
}
main()
Related
getKeyStorageMethod
async function getKeyStorageMethod(): Promise<'keychain' | 'file' | 'env' | 'none'>
Use this to detect where an API key is currently stored, so you can display storage status to users, conditionally prompt for re-authentication, or branch logic based on the active credential source.
Returns a promise resolving to one of four string literals indicating the active storage method — checked in priority order: environment variable → keychain → file → none.
Return Values
| Value | Description |
|---|---|
'env' | API key found in SKRYPT_API_KEY environment variable |
'keychain' | API key stored in the OS keychain (macOS/Linux/Windows credential store) |
'file' | API key stored in a local auth file on disk |
'none' | No API key found in any storage location |
Parameters
This function takes no parameters.
Notes
- Checks are performed in priority order — if
SKRYPT_API_KEYis set in the environment, it returns'env'immediately without checking other sources. - Use the result to give users meaningful feedback (e.g. "Logged in via keychain") or to decide whether to prompt for a new key.
Example
// Simulate the storage detection logic inline
// (mirrors the real priority order: env → keychain → file → none)
import { existsSync } from 'fs'
import { join } from 'path'
import { homedir } from 'os'
type KeyStorageMethod = 'keychain' | 'file' | 'env' | 'none'
// Simulate keychain lookup (real impl queries OS credential store)
async function simulateKeychainRetrieve(): Promise<string | null> {
// In production, this calls the OS keychain API
// Here we simulate "no keychain entry found"
return null
}
// Simulate file-based auth check
function simulateFileKeyExists(): boolean {
const AUTH_FILE = join(homedir(), '.skrypt', 'auth.json')
return existsSync(AUTH_FILE)
}
async function getKeyStorageMethod(): Promise<KeyStorageMethod> {
// Priority 1: environment variable
if (process.env.SKRYPT_API_KEY) return 'env'
// Priority 2: OS keychain
const keychainKey = await simulateKeychainRetrieve()
if (keychainKey) return 'keychain'
// Priority 3: local auth file
if (simulateFileKeyExists()) return 'file'
// Priority 4: not found anywhere
return 'none'
}
async function main() {
try {
const method = await getKeyStorageMethod()
console.log('Storage method detected:', method)
// Output (no key set): Storage method detected: none
// Example: branch on result to show user-facing status
const statusMessages: Record<KeyStorageMethod, string> = {
env: '🔑 Using API key from environment variable (SKRYPT_API_KEY)',
keychain: '🔐 Using API key stored in OS keychain',
file: '📄 Using API key stored in local auth file (~/.skrypt/auth.json)',
none: '⚠️ No API key found — please run `skrypt login`',
}
console.log(statusMessages[method])
// Output: ⚠️ No API key found — please run `skrypt login`
// Example: simulate env variable being set
process.env.SKRYPT_API_KEY = 'sk-prod-abc123xyz'
const methodWithEnv = await getKeyStorageMethod()
console.log('With env var set:', methodWithEnv)
// Output: With env var set: env
} catch (error) {
console.error('Failed to detect key storage method:', error)
}
}
main()
Related
requirePro
async function requirePro(commandName: string): Promise<boolean>
Use this to gate CLI commands behind a Pro subscription, automatically printing upgrade instructions and returning false if the user isn't authenticated or doesn't have a Pro plan.
Call requirePro at the top of any command handler that requires a paid plan. It handles all messaging to the user — you just check the return value and exit early if it's false.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
commandName | string | ✅ | The name of the CLI command being gated. Shown in the error message so users know which command requires Pro. |
Returns
| Value | When |
|---|---|
Promise<true> | User is authenticated and has an active Pro plan — safe to proceed |
Promise<false> | User has no API key, is on the free plan, or the auth check failed — command should abort |
Behavior
- Reads the stored auth config (API key + plan info)
- If no API key is found → prints upgrade prompt and returns
false - If plan is
freeor invalid → prints upgrade prompt and returnsfalse - If Pro is confirmed → returns
truesilently, letting the command continue
Example
// Inline types — no external imports needed
type Plan = 'free' | 'pro' | 'enterprise'
interface AuthConfig {
apiKey: string | null
plan: Plan
email: string
}
// Simulate stored auth config (in real usage, read from disk/keychain)
const mockAuthStore: AuthConfig = {
apiKey: process.env.SKRYPT_API_KEY || null,
plan: 'free',
email: 'user@example.com',
}
async function getAuthConfigAsync(): Promise<AuthConfig> {
// Simulate async config read (e.g., from ~/.config or keychain)
return new Promise((resolve) => setTimeout(() => resolve(mockAuthStore), 50))
}
async function requirePro(commandName: string): Promise<boolean> {
const config = await getAuthConfigAsync()
if (!config.apiKey) {
console.error(`\n ⚡ ${commandName} requires Skrypt Pro\n`)
console.error(' Get started:')
console.error(' https://skrypt.dev/pro\n')
return false
}
if (config.plan !== 'pro' && config.plan !== 'enterprise') {
console.error(`\n ⚡ ${commandName} requires Skrypt Pro\n`)
console.error(` You're currently on the ${config.plan} plan.`)
console.error(' Upgrade at: https://skrypt.dev/pro\n')
return false
}
return true
}
// --- Example usage in a CLI command handler ---
async function runExportCommand() {
try {
const allowed = await requirePro('export')
if (!allowed) {
// requirePro already printed the error — just exit
process.exitCode = 1
return
}
// Pro-only logic runs here
console.log('✅ Pro confirmed — running export...')
// Output: ⚡ export requires Skrypt Pro (when no API key)
// Output: ✅ Pro confirmed — running export... (when Pro plan)
} catch (error) {
console.error('Unexpected error during auth check:', error)
process.exitCode = 1
}
}
runExportCommand()
Related
runImport
function runImport(dir: string, format: ImportFormat, name?: string): ImportResult
Use this to automatically import and process a documentation directory based on its detected format (Mintlify, Docusaurus, GitBook, ReadMe, Confluence, or plain Markdown).
Instead of manually selecting the right importer, pass the directory path and detected format — runImport routes to the correct importer and returns a normalized result.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
dir | string | ✅ Yes | Path to the documentation directory to import |
format | ImportFormat | ✅ Yes | The detected format of the docs ('mintlify', 'docusaurus', 'gitbook', 'readme', 'confluence', 'markdown') |
name | string | ❌ No | Optional display name for the imported documentation set |
Returns
Returns an ImportResult object containing the processed documentation content, metadata, and any warnings or errors encountered during import.
| Scenario | Result |
|---|---|
| Successful import | ImportResult with parsed pages, metadata, and file tree |
| Unrecognized format | Throws or falls through to default handler |
| Invalid directory | Throws with a path-related error |
Example
// --- Inline types (mirrors Skrypt internals) ---
type ImportFormat = 'mintlify' | 'docusaurus' | 'gitbook' | 'readme' | 'confluence' | 'markdown'
interface ImportedPage {
path: string
title: string
content: string
}
interface ImportResult {
name: string
format: ImportFormat
pages: ImportedPage[]
warnings: string[]
}
// --- Simulated per-format importers ---
function importMintlify(dir: string, name?: string): ImportResult {
return {
name: name ?? 'Mintlify Docs',
format: 'mintlify',
pages: [{ path: `${dir}/index.mdx`, title: 'Introduction', content: '# Welcome to Mintlify' }],
warnings: [],
}
}
function importDocusaurus(dir: string, name?: string): ImportResult {
return {
name: name ?? 'Docusaurus Docs',
format: 'docusaurus',
pages: [{ path: `${dir}/docs/intro.md`, title: 'Intro', content: '# Getting Started' }],
warnings: ['Sidebar config not found, using default order'],
}
}
function importGitBook(dir: string, name?: string): ImportResult {
return {
name: name ?? 'GitBook Docs',
format: 'gitbook',
pages: [{ path: `${dir}/README.md`, title: 'Overview', content: '# Overview' }],
warnings: [],
}
}
function importReadme(dir: string, name?: string): ImportResult {
return {
name: name ?? 'ReadMe Docs',
format: 'readme',
pages: [{ path: `${dir}/v1.0/getting-started.md`, title: 'Getting Started', content: '# API Docs' }],
warnings: [],
}
}
function importConfluence(dir: string, name?: string): ImportResult {
return {
name: name ?? 'Confluence Export',
format: 'confluence',
pages: [{ path: `${dir}/space/page.html`, title: 'Home', content: '<h1>Confluence Page</h1>' }],
warnings: ['HTML content detected — consider converting to Markdown'],
}
}
function importMarkdown(dir: string, name?: string): ImportResult {
return {
name: name ?? 'Markdown Docs',
format: 'markdown',
pages: [{ path: `${dir}/README.md`, title: 'README', content: '# My Project' }],
warnings: [],
}
}
// --- The function under documentation ---
function runImport(dir: string, format: ImportFormat, name?: string): ImportResult {
switch (format) {
case 'mintlify': return importMintlify(dir, name)
case 'docusaurus': return importDocusaurus(dir, name)
case 'gitbook': return importGitBook(dir, name)
case 'readme': return importReadme(dir, name)
case 'confluence': return importConfluence(dir, name)
case 'markdown': return importMarkdown(dir, name)
default:
throw new Error(`Unsupported import format: ${format}`)
}
}
// --- Usage examples ---
async function main() {
const docsDir = process.env.DOCS_DIR || './docs'
const formats: ImportFormat[] = ['mintlify', 'docusaurus', 'gitbook', 'readme', 'confluence', 'markdown']
for (const format of formats) {
try {
const result = runImport(docsDir, format, `My ${format} Project`)
console.log(`\n[${format.toUpperCase()}]`)
console.log(` Name : ${result.name}`)
console.log(` Pages : ${result.pages.length}`)
console.log(` First : ${result.pages[0].title} → ${result.pages[0].path}`)
if (result.warnings.length > 0) {
console.log(` Warnings: ${result.warnings.join('; ')}`)
}
} catch (error) {
console.error(`Failed to import [${format}]:`, error)
}
}
// Expected output:
// [MINTLIFY]
// Name : My mintlify Project
// Pages : 1
// First : Introduction → ./docs/index.mdx
//
// [DOCUSAURUS]
// Name : My docusaurus Project
// Pages : 1
// First : Intro → ./docs/docs/intro.md
// Warnings: Sidebar config not found, using default order
// ... (and so on for each format)
}
main()
Related
saveAuthConfig
async function saveAuthConfig(config: AuthConfig): Promise<void>
Use this to persist authentication configuration (API keys, endpoints, tokens) to a secure local config directory, with automatic keychain integration when available.
This function:
- Creates the config directory with restricted permissions (
0o700) if it doesn't exist - Attempts to store the API key in the system keychain for added security
- Falls back to writing credentials to a local config file if keychain storage is unavailable
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
config | AuthConfig | Yes | Authentication configuration object containing credentials to persist |
config.apiKey | string | No | API key to store — saved to system keychain when possible, otherwise written to config file |
Returns
Returns Promise<void>. Resolves when the config has been successfully saved. Throws if the config directory cannot be created or the file cannot be written.
Notes
- The config directory is created with
0o700permissions (owner read/write/execute only) — no other users can access it - If keychain storage succeeds, the API key is not written to disk in plaintext
- Call
saveAuthConfigany time credentials change (login, token refresh, API key rotation)
Example
import { existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync } from 'fs'
import { join } from 'path'
import { homedir } from 'os'
// --- Inline types ---
interface AuthConfig {
apiKey?: string
endpoint?: string
token?: string
}
// --- Inline config path constants ---
const CONFIG_DIR = join(homedir(), '.config', 'myapp')
const CONFIG_FILE = join(CONFIG_DIR, 'auth.json')
// --- Simulate keychain (no real keychain in this example) ---
async function keychainStore(apiKey: string): Promise<boolean> {
// In production, this would call the OS keychain (e.g. macOS Keychain, libsecret)
console.log(`[keychain] Attempted to store API key: ${apiKey.slice(0, 8)}...`)
return false // Simulate keychain unavailable — falls back to file storage
}
// --- Self-contained saveAuthConfig implementation ---
async function saveAuthConfig(config: AuthConfig): Promise<void> {
// Create config directory with restricted permissions (owner only)
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
let keyInKeychain = false
if (config.apiKey) {
keyInKeychain = await keychainStore(config.apiKey)
}
// Build the object to persist — omit apiKey if it's safely in the keychain
const persistedConfig: Partial<AuthConfig> & { apiKeyInKeychain?: boolean } = {
endpoint: config.endpoint,
token: config.token,
apiKeyInKeychain: keyInKeychain,
// Only write apiKey to disk if keychain storage failed
...(!keyInKeychain && config.apiKey ? { apiKey: config.apiKey } : {}),
}
writeFileSync(CONFIG_FILE, JSON.stringify(persistedConfig, null, 2), { mode: 0o600 })
chmodSync(CONFIG_FILE, 0o600) // Ensure file is owner read/write only
console.log(`[saveAuthConfig] Config saved to: ${CONFIG_FILE}`)
}
// --- Usage example ---
async function main() {
const authConfig: AuthConfig = {
apiKey: process.env.MY_API_KEY || 'sk-abc123def456ghi789',
endpoint: process.env.API_ENDPOINT || 'https://api.example.com',
token: process.env.AUTH_TOKEN || 'tok-xyz987',
}
try {
await saveAuthConfig(authConfig)
// Verify what was written
const saved = JSON.parse(readFileSync(join(homedir(), '.config', 'myapp', 'auth.json'), 'utf-8'))
console.log('Saved config:', saved)
// Output (keychain unavailable): {
// endpoint: 'https://api.example.com',
// token: 'tok-xyz987',
// apiKeyInKeychain: false,
// apiKey: 'sk-abc123def456ghi789'
// }
// Output (keychain available): {
// endpoint: 'https://api.example.com',
// token: 'tok-xyz987',
// apiKeyInKeychain: true <-- apiKey NOT written to disk
// }
} catch (error) {
console.error('Failed to save auth config:', error)
process.exit(1)
}
}
main()
Related
scanDirectory
async function scanDirectory(dir: string, options: ScanOptions = {}): Promise<ScanAllResult>
Use this to recursively scan a directory (or single file) for all API elements — functions, classes, types, and exports — across multiple languages including Python, TypeScript, JavaScript, Go, and Rust.
This is the primary entry point for automated documentation generation pipelines. Point it at a project root and get back a structured inventory of every public API element found.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
dir | string | ✅ | Path to the directory or single file to scan |
options | ScanOptions | ❌ | Configuration to control which files are included or excluded |
options.include | string[] | ❌ | Glob patterns for files to scan. Defaults to ['**/*.py', '**/*.ts', '**/*.js', '**/*.go', '**/*.rs'] |
options.exclude | string[] | ❌ | Glob patterns for files to skip. Defaults to ['**/node_modules/**', '**/__pycache__/**', '**/dist/**'] |
Returns
Returns a Promise<ScanAllResult> that resolves to an object containing:
| Field | Type | Description |
|---|---|---|
files | ScanResult[] | One entry per scanned file, each containing the file path and its discovered API elements |
totalElements | number | Total count of API elements found across all files |
errors | ScanError[] | Any files that failed to parse, with their error details |
Returns an empty files array if no matching files are found. Files that fail to parse are collected in errors rather than throwing, so a single bad file won't abort the entire scan.
scanFile
async function scanFile(filePath: string): Promise<ScanResult>
Use this to extract structured metadata from a single source file — functions, classes, types, and other symbols — without scanning an entire directory.
scanFile inspects a file at the given path, selects the appropriate language scanner (TypeScript, JavaScript, Python, etc.), and returns a structured ScanResult containing all discovered symbols and language information.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
filePath | string | ✅ | Absolute or relative path to the source file to scan |
Returns
Returns Promise<ScanResult> — resolves with a structured object containing:
| Field | Type | Description |
|---|---|---|
language | string | Detected language ('typescript', 'javascript', 'python', etc.) |
symbols | Symbol[] | Array of discovered functions, classes, types, and other declarations |
filePath | string | The resolved path of the scanned file |
Throws if the file does not exist, cannot be read, or has an unsupported extension with no registered scanner.
Example
// Inline types matching the Skrypt ScanResult shape
type SymbolKind = 'function' | 'class' | 'type' | 'interface' | 'variable'
interface DocSymbol {
name: string
kind: SymbolKind
line: number
docstring?: string
signature?: string
}
interface ScanResult {
filePath: string
language: 'typescript' | 'javascript' | 'python' | 'unknown'
symbols: DocSymbol[]
}
// Simulated scanFile — replace with the real import in your project
async function scanFile(filePath: string): Promise<ScanResult> {
const ext = filePath.split('.').pop()?.toLowerCase()
const langMap: Record<string, ScanResult['language']> = {
ts: 'typescript',
tsx: 'typescript',
js: 'javascript',
jsx: 'javascript',
py: 'python',
}
const language = langMap[ext ?? ''] ?? 'unknown'
if (language === 'unknown') {
throw new Error(`No scanner available for file extension: .${ext}`)
}
// Simulated scan output — real implementation parses the AST
return {
filePath,
language,
symbols: [
{
name: 'getUserById',
kind: 'function',
line: 12,
docstring: 'Fetch a user record by their unique ID.',
signature: 'async function getUserById(id: string): Promise<User>',
},
{
name: 'User',
kind: 'interface',
line: 4,
docstring: 'Represents an authenticated user.',
signature: 'interface User { id: string; email: string; role: string }',
},
],
}
}
// --- Usage ---
async function main() {
const targetFile = process.env.SCAN_TARGET || './src/users.ts'
try {
const result = await scanFile(targetFile)
console.log(`Scanned: ${result.filePath}`)
console.log(`Language: ${result.language}`)
console.log(`Symbols found: ${result.symbols.length}`)
for (const symbol of result.symbols) {
console.log(`\n [${symbol.kind}] ${symbol.name} (line ${symbol.line})`)
if (symbol.docstring) console.log(` 📝 ${symbol.docstring}`)
if (symbol.signature) console.log(` 🔷 ${symbol.signature}`)
}
// Expected output:
// Scanned: ./src/users.ts
// Language: typescript
// Symbols found: 2
//
// [function] getUserById (line 12)
// 📝 Fetch a user record by their unique ID.
// 🔷 async function getUserById(id: string): Promise<User>
//
// [interface] User (line 4)
// 📝 Represents an authenticated user.
// 🔷 interface User { id: string; email: string; role: string }
} catch (error) {
console.error('Scan failed:', error instanceof Error ? error.message : error)
process.exit(1)
}
}
main()
Related
constructor
constructor(sourcePath: string, outputPath: string, config: Record<string, unknown> = {})
Use this to initialize a plugin manager that coordinates documentation plugins across a source codebase, wiring together input/output paths and shared configuration before any plugins are registered or executed.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
sourcePath | string | ✅ Yes | Absolute or relative path to the source code directory to be processed |
outputPath | string | ✅ Yes | Absolute or relative path to the directory where generated docs will be written |
config | Record<string, unknown> | ❌ No | Optional key-value configuration passed to all plugins via shared context. Defaults to {} |
Returns
A PluginManager instance with an initialized internal PluginContext containing the provided paths and config. The plugin registry starts empty — use subsequent register/load methods to add plugins.
Notes
sourcePathandoutputPathare stored on the sharedPluginContext, making them accessible to every plugin that runs- Passing
confighere is the recommended way to share global settings (e.g., output format, version flags) across all plugins without coupling them directly
Example
// --- Inline types (mirroring the real Skrypt internals) ---
interface PluginContext {
sourcePath: string
outputPath: string
config: Record<string, unknown>
logger: {
info: (msg: string) => void
warn: (msg: string) => void
error: (msg: string) => void
}
}
interface SkryptPlugin {
name: string
run: (context: PluginContext) => Promise<void>
}
// --- Self-contained PluginManager implementation ---
class PluginManager {
private plugins: SkryptPlugin[] = []
private context: PluginContext
constructor(
sourcePath: string,
outputPath: string,
config: Record<string, unknown> = {}
) {
this.context = {
sourcePath,
outputPath,
config,
logger: {
info: (msg: string) => console.log(`[INFO] ${msg}`),
warn: (msg: string) => console.warn(`[WARN] ${msg}`),
error: (msg: string) => console.error(`[ERROR] ${msg}`),
},
}
}
/** Expose context for inspection in this example */
getContext(): PluginContext {
return this.context
}
register(plugin: SkryptPlugin): void {
this.plugins.push(plugin)
this.context.logger.info(`Registered plugin: ${plugin.name}`)
}
async runAll(): Promise<void> {
for (const plugin of this.plugins) {
this.context.logger.info(`Running plugin: ${plugin.name}`)
await plugin.run(this.context)
}
}
}
// --- Usage example ---
async function main() {
try {
const manager = new PluginManager(
process.env.SOURCE_PATH || './src',
process.env.OUTPUT_PATH || './docs',
{
outputFormat: 'markdown',
version: '2.1.0',
includePrivate: false,
}
)
// Inspect the initialized context
const ctx = manager.getContext()
console.log('PluginManager initialized:')
console.log(' sourcePath:', ctx.sourcePath) // './src'
console.log(' outputPath:', ctx.outputPath) // './docs'
console.log(' config:', ctx.config)
// {
// outputFormat: 'markdown',
// version: '2.1.0',
// includePrivate: false
// }
// Register a sample plugin to confirm context flows through
manager.register({
name: 'typescript-extractor',
run: async (context) => {
console.log(`\n[typescript-extractor] Reading from: ${context.sourcePath}`)
console.log(`[typescript-extractor] Writing to: ${context.outputPath}`)
console.log(`[typescript-extractor] Format: ${context.config.outputFormat}`)
},
})
await manager.runAll()
// Output:
// [INFO] Registered plugin: typescript-extractor
// [INFO] Running plugin: typescript-extractor
// [typescript-extractor] Reading from: ./src
// [typescript-extractor] Writing to: ./docs
// [typescript-extractor] Format: markdown
} catch (error) {
console.error('PluginManager setup failed:', error)
process.exit(1)
}
}
main()
Related
PluginManager
Uses
AnthropicClient
Used by
OpenAICompatibleClient
Used by
PluginManager
Used by
AnthropicClient
Related
OpenAICompatibleClient
Related
loadPlugins
async loadPlugins(configPath?: string): Promise<void>
Use this to initialize and load plugins into the PluginManager from a configuration file, enabling plugin-based extensibility in your application.
This method reads plugin definitions from a skrypt.config.js or skrypt.config.ts file (auto-discovered if no path is provided) and registers them with the manager. Call this once during application startup before executing any plugin-dependent logic.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
configPath | string | No | Absolute or relative path to a config file. If omitted, the manager auto-discovers skrypt.config.js or skrypt.config.ts in the current working directory. |
Returns
Returns Promise<void>. Resolves when all plugins have been loaded and registered. If no config file is found, resolves immediately without error.
Behavior Notes
- No config found → resolves silently with no plugins loaded
- Config found, valid → plugins are registered and ready to use
- Config found, invalid → throws or logs an error depending on the failure type
- Passing an explicit
configPathoverrides auto-discovery entirely
Example
// Inline types to simulate PluginManager behavior
type Plugin = {
name: string
execute: (input: string) => string
}
type PluginConfig = {
plugins: Plugin[]
}
// Simulated PluginManager class (self-contained, no external imports)
class PluginManager {
private plugins: Map<string, Plugin> = new Map()
private configSearchPaths = ['./skrypt.config.js', './skrypt.config.ts']
private findConfigFile(): string | null {
// In real usage, this checks the filesystem via existsSync
// Here we simulate "not found" unless overridden
return null
}
private async importConfig(configPath: string): Promise<PluginConfig> {
// Simulate loading a config file by returning a mock config
console.log(`[plugin] Loading config from: ${configPath}`)
return {
plugins: [
{ name: 'markdown-renderer', execute: (input) => `<p>${input}</p>` },
{ name: 'syntax-highlighter', execute: (input) => `<code>${input}</code>` },
],
}
}
async loadPlugins(configPath?: string): Promise<void> {
const configFile = configPath || this.findConfigFile()
if (!configFile) {
console.log('[plugin] No config file found — skipping plugin load')
return
}
try {
const config = await this.importConfig(configFile)
for (const plugin of config.plugins) {
this.plugins.set(plugin.name, plugin)
console.log(`[plugin] Registered: ${plugin.name}`)
}
console.log(`[plugin] Loaded ${this.plugins.size} plugin(s) successfully`)
} catch (error) {
console.error('[plugin] Failed to load plugins:', error)
throw error
}
}
getPlugin(name: string): Plugin | undefined {
return this.plugins.get(name)
}
listPlugins(): string[] {
return Array.from(this.plugins.keys())
}
}
// --- Usage ---
async function main() {
const manager = new PluginManager()
try {
// Option 1: Auto-discover config (resolves silently if not found)
await manager.loadPlugins()
// Option 2: Explicit config path (e.g., from env or CLI arg)
const configPath = process.env.PLUGIN_CONFIG_PATH || './skrypt.config.js'
await manager.loadPlugins(configPath)
// Plugins are now available
const availablePlugins = manager.listPlugins()
console.log('Available plugins:', availablePlugins)
// Output: Available plugins: [ 'markdown-renderer', 'syntax-highlighter' ]
const renderer = manager.getPlugin('markdown-renderer')
if (renderer) {
const output = renderer.execute('Hello, world!')
console.log('Renderer output:', output)
// Output: Renderer output: <p>Hello, world!</p>
}
} catch (error) {
console.error('Startup failed — could not load plugins:', error)
process.exit(1)
}
}
main()
Related
onAfterGenerate
async onAfterGenerate<T>(docs: T[]): Promise<T[]>
Use this to run all registered plugins' onAfterGenerate hooks against a collection of generated documentation objects, allowing plugins to transform, filter, enrich, or reorder docs before they are written to disk.
This is the final transformation stage in the generation pipeline — ideal for plugins that need to post-process docs (e.g., injecting metadata, removing internal-only entries, sorting output).
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
docs | T[] | Yes | The array of generated documentation objects to pass through all registered plugin hooks. The generic type T can be any doc shape. |
Returns
| Condition | Returns |
|---|---|
| A plugin hook transforms the docs | Promise<T[]> — the transformed array returned by the plugin |
| No plugin hook is registered / hook returns falsy | Promise<T[]> — the original docs array, unchanged |
The method always resolves to a T[] — it never returns undefined or null.
Example
// Inline types to simulate the PluginManager behavior
type Hook<T> = (data: T) => Promise<T> | T
interface Plugin {
name: string
onAfterGenerate?: Hook<any[]>
}
interface DocEntry {
id: string
title: string
content: string
internal?: boolean
}
// Minimal self-contained PluginManager simulation
class PluginManager {
private plugins: Plugin[] = []
register(plugin: Plugin) {
this.plugins.push(plugin)
console.log(`Plugin registered: ${plugin.name}`)
}
private async runHook<T>(hookName: keyof Plugin, data: T): Promise<T | undefined> {
let result: T = data
for (const plugin of this.plugins) {
const hook = plugin[hookName] as Hook<T> | undefined
if (typeof hook === 'function') {
result = await hook(result)
}
}
return this.plugins.some(p => typeof p[hookName] === 'function') ? result : undefined
}
async onAfterGenerate<T>(docs: T[]): Promise<T[]> {
return (await this.runHook<T[]>('onAfterGenerate', docs)) || docs
}
}
// --- Example usage ---
const manager = new PluginManager()
// Plugin 1: Remove internal-only docs
manager.register({
name: 'filter-internal-plugin',
onAfterGenerate: async (docs: DocEntry[]) => {
const filtered = docs.filter(doc => !doc.internal)
console.log(`[filter-internal-plugin] Removed ${docs.length - filtered.length} internal doc(s)`)
return filtered
}
})
// Plugin 2: Inject a generated timestamp into each doc
manager.register({
name: 'timestamp-plugin',
onAfterGenerate: async (docs: DocEntry[]) => {
return docs.map(doc => ({
...doc,
content: `${doc.content}\n\n_Generated at: 2024-01-15T10:30:00Z_`
}))
}
})
const generatedDocs: DocEntry[] = [
{ id: 'doc-001', title: 'Getting Started', content: 'Install the package...' },
{ id: 'doc-002', title: 'Internal Roadmap', content: 'Q3 plans...', internal: true },
{ id: 'doc-003', title: 'API Reference', content: 'All public methods...' },
]
async function main() {
try {
console.log(`\nInput docs: ${generatedDocs.length}`)
const finalDocs = await manager.onAfterGenerate(generatedDocs)
console.log(`\nOutput docs: ${finalDocs.length}`)
finalDocs.forEach(doc => {
console.log(`\n--- ${doc.title} ---`)
console.log(doc.content)
})
// Expected output:
// Input docs: 3
// [filter-internal-plugin] Removed 1 internal doc(s)
// Output docs: 2
// --- Getting Started ---
// Install the package...
// _Generated at: 2024-01-15T10:30:00Z_
// --- API Reference ---
// All public methods...
// _Generated at: 2024-01-15T10:30:00Z_
} catch (error) {
console.error('onAfterGenerate failed:', error)
}
}
main()
Related
runHook
Uses
onBeforeGenerate
Uses
onBeforeWrite
Uses
onBeforeGenerate
Used by
onBeforeWrite
Used by
PluginManager
Related
onAfterScan
async onAfterScan<T>(elements: T[]): Promise<T[]>
Use this to run all registered plugins' onAfterScan hooks against a list of scanned elements, allowing plugins to filter, transform, or enrich the results before further processing.
This is called automatically by the PluginManager after a scan completes. Each plugin that implements onAfterScan gets a chance to modify the elements array in sequence. If no plugin modifies the elements, the original array is returned unchanged.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
elements | T[] | Yes | The array of scanned elements to pass through the plugin hook pipeline |
Returns
Promise<T[]> — Resolves to the (potentially modified) array of elements after all plugins have processed them. Falls back to the original elements array if no plugin returns a value.
Example
// Inline types to simulate PluginManager behavior
type Plugin = {
name: string
onAfterScan?: <T>(elements: T[]) => Promise<T[]>
}
type ScannedFile = {
path: string
type: string
exported: boolean
}
// Simulate the PluginManager class
class PluginManager {
private plugins: Plugin[] = []
register(plugin: Plugin) {
this.plugins.push(plugin)
}
private async runHook<T>(hookName: string, arg?: T): Promise<T | undefined> {
let result = arg
for (const plugin of this.plugins) {
const hook = (plugin as any)[hookName]
if (typeof hook === 'function') {
const hookResult = await hook.call(plugin, result)
if (hookResult !== undefined) {
result = hookResult
}
}
}
return result
}
async onAfterScan<T>(elements: T[]): Promise<T[]> {
return (await this.runHook<T[]>('onAfterScan', elements)) || elements
}
}
// Example plugins that transform scanned elements
const exportFilterPlugin: Plugin = {
name: 'export-filter',
async onAfterScan<T>(elements: T[]): Promise<T[]> {
const files = elements as unknown as ScannedFile[]
const filtered = files.filter(f => f.exported)
console.log(`[export-filter] Filtered ${elements.length} → ${filtered.length} exported files`)
return filtered as unknown as T[]
}
}
const pathNormalizerPlugin: Plugin = {
name: 'path-normalizer',
async onAfterScan<T>(elements: T[]): Promise<T[]> {
const files = elements as unknown as ScannedFile[]
const normalized = files.map(f => ({ ...f, path: f.path.replace(/\\/g, '/') }))
console.log(`[path-normalizer] Normalized ${normalized.length} file paths`)
return normalized as unknown as T[]
}
}
async function main() {
try {
const manager = new PluginManager()
manager.register(exportFilterPlugin)
manager.register(pathNormalizerPlugin)
const scannedFiles: ScannedFile[] = [
{ path: 'src\\components\\Button.ts', type: 'component', exported: true },
{ path: 'src\\utils\\internal.ts', type: 'utility', exported: false },
{ path: 'src\\hooks\\useAuth.ts', type: 'hook', exported: true },
{ path: 'src\\helpers\\debug.ts', type: 'helper', exported: false },
]
console.log('Before onAfterScan:', scannedFiles.length, 'files')
const result = await manager.onAfterScan(scannedFiles)
console.log('\nAfter onAfterScan:', result.length, 'files')
console.log('Final elements:', result)
// Output:
// Before onAfterScan: 4 files
// [export-filter] Filtered 4 → 2 exported files
// [path-normalizer] Normalized 2 file paths
// After onAfterScan: 2 files
// Final elements: [
// { path: 'src/components/Button.ts', type: 'component', exported: true },
// { path: 'src/hooks/useAuth.ts', type: 'hook', exported: true }
// ]
} catch (error) {
console.error('onAfterScan failed:', error)
}
}
main()
Related
runHook
Uses
onBeforeScan
Uses
onBeforeGenerate
Uses
onBeforeScan
Used by
onBeforeGenerate
Used by
PluginManager
Related
onAfterWrite
async onAfterWrite(): Promise<void>
Use this to trigger all registered plugin hooks after documentation has been written to disk — ideal for post-write tasks like cache invalidation, notifications, or cleanup.
onAfterWrite runs through every loaded plugin's onAfterWrite hook in sequence. It resolves when all hooks complete and does not return a value. If no plugins define this hook, it resolves immediately.
Parameters
This method takes no parameters.
Returns
| Type | Description |
|---|---|
Promise<void> | Resolves when all plugin onAfterWrite hooks have completed. Does not return a value. |
When to Call
Call onAfterWrite immediately after all documentation files have been written. It pairs with onBeforeWrite to form a write lifecycle:
onBeforeWrite(docs)— transform/filter docs before writing- (write files to disk)
onAfterWrite()— notify plugins that writing is complete
Example
// Inline types to keep example self-contained
type Hook = (...args: unknown[]) => Promise<unknown> | unknown
interface Plugin {
name: string
onAfterWrite?: () => Promise<void> | void
onBeforeWrite?: <T>(docs: T[]) => Promise<T[]> | T[]
}
// Simulated PluginManager implementation
class PluginManager {
private plugins: Plugin[] = []
register(plugin: Plugin): void {
this.plugins.push(plugin)
console.log(`Plugin registered: ${plugin.name}`)
}
async runHook(hookName: keyof Plugin, ...args: unknown[]): Promise<unknown> {
let result: unknown
for (const plugin of this.plugins) {
const hook = plugin[hookName] as Hook | undefined
if (typeof hook === 'function') {
result = await hook(...args)
}
}
return result
}
async onBeforeWrite<T>(docs: T[]): Promise<T[]> {
return (await this.runHook('onBeforeWrite', docs) as T[]) || docs
}
async onAfterWrite(): Promise<void> {
await this.runHook('onAfterWrite')
}
}
// --- Example usage ---
const manager = new PluginManager()
// Register a plugin that logs after writing
manager.register({
name: 'cache-invalidation-plugin',
onAfterWrite: async () => {
console.log('[cache-invalidation-plugin] Cache cleared after write.')
}
})
// Register a plugin that sends a notification
manager.register({
name: 'notify-plugin',
onAfterWrite: async () => {
const webhookUrl = process.env.NOTIFY_WEBHOOK_URL || 'https://hooks.example.com/notify'
console.log(`[notify-plugin] POST notification sent to: ${webhookUrl}`)
}
})
async function runWriteLifecycle() {
try {
const docs = [
{ id: 'doc-001', content: 'Getting Started' },
{ id: 'doc-002', content: 'API Reference' }
]
// Step 1: Pre-write hook
const processedDocs = await manager.onBeforeWrite(docs)
console.log(`Writing ${processedDocs.length} docs to disk...`)
// Step 2: (Simulated) write to disk
processedDocs.forEach(doc => {
console.log(` Written: ${doc.id}.md`)
})
// Step 3: Post-write hook — notify all plugins
await manager.onAfterWrite()
console.log('Write lifecycle complete.')
// Expected output:
// Plugin registered: cache-invalidation-plugin
// Plugin registered: notify-plugin
// Writing 2 docs to disk...
// Written: doc-001.md
// Written: doc-002.md
// [cache-invalidation-plugin] Cache cleared after write.
// [notify-plugin] POST notification sent to: https://hooks.example.com/notify
// Write lifecycle complete.
} catch (error) {
console.error('Write lifecycle failed:', error)
}
}
runWriteLifecycle()
Related
runHook
Uses
onBeforeWrite
Uses
transformContent
Uses
onBeforeWrite
Used by
transformContent
Used by
PluginManager
Related
onBeforeGenerate
async onBeforeGenerate<T>(elements: T[]): Promise<T[]>
Use this to run all registered plugins' onBeforeGenerate hooks against a collection of elements before documentation generation begins. This is the ideal interception point for filtering, transforming, sorting, or enriching your elements (e.g., parsed AST nodes, function signatures, class definitions) before they are handed off to the doc generator.
If no plugin hook modifies the elements, the original array is returned unchanged.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
elements | T[] | Yes | The array of elements (e.g., parsed code nodes, doc entries) to be processed by registered plugin hooks before generation |
Returns
| Condition | Returns |
|---|---|
| A plugin hook transforms the elements | Promise<T[]> — the modified array returned by the hook |
| No plugin hook is registered or hook returns falsy | Promise<T[]> — the original elements array, unchanged |
Example
// Inline types to simulate the PluginManager behavior
type Hook<T> = (elements: T[]) => Promise<T[] | null | undefined>
interface Plugin {
onBeforeGenerate?: <T>(elements: T[]) => Promise<T[]>
}
// Simulated PluginManager with onBeforeGenerate support
class PluginManager {
private plugins: Plugin[] = []
register(plugin: Plugin) {
this.plugins.push(plugin)
}
private async runHook<T>(hookName: keyof Plugin, elements: T[]): Promise<T[] | null> {
let current: T[] = elements
for (const plugin of this.plugins) {
const hook = plugin[hookName] as Hook<T> | undefined
if (typeof hook === 'function') {
const result = await hook(current)
if (result) current = result
}
}
return current.length ? current : null
}
async onBeforeGenerate<T>(elements: T[]): Promise<T[]> {
return (await this.runHook<T>('onBeforeGenerate', elements)) || elements
}
}
// Simulated doc element type
interface DocElement {
name: string
isPublic: boolean
description: string
}
// Example: a plugin that filters out private elements before generation
const publicOnlyPlugin: Plugin = {
onBeforeGenerate: async <T>(elements: T[]): Promise<T[]> => {
const docs = elements as unknown as DocElement[]
const filtered = docs.filter((el) => el.isPublic)
console.log(`[Plugin] Filtered ${docs.length - filtered.length} private element(s)`)
return filtered as unknown as T[]
},
}
// Example: a plugin that uppercases all names
const uppercasePlugin: Plugin = {
onBeforeGenerate: async <T>(elements: T[]): Promise<T[]> => {
const docs = elements as unknown as DocElement[]
const transformed = docs.map((el) => ({ ...el, name: el.name.toUpperCase() }))
return transformed as unknown as T[]
},
}
async function main() {
const manager = new PluginManager()
manager.register(publicOnlyPlugin)
manager.register(uppercasePlugin)
const rawElements: DocElement[] = [
{ name: 'getUserById', isPublic: true, description: 'Fetches a user by ID' },
{ name: '_internalSync', isPublic: false, description: 'Internal sync helper' },
{ name: 'createSession', isPublic: true, description: 'Creates a new session' },
]
try {
const processed = await manager.onBeforeGenerate(rawElements)
console.log('Elements ready for generation:', processed)
// Expected output:
// [Plugin] Filtered 1 private element(s)
// Elements ready for generation: [
// { name: 'GETUSERBYID', isPublic: true, description: 'Fetches a user by ID' },
// { name: 'CREATESESSION', isPublic: true, description: 'Creates a new session' }
// ]
} catch (error) {
console.error('onBeforeGenerate failed:', error)
}
}
main()
Related
runHook
Uses
onAfterScan
Uses
onAfterGenerate
Uses
onAfterScan
Used by
onAfterGenerate
Used by
PluginManager
Related
onBeforeScan
async onBeforeScan(): Promise<void>
Use this to trigger all registered plugins' pre-scan lifecycle hooks before a documentation scan begins. This allows plugins to perform setup tasks, validate configurations, or prepare resources before any files are processed.
Parameters
This method takes no parameters.
Returns
| Type | Description |
|---|---|
Promise<void> | Resolves when all registered plugins have completed their onBeforeScan hooks. Rejects if any plugin hook throws an error. |
When to Call
Call onBeforeScan() after onInit() but before any file scanning or parsing begins. This is the correct place for plugins to:
- Set up temporary directories or caches
- Validate that required external tools are available
- Reset state from a previous scan run
- Log scan start events or metrics
Example
// Inline types to simulate the PluginManager behavior
type HookName = 'onInit' | 'onBeforeScan' | 'onAfterScan'
interface Plugin {
name: string
onInit?: () => Promise<void>
onBeforeScan?: () => Promise<void>
onAfterScan?: <T>(elements: T[]) => Promise<T[]>
}
// Simulated PluginManager class
class PluginManager {
private plugins: Plugin[] = []
register(plugin: Plugin): void {
this.plugins.push(plugin)
console.log(`Plugin registered: ${plugin.name}`)
}
private async runHook(hookName: HookName, ...args: unknown[]): Promise<unknown> {
for (const plugin of this.plugins) {
const hook = plugin[hookName] as ((...a: unknown[]) => Promise<unknown>) | undefined
if (typeof hook === 'function') {
await hook.call(plugin, ...args)
}
}
return undefined
}
async onInit(): Promise<void> {
await this.runHook('onInit')
}
async onBeforeScan(): Promise<void> {
await this.runHook('onBeforeScan')
}
async onAfterScan<T>(elements: T[]): Promise<T[]> {
return ((await this.runHook('onAfterScan', elements)) as T[]) || elements
}
}
// Example plugins that use the onBeforeScan hook
const cachePlugin: Plugin = {
name: 'cache-plugin',
onBeforeScan: async () => {
const cacheDir = process.env.CACHE_DIR || '/tmp/skrypt-cache'
console.log(`[cache-plugin] Clearing cache at: ${cacheDir}`)
// Simulate async cache clearing
await new Promise((resolve) => setTimeout(resolve, 10))
console.log('[cache-plugin] Cache cleared successfully')
},
}
const metricsPlugin: Plugin = {
name: 'metrics-plugin',
onBeforeScan: async () => {
const scanId = `scan_${Date.now()}`
console.log(`[metrics-plugin] Starting scan session: ${scanId}`)
},
}
async function main() {
const manager = new PluginManager()
manager.register(cachePlugin)
manager.register(metricsPlugin)
try {
// Step 1: Initialize all plugins
await manager.onInit()
console.log('✓ Plugins initialized')
// Step 2: Run pre-scan hooks before processing any files
await manager.onBeforeScan()
console.log('✓ Pre-scan hooks completed — safe to begin file scanning')
// Step 3: (Scanning would happen here)
// const files = await scanner.findFiles('./src')
// const elements = await parser.parse(files)
// const processed = await manager.onAfterScan(elements)
} catch (error) {
console.error('Pre-scan hook failed — aborting scan:', error)
process.exit(1)
}
}
main()
// Expected output:
// Plugin registered: cache-plugin
// Plugin registered: metrics-plugin
// ✓ Plugins initialized
// [cache-plugin] Clearing cache at: /tmp/skrypt-cache
// [cache-plugin] Cache cleared successfully
// [metrics-plugin] Starting scan session: scan_1718000000000
// ✓ Pre-scan hooks completed — safe to begin file scanning
Related
onBeforeWrite
async onBeforeWrite<T>(docs: T[]): Promise<T[]>
Use this to run all registered plugins' onBeforeWrite hooks against your generated docs array, allowing plugins to transform, filter, or enrich documents just before they are written to disk.
This is the last interception point before output is committed — ideal for plugins that need to inject metadata, reorder entries, strip internal fields, or validate documents.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
docs | T[] | Yes | Array of documents to be passed through all registered onBeforeWrite plugin hooks before writing |
Returns
| Condition | Return Value |
|---|---|
| One or more plugins transform the docs | Promise<T[]> — the transformed array returned by the last hook in the chain |
| No plugins modify the docs | Promise<T[]> — the original docs array passed in, unchanged |
The method always returns an array — it falls back to the original docs if the hook pipeline returns a falsy value.
Example
// Inline types to simulate the PluginManager behavior
type Plugin<T> = {
onBeforeWrite?: (docs: T[]) => Promise<T[]>
}
type DocEntry = {
name: string
content: string
metadata?: Record<string, unknown>
}
// Simulated PluginManager with onBeforeWrite support
class PluginManager<T> {
private plugins: Plugin<T>[] = []
register(plugin: Plugin<T>) {
this.plugins.push(plugin)
}
private async runHook(hookName: keyof Plugin<T>, data: T[]): Promise<T[] | null> {
let result: T[] = data
for (const plugin of this.plugins) {
const hook = plugin[hookName]
if (typeof hook === 'function') {
result = await (hook as (docs: T[]) => Promise<T[]>)(result)
}
}
return result.length > 0 ? result : null
}
async onBeforeWrite(docs: T[]): Promise<T[]> {
return (await this.runHook('onBeforeWrite', docs)) || docs
}
}
// Example plugins that transform docs before writing
const timestampPlugin: Plugin<DocEntry> = {
onBeforeWrite: async (docs) => {
console.log('[timestampPlugin] Injecting write timestamps...')
return docs.map((doc) => ({
...doc,
metadata: {
...doc.metadata,
writtenAt: new Date().toISOString(),
},
}))
},
}
const filterPlugin: Plugin<DocEntry> = {
onBeforeWrite: async (docs) => {
console.log('[filterPlugin] Removing draft documents...')
return docs.filter((doc) => !doc.name.startsWith('DRAFT_'))
},
}
// Setup
const manager = new PluginManager<DocEntry>()
manager.register(timestampPlugin)
manager.register(filterPlugin)
const rawDocs: DocEntry[] = [
{ name: 'getting-started', content: '# Getting Started\n...' },
{ name: 'DRAFT_advanced-usage', content: '# Advanced Usage (WIP)' },
{ name: 'api-reference', content: '# API Reference\n...' },
]
async function main() {
try {
console.log('Docs before onBeforeWrite:', rawDocs.map((d) => d.name))
const finalDocs = await manager.onBeforeWrite(rawDocs)
console.log('\nDocs after onBeforeWrite:')
finalDocs.forEach((doc) => {
console.log(` - ${doc.name}`, doc.metadata ? `(writtenAt: ${doc.metadata.writtenAt})` : '')
})
// Expected output:
// Docs before onBeforeWrite: ['getting-started', 'DRAFT_advanced-usage', 'api-reference']
// [timestampPlugin] Injecting write timestamps...
// [filterPlugin] Removing draft documents...
// Docs after onBeforeWrite:
// - getting-started (writtenAt: 2024-01-15T10:30:00.000Z)
// - api-reference (writtenAt: 2024-01-15T10:30:00.000Z)
} catch (error) {
console.error('onBeforeWrite pipeline failed:', error)
}
}
main()
Related
runHook
Uses
onAfterGenerate
Uses
onAfterWrite
Uses
onAfterGenerate
Used by
onAfterWrite
Used by
PluginManager
Related
onInit
async onInit(): Promise<void>
Use this to trigger the initialization lifecycle hook across all registered plugins in a PluginManager instance. Call this once after all plugins have been registered and before any scanning or processing begins.
This method delegates to the internal runHook('onInit') mechanism, ensuring every plugin's onInit handler is executed in sequence.
Parameters
None
Returns
| Type | Description |
|---|---|
Promise<void> | Resolves when all plugin onInit hooks have completed. Rejects if any plugin's onInit throws an error. |
When to Call
- After instantiating
PluginManagerand registering all plugins - Before calling
onBeforeScan()or any other lifecycle hooks - Typically called once per application startup
Example
// Inline types to simulate the PluginManager lifecycle
type HookName = 'onInit' | 'onBeforeScan' | 'onAfterScan'
type Plugin = {
name: string
onInit?: () => Promise<void>
onBeforeScan?: () => Promise<void>
}
// Simulated PluginManager class (self-contained, no external imports)
class PluginManager {
private plugins: Plugin[] = []
register(plugin: Plugin): void {
this.plugins.push(plugin)
console.log(`Plugin registered: ${plugin.name}`)
}
private async runHook(hookName: HookName): Promise<void> {
for (const plugin of this.plugins) {
const hook = plugin[hookName]
if (typeof hook === 'function') {
await hook()
}
}
}
async onInit(): Promise<void> {
await this.runHook('onInit')
}
async onBeforeScan(): Promise<void> {
await this.runHook('onBeforeScan')
}
}
// Example plugins with onInit handlers
const analyticsPlugin: Plugin = {
name: 'analytics-plugin',
onInit: async () => {
const apiKey = process.env.ANALYTICS_API_KEY || 'demo-key-abc123'
console.log(`[analytics-plugin] Initialized with key: ${apiKey.slice(0, 8)}...`)
},
}
const cachePlugin: Plugin = {
name: 'cache-plugin',
onInit: async () => {
console.log('[cache-plugin] Cache warmed up and ready')
},
}
const loggingPlugin: Plugin = {
name: 'logging-plugin',
// No onInit — should be safely skipped
}
async function main() {
const manager = new PluginManager()
manager.register(analyticsPlugin)
manager.register(cachePlugin)
manager.register(loggingPlugin)
console.log('\n--- Running onInit lifecycle hook ---')
try {
await manager.onInit()
console.log('\n✓ All plugin onInit hooks completed successfully')
// Expected output:
// Plugin registered: analytics-plugin
// Plugin registered: cache-plugin
// Plugin registered: logging-plugin
// --- Running onInit lifecycle hook ---
// [analytics-plugin] Initialized with key: demo-key...
// [cache-plugin] Cache warmed up and ready
// ✓ All plugin onInit hooks completed successfully
} catch (error) {
console.error('✗ Plugin initialization failed:', error)
process.exit(1)
}
}
main()
Related
register
register(plugin: SkryptPlugin): void
Use this to manually register a plugin with the PluginManager, adding it to the active plugin list so its hooks can be executed during the build or runtime lifecycle.
This is the direct registration path — useful when you have a plugin object already constructed in memory and don't need to load it from disk.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
plugin | SkryptPlugin | ✅ Yes | The plugin object to register. Must have at minimum a name property; version is optional but will be included in the log output if present. |
Returns
void — Pushes the plugin into the internal plugins array and logs a confirmation message to the console in the format:
Loaded plugin: <name>(no version)Loaded plugin: <name> v<version>(with version)
Behavior Notes
- Plugins are stored in insertion order and hooks are run in that order.
- There is no deduplication check — registering the same plugin twice will result in it being added twice.
- The log output is a side effect of every registration call.
Example
// --- Inline types (do not import from skrypt) ---
type HookFn = (...args: unknown[]) => unknown | Promise<unknown>
interface SkryptPlugin {
name: string
version?: string
onBuildStart?: HookFn
onBuildEnd?: HookFn
transform?: HookFn
}
// --- Inline PluginManager implementation ---
class PluginManager {
private plugins: SkryptPlugin[] = []
register(plugin: SkryptPlugin): void {
this.plugins.push(plugin)
console.log(
`Loaded plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''}`
)
}
async runHook<T>(hook: keyof SkryptPlugin, ...args: unknown[]): Promise<T | undefined> {
for (const plugin of this.plugins) {
const fn = plugin[hook]
if (typeof fn === 'function') {
const result = await fn(...args)
if (result !== undefined) return result as T
}
}
return undefined
}
getPlugins(): SkryptPlugin[] {
return [...this.plugins]
}
}
// --- Usage example ---
const manager = new PluginManager()
// Plugin with a version
const sassPlugin: SkryptPlugin = {
name: 'sass-compiler',
version: '2.1.0',
onBuildStart: async () => {
console.log(' [sass-compiler] Compiling stylesheets...')
},
}
// Plugin without a version
const envPlugin: SkryptPlugin = {
name: 'env-injector',
onBuildStart: async () => {
console.log(' [env-injector] Injecting environment variables...')
},
}
async function main() {
try {
manager.register(sassPlugin)
// Output: Loaded plugin: sass-compiler v2.1.0
manager.register(envPlugin)
// Output: Loaded plugin: env-injector
console.log('\nRegistered plugins:', manager.getPlugins().map(p => p.name))
// Output: Registered plugins: [ 'sass-compiler', 'env-injector' ]
console.log('\nRunning onBuildStart hooks...')
await manager.runHook('onBuildStart')
// Output:
// [sass-compiler] Compiling stylesheets...
// [env-injector] Injecting environment variables...
} catch (error) {
console.error('Plugin registration failed:', error)
}
}
main()
Related
runHook
async runHook<T>(hook: keyof SkryptPlugin, ...args: unknown[]): Promise<T | undefined>
Use this to execute a named hook across all registered plugins in sequence, passing data through each plugin's implementation of that hook.
runHook iterates over every registered plugin, finds those that implement the specified hook method, and calls them in registration order. Each plugin receives the result of the previous plugin, enabling a pipeline/middleware pattern where plugins can transform data as it flows through.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
hook | keyof SkryptPlugin | Yes | The name of the plugin lifecycle method to invoke (e.g., 'beforeCompile', 'afterCompile') |
...args | unknown[] | No | Arguments passed to the hook. The first argument (args[0]) is used as the initial result value and passed through the plugin chain |
Returns
| Condition | Return Value |
|---|---|
| One or more plugins implement the hook | Promise<T> — the final transformed value after all plugins have processed it |
| No plugins implement the hook | Promise<undefined> — resolves with the initial args[0] cast as T, or undefined if no args provided |
The return type T is inferred from the generic parameter you provide at the call site.
Example
// --- Inline types (do not import from skrypt) ---
interface SkryptPlugin {
name: string
version?: string
beforeCompile?: (source: string) => Promise<string> | string
afterCompile?: (output: string) => Promise<string> | string
onError?: (error: Error) => Promise<void> | void
}
// --- Inline PluginManager implementation ---
class PluginManager {
private plugins: SkryptPlugin[] = []
register(plugin: SkryptPlugin): void {
this.plugins.push(plugin)
console.log(
`Loaded plugin: ${plugin.name}${plugin.version ? ` v${plugin.version}` : ''}`
)
}
async runHook<T>(hook: keyof SkryptPlugin, ...args: unknown[]): Promise<T | undefined> {
let result = args[0] as T
for (const plugin of this.plugins) {
const fn = plugin[hook] as ((...args: unknown[]) => Promise<T> | T) | undefined
if (typeof fn === 'function') {
result = await fn.call(plugin, result)
}
}
return result
}
}
// --- Example plugins ---
const stripCommentsPlugin: SkryptPlugin = {
name: 'strip-comments',
version: '1.0.0',
beforeCompile(source: string) {
console.log('[strip-comments] Removing comments...')
return source.replace(/\/\/.*$/gm, '').trim()
},
}
const minifyPlugin: SkryptPlugin = {
name: 'minify',
version: '2.1.0',
beforeCompile(source: string) {
console.log('[minify] Minifying source...')
return source.replace(/\s+/g, ' ').trim()
},
afterCompile(output: string) {
console.log('[minify] Post-processing output...')
return `/* minified */\n${output}`
},
}
const bannerPlugin: SkryptPlugin = {
name: 'banner',
afterCompile(output: string) {
console.log('[banner] Injecting banner...')
return `// Built with Skrypt\n${output}`
},
}
// --- Run the example ---
async function main() {
const manager = new PluginManager()
manager.register(stripCommentsPlugin)
manager.register(minifyPlugin)
manager.register(bannerPlugin)
const rawSource = `
// This is a comment
let x = 1
let y = 2
console.log(x + y)
`
try {
// Run the beforeCompile hook — source flows through each plugin in order
const processedSource = await manager.runHook<string>('beforeCompile', rawSource)
console.log('\n--- Processed Source ---')
console.log(processedSource)
// Output: "let x = 1 let y = 2 console.log(x + y)"
// Run the afterCompile hook — only minify and banner plugins implement it
const finalOutput = await manager.runHook<string>('afterCompile', processedSource)
console.log('\n--- Final Output ---')
console.log(finalOutput)
// Output:
// // Built with Skrypt
// /* minified */
// let x = 1 let y = 2 console.log(x + y)
// Run a hook with no implementations — returns the initial value unchanged
const unchanged = await manager.runHook<string>('onError', 'no-op value')
console.log('\n--- Hook with no matching implementations ---')
console.log(unchanged) // Output: no-op value (onError expects void, shown for demonstration)
} catch (error) {
console.error('Hook execution failed:', error)
}
}
main()
Related
register
Uses
register
Used by
onInit
Used by
onBeforeScan
Used by
onAfterScan
Used by
onBeforeGenerate
Used by
transformContent
async transformContent(content: string, filePath: string): Promise<string>
Use this to pipe file content through a chain of registered plugins, each of which can modify the content before it's written to disk. Ideal for applying transformations like syntax highlighting, markdown processing, code formatting, or custom replacements across your documentation pipeline.
Each plugin in the manager that implements transformContent is called in sequence — the output of one becomes the input of the next.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
content | string | ✅ | The raw file content to be transformed |
filePath | string | ✅ | The path of the file being processed — plugins can use this to apply conditional logic based on file type or location |
Returns
| Condition | Return Value |
|---|---|
| All plugins succeed | Promise<string> — the fully transformed content after passing through every plugin in the chain |
| A plugin throws | The error propagates — content from that point forward is not processed |
No plugins implement transformContent | Promise<string> — original content returned unchanged |
Example
// Inline types — no external imports needed
type TransformPlugin = {
name: string
transformContent?: (content: string, filePath: string) => Promise<string>
}
// Simulated PluginManager class
class PluginManager {
private plugins: TransformPlugin[]
constructor(plugins: TransformPlugin[]) {
this.plugins = plugins
}
async transformContent(content: string, filePath: string): Promise<string> {
let result = content
for (const plugin of this.plugins) {
if (typeof plugin.transformContent === 'function') {
try {
result = await plugin.transformContent(result, filePath)
console.log(`[${plugin.name}] transformation applied`)
} catch (error) {
console.error(`[${plugin.name}] failed to transform content:`, error)
throw error
}
}
}
return result
}
}
// Example plugins — each transforms content in sequence
const markdownHeaderPlugin: TransformPlugin = {
name: 'markdown-header',
transformContent: async (content, filePath) => {
if (!filePath.endsWith('.md')) return content
const timestamp = new Date().toISOString()
return `<!-- Generated: ${timestamp} -->\n${content}`
}
}
const codeFencePlugin: TransformPlugin = {
name: 'code-fence-formatter',
transformContent: async (content, _filePath) => {
// Normalize code fences: replace ``` with properly spaced versions
return content.replace(/```(\w+)/g, '```$1\n').trimEnd()
}
}
const noOpPlugin: TransformPlugin = {
name: 'logger-only'
// No transformContent — will be skipped automatically
}
async function main() {
try {
const manager = new PluginManager([
markdownHeaderPlugin,
codeFencePlugin,
noOpPlugin
])
const rawContent = `# My Docs\n\nHere is some code:\n\`\`\`typescript\nconst x = 1\n\`\`\``
const filePath = '/docs/guide/getting-started.md'
console.log('--- Input ---')
console.log(rawContent)
console.log('\n--- Transforming... ---')
const transformed = await manager.transformContent(rawContent, filePath)
console.log('\n--- Output ---')
console.log(transformed)
// Expected output:
// <!-- Generated: 2024-01-15T10:30:00.000Z -->
// # My Docs
//
// Here is some code:
// ```typescript
//
// const x = 1
// ```
} catch (error) {
console.error('Transformation pipeline failed:', error)
process.exit(1)
}
}
main()