gitshift 2.1.0 → 2.2.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/README.md +73 -0
- package/package.json +1 -1
- package/src/commands/add.js +23 -22
- package/src/commands/auto.js +14 -26
- package/src/commands/backup.js +29 -0
- package/src/commands/current.js +7 -27
- package/src/commands/doctor.js +6 -28
- package/src/commands/link.js +43 -53
- package/src/commands/links.js +1 -3
- package/src/commands/remove.js +6 -24
- package/src/commands/restore.js +33 -0
- package/src/commands/scan.js +13 -24
- package/src/commands/unlink.js +2 -10
- package/src/commands/use.js +11 -41
- package/src/server.js +19 -3
- package/src/services/git.js +1 -4
- package/src/services/profile.js +25 -24
- package/src/services/scan.js +1 -3
- package/src/services/ssh.js +7 -23
- package/src/utils/logger.js +3 -6
package/README.md
CHANGED
|
@@ -39,6 +39,47 @@ gitshift --help
|
|
|
39
39
|
- `gitshift remove <profile>` - Delete a saved profile.
|
|
40
40
|
- `gitshift scan` - Scan your `~/.ssh` folder and import existing SSH keys into new profiles.
|
|
41
41
|
- `gitshift doctor` - Check whether Git, SSH, and GitHub CLI are installed.
|
|
42
|
+
- `gitshift backup [file]` - Export profiles, folder mappings, and current profile to a JSON backup file (default: `gitshift-backup.json`).
|
|
43
|
+
- `gitshift restore <file>` - Restore profiles and mappings from a previously created backup JSON file (prompts to confirm overwrite).
|
|
44
|
+
|
|
45
|
+
- `gitshift link <folder>` - Link a local folder to a profile (prompts to select or create a profile).
|
|
46
|
+
- `gitshift unlink <folder>` - Remove an existing folder mapping.
|
|
47
|
+
- `gitshift links` - List folder → profile mappings.
|
|
48
|
+
- `gitshift auto` - Auto-switch profile based on the current working directory and configured folder mappings.
|
|
49
|
+
|
|
50
|
+
### Add Command
|
|
51
|
+
|
|
52
|
+
- **Interactive prompts**: `Profile Name`, `GitHub Username`, `Email` (all required).
|
|
53
|
+
- **SSH key generation**: prompts `Generate SSH key automatically?` (default: **yes**). If accepted, an SSH key is generated and saved under `~/.ssh` with the pattern `gitshift-<profile-name>`.
|
|
54
|
+
- **Validation**: profile names must be unique; empty values for name, username, or email are rejected.
|
|
55
|
+
- **Cancelation**: pressing Ctrl+C during prompts exits gracefully and cancels creation.
|
|
56
|
+
|
|
57
|
+
### Folder mappings
|
|
58
|
+
|
|
59
|
+
- `gitshift link <folder>`: associates a local folder path with a profile. If no profiles exist, you'll be prompted to create one; otherwise you can pick an existing profile or create a new one. Linking stores an absolute path mapping so GitShift can detect and switch profiles when you `cd` into that folder.
|
|
60
|
+
- `gitshift unlink <folder>`: removes the mapping for the given folder path.
|
|
61
|
+
- `gitshift links`: prints all configured folder mappings in the form `profile → /absolute/path`.
|
|
62
|
+
|
|
63
|
+
Example linking a folder
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
$ gitshift link ~/projects/my-repo
|
|
67
|
+
Select Profile: personal
|
|
68
|
+
Linked /Users/akashs/projects/my-repo
|
|
69
|
+
Profile: personal
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Auto switching
|
|
73
|
+
|
|
74
|
+
- `gitshift auto` checks the current working directory against your configured folder mappings. If a matching mapping is found, GitShift will set the Git user (`user.name` and `user.email`) and mark the matched profile as current.
|
|
75
|
+
|
|
76
|
+
Run `gitshift auto` inside a linked folder (or any child path) to switch automatically:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
$ cd ~/projects/my-repo
|
|
80
|
+
$ gitshift auto
|
|
81
|
+
Switched to personal
|
|
82
|
+
```
|
|
42
83
|
|
|
43
84
|
## Example Workflow
|
|
44
85
|
|
|
@@ -46,6 +87,7 @@ gitshift --help
|
|
|
46
87
|
gitshift add
|
|
47
88
|
gitshift list
|
|
48
89
|
gitshift scan
|
|
90
|
+
gitshift backup
|
|
49
91
|
gitshift use personal
|
|
50
92
|
gitshift current
|
|
51
93
|
gitshift doctor
|
|
@@ -55,6 +97,37 @@ When you create a profile and choose SSH generation, GitShift creates a key unde
|
|
|
55
97
|
|
|
56
98
|
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
99
|
|
|
100
|
+
Interactive `add` example
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
$ gitshift add
|
|
104
|
+
Profile Name: personal
|
|
105
|
+
GitHub Username: akash
|
|
106
|
+
Email: akash@example.com
|
|
107
|
+
Generate SSH key automatically? (Y/n) [Y]
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Backup & Restore
|
|
111
|
+
|
|
112
|
+
- `gitshift backup [file]` writes a JSON file containing your saved profiles, folder mappings, and the currently selected profile. If no file is provided it defaults to `gitshift-backup.json` in your current directory.
|
|
113
|
+
|
|
114
|
+
Example backup:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
$ gitshift backup
|
|
118
|
+
Backup saved to /Users/akashs/gitshift-backup.json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
- `gitshift restore <file>` reads the JSON backup and restores profiles and mappings. It prompts to confirm overwriting existing data.
|
|
122
|
+
|
|
123
|
+
Example restore:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
$ gitshift restore gitshift-backup.json
|
|
127
|
+
This will overwrite current data. Continue? (y/N)
|
|
128
|
+
Backup restored
|
|
129
|
+
```
|
|
130
|
+
|
|
58
131
|
## How It Works
|
|
59
132
|
|
|
60
133
|
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
package/src/commands/add.js
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
confirm,
|
|
3
|
-
input,
|
|
4
|
-
} from "@inquirer/prompts";
|
|
5
|
-
|
|
1
|
+
import { confirm, input } from "@inquirer/prompts";
|
|
6
2
|
import ora from "ora";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
saveProfile,
|
|
11
|
-
} from "../services/profile.js";
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
generateSSHKey,
|
|
15
|
-
} from "../services/ssh.js";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
error,
|
|
19
|
-
success,
|
|
20
|
-
} from "../utils/logger.js";
|
|
3
|
+
import { getProfile, saveProfile } from "../services/profile.js";
|
|
4
|
+
import { generateSSHKey } from "../services/ssh.js";
|
|
5
|
+
import { error, success } from "../utils/logger.js";
|
|
21
6
|
|
|
22
7
|
export async function addCommand() {
|
|
23
8
|
try {
|
|
@@ -25,6 +10,12 @@ export async function addCommand() {
|
|
|
25
10
|
message: "Profile Name",
|
|
26
11
|
});
|
|
27
12
|
|
|
13
|
+
if (!name.trim()) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Profile name is required"
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
28
19
|
if (getProfile(name)) {
|
|
29
20
|
error(`Profile "${name}" already exists`);
|
|
30
21
|
|
|
@@ -36,10 +27,22 @@ export async function addCommand() {
|
|
|
36
27
|
message: "GitHub Username",
|
|
37
28
|
});
|
|
38
29
|
|
|
30
|
+
if (!username.trim()) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
"GitHub username is required"
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
39
36
|
const email = await input({
|
|
40
37
|
message: "Email",
|
|
41
38
|
});
|
|
42
39
|
|
|
40
|
+
if (!email.trim()) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Email is required"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
43
46
|
const createSSH = await confirm({
|
|
44
47
|
message: "Generate SSH key automatically?",
|
|
45
48
|
default: true,
|
|
@@ -80,9 +83,7 @@ export async function addCommand() {
|
|
|
80
83
|
sshKey,
|
|
81
84
|
});
|
|
82
85
|
|
|
83
|
-
success(
|
|
84
|
-
`Profile "${name}" saved locally`
|
|
85
|
-
);
|
|
86
|
+
success(`Profile "${name}" saved locally`);
|
|
86
87
|
} catch (err) {
|
|
87
88
|
if (err && err.name === "ExitPromptError") {
|
|
88
89
|
// User canceled the prompt (Ctrl+C / SIGINT). Exit gracefully.
|
package/src/commands/auto.js
CHANGED
|
@@ -1,43 +1,31 @@
|
|
|
1
1
|
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
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";
|
|
2
|
+
import { setGitUser } from "../services/git.js";
|
|
3
|
+
import { getFolderMappings, getProfile, setCurrentProfile } from "../services/profile.js";
|
|
4
|
+
import { success } from "../utils/logger.js";
|
|
15
5
|
|
|
16
6
|
export async function autoCommand() {
|
|
17
7
|
const cwd = process.cwd();
|
|
18
8
|
|
|
19
9
|
const mappings = getFolderMappings();
|
|
20
10
|
|
|
21
|
-
const matched =
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
11
|
+
const matched = mappings
|
|
12
|
+
.sort(
|
|
13
|
+
(a, b) =>
|
|
14
|
+
b.path.length -
|
|
15
|
+
a.path.length
|
|
16
|
+
)
|
|
17
|
+
.find((mapping) =>
|
|
18
|
+
cwd.startsWith(
|
|
19
|
+
mapping.path
|
|
27
20
|
)
|
|
28
|
-
|
|
29
|
-
cwd.startsWith(
|
|
30
|
-
mapping.path
|
|
31
|
-
)
|
|
32
|
-
);
|
|
21
|
+
);
|
|
33
22
|
|
|
34
23
|
if (!matched) {
|
|
35
24
|
console.log("\nNo matching profile\n");
|
|
36
25
|
return;
|
|
37
26
|
}
|
|
38
27
|
|
|
39
|
-
const profile =
|
|
40
|
-
getProfile(matched.profile);
|
|
28
|
+
const profile = getProfile(matched.profile);
|
|
41
29
|
|
|
42
30
|
if (!profile) {
|
|
43
31
|
return;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from "fs-extra";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { getCurrentProfile, getFolderMappings, getProfiles } from "../services/profile.js";
|
|
4
|
+
import { error, success } from "../utils/logger.js";
|
|
5
|
+
|
|
6
|
+
export async function backupCommand(fileName = "gitshift-backup.json") {
|
|
7
|
+
try {
|
|
8
|
+
const backup = {
|
|
9
|
+
profiles: getProfiles(),
|
|
10
|
+
folderMappings: getFolderMappings(),
|
|
11
|
+
currentProfile: getCurrentProfile(),
|
|
12
|
+
createdAt: new Date().toISOString(),
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const filePath = path.resolve(fileName);
|
|
16
|
+
|
|
17
|
+
await fs.writeJson(
|
|
18
|
+
filePath,
|
|
19
|
+
backup,
|
|
20
|
+
{
|
|
21
|
+
spaces: 2,
|
|
22
|
+
}
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
success(`Backup saved to ${filePath}`);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
error(err.message);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/src/commands/current.js
CHANGED
|
@@ -1,21 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getCurrentProfile,
|
|
3
|
-
getProfile,
|
|
4
|
-
} from "../services/profile.js";
|
|
5
|
-
|
|
1
|
+
import { getCurrentProfile, getProfile, } from "../services/profile.js";
|
|
6
2
|
import { info } from "../utils/logger.js";
|
|
7
3
|
|
|
8
4
|
export async function currentCommand() {
|
|
9
|
-
const current =
|
|
10
|
-
getCurrentProfile();
|
|
5
|
+
const current = getCurrentProfile();
|
|
11
6
|
|
|
12
7
|
if (!current) {
|
|
13
8
|
info("No active profile");
|
|
14
9
|
return;
|
|
15
10
|
}
|
|
16
11
|
|
|
17
|
-
const profile =
|
|
18
|
-
getProfile(current);
|
|
12
|
+
const profile = getProfile(current);
|
|
19
13
|
|
|
20
14
|
if (!profile) {
|
|
21
15
|
info("Profile not found");
|
|
@@ -23,23 +17,9 @@ export async function currentCommand() {
|
|
|
23
17
|
}
|
|
24
18
|
|
|
25
19
|
console.log();
|
|
26
|
-
|
|
27
|
-
console.log(
|
|
28
|
-
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
console.log(
|
|
32
|
-
`Username: ${profile.username}`
|
|
33
|
-
);
|
|
34
|
-
|
|
35
|
-
console.log(
|
|
36
|
-
`Email : ${profile.email}`
|
|
37
|
-
);
|
|
38
|
-
|
|
39
|
-
console.log(
|
|
40
|
-
`SSH Key : ${profile.sshKey || "Not configured"
|
|
41
|
-
}`
|
|
42
|
-
);
|
|
43
|
-
|
|
20
|
+
console.log(`Profile : ${profile.name}`);
|
|
21
|
+
console.log(`Username: ${profile.username}`);
|
|
22
|
+
console.log(`Email : ${profile.email}`);
|
|
23
|
+
console.log(`SSH Key : ${profile.sshKey || "Not configured"}`);
|
|
44
24
|
console.log();
|
|
45
25
|
}
|
package/src/commands/doctor.js
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
|
+
import { error, success } from "../utils/logger.js";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
error,
|
|
5
|
-
success,
|
|
6
|
-
} from "../utils/logger.js";
|
|
7
|
-
|
|
8
|
-
async function check(
|
|
9
|
-
name,
|
|
10
|
-
command,
|
|
11
|
-
args = []
|
|
12
|
-
) {
|
|
4
|
+
async function check(name, command, args = []) {
|
|
13
5
|
try {
|
|
14
6
|
await execa(command, args);
|
|
15
|
-
|
|
16
7
|
success(`${name} installed`);
|
|
17
|
-
|
|
18
8
|
return true;
|
|
19
9
|
} catch {
|
|
20
10
|
error(`${name} missing`);
|
|
21
|
-
|
|
22
11
|
return false;
|
|
23
12
|
}
|
|
24
13
|
}
|
|
@@ -26,25 +15,14 @@ async function check(
|
|
|
26
15
|
export async function doctorCommand() {
|
|
27
16
|
console.log();
|
|
28
17
|
|
|
29
|
-
await check("Git", "git", [
|
|
30
|
-
|
|
31
|
-
]);
|
|
32
|
-
|
|
33
|
-
await check("SSH", "ssh", [
|
|
34
|
-
"-V",
|
|
35
|
-
]);
|
|
18
|
+
await check("Git", "git", ["--version",]);
|
|
19
|
+
await check("SSH", "ssh", ["-V",]);
|
|
36
20
|
|
|
37
|
-
const ghInstalled = await check(
|
|
38
|
-
"GitHub CLI",
|
|
39
|
-
"gh",
|
|
40
|
-
["--version"]
|
|
41
|
-
);
|
|
21
|
+
const ghInstalled = await check("GitHub CLI", "gh", ["--version"]);
|
|
42
22
|
|
|
43
23
|
if (!ghInstalled) {
|
|
44
24
|
console.log();
|
|
45
|
-
console.log(
|
|
46
|
-
"Hint: Install GitHub CLI (macOS): `brew install gh`"
|
|
47
|
-
);
|
|
25
|
+
console.log("Hint: Install GitHub CLI (macOS): `brew install gh`");
|
|
48
26
|
console.log("Then authenticate with: `gh auth login`");
|
|
49
27
|
}
|
|
50
28
|
|
package/src/commands/link.js
CHANGED
|
@@ -1,33 +1,14 @@
|
|
|
1
|
+
import { confirm, input, select } from "@inquirer/prompts";
|
|
1
2
|
import fs from "fs-extra";
|
|
3
|
+
import ora from "ora";
|
|
2
4
|
import path from "path";
|
|
5
|
+
import { addFolderMapping, getProfile, getProfiles, saveProfile } from "../services/profile.js";
|
|
6
|
+
import { generateSSHKey } from "../services/ssh.js";
|
|
7
|
+
import { error, success } from "../utils/logger.js";
|
|
3
8
|
|
|
4
|
-
|
|
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";
|
|
9
|
+
export async function linkCommand(folder) {
|
|
19
10
|
|
|
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
11
|
const fullPath = path.resolve(folder);
|
|
30
|
-
|
|
31
12
|
const exists = await fs.pathExists(fullPath);
|
|
32
13
|
|
|
33
14
|
if (!exists) {
|
|
@@ -36,33 +17,28 @@ export async function linkCommand(
|
|
|
36
17
|
}
|
|
37
18
|
|
|
38
19
|
let profiles = getProfiles();
|
|
39
|
-
|
|
40
20
|
let selectedProfile;
|
|
41
21
|
|
|
42
22
|
if (profiles.length === 0) {
|
|
43
23
|
console.log("\nNo profiles found.\n");
|
|
44
|
-
|
|
45
24
|
selectedProfile = await createProfile();
|
|
46
25
|
} else {
|
|
47
|
-
const choice =
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
});
|
|
26
|
+
const choice = await select({
|
|
27
|
+
message: "Select Profile",
|
|
28
|
+
choices: [
|
|
29
|
+
...profiles.map(
|
|
30
|
+
(profile) => ({
|
|
31
|
+
name: `${profile.name} (${profile.username})`,
|
|
32
|
+
value:
|
|
33
|
+
profile.name,
|
|
34
|
+
})
|
|
35
|
+
),
|
|
36
|
+
{
|
|
37
|
+
name: "+ Create New Profile",
|
|
38
|
+
value: "__create__",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
});
|
|
66
42
|
|
|
67
43
|
if (choice === "__create__") {
|
|
68
44
|
selectedProfile = await createProfile();
|
|
@@ -73,9 +49,7 @@ export async function linkCommand(
|
|
|
73
49
|
|
|
74
50
|
try {
|
|
75
51
|
addFolderMapping(selectedProfile, fullPath);
|
|
76
|
-
|
|
77
52
|
success(`Linked ${fullPath}`);
|
|
78
|
-
|
|
79
53
|
success(`Profile: ${selectedProfile}`);
|
|
80
54
|
} catch (err) {
|
|
81
55
|
error(err.message);
|
|
@@ -83,21 +57,38 @@ export async function linkCommand(
|
|
|
83
57
|
}
|
|
84
58
|
|
|
85
59
|
async function createProfile() {
|
|
86
|
-
const name =
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
60
|
+
const name = await input({
|
|
61
|
+
message: "Profile Name",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (!name.trim()) {
|
|
65
|
+
throw new Error("Profile name is required");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (getProfile(name)) {
|
|
69
|
+
error(`Profile "${name}" already exists`);
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
90
73
|
|
|
91
74
|
const username =
|
|
92
75
|
await input({
|
|
93
76
|
message: "GitHub Username",
|
|
94
77
|
});
|
|
95
78
|
|
|
79
|
+
if (!username.trim()) {
|
|
80
|
+
throw new Error("GitHub username is required");
|
|
81
|
+
}
|
|
82
|
+
|
|
96
83
|
const email =
|
|
97
84
|
await input({
|
|
98
85
|
message: "Email",
|
|
99
86
|
});
|
|
100
87
|
|
|
88
|
+
if (!email.trim()) {
|
|
89
|
+
throw new Error("Email is required");
|
|
90
|
+
}
|
|
91
|
+
|
|
101
92
|
const shouldCreateSSH =
|
|
102
93
|
await confirm({
|
|
103
94
|
message: "Generate SSH Key?",
|
|
@@ -111,7 +102,6 @@ async function createProfile() {
|
|
|
111
102
|
|
|
112
103
|
try {
|
|
113
104
|
sshKey = await generateSSHKey(name, email);
|
|
114
|
-
|
|
115
105
|
spinner.succeed("SSH key generated");
|
|
116
106
|
} catch (err) {
|
|
117
107
|
spinner.fail("Unable to generate SSH key");
|
package/src/commands/links.js
CHANGED
package/src/commands/remove.js
CHANGED
|
@@ -1,37 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
getProfile,
|
|
4
|
-
removeProfile,
|
|
5
|
-
setCurrentProfile,
|
|
6
|
-
} from "../services/profile.js";
|
|
1
|
+
import { getCurrentProfile, getProfile, removeProfile, setCurrentProfile } from "../services/profile.js";
|
|
2
|
+
import { error, success } from "../utils/logger.js";
|
|
7
3
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
success,
|
|
11
|
-
} from "../utils/logger.js";
|
|
12
|
-
|
|
13
|
-
export async function removeCommand(
|
|
14
|
-
profileName
|
|
15
|
-
) {
|
|
16
|
-
const profile =
|
|
17
|
-
getProfile(profileName);
|
|
4
|
+
export async function removeCommand(profileName) {
|
|
5
|
+
const profile = getProfile(profileName);
|
|
18
6
|
|
|
19
7
|
if (!profile) {
|
|
20
8
|
error("Profile not found");
|
|
21
|
-
|
|
22
9
|
return;
|
|
23
10
|
}
|
|
24
11
|
|
|
25
12
|
removeProfile(profileName);
|
|
26
13
|
|
|
27
|
-
if (
|
|
28
|
-
getCurrentProfile() ===
|
|
29
|
-
profileName
|
|
30
|
-
) {
|
|
14
|
+
if (getCurrentProfile() === profileName) {
|
|
31
15
|
setCurrentProfile(null);
|
|
32
16
|
}
|
|
33
17
|
|
|
34
|
-
success(
|
|
35
|
-
`Profile "${profileName}" removed`
|
|
36
|
-
);
|
|
18
|
+
success(`Profile "${profileName}" removed`);
|
|
37
19
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { confirm } from "@inquirer/prompts";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { restoreData } from "../services/profile.js";
|
|
5
|
+
import { error, success } from "../utils/logger.js";
|
|
6
|
+
|
|
7
|
+
export async function restoreCommand(file) {
|
|
8
|
+
try {
|
|
9
|
+
const filePath = path.resolve(file);
|
|
10
|
+
const exists = await fs.pathExists(filePath);
|
|
11
|
+
|
|
12
|
+
if (!exists) {
|
|
13
|
+
error("Backup file not found");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const overwrite = await confirm({
|
|
18
|
+
message: "This will overwrite current data. Continue?",
|
|
19
|
+
default: false,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (!overwrite) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const backup = await fs.readJson(filePath);
|
|
27
|
+
restoreData(backup);
|
|
28
|
+
|
|
29
|
+
success("Backup restored");
|
|
30
|
+
} catch (err) {
|
|
31
|
+
error(err.message);
|
|
32
|
+
}
|
|
33
|
+
}
|
package/src/commands/scan.js
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
|
+
import { input, select } from "@inquirer/prompts";
|
|
1
2
|
import path from "path";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
select,
|
|
6
|
-
} from "@inquirer/prompts";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
findSSHKeys,
|
|
10
|
-
} from "../services/scan.js";
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
getProfiles,
|
|
14
|
-
saveProfile,
|
|
15
|
-
} from "../services/profile.js";
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
error,
|
|
19
|
-
info,
|
|
20
|
-
success,
|
|
21
|
-
} from "../utils/logger.js";
|
|
3
|
+
import { getProfiles, saveProfile } from "../services/profile.js";
|
|
4
|
+
import { findSSHKeys } from "../services/scan.js";
|
|
5
|
+
import { error, info, success } from "../utils/logger.js";
|
|
22
6
|
|
|
23
7
|
export async function scanCommand() {
|
|
24
8
|
try {
|
|
@@ -30,9 +14,7 @@ export async function scanCommand() {
|
|
|
30
14
|
}
|
|
31
15
|
|
|
32
16
|
const existing = getProfiles();
|
|
33
|
-
|
|
34
17
|
const importedPaths = existing.map((profile) => profile.sshKey);
|
|
35
|
-
|
|
36
18
|
const available = keys.filter((key) => !importedPaths.includes(key));
|
|
37
19
|
|
|
38
20
|
if (!available.length) {
|
|
@@ -57,18 +39,25 @@ export async function scanCommand() {
|
|
|
57
39
|
message: "GitHub Username",
|
|
58
40
|
});
|
|
59
41
|
|
|
42
|
+
if (!username.trim()) {
|
|
43
|
+
throw new Error("GitHub username is required");
|
|
44
|
+
}
|
|
45
|
+
|
|
60
46
|
const email = await input({
|
|
61
47
|
message: "Email",
|
|
62
48
|
});
|
|
63
49
|
|
|
50
|
+
if (!email.trim()) {
|
|
51
|
+
throw new Error("Email is required");
|
|
52
|
+
}
|
|
53
|
+
|
|
64
54
|
try {
|
|
65
55
|
saveProfile({
|
|
66
56
|
name: profileName,
|
|
67
57
|
username,
|
|
68
58
|
email,
|
|
69
59
|
sshKey: selected,
|
|
70
|
-
source:
|
|
71
|
-
"imported",
|
|
60
|
+
source: "imported",
|
|
72
61
|
});
|
|
73
62
|
|
|
74
63
|
success(`Existing SSH Key Imported "${profileName}"`);
|
package/src/commands/unlink.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
removeFolderMapping,
|
|
5
|
-
} from "../services/profile.js";
|
|
6
|
-
|
|
2
|
+
import { removeFolderMapping } from "../services/profile.js";
|
|
7
3
|
import { success } from "../utils/logger.js";
|
|
8
4
|
|
|
9
|
-
export async function unlinkCommand(
|
|
10
|
-
folder
|
|
11
|
-
) {
|
|
5
|
+
export async function unlinkCommand(folder) {
|
|
12
6
|
const fullPath = path.resolve(folder);
|
|
13
|
-
|
|
14
7
|
removeFolderMapping(fullPath);
|
|
15
|
-
|
|
16
8
|
success(`Removed ${fullPath}`);
|
|
17
9
|
}
|
package/src/commands/use.js
CHANGED
|
@@ -1,57 +1,27 @@
|
|
|
1
1
|
import ora from "ora";
|
|
2
|
+
import { setGitUser } from "../services/git.js";
|
|
3
|
+
import { getProfile, setCurrentProfile } from "../services/profile.js";
|
|
4
|
+
import { error, success } from "../utils/logger.js";
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
getProfile
|
|
5
|
-
setCurrentProfile,
|
|
6
|
-
} from "../services/profile.js";
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
setGitUser,
|
|
10
|
-
} from "../services/git.js";
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
error,
|
|
14
|
-
success,
|
|
15
|
-
} from "../utils/logger.js";
|
|
16
|
-
|
|
17
|
-
export async function useCommand(
|
|
18
|
-
profileName
|
|
19
|
-
) {
|
|
20
|
-
const profile =
|
|
21
|
-
getProfile(profileName);
|
|
6
|
+
export async function useCommand(profileName) {
|
|
7
|
+
const profile = getProfile(profileName);
|
|
22
8
|
|
|
23
9
|
if (!profile) {
|
|
24
|
-
error(
|
|
25
|
-
`Profile "${profileName}" not found`
|
|
26
|
-
);
|
|
27
|
-
|
|
10
|
+
error(`Profile "${profileName}" not found`);
|
|
28
11
|
return;
|
|
29
12
|
}
|
|
30
13
|
|
|
31
|
-
const spinner = ora(
|
|
32
|
-
"Switching profile..."
|
|
33
|
-
).start();
|
|
14
|
+
const spinner = ora("Switching profile...").start();
|
|
34
15
|
|
|
35
16
|
try {
|
|
36
|
-
await setGitUser(
|
|
37
|
-
profile.username,
|
|
38
|
-
profile.email
|
|
39
|
-
);
|
|
40
|
-
|
|
17
|
+
await setGitUser(profile.username, profile.email);
|
|
41
18
|
setCurrentProfile(profile.name);
|
|
42
19
|
|
|
43
|
-
spinner.succeed(
|
|
44
|
-
|
|
45
|
-
);
|
|
20
|
+
spinner.succeed("Profile switched");
|
|
21
|
+
success(`Current profile: ${profile.name}`);
|
|
46
22
|
|
|
47
|
-
success(
|
|
48
|
-
`Current profile: ${profile.name}`
|
|
49
|
-
);
|
|
50
23
|
} catch (err) {
|
|
51
|
-
spinner.fail(
|
|
52
|
-
"Unable to switch profile"
|
|
53
|
-
);
|
|
54
|
-
|
|
24
|
+
spinner.fail("Unable to switch profile");
|
|
55
25
|
error(err.message);
|
|
56
26
|
}
|
|
57
27
|
}
|
package/src/server.js
CHANGED
|
@@ -6,12 +6,14 @@ import { Command } from "commander";
|
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
7
|
import { addCommand } from "./commands/add.js";
|
|
8
8
|
import { autoCommand } from "./commands/auto.js";
|
|
9
|
+
import { backupCommand } from "./commands/backup.js";
|
|
9
10
|
import { currentCommand } from "./commands/current.js";
|
|
10
11
|
import { doctorCommand } from "./commands/doctor.js";
|
|
11
12
|
import { linkCommand } from "./commands/link.js";
|
|
12
13
|
import { linksCommand } from "./commands/links.js";
|
|
13
14
|
import { listCommand } from "./commands/list.js";
|
|
14
15
|
import { removeCommand } from "./commands/remove.js";
|
|
16
|
+
import { restoreCommand } from "./commands/restore.js";
|
|
15
17
|
import { scanCommand } from "./commands/scan.js";
|
|
16
18
|
import { unlinkCommand } from "./commands/unlink.js";
|
|
17
19
|
import { useCommand } from "./commands/use.js";
|
|
@@ -62,9 +64,7 @@ async function checkForUpdates() {
|
|
|
62
64
|
const latestVersion = response.data.version;
|
|
63
65
|
|
|
64
66
|
if (latestVersion && compareVersions(version, latestVersion) < 0) {
|
|
65
|
-
console.log(
|
|
66
|
-
`A new version of ${name} is available: ${version} -> ${latestVersion}`,
|
|
67
|
-
);
|
|
67
|
+
console.log(`A new version of ${name} is available: ${version} -> ${latestVersion}`);
|
|
68
68
|
console.log(`Run: npm install -g ${name}`);
|
|
69
69
|
}
|
|
70
70
|
} catch (error) {
|
|
@@ -165,6 +165,22 @@ async function main() {
|
|
|
165
165
|
)
|
|
166
166
|
.action(doctorCommand);
|
|
167
167
|
|
|
168
|
+
program
|
|
169
|
+
.command("backup")
|
|
170
|
+
.description(
|
|
171
|
+
"Backup profiles and mappings"
|
|
172
|
+
)
|
|
173
|
+
.argument("[file]")
|
|
174
|
+
.action(backupCommand);
|
|
175
|
+
|
|
176
|
+
program
|
|
177
|
+
.command("restore")
|
|
178
|
+
.description(
|
|
179
|
+
"Restore backup"
|
|
180
|
+
)
|
|
181
|
+
.argument("<file>")
|
|
182
|
+
.action(restoreCommand);
|
|
183
|
+
|
|
168
184
|
program.exitOverride();
|
|
169
185
|
|
|
170
186
|
try {
|
package/src/services/git.js
CHANGED
package/src/services/profile.js
CHANGED
|
@@ -9,20 +9,27 @@ export function getProfiles() {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function saveProfile(profile) {
|
|
12
|
-
const profiles = getProfiles();
|
|
13
12
|
|
|
13
|
+
if (
|
|
14
|
+
!profile.name?.trim() ||
|
|
15
|
+
!profile.username?.trim() ||
|
|
16
|
+
!profile.email?.trim()
|
|
17
|
+
) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
"Invalid profile data"
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const profiles = getProfiles();
|
|
14
24
|
const exists = profiles.find(
|
|
15
25
|
(item) => item.name === profile.name
|
|
16
26
|
);
|
|
17
27
|
|
|
18
28
|
if (exists) {
|
|
19
|
-
throw new Error(
|
|
20
|
-
`Profile "${profile.name}" already exists`
|
|
21
|
-
);
|
|
29
|
+
throw new Error(`Profile "${profile.name}" already exists`);
|
|
22
30
|
}
|
|
23
31
|
|
|
24
32
|
profiles.push(profile);
|
|
25
|
-
|
|
26
33
|
config.set("profiles", profiles);
|
|
27
34
|
}
|
|
28
35
|
|
|
@@ -44,9 +51,7 @@ export function removeProfile(name) {
|
|
|
44
51
|
config.set("profiles", filtered);
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
export function setCurrentProfile(
|
|
48
|
-
name
|
|
49
|
-
) {
|
|
54
|
+
export function setCurrentProfile(name) {
|
|
50
55
|
if (!name) {
|
|
51
56
|
config.delete("current");
|
|
52
57
|
return;
|
|
@@ -63,12 +68,8 @@ export function getFolderMappings() {
|
|
|
63
68
|
return config.get("folderMappings", []);
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
export function addFolderMapping(
|
|
67
|
-
profile,
|
|
68
|
-
folderPath
|
|
69
|
-
) {
|
|
71
|
+
export function addFolderMapping(profile, folderPath) {
|
|
70
72
|
const mappings = getFolderMappings();
|
|
71
|
-
|
|
72
73
|
const exists = mappings.find((item) => item.path === folderPath);
|
|
73
74
|
|
|
74
75
|
if (exists) {
|
|
@@ -76,20 +77,20 @@ export function addFolderMapping(
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
mappings.push({ profile, path: folderPath, });
|
|
79
|
-
|
|
80
80
|
config.set("folderMappings", mappings);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export function removeFolderMapping(
|
|
84
|
-
folderPath
|
|
85
|
-
) {
|
|
83
|
+
export function removeFolderMapping(folderPath) {
|
|
86
84
|
const mappings = getFolderMappings();
|
|
85
|
+
config.set("folderMappings", mappings.filter((item) => item.path !== folderPath));
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
88
|
+
export function restoreData(data) {
|
|
89
|
+
|
|
90
|
+
config.set("profiles", data.profiles || []);
|
|
91
|
+
config.set("folderMappings", data.folderMappings || []);
|
|
92
|
+
|
|
93
|
+
if (data.currentProfile) {
|
|
94
|
+
config.set("current", data.currentProfile);
|
|
95
|
+
}
|
|
95
96
|
}
|
package/src/services/scan.js
CHANGED
package/src/services/ssh.js
CHANGED
|
@@ -13,32 +13,21 @@ function toSafeKeyName(profileName) {
|
|
|
13
13
|
return normalized || "profile";
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export async function generateSSHKey(
|
|
17
|
-
profileName
|
|
18
|
-
email
|
|
19
|
-
) {
|
|
20
|
-
const safeProfileName = toSafeKeyName(
|
|
21
|
-
profileName
|
|
22
|
-
);
|
|
23
|
-
|
|
16
|
+
export async function generateSSHKey(profileName, email) {
|
|
17
|
+
const safeProfileName = toSafeKeyName(profileName);
|
|
24
18
|
const keyPath = path.join(
|
|
25
19
|
os.homedir(),
|
|
26
20
|
".ssh",
|
|
27
21
|
`gitshift-${safeProfileName}`
|
|
28
22
|
);
|
|
29
23
|
|
|
30
|
-
const exists = await fs.pathExists(
|
|
31
|
-
keyPath
|
|
32
|
-
);
|
|
24
|
+
const exists = await fs.pathExists(keyPath);
|
|
33
25
|
|
|
34
26
|
if (exists) {
|
|
35
27
|
return keyPath;
|
|
36
28
|
}
|
|
37
29
|
|
|
38
|
-
await fs.ensureDir(
|
|
39
|
-
path.dirname(keyPath)
|
|
40
|
-
);
|
|
41
|
-
|
|
30
|
+
await fs.ensureDir(path.dirname(keyPath));
|
|
42
31
|
await execa("ssh-keygen", [
|
|
43
32
|
"-t",
|
|
44
33
|
"ed25519",
|
|
@@ -53,14 +42,9 @@ export async function generateSSHKey(
|
|
|
53
42
|
return keyPath;
|
|
54
43
|
}
|
|
55
44
|
|
|
56
|
-
export async function getPublicKey(
|
|
57
|
-
privateKeyPath
|
|
58
|
-
)
|
|
59
|
-
const publicKey =
|
|
60
|
-
`${privateKeyPath}.pub`;
|
|
61
|
-
|
|
62
|
-
const exists =
|
|
63
|
-
await fs.pathExists(publicKey);
|
|
45
|
+
export async function getPublicKey(privateKeyPath) {
|
|
46
|
+
const publicKey = `${privateKeyPath}.pub`;
|
|
47
|
+
const exists = await fs.pathExists(publicKey);
|
|
64
48
|
|
|
65
49
|
if (!exists) {
|
|
66
50
|
return null;
|
package/src/utils/logger.js
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
|
|
3
|
-
export const success = (msg) =>
|
|
4
|
-
console.log(chalk.green(`✓ ${msg}`));
|
|
3
|
+
export const success = (msg) => console.log(chalk.green(`✓ ${msg}`));
|
|
5
4
|
|
|
6
|
-
export const error = (msg) =>
|
|
7
|
-
console.log(chalk.red(`✗ ${msg}`));
|
|
5
|
+
export const error = (msg) => console.log(chalk.red(`✗ ${msg}`));
|
|
8
6
|
|
|
9
|
-
export const info = (msg) =>
|
|
10
|
-
console.log(chalk.cyan(msg));
|
|
7
|
+
export const info = (msg) => console.log(chalk.cyan(msg));
|