Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Node Scripts

TypeScript scripts for Node-specific tasks (markdown linting, AST processing, etc.) that lack mature Python equivalents. Runs on Bun — no build step required.


Prerequisites

  • Bun ≥1.3.14 — the runtime and package manager for this workspace. Install: curl -fsSL https://bun.sh/install | bash
  • TypeScript 6.x — for type checking only (tsc --noEmit).

All dependencies are declared in package.json and resolved via bun install.


Directory Layout

scripts/node/
  src/
    cli/                 # CLI entry points (one per script, cleye framework)
      lint-md.ts         # Markdown linter — cleye CLI wrapper
      example.ts         # Example script — cleye CLI wrapper
    lib/
      lint-md/           # Per-script library package for lint-md
        core.ts          # remark processing logic
        rules.ts         # Custom remark plugins
      example/           # Per-script library package for example
        core.ts          # Example logic
      shared/            # Utilities shared across all scripts
        exit-codes.ts    # ExitCode enum (CLEAN, VIOLATIONS, CONFIG_ERROR, INVALID_INPUT)
        format.ts        # formatViolation(), die() — error/output formatting helpers
        path.ts          # resolveTarget(), isExempted() — path resolution utilities
  tests/
    lint-md.test.ts           # Unit tests for lib/lint-md/
    lint-md.cli.test.ts       # CLI integration tests (Bun.spawnSync)
    example.test.ts           # Unit tests for lib/example/
    example.cli.test.ts       # CLI integration tests for example
    shared.test.ts            # Tests for lib/shared/
    .gitkeep                  # Ensures tests/ directory is versioned
  package.json           # Script entries, dependencies, devDependencies
  tsconfig.json          # TypeScript config (type checking only — noEmit)
  biome.json             # Lint and format configuration
  README.md              # This file
  .gitignore             # Git ignores (node_modules, bun.lock, etc.)
  bun.lock               # Lockfile (auto-generated by bun install)

Key conventions:

  • src/cli/ — One file per CLI command, uses cleye for argument parsing.
  • src/lib/<name>/ — Per-script library modules (e.g., src/lib/lint-md/core.ts).
  • src/lib/shared/ — Code shared across all scripts (exit codes, formatting, path utils).
  • tests/ — One .test.ts file per lib module, one .cli.test.ts file per CLI command.

CLI Conventions

All CLI entry points use cleye for argument parsing.

Pattern

import { cli } from 'cleye';

const argv = cli({
  name: 'lint-md',
  version: '1.0.0',
  parameters: ['<input-path>'],
});

// argv._.inputPath is typed as string
// --help is auto-generated

Invocation

# Via package.json script entry:
bun run --cwd scripts/node lint:md -- <path>

# Directly with Bun:
bun src/cli/lint-md.ts -- <path>

All scripts accept --help (auto-generated by cleye) and --version.


Exit Code Reference

Code Constant Meaning
0 ExitCode.CLEAN Success — no violations
1 ExitCode.VIOLATIONS Violations found (e.g., lint errors)
2 ExitCode.CONFIG_ERROR Configuration error
3 ExitCode.INVALID_INPUT Invalid input (file not found, bad args)

The ExitCode enum is defined in src/lib/shared/exit-codes.ts and reused by all scripts.

import { ExitCode } from '../lib/shared/exit-codes.ts';

function die(message: string, code: ExitCode): never {
  process.stderr.write(`Error: ${message}\n`);
  process.exit(code);
}

// Usage:
die('file not found', ExitCode.INVALID_INPUT);

Error Message Formatting

  • All error messages are written to stderr via process.stderr.write().
  • Format: Error: <human-readable description>
  • Include the specific cause (file name, invalid value, etc.).
  • Never include stack traces in user-facing output.
  • Exit with the appropriate non-zero ExitCode.

The die() helper in src/lib/shared/format.ts encapsulates this pattern.


Lint and Format (Biome)

Biome handles both linting and formatting in a single tool.

# Check lint + format (read-only):
bun run --cwd scripts/node lint

# Apply fixes automatically:
bun run --cwd scripts/node lint:fix

These map to biome check src/ and biome check --apply src/ respectively. Configuration is in biome.json — includes src/**/*.ts and tests/**/*.ts.


Type Checking

TypeScript is used for type checking only — Bun handles runtime execution directly.

bun run --cwd scripts/node typecheck

This maps to tsc --noEmit using tsconfig.json with strict: true and noEmit: true.


Testing (bun test)

Tests use bun test — zero configuration, built-in coverage.

# Run all tests:
bun run --cwd scripts/node test

# With coverage:
bun run --cwd scripts/node test:coverage

Test structure

File What it tests
tests/<name>.test.ts Unit tests for src/lib/<name>/ modules
tests/<name>.cli.test.ts CLI integration tests (spawn entry points, assert exit codes)
tests/shared.test.ts Tests for shared utilities in src/lib/shared/

CLI integration pattern (Bun.spawnSync)

import { describe, expect, it } from 'bun:test';
import { spawnSync } from 'bun';

describe('lint-md CLI', () => {
  it('exits 0 on clean markdown', () => {
    const proc = spawnSync(['bun', 'src/cli/lint-md.ts', 'test/fixtures/clean.md'], {
      cwd: import.meta.dir,
    });
    expect(proc.exitCode).toBe(0);
    expect(proc.stderr.toString()).toBe('');
  });

  it('exits 3 on invalid input', () => {
    const proc = spawnSync(['bun', 'src/cli/lint-md.ts', '/nonexistent/path.md'], {
      cwd: import.meta.dir,
    });
    expect(proc.exitCode).toBe(3);
    expect(proc.stderr.toString()).toContain('Error:');
  });
});

Temp file fixture pattern

import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'bun';
import { describe, expect, it, beforeAll, afterAll } from 'bun:test';

describe('lint-md with temp files', () => {
  let tmpDir: string;

  beforeAll(() => {
    tmpDir = mkdtempSync(join(tmpdir(), 'lint-md-test-'));
    writeFileSync(join(tmpDir, 'good.md'), '# Title\n\nSome content.');
  });

  afterAll(() => {
    rmSync(tmpDir, { recursive: true, force: true });
  });

  it('lints a temp file', () => {
    const proc = spawnSync(['bun', 'src/cli/lint-md.ts', join(tmpDir, 'good.md')], {
      cwd: import.meta.dir,
    });
    expect(proc.exitCode).toBe(0);
  });
});

Shared Lib Convention

Code shared across all scripts lives in src/lib/shared/:

File Exports Purpose
exit-codes.ts ExitCode enum Standard exit codes (CLEAN, VIOLATIONS, CONFIG_ERROR, INVALID_INPUT)
format.ts formatViolation(), die() Error/output formatting helpers
path.ts resolveTarget(), isExempted() Path resolution and exemption utilities

Import convention

import { ExitCode } from '../lib/shared/exit-codes.ts';
import { formatViolation, die } from '../lib/shared/format.ts';
import { resolveTarget, isExempted } from '../lib/shared/path.ts';

Skill Integration

Skills invoke Node scripts using the canonical pattern:

bun run --cwd ~/.config/opencode/scripts/node <script> -- [args]

Examples

# Lint a markdown file:
bun run --cwd ~/.config/opencode/scripts/node lint:md -- ~/.config/opencode/.proposals/my-proposal.md

# Run the example script:
bun run --cwd ~/.config/opencode/scripts/node example

From a skill (SKILL.md)

Run the linter:
\`\`\`bash
bun run --cwd ~/.config/opencode/scripts/node lint:md -- <path>
\`\`\`

The --cwd flag ensures Bun uses the correct package.json for dependency resolution. The -- separator prevents cleye from interpreting file paths as flags.

Platform selection

Python is the default platform for script tasks. Node is used only when the core logic requires a Node-specific library (remark, mdast, babel, TypeScript compiler API) that has no mature Python equivalent. See the Node Script Support proposal for the full decision framework.


Maintenance: How to Add a New Script

  1. Create CLI entry pointsrc/cli/<name>.ts

    • Use cleye for argument parsing.
    • Import ExitCode from ../lib/shared/exit-codes.ts.
    • Write errors to stderr with die() from ../lib/shared/format.ts.
  2. Create library modulesrc/lib/<name>/core.ts

    • Implement the core logic here.
    • Keep CLI entry point thin (parse args → call core → exit).
  3. Register in package.json

    • Add a "scripts" entry: "<name>": "bun src/cli/<name>.ts --"
    • Add any new dependencies to "dependencies".
  4. Add tests

    • tests/<name>.test.ts — unit tests for library functions.
    • tests/<name>.cli.test.ts — CLI integration tests.
  5. Verify

    bun run --cwd scripts/node lint        # biome check
    bun run --cwd scripts/node typecheck   # tsc --noEmit
    bun run --cwd scripts/node test        # bun test
  6. Create or update a skill to invoke the new script (see Skill Integration above).