gitface
v0.8.0
Published
A simple CLI tool to change your “face” in Git.
Readme

GitFace ·

Commit with the right face, every time.
GitFace keeps your Git personas in sync. Capture each identity once, store it as
JSON, and apply it to any repository without touching git config.
Why GitFace?
- Frictionless switching – apply profiles to any repo with
gitface use <profile>. - Delightful UX – flags for scripts, Interactive prompts for human.
- Local first – profiles live right on your machine.
- Safety rails – default scope is local, never pollute your global config by mistake.
Install
npm install --global gitfacePrefer one-off use? Run npx gitface --help.
Quick Start
# Create a profile interactively
gitface new work
# Non-interactive creation (useful for CI scripts)
gitface new oss --git-name "Open Source" --email "[email protected]"
# See what is saved (sorted by last update)
gitface list
# Apply a profile to the current repo (local scope is the default)
gitface use work
# Inspect the active Git identity
gitface currentRun gitface <command> --help to see all flags and examples.
Enable Shell Completion
To make it persistent, add one of these lines to your shell rc:
# ~/.zshrc
eval "$(gitface completion snippet --shell zsh)"For Bash:
# ~/.bashrc
eval "$(gitface completion snippet --shell bash)"Documentation
- User Manual (Traditional Chinese) – step-by-step onboarding and common workflows.
- CLI Reference – full command/flag behavior and JSON outputs.
- Profiles & Storage Notes – persistence layout and implementation notes.
Tab Completion
- Generate shell snippets from the CLI to stay in sync with new releases:
- Zsh:
gitface completion snippet --shell zsh - Bash:
gitface completion snippet --shell bash
- Zsh:
- Append the snippet to your shell rc (or source it) and reload your terminal.
- Generated snippets use
gitface completion profiles --limit 50to keep completion responsive for large profile sets. gitface completion profiles --prefix <value>uses case-insensitive prefix matching, sowocan matchWork/work-admin.gitface completion profiles --jsonemits machine-readable payload:{ "topic": "profiles", "prefix": "wo", "limit": 5, "count": 1, "names": ["work-admin"] }.gitface completion profiles --json-envelopeemits Result Envelope output for automation/CI observability:{ "status": "success", "code": "COMPLETION_PROFILES_OK", "message": "Completion profiles resolved.", "data": { "topic": "profiles", "prefix": "wo", "limit": 5, "count": 1, "names": ["work-admin"] }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface completion profilesreads profile names from local profile filenames, so unrelated malformed profile JSON payloads do not block tab completion.- Completion is scoped to source-profile arguments for
use,rm/remove,edit,clone, andrename/mv, plusrules addprofile argument.
Command Reference
| Command | Description |
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| gitface new <profile> | Create a profile from prompts or flags (--git-name, --email, --signing-key, --force, --dry-run, --json). |
| gitface edit <profile> | Update a stored profile via flags or an interactive editor; supports --dry-run and --json output. |
| gitface list | Render saved profiles (Ink on TTY, plain text on non-TTY), filter with --query, sort with --sort (updated/name), cap output with --limit, or use list --json / list --json-envelope. |
| gitface use <profile> | Apply a profile to Git config; supports --scope, --query, --dry-run, plus use --json and use --json-envelope output. |
| gitface current | Display active Git identity; supports --scope, current --json, and current --json-envelope machine-readable output. |
| gitface doctor | Run environment diagnostics; checks Git install, profile store, and explicit global Git identity (--json, --json-envelope, --strict available). |
| gitface export [file] | Export all profiles as JSON to stdout or a file; supports legacy --json summary and --json-envelope unified output. |
| gitface import <file> | Import profiles from JSON; supports --dry-run, --strict, --atomic, plus --json / --json-envelope for automation and CI gating. |
| gitface clone <src> <tgt> | Clone a profile to a new name; supports --dry-run and --json output. |
| gitface rename <old> <new> | Rename a profile (alias: mv); supports --dry-run, rename --json, and rename --json-envelope for safer automation. |
| gitface rm <profile> | Remove a profile; supports --dry-run, --force, plus remove --json and remove --json-envelope for safer automation. |
| gitface rules <subcommand> | Manage folder rules (list, add, remove, resolve, apply, doctor, prune) with optional --json; rules add/remove/resolve/apply also support --json-envelope; mutations support --dry-run; rules list supports --query, --limit, and --health (--concurrency in health mode); rules apply supports --fallback-profile; rules resolve/apply/doctor/prune --strict support CI gating; rules doctor/prune --concurrency tune integrity scan parallelism. |
Profiles & Storage
- Files are saved as prettified JSON in
~/.config/gitface/profiles/<profile>.json. - Git include files are generated in
~/.config/gitface/identities/<profile>.gitconfigfor folder rules and advanced git config workflows. - Profile and identity files are written with atomic replace semantics (temp file + rename) to reduce partial-write corruption risk.
- Profile names must be non-empty and must not contain path separators (
/,\), NUL, or reserved dot segments (./..). - When you omit
--git-nameor--email, GitFace falls back to the identity reported bygit config. - Created profiles capture
createdAtandupdatedAtISO timestamps for auditing. --signing-keyvalues map touser.signingkey; usegitface edit <name> --unset-signing-keyto remove it.gitface new <name> --git-name <value> --email <value> --jsonemits:{ "status": "created", "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }.gitface new <name> --git-name <value> --email <value> --dry-run --jsonpreviews creation without writing:{ "status": "dry-run", "name": "work", "overwrite": false, "gitName": "Work User", "email": "[email protected]", "signingKey": null }.gitface new <name> --jsonwithout non-interactive field flags emits:{ "status": "error", "name": "work", "reason": "Non-interactive flags are required when using --json output mode." }.gitface edit <name> --git-name <value> --jsonemits:{ "status": "updated", "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }.gitface edit <name> --git-name <value> --dry-run --jsonpreviews updates without writing:{ "status": "dry-run", "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }.gitface edit <name> --jsonwithout non-interactive field flags emits:{ "status": "error", "name": "work", "reason": "Non-interactive flags are required when using --json output mode." }.gitface import <file> --dry-runvalidates payload and duplicate handling without changing local profile files.gitface import <file> --strictexits with code1when any entry fails to import/validate (also works with--dry-runand--json) for CI gating.gitface import <file> --atomicruns a full precheck first; if any entry fails, no profile is written in that run and exit code is1.gitface import <file> --jsonemits machine-readable summary:{ "dryRun": false, "total": 2, "imported": 2, "failed": 0, "results": [{ "name": "work", "status": "imported", "message": "Imported." }] }.gitface import <file> --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "IMPORT_PROFILES_OK", "message": "Profiles imported successfully.", "data": { "file": "./profiles.json", "strict": false, "overwrite": false, "atomic": false, "dryRun": false, "total": 2, "imported": 2, "failed": 0, "results": [{ "name": "work", "status": "imported", "message": "Imported." }] }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface import <file> --atomic --jsonon precheck failure emits all entries as failed (invalid entries + skipped entries), for example:{ "dryRun": false, "total": 2, "imported": 0, "failed": 2, "results": [{ "name": "work", "status": "failed", "message": "Profile already exists. Use --overwrite to replace." }, { "name": "personal", "status": "failed", "message": "Skipped due to --atomic precheck failure." }] }.gitface import <file> --atomic --json-envelopeon precheck failure emits envelope error and exits with code1:{ "status": "error", "code": "IMPORT_PROFILES_ATOMIC_ABORTED", "message": "Atomic precheck failed; no profiles were written.", "data": { "atomic": true, "failed": 2 }, "errors": [{ "code": "IMPORT_PROFILE_ATOMIC_FAILED", "message": "work: Profile 'work' already exists." }], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface remove <name> --jsonemits machine-readable status:{ "status": "removed", "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }.gitface remove <name> --dry-run --jsonpreviews deletion without writing:{ "status": "dry-run", "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }.gitface remove <name> --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "REMOVE_PROFILE_OK", "message": "Profile removed successfully.", "data": { "result": "removed", "name": "work", "force": false, "profile": { "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }, "reason": null }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface remove missing --force --json-envelopeemits skipped Result Envelope output:{ "status": "success", "code": "REMOVE_PROFILE_SKIPPED", "message": "Profile removal skipped due to --force missing profile.", "data": { "result": "skipped", "name": "missing", "force": true, "profile": null, "reason": "Profile not found." }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 1, "traceId": "..." } }.- Missing-profile failures in
use/clone/rename/remove/rules addnow include best-effortDid you mean ...suggestions. gitface use <profile> --dry-run --jsonpreviews scope-specific git config changes without writing:{ "status": "dry-run", "scope": "local", "hasChanges": true, "profile": { "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }, "current": { "gitName": "Current User", "email": "[email protected]", "signingKey": null }, "changes": [{ "key": "user.name", "action": "set", "before": "Current User", "after": "Work User" }] }.gitface use <profile> --jsonemits machine-readable success output:{ "status": "applied", "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null, "scope": "local", "hasChanges": true, "changes": [{ "key": "user.name", "action": "set", "before": "Current User", "after": "Work User" }] }.gitface use <profile> --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "USE_PROFILE_APPLIED", "message": "Profile applied to Git config.", "data": { "result": "applied", "scope": "local", "profile": { "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }, "hasChanges": true, "changes": [{ "key": "user.name", "action": "set", "before": "Current User", "after": "Work User" }] }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface current --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "CURRENT_IDENTITY_RESOLVED", "message": "Current Git identity resolved.", "data": { "gitName": "Work User", "email": "[email protected]", "signingKey": "ABC123", "scope": "global" }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 1, "traceId": "..." } }.gitface use <profile> --jsonreturns an explicit no-op payload when the active scope already matches the profile:{ "status": "unchanged", "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null, "scope": "local", "changes": [] }.gitface list --query worfilters profiles by case-insensitive name substring matching (works in both human and JSON modes).gitface list --sort namesorts output alphabetically by profile name; default--sort updatedkeeps most-recently-updated first.gitface list --limit 10caps displayed/JSON rows after sorting and query filtering.gitface list --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "LIST_PROFILES_OK", "message": "Profiles listed successfully.", "data": { "profiles": [{ "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null, "createdAt": "...", "updatedAt": "..." }], "query": "wo", "sort": "updated", "limit": 10, "count": 1 }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface listautomatically falls back to deterministic plain-text output when stdout is not a TTY (for example when piped in scripts).gitface rules add <dir> <profile> --jsonemits machine-readable status:{ "status": "added", "directory": "/abs/path/", "profileName": "work" }.gitface rules add <dir> <profile> --dry-run --jsonpreviews add/update without writing:{ "status": "dry-run", "directory": "/abs/path/", "profileName": "work", "overwrite": false }.gitface rules add <dir> <profile> --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "RULE_ADD_OK", "message": "Rule added successfully.", "data": { "result": "added", "directory": "/abs/path/", "profileName": "work", "overwrite": false }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface rules remove <dir> --jsonemits machine-readable status:{ "status": "removed", "directory": "/abs/path/" }.gitface rules remove <dir> --dry-run --jsonpreviews removal without writing:{ "status": "dry-run", "directory": "/abs/path/", "exists": true }.gitface rules remove <dir> --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "RULE_REMOVE_OK", "message": "Rule removed successfully.", "data": { "result": "removed", "directory": "/abs/path/", "exists": null }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface rules list --query work --limit 10 --jsonfilters by directory/profile substring, returns deterministic directory-sorted rows, and caps output size for scripts.gitface rules list --health --jsonemits integrity-aware report output:{ "rules": [{ "directory": "/abs/path/", "profileName": "work", "status": "pass", "profileExists": true, "directoryExists": true }], "summary": { "total": 1, "pass": 1, "warn": 0, "fail": 0 }, "metrics": { "concurrency": 1, "scanned": 1, "uniqueProfilesChecked": 1, "uniqueDirectoriesChecked": 1, "scanDurationMs": 2 } }.gitface rules list --health --concurrency <number>limits concurrent integrity checks in health mode (default8; must be a positive integer).- Rules commands read
includeIf.gitdir:*entries via targeted regexp lookup (with fallback to full global config scan on unexpected Git errors), keeping rule resolution responsive in large.gitconfigsetups. gitface rules resolve [dir] --jsonresolves the most specific matching rule for a target directory:{ "status": "matched", "directory": "/abs/path/repo/", "matchedRule": { "directory": "/abs/path/", "profileName": "work" }, "profileExists": true }.gitface rules resolve [dir] --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "RULE_RESOLVE_MATCHED", "message": "Rule resolved successfully.", "data": { "result": "matched", "directory": "/abs/path/repo/", "matchedRule": { "directory": "/abs/path/", "profileName": "work" }, "profileExists": true }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.- On macOS/Windows,
rules resolve/applytreat directory matching as case-insensitive to align with common filesystem behavior; Linux keeps case-sensitive matching. gitface rules resolve [dir] --jsonwhen no rule matches:{ "status": "unmatched", "directory": "/abs/path/repo/", "matchedRule": null, "profileExists": null }.gitface rules resolve [dir] --stricttreatsunmatchedandmatched + profileExists=falseas non-zero exit results for CI gating.gitface rules apply [dir] --jsonresolves and applies matched profile in one step:{ "status": "applied", "directory": "/abs/path/repo/", "scope": "local", "matchedRule": { "directory": "/abs/path/", "profileName": "work" }, "profile": { "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null } }.gitface rules apply [dir] --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "RULE_APPLY_APPLIED", "message": "Matched rule profile applied successfully.", "data": { "result": "applied", "resolution": "matched", "directory": "/abs/path/repo/", "scope": "local", "matchedRule": { "directory": "/abs/path/", "profileName": "work" }, "fallbackProfileName": null, "profile": { "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }, "hasChanges": true, "changes": [{ "key": "user.name", "action": "set", "before": "Current User", "after": "Work User" }] }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface rules apply [dir]applies local scope against the target directory directly and does not mutate the caller process working directory.gitface rules apply [dir] --dry-run --jsonpreviews scope-specific changes without writing:{ "status": "dry-run", "directory": "/abs/path/repo/", "scope": "local", "matchedRule": { "directory": "/abs/path/", "profileName": "work" }, "profile": { "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }, "current": { "gitName": "Current User", "email": "[email protected]", "signingKey": null }, "hasChanges": true, "changes": [{ "key": "user.name", "action": "set", "before": "Current User", "after": "Work User" }] }.gitface rules apply [dir] --jsonwhen no rule matches:{ "status": "unmatched", "directory": "/abs/path/repo/", "scope": "local", "matchedRule": null }.gitface rules apply [dir] --fallback-profile work --jsonapplies fallback profile when no rule matches:{ "status": "applied", "resolution": "fallback", "directory": "/abs/path/repo/", "scope": "local", "matchedRule": null, "fallbackProfileName": "work", "profile": { "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null } }.gitface rules apply [dir] --dry-run --fallback-profile work --jsonpreviews fallback application without writing:{ "status": "dry-run", "resolution": "fallback", "directory": "/abs/path/repo/", "scope": "local", "matchedRule": null, "fallbackProfileName": "work", "profile": { "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }, "current": { "gitName": "Current User", "email": "[email protected]", "signingKey": null }, "hasChanges": true, "changes": [{ "key": "user.name", "action": "set", "before": "Current User", "after": "Work User" }] }.gitface rules apply [dir] --stricttreatsunmatchedas non-zero exit results for CI gating.gitface rules doctor --jsonchecks every rule for missing profile/directory and emits:{ "status": "issues", "strict": false, "summary": { "total": 2, "pass": 1, "warn": 1, "fail": 0 }, "metrics": { "concurrency": 2, "scanned": 2, "uniqueProfilesChecked": 1, "uniqueDirectoriesChecked": 2, "scanDurationMs": 4 }, "results": [{ "directory": "/abs/path/", "profileName": "work", "status": "warn", "profileExists": true, "directoryExists": false }] }.gitface rules doctor --stricttreats bothwarnandfailas non-zero exit results for CI gating.gitface rules doctor --concurrency <number>limits concurrent integrity checks (default8; must be a positive integer).gitface rules prune --dry-run --jsonpreviews stale rules that reference missing profiles:{ "status": "dry-run", "dryRun": true, "strict": false, "summary": { "scanned": 3, "prunable": 1, "pruned": 0, "skipped": 0 }, "metrics": { "concurrency": 3, "scanned": 3, "uniqueProfilesChecked": 2, "uniqueDirectoriesChecked": 0, "scanDurationMs": 3 }, "results": [{ "directory": "/abs/path/stale/", "profileName": "old-profile", "profileExists": false, "status": "candidate" }] }.gitface rules prune --dry-run --stricttreats detected stale candidates as non-zero exit results for CI gating (summary.prunable > 0=> exit code1).gitface rules prune --concurrency <number>limits concurrent integrity checks (default8; must be a positive integer).gitface rules prune --dry-run --include-missing-directory --jsonalso previews stale rules whose target directory is missing:{ "status": "dry-run", "dryRun": true, "strict": false, "summary": { "scanned": 3, "prunable": 1, "pruned": 0, "skipped": 0 }, "metrics": { "concurrency": 3, "scanned": 3, "uniqueProfilesChecked": 2, "uniqueDirectoriesChecked": 3, "scanDurationMs": 4 }, "results": [{ "directory": "/abs/path/deleted/", "profileName": "work", "profileExists": true, "directoryExists": false, "staleReason": "missing-directory", "status": "candidate" }] }.gitface rules prune --jsonremoves stale rules and emits:{ "status": "pruned", "dryRun": false, "strict": false, "summary": { "scanned": 3, "prunable": 1, "pruned": 1, "skipped": 0 }, "metrics": { "concurrency": 3, "scanned": 3, "uniqueProfilesChecked": 2, "uniqueDirectoriesChecked": 0, "scanDurationMs": 3 }, "results": [{ "directory": "/abs/path/stale/", "profileName": "old-profile", "profileExists": false, "status": "pruned" }] }.gitface rename <old> <new> --jsonemits machine-readable status:{ "status": "renamed", "oldName": "old", "name": "new", "rulesUpdated": 2, "gitName": "Work User", "email": "[email protected]", "signingKey": null }.gitface rename <old> <new> --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "RENAME_PROFILE_OK", "message": "Profile renamed successfully.", "data": { "result": "renamed", "oldName": "old", "newName": "new", "rulesUpdated": 2, "profile": { "name": "new", "gitName": "Work User", "email": "[email protected]", "signingKey": null } }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface rename <old> <new> --dry-run --jsonpreviews rename metadata without writing:{ "status": "dry-run", "oldName": "old", "newName": "new", "overwrite": false, "rulesToUpdate": 2, "gitName": "Work User", "email": "[email protected]", "signingKey": null }.gitface renameautomatically migrates folder rules that reference the old profile name.gitface clone <src> <tgt> --jsonemits machine-readable status:{ "status": "cloned", "sourceName": "work", "name": "work-copy", "gitName": "Work User", "email": "[email protected]", "signingKey": null }.gitface clone <src> <tgt> --dry-run --jsonpreviews clone metadata without writing:{ "status": "dry-run", "sourceName": "work", "targetName": "work-copy", "overwrite": false, "gitName": "Work User", "email": "[email protected]", "signingKey": null }.gitface export --jsonemits machine-readable summary:{ "status": "exported", "count": 2, "profiles": [{ "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null, "createdAt": "...", "updatedAt": "..." }] }.gitface export ./profiles.json --jsonemits machine-readable file result:{ "status": "exported", "count": 2, "file": "./profiles.json" }.gitface export --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "EXPORT_PROFILES_STDOUT", "message": "Profiles exported to stdout successfully.", "data": { "count": 2, "profiles": [{ "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null, "createdAt": "...", "updatedAt": "..." }] }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface export ./profiles.json --json-envelopeemits unified file result:{ "status": "success", "code": "EXPORT_PROFILES_WRITTEN", "message": "Profiles exported to file successfully.", "data": { "count": 2, "file": "./profiles.json" }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.--json-envelopewrite failures return envelope errors with exit code1:{ "status": "error", "code": "EXPORT_WRITE_FAILED", "message": "...", "data": { "count": 0, "file": "./profiles.json" }, "errors": [{ "code": "EXPORT_WRITE_FAILED", "message": "..." }], "meta": { "schemaVersion": "1.0.0", "durationMs": 1, "traceId": "..." } }.
Example profile file
{
"name": "work",
"gitName": "Company Dev",
"email": "[email protected]",
"signingKey": "ABC123",
"createdAt": "2024-12-01T17:33:14.023Z",
"updatedAt": "2024-12-01T17:33:14.023Z"
}Scopes & Safety
localscope updates.git/configin the current repo (default).globalwrites to your user config (~/.gitconfig), handy when you swap machines.systemforwards to the system config for admin setups.- GitFace automatically wipes
user.signingkeywhen the target profile has no key. gitface use <profile> --jsonemits machine-readable output:{ "status": "applied", "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null, "scope": "local", "hasChanges": true, "changes": [{ "key": "user.name", "action": "set", "before": "Current User", "after": "Work User" }] }.gitface use <profile> --json-envelopeemits machine-readable Result Envelope:{ "status": "success", "code": "USE_PROFILE_APPLIED", "message": "Profile applied to Git config.", "data": { "result": "applied", "scope": "local", "profile": { "name": "work", "gitName": "Work User", "email": "[email protected]", "signingKey": null }, "hasChanges": true, "changes": [{ "key": "user.name", "action": "set", "before": "Current User", "after": "Work User" }] }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface use(without<profile>) opens an interactive selector and applies the selected profile immediately.gitface use --query <text>pre-filters profile candidates by case-insensitive substring matching when<profile>is omitted.gitface use --query <text>auto-applies directly when exactly one profile matches.gitface use --query <text>in non-TTY mode fails fast when multiple profiles match and asks for an explicit profile name.gitface use --query <text> --jsonkeeps machine-readable, non-interactive behavior: unique match auto-applies; ambiguous matches return JSON error:{ "status": "error", "reason": "Multiple profiles matched query \"work\". Re-run with an explicit profile name, for example: \gitface use work-main`." }`.gitface current --scope global --jsoninspects one scope and emits:{ "gitName": "Work User", "email": "[email protected]", "signingKey": "ABC123", "scope": "global" }.gitface doctor --json-envelopeemits unified Result Envelope output:{ "status": "success", "code": "DOCTOR_CHECKS_OK", "message": "Doctor checks passed.", "data": { "strict": false, "hasFatalChecks": false, "summary": { "total": 3, "pass": 3, "warn": 0, "fail": 0 }, "checks": [{ "status": "pass", "message": "..." }] }, "errors": [], "meta": { "schemaVersion": "1.0.0", "durationMs": 2, "traceId": "..." } }.gitface doctor --strict --json-envelopeemits envelope errors and exits1when warnings/failures are present.- Scoped identity reads (
current --scope,useplanning, and doctor global checks) use a singlegit config --listsnapshot per scope by default, with safe fallback behavior when listing fails. gitface use <profile>guards multi-key writes with rollback: if any write fails, GitFace restores the previous scoped identity before exiting.gitface doctor --stricttreats warnings as CI-failing results (exit code1) while keeping default doctor behavior unchanged.gitface use <profile> --dry-runpreviews planned scoped config updates and does not mutate.git/config; dry-run output only lists effective changes.gitface remove <name> --dry-runpreviews profile deletion and does not mutate~/.config/gitface/profiles/*.json.gitface useexits with code1and a guidance message when no profiles are available to select.
Set GITFACE_LOG_LEVEL=debug (or GITFACE_DEBUG=1) to print stack traces and
additional diagnostics. Supported levels: critical, error, warn, info,
debug, trace, silent.
Development
pnpm install # install dependencies
pnpm run lint # Biome checks
pnpm run typecheck # tsc --noEmit
pnpm run test # Vitest (coverage enabled)
pnpm run build # tsc + tsdown bundlepnpm run devruns tsdown in watch mode for local hacking.- CI requires
pnpm run lintto pass before test workflow continues. Usepnpm exec biome check --write .for safe auto-fixes. - Biome configuration is pinned to the local CLI schema and includes both
src/**andtests/**TypeScript files so test code also stays under the same quality gate. - Vitest is configured with serial file execution (
fileParallelism: false) because E2E tests intentionally mutate process globals (cwd/env/argv) and parallel file runs can produce flaky timeouts. make link(ornpm link) exposes the CLI globally for manual testing.- Non-interactive paths (for example
list --json,use <name> --json,new --json, andedit --json) lazy-load Ink UI modules to keep script startup lean. - Release automation lives in
docs/release.md; keep CI green before tagging.
