@hyperfrontend/versioning/commits/author

author/

Interactive commit authoring session.

Overview

Step-sequenced session that accumulates a conventional commit draft field-by-field (type → scope → subject → body → breaking → issues), renders a preview, and (by default) executes git commit. Each step is a small pure-ish function operating over a shared SessionContext; the runner walks the list, honors goto jumps (used by the preview's "commit this message? no" restart), and surfaces Ctrl-C as a cancelled outcome.

API

Export Description
createAuthorSession Build a session from a partial config (steps default to conventionalPreset)
runAuthorSession Execute the session; returns { status: 'committed' | 'cancelled', message? }
loadCommitConfig Discover commit.config.{js,mjs,cjs} upward from cwd
resolveSessionConfig Overlay a partial config on the built-in defaults
conventionalPreset Default ordered step list (D8)
Individual steps resolveScopeStep, typeStep, scopeStep, subjectStep, bodyStep, breakingStep, issuesStep, previewStep, commitStep
SessionConfig Fully resolved config type consumed by the runner
SessionContext Mutable container threaded through every step

Configuration

The commit.config.{js,mjs,cjs} file exports a PartialSessionConfig:

// commit.config.cjs
module.exports = {
  types: [
    { name: 'feat', description: 'A new feature' },
    { name: 'fix', description: 'A bug fix' },
  ],
  scopeMulti: false,
  scopeOptional: false,
  // Filter discovered projects; receives { path, name } per candidate
  scopeFilter: ({ path }) => !path.includes('/__fixtures__/'),
  headerMaxLength: 72,
  // Ruleset reused by the `cl` validator bin and the preview step
  validateRuleset: {
    'type-enum': ['error', { types: ['feat', 'fix', 'docs', 'chore'] }],
    'subject-empty': ['error'],
    'header-max-length': ['warn', { maxLength: 72 }],
    'imperative-mood': ['warn'],
  },
  skipCommit: false,
}

Key fields:

Field Behaviour
types Conventional types shown in the type step (falls back to the 11-entry Conventional list)
scopeFilter Exclude discovered projects from the scope picker by path/name
headerMaxLength Drives the live countdown in the subject step (green → yellow → red); null disables
validateRuleset Shared with cl and the preview step; see commits/validate presets
skipCommit When true, the session returns the formatted message without touching the git index
commitExecutor Inject a custom executor (e.g. signed commits, alternate cwd) instead of the default

Resolution:

  1. --config <path> override (relative paths resolve against cwd)
  2. Walk upward from cwd looking for one of commit.config.{js,mjs,cjs}
  3. Stop at a workspace boundary (.git/, pnpm-workspace.yaml)

Usage

import { createAuthorSession, runAuthorSession } from '@hyperfrontend/versioning/commits/author'

const session = createAuthorSession({ config: { skipCommit: false } })
const outcome = await runAuthorSession(session)

if (outcome.status === 'committed') {
  console.log('Committed:', outcome.message)
} else {
  console.error('Cancelled:', outcome.error?.message ?? '<user abort>')
  process.exit(130)
}

Design Decisions

  • D8 — Step order: resolve-scope → type → scope → subject → body → breaking → issues → preview → commit
  • D15c — Commit execution is default-on; inject skipCommit / commitExecutor to override
  • D17a — Config file lives at workspace root (not in package.json)
  • E1a — Empty staging refuses with an error; bins translate that to a non-zero exit code

API Reference

ƒ Functions

§function

cancelled(error?: Error): StepResult

Helper for building a cancelled result with an optional error.

Parameters

NameTypeDescription
§error?
Error
Cause of cancellation attached to the result (omit when the user aborted cleanly)

Returns

StepResult
Step result that stops the session

Example

Cancelling after an executor throws

try { await executor() } catch (err) { return cancelled(err as Error) }
§function

createAuthorSession(options: CreateAuthorSessionOptions): AuthorSession

Prepares an authoring session without executing it. The returned object carries a resolved config (defaults filled in) and the effective step list — allowing bin wrappers to introspect before running.

Parameters

NameTypeDescription
§options
CreateAuthorSessionOptions
Optional overrides for steps and partial config
(default: {})

Returns

AuthorSession
Session description consumable by runAuthorSession

Examples

Using the conventional preset and default config

const session = createAuthorSession()
await runAuthorSession(session)

Overriding only the validate ruleset

createAuthorSession({ config: { validateRuleset: customRuleset } })
§function

createSessionContext(config: SessionConfig): SessionContext

Creates a fresh session context seeded with an empty draft.

Parameters

NameTypeDescription
§config
SessionConfig
Resolved session configuration

Returns

SessionContext
Session context ready for the first step

Example

Creating a bare context

createSessionContext(config)
// => { draft: {}, candidateScopes: [], defaultScope: undefined, config }
§function

defaultScope(candidates: unknown): string

Computes the default scope to pre-select in the scope step.
Algorithm (matches locked decision D3):
  1. A single candidate wins outright.
  2. Otherwise, if one candidate's path is a prefix of every other
candidate's path, that nearest common ancestor wins.
  1. Otherwise, there is no default.

Parameters

NameTypeDescription
§candidates
unknown
Scopes produced by discoverScopes

Returns

string
Scope name to pre-select, or undefined when no default is resolvable

Examples

Single owner wins

defaultScope([{ name: 'alpha', path: '/repo/libs/alpha' }])
// => 'alpha'

Common ancestor wins

defaultScope([
  { name: 'root', path: '/repo' },
  { name: 'alpha', path: '/repo/libs/alpha' },
])
// => 'root'

Disjoint ownership has no default

defaultScope([
  { name: 'alpha', path: '/repo/libs/alpha' },
  { name: 'beta', path: '/repo/libs/beta' },
])
// => undefined
§function

discoverScopes(stagedPaths: unknown, options: DiscoverScopesOptions): unknown

Resolves each staged path to its owning project by walking upward to the nearest project.json / package.json, reads the name field, and returns the unique set as DiscoveredScope entries. Entries inside node_modules or .git are dropped before walking; the caller's scopeFilter is applied after collection.

Parameters

NameTypeDescription
§stagedPaths
unknown
Paths emitted by the staged-paths provider (relative or absolute)
§options
DiscoverScopesOptions
Filter + cwd configuration

Returns

unknown
Unique scopes in staging order (first appearance wins)

Example

Discovering scopes for a set of staged files

discoverScopes(['libs/versioning/src/a.ts', 'libs/questions/src/b.ts'], { cwd: '/repo' })
// => [
//   { name: '@hyperfrontend/versioning', path: '/repo/libs/versioning' },
//   { name: '@hyperfrontend/questions', path: '/repo/libs/questions' },
// ]
§function

done(): StepResult

Helper for building a done result.

Returns

StepResult
Step result that advances the runner to the next step

Example

Returning done from a step

async run(): Promise<StepResult> { return done() }
§function

getStagedPaths(options: StagedPathsOptions): unknown

Default staged-paths provider. Shells out to git diff --cached --name-only -z, splits on NUL, and filters out the trailing empty entry. Returns paths relative to the git repository root (same as git's own output).

Parameters

NameTypeDescription
§options
StagedPathsOptions
Resolver options (cwd, optional timeout)

Returns

unknown
Staged file paths as emitted by git

Example

Reading the current staging area

getStagedPaths({ cwd: '/repo' })
// => ['libs/versioning/src/commits/author/index.ts']
§function

goto(stepId: string): StepResult

Helper for building a goto result pointing at the supplied step id.

Parameters

NameTypeDescription
§stepId
string
Target step id the runner should jump to

Returns

StepResult
Step result that redirects the runner to the named step

Example

Sending the runner back to the `type` step

return goto('type')
§function

loadCommitConfig(options: LoadCommitConfigOptions): Promise<LoadedCommitConfig>

Loads the user's commit config. Resolution:
  1. overridePath wins when set
  2. Otherwise walk upward from cwd looking for one of CONFIG_FILE_NAMES
until a workspace boundary marker (.git, pnpm-workspace.yaml) is hit
  1. Missing config is not an error — config becomes {}

Parameters

NameTypeDescription
§options
LoadCommitConfigOptions
Search inputs

Returns

Promise<LoadedCommitConfig>
Loaded partial config plus the file it came from (if any)

Example

Auto-discovery from cwd

const { config, sourcePath } = await loadCommitConfig({ cwd: process.cwd() })
§function

normalizeSubject(raw: string): string

Applies subject normalisation rules (decision D10): trim edges and strip a single trailing period.

Parameters

NameTypeDescription
§raw
string
Subject string as entered by the user

Returns

string
Normalized subject

Example

Stripping whitespace and a trailing period

normalizeSubject('  add login flow.  ') // => 'add login flow'
§function

parseReferences(raw: string): unknown

Parses a free-form references string into issue footers. Accepts commas, semicolons, or whitespace between entries; ignores tokens that don't match the keyword #number shape.

Parameters

NameTypeDescription
§raw
string
Raw user input from the issues prompt

Returns

unknown
Footers matching each recognized reference

Example

Parsing a mixed references string

parseReferences('fixes #123, re #456')
// => [
//   { key: 'Fixes', value: '123', separator: ' #' },
//   { key: 'Re', value: '456', separator: ' #' },
// ]
§function

resolveSessionConfig(overrides: PartialSessionConfig): SessionConfig

Overlays a partial config over the built-in defaults, producing a fully populated SessionConfig usable by the runner.

Parameters

NameTypeDescription
§overrides
PartialSessionConfig
Partial config supplied by the caller (may be undefined)
(default: {})

Returns

SessionConfig
Fully resolved configuration

Example

Resolving with no overrides

resolveSessionConfig()
// => { types: DEFAULT_SESSION_TYPES, scopeOptional: false, ... }
§function

runAuthorSession(session: AuthorSession): Promise<SessionOutcome>

Executes the session step-by-step. Handles goto jumps, surfaces Ctrl-C cancellations as { status: 'cancelled' } outcomes, and — on success — returns the final formatted message alongside the committed status.

Parameters

NameTypeDescription
§session
AuthorSession
Session description produced by createAuthorSession

Returns

Promise<SessionOutcome>
Terminal outcome describing how the session ended

Example

Running a session to completion

const session = createAuthorSession({ config: { skipCommit: true } })
const outcome = await runAuthorSession(session)
// => { status: 'committed', message: 'feat: ...' }
§function

typeToChoice(type: SessionType): Choice<string>

Maps a SessionType to a Choice usable by the select prompt.

Parameters

NameTypeDescription
§type
SessionType
Session type entry to render as a selectable choice

Returns

Choice<string>
Choice with the type name as both label and value, plus an optional hint

Example

Rendering the feat type as a select choice

typeToChoice({ name: 'feat', description: 'A new feature' })
// => { label: 'feat', value: 'feat', hint: 'A new feature' }

Interfaces

§interface

AuthorSession

Prepared session ready to be handed to runAuthorSession. Contains the fully resolved config and the ordered step list that will be executed.

Properties

§readonly config:SessionConfig
Fully resolved session configuration
§readonly steps:unknown
Ordered step list the runner will iterate
§interface

CreateAuthorSessionOptions

Inputs accepted by createAuthorSession — the caller may supply their own step list; anything missing is resolved against the defaults used by the conventional preset.

Properties

§readonly config?:PartialSessionConfig
Partial configuration overlaid on the built-in defaults
§readonly steps?:unknown
Override for the step sequence; defaults to conventionalPreset
§interface

DiscoveredScope

Single discovered scope with the project root it came from.

Properties

§readonly name:string
Scope identifier (the project's name field)
§readonly path:string
Absolute path to the owning project root
§interface

DiscoverScopesOptions

Options controlling discoverScopes.

Properties

§readonly cwd:string
Working directory that staged paths are resolved against
§readonly scopeFilter?:(context: ScopeFilterContext) => boolean
Optional caller-provided filter; applied after built-in exclusions
§interface

LoadCommitConfigOptions

Inputs accepted by loadCommitConfig.

Properties

§readonly cwd:string
Directory to start the upward search from
§readonly overridePath?:string
Explicit override path from --config <path> — resolved against cwd when relative
§interface

LoadedCommitConfig

Return shape of loadCommitConfig.

Properties

§readonly config:PartialSessionConfig
Resolved configuration (empty object when no config was found)
§readonly sourcePath?:string
Absolute path of the config file that was loaded (undefined when nothing was found)
§interface

ScopeFilterContext

Signature for the scope filter passed in SessionConfig.scopeFilter.

Properties

§readonly name:string
name field read from project.json (or package.json as fallback)
§readonly path:string
Absolute path to the discovered project root
§interface

SessionConfig

Fully resolved authoring session configuration. Consumers typically supply a partial shape — the config loader (or createAuthorSession) fills the gaps with defaults before handing the result to the runner.

Properties

§readonly commitExecutor?:CommitExecutor
Overrides the default git commit executor
§readonly cwd:string
Working directory used for git operations and scope discovery
§readonly headerMaxLength:number
Maximum header length used by the subject countdown (null disables)
§readonly imperativeWordlist:Readonly<Record<string, string>>
Wordlist used by the imperative-mood rule in inline validation
§readonly input?:ReadStream
Optional stream override passed to every prompt in the session
§readonly output?:WriteStream
Optional stream override passed to every prompt in the session
§readonly scopeFilter?:(context: ScopeFilterContext) => boolean
Optional filter applied to discovered scopes — receives { path, name }
§readonly scopeMulti:boolean
When true, the scope step collects multiple scopes via multiselect
§readonly scopeOptional:boolean
When true, the scope step may be skipped even if no candidates are found
§readonly skipCommit:boolean
When true, the final commit step is a no-op (session returns the message only)
§readonly stagedPathsProvider:StagedPathsProvider
Resolver for the staged file list; defaults to git diff --cached --name-only -z
§readonly types:unknown
Commit type enum shown in the type step
§readonly validateRuleset:Ruleset
Validation ruleset used for inline and preview-time warnings
§interface

SessionContext

Mutable container threaded through every step. The draft field accumulates the in-progress commit message; candidateScopes/defaultScope are filled by the resolve-scope step and consumed by later steps. config is the fully resolved SessionConfig shared across the session.

Properties

§candidateScopes:unknown
Scopes discovered from the staged file set (may be empty)
§readonly config:SessionConfig
Resolved configuration for the session
§defaultScope:string
Default scope pre-selected by the scope step (undefined = no default)
§draft:CommitDraft
In-progress commit draft accumulated as steps execute
§interface

SessionOutcome

Terminal outcome returned by runAuthorSession.

Properties

§readonly error?:Error
Error attached when the session cancelled due to an explicit failure
§readonly message?:string
Formatted commit message (populated whenever the draft reached the preview step)
§readonly status:SessionStatus
Status tag describing how the session ended
§interface

SessionType

Conventional-commit type entry shown in the type step's choice list.

Properties

§readonly description?:string
Short description rendered as a prompt hint
§readonly name:string
Bare identifier (e.g. feat, fix)
§interface

StagedPathsOptions

Options accepted by the default staged-paths provider.

Properties

§readonly cwd:string
Working directory used as the git command cwd
§readonly timeout?:number
Timeout in milliseconds (default: 30000)
§interface

Step

A named step in an authoring session.

Properties

§readonly id:string
Stable identifier used by goto and for debugging
§interface

StepResult

Outcome produced by a step implementation.

Properties

§readonly error?:Error
Error attached when status === 'cancelled'
§readonly gotoStepId?:string
Target step id when status === 'goto'
§readonly status:StepStatus
Status tag used by the runner to decide how to proceed

Types

§type

CommitExecutor

Signature for executing the final git commit.
type CommitExecutor = (message: string) => void | Promise<void>
§type

PartialSessionConfig

Partial shape accepted by createAuthorSession/loadCommitConfig.
type PartialSessionConfig = Partial<SessionConfig>
§type

SessionStatus

Session outcome status constants.
type SessionStatus = Readonly<{ Cancelled: "cancelled"; Committed: "committed" }>
§type

SessionStatus

Session outcome status constants.
type SessionStatus = Readonly<{ Cancelled: "cancelled"; Committed: "committed" }>
§type

StagedPathsProvider

Signature for resolving the staged file paths the session operates on.
type StagedPathsProvider = (options: StagedPathsProviderOptions) => unknown
§type

StepStatus

Result status constants returned by steps.
type StepStatus = Readonly<{ Cancelled: "cancelled"; Done: "done"; Goto: "goto" }>
§type

StepStatus

Result status constants returned by steps.
type StepStatus = Readonly<{ Cancelled: "cancelled"; Done: "done"; Goto: "goto" }>

Variables

§type

bodyStep

Step that prompts for an optional commit body. Empty input leaves draft.body unset (the formatter then omits the body section).
§type

breakingStep

Step that asks whether the commit introduces a breaking change. A positive answer follows up with a description prompt; the description is written to draft.breakingDescription and is eventually synthesized into a BREAKING CHANGE: footer by formatCommitMessage.
§type

commitStep

Step that renders the draft one last time and hands the message to the configured commit executor. When config.skipCommit is true the step short-circuits so the caller receives the formatted message without touching the working tree.
§type

CONFIG_FILE_NAMES

Supported config file names, searched in this order per directory.
§type

conventionalStepPreset

Default ordered step list used by createAuthorSession when the caller does not supply their own sequence. Matches decision D8: resolve-scope → type → scope → subject → body → breaking → issues → preview → commit.
§type

DEFAULT_SESSION_TYPES

Default type enum rendered by the type step when the caller does not supply their own list. Each conventional type is paired with a short hint.
§type

EMPTY_STAGING_MESSAGE

Error thrown to the runner when the staging area is empty.
§type

issuesStep

Step that optionally collects issue references (e.g. fixes #123, re #456) and appends them to draft.footers using the # separator form.
The step is additive: existing footers stay, and any breaking-change footer synthesized later by the formatter is unaffected.
§type

NO_SCOPE_CANDIDATES_MESSAGE

Error thrown when the scope step cannot proceed because no candidates were found.
§type

PREVIEW_RESTART_STEP_ID

Step id the preview jumps back to when the user rejects the draft.
§type

previewStep

Step that renders the accumulated draft as the final commit message and asks the user to confirm. A negative answer jumps back to the type step while keeping the draft intact for round-trip editing. Validation warnings are printed before the prompt.
§type

resolveScopeStep

Non-prompt step that computes the candidate scope list and default scope.
Calls config.stagedPathsProvider()discoverScopesdefaultScope and writes the results into the context. Cancels the session when the staging area is empty (decision E1a).
§type

scopeStep

Step that prompts the user to pick the commit scope(s) from the candidate list discovered by resolve-scope. Respects scopeOptional (cancels with an actionable error when no candidates exist and scope is required) and scopeMulti (multiselect vs. single-select).
§type

subjectStep

Step that prompts for the commit subject. Strips the trailing period, trims edges, and enforces a non-empty subject. When headerMaxLength is set, a live countdown re-renders on every keystroke (green → yellow → red as the remaining budget shrinks).
§type

typeStep

Step that prompts the user for the commit type. Uses the searchable select prompt and seeds the cursor from any existing ctx.draft.type (so a preview → cancel restart re-opens on the previous choice).