gitshift 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +41 -0
- package/package.json +2 -1
- package/src/commands/add.js +41 -8
- package/src/server.js +124 -62
- package/src/services/ssh.js +19 -1
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: ${{ matrix.os }} / Node ${{ matrix.node-version }}
|
|
12
|
+
runs-on: ${{ matrix.os }}
|
|
13
|
+
|
|
14
|
+
strategy:
|
|
15
|
+
fail-fast: false
|
|
16
|
+
matrix:
|
|
17
|
+
os:
|
|
18
|
+
- ubuntu-latest
|
|
19
|
+
- macos-latest
|
|
20
|
+
- windows-latest
|
|
21
|
+
node-version:
|
|
22
|
+
- "20"
|
|
23
|
+
- "22"
|
|
24
|
+
|
|
25
|
+
steps:
|
|
26
|
+
- name: Check out repository
|
|
27
|
+
uses: actions/checkout@v4
|
|
28
|
+
|
|
29
|
+
- name: Set up Node.js
|
|
30
|
+
uses: actions/setup-node@v4
|
|
31
|
+
with:
|
|
32
|
+
node-version: ${{ matrix.node-version }}
|
|
33
|
+
cache: npm
|
|
34
|
+
|
|
35
|
+
- name: Install dependencies
|
|
36
|
+
run: npm ci
|
|
37
|
+
|
|
38
|
+
- name: CLI smoke test
|
|
39
|
+
run: |
|
|
40
|
+
node src/server.js --version
|
|
41
|
+
node src/server.js --help
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitshift",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "GitHub Account Switcher CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"bin": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"type": "module",
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@inquirer/prompts": "^8.5.1",
|
|
19
|
+
"axios": "^1.16.1",
|
|
19
20
|
"chalk": "^5.6.2",
|
|
20
21
|
"commander": "^15.0.0",
|
|
21
22
|
"conf": "^15.1.0",
|
package/src/commands/add.js
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
import ora from "ora";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
getProfile,
|
|
9
10
|
saveProfile,
|
|
10
11
|
} from "../services/profile.js";
|
|
11
12
|
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from "../services/ssh.js";
|
|
15
16
|
|
|
16
17
|
import {
|
|
18
|
+
error,
|
|
17
19
|
success,
|
|
18
20
|
} from "../utils/logger.js";
|
|
19
21
|
|
|
@@ -22,6 +24,15 @@ export async function addCommand() {
|
|
|
22
24
|
message: "Profile Name",
|
|
23
25
|
});
|
|
24
26
|
|
|
27
|
+
if (getProfile(name)) {
|
|
28
|
+
error(
|
|
29
|
+
`Profile "${name}" already exists`
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
25
36
|
const username = await input({
|
|
26
37
|
message: "GitHub Username",
|
|
27
38
|
});
|
|
@@ -43,14 +54,36 @@ export async function addCommand() {
|
|
|
43
54
|
"Generating SSH key..."
|
|
44
55
|
).start();
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
try {
|
|
58
|
+
sshKey = await generateSSHKey(
|
|
59
|
+
name,
|
|
60
|
+
email
|
|
61
|
+
);
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
63
|
+
spinner.succeed(
|
|
64
|
+
"SSH key generated"
|
|
65
|
+
);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
spinner.fail(
|
|
68
|
+
"Unable to generate SSH key"
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
error(
|
|
72
|
+
"Could not create SSH key. Ensure OpenSSH is installed and available."
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
err &&
|
|
77
|
+
typeof err === "object" &&
|
|
78
|
+
"shortMessage" in err &&
|
|
79
|
+
err.shortMessage
|
|
80
|
+
) {
|
|
81
|
+
error(String(err.shortMessage));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
54
87
|
}
|
|
55
88
|
|
|
56
89
|
saveProfile({
|
|
@@ -61,6 +94,6 @@ export async function addCommand() {
|
|
|
61
94
|
});
|
|
62
95
|
|
|
63
96
|
success(
|
|
64
|
-
`Profile "${name}"
|
|
97
|
+
`Profile "${name}" saved locally`
|
|
65
98
|
);
|
|
66
99
|
}
|
package/src/server.js
CHANGED
|
@@ -1,69 +1,131 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import chalk from "chalk";
|
|
3
5
|
import { Command } from "commander";
|
|
4
|
-
|
|
6
|
+
import { createRequire } from "node:module";
|
|
5
7
|
import { addCommand } from "./commands/add.js";
|
|
6
|
-
|
|
7
|
-
import { listCommand } from "./commands/list.js";
|
|
8
|
-
|
|
9
8
|
import { currentCommand } from "./commands/current.js";
|
|
10
|
-
|
|
11
|
-
import { useCommand } from "./commands/use.js";
|
|
12
|
-
|
|
13
|
-
import { removeCommand } from "./commands/remove.js";
|
|
14
|
-
|
|
15
9
|
import { doctorCommand } from "./commands/doctor.js";
|
|
10
|
+
import { listCommand } from "./commands/list.js";
|
|
11
|
+
import { removeCommand } from "./commands/remove.js";
|
|
12
|
+
import { useCommand } from "./commands/use.js";
|
|
16
13
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const { name, version } = require("../package.json");
|
|
16
|
+
|
|
17
|
+
function printBanner() {
|
|
18
|
+
const logo = String.raw`
|
|
19
|
+
██████╗ ██╗████████╗███████╗██╗ ██╗██╗███████╗████████╗
|
|
20
|
+
██╔════╝ ██║╚══██╔══╝██╔════╝██║ ██║██║██╔════╝╚══██╔══╝
|
|
21
|
+
██║ ███╗██║ ██║ ███████╗███████║██║█████╗ ██║
|
|
22
|
+
██║ ██║██║ ██║ ╚════██║██╔══██║██║██╔══╝ ██║
|
|
23
|
+
╚██████╔╝██║ ██║ ███████║██║ ██║██║██║ ██║
|
|
24
|
+
╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝╚═╝ ╚═╝
|
|
25
|
+
`;
|
|
26
|
+
console.log(chalk.cyanBright(logo));
|
|
27
|
+
console.log(chalk.bold("GitShift CLI"));
|
|
28
|
+
console.log(
|
|
29
|
+
chalk.dim("Manage and switch GitHub accounts effortlessly\n")
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function compareVersions(currentVersion, latestVersion) {
|
|
34
|
+
const currentParts = currentVersion.split(".").map(Number);
|
|
35
|
+
const latestParts = latestVersion.split(".").map(Number);
|
|
36
|
+
|
|
37
|
+
for (let index = 0; index < 3; index += 1) {
|
|
38
|
+
const currentPart = currentParts[index] || 0;
|
|
39
|
+
const latestPart = latestParts[index] || 0;
|
|
40
|
+
|
|
41
|
+
if (currentPart > latestPart) return 1;
|
|
42
|
+
if (currentPart < latestPart) return -1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function checkForUpdates() {
|
|
49
|
+
try {
|
|
50
|
+
const response = await axios.get(
|
|
51
|
+
`https://registry.npmjs.org/${name}/latest`,
|
|
52
|
+
{
|
|
53
|
+
timeout: 3000,
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const latestVersion = response.data.version;
|
|
58
|
+
|
|
59
|
+
if (latestVersion && compareVersions(version, latestVersion) < 0) {
|
|
60
|
+
console.log(
|
|
61
|
+
`A new version of ${name} is available: ${version} -> ${latestVersion}`,
|
|
62
|
+
);
|
|
63
|
+
console.log(`Run: npm install -g ${name}`);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Ignore version check failures so the CLI still starts offline.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function main() {
|
|
71
|
+
printBanner();
|
|
72
|
+
await checkForUpdates();
|
|
73
|
+
|
|
74
|
+
const program = new Command();
|
|
75
|
+
|
|
76
|
+
program
|
|
77
|
+
.name("gitshift")
|
|
78
|
+
.description(
|
|
79
|
+
"GitHub Account Switcher"
|
|
80
|
+
)
|
|
81
|
+
.version(version);
|
|
82
|
+
|
|
83
|
+
program
|
|
84
|
+
.command("add")
|
|
85
|
+
.description(
|
|
86
|
+
"Create local profile"
|
|
87
|
+
)
|
|
88
|
+
.action(addCommand);
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command("list")
|
|
92
|
+
.description(
|
|
93
|
+
"List profiles"
|
|
94
|
+
)
|
|
95
|
+
.action(listCommand);
|
|
96
|
+
|
|
97
|
+
program
|
|
98
|
+
.command("current")
|
|
99
|
+
.description(
|
|
100
|
+
"Show active profile"
|
|
101
|
+
)
|
|
102
|
+
.action(currentCommand);
|
|
103
|
+
|
|
104
|
+
program
|
|
105
|
+
.command("use <profile>")
|
|
106
|
+
.description(
|
|
107
|
+
"Switch profile and update git config"
|
|
108
|
+
)
|
|
109
|
+
.action(useCommand);
|
|
110
|
+
|
|
111
|
+
program
|
|
112
|
+
.command("remove <profile>")
|
|
113
|
+
.description(
|
|
114
|
+
"Delete profile"
|
|
115
|
+
)
|
|
116
|
+
.action(removeCommand);
|
|
117
|
+
|
|
118
|
+
program
|
|
119
|
+
.command("doctor")
|
|
120
|
+
.description(
|
|
121
|
+
"System health check"
|
|
122
|
+
)
|
|
123
|
+
.action(doctorCommand);
|
|
124
|
+
|
|
125
|
+
await program.parseAsync(process.argv);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main().catch((error) => {
|
|
129
|
+
console.error(error);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
});
|
package/src/services/ssh.js
CHANGED
|
@@ -3,14 +3,28 @@ import fs from "fs-extra";
|
|
|
3
3
|
import os from "os";
|
|
4
4
|
import path from "path";
|
|
5
5
|
|
|
6
|
+
function toSafeKeyName(profileName) {
|
|
7
|
+
const normalized = profileName
|
|
8
|
+
.trim()
|
|
9
|
+
.replace(/[^a-zA-Z0-9._-]+/g, "-")
|
|
10
|
+
.replace(/-+/g, "-")
|
|
11
|
+
.replace(/^-|-$/g, "");
|
|
12
|
+
|
|
13
|
+
return normalized || "profile";
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
export async function generateSSHKey(
|
|
7
17
|
profileName,
|
|
8
18
|
email
|
|
9
19
|
) {
|
|
20
|
+
const safeProfileName = toSafeKeyName(
|
|
21
|
+
profileName
|
|
22
|
+
);
|
|
23
|
+
|
|
10
24
|
const keyPath = path.join(
|
|
11
25
|
os.homedir(),
|
|
12
26
|
".ssh",
|
|
13
|
-
`ghswitch-${
|
|
27
|
+
`ghswitch-${safeProfileName}`
|
|
14
28
|
);
|
|
15
29
|
|
|
16
30
|
const exists = await fs.pathExists(
|
|
@@ -21,6 +35,10 @@ export async function generateSSHKey(
|
|
|
21
35
|
return keyPath;
|
|
22
36
|
}
|
|
23
37
|
|
|
38
|
+
await fs.ensureDir(
|
|
39
|
+
path.dirname(keyPath)
|
|
40
|
+
);
|
|
41
|
+
|
|
24
42
|
await execa("ssh-keygen", [
|
|
25
43
|
"-t",
|
|
26
44
|
"ed25519",
|