Update readme and clean up
This commit is contained in:
parent
6a3157762a
commit
435d555394
17 changed files with 320 additions and 486 deletions
225
README.md
225
README.md
|
@ -1,6 +1,20 @@
|
|||
# Playbill Builder CLI
|
||||
# Muse
|
||||
|
||||
This is a CLI tool for compiling Markdown and YAML files into a validated Playbill for [Proscenium](https://proscenium.game)
|
||||
A CLI for wrangling directories of source files.
|
||||
|
||||
Like a static generator, but for whatever.
|
||||
|
||||
## Overview
|
||||
|
||||
**Muse** is a CLI and toolchain for operating on directories of
|
||||
json, yaml, toml, and markdown files. Each file can specify any shape
|
||||
of data. Muse scans included files, parses them into data, and streams
|
||||
the loaded data through processors and plugins.
|
||||
|
||||
Each processor can modify the data at compile time, as well as enact
|
||||
side effects such as writing files to disk.
|
||||
|
||||
Muse does not edit source files, it only reads them in.
|
||||
|
||||
## Usage
|
||||
|
||||
|
@ -9,209 +23,24 @@ Usage:
|
|||
muse [/path/to/binding.yaml] <options>
|
||||
|
||||
Options:
|
||||
--check Only load and check the current binding and resources, but do not compile
|
||||
--outfile, -o Specify the output file path. If not specified, output to stdout
|
||||
--watch, -w Watch the directory for changes and recompile
|
||||
--renderer, -r Specify the output renderer. Options: json, html
|
||||
--stdout -s Output final data to stdout
|
||||
--verbose, -v Enable verbose logging
|
||||
--help, -h Show this help message
|
||||
```
|
||||
|
||||
## Binding File
|
||||
|
||||
Each Muse project should specify a `binding.yaml` file with the following shape:
|
||||
Each Muse project should specify a `binding` file with the following shape:
|
||||
```yaml
|
||||
name: My Playbill # Required
|
||||
author: My Name # Required
|
||||
version: 0.0.1 # Required
|
||||
extend: ../another-playbill # Optional
|
||||
include:
|
||||
- ../another-project # Optional
|
||||
files: # Optional
|
||||
- "**/*.yaml"
|
||||
styles: # Optional
|
||||
- stylesheet.css
|
||||
terms: # Optional
|
||||
Some Term: Some Definition
|
||||
Another Term: Another Definition
|
||||
contentKey: "content" # Optional
|
||||
options: # Optional
|
||||
someOption: someValue
|
||||
processors:
|
||||
- first-processor
|
||||
- second-processor
|
||||
```
|
||||
|
||||
The **Binding** file is used to specify the metadata for the Playbill, as well as the files to be compiled. It is also the entry point for a project.
|
||||
|
||||
When using the `muse` CLI, you must provide either a path to a `binding.yaml` file or a directory which contains a `binding.yaml` file.
|
||||
|
||||
## Common Types
|
||||
|
||||
Many Playbill components share common properties that expect a value
|
||||
from a predefined list. These are the common types used in the Playbill.
|
||||
|
||||
### `Roll` Options
|
||||
|
||||
- `none`
|
||||
- `attack`
|
||||
- `fate`
|
||||
- `muscle`
|
||||
- `focus`
|
||||
- `knowledge`
|
||||
- `charm`
|
||||
- `cunning`
|
||||
- `spark`
|
||||
|
||||
### `Damage Type` Options
|
||||
|
||||
- `phy` (denotes "physical damage")
|
||||
- `arc` (denotes "arcane damage")
|
||||
|
||||
### `Prowess` Options
|
||||
|
||||
- `novice`
|
||||
- `adept`
|
||||
- `master`
|
||||
|
||||
### `Challenge` Options
|
||||
|
||||
- `novice`
|
||||
- `adept`
|
||||
- `master`
|
||||
- `theatrical`
|
||||
|
||||
### `Ability Type` Options
|
||||
|
||||
- `action`
|
||||
- `cue`
|
||||
- `trait`
|
||||
|
||||
## Definition Shapes
|
||||
|
||||
These YAML representations of definitions show the properties available for each Playbill component.
|
||||
|
||||
Note that every Playbill component definition can define the following properties:
|
||||
|
||||
```yaml
|
||||
id: an-id # defaults to a slugified version of the file name
|
||||
name: Component Name # defaults to the file name
|
||||
description: Markdown description # defaults to a placeholder
|
||||
categories: # defaults to no categories
|
||||
- list
|
||||
- of
|
||||
- categories
|
||||
```
|
||||
|
||||
### Ability Definition
|
||||
|
||||
```yaml
|
||||
$define: ability
|
||||
type: <ability type> # Defaults to action
|
||||
ap: 0 # AP cost to use this ability (default 0)
|
||||
hp: 0 # HP cost to use this ability (default 0)
|
||||
ep: 0 # EP cost to use this ability (default 0)
|
||||
xp: 0 # XP cost to learn this ability (default 1)
|
||||
damage: 0 # Damage dealt by this ability (default 0)
|
||||
damageType: <damage type> # Type of damage dealt by this ability (default phy)
|
||||
roll: <roll type> # Roll type for this ability (default none)
|
||||
```
|
||||
|
||||
### Blueprint Definition
|
||||
|
||||
```yaml
|
||||
$define: blueprint
|
||||
species: species-id # ID of the species this blueprint is for
|
||||
items: # List of item IDs (default empty)
|
||||
- item-id
|
||||
abilities: # List of ability IDs (default empty)
|
||||
- ability-id
|
||||
ap: 0 # Starting AP (default 4)
|
||||
hp: 0 # Starting HP (default 5)
|
||||
ep: 0 # Starting EP (default 1)
|
||||
xp: 0 # Starting XP (default 0)
|
||||
muscle: <prowess> # Starting muscle prowess (default novice)
|
||||
focus: <prowess> # Starting focus prowess (default novice)
|
||||
knowledge: <prowess> # Starting knowledge prowess (default novice)
|
||||
charm: <prowess> # Starting charm prowess (default novice)
|
||||
cunning: <prowess> # Starting cunning prowess (default novice)
|
||||
spark: <prowess> # Starting spark prowess (default novice)
|
||||
```
|
||||
|
||||
### Item Definition
|
||||
|
||||
```yaml
|
||||
$define: item
|
||||
type: <item type> # Defaults to wielded
|
||||
rarity: <rarity> # Defaults to common
|
||||
affinity: <roll type> # Defaults to none
|
||||
damage: 0 # Defaults to 0
|
||||
damageType: <damage type> # Defaults to phy
|
||||
hands: 1 | 2 # Only for wielded items
|
||||
slot: <item slot> # Only for worn items
|
||||
```
|
||||
|
||||
#### `Item Type` Options
|
||||
|
||||
- `wielded`
|
||||
- `worn`
|
||||
- `trinket`
|
||||
|
||||
#### `Rarity` Options
|
||||
|
||||
- `common`
|
||||
- `uncommon`
|
||||
- `rare`
|
||||
- `legendary`
|
||||
|
||||
#### `Item Slot` Options
|
||||
|
||||
- `headgear`
|
||||
- `outfit`
|
||||
- `gloves`
|
||||
- `boots`
|
||||
- `adornment`
|
||||
|
||||
### Method Definition
|
||||
|
||||
```yaml
|
||||
$define: method
|
||||
curator: Someone # The name of the curator
|
||||
abilities: # A 2-d array of ability IDs
|
||||
- [rank-1-ability-id-one, rank-1-ability-id-two]
|
||||
- [rank-2-ability-id-one]
|
||||
```
|
||||
|
||||
### Resource Definition
|
||||
|
||||
```yaml
|
||||
$define: resource
|
||||
type: <resource type> # Defaults to text
|
||||
url: https://url.com/image.jpeg # Only for image resources
|
||||
data: <a 2-d array of strings> # Only for table resources
|
||||
```
|
||||
|
||||
#### `Resource Type` Options
|
||||
|
||||
- `text`
|
||||
- `image`
|
||||
- `table`
|
||||
|
||||
### Rule Definition
|
||||
|
||||
```yaml
|
||||
$define: rule
|
||||
overrule: another-rule-id # Optional
|
||||
order: 0 # Optional
|
||||
```
|
||||
|
||||
### Species Definition
|
||||
|
||||
```yaml
|
||||
$define: species
|
||||
hands: 2 # Defaults to 2
|
||||
abilities: # A list of innate ability IDs
|
||||
- some-ability-id
|
||||
- another-ability-id
|
||||
ap: 0 # Starting AP modifier
|
||||
hp: 0 # Starting HP modifier
|
||||
ep: 0 # Starting EP modifier
|
||||
xp: 0 # Starting XP modifier
|
||||
muscle: <prowess> # Defaults to novice
|
||||
focus: <prowess> # Defaults to novice
|
||||
knowledge: <prowess> # Defaults to novice
|
||||
charm: <prowess> # Defaults to novice
|
||||
cunning: <prowess> # Defaults to novice
|
||||
spark: <prowess> # Defaults to novice
|
||||
```
|
||||
|
|
11
demo/main/binding.md
Normal file
11
demo/main/binding.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
markdownKey: description
|
||||
options:
|
||||
dunks:
|
||||
key: dunkaroos
|
||||
processors:
|
||||
- ./processors/scream.js
|
||||
- ./processors/announce-slam-dunks.js
|
||||
---
|
||||
|
||||
A markdown-powered binding file!
|
|
@ -1,3 +0,0 @@
|
|||
markdownKey: description
|
||||
processors:
|
||||
- ./processors/scream.js
|
13
demo/main/processors/announce-slam-dunks.js
Normal file
13
demo/main/processors/announce-slam-dunks.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
export const name = "Announce Slam Dunks";
|
||||
|
||||
export const process = (binding, { dunks }) => {
|
||||
const slamDunkKey = dunks?.key ?? "slamDunks";
|
||||
|
||||
for (const entry of binding.entries) {
|
||||
if (slamDunkKey in entry.data) {
|
||||
console.log(`Slam dunk!`);
|
||||
}
|
||||
}
|
||||
|
||||
return binding;
|
||||
};
|
|
@ -1,5 +1,5 @@
|
|||
export const name = "Scream";
|
||||
export function process(binding, opts) {
|
||||
export function process(binding) {
|
||||
const modifiedEntries = binding.entries.map(entry => {
|
||||
if (binding.markdownKey in entry.data) {
|
||||
entry.data = {
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
slamDunks = 100
|
||||
player = "Flynn"
|
||||
dunkaroos = 100
|
||||
|
|
21
package.json
21
package.json
|
@ -1,8 +1,17 @@
|
|||
{
|
||||
"name": "@proscenium/muse",
|
||||
"version": "0.0.1",
|
||||
"module": "index.ts",
|
||||
"version": "0.1.0",
|
||||
"module": "dist/index.js",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/muse.js",
|
||||
"types": "./dist/muse.d.ts"
|
||||
}
|
||||
},
|
||||
"type": "module",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/bun": "latest",
|
||||
|
@ -20,9 +29,11 @@
|
|||
},
|
||||
"scripts": {
|
||||
"fmt": "bunx --bun biome check --fix",
|
||||
"build": "bun build ./src/index.ts --outfile muse --compile",
|
||||
"demo": "bun run ./src/index.ts -- ./demo-data/great-spires/binding.yaml",
|
||||
"clean": "rm -rf dist",
|
||||
"types": "dts-bundle-generator -o dist/muse.d.ts --project ./tsconfig.json ./src/exports.ts",
|
||||
"compile": "bun build ./src/cli.ts --outfile ./dist/muse --compile",
|
||||
"build": "bun run clean && bun run compile && bun run types",
|
||||
"build:install": "bun run build && mv muse ~/.local/bin/muse",
|
||||
"types": "dts-bundle-generator -o types/muse.d.ts --project ./tsconfig.json ./src/types.ts"
|
||||
"demo": "bun run ./src/cli.ts -- ./demo/main"
|
||||
}
|
||||
}
|
||||
|
|
72
src/args.ts
72
src/args.ts
|
@ -1,72 +0,0 @@
|
|||
import { parseArgs } from "node:util";
|
||||
import chalk from "chalk";
|
||||
import { version } from "../package.json" with { type: "json" };
|
||||
|
||||
export const USAGE = `
|
||||
${chalk.bold("muse")} - Compile and process troves of data
|
||||
${chalk.dim(`v${version}`)}
|
||||
|
||||
Usage:
|
||||
muse [/path/to/binding.yaml] <options>
|
||||
|
||||
Options:
|
||||
--stdout -s Output final data to stdout
|
||||
--verbose, -v Enable verbose logging
|
||||
--help, -h Show this help message
|
||||
`.trim();
|
||||
|
||||
/**
|
||||
* A shape representing the arguments passed to the CLI
|
||||
*/
|
||||
export interface CLIArguments {
|
||||
inputFilePath: string;
|
||||
|
||||
options: {
|
||||
help: boolean;
|
||||
verbose: boolean;
|
||||
stdout: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of CLI arguments, parse them into a structured object
|
||||
*
|
||||
* @param argv The arguments to parse
|
||||
* @returns The parsed CLI arguments
|
||||
*
|
||||
* @throws {CLIError} if the arguments are invalid
|
||||
*/
|
||||
export function parseCLIArguments(argv: string[]): CLIArguments {
|
||||
const { values: options, positionals: args } = parseArgs({
|
||||
args: argv,
|
||||
options: {
|
||||
help: {
|
||||
short: "h",
|
||||
default: false,
|
||||
type: "boolean",
|
||||
},
|
||||
verbose: {
|
||||
short: "v",
|
||||
default: false,
|
||||
type: "boolean",
|
||||
},
|
||||
stdout: {
|
||||
short: "s",
|
||||
default: false,
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
return {
|
||||
inputFilePath: args[0] ?? "./binding.yaml",
|
||||
|
||||
options: {
|
||||
help: options.help,
|
||||
verbose: options.verbose,
|
||||
stdout: options.stdout,
|
||||
},
|
||||
};
|
||||
}
|
194
src/binding.ts
194
src/binding.ts
|
@ -1,26 +1,42 @@
|
|||
import path, { dirname } from "node:path";
|
||||
import { Glob } from "bun";
|
||||
import z from "zod";
|
||||
import { FileNotFoundError, MuseError } from "./errors";
|
||||
import { parseMuseFile, type MuseEntry } from "./muse-file";
|
||||
import { parseMuseFile } from "./parse";
|
||||
import {
|
||||
type Binding,
|
||||
BindingSchema,
|
||||
type MuseEntry,
|
||||
type MuseProcessor,
|
||||
} from "./types";
|
||||
import { resolveFilePath } from "./util";
|
||||
|
||||
// binding.yaml file schema
|
||||
const BindingSchema = z.object({
|
||||
include: z.array(z.string()).default([]),
|
||||
markdownKey: z.string().default("content"),
|
||||
files: z
|
||||
.array(z.string())
|
||||
.default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]),
|
||||
options: z.record(z.string(), z.any()).default({}),
|
||||
processors: z.array(z.string()).default([]),
|
||||
});
|
||||
async function loadProcessor(
|
||||
bindingDirname: string,
|
||||
processorPath: string,
|
||||
): Promise<MuseProcessor> {
|
||||
const resolvedProcessorPath = path.resolve(bindingDirname, processorPath);
|
||||
const { process, name, description } = await import(resolvedProcessorPath);
|
||||
|
||||
type ParsedBinding = z.infer<typeof BindingSchema>;
|
||||
if (!process || typeof process !== "function") {
|
||||
throw new MuseError(
|
||||
`Processor at ${processorPath} does not export a process() function`,
|
||||
);
|
||||
}
|
||||
|
||||
const processorName = String(name ?? "Unnamed Processor");
|
||||
const processorDescription = String(description ?? "No description provided");
|
||||
|
||||
return {
|
||||
filePath: resolvedProcessorPath,
|
||||
process: process as MuseProcessor["process"],
|
||||
name: processorName,
|
||||
description: processorDescription,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Given a path, find the binding.yaml file
|
||||
* If the path is to a directory, check for a binding.yaml inside
|
||||
* If the path is to a file, check if it exists and is a binding.yaml
|
||||
* Given a path, find the binding file
|
||||
* If the path is to a directory, check for a binding inside
|
||||
* Check order: binding.json, binding.yaml, binding.toml, binding.md
|
||||
* Otherwise throw an error
|
||||
*
|
||||
* @param bindingPath - The path to the binding file
|
||||
|
@ -30,45 +46,50 @@ export async function resolveBindingPath(bindingPath: string): Promise<string> {
|
|||
// If the path does not specify a filename, use binding.yaml
|
||||
const inputLocation = Bun.file(bindingPath);
|
||||
|
||||
// If it is a directory, try again seeking a binding.yaml inside
|
||||
// If it is a directory, try for the four main supported types
|
||||
const stat = await inputLocation.stat();
|
||||
if (stat.isDirectory()) {
|
||||
return resolveBindingPath(path.resolve(bindingPath, "binding.yaml"));
|
||||
}
|
||||
const jsonPath = await resolveFilePath(
|
||||
path.resolve(bindingPath, "binding.json"),
|
||||
);
|
||||
if (jsonPath) {
|
||||
return jsonPath;
|
||||
}
|
||||
|
||||
const yamlPath = await resolveFilePath(
|
||||
path.resolve(bindingPath, "binding.yaml"),
|
||||
);
|
||||
if (yamlPath) {
|
||||
return yamlPath;
|
||||
}
|
||||
|
||||
const tomlPath = await resolveFilePath(
|
||||
path.resolve(bindingPath, "binding.toml"),
|
||||
);
|
||||
if (tomlPath) {
|
||||
return tomlPath;
|
||||
}
|
||||
|
||||
const mdPath = await resolveFilePath(
|
||||
path.resolve(bindingPath, "binding.md"),
|
||||
);
|
||||
if (mdPath) {
|
||||
return mdPath;
|
||||
}
|
||||
|
||||
// If it doesnt exist, bail
|
||||
if (!(await inputLocation.exists())) {
|
||||
throw new FileNotFoundError(bindingPath);
|
||||
}
|
||||
|
||||
// Resolve the path
|
||||
const exists = await inputLocation.exists();
|
||||
|
||||
if (!exists) {
|
||||
throw new FileNotFoundError(bindingPath);
|
||||
}
|
||||
|
||||
// If it is a file, return the path
|
||||
return path.resolve(bindingPath);
|
||||
}
|
||||
|
||||
export type MuseProcessorFn = (
|
||||
binding: Binding,
|
||||
opts: Record<string, unknown>,
|
||||
) => Promise<Binding>;
|
||||
|
||||
export interface MuseProcessor {
|
||||
readonly filePath: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly process: MuseProcessorFn;
|
||||
}
|
||||
|
||||
export interface Binding {
|
||||
readonly _raw: ParsedBinding;
|
||||
readonly bindingPath: string;
|
||||
readonly markdownKey: string;
|
||||
readonly includedFiles: string[];
|
||||
readonly entries: MuseEntry[];
|
||||
readonly meta: Record<string, unknown>;
|
||||
readonly options: Record<string, unknown>;
|
||||
readonly processors: MuseProcessor[];
|
||||
readonly imports: Binding[];
|
||||
}
|
||||
|
||||
export async function loadBinding(initialPath: string): Promise<Binding> {
|
||||
// Resolve the file location if possible
|
||||
const bindingPath = await resolveBindingPath(initialPath);
|
||||
|
@ -95,68 +116,41 @@ export async function loadBinding(initialPath: string): Promise<Binding> {
|
|||
entries.push(...loadedBinding.entries);
|
||||
}
|
||||
|
||||
// Collect file manifest from globs
|
||||
const allFilePaths: string[] = [];
|
||||
for (const thisGlob of parsedBinding.files) {
|
||||
const glob = new Glob(thisGlob);
|
||||
// Aggregate file paths to import
|
||||
const includedFilePaths = parsedBinding.files
|
||||
.flatMap((thisGlob) => {
|
||||
return Array.from(
|
||||
new Glob(thisGlob).scanSync({
|
||||
cwd: bindingDirname,
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
onlyFiles: true,
|
||||
}),
|
||||
);
|
||||
})
|
||||
.filter((filePath) => filePath !== bindingPath);
|
||||
|
||||
const results = glob.scanSync({
|
||||
cwd: bindingDirname,
|
||||
absolute: true,
|
||||
followSymlinks: true,
|
||||
onlyFiles: true,
|
||||
});
|
||||
// Load files
|
||||
const loadedEntries = await Promise.all(
|
||||
includedFilePaths.map((filePath) => {
|
||||
return parseMuseFile(filePath, {
|
||||
contentKey: parsedBinding.contentKey,
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
allFilePaths.push(...results);
|
||||
}
|
||||
|
||||
// Exclude the binding.yaml file
|
||||
const includedFilePaths = allFilePaths.filter((filePath) => {
|
||||
return filePath !== bindingPath;
|
||||
});
|
||||
|
||||
const fileLoadPromises: Promise<MuseEntry[]>[] = [];
|
||||
for (const filePath of includedFilePaths) {
|
||||
fileLoadPromises.push(
|
||||
parseMuseFile(filePath, {
|
||||
markdownKey: parsedBinding.markdownKey,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const mainEntries = await Promise.all(fileLoadPromises);
|
||||
entries.push(...mainEntries.flat());
|
||||
// Add loaded entries to main list
|
||||
entries.push(...loadedEntries.flat());
|
||||
|
||||
// Load and check processors
|
||||
const processors: MuseProcessor[] = [];
|
||||
for (const processorPath of parsedBinding.processors) {
|
||||
const resolvedProcessorPath = path.resolve(bindingDirname, processorPath);
|
||||
const { process, name, description } = await import(resolvedProcessorPath);
|
||||
|
||||
if (!process || typeof process !== "function") {
|
||||
throw new MuseError(
|
||||
`Processor at ${processorPath} does not export a process() function`,
|
||||
);
|
||||
}
|
||||
|
||||
const processorName = String(name ?? "Unnamed Processor");
|
||||
const processorDescription = String(
|
||||
description ?? "No description provided",
|
||||
);
|
||||
|
||||
processors.push({
|
||||
filePath: resolvedProcessorPath,
|
||||
process: process as MuseProcessor["process"],
|
||||
name: processorName,
|
||||
description: processorDescription,
|
||||
});
|
||||
}
|
||||
const processors: MuseProcessor[] = await Promise.all(
|
||||
parsedBinding.processors.map(loadProcessor.bind(null, bindingDirname)),
|
||||
);
|
||||
|
||||
return {
|
||||
_raw: parsedBinding,
|
||||
bindingPath,
|
||||
markdownKey: parsedBinding.markdownKey,
|
||||
includedFiles: includedFilePaths,
|
||||
contentKey: parsedBinding.contentKey,
|
||||
entries,
|
||||
options: parsedBinding.options,
|
||||
processors: processors,
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { parseArgs } from "node:util";
|
||||
import chalk from "chalk";
|
||||
import { MuseError } from "./errors.ts";
|
||||
import { loadBinding, type Binding } from "./binding";
|
||||
import { type CLIArguments, parseCLIArguments, USAGE } from "./args";
|
||||
import { MuseError } from "./errors";
|
||||
import { loadBinding } from "./binding";
|
||||
import { version } from "../package.json";
|
||||
import type { Binding, CLIArguments } from "./types";
|
||||
|
||||
interface Loggers {
|
||||
log: (msg: string) => void;
|
||||
|
@ -13,8 +15,62 @@ enum ExitCode {
|
|||
Error = 1,
|
||||
}
|
||||
|
||||
export const USAGE = `
|
||||
${chalk.bold("muse")} - Compile and process troves of data
|
||||
${chalk.dim(`v${version}`)}
|
||||
|
||||
Usage:
|
||||
muse [/path/to/binding.yaml] <options>
|
||||
|
||||
Options:
|
||||
--stdout -s Output final data to stdout
|
||||
--verbose, -v Enable verbose logging
|
||||
--help, -h Show this help message
|
||||
`.trim();
|
||||
|
||||
/**
|
||||
* Given an array of CLI arguments, parse them into a structured object
|
||||
*
|
||||
* @param argv The arguments to parse
|
||||
* @returns The parsed CLI arguments
|
||||
*
|
||||
* @throws {CLIError} if the arguments are invalid
|
||||
*/
|
||||
export function parseCLIArguments(argv: string[]): CLIArguments {
|
||||
const { values: options, positionals: args } = parseArgs({
|
||||
args: argv,
|
||||
options: {
|
||||
help: {
|
||||
short: "h",
|
||||
default: false,
|
||||
type: "boolean",
|
||||
},
|
||||
verbose: {
|
||||
short: "v",
|
||||
default: false,
|
||||
type: "boolean",
|
||||
},
|
||||
stdout: {
|
||||
short: "s",
|
||||
default: false,
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
return {
|
||||
inputFilePath: args[0] ?? "./binding.yaml",
|
||||
flags: {
|
||||
help: options.help,
|
||||
verbose: options.verbose,
|
||||
stdout: options.stdout,
|
||||
},
|
||||
};
|
||||
}
|
||||
async function processBinding(
|
||||
{ inputFilePath, options }: CLIArguments,
|
||||
{ inputFilePath, flags }: CLIArguments,
|
||||
{ log, verbose }: Loggers,
|
||||
) {
|
||||
// Load the binding
|
||||
|
@ -37,7 +93,7 @@ async function processBinding(
|
|||
const { process, name } = processor;
|
||||
|
||||
log(chalk.bold(`↪ ${name}`) + chalk.dim(` (${processor.description})`));
|
||||
const thisStep = await process(lastStep, binding.options);
|
||||
const thisStep = await process(lastStep, binding.options, flags);
|
||||
processedSteps.push(thisStep);
|
||||
}
|
||||
|
||||
|
@ -48,7 +104,7 @@ async function processBinding(
|
|||
const finalState = processedSteps[processedSteps.length - 1];
|
||||
const serialized = JSON.stringify(finalState.entries, null, 2);
|
||||
|
||||
if (options.stdout) {
|
||||
if (flags.stdout) {
|
||||
console.log(serialized);
|
||||
}
|
||||
return ExitCode.Success;
|
||||
|
@ -56,16 +112,16 @@ async function processBinding(
|
|||
|
||||
async function main(): Promise<number> {
|
||||
const cliArguments = parseCLIArguments(Bun.argv.slice(2));
|
||||
const { options } = cliArguments;
|
||||
const { flags } = cliArguments;
|
||||
|
||||
// If --help is specified, print usage and exit
|
||||
if (options.help) {
|
||||
if (flags.help) {
|
||||
console.log(USAGE);
|
||||
return ExitCode.Success;
|
||||
}
|
||||
|
||||
const logFn = !options.stdout ? console.log : () => {};
|
||||
const verboseFn = options.verbose
|
||||
const logFn = !flags.stdout ? console.log : () => {};
|
||||
const verboseFn = flags.verbose
|
||||
? (msg: string) => {
|
||||
console.log(chalk.dim(msg));
|
||||
}
|
|
@ -19,8 +19,6 @@ export class FileError extends MuseError {
|
|||
}
|
||||
}
|
||||
|
||||
export class MalformedResourceFileError extends FileError {}
|
||||
|
||||
export class FileNotFoundError extends FileError {
|
||||
constructor(filePath: string) {
|
||||
super("File not found", filePath);
|
||||
|
|
2
src/exports.ts
Normal file
2
src/exports.ts
Normal file
|
@ -0,0 +1,2 @@
|
|||
export type * from "./types";
|
||||
export const DEFAULT_CONTENT_KEY = "content";
|
|
@ -2,30 +2,7 @@ import { normalize, extname } from "node:path";
|
|||
import YAML from "yaml";
|
||||
import TOML from "smol-toml";
|
||||
import EMDY from "@endeavorance/emdy";
|
||||
|
||||
type UnknownRecord = Record<string, unknown>;
|
||||
export type MuseFileType = "md" | "yaml" | "json" | "toml";
|
||||
export interface MuseEntry {
|
||||
_raw: string;
|
||||
filePath: string;
|
||||
data: Record<string, unknown>;
|
||||
meta: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function parseFileType(ext: string): MuseFileType {
|
||||
switch (ext) {
|
||||
case "md":
|
||||
return "md";
|
||||
case "yaml":
|
||||
return "yaml";
|
||||
case "json":
|
||||
return "json";
|
||||
case "toml":
|
||||
return "toml";
|
||||
default:
|
||||
throw new Error(`Unsupported file format: ${ext}`);
|
||||
}
|
||||
}
|
||||
import type { MuseEntry, UnknownRecord } from "./types";
|
||||
|
||||
function parseYAMLEntries(text: string): UnknownRecord[] {
|
||||
const parsedDocs = YAML.parseAllDocuments(text);
|
||||
|
@ -75,16 +52,16 @@ function parseTOMLEntries(text: string): UnknownRecord[] {
|
|||
}
|
||||
|
||||
interface ParseMuseFileOptions {
|
||||
markdownKey?: string;
|
||||
contentKey?: string;
|
||||
}
|
||||
|
||||
export async function parseMuseFile(
|
||||
rawFilePath: string,
|
||||
{ markdownKey = "content" }: ParseMuseFileOptions = {},
|
||||
{ contentKey = "content" }: ParseMuseFileOptions = {},
|
||||
): Promise<MuseEntry[]> {
|
||||
const filePath = normalize(rawFilePath);
|
||||
const file = Bun.file(filePath);
|
||||
const fileType = parseFileType(extname(filePath).slice(1));
|
||||
const fileType = extname(filePath).slice(1);
|
||||
const rawFileContent = await file.text();
|
||||
|
||||
const partial = {
|
||||
|
@ -94,7 +71,7 @@ export async function parseMuseFile(
|
|||
};
|
||||
|
||||
if (fileType === "md") {
|
||||
const parsed = EMDY.parse(rawFileContent, markdownKey);
|
||||
const parsed = EMDY.parse(rawFileContent, contentKey);
|
||||
return [
|
||||
{
|
||||
...partial,
|
58
src/types.ts
58
src/types.ts
|
@ -1,3 +1,55 @@
|
|||
import type { Binding, MuseProcessorFn } from "./binding";
|
||||
import type { MuseEntry } from "./muse-file";
|
||||
export type { Binding, MuseProcessorFn, MuseEntry };
|
||||
import { z } from "zod";
|
||||
|
||||
export interface CLIFlags {
|
||||
help: boolean;
|
||||
verbose: boolean;
|
||||
stdout: boolean;
|
||||
}
|
||||
|
||||
export interface CLIArguments {
|
||||
inputFilePath: string;
|
||||
flags: CLIFlags;
|
||||
}
|
||||
|
||||
export const BindingSchema = z.object({
|
||||
include: z.array(z.string()).default([]),
|
||||
contentKey: z.string().default("content"),
|
||||
files: z
|
||||
.array(z.string())
|
||||
.default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]),
|
||||
options: z.record(z.string(), z.any()).default({}),
|
||||
processors: z.array(z.string()).default([]),
|
||||
});
|
||||
|
||||
export interface Binding {
|
||||
readonly _raw: z.infer<typeof BindingSchema>;
|
||||
readonly bindingPath: string;
|
||||
readonly contentKey: string;
|
||||
readonly entries: MuseEntry[];
|
||||
readonly meta: Record<string, unknown>;
|
||||
readonly options: Record<string, unknown>;
|
||||
readonly processors: MuseProcessor[];
|
||||
readonly imports: Binding[];
|
||||
}
|
||||
|
||||
export type MuseProcessorFn = (
|
||||
binding: Binding,
|
||||
opts: Record<string, unknown>,
|
||||
flags: CLIFlags,
|
||||
) => Promise<Binding>;
|
||||
|
||||
export interface MuseProcessor {
|
||||
readonly filePath: string;
|
||||
readonly name: string;
|
||||
readonly description: string;
|
||||
readonly process: MuseProcessorFn;
|
||||
}
|
||||
|
||||
export interface MuseEntry {
|
||||
_raw: string;
|
||||
filePath: string;
|
||||
data: Record<string, unknown>;
|
||||
meta: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type UnknownRecord = Record<string, unknown>;
|
||||
|
|
27
src/util.ts
Normal file
27
src/util.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { normalize } from "node:path";
|
||||
|
||||
/**
|
||||
* Given a file path, ensure it is a file that exists
|
||||
* Otherwise, return null
|
||||
*
|
||||
* @param filePath - The path to the file
|
||||
* @returns A promise which resolves to the resolved file path or null
|
||||
*/
|
||||
export async function resolveFilePath(
|
||||
filePath: string,
|
||||
): Promise<string | null> {
|
||||
const normalized = normalize(filePath);
|
||||
const file = Bun.file(normalized);
|
||||
const exists = await file.exists();
|
||||
|
||||
if (!exists) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const stat = await file.stat();
|
||||
if (stat.isDirectory()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
|
@ -27,7 +27,7 @@
|
|||
"noPropertyAccessFromIndexSignature": false,
|
||||
"baseUrl": "./src",
|
||||
"paths": {},
|
||||
"outDir": "./types",
|
||||
"outDir": "./dist",
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
|
62
types/muse.d.ts
vendored
62
types/muse.d.ts
vendored
|
@ -1,62 +0,0 @@
|
|||
// Generated by dts-bundle-generator v9.5.1
|
||||
|
||||
import z from 'zod';
|
||||
|
||||
export type MuseFileType = "md" | "yaml" | "json" | "toml";
|
||||
export interface MuseFileContent {
|
||||
type: MuseFileType;
|
||||
text: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
declare const BindingSchema: z.ZodObject<{
|
||||
include: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
||||
name: z.ZodDefault<z.ZodString>;
|
||||
author: z.ZodDefault<z.ZodString>;
|
||||
description: z.ZodDefault<z.ZodString>;
|
||||
version: z.ZodDefault<z.ZodString>;
|
||||
files: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
||||
options: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
||||
processors: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
||||
}, "strip", z.ZodTypeAny, {
|
||||
options: Record<string, any>;
|
||||
include: string[];
|
||||
name: string;
|
||||
author: string;
|
||||
description: string;
|
||||
version: string;
|
||||
files: string[];
|
||||
processors: string[];
|
||||
}, {
|
||||
options?: Record<string, any> | undefined;
|
||||
include?: string[] | undefined;
|
||||
name?: string | undefined;
|
||||
author?: string | undefined;
|
||||
description?: string | undefined;
|
||||
version?: string | undefined;
|
||||
files?: string[] | undefined;
|
||||
processors?: string[] | undefined;
|
||||
}>;
|
||||
export type ParsedBinding = z.infer<typeof BindingSchema>;
|
||||
export interface BindingMetadata {
|
||||
name: string;
|
||||
author: string;
|
||||
description: string;
|
||||
version: string;
|
||||
}
|
||||
export type MuseProcessorFn = (binding: Binding) => Promise<Binding>;
|
||||
export interface MuseProcessor {
|
||||
readonly filePath: string;
|
||||
readonly process: MuseProcessorFn;
|
||||
}
|
||||
export interface Binding {
|
||||
readonly _raw: ParsedBinding;
|
||||
readonly bindingPath: string;
|
||||
readonly includedFiles: string[];
|
||||
readonly entries: MuseFileContent[];
|
||||
readonly metadata: BindingMetadata;
|
||||
readonly options: Record<string, unknown>;
|
||||
readonly processors: MuseProcessor[];
|
||||
readonly imports: Binding[];
|
||||
}
|
||||
|
||||
export {};
|
Loading…
Add table
Add a link
Reference in a new issue