Skip to content

Expose global file() function in the functional API #7

@masonmark

Description

@masonmark

Summary

Currently, the only way to configure file logging is via the class API:

import { createScript } from '@axhxrx/script';

const script = createScript();
await script.file({ path: logPath, output: 'full', timestamps: true });
script.add('echo hello').description('Say hello');
await script.execute();

This forces scripts that need file logging to use createScript() even when they don't need any other class-level features. It would be cleaner to expose a global file() function that delegates to Script.default.file(...), matching the pattern of the existing global functions (add, banner, validate, execute):

import { add, banner, execute, file, validate } from '@axhxrx/script';

await file({ path: logPath, output: 'full', timestamps: true });
validate('Running on Linux', isLinux);
banner('Do stuff');
add('echo hello').description('Say hello');
await execute();

This would keep all scripts on the functional API and eliminate the createScript() ceremony when file logging is the only reason for using the class API.

Claude skill for @axhxrx/script

We built a Claude skill that documents the conventions and API patterns for writing scripts with this library. It covers file structure, style rules (named functions for validations/skipIf, Node.js APIs over Deno-specific ones, etc.), and a quick API reference. Attaching it here for reference — it could be useful as a starting point for an official skill to ship with the library.

axhxrx-script SKILL.md
---
name: axhxrx-script
description: Use the @axhxrx/script library to write readable, maintainable TypeScript scripts instead of shell scripts.
---

# @axhxrx/script — TypeScript Setup Scripts

Write setup and automation scripts using `@axhxrx/script`, a TypeScript DSL that replaces shell scripts. Use this skill when writing new install/setup scripts or refactoring existing ones.

## Why this library exists

Shell scripts are hard to debug, hard to test, and their syntax is a nightmare. This library replaces them with TypeScript:
- Steps are queued with `add()`, shown as a plan, then executed sequentially
- Pre-flight validations catch problems before anything runs
- `--dry-run` and `--yes` flags come free
- File logging with timestamps and auto-redaction of secrets
- `.skipIf()` for idempotent steps, `.or()` / `.and()` for conditional chaining
- Function steps let you use TypeScript (fetch, fs, etc.) instead of `curl | python3` hacks

Scripts are published to JSR and runnable on any machine with Deno — no install step needed. They can also be used with Bun, and in most cases also Node.js 24+

## Script file structure

Order matters. Scripts should read top-to-bottom as "what this does", with implementation details at the bottom.

1. Shebang:           #!/usr/bin/env -S deno run -A
2. JSDoc:             What the script does and how to run it
3. Imports:           @axhxrx/script, node:fs, node:process, etc.
4. Constants:         Paths, target values, config
5. Async init:        Top-level await for data that steps depend on
6. Validations:       validate() calls referencing named functions
7. Steps:             banner() + add() chains — the core of the script
8. Execute:           await execute() or await script.execute()
9. Final message:     Post-execution output
10. Utility functions: All named functions used above

Functions are hoisted, so utility functions defined at the bottom are available throughout the file. This keeps the "what" at the top and the "how" at the bottom.

## Style rules

### Use named functions for validations and skipIf conditions

Named functions are used in user-facing output (function names appear in logs and will be used for skip reasons in future versions). They also read better in the step declarations.

CORRECT:
validate('Running on Linux', isLinux);
add('git clone ...').description('Clone repo').skipIf(isRepoAlreadyCloned);

INCORRECT:
validate('Running on Linux', () => process.platform === 'linux' || 'Not Linux');
add('git clone ...').description('Clone repo').skipIf(() => existsSync(path));

### Use .skipIf() instead of conditional add()

Let every step appear in the plan. Use .skipIf() so the skip is logged at execution time.

### Blank lines between step chains

Each add() chain gets a blank line before and after it.

### Use Node.js APIs, not Deno-specific APIs

Scripts should be runtime-agnostic. Use node: imports for everything that has a Node.js equivalent (process.env, process.platform, readFileSync, etc.).

### JSDoc format

No leading asterisks on body lines. Paragraphs separated by blank lines.

(Filed by Claude Opus 4.6 (1M context), running as a VS Code extension agent, per verbal request of @masonmark)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions