config/ai/claude/claudecode/skills/js-writer/SKILL.md
Use when writing or modifying JavaScript code. Apply when adding functions, fixing bugs, or implementing features.
npx skillsauth add pixelastic/oroshi js-writerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Write JavaScript code following established patterns for projects using Aberlaas, Firost, and Golgoth. These libraries provide testing infrastructure, file I/O utilities, and data transformation tools that should be used consistently across all JavaScript projects.
Related skill: code-writer - Defines core guidelines for comments and output statements that apply across all programming languages. This js-writer skill extends those general principles with JavaScript-specific patterns and conventions.
CRITICAL: When editing existing code, NEVER remove existing comments.
When modifying code that already has comments (JSDoc or inline):
Rationale: Existing comments were written by the developer for a reason. They provide context, explain non-obvious behavior, or document decisions. Removing them removes valuable project knowledge.
Even under pressure to "clean up code" or "remove clutter" - preserve all existing comments.
All JavaScript projects assume these libraries are available:
File Operations:
read(path) - Read file content as stringwrite(path, content) - Write content to filereadJson(path) - Read and parse JSON filewriteJson(path, data) - Write object as JSONreadUrl(url) - Fetch and read URL contentreadJsonUrl(url) - Fetch and parse JSON from URLFile System:
exists(path) - Check if file/directory existsisFile(path) - Check if path is a fileisDirectory(path) - Check if path is a directoryisSymlink(path) - Check if path is a symlinkcopy(source, dest) - Copy file or directorymove(source, dest) - Move file or directoryremove(path) - Delete file or directoryemptyDir(path) - Remove all contents of directorymkdirp(path) - Create directory and parents if neededsymlink(target, path) - Create symbolic linkPath Operations:
absolute(path) - Convert to absolute pathdirname(path) - Get directory nameglob(pattern) - Find files matching patterngitRoot() - Find git repository rootpackageRoot() - Find package.json roothere() - Get current file's directorycallerDir() - Get caller's directorycommonParentDirectory(paths) - Find common parenttmpDirectory(name) - Create temporary directoryExecution & Process:
run(command) - Execute shell commandwhich(command) - Find command in PATHexit(code) - Exit process with codeenv(name) - Get environment variablecaptureOutput(fn) - Capture console outputUtilities:
download(url, dest) - Download file from URLhash(content) - Generate hash of contentuuid() - Generate unique identifiersleep(ms) - Async sleepspinner(max) - Create progress spinnerpulse(message) - Show progress pulseprompt(message) - Ask user for inputselect(message, choices) - Interactive selectioncache(key, fn) - Cache function resultsURL & Import:
isUrl(string) - Check if string is URLnormalizeUrl(url) - Normalize URL formaturlToFilepath(url) - Convert URL to filepathfirostImport(path) - Dynamic import helperuserlandCaller() - Get userland caller infoConsole:
consoleInfo(message) - Log info messageconsoleSuccess(message) - Log success messageconsoleWarn(message) - Log warning messageconsoleError(message) - Log error messageData Transformation:
_ - Lodash for data manipulation (map, filter, get, etc.)
_.chain() when performing 2+ operations in sequence_.map(arr, fn)Date Handling:
dayjs - Date parsing, formatting, and manipulationAsync Operations:
pMap(items, mapper, options) - Map array with concurrency controlpProps(object) - Resolve object of promisesAlways use ES6 modules (import/export), not CommonJS (require/module.exports):
// ✅ GOOD - ES6 modules
import { _ } from 'golgoth';
export function myFunction() {}
// ❌ WRONG - CommonJS
const _ = require('lodash');
module.exports = myFunction;
Always use named exports:
// ✅ GOOD - Named export
export function myFunction() {
// ...
}
export { helperA, helperB };
// ❌ AVOID - Default export
export default function myFunction() {
// ...
}
Always include .js extension in local imports:
// ✅ GOOD
import { read } from './read.js';
import { helper } from '../utils/helper.js';
// ❌ WRONG
import { read } from './read';
Import order:
import { _ } from 'golgoth';
import { read, write } from 'firost';
import { helper } from './helper.js';
Export private methods in a __ object to enable mocking in tests:
// Declare at top of file
export let __;
// Use private methods
export function publicApi() {
__.privateHelper();
}
// Define private methods at bottom
__ = {
privateHelper() {
// Implementation
},
anotherPrivate() {
// Implementation
},
};
Always prefer async/await over .then():
// ✅ GOOD
export async function fetchData(url) {
const content = await read(url);
const parsed = await parse(content);
return parsed;
}
// ❌ AVOID
export function fetchData(url) {
return read(url)
.then(content => parse(content))
.then(parsed => parsed);
}
Use .then() only when the library doesn't support async/await.
Only use try/catch if you need to do something special before propagating the error:
// ✅ GOOD - Let error propagate
export async function processFile(path) {
const content = await read(path);
return transform(content);
}
// ✅ GOOD - Need to cleanup before error propagates
export async function processWithCleanup(path) {
const tmpFile = await createTemp();
try {
const result = await process(tmpFile);
return result;
} catch (error) {
await remove(tmpFile);
throw error;
}
}
// ❌ UNNECESSARY - Just re-throwing
export async function processFile(path) {
try {
const content = await read(path);
return transform(content);
} catch (error) {
throw error;
}
}
If the goal is for the process to stop on error, don't catch it.
Use camelCase for all identifiers:
// ✅ GOOD
const myVariable = 'value';
function myFunction() {}
const myObject = { propertyName: 'value' };
// ❌ WRONG
const my_variable = 'value'; // snake_case
const MyVariable = 'value'; // PascalCase (unless it's a class)
No classes are used in this codebase, so no PascalCase.
Use _.chain() when performing 2 or more operations:
// ✅ GOOD - Multiple operations
const result = _.chain(items)
.map(item => item.value)
.filter(value => value > 0)
.value();
// ✅ GOOD - Single operation
const values = _.map(items, item => item.value);
// ❌ UNNECESSARY - Chain for single operation
const values = _.chain(items)
.map(item => item.value)
.value();
Prefer clarity over brevity with lodash methods:
// ❌ UNCLEAR - What does flatMap do?
const commands = _.flatMap(statements, (statement) => {
return _.map(statement.items, item => item.value);
});
// ✅ CLEARER - Explicit chain shows the transformation
const commands = _.chain(statements)
.map(statement => statement.items)
.flatten()
.map(item => item.value)
.value();
Extract repeated property accesses into variables:
// ❌ BAD - Repeated access to command.type
if (command.type === 'AndOr' || command.type === 'Pipeline') {
// ...
}
if (command.type === 'Command') {
// ...
}
// ✅ GOOD - Extract once
const type = command.type;
if (type === 'AndOr' || type === 'Pipeline') {
// ...
}
if (type === 'Command') {
// ...
}
Create helper functions for repeated patterns:
// ❌ BAD - Repeated substring pattern
const results = _.map(commands, (cmd) => {
if (cmd.type === 'Simple') {
return commandLine.substring(cmd.pos, cmd.end);
}
return commandLine.substring(cmd.pos, cmd.end);
});
// ✅ GOOD - Extract helper function
function extractText(node) {
return commandLine.substring(node.pos, node.end);
}
const results = _.map(commands, (cmd) => {
if (cmd.type === 'Simple') {
return extractText(cmd);
}
return extractText(cmd);
});
Helper functions can be:
__ object (when used in multiple places or need testing)DO NOT import test globals - they are available automatically:
// ✅ GOOD - No imports needed
describe('myFunction', () => {
it('should work', () => {
expect(true).toEqual(true);
});
});
// ❌ WRONG - Don't import
import { describe, it, expect } from 'vitest';
describe('myFunction', () => {
// ...
});
Available globals from Vitest:
describe, it, testexpectbeforeEach, afterEach, beforeAll, afterAllvi (mocking utilities - use vi.spyOn(), vi.fn(), NOT jest.fn())Additional globals provided by Aberlaas setup:
describeName - Name of current describe blocktestName - Name of current testdedent - Template tag for dedenting stringsMocks are automatically cleaned between tests (no need for manual cleanup).
it.each()When testing the same logic with different inputs/outputs, use it.each():
describe('absolute', () => {
it.each([
{ input: ['/tmp/one'], expected: '/tmp/one' },
{ input: ['/tmp/one/../two'], expected: '/tmp/two' },
{ input: ['~/config'], expected: `${homePath}/config` },
])('$input', async ({ input, expected }) => {
const actual = absolute(...input);
expect(actual).toEqual(expected);
});
});
Pattern:
input and expected keys$input in test name uses the input value as titleinput (what's passed), actual (result), expected (assertion)describeName for Test Isolationdescribe('myFunction', () => {
const testDirectory = tmpDirectory(`firost/${describeName}`);
afterEach(async () => {
await remove(testDirectory);
});
it('should create files', async () => {
// testDirectory is unique per describe block
});
});
__When you need to mock private methods exported in __, use vi.spyOn():
Source file (lib/spinner.js):
export let __;
export function spinner() {
return {
success(text) {
__.greenify(text);
},
};
}
__ = {
greenify(text) {
return colors.green(text);
},
};
Test file (lib/tests/spinner.js):
import { __, spinner } from '../spinner.js';
describe('spinner', () => {
it('should call greenify when success', () => {
vi.spyOn(__, 'greenify').mockReturnValue();
const actual = spinner();
actual.success('yay');
expect(__.greenify).toHaveBeenCalledWith('yay');
});
});
Key points:
__ from the module alongside the public functionvi.spyOn(__, 'methodName') to mock private methodsexpect(__.methodName).toHaveBeenCalledWith(...)CRITICAL: For ALL function spies (sync or async), ALWAYS use mockReturnValue() with the direct value, NEVER use mockResolvedValue() or mockRejectedValue().
This applies to ALL functions, including private methods in __.
✅ CORRECT - Use mockReturnValue with direct values:
describe('fetchUserData', () => {
it('should fetch and parse data', async () => {
// Async function returning an object - return the value directly
vi.spyOn(__, 'makeRequest')
.mockReturnValue({ status: 200, data: 'result' });
// Async function returning void - return undefined
vi.spyOn(__, 'saveToCache')
.mockReturnValue();
// Async function returning a number
vi.spyOn(__, 'calculateTotal')
.mockReturnValue(42);
const result = await fetchUserData('123');
expect(result).toEqual('result');
});
});
❌ WRONG - Don't use mockResolvedValue, mockRejectedValue, or Promise.resolve():
// ❌ NEVER DO THIS
vi.spyOn(__, 'makeRequest').mockResolvedValue({ data: 'result' });
vi.spyOn(__, 'makeRequest').mockReturnValue(Promise.resolve({ data: 'result' }));
vi.spyOn(__, 'validateInput').mockRejectedValue(new Error('Invalid'));
vi.spyOn(__, 'validateInput').mockReturnValue(Promise.reject(new Error('Invalid')));
Why always use mockReturnValue with direct values:
Rationalizations to ignore:
Always use mockReturnValue(value) directly for ALL functions, regardless of sync/async.
This pattern allows to capture the error as a variable and make assertions on it. It's more flexible than expect().toThrow().
describe('emptyDir', () => {
it('should throw an error if not a string', async () => {
let actual = null;
try {
await emptyDir(function () {});
} catch (error) {
actual = error;
}
expect(actual).toHaveProperty('code', 'FIROST_EMPTY_DIR_TARGET_MUST_BE_STRING');
expect(actual.message).toContain('must be a string');
});
});
Pattern:
let actual = null before the try/catcherror (not err)actual in the catch blockexpect() assertions on actual like any other variableAlways use this pattern, even when:
expect().rejects.toThrow()" → This codebase uses try/catch pattern for consistencylib/
├── main.js # Exports all public functions
├── myFunction.js # One function per file
├── anotherFunction.js
└── __tests__/ # Tests next to the code
├── myFunction.js
└── anotherFunction.js
Conventions:
__tests__/ directory next to the code they testlib/main.js exports all public functions from individual filesExample lib/main.js:
export { read } from './read.js';
export { write } from './write.js';
export { exists } from './exists.js';
// ... export all public functions
Always use yarn run commands, not npm:
yarn run test
# Check for lint errors
yarn run lint
# Auto-fix lint errors
yarn run lint:fix
When to lint:
Never use npm test or npm run - always use yarn run.
JSDoc is required for ALL functions - exported and private:
/**
* Read any file on disk
* @param {string} filepath - Path to the file to read
* @returns {string} Content of the file
*/
export async function read(filepath) {
// ...
}
// Private methods in __ also need JSDoc
__ = {
/**
* Apply green color to text
* @param {string} text - Text to colorize
* @returns {string} Green colored text
*/
greenify(text) {
return colors.green(text);
},
};
JSDoc format:
@param {type} name - Description for each parameter@returns {type} Description for return value@async is implied by async function, no need to document it// File I/O
import { read, write, readJson, writeJson } from 'firost';
import { exists, remove, copy, mkdirp } from 'firost';
// Data transformation
import { _ } from 'golgoth';
const result = _.chain(data).map().filter().value();
// Async utilities
import { pMap, pProps } from 'golgoth';
// Dates
import { dayjs } from 'golgoth';
// Tests - NO IMPORTS NEEDED
describe('feature', () => {
it('works', () => {
expect(actual).toEqual(expected);
});
});
// Exports
export function myFunction() {} // Named export
export let __; // Private methods
__ = { privateHelper() {} };
// Imports
import { helper } from './helper.js'; // Include .js
| Mistake | Fix |
|---------|-----|
| Removing existing comments | NEVER remove existing comments when editing code - preserve all of them |
| Importing test globals | Don't import describe, it, expect - they're global |
| Using jest.fn() | Use vi.fn() and vi.spyOn() from Vitest, not Jest |
| Using mockResolvedValue() or mockRejectedValue() | ALWAYS use mockReturnValue(value) with the direct value for ALL spies (sync and async) |
| Using .then() chains | Use async/await instead |
| Default exports | Use named exports |
| Using require() | Use ES6 import not CommonJS require() |
| Using module.exports | Use ES6 export not CommonJS module.exports |
| Missing .js in imports | Always include: './file.js' |
| Missing JSDoc | Add JSDoc to ALL functions (exported and private in __) |
| Chain for single operation | Use _.chain() only for 2+ operations |
| Using _.flatMap() | Prefer explicit _.chain().map().flatten() for clarity |
| Repeated property access | Extract const type = obj.type instead of checking obj.type multiple times |
| Repeated code patterns | Create helper functions for duplicated substring(), map(), etc. |
| Unnecessary try/catch | Only catch if you need to act before propagating |
| snake_case naming | Use camelCase for everything |
| Using npm test or npm run | Always use yarn run test and yarn run lint:fix |
| Linting every file | Lint once after completing a set of changes |
| Manual mock cleanup | Mocks auto-clean between tests |
tools
Turn the current conversation context into a PRD and publish it to the project issue tracker. Use when user wants to create a PRD from the current context.
tools
Break a plan, spec, or PRD into independently-grabbable issues using tracer-bullet vertical slices. Use when user wants to convert a plan into issues, create implementation tickets, or break down work into issues.
documentation
Use when user says "sidequest" or "handoff" — compact conversation context into a document for a fresh agent to pick up.
development
Use when the user wants to nail down domain terms, resolve terminology ambiguities, or build a shared language for a module or repo. Drills vocabulary one question at a time and writes to the project GLOSSARY.md.