@hyperfrontend/versioning/commits/authorauthor/
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:
--config <path>override (relative paths resolve againstcwd)- Walk upward from
cwdlooking for one ofcommit.config.{js,mjs,cjs} - 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/commitExecutorto 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
cancelled result with an optional error.Parameters
| Name | Type | Description |
|---|---|---|
§error? | Error | Cause of cancellation attached to the result (omit when the user aborted cleanly) |
Returns
StepResultExample
Cancelling after an executor throws
try { await executor() } catch (err) { return cancelled(err as Error) }config (defaults filled in) and the effective step list — allowing bin wrappers to introspect before running.Parameters
| Name | Type | Description |
|---|---|---|
§options | CreateAuthorSessionOptions | Optional overrides for steps and partial config (default: {}) |
Returns
AuthorSessionrunAuthorSessionExamples
Using the conventional preset and default config
const session = createAuthorSession()
await runAuthorSession(session)Overriding only the validate ruleset
createAuthorSession({ config: { validateRuleset: customRuleset } })Parameters
| Name | Type | Description |
|---|---|---|
§config | SessionConfig | Resolved session configuration |
Returns
SessionContextExample
Creating a bare context
createSessionContext(config)
// => { draft: {}, candidateScopes: [], defaultScope: undefined, config }scope step. Algorithm (matches locked decision D3):
- A single candidate wins outright.
- Otherwise, if one candidate's path is a prefix of every other
- Otherwise, there is no default.
Parameters
| Name | Type | Description |
|---|---|---|
§candidates | unknown | Scopes produced by discoverScopes |
Returns
stringExamples
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' },
])
// => undefinedproject.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
Returns
unknownExample
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' },
// ]done result.Returns
StepResultExample
Returning done from a step
async run(): Promise<StepResult> { return done() }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
| Name | Type | Description |
|---|---|---|
§options | StagedPathsOptions | Resolver options (cwd, optional timeout) |
Returns
unknownExample
Reading the current staging area
getStagedPaths({ cwd: '/repo' })
// => ['libs/versioning/src/commits/author/index.ts']goto result pointing at the supplied step id.Parameters
| Name | Type | Description |
|---|---|---|
§stepId | string | Target step id the runner should jump to |
Returns
StepResultExample
Sending the runner back to the `type` step
return goto('type')overridePathwins when set- Otherwise walk upward from
cwdlooking for one ofCONFIG_FILE_NAMES
.git, pnpm-workspace.yaml) is hit - Missing config is not an error —
configbecomes{}
Parameters
| Name | Type | Description |
|---|---|---|
§options | LoadCommitConfigOptions | Search inputs |
Returns
Promise<LoadedCommitConfig>Example
Auto-discovery from cwd
const { config, sourcePath } = await loadCommitConfig({ cwd: process.cwd() })Parameters
| Name | Type | Description |
|---|---|---|
§raw | string | Subject string as entered by the user |
Returns
stringExample
Stripping whitespace and a trailing period
normalizeSubject(' add login flow. ') // => 'add login flow'keyword #number shape.Parameters
| Name | Type | Description |
|---|---|---|
§raw | string | Raw user input from the issues prompt |
Returns
unknownExample
Parsing a mixed references string
parseReferences('fixes #123, re #456')
// => [
// { key: 'Fixes', value: '123', separator: ' #' },
// { key: 'Re', value: '456', separator: ' #' },
// ]SessionConfig usable by the runner.Parameters
| Name | Type | Description |
|---|---|---|
§overrides | PartialSessionConfig | Partial config supplied by the caller (may be undefined) (default: {}) |
Returns
SessionConfigExample
Resolving with no overrides
resolveSessionConfig()
// => { types: DEFAULT_SESSION_TYPES, scopeOptional: false, ... }goto jumps, surfaces Ctrl-C cancellations as { status: 'cancelled' } outcomes, and — on success — returns the final formatted message alongside the committed status.Parameters
| Name | Type | Description |
|---|---|---|
§session | AuthorSession | Session description produced by createAuthorSession |
Returns
Promise<SessionOutcome>Example
Running a session to completion
const session = createAuthorSession({ config: { skipCommit: true } })
const outcome = await runAuthorSession(session)
// => { status: 'committed', message: 'feat: ...' }SessionType to a Choice usable by the select prompt.Parameters
| Name | Type | Description |
|---|---|---|
§type | SessionType | Session type entry to render as a selectable choice |
Returns
Choice<string>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
runAuthorSession. Contains the fully resolved config and the ordered step list that will be executed.createAuthorSession — the caller may supply their own step list; anything missing is resolved against the defaults used by the conventional preset.discoverScopes.loadCommitConfig.loadCommitConfig.SessionConfig.scopeFilter.createAuthorSession) fills the gaps with defaults before handing the result to the runner.Properties
readonly headerMaxLength:number— readonly imperativeWordlist:Readonly<Record<string, string>>— readonly scopeFilter?:(context: ScopeFilterContext) => boolean— { path, name }readonly scopeOptional:boolean— scope step may be skipped even if no candidates are foundreadonly skipCommit:boolean— commit step is a no-op (session returns the message only)readonly stagedPathsProvider:StagedPathsProvider— git diff --cached --name-only -zdraft 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
runAuthorSession.type step's choice list.Properties
◆ Types
type CommitExecutor = (message: string) => void | Promise<void>createAuthorSession/loadCommitConfig.type PartialSessionConfig = Partial<SessionConfig>type SessionStatus = Readonly<{ Cancelled: "cancelled"; Committed: "committed" }>type SessionStatus = Readonly<{ Cancelled: "cancelled"; Committed: "committed" }>type StagedPathsProvider = (options: StagedPathsProviderOptions) => unknowntype StepStatus = Readonly<{ Cancelled: "cancelled"; Done: "done"; Goto: "goto" }>type StepStatus = Readonly<{ Cancelled: "cancelled"; Done: "done"; Goto: "goto" }>● Variables
draft.body unset (the formatter then omits the body section).draft.breakingDescription and is eventually synthesized into a BREAKING CHANGE: footer by formatCommitMessage.config.skipCommit is true the step short-circuits so the caller receives the formatted message without touching the working tree.createAuthorSession when the caller does not supply their own sequence. Matches decision D8: resolve-scope → type → scope → subject → body → breaking → issues → preview → commit.type step when the caller does not supply their own list. Each conventional type is paired with a short hint.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 step while keeping the draft intact for round-trip editing. Validation warnings are printed before the prompt.Calls
config.stagedPathsProvider() → discoverScopes → defaultScope and writes the results into the context. Cancels the session when the staging area is empty (decision E1a).resolve-scope. Respects scopeOptional (cancels with an actionable error when no candidates exist and scope is required) and scopeMulti (multiselect vs. single-select).headerMaxLength is set, a live countdown re-renders on every keystroke (green → yellow → red as the remaining budget shrinks).ctx.draft.type (so a preview → cancel restart re-opens on the previous choice).