Skip to content

Core API Reference

PluginManager

class PluginManager
TypeScript

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

NameTypeRequiredDescription
sourcePathstringYesAbsolute or relative path to the source files directory
outputPathstringYesAbsolute or relative path where generated docs will be written
configRecord<string, unknown>NoArbitrary config values passed to every plugin via context (defaults to {})

Plugin Context

Every registered plugin receives a shared context object:

FieldTypeDescription
sourcePathstringThe source path passed to the constructor
outputPathstringThe output path passed to the constructor
configRecord<string, unknown>The config object passed to the constructor
logger.info(msg: string) => voidLogs an info message prefixed with [plugin]
logger.warn(msg: string) => voidLogs a warning prefixed with [plugin]
logger.error(msg: string) => voidLogs an error prefixed with [plugin]

Key Behaviors

  • Plugins are stored and executed in registration order
  • Each plugin receives the same shared PluginContext instance
  • 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()
TypeScript

autoFixBatch

async function autoFixBatch(examples: CodeExample[], client: LLMClient, options: AutoFixOptions = {}): Promise<Map<number, FixResult>>
TypeScript

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

NameTypeRequiredDescription
examplesCodeExample[]Array of code examples to fix. Each example contains the code string and metadata.
clientLLMClientLLM client instance used to generate fixes for broken examples.
optionsAutoFixOptionsConfiguration 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 FixResult object containing:
    • success: boolean — whether the fix was applied successfully
    • fixedCode?: string — the repaired code (present when success is true)
    • error?: string — description of what went wrong (present when success is false)
    • 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()
TypeScript

autoFixExample

async function autoFixExample(example: CodeExample, client: LLMClient, options: AutoFixOptions = {}): Promise<FixResult>
TypeScript

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

NameTypeRequiredDescription
exampleCodeExampleThe code example to fix, including source code, language, and any known error context
clientLLMClientAn LLM client instance used to generate fixes (e.g., OpenAI, Anthropic wrapper)
optionsAutoFixOptionsConfiguration options such as maxIterations (default: 3) to control retry behavior

Returns

Returns a Promise<FixResult> that resolves with:

FieldTypeDescription
successbooleanWhether the example was successfully fixed
fixedCodestring | nullThe corrected source code, or null if fixing failed
iterationsnumberHow many LLM fix attempts were made
changesstring[]A list of descriptions of what was changed
errorstring | undefinedError 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()
TypeScript

checkPlan

async function checkPlan(apiKey: string): Promise<PlanCheckResponse>
TypeScript

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

NameTypeRequiredDescription
apiKeystringYesThe 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.

ScenarioResult
Valid API keyResolves with plan info (e.g., tier, limits, expiry)
Invalid / expired keyRejects or resolves with an error/unauthorized response
Network failureRejects 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()
TypeScript

clearAuth

async function clearAuth(): Promise<void>
TypeScript

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:

  1. System keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager)
  2. Local auth file on disk (typically ~/.config/yourapp/auth.json or similar)

After calling this, any subsequent authenticated requests will fail until the user logs in again.

Parameters

This function takes no parameters.

Returns

TypeDescription
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()
TypeScript

createLLMClient

function createLLMClient(config: {
  provider: LLMProvider
  model: string
  baseUrl?: string
  timeout?: number
  maxRetries?: number
}): LLMClient
TypeScript

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

NameTypeRequiredDescription
config.providerLLMProvider✅ YesThe AI provider to use. Accepted values: "openai", "anthropic", "azure"
config.modelstring✅ YesThe model identifier to target (e.g. "gpt-4o", "claude-3-5-sonnet-20241022")
config.baseUrlstringNoOverride the default API endpoint. Useful for proxies, local models, or Azure deployments
config.timeoutnumberNoRequest timeout in milliseconds. Defaults to 30000 (30s)
config.maxRetriesnumberNoNumber 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 the provider value.

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()
TypeScript

createPythonValidator

function createPythonValidator(): (code: string) => Promise<ValidationResult>
TypeScript

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:

NameTypeRequiredDescription
codestringThe 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>:

ScenarioResult 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: python3 must be installed and accessible via PATH. The validator uses python3 -m py_compile internally 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)
TypeScript

createTypeScriptValidator

function createTypeScriptValidator(): (code: string) => Promise<ValidationResult>
TypeScript

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

ValueTypeDescription
validator(code: string) => Promise<ValidationResult>A reusable async function that accepts a TypeScript code string and resolves to a ValidationResult

ValidationResult Shape

FieldTypeDescription
validbooleantrue if the code compiled without errors
errorsstring[]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. typescript not 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()
TypeScript

definePlugin

function definePlugin(plugin: SkryptPlugin): SkryptPlugin
TypeScript

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

NameTypeRequiredDescription
pluginSkryptPluginThe 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)
TypeScript

fixCodeSample

async function fixCodeSample(client: LLMClient, code: string, error: string, context: string, iteration: number = 1): Promise<string>
TypeScript

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

NameTypeRequiredDescription
clientLLMClientAn initialized LLM client instance used to generate the fix
codestringThe broken code sample that needs to be repaired
errorstringThe error message or stack trace produced by the broken code
contextstringAdditional context about what the code is supposed to do (e.g., function docs, expected behavior)
iterationnumberWhich 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 iteration parameter enables escalating fix strategies: pass 2 or 3 on retry loops to signal that simpler fixes have already been attempted
  • The context parameter 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()
TypeScript

generateDocumentation

async function generateDocumentation(client: LLMClient, element: ElementContext, options?: { multiLanguage?: boolean }): Promise<GeneratedDocResult>
TypeScript

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

NameTypeRequiredDescription
clientLLMClient✅ YesA configured LLM client instance used to generate the documentation
elementElementContext✅ YesContext object describing the code element to document (name, signature, source, etc.)
options{ multiLanguage?: boolean }❌ NoOptional 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:

FieldDescription
markdownThe generated documentation in Markdown format
codeA self-contained code example
summaryA 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()
TypeScript

getAuthConfig

function getAuthConfig(): AuthConfig
TypeScript

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:

FieldTypeAlways PresentDescription
apiKeystring | undefinedNoThe API key, sourced from SKRYPT_API_KEY env var or auth file
emailstring | undefinedNoThe 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

PrioritySourceNotes
1stSKRYPT_API_KEY env varAlways checked first
2ndLocal auth file~/.skrypt/auth.json or similar
KeychainNever 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
TypeScript

getAuthConfigAsync

async function getAuthConfigAsync(): Promise<AuthConfig>
TypeScript

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

ConditionResult
SKRYPT_API_KEY env var is setReturns AuthConfig with API key from env + email from auth file
Env var not set, keychain has credentialsReturns AuthConfig from keychain
Neither env nor keychain, auth file existsReturns AuthConfig read from auth file
No credentials found anywhereThrows or returns empty/null config

AuthConfig Shape

FieldTypeDescription
apiKeystringThe API key used to authenticate requests
emailstring | undefinedThe 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()
TypeScript

getKeyStorageMethod

async function getKeyStorageMethod(): Promise<'keychain' | 'file' | 'env' | 'none'>
TypeScript

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

ValueDescription
'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_KEY is 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()
TypeScript

requirePro

async function requirePro(commandName: string): Promise<boolean>
TypeScript

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

NameTypeRequiredDescription
commandNamestringThe name of the CLI command being gated. Shown in the error message so users know which command requires Pro.

Returns

ValueWhen
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 free or invalid → prints upgrade prompt and returns false
  • If Pro is confirmed → returns true silently, 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()
TypeScript

runImport

function runImport(dir: string, format: ImportFormat, name?: string): ImportResult
TypeScript

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

NameTypeRequiredDescription
dirstring✅ YesPath to the documentation directory to import
formatImportFormat✅ YesThe detected format of the docs ('mintlify', 'docusaurus', 'gitbook', 'readme', 'confluence', 'markdown')
namestring❌ NoOptional 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.

ScenarioResult
Successful importImportResult with parsed pages, metadata, and file tree
Unrecognized formatThrows or falls through to default handler
Invalid directoryThrows 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()
TypeScript

saveAuthConfig

async function saveAuthConfig(config: AuthConfig): Promise<void>
TypeScript

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

NameTypeRequiredDescription
configAuthConfigYesAuthentication configuration object containing credentials to persist
config.apiKeystringNoAPI 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 0o700 permissions (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 saveAuthConfig any 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()
TypeScript

scanDirectory

async function scanDirectory(dir: string, options: ScanOptions = {}): Promise<ScanAllResult>
TypeScript

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

NameTypeRequiredDescription
dirstringPath to the directory or single file to scan
optionsScanOptionsConfiguration to control which files are included or excluded
options.includestring[]Glob patterns for files to scan. Defaults to ['**/*.py', '**/*.ts', '**/*.js', '**/*.go', '**/*.rs']
options.excludestring[]Glob patterns for files to skip. Defaults to ['**/node_modules/**', '**/__pycache__/**', '**/dist/**']

Returns

Returns a Promise<ScanAllResult> that resolves to an object containing:

FieldTypeDescription
filesScanResult[]One entry per scanned file, each containing the file path and its discovered API elements
totalElementsnumberTotal count of API elements found across all files
errorsScanError[]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>
TypeScript

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

NameTypeRequiredDescription
filePathstringAbsolute or relative path to the source file to scan

Returns

Returns Promise<ScanResult> — resolves with a structured object containing:

FieldTypeDescription
languagestringDetected language ('typescript', 'javascript', 'python', etc.)
symbolsSymbol[]Array of discovered functions, classes, types, and other declarations
filePathstringThe 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()
TypeScript

constructor

constructor(sourcePath: string, outputPath: string, config: Record<string, unknown> = {})
TypeScript

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

NameTypeRequiredDescription
sourcePathstring✅ YesAbsolute or relative path to the source code directory to be processed
outputPathstring✅ YesAbsolute or relative path to the directory where generated docs will be written
configRecord<string, unknown>❌ NoOptional 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

  • sourcePath and outputPath are stored on the shared PluginContext, making them accessible to every plugin that runs
  • Passing config here 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()
TypeScript

loadPlugins

async loadPlugins(configPath?: string): Promise<void>
TypeScript

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

NameTypeRequiredDescription
configPathstringNoAbsolute 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 configPath overrides 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()
TypeScript

onAfterGenerate

async onAfterGenerate<T>(docs: T[]): Promise<T[]>
TypeScript

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

NameTypeRequiredDescription
docsT[]YesThe array of generated documentation objects to pass through all registered plugin hooks. The generic type T can be any doc shape.

Returns

ConditionReturns
A plugin hook transforms the docsPromise<T[]> — the transformed array returned by the plugin
No plugin hook is registered / hook returns falsyPromise<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()
TypeScript

onAfterScan

async onAfterScan<T>(elements: T[]): Promise<T[]>
TypeScript

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

NameTypeRequiredDescription
elementsT[]YesThe 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()
TypeScript

onAfterWrite

async onAfterWrite(): Promise<void>
TypeScript

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

TypeDescription
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:

  1. onBeforeWrite(docs) — transform/filter docs before writing
  2. (write files to disk)
  3. 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()
TypeScript

onBeforeGenerate

async onBeforeGenerate<T>(elements: T[]): Promise<T[]>
TypeScript

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

NameTypeRequiredDescription
elementsT[]YesThe array of elements (e.g., parsed code nodes, doc entries) to be processed by registered plugin hooks before generation

Returns

ConditionReturns
A plugin hook transforms the elementsPromise<T[]> — the modified array returned by the hook
No plugin hook is registered or hook returns falsyPromise<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()
TypeScript

onBeforeScan

async onBeforeScan(): Promise<void>
TypeScript

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

TypeDescription
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
TypeScript

onBeforeWrite

async onBeforeWrite<T>(docs: T[]): Promise<T[]>
TypeScript

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

NameTypeRequiredDescription
docsT[]YesArray of documents to be passed through all registered onBeforeWrite plugin hooks before writing

Returns

ConditionReturn Value
One or more plugins transform the docsPromise<T[]> — the transformed array returned by the last hook in the chain
No plugins modify the docsPromise<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()
TypeScript

onInit

async onInit(): Promise<void>
TypeScript

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

TypeDescription
Promise<void>Resolves when all plugin onInit hooks have completed. Rejects if any plugin's onInit throws an error.

When to Call

  • After instantiating PluginManager and 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()
TypeScript

register

register(plugin: SkryptPlugin): void
TypeScript

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

NameTypeRequiredDescription
pluginSkryptPlugin✅ YesThe 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()
TypeScript

runHook

async runHook<T>(hook: keyof SkryptPlugin, ...args: unknown[]): Promise<T | undefined>
TypeScript

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

NameTypeRequiredDescription
hookkeyof SkryptPluginYesThe name of the plugin lifecycle method to invoke (e.g., 'beforeCompile', 'afterCompile')
...argsunknown[]NoArguments passed to the hook. The first argument (args[0]) is used as the initial result value and passed through the plugin chain

Returns

ConditionReturn Value
One or more plugins implement the hookPromise<T> — the final transformed value after all plugins have processed it
No plugins implement the hookPromise<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()
TypeScript

transformContent

async transformContent(content: string, filePath: string): Promise<string>
TypeScript

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

NameTypeRequiredDescription
contentstringThe raw file content to be transformed
filePathstringThe path of the file being processed — plugins can use this to apply conditional logic based on file type or location

Returns

ConditionReturn Value
All plugins succeedPromise<string> — the fully transformed content after passing through every plugin in the chain
A plugin throwsThe error propagates — content from that point forward is not processed
No plugins implement transformContentPromise<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()
TypeScript

Was this helpful?