@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 rev-parse --show-toplevel, reads git diff --cached --name-only -z, and returns each staged path as an absolute path anchored at that root. Git emits staged paths relative to the repository root no matter which directory the command runs from, so anchoring here keeps downstream project-root discovery correct when the session cwd is a subdirectory.
Parameters
| Name | Type | Description |
|---|---|---|
§options | StagedPathsOptions | Resolver options (cwd, optional timeout) |
Returns
unknownExample
Reading the current staging area from a subdirectory
getStagedPaths({ cwd: '/repo/apps/demo' })
// => ['/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).