Skip to content

Commit 0ff6812

Browse files
committed
feat: added interactive login for github
1 parent bd8f715 commit 0ff6812

5 files changed

Lines changed: 47 additions & 19 deletions

File tree

CLAUDE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,19 @@ const result = await $.spawnSafe('my-tool --version')
258258
await $.spawn('my-tool configure', { interactive: true })
259259
```
260260

261+
**`interactive: true` vs `stdin: true`** — these are distinct options:
262+
263+
- `interactive: true` — passes `-i` to the shell so it sources the user's RC file (`.zshrc`, `.bashrc`). Use this when the command needs PATH entries or shell aliases from the RC. It does **not** allow the user to type input.
264+
- `stdin: true` — connects the user's terminal stdin directly to the spawned process. Use this when the command requires real user input (e.g. browser-based OAuth prompts, interactive wizards, password entry). Without this flag, interactive prompts will hang.
265+
266+
```typescript
267+
// Command needs PATH from shell RC but no user input
268+
await $.spawn('my-tool configure', { interactive: true })
269+
270+
// Command requires the user to interact with it directly (e.g. browser login flow)
271+
await $.spawn('gh auth login --web', { interactive: true, stdin: true })
272+
```
273+
261274
**Never use `sudo` inside `$.spawn` or `$.spawnSafe`.** Use `{ requiresRoot: true }` in the options instead. The framework handles privilege escalation through the parent process.
262275

263276
```typescript

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "default",
3-
"version": "1.11.0-beta.2",
3+
"version": "1.11.0-beta.8",
44
"description": "Default plugin for Codify - provides 50+ declarative resources for managing development tools and system configuration across macOS and Linux",
55
"main": "dist/index.js",
66
"scripts": {

src/resources/github-cli/examples.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import { ExampleConfig } from '@codifycli/plugin-core';
22

33
export const exampleGithubCliBasic: ExampleConfig = {
4-
title: 'Install GitHub CLI with SSH configuration',
5-
description: 'Install gh and configure it to use SSH for git operations and vim as the default editor.',
4+
title: 'Install GitHub CLI with interactive login',
5+
description: 'Install gh and log in via browser — no token needed. Use interactiveLogin: true as a shortcut instead of a separate github-cli-auth block.',
66
configs: [
77
{
88
type: 'github-cli',
99
gitProtocol: 'ssh',
10-
editor: 'vim',
10+
interactiveLogin: true,
1111
},
1212
],
1313
};
1414

1515
export const exampleGithubCliFull: ExampleConfig = {
16-
title: 'Full GitHub CLI setup with authentication',
17-
description: 'Install gh, authenticate with a personal access token, and configure SSH as the default git protocol.',
16+
title: 'GitHub CLI with token authentication',
17+
description: 'Install gh, configure SSH as the default git protocol, and authenticate with a personal access token.',
1818
configs: [
1919
{
2020
type: 'github-cli',

src/resources/github-cli/github-cli-auth.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export const schema = z
1919
.object({
2020
token: z
2121
.string()
22-
.describe('GitHub personal access token (classic or fine-grained) used for authentication'),
22+
.optional()
23+
.describe('GitHub personal access token (classic or fine-grained) used for authentication. Omit to use interactive browser-based login'),
2324
hostname: z
2425
.string()
2526
.optional()
@@ -32,6 +33,7 @@ export type GithubCliAuthConfig = z.infer<typeof schema>;
3233

3334
const defaultConfig: Partial<GithubCliAuthConfig> = {
3435
hostname: 'github.com',
36+
token: undefined,
3537
};
3638

3739
export class GithubCliAuthResource extends Resource<GithubCliAuthConfig> {
@@ -54,7 +56,6 @@ export class GithubCliAuthResource extends Resource<GithubCliAuthConfig> {
5456
importAndDestroy: {
5557
requiredParameters: [],
5658
defaultRefreshValues: {
57-
token: '',
5859
hostname: 'github.com',
5960
},
6061
},
@@ -82,11 +83,11 @@ export class GithubCliAuthResource extends Resource<GithubCliAuthConfig> {
8283
const $ = getPty();
8384
const hostname = params.hostname ?? 'github.com';
8485

85-
const { status } = await $.spawnSafe(`gh auth status --hostname ${hostname}`);
86+
const { status } = await $.spawnSafe(`gh auth status --hostname "${hostname}"`);
8687
if (status === SpawnStatus.ERROR) return null;
8788

8889
const { data: tokenData, status: tokenStatus } = await $.spawnSafe(
89-
`gh auth token --hostname ${hostname}`
90+
`gh auth token --hostname "${hostname}"`
9091
);
9192
if (tokenStatus === SpawnStatus.ERROR) return { hostname };
9293

@@ -98,39 +99,44 @@ export class GithubCliAuthResource extends Resource<GithubCliAuthConfig> {
9899

99100
async create(plan: CreatePlan<GithubCliAuthConfig>): Promise<void> {
100101
const { token, hostname = 'github.com' } = plan.desiredConfig;
101-
await this.loginWithToken(token, hostname);
102+
await this.login(token, hostname);
102103
}
103104

104105
async modify(
105106
_pc: unknown,
106107
plan: ModifyPlan<GithubCliAuthConfig>
107108
): Promise<void> {
108109
const { token, hostname = 'github.com' } = plan.desiredConfig;
109-
await this.loginWithToken(token, hostname);
110+
await this.login(token, hostname);
110111
}
111112

112113
async destroy(plan: DestroyPlan<GithubCliAuthConfig>): Promise<void> {
113114
const $ = getPty();
114115
const hostname = plan.currentConfig.hostname ?? 'github.com';
115116

116-
const { data: statusData } = await $.spawnSafe(`gh auth status --hostname ${hostname}`);
117+
const { data: statusData } = await $.spawnSafe(`gh auth status --hostname "${hostname}"`);
117118
const userMatch = statusData.match(/Logged in to \S+ account (\S+)/);
118119
const username = userMatch?.[1];
119120

120121
if (username) {
121-
await $.spawnSafe(`gh auth logout --hostname ${hostname} --user ${username}`);
122+
await $.spawnSafe(`gh auth logout --hostname "${hostname}" --user "${username}"`);
122123
} else {
123-
await $.spawnSafe(`gh auth logout --hostname ${hostname}`);
124+
await $.spawnSafe(`gh auth logout --hostname "${hostname}"`);
124125
}
125126
}
126127

127-
private async loginWithToken(token: string, hostname: string): Promise<void> {
128+
private async login(token: string | undefined, hostname: string): Promise<void> {
128129
const $ = getPty();
129-
const tmpFile = path.join(os.tmpdir(), `.gh-token-${Date.now()}`);
130130

131+
if (!token) {
132+
await $.spawn(`gh auth login --hostname "${hostname}" --web`, { interactive: true, stdin: true });
133+
return;
134+
}
135+
136+
const tmpFile = path.join(os.tmpdir(), `.gh-token-${Date.now()}`);
131137
await fs.writeFile(tmpFile, token.trim(), { mode: 0o600 });
132138
try {
133-
await $.spawn(`gh auth login --with-token --hostname ${hostname} < "${tmpFile}"`, {
139+
await $.spawn(`gh auth login --with-token --hostname "${hostname}" < "${tmpFile}"`, {
134140
interactive: true,
135141
});
136142
} finally {

src/resources/github-cli/github-cli.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,17 @@ export const schema = z
3636
.string()
3737
.optional()
3838
.describe('Default web browser for opening URLs'),
39+
interactiveLogin: z
40+
.boolean()
41+
.optional()
42+
.describe('If true, runs gh auth login --web after installation for browser-based authentication. Use this as a shortcut instead of declaring a separate github-cli-auth block'),
3943
})
4044
.meta({ $comment: 'https://cli.github.com/manual/' })
4145
.describe('GitHub CLI (gh) — installs gh and manages global configuration');
4246

4347
export type GithubCliConfig = z.infer<typeof schema>;
4448

45-
const CONFIG_KEY_MAP: Record<keyof GithubCliConfig, string> = {
49+
const CONFIG_KEY_MAP: Partial<Record<keyof GithubCliConfig, string>> = {
4650
gitProtocol: 'git_protocol',
4751
editor: 'editor',
4852
prompt: 'prompt',
@@ -72,6 +76,7 @@ export class GithubCliResource extends Resource<GithubCliConfig> {
7276
prompt: { canModify: true },
7377
pager: { canModify: true },
7478
browser: { canModify: true },
79+
interactiveLogin: { type: 'boolean', setting: true },
7580
},
7681
};
7782
}
@@ -116,8 +121,12 @@ export class GithubCliResource extends Resource<GithubCliConfig> {
116121
}
117122

118123
async create(plan: CreatePlan<GithubCliConfig>): Promise<void> {
124+
const $ = getPty();
119125
await Utils.installViaPkgMgr('gh');
120126
await this.applyConfig(plan.desiredConfig);
127+
if (plan.desiredConfig.interactiveLogin) {
128+
await $.spawn('gh auth login --web', { interactive: true, stdin: true });
129+
}
121130
}
122131

123132
async modify(pc: ParameterChange<GithubCliConfig>, _plan: ModifyPlan<GithubCliConfig>): Promise<void> {

0 commit comments

Comments
 (0)