@hypercodingdev/zit 1.0.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/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +22 -0
- package/dist/commands/add.d.ts +2 -0
- package/dist/commands/add.js +53 -0
- package/dist/commands/clone.d.ts +2 -0
- package/dist/commands/clone.js +53 -0
- package/dist/commands/list.d.ts +2 -0
- package/dist/commands/list.js +23 -0
- package/dist/commands/remove.d.ts +2 -0
- package/dist/commands/remove.js +47 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +32 -0
- package/dist/commands/workspace.d.ts +2 -0
- package/dist/commands/workspace.js +133 -0
- package/dist/config.d.ts +25 -0
- package/dist/config.js +132 -0
- package/dist/gitconfig.d.ts +19 -0
- package/dist/gitconfig.js +130 -0
- package/dist/ssh.d.ts +8 -0
- package/dist/ssh.js +78 -0
- package/dist/utils.d.ts +12 -0
- package/dist/utils.js +115 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# zit
|
|
2
|
+
|
|
3
|
+
A folder-based Git identity manager. Associate workspace folders with Git accounts — SSH keys, author name, and email are applied automatically to every repo inside.
|
|
4
|
+
|
|
5
|
+
**No manual switching.** No ssh-agent. No shell hooks. Uses git's native `includeIf` to make it work everywhere — terminal, VS Code, JetBrains, any git client.
|
|
6
|
+
|
|
7
|
+
## How It Works
|
|
8
|
+
|
|
9
|
+
1. Register your git accounts with `zit add`
|
|
10
|
+
2. Create workspace folders linked to accounts with `zit workspace init`
|
|
11
|
+
3. Clone repos inside workspace folders with `zit clone`
|
|
12
|
+
4. After that, `git push`, `git pull`, `git commit` — everything uses the correct identity automatically
|
|
13
|
+
|
|
14
|
+
Under the hood, `zit` uses:
|
|
15
|
+
- **`includeIf "gitdir:"`** — git's native conditional config that auto-loads identity settings based on repo location
|
|
16
|
+
- **`core.sshCommand`** — tells git which SSH key to use, no ssh-agent needed
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```shell
|
|
21
|
+
npm install -g zit-cli
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node.js >= 18, `git`, and `ssh-keygen`.
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```shell
|
|
29
|
+
# Register an account
|
|
30
|
+
zit add
|
|
31
|
+
# Account name: work
|
|
32
|
+
# Git user name: John Doe
|
|
33
|
+
# Git email: john@company.com
|
|
34
|
+
# → Generates SSH key, prints public key to paste into GitHub/GitLab
|
|
35
|
+
|
|
36
|
+
# Create a workspace folder linked to that account
|
|
37
|
+
zit workspace init ~/work work
|
|
38
|
+
# → Creates ~/work/, configures git includeIf
|
|
39
|
+
|
|
40
|
+
# Clone a repo inside the workspace
|
|
41
|
+
cd ~/work
|
|
42
|
+
zit clone git@github.com:my-org/my-repo.git
|
|
43
|
+
|
|
44
|
+
# From now on, git just works — no zit needed
|
|
45
|
+
cd ~/work/my-repo
|
|
46
|
+
git commit -m "uses work identity automatically"
|
|
47
|
+
git push # uses work SSH key automatically
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
### `zit add`
|
|
53
|
+
|
|
54
|
+
Register a new git account interactively. Generates an SSH keypair.
|
|
55
|
+
|
|
56
|
+
```shell
|
|
57
|
+
zit add # default: ed25519
|
|
58
|
+
zit add -t rsa # specify key type
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Supported key types: `ed25519` (default), `rsa`, `ecdsa`, `ecdsa-sk`, `ed25519-sk`, `dsa`
|
|
62
|
+
|
|
63
|
+
### `zit remove <account>`
|
|
64
|
+
|
|
65
|
+
Remove an account and its SSH keys.
|
|
66
|
+
|
|
67
|
+
```shell
|
|
68
|
+
zit remove work
|
|
69
|
+
zit remove work --force # also removes linked workspaces
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `zit list`
|
|
73
|
+
|
|
74
|
+
List all registered accounts with their emails.
|
|
75
|
+
|
|
76
|
+
```shell
|
|
77
|
+
$ zit list
|
|
78
|
+
Registered accounts:
|
|
79
|
+
|
|
80
|
+
personal john@personal.com
|
|
81
|
+
work john@company.com
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### `zit workspace init <path> <account>`
|
|
85
|
+
|
|
86
|
+
Create a workspace folder linked to an account.
|
|
87
|
+
|
|
88
|
+
```shell
|
|
89
|
+
zit workspace init ~/work work
|
|
90
|
+
zit workspace init ~/personal personal
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### `zit workspace list`
|
|
94
|
+
|
|
95
|
+
List all workspaces.
|
|
96
|
+
|
|
97
|
+
```shell
|
|
98
|
+
$ zit workspace list
|
|
99
|
+
Workspaces:
|
|
100
|
+
|
|
101
|
+
work ~/work work
|
|
102
|
+
personal ~/personal personal
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `zit workspace remove <name>`
|
|
106
|
+
|
|
107
|
+
Unlink a workspace. Keeps the folder and repos, but removes the identity link.
|
|
108
|
+
|
|
109
|
+
```shell
|
|
110
|
+
zit workspace remove work
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### `zit clone <url>`
|
|
114
|
+
|
|
115
|
+
Clone a repo using the current workspace's SSH key. Must be run inside a workspace folder.
|
|
116
|
+
|
|
117
|
+
```shell
|
|
118
|
+
cd ~/work
|
|
119
|
+
zit clone git@github.com:org/repo.git
|
|
120
|
+
zit clone git@github.com:org/repo.git --depth 1 # extra git clone flags work
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `zit status`
|
|
124
|
+
|
|
125
|
+
Show the current directory's workspace and account info.
|
|
126
|
+
|
|
127
|
+
```shell
|
|
128
|
+
$ cd ~/work/my-repo && zit status
|
|
129
|
+
Workspace: work
|
|
130
|
+
Path: ~/work
|
|
131
|
+
Account: work
|
|
132
|
+
Name: John Doe
|
|
133
|
+
Email: john@company.com
|
|
134
|
+
SSH Key: ~/.ssh/id_ed25519_work
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## What zit Does to Your System
|
|
138
|
+
|
|
139
|
+
- **`~/.config/zit/config`** — stores registered accounts and workspace mappings
|
|
140
|
+
- **`~/.config/zit/workspaces/*.gitconfig`** — per-workspace git config files
|
|
141
|
+
- **`~/.gitconfig`** — adds `includeIf` entries (one per workspace)
|
|
142
|
+
- **`~/.ssh/id_<type>_<name>`** — SSH keypairs for each account
|
|
143
|
+
|
|
144
|
+
## Platform Support
|
|
145
|
+
|
|
146
|
+
- Linux
|
|
147
|
+
- macOS
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const add_js_1 = require("./commands/add.js");
|
|
6
|
+
const remove_js_1 = require("./commands/remove.js");
|
|
7
|
+
const list_js_1 = require("./commands/list.js");
|
|
8
|
+
const workspace_js_1 = require("./commands/workspace.js");
|
|
9
|
+
const clone_js_1 = require("./commands/clone.js");
|
|
10
|
+
const status_js_1 = require("./commands/status.js");
|
|
11
|
+
const program = new commander_1.Command();
|
|
12
|
+
program
|
|
13
|
+
.name("zit")
|
|
14
|
+
.description("Folder-based Git identity manager. Associate workspace folders with Git accounts.")
|
|
15
|
+
.version("1.0.0");
|
|
16
|
+
(0, add_js_1.registerAddCommand)(program);
|
|
17
|
+
(0, remove_js_1.registerRemoveCommand)(program);
|
|
18
|
+
(0, list_js_1.registerListCommand)(program);
|
|
19
|
+
(0, workspace_js_1.registerWorkspaceCommand)(program);
|
|
20
|
+
(0, clone_js_1.registerCloneCommand)(program);
|
|
21
|
+
(0, status_js_1.registerStatusCommand)(program);
|
|
22
|
+
program.parse();
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerAddCommand = registerAddCommand;
|
|
4
|
+
const config_js_1 = require("../config.js");
|
|
5
|
+
const ssh_js_1 = require("../ssh.js");
|
|
6
|
+
const utils_js_1 = require("../utils.js");
|
|
7
|
+
function registerAddCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command("add")
|
|
10
|
+
.description("Register a new git account with SSH key generation")
|
|
11
|
+
.option("-t, --type <type>", "SSH key type (ed25519, rsa, ecdsa, ...)", "ed25519")
|
|
12
|
+
.action(async (opts) => {
|
|
13
|
+
if (!(0, ssh_js_1.isValidKeyType)(opts.type)) {
|
|
14
|
+
console.log((0, utils_js_1.red)(`Invalid key type: ${opts.type}`));
|
|
15
|
+
console.log("Valid types: ed25519, rsa, ecdsa, ecdsa-sk, ed25519-sk, dsa");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const keyType = opts.type;
|
|
19
|
+
const accountName = await (0, utils_js_1.prompt)("Account name: ");
|
|
20
|
+
if (!accountName) {
|
|
21
|
+
console.log((0, utils_js_1.red)("Account name is required."));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
const config = (0, config_js_1.readConfig)();
|
|
25
|
+
if (config.accounts[accountName]) {
|
|
26
|
+
const overwrite = await (0, utils_js_1.promptYesNo)(`Account "${accountName}" already exists. Overwrite?`);
|
|
27
|
+
if (!overwrite) {
|
|
28
|
+
console.log((0, utils_js_1.yellow)("Cancelled."));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const userName = await (0, utils_js_1.prompt)("Git user name: ");
|
|
33
|
+
const userEmail = await (0, utils_js_1.prompt)("Git email: ");
|
|
34
|
+
if (!userName || !userEmail) {
|
|
35
|
+
console.log((0, utils_js_1.red)("Name and email are required."));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
const { privateKey, publicKey: publicKeyPath } = (0, ssh_js_1.getSshKeyPath)(accountName, keyType);
|
|
39
|
+
console.log(`\nGenerating ${keyType} SSH key...`);
|
|
40
|
+
const publicKey = (0, ssh_js_1.generateSshKey)(userEmail, privateKey, keyType);
|
|
41
|
+
config.accounts[accountName] = {
|
|
42
|
+
name: userName,
|
|
43
|
+
email: userEmail,
|
|
44
|
+
private_key: privateKey,
|
|
45
|
+
public_key: publicKeyPath,
|
|
46
|
+
};
|
|
47
|
+
(0, config_js_1.writeConfig)(config);
|
|
48
|
+
console.log((0, utils_js_1.green)("\nYour SSH public key:"));
|
|
49
|
+
console.log(publicKey);
|
|
50
|
+
console.log((0, utils_js_1.green)("\nPaste this key into your GitHub/GitLab SSH settings."));
|
|
51
|
+
console.log((0, utils_js_1.green)(`Account "${accountName}" registered successfully.`));
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerCloneCommand = registerCloneCommand;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const config_js_1 = require("../config.js");
|
|
6
|
+
const utils_js_1 = require("../utils.js");
|
|
7
|
+
function registerCloneCommand(program) {
|
|
8
|
+
program
|
|
9
|
+
.command("clone <url>")
|
|
10
|
+
.description("Clone a repo using the current workspace's SSH key")
|
|
11
|
+
.allowUnknownOption(true)
|
|
12
|
+
.allowExcessArguments(true)
|
|
13
|
+
.action((url, _opts, cmd) => {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const match = (0, config_js_1.findWorkspaceByPath)(cwd);
|
|
16
|
+
if (!match) {
|
|
17
|
+
console.log((0, utils_js_1.red)("Not inside a zit workspace."));
|
|
18
|
+
const config = (0, config_js_1.readConfig)();
|
|
19
|
+
const workspaces = Object.entries(config.workspaces);
|
|
20
|
+
if (workspaces.length > 0) {
|
|
21
|
+
console.log("\nAvailable workspaces:");
|
|
22
|
+
for (const [name, ws] of workspaces) {
|
|
23
|
+
console.log(` ${name} ${(0, utils_js_1.collapseHome)(ws.path)}`);
|
|
24
|
+
}
|
|
25
|
+
console.log((0, utils_js_1.yellow)("\ncd into a workspace folder first, then run `zit clone <url>`."));
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
console.log((0, utils_js_1.yellow)("Create a workspace first: `zit workspace init <path> <account>`"));
|
|
29
|
+
}
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
const { name, workspace } = match;
|
|
33
|
+
const config = (0, config_js_1.readConfig)();
|
|
34
|
+
const account = config.accounts[workspace.account];
|
|
35
|
+
if (!account) {
|
|
36
|
+
console.log((0, utils_js_1.red)(`Account "${workspace.account}" referenced by workspace "${name}" not found.`));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
// Collect any extra args passed after the url
|
|
40
|
+
const extraArgs = cmd.args.slice(1).join(" ");
|
|
41
|
+
const sshCommand = `ssh -i ${account.private_key} -o IdentitiesOnly=yes`;
|
|
42
|
+
console.log(`Cloning with account "${workspace.account}" (${account.email})...`);
|
|
43
|
+
try {
|
|
44
|
+
const fullCmd = `GIT_SSH_COMMAND='${sshCommand}' git clone ${url}${extraArgs ? " " + extraArgs : ""}`;
|
|
45
|
+
(0, node_child_process_1.execSync)(fullCmd, { stdio: "inherit", cwd });
|
|
46
|
+
console.log((0, utils_js_1.green)("\nClone complete. Push/pull will use this identity automatically."));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
console.log((0, utils_js_1.red)("\nClone failed."));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerListCommand = registerListCommand;
|
|
4
|
+
const config_js_1 = require("../config.js");
|
|
5
|
+
const utils_js_1 = require("../utils.js");
|
|
6
|
+
function registerListCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("List all registered accounts")
|
|
10
|
+
.action(() => {
|
|
11
|
+
const config = (0, config_js_1.readConfig)();
|
|
12
|
+
const accounts = Object.entries(config.accounts);
|
|
13
|
+
if (accounts.length === 0) {
|
|
14
|
+
console.log((0, utils_js_1.yellow)("No accounts registered. Use `zit add` to register one."));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
console.log((0, utils_js_1.green)("Registered accounts:\n"));
|
|
18
|
+
const maxNameLen = Math.max(...accounts.map(([key]) => key.length));
|
|
19
|
+
for (const [key, acc] of accounts) {
|
|
20
|
+
console.log(` ${key.padEnd(maxNameLen + 2)} ${acc.email}`);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerRemoveCommand = registerRemoveCommand;
|
|
4
|
+
const config_js_1 = require("../config.js");
|
|
5
|
+
const ssh_js_1 = require("../ssh.js");
|
|
6
|
+
const gitconfig_js_1 = require("../gitconfig.js");
|
|
7
|
+
const config_js_2 = require("../config.js");
|
|
8
|
+
const utils_js_1 = require("../utils.js");
|
|
9
|
+
function registerRemoveCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command("remove <account>")
|
|
12
|
+
.description("Remove a registered git account and its SSH keys")
|
|
13
|
+
.option("--force", "Remove even if workspaces reference this account")
|
|
14
|
+
.action((accountName, opts) => {
|
|
15
|
+
const config = (0, config_js_1.readConfig)();
|
|
16
|
+
const account = config.accounts[accountName];
|
|
17
|
+
if (!account) {
|
|
18
|
+
console.log((0, utils_js_1.red)(`Account "${accountName}" not found.`));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
// Check if any workspaces reference this account
|
|
22
|
+
const linkedWorkspaces = Object.entries(config.workspaces).filter(([, ws]) => ws.account === accountName);
|
|
23
|
+
if (linkedWorkspaces.length > 0 && !opts.force) {
|
|
24
|
+
console.log((0, utils_js_1.red)(`Account "${accountName}" is used by these workspaces:`));
|
|
25
|
+
for (const [name, ws] of linkedWorkspaces) {
|
|
26
|
+
console.log(` - ${name} (${ws.path})`);
|
|
27
|
+
}
|
|
28
|
+
console.log((0, utils_js_1.yellow)("\nRemove the workspaces first, or use --force to remove everything."));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
// If --force, also remove linked workspaces
|
|
32
|
+
if (linkedWorkspaces.length > 0 && opts.force) {
|
|
33
|
+
for (const [name, ws] of linkedWorkspaces) {
|
|
34
|
+
(0, gitconfig_js_1.removeIncludeIf)(ws.path);
|
|
35
|
+
(0, gitconfig_js_1.removeWorkspaceGitconfig)((0, config_js_2.getWorkspaceGitconfigPath)(name));
|
|
36
|
+
delete config.workspaces[name];
|
|
37
|
+
console.log((0, utils_js_1.yellow)(`Removed workspace "${name}".`));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Remove SSH keys
|
|
41
|
+
(0, ssh_js_1.removeSshKey)(account.private_key);
|
|
42
|
+
// Remove from config
|
|
43
|
+
delete config.accounts[accountName];
|
|
44
|
+
(0, config_js_1.writeConfig)(config);
|
|
45
|
+
console.log((0, utils_js_1.green)(`Account "${accountName}" removed.`));
|
|
46
|
+
});
|
|
47
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerStatusCommand = registerStatusCommand;
|
|
4
|
+
const config_js_1 = require("../config.js");
|
|
5
|
+
const utils_js_1 = require("../utils.js");
|
|
6
|
+
function registerStatusCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("status")
|
|
9
|
+
.description("Show the current directory's workspace and account info")
|
|
10
|
+
.action(() => {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const match = (0, config_js_1.findWorkspaceByPath)(cwd);
|
|
13
|
+
if (!match) {
|
|
14
|
+
console.log((0, utils_js_1.yellow)("Not inside a zit workspace."));
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const { name, workspace } = match;
|
|
18
|
+
const config = (0, config_js_1.readConfig)();
|
|
19
|
+
const account = config.accounts[workspace.account];
|
|
20
|
+
console.log((0, utils_js_1.green)("Workspace: ") + name);
|
|
21
|
+
console.log((0, utils_js_1.green)("Path: ") + (0, utils_js_1.collapseHome)(workspace.path));
|
|
22
|
+
console.log((0, utils_js_1.green)("Account: ") + workspace.account);
|
|
23
|
+
if (account) {
|
|
24
|
+
console.log((0, utils_js_1.green)("Name: ") + account.name);
|
|
25
|
+
console.log((0, utils_js_1.green)("Email: ") + account.email);
|
|
26
|
+
console.log((0, utils_js_1.green)("SSH Key: ") + (0, utils_js_1.collapseHome)(account.private_key));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
console.log((0, utils_js_1.yellow)(`Warning: Account "${workspace.account}" not found in config.`));
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.registerWorkspaceCommand = registerWorkspaceCommand;
|
|
37
|
+
const fs = __importStar(require("node:fs"));
|
|
38
|
+
const config_js_1 = require("../config.js");
|
|
39
|
+
const gitconfig_js_1 = require("../gitconfig.js");
|
|
40
|
+
const utils_js_1 = require("../utils.js");
|
|
41
|
+
function registerWorkspaceCommand(program) {
|
|
42
|
+
const ws = program
|
|
43
|
+
.command("workspace")
|
|
44
|
+
.description("Manage workspace folders linked to accounts");
|
|
45
|
+
ws.command("init <path> <account>")
|
|
46
|
+
.description("Create a workspace folder linked to an account")
|
|
47
|
+
.action((wsPath, accountName) => {
|
|
48
|
+
const config = (0, config_js_1.readConfig)();
|
|
49
|
+
const account = config.accounts[accountName];
|
|
50
|
+
if (!account) {
|
|
51
|
+
console.log((0, utils_js_1.red)(`Account "${accountName}" not found.`));
|
|
52
|
+
console.log("Use `zit list` to see registered accounts.");
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
const resolvedPath = (0, utils_js_1.resolvePath)(wsPath);
|
|
56
|
+
// Check if a workspace already exists for this path
|
|
57
|
+
for (const [name, existing] of Object.entries(config.workspaces)) {
|
|
58
|
+
if ((0, utils_js_1.resolvePath)(existing.path) === resolvedPath) {
|
|
59
|
+
console.log((0, utils_js_1.yellow)(`Workspace "${name}" already exists for this path. Updating account to "${accountName}".`));
|
|
60
|
+
// Update existing workspace
|
|
61
|
+
existing.account = accountName;
|
|
62
|
+
const gitconfigPath = (0, config_js_1.getWorkspaceGitconfigPath)(name);
|
|
63
|
+
(0, gitconfig_js_1.writeWorkspaceGitconfig)(gitconfigPath, account.name, account.email, account.private_key);
|
|
64
|
+
(0, config_js_1.writeConfig)(config);
|
|
65
|
+
console.log((0, utils_js_1.green)(`Workspace "${name}" updated.`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// Create the folder
|
|
70
|
+
fs.mkdirSync(resolvedPath, { recursive: true });
|
|
71
|
+
// Derive workspace name from the folder name
|
|
72
|
+
const wsName = resolvedPath.split("/").filter(Boolean).pop();
|
|
73
|
+
// Check name collision
|
|
74
|
+
let finalName = wsName;
|
|
75
|
+
if (config.workspaces[finalName]) {
|
|
76
|
+
let i = 2;
|
|
77
|
+
while (config.workspaces[`${wsName}-${i}`])
|
|
78
|
+
i++;
|
|
79
|
+
finalName = `${wsName}-${i}`;
|
|
80
|
+
}
|
|
81
|
+
// Generate workspace gitconfig
|
|
82
|
+
const gitconfigPath = (0, config_js_1.getWorkspaceGitconfigPath)(finalName);
|
|
83
|
+
(0, gitconfig_js_1.writeWorkspaceGitconfig)(gitconfigPath, account.name, account.email, account.private_key);
|
|
84
|
+
// Add includeIf to ~/.gitconfig
|
|
85
|
+
(0, gitconfig_js_1.addIncludeIf)(resolvedPath, gitconfigPath);
|
|
86
|
+
// Save workspace to config
|
|
87
|
+
config.workspaces[finalName] = {
|
|
88
|
+
path: resolvedPath,
|
|
89
|
+
account: accountName,
|
|
90
|
+
};
|
|
91
|
+
(0, config_js_1.writeConfig)(config);
|
|
92
|
+
console.log((0, utils_js_1.green)(`Workspace "${finalName}" created.`));
|
|
93
|
+
console.log(` Path: ${(0, utils_js_1.collapseHome)(resolvedPath)}`);
|
|
94
|
+
console.log(` Account: ${accountName} (${account.email})`);
|
|
95
|
+
console.log(`\nRepos cloned inside ${(0, utils_js_1.collapseHome)(resolvedPath)} will use this identity automatically.`);
|
|
96
|
+
});
|
|
97
|
+
ws.command("list")
|
|
98
|
+
.description("List all workspaces and their linked accounts")
|
|
99
|
+
.action(() => {
|
|
100
|
+
const config = (0, config_js_1.readConfig)();
|
|
101
|
+
const workspaces = Object.entries(config.workspaces);
|
|
102
|
+
if (workspaces.length === 0) {
|
|
103
|
+
console.log((0, utils_js_1.yellow)("No workspaces configured. Use `zit workspace init <path> <account>` to create one."));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
console.log((0, utils_js_1.green)("Workspaces:\n"));
|
|
107
|
+
const maxNameLen = Math.max(...workspaces.map(([name]) => name.length));
|
|
108
|
+
const maxPathLen = Math.max(...workspaces.map(([, ws]) => (0, utils_js_1.collapseHome)(ws.path).length));
|
|
109
|
+
for (const [name, ws] of workspaces) {
|
|
110
|
+
console.log(` ${name.padEnd(maxNameLen + 2)} ${(0, utils_js_1.collapseHome)(ws.path).padEnd(maxPathLen + 2)} ${ws.account}`);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
ws.command("remove <name>")
|
|
114
|
+
.description("Unlink a workspace (keeps the folder and repos)")
|
|
115
|
+
.action((name) => {
|
|
116
|
+
const config = (0, config_js_1.readConfig)();
|
|
117
|
+
const workspace = config.workspaces[name];
|
|
118
|
+
if (!workspace) {
|
|
119
|
+
console.log((0, utils_js_1.red)(`Workspace "${name}" not found.`));
|
|
120
|
+
console.log("Use `zit workspace list` to see available workspaces.");
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
// Remove includeIf from ~/.gitconfig
|
|
124
|
+
(0, gitconfig_js_1.removeIncludeIf)(workspace.path);
|
|
125
|
+
// Remove workspace gitconfig file
|
|
126
|
+
(0, gitconfig_js_1.removeWorkspaceGitconfig)((0, config_js_1.getWorkspaceGitconfigPath)(name));
|
|
127
|
+
// Remove from config
|
|
128
|
+
delete config.workspaces[name];
|
|
129
|
+
(0, config_js_1.writeConfig)(config);
|
|
130
|
+
console.log((0, utils_js_1.green)(`Workspace "${name}" removed.`));
|
|
131
|
+
console.log((0, utils_js_1.yellow)("The folder and its repos still exist, but they no longer use the linked identity."));
|
|
132
|
+
});
|
|
133
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface Account {
|
|
2
|
+
name: string;
|
|
3
|
+
email: string;
|
|
4
|
+
private_key: string;
|
|
5
|
+
public_key: string;
|
|
6
|
+
}
|
|
7
|
+
export interface Workspace {
|
|
8
|
+
path: string;
|
|
9
|
+
account: string;
|
|
10
|
+
}
|
|
11
|
+
export interface ZitConfig {
|
|
12
|
+
accounts: Record<string, Account>;
|
|
13
|
+
workspaces: Record<string, Workspace>;
|
|
14
|
+
}
|
|
15
|
+
export declare function getConfigPath(): string;
|
|
16
|
+
export declare function getWorkspacesDir(): string;
|
|
17
|
+
export declare function getWorkspaceGitconfigPath(name: string): string;
|
|
18
|
+
export declare function readConfig(): ZitConfig;
|
|
19
|
+
export declare function writeConfig(config: ZitConfig): void;
|
|
20
|
+
export declare function getAccount(name: string): Account | undefined;
|
|
21
|
+
export declare function getWorkspace(name: string): Workspace | undefined;
|
|
22
|
+
export declare function findWorkspaceByPath(dir: string): {
|
|
23
|
+
name: string;
|
|
24
|
+
workspace: Workspace;
|
|
25
|
+
} | null;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getConfigPath = getConfigPath;
|
|
37
|
+
exports.getWorkspacesDir = getWorkspacesDir;
|
|
38
|
+
exports.getWorkspaceGitconfigPath = getWorkspaceGitconfigPath;
|
|
39
|
+
exports.readConfig = readConfig;
|
|
40
|
+
exports.writeConfig = writeConfig;
|
|
41
|
+
exports.getAccount = getAccount;
|
|
42
|
+
exports.getWorkspace = getWorkspace;
|
|
43
|
+
exports.findWorkspaceByPath = findWorkspaceByPath;
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const path = __importStar(require("node:path"));
|
|
46
|
+
const os = __importStar(require("node:os"));
|
|
47
|
+
const ini = __importStar(require("ini"));
|
|
48
|
+
function getConfigDir() {
|
|
49
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
50
|
+
const base = xdg || path.join(os.homedir(), ".config");
|
|
51
|
+
return path.join(base, "zit");
|
|
52
|
+
}
|
|
53
|
+
function getConfigPath() {
|
|
54
|
+
return path.join(getConfigDir(), "config");
|
|
55
|
+
}
|
|
56
|
+
function getWorkspacesDir() {
|
|
57
|
+
return path.join(getConfigDir(), "workspaces");
|
|
58
|
+
}
|
|
59
|
+
function getWorkspaceGitconfigPath(name) {
|
|
60
|
+
return path.join(getWorkspacesDir(), `${name}.gitconfig`);
|
|
61
|
+
}
|
|
62
|
+
function ensureConfigDir() {
|
|
63
|
+
const dir = getConfigDir();
|
|
64
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
65
|
+
fs.mkdirSync(getWorkspacesDir(), { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
function readConfig() {
|
|
68
|
+
const configPath = getConfigPath();
|
|
69
|
+
const config = { accounts: {}, workspaces: {} };
|
|
70
|
+
if (!fs.existsSync(configPath)) {
|
|
71
|
+
return config;
|
|
72
|
+
}
|
|
73
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
74
|
+
const parsed = ini.parse(raw);
|
|
75
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
76
|
+
if (typeof value === "object" &&
|
|
77
|
+
value !== null &&
|
|
78
|
+
"name" in value &&
|
|
79
|
+
"email" in value) {
|
|
80
|
+
if ("private_key" in value && "public_key" in value) {
|
|
81
|
+
config.accounts[key] = value;
|
|
82
|
+
}
|
|
83
|
+
else if ("path" in value && "account" in value) {
|
|
84
|
+
config.workspaces[key] = value;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
function writeConfig(config) {
|
|
91
|
+
ensureConfigDir();
|
|
92
|
+
const configPath = getConfigPath();
|
|
93
|
+
const obj = {};
|
|
94
|
+
for (const [key, acc] of Object.entries(config.accounts)) {
|
|
95
|
+
obj[key] = {
|
|
96
|
+
name: acc.name,
|
|
97
|
+
email: acc.email,
|
|
98
|
+
private_key: acc.private_key,
|
|
99
|
+
public_key: acc.public_key,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
for (const [key, ws] of Object.entries(config.workspaces)) {
|
|
103
|
+
obj[`workspace "${key}"`] = {
|
|
104
|
+
path: ws.path,
|
|
105
|
+
account: ws.account,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
fs.writeFileSync(configPath, ini.stringify(obj), "utf-8");
|
|
109
|
+
}
|
|
110
|
+
function getAccount(name) {
|
|
111
|
+
const config = readConfig();
|
|
112
|
+
return config.accounts[name];
|
|
113
|
+
}
|
|
114
|
+
function getWorkspace(name) {
|
|
115
|
+
const config = readConfig();
|
|
116
|
+
return config.workspaces[name];
|
|
117
|
+
}
|
|
118
|
+
function findWorkspaceByPath(dir) {
|
|
119
|
+
const config = readConfig();
|
|
120
|
+
const resolved = path.resolve(dir);
|
|
121
|
+
let bestMatch = null;
|
|
122
|
+
let bestLen = 0;
|
|
123
|
+
for (const [name, ws] of Object.entries(config.workspaces)) {
|
|
124
|
+
const wsPath = path.resolve(ws.path);
|
|
125
|
+
if ((resolved === wsPath || resolved.startsWith(wsPath + "/")) &&
|
|
126
|
+
wsPath.length > bestLen) {
|
|
127
|
+
bestMatch = { name, workspace: ws };
|
|
128
|
+
bestLen = wsPath.length;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return bestMatch;
|
|
132
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add an includeIf entry to ~/.gitconfig for a workspace.
|
|
3
|
+
* Format:
|
|
4
|
+
* [includeIf "gitdir:<path>/"]
|
|
5
|
+
* path = <gitconfig_path>
|
|
6
|
+
*/
|
|
7
|
+
export declare function addIncludeIf(workspacePath: string, gitconfigPath: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Remove an includeIf entry from ~/.gitconfig for a workspace.
|
|
10
|
+
*/
|
|
11
|
+
export declare function removeIncludeIf(workspacePath: string): void;
|
|
12
|
+
/**
|
|
13
|
+
* Write a per-workspace .gitconfig file with user identity and SSH command.
|
|
14
|
+
*/
|
|
15
|
+
export declare function writeWorkspaceGitconfig(gitconfigPath: string, userName: string, userEmail: string, privateKeyPath: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Remove a per-workspace .gitconfig file.
|
|
18
|
+
*/
|
|
19
|
+
export declare function removeWorkspaceGitconfig(gitconfigPath: string): void;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.addIncludeIf = addIncludeIf;
|
|
37
|
+
exports.removeIncludeIf = removeIncludeIf;
|
|
38
|
+
exports.writeWorkspaceGitconfig = writeWorkspaceGitconfig;
|
|
39
|
+
exports.removeWorkspaceGitconfig = removeWorkspaceGitconfig;
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const os = __importStar(require("node:os"));
|
|
43
|
+
const utils_js_1 = require("./utils.js");
|
|
44
|
+
function getGlobalGitconfigPath() {
|
|
45
|
+
return path.join(os.homedir(), ".gitconfig");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Add an includeIf entry to ~/.gitconfig for a workspace.
|
|
49
|
+
* Format:
|
|
50
|
+
* [includeIf "gitdir:<path>/"]
|
|
51
|
+
* path = <gitconfig_path>
|
|
52
|
+
*/
|
|
53
|
+
function addIncludeIf(workspacePath, gitconfigPath) {
|
|
54
|
+
const globalPath = getGlobalGitconfigPath();
|
|
55
|
+
const dirPattern = (0, utils_js_1.ensureTrailingSlash)(workspacePath);
|
|
56
|
+
let content = "";
|
|
57
|
+
if (fs.existsSync(globalPath)) {
|
|
58
|
+
content = fs.readFileSync(globalPath, "utf-8");
|
|
59
|
+
}
|
|
60
|
+
// Check if this includeIf already exists
|
|
61
|
+
const marker = `[includeIf "gitdir:${dirPattern}"]`;
|
|
62
|
+
if (content.includes(marker)) {
|
|
63
|
+
// Update the path in case it changed
|
|
64
|
+
const lines = content.split("\n");
|
|
65
|
+
const idx = lines.findIndex((l) => l.trim() === marker);
|
|
66
|
+
if (idx !== -1 && idx + 1 < lines.length) {
|
|
67
|
+
lines[idx + 1] = `\tpath = ${gitconfigPath}`;
|
|
68
|
+
}
|
|
69
|
+
fs.writeFileSync(globalPath, lines.join("\n"), "utf-8");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Append new entry
|
|
73
|
+
const entry = `\n${marker}\n\tpath = ${gitconfigPath}\n`;
|
|
74
|
+
fs.writeFileSync(globalPath, content + entry, "utf-8");
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Remove an includeIf entry from ~/.gitconfig for a workspace.
|
|
78
|
+
*/
|
|
79
|
+
function removeIncludeIf(workspacePath) {
|
|
80
|
+
const globalPath = getGlobalGitconfigPath();
|
|
81
|
+
if (!fs.existsSync(globalPath))
|
|
82
|
+
return;
|
|
83
|
+
const dirPattern = (0, utils_js_1.ensureTrailingSlash)(workspacePath);
|
|
84
|
+
const marker = `[includeIf "gitdir:${dirPattern}"]`;
|
|
85
|
+
const content = fs.readFileSync(globalPath, "utf-8");
|
|
86
|
+
if (!content.includes(marker))
|
|
87
|
+
return;
|
|
88
|
+
const lines = content.split("\n");
|
|
89
|
+
const result = [];
|
|
90
|
+
let i = 0;
|
|
91
|
+
while (i < lines.length) {
|
|
92
|
+
if (lines[i].trim() === marker) {
|
|
93
|
+
// Skip this line and any indented lines that follow (the section body)
|
|
94
|
+
i++;
|
|
95
|
+
while (i < lines.length && /^\s+/.test(lines[i]) && lines[i].trim()) {
|
|
96
|
+
i++;
|
|
97
|
+
}
|
|
98
|
+
// Skip trailing blank line
|
|
99
|
+
if (i < lines.length && lines[i].trim() === "") {
|
|
100
|
+
i++;
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
result.push(lines[i]);
|
|
105
|
+
i++;
|
|
106
|
+
}
|
|
107
|
+
fs.writeFileSync(globalPath, result.join("\n"), "utf-8");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Write a per-workspace .gitconfig file with user identity and SSH command.
|
|
111
|
+
*/
|
|
112
|
+
function writeWorkspaceGitconfig(gitconfigPath, userName, userEmail, privateKeyPath) {
|
|
113
|
+
const dir = path.dirname(gitconfigPath);
|
|
114
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
115
|
+
const content = `[user]
|
|
116
|
+
\tname = ${userName}
|
|
117
|
+
\temail = ${userEmail}
|
|
118
|
+
[core]
|
|
119
|
+
\tsshCommand = ssh -i ${privateKeyPath} -o IdentitiesOnly=yes
|
|
120
|
+
`;
|
|
121
|
+
fs.writeFileSync(gitconfigPath, content, "utf-8");
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Remove a per-workspace .gitconfig file.
|
|
125
|
+
*/
|
|
126
|
+
function removeWorkspaceGitconfig(gitconfigPath) {
|
|
127
|
+
if (fs.existsSync(gitconfigPath)) {
|
|
128
|
+
fs.unlinkSync(gitconfigPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/ssh.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type SshKeyType = "ed25519" | "rsa" | "ecdsa" | "ecdsa-sk" | "ed25519-sk" | "dsa";
|
|
2
|
+
export declare function isValidKeyType(t: string): t is SshKeyType;
|
|
3
|
+
export declare function getSshKeyPath(accountName: string, keyType: SshKeyType): {
|
|
4
|
+
privateKey: string;
|
|
5
|
+
publicKey: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function generateSshKey(email: string, privateKeyPath: string, keyType: SshKeyType): string;
|
|
8
|
+
export declare function removeSshKey(privateKeyPath: string): void;
|
package/dist/ssh.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.isValidKeyType = isValidKeyType;
|
|
37
|
+
exports.getSshKeyPath = getSshKeyPath;
|
|
38
|
+
exports.generateSshKey = generateSshKey;
|
|
39
|
+
exports.removeSshKey = removeSshKey;
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const os = __importStar(require("node:os"));
|
|
42
|
+
const fs = __importStar(require("node:fs"));
|
|
43
|
+
const node_child_process_1 = require("node:child_process");
|
|
44
|
+
const VALID_KEY_TYPES = [
|
|
45
|
+
"ed25519",
|
|
46
|
+
"rsa",
|
|
47
|
+
"ecdsa",
|
|
48
|
+
"ecdsa-sk",
|
|
49
|
+
"ed25519-sk",
|
|
50
|
+
"dsa",
|
|
51
|
+
];
|
|
52
|
+
function isValidKeyType(t) {
|
|
53
|
+
return VALID_KEY_TYPES.includes(t);
|
|
54
|
+
}
|
|
55
|
+
function getSshKeyPath(accountName, keyType) {
|
|
56
|
+
const sshDir = path.join(os.homedir(), ".ssh");
|
|
57
|
+
const base = path.join(sshDir, `id_${keyType}_${accountName}`);
|
|
58
|
+
return {
|
|
59
|
+
privateKey: base,
|
|
60
|
+
publicKey: base + ".pub",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function generateSshKey(email, privateKeyPath, keyType) {
|
|
64
|
+
const sshDir = path.dirname(privateKeyPath);
|
|
65
|
+
fs.mkdirSync(sshDir, { recursive: true });
|
|
66
|
+
(0, node_child_process_1.execSync)(`ssh-keygen -t ${keyType} -C "${email}" -f "${privateKeyPath}" -N ""`, { stdio: "inherit" });
|
|
67
|
+
const publicKey = fs.readFileSync(privateKeyPath + ".pub", "utf-8").trim();
|
|
68
|
+
return publicKey;
|
|
69
|
+
}
|
|
70
|
+
function removeSshKey(privateKeyPath) {
|
|
71
|
+
if (fs.existsSync(privateKeyPath)) {
|
|
72
|
+
fs.unlinkSync(privateKeyPath);
|
|
73
|
+
}
|
|
74
|
+
const pubPath = privateKeyPath + ".pub";
|
|
75
|
+
if (fs.existsSync(pubPath)) {
|
|
76
|
+
fs.unlinkSync(pubPath);
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function expandHome(p: string): string;
|
|
2
|
+
export declare function resolvePath(p: string): string;
|
|
3
|
+
/** Collapse home directory to ~ for display */
|
|
4
|
+
export declare function collapseHome(p: string): string;
|
|
5
|
+
export declare function ensureTrailingSlash(p: string): string;
|
|
6
|
+
export declare function red(s: string): string;
|
|
7
|
+
export declare function green(s: string): string;
|
|
8
|
+
export declare function yellow(s: string): string;
|
|
9
|
+
export declare function blue(s: string): string;
|
|
10
|
+
export declare function bold(s: string): string;
|
|
11
|
+
export declare function prompt(question: string): Promise<string>;
|
|
12
|
+
export declare function promptYesNo(question: string): Promise<boolean>;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.expandHome = expandHome;
|
|
37
|
+
exports.resolvePath = resolvePath;
|
|
38
|
+
exports.collapseHome = collapseHome;
|
|
39
|
+
exports.ensureTrailingSlash = ensureTrailingSlash;
|
|
40
|
+
exports.red = red;
|
|
41
|
+
exports.green = green;
|
|
42
|
+
exports.yellow = yellow;
|
|
43
|
+
exports.blue = blue;
|
|
44
|
+
exports.bold = bold;
|
|
45
|
+
exports.prompt = prompt;
|
|
46
|
+
exports.promptYesNo = promptYesNo;
|
|
47
|
+
const path = __importStar(require("node:path"));
|
|
48
|
+
const os = __importStar(require("node:os"));
|
|
49
|
+
const readline = __importStar(require("node:readline"));
|
|
50
|
+
function expandHome(p) {
|
|
51
|
+
if (p.startsWith("~/") || p === "~") {
|
|
52
|
+
return path.join(os.homedir(), p.slice(1));
|
|
53
|
+
}
|
|
54
|
+
return p;
|
|
55
|
+
}
|
|
56
|
+
function resolvePath(p) {
|
|
57
|
+
return path.resolve(expandHome(p));
|
|
58
|
+
}
|
|
59
|
+
/** Collapse home directory to ~ for display */
|
|
60
|
+
function collapseHome(p) {
|
|
61
|
+
const home = os.homedir();
|
|
62
|
+
if (p.startsWith(home + "/") || p === home) {
|
|
63
|
+
return "~" + p.slice(home.length);
|
|
64
|
+
}
|
|
65
|
+
return p;
|
|
66
|
+
}
|
|
67
|
+
function ensureTrailingSlash(p) {
|
|
68
|
+
return p.endsWith("/") ? p : p + "/";
|
|
69
|
+
}
|
|
70
|
+
const colors = {
|
|
71
|
+
red: "\x1b[31m",
|
|
72
|
+
green: "\x1b[32m",
|
|
73
|
+
yellow: "\x1b[33m",
|
|
74
|
+
blue: "\x1b[34m",
|
|
75
|
+
bold: "\x1b[1m",
|
|
76
|
+
reset: "\x1b[0m",
|
|
77
|
+
};
|
|
78
|
+
function red(s) {
|
|
79
|
+
return `${colors.red}${s}${colors.reset}`;
|
|
80
|
+
}
|
|
81
|
+
function green(s) {
|
|
82
|
+
return `${colors.green}${s}${colors.reset}`;
|
|
83
|
+
}
|
|
84
|
+
function yellow(s) {
|
|
85
|
+
return `${colors.yellow}${s}${colors.reset}`;
|
|
86
|
+
}
|
|
87
|
+
function blue(s) {
|
|
88
|
+
return `${colors.blue}${s}${colors.reset}`;
|
|
89
|
+
}
|
|
90
|
+
function bold(s) {
|
|
91
|
+
return `${colors.bold}${s}${colors.reset}`;
|
|
92
|
+
}
|
|
93
|
+
function prompt(question) {
|
|
94
|
+
const rl = readline.createInterface({
|
|
95
|
+
input: process.stdin,
|
|
96
|
+
output: process.stdout,
|
|
97
|
+
});
|
|
98
|
+
return new Promise((resolve) => {
|
|
99
|
+
rl.question(question, (answer) => {
|
|
100
|
+
rl.close();
|
|
101
|
+
resolve(answer.trim());
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
function promptYesNo(question) {
|
|
106
|
+
return prompt(`${yellow(question)} [y/n] `).then((answer) => {
|
|
107
|
+
const lower = answer.toLowerCase();
|
|
108
|
+
if (lower === "y" || lower === "yes")
|
|
109
|
+
return true;
|
|
110
|
+
if (lower === "n" || lower === "no")
|
|
111
|
+
return false;
|
|
112
|
+
console.log(red("Please answer y or n."));
|
|
113
|
+
return promptYesNo(question);
|
|
114
|
+
});
|
|
115
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hypercodingdev/zit",
|
|
3
|
+
"publishConfig": {
|
|
4
|
+
"access": "public"
|
|
5
|
+
},
|
|
6
|
+
"version": "1.0.0",
|
|
7
|
+
"description": "Folder-based Git identity manager. Associate workspace folders with Git accounts — SSH keys, author name, and email are applied automatically.",
|
|
8
|
+
"main": "dist/cli.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"zit": "dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"start": "node dist/cli.js",
|
|
16
|
+
"prepare": "tsc"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"git",
|
|
20
|
+
"ssh",
|
|
21
|
+
"identity",
|
|
22
|
+
"account",
|
|
23
|
+
"workspace",
|
|
24
|
+
"multi-account"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"commander": "^12.0.0",
|
|
35
|
+
"ini": "^4.1.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/ini": "^4.1.0",
|
|
39
|
+
"@types/node": "^20.0.0",
|
|
40
|
+
"typescript": "^5.4.0"
|
|
41
|
+
}
|
|
42
|
+
}
|