gitshift 1.0.9 → 2.1.0
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/publish.yml +1 -1
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/commands/auto.js +51 -0
- package/src/commands/link.js +146 -0
- package/src/commands/links.js +20 -0
- package/src/commands/unlink.js +17 -0
- package/src/server.js +35 -1
- package/src/services/profile.js +35 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# GitShift CLI
|
|
2
2
|
|
|
3
|
-
GitShift CLI helps you create, manage, and switch between GitHub identity profiles from the terminal. It stores profiles locally, updates your global Git config,
|
|
3
|
+
GitShift CLI helps you create, manage, and switch between GitHub identity profiles from the terminal. It stores profiles locally, updates your global Git config, can generate SSH keys for each profile when needed, and can import existing SSH keys from your `~/.ssh` folder.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -37,6 +37,7 @@ gitshift --help
|
|
|
37
37
|
- `gitshift current` - Display the active profile.
|
|
38
38
|
- `gitshift use <profile>` - Switch to a saved profile and update global Git user name and email.
|
|
39
39
|
- `gitshift remove <profile>` - Delete a saved profile.
|
|
40
|
+
- `gitshift scan` - Scan your `~/.ssh` folder and import existing SSH keys into new profiles.
|
|
40
41
|
- `gitshift doctor` - Check whether Git, SSH, and GitHub CLI are installed.
|
|
41
42
|
|
|
42
43
|
## Example Workflow
|
|
@@ -44,6 +45,7 @@ gitshift --help
|
|
|
44
45
|
```bash
|
|
45
46
|
gitshift add
|
|
46
47
|
gitshift list
|
|
48
|
+
gitshift scan
|
|
47
49
|
gitshift use personal
|
|
48
50
|
gitshift current
|
|
49
51
|
gitshift doctor
|
|
@@ -51,6 +53,8 @@ gitshift doctor
|
|
|
51
53
|
|
|
52
54
|
When you create a profile and choose SSH generation, GitShift creates a key under your home directory in `.ssh` using the pattern `gitshift-<profile-name>`.
|
|
53
55
|
|
|
56
|
+
If you already have SSH keys on your machine, `gitshift scan` will list the available keys, let you pick one, and save it as a new imported profile.
|
|
57
|
+
|
|
54
58
|
## How It Works
|
|
55
59
|
|
|
56
60
|
Profiles are saved locally on your machine using the app's configuration store. Switching profiles updates your global Git identity with:
|
package/package.json
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
import {
|
|
3
|
+
getFolderMappings,
|
|
4
|
+
getProfile,
|
|
5
|
+
setCurrentProfile,
|
|
6
|
+
} from "../services/profile.js";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
setGitUser,
|
|
10
|
+
} from "../services/git.js";
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
success,
|
|
14
|
+
} from "../utils/logger.js";
|
|
15
|
+
|
|
16
|
+
export async function autoCommand() {
|
|
17
|
+
const cwd = process.cwd();
|
|
18
|
+
|
|
19
|
+
const mappings = getFolderMappings();
|
|
20
|
+
|
|
21
|
+
const matched =
|
|
22
|
+
mappings
|
|
23
|
+
.sort(
|
|
24
|
+
(a, b) =>
|
|
25
|
+
b.path.length -
|
|
26
|
+
a.path.length
|
|
27
|
+
)
|
|
28
|
+
.find((mapping) =>
|
|
29
|
+
cwd.startsWith(
|
|
30
|
+
mapping.path
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (!matched) {
|
|
35
|
+
console.log("\nNo matching profile\n");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const profile =
|
|
40
|
+
getProfile(matched.profile);
|
|
41
|
+
|
|
42
|
+
if (!profile) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await setGitUser(profile.username, profile.email);
|
|
47
|
+
|
|
48
|
+
setCurrentProfile(profile.name);
|
|
49
|
+
|
|
50
|
+
success(`Switched to ${profile.name}`);
|
|
51
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
confirm,
|
|
6
|
+
input,
|
|
7
|
+
select,
|
|
8
|
+
} from "@inquirer/prompts";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
addFolderMapping,
|
|
12
|
+
getProfiles,
|
|
13
|
+
saveProfile,
|
|
14
|
+
} from "../services/profile.js";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
generateSSHKey,
|
|
18
|
+
} from "../services/ssh.js";
|
|
19
|
+
|
|
20
|
+
import ora from "ora";
|
|
21
|
+
import {
|
|
22
|
+
error,
|
|
23
|
+
success,
|
|
24
|
+
} from "../utils/logger.js";
|
|
25
|
+
|
|
26
|
+
export async function linkCommand(
|
|
27
|
+
folder
|
|
28
|
+
) {
|
|
29
|
+
const fullPath = path.resolve(folder);
|
|
30
|
+
|
|
31
|
+
const exists = await fs.pathExists(fullPath);
|
|
32
|
+
|
|
33
|
+
if (!exists) {
|
|
34
|
+
error("Folder does not exist");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let profiles = getProfiles();
|
|
39
|
+
|
|
40
|
+
let selectedProfile;
|
|
41
|
+
|
|
42
|
+
if (profiles.length === 0) {
|
|
43
|
+
console.log("\nNo profiles found.\n");
|
|
44
|
+
|
|
45
|
+
selectedProfile = await createProfile();
|
|
46
|
+
} else {
|
|
47
|
+
const choice =
|
|
48
|
+
await select({
|
|
49
|
+
message: "Select Profile",
|
|
50
|
+
choices: [
|
|
51
|
+
...profiles.map(
|
|
52
|
+
(
|
|
53
|
+
profile
|
|
54
|
+
) => ({
|
|
55
|
+
name: `${profile.name} (${profile.username})`,
|
|
56
|
+
value:
|
|
57
|
+
profile.name,
|
|
58
|
+
})
|
|
59
|
+
),
|
|
60
|
+
{
|
|
61
|
+
name: "+ Create New Profile",
|
|
62
|
+
value: "__create__",
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (choice === "__create__") {
|
|
68
|
+
selectedProfile = await createProfile();
|
|
69
|
+
} else {
|
|
70
|
+
selectedProfile = choice;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
addFolderMapping(selectedProfile, fullPath);
|
|
76
|
+
|
|
77
|
+
success(`Linked ${fullPath}`);
|
|
78
|
+
|
|
79
|
+
success(`Profile: ${selectedProfile}`);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
error(err.message);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function createProfile() {
|
|
86
|
+
const name =
|
|
87
|
+
await input({
|
|
88
|
+
message: "Profile Name",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const username =
|
|
92
|
+
await input({
|
|
93
|
+
message: "GitHub Username",
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const email =
|
|
97
|
+
await input({
|
|
98
|
+
message: "Email",
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const shouldCreateSSH =
|
|
102
|
+
await confirm({
|
|
103
|
+
message: "Generate SSH Key?",
|
|
104
|
+
default: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
let sshKey = null;
|
|
108
|
+
|
|
109
|
+
if (shouldCreateSSH) {
|
|
110
|
+
const spinner = ora("Generating SSH key...").start();
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
sshKey = await generateSSHKey(name, email);
|
|
114
|
+
|
|
115
|
+
spinner.succeed("SSH key generated");
|
|
116
|
+
} catch (err) {
|
|
117
|
+
spinner.fail("Unable to generate SSH key");
|
|
118
|
+
|
|
119
|
+
error("Could not create SSH key. Ensure OpenSSH is installed and available.");
|
|
120
|
+
|
|
121
|
+
if (
|
|
122
|
+
err &&
|
|
123
|
+
typeof err === "object" &&
|
|
124
|
+
"shortMessage" in err &&
|
|
125
|
+
err.shortMessage
|
|
126
|
+
) {
|
|
127
|
+
error(String(err.shortMessage));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
saveProfile({
|
|
136
|
+
name,
|
|
137
|
+
username,
|
|
138
|
+
email,
|
|
139
|
+
sshKey,
|
|
140
|
+
source: "manual",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
success(`Profile "${name}" created`);
|
|
144
|
+
|
|
145
|
+
return name;
|
|
146
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getFolderMappings,
|
|
3
|
+
} from "../services/profile.js";
|
|
4
|
+
|
|
5
|
+
export async function linksCommand() {
|
|
6
|
+
const mappings = getFolderMappings();
|
|
7
|
+
|
|
8
|
+
if (!mappings.length) {
|
|
9
|
+
console.log("\nNo folder mappings\n");
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
console.log();
|
|
14
|
+
|
|
15
|
+
mappings.forEach((mapping) => {
|
|
16
|
+
console.log(`${mapping.profile} → ${mapping.path}`);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
console.log();
|
|
20
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
removeFolderMapping,
|
|
5
|
+
} from "../services/profile.js";
|
|
6
|
+
|
|
7
|
+
import { success } from "../utils/logger.js";
|
|
8
|
+
|
|
9
|
+
export async function unlinkCommand(
|
|
10
|
+
folder
|
|
11
|
+
) {
|
|
12
|
+
const fullPath = path.resolve(folder);
|
|
13
|
+
|
|
14
|
+
removeFolderMapping(fullPath);
|
|
15
|
+
|
|
16
|
+
success(`Removed ${fullPath}`);
|
|
17
|
+
}
|
package/src/server.js
CHANGED
|
@@ -5,11 +5,15 @@ import chalk from "chalk";
|
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { addCommand } from "./commands/add.js";
|
|
8
|
+
import { autoCommand } from "./commands/auto.js";
|
|
8
9
|
import { currentCommand } from "./commands/current.js";
|
|
9
10
|
import { doctorCommand } from "./commands/doctor.js";
|
|
11
|
+
import { linkCommand } from "./commands/link.js";
|
|
12
|
+
import { linksCommand } from "./commands/links.js";
|
|
10
13
|
import { listCommand } from "./commands/list.js";
|
|
11
14
|
import { removeCommand } from "./commands/remove.js";
|
|
12
15
|
import { scanCommand } from "./commands/scan.js";
|
|
16
|
+
import { unlinkCommand } from "./commands/unlink.js";
|
|
13
17
|
import { useCommand } from "./commands/use.js";
|
|
14
18
|
|
|
15
19
|
const require = createRequire(import.meta.url);
|
|
@@ -113,7 +117,7 @@ async function main() {
|
|
|
113
117
|
program
|
|
114
118
|
.command("remove <profile>")
|
|
115
119
|
.description(
|
|
116
|
-
"
|
|
120
|
+
"Remove profile"
|
|
117
121
|
)
|
|
118
122
|
.action(removeCommand);
|
|
119
123
|
|
|
@@ -124,6 +128,36 @@ async function main() {
|
|
|
124
128
|
)
|
|
125
129
|
.action(scanCommand);
|
|
126
130
|
|
|
131
|
+
program
|
|
132
|
+
.command(
|
|
133
|
+
"link <folder>"
|
|
134
|
+
)
|
|
135
|
+
.description(
|
|
136
|
+
"Link folder to profile"
|
|
137
|
+
)
|
|
138
|
+
.action(linkCommand);
|
|
139
|
+
|
|
140
|
+
program
|
|
141
|
+
.command("unlink <folder>")
|
|
142
|
+
.description(
|
|
143
|
+
"Remove folder mapping"
|
|
144
|
+
)
|
|
145
|
+
.action(unlinkCommand);
|
|
146
|
+
|
|
147
|
+
program
|
|
148
|
+
.command("links")
|
|
149
|
+
.description(
|
|
150
|
+
"List folder mappings"
|
|
151
|
+
)
|
|
152
|
+
.action(linksCommand);
|
|
153
|
+
|
|
154
|
+
program
|
|
155
|
+
.command("auto")
|
|
156
|
+
.description(
|
|
157
|
+
"Auto switch profile"
|
|
158
|
+
)
|
|
159
|
+
.action(autoCommand);
|
|
160
|
+
|
|
127
161
|
program
|
|
128
162
|
.command("doctor")
|
|
129
163
|
.description(
|
package/src/services/profile.js
CHANGED
|
@@ -57,4 +57,39 @@ export function setCurrentProfile(
|
|
|
57
57
|
|
|
58
58
|
export function getCurrentProfile() {
|
|
59
59
|
return config.get("current", null);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function getFolderMappings() {
|
|
63
|
+
return config.get("folderMappings", []);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function addFolderMapping(
|
|
67
|
+
profile,
|
|
68
|
+
folderPath
|
|
69
|
+
) {
|
|
70
|
+
const mappings = getFolderMappings();
|
|
71
|
+
|
|
72
|
+
const exists = mappings.find((item) => item.path === folderPath);
|
|
73
|
+
|
|
74
|
+
if (exists) {
|
|
75
|
+
throw new Error("Folder already mapped");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
mappings.push({ profile, path: folderPath, });
|
|
79
|
+
|
|
80
|
+
config.set("folderMappings", mappings);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function removeFolderMapping(
|
|
84
|
+
folderPath
|
|
85
|
+
) {
|
|
86
|
+
const mappings = getFolderMappings();
|
|
87
|
+
|
|
88
|
+
config.set(
|
|
89
|
+
"folderMappings",
|
|
90
|
+
mappings.filter(
|
|
91
|
+
(item) =>
|
|
92
|
+
item.path !== folderPath
|
|
93
|
+
)
|
|
94
|
+
);
|
|
60
95
|
}
|