Skip to content
Docs

Extending fusion-config

fusion-config is a small Bun CLI built on Commander: a single entry point at src/index.ts plus a templates/ directory. Adding a template needs no code at all; adding a command is one declarative block — help text, flag parsing, and error messages are generated from it.

Clone the repo and install dev dependencies first:

git clone https://github.com/tikab-interactive/fusion-config.git
cd fusion-config
bun install

Run the CLI straight from source while you work — there's no build step:

bun src/index.ts list

Add a template

Templates are just directories under templates/. The CLI discovers them at runtime — listTemplates() reads every sub-directory of templates/ — so no code change is needed.

  1. Create a directory named after your template and add the files you want scaffolded:

    mkdir -p templates/my-template
    echo "export const example = true;" > templates/my-template/index.ts
  2. Confirm the CLI sees it, then try copying it somewhere:

    bun src/index.ts list                        # my-template now appears
    bun src/index.ts add my-template --dest /tmp  # copies to /tmp/my-template

templates is already in package.jsonfiles, so your new template ships automatically the next time the package is published.

Optional: pull in companions with template.json

A template can drop a template.json file at its root to copy companion folders alongside itself, or to print follow-up instructions. The CLI reads it but never copies it into the consumer:

templates/docs/template.json
{
  "description": "Vocs documentation site (Fusion-branded).",
  "with": ["patches"],
  "notes": "Add \"docs\" to workspaces and the vocs patch to patchedDependencies, then run bun install."
}
FieldPurpose
descriptionA human summary of the template.
withOther template folders copied into the same destination root. They become companions — hidden from list and not addable on their own.
notesPrinted after a successful copy. Use it for wiring the CLI can't do (workspace entries, patchedDependencies, …).

So bunx fusion-config add docs copies templates/docs/ and templates/patches/ into the project, then prints the notes. Companions are resolved by name against templates/, and template.json is strict JSON — no comments.

Templates can also carry build output safely: node_modules/, dist/, .DS_Store, and template.json itself are always skipped when copying and excluded from the published package — so you can install deps or run a build inside a template without that leaking downstream.

Add a command

Every command is a program.command() block in src/index.ts, registered alongside the existing list and add blocks. The description, argument, and option declarations are the single source of truth: Commander generates --help for the program and for each command from them, so there is no usage string to keep in sync.

  1. Register the command next to the existing blocks:

    src/index.ts
    program
    	.command("remove")
    	.description("Delete a previously copied template")
    	.argument("<template>", "template to remove")
    	.option("-d, --dest <dir>", "directory the template was copied into", ".")
    	.option("--dry-run", "print what would be removed without removing it", false)
    	.action((template: string, opts: { dest: string; dryRun: boolean }) =>
    		removeTemplate(template, opts.dest, opts.dryRun),
    	);

    Flag names are camelCased on the options object (--dry-runopts.dryRun), and defaults passed as the third option() argument are shown in the generated help.

  2. Implement the logic as a named function (like the existing addTemplate), keeping the .command() block declarative. Two error-handling conventions:

    • Expected failures (bad input, missing template): print a one-line console.error and process.exit(1).
    • Unexpected failures: just throw — the top-level try/catch around program.parseAsync() prints the message and sets the exit code.
  3. Check the generated help — no extra work needed, it should already be there:

    bun src/index.ts --help          # `remove` appears in the command list
    bun src/index.ts remove --help   # arguments, flags, and defaults
  4. Test it from source:

    bun src/index.ts remove my-template --dry-run

Misspelled commands and flags get suggestions for free (fusion-config ad"Did you mean add?"), and unknown or missing arguments are rejected with proper errors — none of that needs to be handled in your command.

Change a shared config

The configs documented in Shared configs live in configs/:

FileConsumed by projects as
configs/tsconfig.base.json"extends": "@tikab-interactive/fusion-config/tsconfig.json"
configs/oxlint.jsonimport config from "@tikab-interactive/fusion-config/oxlint"
configs/oxfmt.jsonimport config from "@tikab-interactive/fusion-config/oxfmt"

Edit the JSON and every consumer picks the change up with their next install — both import routes read the same file. (The two-line .mjs files next to them are re-export shims that make the JSON importable from oxlint.config.ts / oxfmt.config.ts.)

Two constraints to know about:

  • Keep configs/oxlint.json and configs/oxfmt.json comment-free. The .mjs shims load them with Node JSON imports, which reject comments (oxlint itself would tolerate them; Node won't). Put the why of a rule in the commit message or on this site instead. tsconfig.base.json is only read by TypeScript, which accepts JSONC, so comments are fine there.
  • ignorePatterns doesn't travel. Oxlint only honors it in the project's root config, so adding it to configs/oxlint.json silently does nothing for consumers.

This repo consumes its own configs — the root tsconfig.json, .oxlintrc.json, and oxfmt.config.ts all extend configs/ exactly the way a downstream project does — so a config change is verified by the local checks:

bun run typecheck && bun run lint && bun run fmt:check

A brand-new config file additionally needs an entry in package.jsonexports (and ships via the existing configs entry in files).

Publish a release

Templates and commands ship together when the package is published to GitHub Packages. You need a token with the write:packages scope (see Setup for the .npmrc), then:

npm version patch   # bump the version
npm publish         # publishConfig points at GitHub Packages