Skip to content

Commit b906947

Browse files
guplemclaude
andcommitted
Add github-plus as a CLI alias for terminal launch
Register `github-plus` alongside `github` so users can launch the app from the terminal with either command. On Windows, this creates additional trampoline scripts in the bin directory. On macOS, it creates a second symlink in /usr/local/bin. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b135b0a commit b906947

3 files changed

Lines changed: 48 additions & 28 deletions

File tree

app/src/cli/main.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ const usage = (exitCode = 1): never => {
4545
' github-desktop-plus-cli open [path] Open the provided path\n' +
4646
' github-desktop-plus-cli clone [-b branch] <url> Clone the repository by url or name/owner\n' +
4747
' (ex torvalds/linux), optionally checking\n' +
48-
' out the branch\n'
48+
' out the branch\n' +
49+
'\n' +
50+
'Alias: github-plus\n'
4951
)
5052
process.exit(exitCode)
5153
}

app/src/main-process/squirrel-updater.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,10 @@ async function handleUpdated(): Promise<void> {
4949
export async function installWindowsCLI(): Promise<void> {
5050
const binPath = getBinPath()
5151
await mkdir(binPath, { recursive: true })
52-
await writeBatchScriptCLITrampoline(binPath)
53-
await writeShellScriptCLITrampoline(binPath)
52+
await writeBatchScriptCLITrampoline(binPath, 'github-desktop-plus-cli')
53+
await writeBatchScriptCLITrampoline(binPath, 'github-plus')
54+
await writeShellScriptCLITrampoline(binPath, 'github-desktop-plus-cli')
55+
await writeShellScriptCLITrampoline(binPath, 'github-plus')
5456
try {
5557
const paths = getPathSegments()
5658
if (paths.indexOf(binPath) < 0) {
@@ -96,19 +98,25 @@ function resolveVersionedPath(binPath: string, relativePath: string): string {
9698
* rewrite the trampoline to point to the new, version-specific path. Bingo
9799
* bango Bob's your uncle.
98100
*/
99-
function writeBatchScriptCLITrampoline(binPath: string): Promise<void> {
101+
function writeBatchScriptCLITrampoline(
102+
binPath: string,
103+
name: string
104+
): Promise<void> {
100105
const versionedPath = resolveVersionedPath(
101106
binPath,
102107
'resources/app/static/github-desktop-plus-cli.bat'
103108
)
104109

105110
const trampoline = `@echo off\n"%~dp0\\${versionedPath}" %*`
106-
const trampolinePath = Path.join(binPath, 'github-desktop-plus-cli.bat')
111+
const trampolinePath = Path.join(binPath, `${name}.bat`)
107112

108113
return writeFile(trampolinePath, trampoline)
109114
}
110115

111-
function writeShellScriptCLITrampoline(binPath: string): Promise<void> {
116+
function writeShellScriptCLITrampoline(
117+
binPath: string,
118+
name: string
119+
): Promise<void> {
112120
// The path we get from `resolveVersionedPath` is a Win32 relative
113121
// path (something like `..\app-2.5.0\resources\app\static\github.sh`).
114122
// We need to make sure it's a POSIX path in order for WSL to be able
@@ -121,7 +129,7 @@ function writeShellScriptCLITrampoline(binPath: string): Promise<void> {
121129
const trampoline = `#!/usr/bin/env bash
122130
DIR="$( cd "$( dirname "\$\{BASH_SOURCE[0]\}" )" && pwd )"
123131
sh "$DIR/${versionedPath}" "$@"`
124-
const trampolinePath = Path.join(binPath, 'github-desktop-plus-cli')
132+
const trampolinePath = Path.join(binPath, name)
125133

126134
return writeFile(trampolinePath, trampoline, { encoding: 'utf8', mode: 755 })
127135
}

app/src/ui/lib/install-cli.ts

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ import { mkdir, readlink, symlink, unlink } from 'fs/promises'
66
/** The path for the installed command line tool. */
77
export const InstalledCLIPath = '/usr/local/bin/github-desktop-plus-cli'
88

9+
/** Shorter alias for the CLI. */
10+
const InstalledCLIAliasPath = '/usr/local/bin/github-plus'
11+
912
/** The path to the packaged CLI. */
1013
const PackagedPath = Path.resolve(
1114
__dirname,
@@ -15,38 +18,45 @@ const PackagedPath = Path.resolve(
1518

1619
/** Install the command line tool on macOS. */
1720
export async function installCLI(): Promise<void> {
18-
const installedPath = await getResolvedInstallPath()
19-
if (installedPath === PackagedPath) {
21+
await installCLIAt(InstalledCLIPath)
22+
await installCLIAt(InstalledCLIAliasPath)
23+
}
24+
25+
async function installCLIAt(installPath: string): Promise<void> {
26+
const resolvedPath = await getResolvedInstallPath(installPath)
27+
if (resolvedPath === PackagedPath) {
2028
return
2129
}
2230

2331
try {
24-
await symlinkCLI(false)
32+
await symlinkCLI(installPath, false)
2533
} catch (e) {
2634
// If we error without running as an admin, try again as an admin.
27-
await symlinkCLI(true)
35+
await symlinkCLI(installPath, true)
2836
}
2937
}
3038

31-
async function getResolvedInstallPath(): Promise<string | null> {
39+
async function getResolvedInstallPath(
40+
installPath: string
41+
): Promise<string | null> {
3242
try {
33-
return await readlink(InstalledCLIPath)
43+
return await readlink(installPath)
3444
} catch {
3545
return null
3646
}
3747
}
3848

39-
function removeExistingSymlink(asAdmin: boolean) {
49+
function removeExistingSymlink(installPath: string, asAdmin: boolean) {
4050
if (!asAdmin) {
41-
return unlink(InstalledCLIPath)
51+
return unlink(installPath)
4252
}
4353

4454
return new Promise<void>((resolve, reject) => {
45-
fsAdmin.unlink(InstalledCLIPath, error => {
55+
fsAdmin.unlink(installPath, error => {
4656
if (error !== null) {
4757
reject(
4858
new Error(
49-
`Failed to remove file at ${InstalledCLIPath}. Authorization of GitHub Desktop Helper is required.`
59+
`Failed to remove file at ${installPath}. Authorization of GitHub Desktop Helper is required.`
5060
)
5161
)
5262
return
@@ -57,8 +67,8 @@ function removeExistingSymlink(asAdmin: boolean) {
5767
})
5868
}
5969

60-
function createDirectories(asAdmin: boolean) {
61-
const path = Path.dirname(InstalledCLIPath)
70+
function createDirectories(installPath: string, asAdmin: boolean) {
71+
const path = Path.dirname(installPath)
6272

6373
if (!asAdmin) {
6474
return mkdir(path, { recursive: true })
@@ -69,7 +79,7 @@ function createDirectories(asAdmin: boolean) {
6979
if (error !== null) {
7080
reject(
7181
new Error(
72-
`Failed to create intermediate directories to ${InstalledCLIPath}`
82+
`Failed to create intermediate directories to ${installPath}`
7383
)
7484
)
7585
return
@@ -80,16 +90,16 @@ function createDirectories(asAdmin: boolean) {
8090
})
8191
}
8292

83-
function createNewSymlink(asAdmin: boolean) {
93+
function createNewSymlink(installPath: string, asAdmin: boolean) {
8494
if (!asAdmin) {
85-
return symlink(PackagedPath, InstalledCLIPath)
95+
return symlink(PackagedPath, installPath)
8696
}
8797

8898
return new Promise<void>((resolve, reject) => {
89-
fsAdmin.symlink(PackagedPath, InstalledCLIPath, error => {
99+
fsAdmin.symlink(PackagedPath, installPath, error => {
90100
if (error !== null) {
91101
reject(
92-
new Error(`Failed to symlink ${PackagedPath} to ${InstalledCLIPath}`)
102+
new Error(`Failed to symlink ${PackagedPath} to ${installPath}`)
93103
)
94104
return
95105
}
@@ -99,8 +109,8 @@ function createNewSymlink(asAdmin: boolean) {
99109
})
100110
}
101111

102-
async function symlinkCLI(asAdmin: boolean): Promise<void> {
103-
await removeExistingSymlink(asAdmin)
104-
await createDirectories(asAdmin)
105-
await createNewSymlink(asAdmin)
112+
async function symlinkCLI(installPath: string, asAdmin: boolean): Promise<void> {
113+
await removeExistingSymlink(installPath, asAdmin)
114+
await createDirectories(installPath, asAdmin)
115+
await createNewSymlink(installPath, asAdmin)
106116
}

0 commit comments

Comments
 (0)