@fuad24/gitbridge 0.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/LICENSE +21 -0
- package/README.md +223 -0
- package/dist/bin/gb.js +3275 -0
- package/dist/bin/gitbridge.js +3275 -0
- package/dist/cli/commands/account.d.ts +3 -0
- package/dist/cli/commands/auth.d.ts +7 -0
- package/dist/cli/commands/context.d.ts +2 -0
- package/dist/cli/commands/credential.d.ts +18 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/enable.d.ts +3 -0
- package/dist/cli/commands/hook.d.ts +1 -0
- package/dist/cli/commands/identity.d.ts +11 -0
- package/dist/cli/commands/provider.d.ts +1 -0
- package/dist/cli/commands/push.d.ts +5 -0
- package/dist/cli/commands/remote.d.ts +5 -0
- package/dist/cli/commands/repo.d.ts +2 -0
- package/dist/cli/commands/rule.d.ts +8 -0
- package/dist/cli/commands/setup.d.ts +2 -0
- package/dist/cli/commands/status.d.ts +2 -0
- package/dist/cli/commands/switch.d.ts +4 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/ui/banners.d.ts +2 -0
- package/dist/cli/ui/prompts.d.ts +31 -0
- package/dist/cli/ui/tables.d.ts +6 -0
- package/dist/core/config/config-store.d.ts +56 -0
- package/dist/core/config/path-resolver.d.ts +20 -0
- package/dist/core/config/schema.d.ts +461 -0
- package/dist/core/git/config-generator.d.ts +12 -0
- package/dist/core/git/git-cli.d.ts +32 -0
- package/dist/core/git/gitconfig-injector.d.ts +14 -0
- package/dist/core/git/url-parser.d.ts +16 -0
- package/dist/core/identity/identity-resolver.d.ts +23 -0
- package/dist/core/providers/bitbucket.provider.d.ts +12 -0
- package/dist/core/providers/github.provider.d.ts +15 -0
- package/dist/core/providers/gitlab.provider.d.ts +12 -0
- package/dist/core/providers/provider-registry.d.ts +11 -0
- package/dist/core/providers/provider.interface.d.ts +50 -0
- package/dist/core/safety/identity-guard.d.ts +18 -0
- package/dist/core/ssh/ssh-config-generator.d.ts +6 -0
- package/dist/core/ssh/ssh-injector.d.ts +14 -0
- package/dist/core/ssh/ssh-key-detector.d.ts +10 -0
- package/dist/core/storage/credential-store.d.ts +7 -0
- package/dist/core/storage/encrypted-vault.d.ts +16 -0
- package/dist/core/storage/linux-keyring.d.ts +8 -0
- package/dist/core/storage/macos-keychain.d.ts +8 -0
- package/dist/core/storage/store-factory.d.ts +5 -0
- package/dist/core/storage/windows-cred.d.ts +9 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +3356 -0
- package/dist/index.js.map +7 -0
- package/dist/utils/errors.d.ts +22 -0
- package/dist/utils/http.d.ts +10 -0
- package/dist/utils/logger.d.ts +15 -0
- package/dist/utils/platform.d.ts +8 -0
- package/package.json +62 -0
package/dist/bin/gb.js
ADDED
|
@@ -0,0 +1,3275 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/cli/commands/status.ts
|
|
30
|
+
var import_picocolors3 = __toESM(require("picocolors"));
|
|
31
|
+
|
|
32
|
+
// src/core/config/config-store.ts
|
|
33
|
+
var import_node_fs = __toESM(require("node:fs"));
|
|
34
|
+
var import_node_path3 = __toESM(require("node:path"));
|
|
35
|
+
|
|
36
|
+
// src/core/config/path-resolver.ts
|
|
37
|
+
var import_node_path2 = __toESM(require("node:path"));
|
|
38
|
+
|
|
39
|
+
// src/utils/platform.ts
|
|
40
|
+
var import_node_os = __toESM(require("node:os"));
|
|
41
|
+
var import_node_path = __toESM(require("node:path"));
|
|
42
|
+
function getPlatform() {
|
|
43
|
+
const p2 = process.platform;
|
|
44
|
+
if (p2 === "linux" || p2 === "darwin" || p2 === "win32") {
|
|
45
|
+
return p2;
|
|
46
|
+
}
|
|
47
|
+
return "unknown";
|
|
48
|
+
}
|
|
49
|
+
function getHomeDir() {
|
|
50
|
+
return import_node_os.default.homedir();
|
|
51
|
+
}
|
|
52
|
+
function expandTilde(filepath) {
|
|
53
|
+
if (!filepath) return filepath;
|
|
54
|
+
if (filepath.startsWith("~/") || filepath === "~") {
|
|
55
|
+
return import_node_path.default.join(getHomeDir(), filepath.slice(1));
|
|
56
|
+
}
|
|
57
|
+
return filepath;
|
|
58
|
+
}
|
|
59
|
+
function collapseTilde(filepath) {
|
|
60
|
+
if (!filepath) return filepath;
|
|
61
|
+
const home = getHomeDir();
|
|
62
|
+
if (filepath === home) return "~";
|
|
63
|
+
if (filepath.startsWith(home + import_node_path.default.sep)) {
|
|
64
|
+
return `~${filepath.slice(home.length)}`;
|
|
65
|
+
}
|
|
66
|
+
return filepath;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/core/config/path-resolver.ts
|
|
70
|
+
var PathResolver = class {
|
|
71
|
+
baseDir;
|
|
72
|
+
constructor(customBaseDir) {
|
|
73
|
+
if (customBaseDir) {
|
|
74
|
+
this.baseDir = expandTilde(customBaseDir);
|
|
75
|
+
} else if (process.env.GITBRIDGE_HOME) {
|
|
76
|
+
this.baseDir = expandTilde(process.env.GITBRIDGE_HOME);
|
|
77
|
+
} else if (process.env.XDG_CONFIG_HOME) {
|
|
78
|
+
this.baseDir = import_node_path2.default.join(expandTilde(process.env.XDG_CONFIG_HOME), "gitbridge");
|
|
79
|
+
} else {
|
|
80
|
+
this.baseDir = import_node_path2.default.join(getHomeDir(), ".gitbridge");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
getBaseDir() {
|
|
84
|
+
return this.baseDir;
|
|
85
|
+
}
|
|
86
|
+
getConfigFile() {
|
|
87
|
+
return import_node_path2.default.join(this.baseDir, "config.json");
|
|
88
|
+
}
|
|
89
|
+
getIdentitiesFile() {
|
|
90
|
+
return import_node_path2.default.join(this.baseDir, "identities.json");
|
|
91
|
+
}
|
|
92
|
+
getAccountsFile() {
|
|
93
|
+
return import_node_path2.default.join(this.baseDir, "accounts.json");
|
|
94
|
+
}
|
|
95
|
+
getReposFile() {
|
|
96
|
+
return import_node_path2.default.join(this.baseDir, "repos.json");
|
|
97
|
+
}
|
|
98
|
+
getGeneratedDir() {
|
|
99
|
+
return import_node_path2.default.join(this.baseDir, "generated");
|
|
100
|
+
}
|
|
101
|
+
getMainGitConfigFile() {
|
|
102
|
+
return import_node_path2.default.join(this.getGeneratedDir(), "main.gitconfig");
|
|
103
|
+
}
|
|
104
|
+
getGeneratedSshConfigFile() {
|
|
105
|
+
return import_node_path2.default.join(this.getGeneratedDir(), "ssh_config");
|
|
106
|
+
}
|
|
107
|
+
getRulesDir() {
|
|
108
|
+
return import_node_path2.default.join(this.getGeneratedDir(), "rules");
|
|
109
|
+
}
|
|
110
|
+
getRuleGitConfigFile(ruleId) {
|
|
111
|
+
const sanitized = ruleId.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
112
|
+
return import_node_path2.default.join(this.getRulesDir(), `${sanitized}.gitconfig`);
|
|
113
|
+
}
|
|
114
|
+
getBackupsDir() {
|
|
115
|
+
return import_node_path2.default.join(this.baseDir, "backups");
|
|
116
|
+
}
|
|
117
|
+
getEncryptedVaultFile() {
|
|
118
|
+
return import_node_path2.default.join(this.baseDir, "vault.enc");
|
|
119
|
+
}
|
|
120
|
+
getUserGitConfigFile() {
|
|
121
|
+
return import_node_path2.default.join(getHomeDir(), ".gitconfig");
|
|
122
|
+
}
|
|
123
|
+
getUserSshConfigFile() {
|
|
124
|
+
return import_node_path2.default.join(getHomeDir(), ".ssh", "config");
|
|
125
|
+
}
|
|
126
|
+
getUserSshDir() {
|
|
127
|
+
return import_node_path2.default.join(getHomeDir(), ".ssh");
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
var defaultPathResolver = new PathResolver();
|
|
131
|
+
|
|
132
|
+
// src/core/config/schema.ts
|
|
133
|
+
var import_zod = require("zod");
|
|
134
|
+
var GitProviderTypeSchema = import_zod.z.enum(["github", "gitlab", "bitbucket", "gitea", "custom"]);
|
|
135
|
+
var AuthTypeSchema = import_zod.z.enum(["oauth", "pat", "ssh", "password"]);
|
|
136
|
+
var GitIdentitySchema = import_zod.z.object({
|
|
137
|
+
id: import_zod.z.string().min(1),
|
|
138
|
+
name: import_zod.z.string().min(1),
|
|
139
|
+
email: import_zod.z.string().email(),
|
|
140
|
+
signingKey: import_zod.z.string().nullable().optional().default(null),
|
|
141
|
+
isDefault: import_zod.z.boolean().optional().default(false),
|
|
142
|
+
createdAt: import_zod.z.string().datetime().optional().default(() => (/* @__PURE__ */ new Date()).toISOString())
|
|
143
|
+
});
|
|
144
|
+
var IdentitiesFileSchema = import_zod.z.object({
|
|
145
|
+
identities: import_zod.z.array(GitIdentitySchema).default([])
|
|
146
|
+
});
|
|
147
|
+
var ProviderAccountSchema = import_zod.z.object({
|
|
148
|
+
id: import_zod.z.string().min(1),
|
|
149
|
+
providerId: GitProviderTypeSchema,
|
|
150
|
+
host: import_zod.z.string().min(1),
|
|
151
|
+
username: import_zod.z.string().min(1),
|
|
152
|
+
displayName: import_zod.z.string().optional(),
|
|
153
|
+
authType: AuthTypeSchema,
|
|
154
|
+
sshKeyPath: import_zod.z.string().optional(),
|
|
155
|
+
createdAt: import_zod.z.string().datetime().optional().default(() => (/* @__PURE__ */ new Date()).toISOString())
|
|
156
|
+
});
|
|
157
|
+
var AccountsFileSchema = import_zod.z.object({
|
|
158
|
+
accounts: import_zod.z.array(ProviderAccountSchema).default([])
|
|
159
|
+
});
|
|
160
|
+
var DirectoryRuleSchema = import_zod.z.object({
|
|
161
|
+
id: import_zod.z.string().min(1),
|
|
162
|
+
path: import_zod.z.string().min(1),
|
|
163
|
+
identityId: import_zod.z.string().min(1),
|
|
164
|
+
defaultProvider: GitProviderTypeSchema.optional(),
|
|
165
|
+
defaultAccountId: import_zod.z.string().optional()
|
|
166
|
+
});
|
|
167
|
+
var GitBridgeSettingsSchema = import_zod.z.object({
|
|
168
|
+
autoSyncGitConfig: import_zod.z.boolean().default(true),
|
|
169
|
+
credentialHelperEnabled: import_zod.z.boolean().default(true),
|
|
170
|
+
sshManagementEnabled: import_zod.z.boolean().default(true),
|
|
171
|
+
commitIdentitySafety: import_zod.z.boolean().default(true),
|
|
172
|
+
fallbackEncryptedStore: import_zod.z.boolean().default(false)
|
|
173
|
+
});
|
|
174
|
+
var MainConfigSchema = import_zod.z.object({
|
|
175
|
+
$schema: import_zod.z.string().optional(),
|
|
176
|
+
version: import_zod.z.string().default("1.0.0"),
|
|
177
|
+
enabled: import_zod.z.boolean().default(true),
|
|
178
|
+
defaultIdentityId: import_zod.z.string().nullable().optional().default(null),
|
|
179
|
+
rules: import_zod.z.array(DirectoryRuleSchema).default([]),
|
|
180
|
+
settings: GitBridgeSettingsSchema.default({})
|
|
181
|
+
});
|
|
182
|
+
var RepositoryRemoteSchema = import_zod.z.object({
|
|
183
|
+
name: import_zod.z.string().min(1),
|
|
184
|
+
providerId: GitProviderTypeSchema,
|
|
185
|
+
host: import_zod.z.string().min(1),
|
|
186
|
+
accountId: import_zod.z.string().optional(),
|
|
187
|
+
url: import_zod.z.string().min(1),
|
|
188
|
+
rawUrl: import_zod.z.string().optional()
|
|
189
|
+
});
|
|
190
|
+
var RepositoryProfileSchema = import_zod.z.object({
|
|
191
|
+
path: import_zod.z.string().min(1),
|
|
192
|
+
identityId: import_zod.z.string().optional(),
|
|
193
|
+
remotes: import_zod.z.array(RepositoryRemoteSchema).default([]),
|
|
194
|
+
safetyHookInstalled: import_zod.z.boolean().optional().default(false),
|
|
195
|
+
updatedAt: import_zod.z.string().datetime().optional().default(() => (/* @__PURE__ */ new Date()).toISOString())
|
|
196
|
+
});
|
|
197
|
+
var RepositoriesFileSchema = import_zod.z.object({
|
|
198
|
+
repositories: import_zod.z.array(RepositoryProfileSchema).default([])
|
|
199
|
+
});
|
|
200
|
+
var CustomProviderSchema = import_zod.z.object({
|
|
201
|
+
id: import_zod.z.string().min(1),
|
|
202
|
+
name: import_zod.z.string().min(1),
|
|
203
|
+
host: import_zod.z.string().min(1),
|
|
204
|
+
type: GitProviderTypeSchema.default("custom"),
|
|
205
|
+
apiBaseUrl: import_zod.z.string().url().optional()
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// src/utils/errors.ts
|
|
209
|
+
var GitBridgeError = class extends Error {
|
|
210
|
+
constructor(message, code) {
|
|
211
|
+
super(message);
|
|
212
|
+
this.code = code;
|
|
213
|
+
this.name = "GitBridgeError";
|
|
214
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
215
|
+
}
|
|
216
|
+
code;
|
|
217
|
+
};
|
|
218
|
+
var ConfigError = class extends GitBridgeError {
|
|
219
|
+
constructor(message) {
|
|
220
|
+
super(message, "CONFIG_ERROR");
|
|
221
|
+
this.name = "ConfigError";
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
var ProviderError = class extends GitBridgeError {
|
|
225
|
+
constructor(message, provider) {
|
|
226
|
+
super(`[${provider}] ${message}`, "PROVIDER_ERROR");
|
|
227
|
+
this.provider = provider;
|
|
228
|
+
this.name = "ProviderError";
|
|
229
|
+
}
|
|
230
|
+
provider;
|
|
231
|
+
};
|
|
232
|
+
var GitCliError = class extends GitBridgeError {
|
|
233
|
+
constructor(message, exitCode, stderr) {
|
|
234
|
+
super(`Git error (${exitCode}): ${message}`, "GIT_CLI_ERROR");
|
|
235
|
+
this.exitCode = exitCode;
|
|
236
|
+
this.stderr = stderr;
|
|
237
|
+
this.name = "GitCliError";
|
|
238
|
+
}
|
|
239
|
+
exitCode;
|
|
240
|
+
stderr;
|
|
241
|
+
};
|
|
242
|
+
var CredentialStoreError = class extends GitBridgeError {
|
|
243
|
+
constructor(message) {
|
|
244
|
+
super(message, "CREDENTIAL_STORE_ERROR");
|
|
245
|
+
this.name = "CredentialStoreError";
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// src/core/config/config-store.ts
|
|
250
|
+
var ConfigStore = class {
|
|
251
|
+
paths;
|
|
252
|
+
constructor(paths = defaultPathResolver) {
|
|
253
|
+
this.paths = paths;
|
|
254
|
+
}
|
|
255
|
+
getPathResolver() {
|
|
256
|
+
return this.paths;
|
|
257
|
+
}
|
|
258
|
+
ensureDirectories() {
|
|
259
|
+
const dirs = [
|
|
260
|
+
this.paths.getBaseDir(),
|
|
261
|
+
this.paths.getGeneratedDir(),
|
|
262
|
+
this.paths.getRulesDir(),
|
|
263
|
+
this.paths.getBackupsDir()
|
|
264
|
+
];
|
|
265
|
+
for (const dir of dirs) {
|
|
266
|
+
if (!import_node_fs.default.existsSync(dir)) {
|
|
267
|
+
import_node_fs.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
atomicWriteJson(filepath, data) {
|
|
272
|
+
this.ensureDirectories();
|
|
273
|
+
const tempFile = `${filepath}.tmp.${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
|
|
274
|
+
const content = JSON.stringify(data, null, 2);
|
|
275
|
+
import_node_fs.default.writeFileSync(tempFile, content, { encoding: "utf-8", mode: 384 });
|
|
276
|
+
import_node_fs.default.renameSync(tempFile, filepath);
|
|
277
|
+
}
|
|
278
|
+
// --- Main Config ---
|
|
279
|
+
loadConfig() {
|
|
280
|
+
const file = this.paths.getConfigFile();
|
|
281
|
+
if (!import_node_fs.default.existsSync(file)) {
|
|
282
|
+
return MainConfigSchema.parse({});
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
const raw = JSON.parse(import_node_fs.default.readFileSync(file, "utf-8"));
|
|
286
|
+
return MainConfigSchema.parse(raw);
|
|
287
|
+
} catch (err) {
|
|
288
|
+
throw new ConfigError(`Failed to load config from ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
saveConfig(config) {
|
|
292
|
+
const current = this.loadConfig();
|
|
293
|
+
const merged = MainConfigSchema.parse({ ...current, ...config });
|
|
294
|
+
this.atomicWriteJson(this.paths.getConfigFile(), merged);
|
|
295
|
+
return merged;
|
|
296
|
+
}
|
|
297
|
+
updateSettings(settings) {
|
|
298
|
+
const current = this.loadConfig();
|
|
299
|
+
const updated = {
|
|
300
|
+
...current,
|
|
301
|
+
settings: { ...current.settings, ...settings }
|
|
302
|
+
};
|
|
303
|
+
return this.saveConfig(updated);
|
|
304
|
+
}
|
|
305
|
+
setEnabled(enabled) {
|
|
306
|
+
return this.saveConfig({ enabled });
|
|
307
|
+
}
|
|
308
|
+
// --- Identities ---
|
|
309
|
+
loadIdentities() {
|
|
310
|
+
const file = this.paths.getIdentitiesFile();
|
|
311
|
+
if (!import_node_fs.default.existsSync(file)) {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
const raw = JSON.parse(import_node_fs.default.readFileSync(file, "utf-8"));
|
|
316
|
+
const parsed = IdentitiesFileSchema.parse(raw);
|
|
317
|
+
return parsed.identities;
|
|
318
|
+
} catch (err) {
|
|
319
|
+
throw new ConfigError(`Failed to load identities from ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
saveIdentities(identities) {
|
|
323
|
+
const validated = IdentitiesFileSchema.parse({ identities });
|
|
324
|
+
this.atomicWriteJson(this.paths.getIdentitiesFile(), validated);
|
|
325
|
+
}
|
|
326
|
+
getIdentity(id) {
|
|
327
|
+
const list = this.loadIdentities();
|
|
328
|
+
return list.find((i) => i.id === id);
|
|
329
|
+
}
|
|
330
|
+
addIdentity(identity) {
|
|
331
|
+
const list = this.loadIdentities();
|
|
332
|
+
if (list.some((i) => i.id === identity.id)) {
|
|
333
|
+
throw new ConfigError(`Identity with ID '${identity.id}' already exists.`);
|
|
334
|
+
}
|
|
335
|
+
const isFirst = list.length === 0;
|
|
336
|
+
const newIdentity = {
|
|
337
|
+
id: identity.id,
|
|
338
|
+
name: identity.name,
|
|
339
|
+
email: identity.email,
|
|
340
|
+
signingKey: identity.signingKey || null,
|
|
341
|
+
isDefault: identity.isDefault ?? isFirst,
|
|
342
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
343
|
+
};
|
|
344
|
+
if (newIdentity.isDefault) {
|
|
345
|
+
list.forEach((i) => {
|
|
346
|
+
i.isDefault = false;
|
|
347
|
+
});
|
|
348
|
+
this.saveConfig({ defaultIdentityId: newIdentity.id });
|
|
349
|
+
}
|
|
350
|
+
list.push(newIdentity);
|
|
351
|
+
this.saveIdentities(list);
|
|
352
|
+
return newIdentity;
|
|
353
|
+
}
|
|
354
|
+
updateIdentity(id, updates) {
|
|
355
|
+
const list = this.loadIdentities();
|
|
356
|
+
const index = list.findIndex((i) => i.id === id);
|
|
357
|
+
if (index === -1) {
|
|
358
|
+
throw new ConfigError(`Identity with ID '${id}' not found.`);
|
|
359
|
+
}
|
|
360
|
+
if (updates.isDefault) {
|
|
361
|
+
list.forEach((i) => {
|
|
362
|
+
i.isDefault = false;
|
|
363
|
+
});
|
|
364
|
+
this.saveConfig({ defaultIdentityId: id });
|
|
365
|
+
}
|
|
366
|
+
const updated = {
|
|
367
|
+
...list[index],
|
|
368
|
+
...updates,
|
|
369
|
+
signingKey: updates.signingKey !== void 0 ? updates.signingKey : list[index].signingKey
|
|
370
|
+
};
|
|
371
|
+
list[index] = updated;
|
|
372
|
+
this.saveIdentities(list);
|
|
373
|
+
return updated;
|
|
374
|
+
}
|
|
375
|
+
removeIdentity(id) {
|
|
376
|
+
const list = this.loadIdentities();
|
|
377
|
+
const filtered = list.filter((i) => i.id !== id);
|
|
378
|
+
if (filtered.length === list.length) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
const config = this.loadConfig();
|
|
382
|
+
if (config.defaultIdentityId === id) {
|
|
383
|
+
const nextDefault = filtered[0]?.id ?? null;
|
|
384
|
+
if (filtered[0]) filtered[0].isDefault = true;
|
|
385
|
+
this.saveConfig({ defaultIdentityId: nextDefault });
|
|
386
|
+
}
|
|
387
|
+
this.saveIdentities(filtered);
|
|
388
|
+
return true;
|
|
389
|
+
}
|
|
390
|
+
setDefaultIdentity(id) {
|
|
391
|
+
const list = this.loadIdentities();
|
|
392
|
+
const target = list.find((i) => i.id === id);
|
|
393
|
+
if (!target) {
|
|
394
|
+
throw new ConfigError(`Identity with ID '${id}' not found.`);
|
|
395
|
+
}
|
|
396
|
+
list.forEach((i) => {
|
|
397
|
+
i.isDefault = i.id === id;
|
|
398
|
+
});
|
|
399
|
+
this.saveIdentities(list);
|
|
400
|
+
this.saveConfig({ defaultIdentityId: id });
|
|
401
|
+
}
|
|
402
|
+
// --- Accounts ---
|
|
403
|
+
loadAccounts() {
|
|
404
|
+
const file = this.paths.getAccountsFile();
|
|
405
|
+
if (!import_node_fs.default.existsSync(file)) {
|
|
406
|
+
return [];
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const raw = JSON.parse(import_node_fs.default.readFileSync(file, "utf-8"));
|
|
410
|
+
const parsed = AccountsFileSchema.parse(raw);
|
|
411
|
+
return parsed.accounts;
|
|
412
|
+
} catch (err) {
|
|
413
|
+
throw new ConfigError(`Failed to load accounts from ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
saveAccounts(accounts) {
|
|
417
|
+
const validated = AccountsFileSchema.parse({ accounts });
|
|
418
|
+
this.atomicWriteJson(this.paths.getAccountsFile(), validated);
|
|
419
|
+
}
|
|
420
|
+
getAccount(id) {
|
|
421
|
+
return this.loadAccounts().find((a) => a.id === id);
|
|
422
|
+
}
|
|
423
|
+
addAccount(account) {
|
|
424
|
+
const list = this.loadAccounts();
|
|
425
|
+
const existingIndex = list.findIndex((a) => a.id === account.id);
|
|
426
|
+
const newAccount = {
|
|
427
|
+
...account,
|
|
428
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
429
|
+
};
|
|
430
|
+
if (existingIndex >= 0) {
|
|
431
|
+
list[existingIndex] = newAccount;
|
|
432
|
+
} else {
|
|
433
|
+
list.push(newAccount);
|
|
434
|
+
}
|
|
435
|
+
this.saveAccounts(list);
|
|
436
|
+
return newAccount;
|
|
437
|
+
}
|
|
438
|
+
removeAccount(id) {
|
|
439
|
+
const list = this.loadAccounts();
|
|
440
|
+
const filtered = list.filter((a) => a.id !== id);
|
|
441
|
+
if (filtered.length === list.length) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
this.saveAccounts(filtered);
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
// --- Directory Rules ---
|
|
448
|
+
loadRules() {
|
|
449
|
+
return this.loadConfig().rules;
|
|
450
|
+
}
|
|
451
|
+
addRule(rule) {
|
|
452
|
+
const config = this.loadConfig();
|
|
453
|
+
const existingIndex = config.rules.findIndex((r) => r.id === rule.id || r.path === rule.path);
|
|
454
|
+
if (existingIndex >= 0) {
|
|
455
|
+
config.rules[existingIndex] = rule;
|
|
456
|
+
} else {
|
|
457
|
+
config.rules.push(rule);
|
|
458
|
+
}
|
|
459
|
+
this.saveConfig(config);
|
|
460
|
+
return rule;
|
|
461
|
+
}
|
|
462
|
+
removeRule(ruleIdOrPath) {
|
|
463
|
+
const config = this.loadConfig();
|
|
464
|
+
const initialLen = config.rules.length;
|
|
465
|
+
config.rules = config.rules.filter((r) => r.id !== ruleIdOrPath && r.path !== ruleIdOrPath);
|
|
466
|
+
if (config.rules.length === initialLen) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
this.saveConfig(config);
|
|
470
|
+
return true;
|
|
471
|
+
}
|
|
472
|
+
// --- Repository Profiles ---
|
|
473
|
+
loadRepositories() {
|
|
474
|
+
const file = this.paths.getReposFile();
|
|
475
|
+
if (!import_node_fs.default.existsSync(file)) {
|
|
476
|
+
return [];
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const raw = JSON.parse(import_node_fs.default.readFileSync(file, "utf-8"));
|
|
480
|
+
const parsed = RepositoriesFileSchema.parse(raw);
|
|
481
|
+
return parsed.repositories;
|
|
482
|
+
} catch (err) {
|
|
483
|
+
throw new ConfigError(`Failed to load repositories from ${file}: ${err instanceof Error ? err.message : String(err)}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
saveRepositories(repositories) {
|
|
487
|
+
const validated = RepositoriesFileSchema.parse({ repositories });
|
|
488
|
+
this.atomicWriteJson(this.paths.getReposFile(), validated);
|
|
489
|
+
}
|
|
490
|
+
getRepository(repoPath) {
|
|
491
|
+
const normalized = import_node_path3.default.resolve(repoPath);
|
|
492
|
+
return this.loadRepositories().find((r) => import_node_path3.default.resolve(r.path) === normalized);
|
|
493
|
+
}
|
|
494
|
+
saveRepositoryProfile(profile) {
|
|
495
|
+
const list = this.loadRepositories();
|
|
496
|
+
const normalized = import_node_path3.default.resolve(profile.path);
|
|
497
|
+
const existingIndex = list.findIndex((r) => import_node_path3.default.resolve(r.path) === normalized);
|
|
498
|
+
const updatedProfile = {
|
|
499
|
+
path: normalized,
|
|
500
|
+
identityId: profile.identityId,
|
|
501
|
+
remotes: profile.remotes || [],
|
|
502
|
+
safetyHookInstalled: profile.safetyHookInstalled || false,
|
|
503
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
504
|
+
};
|
|
505
|
+
if (existingIndex >= 0) {
|
|
506
|
+
list[existingIndex] = updatedProfile;
|
|
507
|
+
} else {
|
|
508
|
+
list.push(updatedProfile);
|
|
509
|
+
}
|
|
510
|
+
this.saveRepositories(list);
|
|
511
|
+
return updatedProfile;
|
|
512
|
+
}
|
|
513
|
+
removeRepositoryProfile(repoPath) {
|
|
514
|
+
const list = this.loadRepositories();
|
|
515
|
+
const normalized = import_node_path3.default.resolve(repoPath);
|
|
516
|
+
const filtered = list.filter((r) => import_node_path3.default.resolve(r.path) !== normalized);
|
|
517
|
+
if (filtered.length === list.length) {
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
this.saveRepositories(filtered);
|
|
521
|
+
return true;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
var defaultConfigStore = new ConfigStore();
|
|
525
|
+
|
|
526
|
+
// src/core/git/gitconfig-injector.ts
|
|
527
|
+
var import_node_fs3 = __toESM(require("node:fs"));
|
|
528
|
+
var import_node_path4 = __toESM(require("node:path"));
|
|
529
|
+
|
|
530
|
+
// src/core/git/config-generator.ts
|
|
531
|
+
var import_node_fs2 = __toESM(require("node:fs"));
|
|
532
|
+
var GitConfigGenerator = class {
|
|
533
|
+
store;
|
|
534
|
+
constructor(store) {
|
|
535
|
+
this.store = store;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Generates all Git configuration files into ~/.gitbridge/generated/
|
|
539
|
+
*/
|
|
540
|
+
generate() {
|
|
541
|
+
this.store.ensureDirectories();
|
|
542
|
+
const config = this.store.loadConfig();
|
|
543
|
+
const identities = this.store.loadIdentities();
|
|
544
|
+
const accounts = this.store.loadAccounts();
|
|
545
|
+
const paths = this.store.getPathResolver();
|
|
546
|
+
const defaultIdentity = identities.find((i) => i.id === config.defaultIdentityId) || identities.find((i) => i.isDefault) || identities[0];
|
|
547
|
+
const ruleFiles = [];
|
|
548
|
+
for (const rule of config.rules) {
|
|
549
|
+
const ruleIdentity = identities.find((i) => i.id === rule.identityId);
|
|
550
|
+
const ruleFile = paths.getRuleGitConfigFile(rule.id);
|
|
551
|
+
let ruleContent = `# Managed by GitBridge for rule: ${rule.id}
|
|
552
|
+
`;
|
|
553
|
+
if (ruleIdentity) {
|
|
554
|
+
ruleContent += `[user]
|
|
555
|
+
`;
|
|
556
|
+
ruleContent += ` name = ${ruleIdentity.name}
|
|
557
|
+
`;
|
|
558
|
+
ruleContent += ` email = ${ruleIdentity.email}
|
|
559
|
+
`;
|
|
560
|
+
if (ruleIdentity.signingKey) {
|
|
561
|
+
ruleContent += ` signingkey = ${ruleIdentity.signingKey}
|
|
562
|
+
`;
|
|
563
|
+
ruleContent += ` # format = ssh or gpg
|
|
564
|
+
`;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (rule.defaultAccountId) {
|
|
568
|
+
const account = accounts.find((a) => a.id === rule.defaultAccountId);
|
|
569
|
+
if (account && account.host) {
|
|
570
|
+
const aliasHost = `${account.host}-${account.id}`;
|
|
571
|
+
ruleContent += `
|
|
572
|
+
[url "git@${aliasHost}:"]
|
|
573
|
+
`;
|
|
574
|
+
ruleContent += ` insteadOf = git@${account.host}:
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
import_node_fs2.default.writeFileSync(ruleFile, ruleContent, { encoding: "utf-8", mode: 420 });
|
|
579
|
+
ruleFiles.push(ruleFile);
|
|
580
|
+
}
|
|
581
|
+
let mainContent = `# Auto-generated by GitBridge. DO NOT EDIT DIRECTLY.
|
|
582
|
+
`;
|
|
583
|
+
mainContent += `# Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
584
|
+
|
|
585
|
+
`;
|
|
586
|
+
if (defaultIdentity) {
|
|
587
|
+
mainContent += `# Default Identity
|
|
588
|
+
`;
|
|
589
|
+
mainContent += `[user]
|
|
590
|
+
`;
|
|
591
|
+
mainContent += ` name = ${defaultIdentity.name}
|
|
592
|
+
`;
|
|
593
|
+
mainContent += ` email = ${defaultIdentity.email}
|
|
594
|
+
`;
|
|
595
|
+
if (defaultIdentity.signingKey) {
|
|
596
|
+
mainContent += ` signingkey = ${defaultIdentity.signingKey}
|
|
597
|
+
`;
|
|
598
|
+
}
|
|
599
|
+
mainContent += `
|
|
600
|
+
`;
|
|
601
|
+
}
|
|
602
|
+
if (config.settings.credentialHelperEnabled) {
|
|
603
|
+
mainContent += `# Git Credential Helper Bridge
|
|
604
|
+
`;
|
|
605
|
+
mainContent += `[credential]
|
|
606
|
+
`;
|
|
607
|
+
mainContent += ` helper = gitbridge credential
|
|
608
|
+
|
|
609
|
+
`;
|
|
610
|
+
}
|
|
611
|
+
if (config.rules.length > 0) {
|
|
612
|
+
mainContent += `# Directory-Based Conditional Includes
|
|
613
|
+
`;
|
|
614
|
+
for (const rule of config.rules) {
|
|
615
|
+
const ruleFile = paths.getRuleGitConfigFile(rule.id);
|
|
616
|
+
const expandedRulePath = expandTilde(rule.path);
|
|
617
|
+
const gitDirPattern = expandedRulePath.endsWith("/") ? `${expandedRulePath}**` : `${expandedRulePath}/**`;
|
|
618
|
+
mainContent += `[includeIf "gitdir:${gitDirPattern}"]
|
|
619
|
+
`;
|
|
620
|
+
mainContent += ` path = ${ruleFile}
|
|
621
|
+
|
|
622
|
+
`;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
const mainFile = paths.getMainGitConfigFile();
|
|
626
|
+
import_node_fs2.default.writeFileSync(mainFile, mainContent, { encoding: "utf-8", mode: 420 });
|
|
627
|
+
return {
|
|
628
|
+
mainConfigPath: mainFile,
|
|
629
|
+
generatedRules: ruleFiles
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
// src/core/git/gitconfig-injector.ts
|
|
635
|
+
var GITCONFIG_BLOCK_START = "# --- BEGIN GITBRIDGE MANAGED BLOCK ---";
|
|
636
|
+
var GITCONFIG_BLOCK_END = "# --- END GITBRIDGE MANAGED BLOCK ---";
|
|
637
|
+
var GitConfigInjector = class {
|
|
638
|
+
store;
|
|
639
|
+
constructor(store) {
|
|
640
|
+
this.store = store;
|
|
641
|
+
}
|
|
642
|
+
isInstalled(targetFile) {
|
|
643
|
+
const gitConfigFile = targetFile || this.store.getPathResolver().getUserGitConfigFile();
|
|
644
|
+
if (!import_node_fs3.default.existsSync(gitConfigFile)) return false;
|
|
645
|
+
const content = import_node_fs3.default.readFileSync(gitConfigFile, "utf-8");
|
|
646
|
+
return content.includes(GITCONFIG_BLOCK_START) && content.includes(GITCONFIG_BLOCK_END);
|
|
647
|
+
}
|
|
648
|
+
createBackup(gitConfigFile) {
|
|
649
|
+
if (!import_node_fs3.default.existsSync(gitConfigFile)) return null;
|
|
650
|
+
const backupsDir = this.store.getPathResolver().getBackupsDir();
|
|
651
|
+
if (!import_node_fs3.default.existsSync(backupsDir)) {
|
|
652
|
+
import_node_fs3.default.mkdirSync(backupsDir, { recursive: true });
|
|
653
|
+
}
|
|
654
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
655
|
+
const backupFile = import_node_path4.default.join(backupsDir, `gitconfig.${timestamp}.bak`);
|
|
656
|
+
import_node_fs3.default.copyFileSync(gitConfigFile, backupFile);
|
|
657
|
+
const primaryBak = `${gitConfigFile}.gitbridge.bak`;
|
|
658
|
+
if (!import_node_fs3.default.existsSync(primaryBak)) {
|
|
659
|
+
import_node_fs3.default.copyFileSync(gitConfigFile, primaryBak);
|
|
660
|
+
}
|
|
661
|
+
return backupFile;
|
|
662
|
+
}
|
|
663
|
+
inject(targetFile) {
|
|
664
|
+
const generator = new GitConfigGenerator(this.store);
|
|
665
|
+
const { mainConfigPath } = generator.generate();
|
|
666
|
+
const gitConfigFile = targetFile || this.store.getPathResolver().getUserGitConfigFile();
|
|
667
|
+
const backupPath = this.createBackup(gitConfigFile);
|
|
668
|
+
let originalContent = "";
|
|
669
|
+
if (import_node_fs3.default.existsSync(gitConfigFile)) {
|
|
670
|
+
originalContent = import_node_fs3.default.readFileSync(gitConfigFile, "utf-8");
|
|
671
|
+
}
|
|
672
|
+
const blockContent = [
|
|
673
|
+
GITCONFIG_BLOCK_START,
|
|
674
|
+
`# Do not edit this block directly. Manage via: gitbridge / gb`,
|
|
675
|
+
`[include]`,
|
|
676
|
+
` path = ${mainConfigPath}`,
|
|
677
|
+
GITCONFIG_BLOCK_END
|
|
678
|
+
].join("\n");
|
|
679
|
+
let newContent;
|
|
680
|
+
if (originalContent.includes(GITCONFIG_BLOCK_START) && originalContent.includes(GITCONFIG_BLOCK_END)) {
|
|
681
|
+
const before = originalContent.substring(0, originalContent.indexOf(GITCONFIG_BLOCK_START));
|
|
682
|
+
const after = originalContent.substring(originalContent.indexOf(GITCONFIG_BLOCK_END) + GITCONFIG_BLOCK_END.length);
|
|
683
|
+
newContent = `${before.trimEnd()}
|
|
684
|
+
|
|
685
|
+
${blockContent}
|
|
686
|
+
|
|
687
|
+
${after.trimStart()}`.trim() + "\n";
|
|
688
|
+
} else {
|
|
689
|
+
newContent = `${originalContent.trimEnd()}
|
|
690
|
+
|
|
691
|
+
${blockContent}
|
|
692
|
+
`.trimStart();
|
|
693
|
+
}
|
|
694
|
+
import_node_fs3.default.writeFileSync(gitConfigFile, newContent, { encoding: "utf-8", mode: 420 });
|
|
695
|
+
return { success: true, backupPath };
|
|
696
|
+
}
|
|
697
|
+
remove(targetFile) {
|
|
698
|
+
const gitConfigFile = targetFile || this.store.getPathResolver().getUserGitConfigFile();
|
|
699
|
+
if (!import_node_fs3.default.existsSync(gitConfigFile)) return true;
|
|
700
|
+
const originalContent = import_node_fs3.default.readFileSync(gitConfigFile, "utf-8");
|
|
701
|
+
if (!originalContent.includes(GITCONFIG_BLOCK_START)) return true;
|
|
702
|
+
const before = originalContent.substring(0, originalContent.indexOf(GITCONFIG_BLOCK_START));
|
|
703
|
+
const after = originalContent.substring(originalContent.indexOf(GITCONFIG_BLOCK_END) + GITCONFIG_BLOCK_END.length);
|
|
704
|
+
const cleaned = `${before.trimEnd()}
|
|
705
|
+
${after.trimStart()}`.trim();
|
|
706
|
+
if (cleaned.length === 0) {
|
|
707
|
+
import_node_fs3.default.writeFileSync(gitConfigFile, "", { encoding: "utf-8", mode: 420 });
|
|
708
|
+
} else {
|
|
709
|
+
import_node_fs3.default.writeFileSync(gitConfigFile, `${cleaned}
|
|
710
|
+
`, { encoding: "utf-8", mode: 420 });
|
|
711
|
+
}
|
|
712
|
+
return true;
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// src/core/ssh/ssh-injector.ts
|
|
717
|
+
var import_node_fs5 = __toESM(require("node:fs"));
|
|
718
|
+
var import_node_path5 = __toESM(require("node:path"));
|
|
719
|
+
|
|
720
|
+
// src/core/ssh/ssh-config-generator.ts
|
|
721
|
+
var import_node_fs4 = __toESM(require("node:fs"));
|
|
722
|
+
var SshConfigGenerator = class {
|
|
723
|
+
store;
|
|
724
|
+
constructor(store) {
|
|
725
|
+
this.store = store;
|
|
726
|
+
}
|
|
727
|
+
generate() {
|
|
728
|
+
this.store.ensureDirectories();
|
|
729
|
+
const accounts = this.store.loadAccounts();
|
|
730
|
+
const paths = this.store.getPathResolver();
|
|
731
|
+
const generatedSshFile = paths.getGeneratedSshConfigFile();
|
|
732
|
+
let content = `# Auto-generated by GitBridge. DO NOT EDIT DIRECTLY.
|
|
733
|
+
`;
|
|
734
|
+
content += `# Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
735
|
+
|
|
736
|
+
`;
|
|
737
|
+
const sshAccounts = accounts.filter((a) => a.sshKeyPath);
|
|
738
|
+
if (sshAccounts.length === 0) {
|
|
739
|
+
content += `# No accounts currently configured with custom SSH keys.
|
|
740
|
+
`;
|
|
741
|
+
} else {
|
|
742
|
+
for (const account of sshAccounts) {
|
|
743
|
+
const hostAlias = `${account.host}-${account.id}`;
|
|
744
|
+
const keyPath = expandTilde(account.sshKeyPath);
|
|
745
|
+
content += `# Account: ${account.displayName || account.username} (${account.providerId})
|
|
746
|
+
`;
|
|
747
|
+
content += `Host ${hostAlias}
|
|
748
|
+
`;
|
|
749
|
+
content += ` HostName ${account.host}
|
|
750
|
+
`;
|
|
751
|
+
content += ` User git
|
|
752
|
+
`;
|
|
753
|
+
content += ` IdentityFile ${keyPath}
|
|
754
|
+
`;
|
|
755
|
+
content += ` IdentitiesOnly yes
|
|
756
|
+
|
|
757
|
+
`;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
import_node_fs4.default.writeFileSync(generatedSshFile, content, { encoding: "utf-8", mode: 384 });
|
|
761
|
+
return generatedSshFile;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
// src/core/ssh/ssh-injector.ts
|
|
766
|
+
var SSH_BLOCK_START = "# --- BEGIN GITBRIDGE MANAGED BLOCK ---";
|
|
767
|
+
var SSH_BLOCK_END = "# --- END GITBRIDGE MANAGED BLOCK ---";
|
|
768
|
+
var SshInjector = class {
|
|
769
|
+
store;
|
|
770
|
+
constructor(store) {
|
|
771
|
+
this.store = store;
|
|
772
|
+
}
|
|
773
|
+
isInstalled(targetFile) {
|
|
774
|
+
const sshConfigFile = targetFile || this.store.getPathResolver().getUserSshConfigFile();
|
|
775
|
+
if (!import_node_fs5.default.existsSync(sshConfigFile)) return false;
|
|
776
|
+
const content = import_node_fs5.default.readFileSync(sshConfigFile, "utf-8");
|
|
777
|
+
return content.includes(SSH_BLOCK_START) && content.includes(SSH_BLOCK_END);
|
|
778
|
+
}
|
|
779
|
+
createBackup(sshConfigFile) {
|
|
780
|
+
if (!import_node_fs5.default.existsSync(sshConfigFile)) return null;
|
|
781
|
+
const backupsDir = this.store.getPathResolver().getBackupsDir();
|
|
782
|
+
if (!import_node_fs5.default.existsSync(backupsDir)) {
|
|
783
|
+
import_node_fs5.default.mkdirSync(backupsDir, { recursive: true });
|
|
784
|
+
}
|
|
785
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
786
|
+
const backupFile = import_node_path5.default.join(backupsDir, `ssh_config.${timestamp}.bak`);
|
|
787
|
+
import_node_fs5.default.copyFileSync(sshConfigFile, backupFile);
|
|
788
|
+
const primaryBak = `${sshConfigFile}.gitbridge.bak`;
|
|
789
|
+
if (!import_node_fs5.default.existsSync(primaryBak)) {
|
|
790
|
+
import_node_fs5.default.copyFileSync(sshConfigFile, primaryBak);
|
|
791
|
+
}
|
|
792
|
+
return backupFile;
|
|
793
|
+
}
|
|
794
|
+
inject(targetFile) {
|
|
795
|
+
const generator = new SshConfigGenerator(this.store);
|
|
796
|
+
const generatedPath = generator.generate();
|
|
797
|
+
const sshConfigFile = targetFile || this.store.getPathResolver().getUserSshConfigFile();
|
|
798
|
+
const sshDir = import_node_path5.default.dirname(sshConfigFile);
|
|
799
|
+
if (!import_node_fs5.default.existsSync(sshDir)) {
|
|
800
|
+
import_node_fs5.default.mkdirSync(sshDir, { recursive: true, mode: 448 });
|
|
801
|
+
}
|
|
802
|
+
const backupPath = this.createBackup(sshConfigFile);
|
|
803
|
+
let originalContent = "";
|
|
804
|
+
if (import_node_fs5.default.existsSync(sshConfigFile)) {
|
|
805
|
+
originalContent = import_node_fs5.default.readFileSync(sshConfigFile, "utf-8");
|
|
806
|
+
}
|
|
807
|
+
const includePath = collapseTilde(generatedPath);
|
|
808
|
+
const blockContent = [
|
|
809
|
+
SSH_BLOCK_START,
|
|
810
|
+
`# Do not edit this block directly. Manage via: gitbridge / gb`,
|
|
811
|
+
`Include ${includePath}`,
|
|
812
|
+
SSH_BLOCK_END
|
|
813
|
+
].join("\n");
|
|
814
|
+
let newContent;
|
|
815
|
+
if (originalContent.includes(SSH_BLOCK_START) && originalContent.includes(SSH_BLOCK_END)) {
|
|
816
|
+
const before = originalContent.substring(0, originalContent.indexOf(SSH_BLOCK_START));
|
|
817
|
+
const after = originalContent.substring(originalContent.indexOf(SSH_BLOCK_END) + SSH_BLOCK_END.length);
|
|
818
|
+
newContent = `${blockContent}
|
|
819
|
+
|
|
820
|
+
${before.trim()}
|
|
821
|
+
${after.trim()}`.trim() + "\n";
|
|
822
|
+
} else {
|
|
823
|
+
newContent = `${blockContent}
|
|
824
|
+
|
|
825
|
+
${originalContent.trim()}`.trim() + "\n";
|
|
826
|
+
}
|
|
827
|
+
import_node_fs5.default.writeFileSync(sshConfigFile, newContent, { encoding: "utf-8", mode: 384 });
|
|
828
|
+
return { success: true, backupPath };
|
|
829
|
+
}
|
|
830
|
+
remove(targetFile) {
|
|
831
|
+
const sshConfigFile = targetFile || this.store.getPathResolver().getUserSshConfigFile();
|
|
832
|
+
if (!import_node_fs5.default.existsSync(sshConfigFile)) return true;
|
|
833
|
+
const originalContent = import_node_fs5.default.readFileSync(sshConfigFile, "utf-8");
|
|
834
|
+
if (!originalContent.includes(SSH_BLOCK_START)) return true;
|
|
835
|
+
const before = originalContent.substring(0, originalContent.indexOf(SSH_BLOCK_START));
|
|
836
|
+
const after = originalContent.substring(originalContent.indexOf(SSH_BLOCK_END) + SSH_BLOCK_END.length);
|
|
837
|
+
const cleaned = `${before.trim()}
|
|
838
|
+
${after.trim()}`.trim();
|
|
839
|
+
if (cleaned.length === 0) {
|
|
840
|
+
import_node_fs5.default.writeFileSync(sshConfigFile, "", { encoding: "utf-8", mode: 384 });
|
|
841
|
+
} else {
|
|
842
|
+
import_node_fs5.default.writeFileSync(sshConfigFile, `${cleaned}
|
|
843
|
+
`, { encoding: "utf-8", mode: 384 });
|
|
844
|
+
}
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
|
|
849
|
+
// src/cli/ui/banners.ts
|
|
850
|
+
var import_picocolors = __toESM(require("picocolors"));
|
|
851
|
+
function showBanner() {
|
|
852
|
+
console.log(
|
|
853
|
+
import_picocolors.default.cyan(`
|
|
854
|
+
\u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510
|
|
855
|
+
\u2502 ${import_picocolors.default.bold("GitBridge")} \u2502
|
|
856
|
+
\u2502 Universal Git Identity & Multi-Account Layer \u2502
|
|
857
|
+
\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
|
|
858
|
+
`)
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
function formatBadge(text2, color = "blue") {
|
|
862
|
+
switch (color) {
|
|
863
|
+
case "green":
|
|
864
|
+
return import_picocolors.default.bgGreen(import_picocolors.default.black(` ${text2} `));
|
|
865
|
+
case "blue":
|
|
866
|
+
return import_picocolors.default.bgBlue(import_picocolors.default.white(` ${text2} `));
|
|
867
|
+
case "yellow":
|
|
868
|
+
return import_picocolors.default.bgYellow(import_picocolors.default.black(` ${text2} `));
|
|
869
|
+
case "red":
|
|
870
|
+
return import_picocolors.default.bgRed(import_picocolors.default.white(` ${text2} `));
|
|
871
|
+
case "magenta":
|
|
872
|
+
return import_picocolors.default.bgMagenta(import_picocolors.default.white(` ${text2} `));
|
|
873
|
+
case "cyan":
|
|
874
|
+
return import_picocolors.default.bgCyan(import_picocolors.default.black(` ${text2} `));
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// src/cli/ui/tables.ts
|
|
879
|
+
var import_cli_table3 = __toESM(require("cli-table3"));
|
|
880
|
+
var import_picocolors2 = __toESM(require("picocolors"));
|
|
881
|
+
function renderIdentitiesTable(identities, defaultId) {
|
|
882
|
+
if (identities.length === 0) {
|
|
883
|
+
return import_picocolors2.default.gray(" No identities configured yet. Run 'gitbridge identity add' to create one.\n");
|
|
884
|
+
}
|
|
885
|
+
const table = new import_cli_table3.default({
|
|
886
|
+
head: [import_picocolors2.default.bold("ID"), import_picocolors2.default.bold("Name"), import_picocolors2.default.bold("Email"), import_picocolors2.default.bold("Default"), import_picocolors2.default.bold("Signing Key")],
|
|
887
|
+
style: { head: ["cyan"] }
|
|
888
|
+
});
|
|
889
|
+
for (const id of identities) {
|
|
890
|
+
const isDef = id.id === defaultId || id.isDefault;
|
|
891
|
+
table.push([
|
|
892
|
+
import_picocolors2.default.cyan(id.id),
|
|
893
|
+
id.name,
|
|
894
|
+
id.email,
|
|
895
|
+
isDef ? import_picocolors2.default.green("\u2714 yes") : import_picocolors2.default.gray("no"),
|
|
896
|
+
id.signingKey ? import_picocolors2.default.yellow(id.signingKey.slice(0, 20) + "...") : import_picocolors2.default.gray("none")
|
|
897
|
+
]);
|
|
898
|
+
}
|
|
899
|
+
return table.toString();
|
|
900
|
+
}
|
|
901
|
+
function renderAccountsTable(accounts) {
|
|
902
|
+
if (accounts.length === 0) {
|
|
903
|
+
return import_picocolors2.default.gray(" No accounts logged in yet. Run 'gitbridge auth login <provider>' to add one.\n");
|
|
904
|
+
}
|
|
905
|
+
const table = new import_cli_table3.default({
|
|
906
|
+
head: [import_picocolors2.default.bold("ID"), import_picocolors2.default.bold("Provider"), import_picocolors2.default.bold("Username"), import_picocolors2.default.bold("Host"), import_picocolors2.default.bold("Auth Type"), import_picocolors2.default.bold("SSH Key")],
|
|
907
|
+
style: { head: ["cyan"] }
|
|
908
|
+
});
|
|
909
|
+
for (const acc of accounts) {
|
|
910
|
+
table.push([
|
|
911
|
+
import_picocolors2.default.cyan(acc.id),
|
|
912
|
+
import_picocolors2.default.magenta(acc.providerId.toUpperCase()),
|
|
913
|
+
acc.username,
|
|
914
|
+
acc.host,
|
|
915
|
+
acc.authType,
|
|
916
|
+
acc.sshKeyPath ? import_picocolors2.default.yellow(acc.sshKeyPath) : import_picocolors2.default.gray("none")
|
|
917
|
+
]);
|
|
918
|
+
}
|
|
919
|
+
return table.toString();
|
|
920
|
+
}
|
|
921
|
+
function renderRulesTable(rules) {
|
|
922
|
+
if (rules.length === 0) {
|
|
923
|
+
return import_picocolors2.default.gray(" No directory rules configured. Run 'gitbridge rule add' to map directories.\n");
|
|
924
|
+
}
|
|
925
|
+
const table = new import_cli_table3.default({
|
|
926
|
+
head: [import_picocolors2.default.bold("Rule ID"), import_picocolors2.default.bold("Directory Path"), import_picocolors2.default.bold("Identity"), import_picocolors2.default.bold("Provider"), import_picocolors2.default.bold("Account")],
|
|
927
|
+
style: { head: ["cyan"] }
|
|
928
|
+
});
|
|
929
|
+
for (const rule of rules) {
|
|
930
|
+
table.push([
|
|
931
|
+
import_picocolors2.default.cyan(rule.id),
|
|
932
|
+
import_picocolors2.default.blue(rule.path),
|
|
933
|
+
import_picocolors2.default.green(rule.identityId),
|
|
934
|
+
rule.defaultProvider ? import_picocolors2.default.magenta(rule.defaultProvider) : import_picocolors2.default.gray("any"),
|
|
935
|
+
rule.defaultAccountId ? import_picocolors2.default.yellow(rule.defaultAccountId) : import_picocolors2.default.gray("any")
|
|
936
|
+
]);
|
|
937
|
+
}
|
|
938
|
+
return table.toString();
|
|
939
|
+
}
|
|
940
|
+
function renderRemotesTable(remotes) {
|
|
941
|
+
if (remotes.length === 0) {
|
|
942
|
+
return import_picocolors2.default.gray(" No Git remotes found.\n");
|
|
943
|
+
}
|
|
944
|
+
const table = new import_cli_table3.default({
|
|
945
|
+
head: [import_picocolors2.default.bold("Remote"), import_picocolors2.default.bold("Provider"), import_picocolors2.default.bold("Host"), import_picocolors2.default.bold("Repo / Org"), import_picocolors2.default.bold("Account Alias")],
|
|
946
|
+
style: { head: ["cyan"] }
|
|
947
|
+
});
|
|
948
|
+
for (const r of remotes) {
|
|
949
|
+
const parsed = r.parsedPush || r.parsedFetch;
|
|
950
|
+
table.push([
|
|
951
|
+
import_picocolors2.default.cyan(r.name),
|
|
952
|
+
parsed ? import_picocolors2.default.magenta(parsed.providerId.toUpperCase()) : import_picocolors2.default.gray("unknown"),
|
|
953
|
+
parsed ? parsed.host : import_picocolors2.default.gray("unknown"),
|
|
954
|
+
parsed ? parsed.fullName : import_picocolors2.default.gray("unknown"),
|
|
955
|
+
parsed?.accountAlias ? import_picocolors2.default.yellow(parsed.accountAlias) : import_picocolors2.default.gray("default")
|
|
956
|
+
]);
|
|
957
|
+
}
|
|
958
|
+
return table.toString();
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/cli/commands/status.ts
|
|
962
|
+
async function handleStatusCommand(store = defaultConfigStore) {
|
|
963
|
+
showBanner();
|
|
964
|
+
const config = store.loadConfig();
|
|
965
|
+
const identities = store.loadIdentities();
|
|
966
|
+
const accounts = store.loadAccounts();
|
|
967
|
+
const rules = store.loadRules();
|
|
968
|
+
const gitInjector = new GitConfigInjector(store);
|
|
969
|
+
const sshInjector = new SshInjector(store);
|
|
970
|
+
const gitInstalled = gitInjector.isInstalled();
|
|
971
|
+
const sshInstalled = sshInjector.isInstalled();
|
|
972
|
+
console.log(import_picocolors3.default.bold(" STATUS OVERVIEW"));
|
|
973
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
974
|
+
console.log(` GitBridge Mode: ${config.enabled ? formatBadge("ENABLED", "green") : formatBadge("DISABLED", "red")}`);
|
|
975
|
+
console.log(` Git Extension Block: ${gitInstalled ? import_picocolors3.default.green("\u2714 Active in ~/.gitconfig") : import_picocolors3.default.yellow("\u26A0 Not installed (run 'gitbridge enable')")}`);
|
|
976
|
+
console.log(` SSH Extension Block: ${sshInstalled ? import_picocolors3.default.green("\u2714 Active in ~/.ssh/config") : import_picocolors3.default.gray("\u25CB Not installed")}`);
|
|
977
|
+
console.log(` Default Identity: ${config.defaultIdentityId ? import_picocolors3.default.cyan(config.defaultIdentityId) : import_picocolors3.default.gray("none")}`);
|
|
978
|
+
console.log("");
|
|
979
|
+
console.log(import_picocolors3.default.bold(" IDENTITIES"));
|
|
980
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
981
|
+
console.log(renderIdentitiesTable(identities, config.defaultIdentityId));
|
|
982
|
+
console.log("");
|
|
983
|
+
console.log(import_picocolors3.default.bold(" AUTHENTICATED PROVIDER ACCOUNTS"));
|
|
984
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
985
|
+
console.log(renderAccountsTable(accounts));
|
|
986
|
+
console.log("");
|
|
987
|
+
console.log(import_picocolors3.default.bold(" DIRECTORY RULES"));
|
|
988
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
989
|
+
console.log(renderRulesTable(rules));
|
|
990
|
+
console.log("");
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// src/utils/logger.ts
|
|
994
|
+
var import_picocolors4 = __toESM(require("picocolors"));
|
|
995
|
+
var Logger = class {
|
|
996
|
+
isDebug = process.env.DEBUG === "true" || process.env.GITBRIDGE_DEBUG === "1";
|
|
997
|
+
setDebug(val) {
|
|
998
|
+
this.isDebug = val;
|
|
999
|
+
}
|
|
1000
|
+
debug(msg, ...args) {
|
|
1001
|
+
if (this.isDebug) {
|
|
1002
|
+
console.log(import_picocolors4.default.gray(`[DEBUG] ${msg}`), ...args);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
info(msg) {
|
|
1006
|
+
console.log(import_picocolors4.default.blue("\u2139 ") + msg);
|
|
1007
|
+
}
|
|
1008
|
+
success(msg) {
|
|
1009
|
+
console.log(import_picocolors4.default.green("\u2714 ") + import_picocolors4.default.bold(msg));
|
|
1010
|
+
}
|
|
1011
|
+
warn(msg) {
|
|
1012
|
+
console.log(import_picocolors4.default.yellow("\u26A0 ") + import_picocolors4.default.yellow(msg));
|
|
1013
|
+
}
|
|
1014
|
+
error(msg, err) {
|
|
1015
|
+
console.error(import_picocolors4.default.red("\u2716 ") + import_picocolors4.default.red(msg));
|
|
1016
|
+
if (err && this.isDebug) {
|
|
1017
|
+
console.error(err);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
highlight(text2) {
|
|
1021
|
+
return import_picocolors4.default.cyan(text2);
|
|
1022
|
+
}
|
|
1023
|
+
dim(text2) {
|
|
1024
|
+
return import_picocolors4.default.gray(text2);
|
|
1025
|
+
}
|
|
1026
|
+
bold(text2) {
|
|
1027
|
+
return import_picocolors4.default.bold(text2);
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
var logger = new Logger();
|
|
1031
|
+
|
|
1032
|
+
// src/cli/commands/enable.ts
|
|
1033
|
+
var import_picocolors5 = __toESM(require("picocolors"));
|
|
1034
|
+
async function handleEnableCommand(store = defaultConfigStore) {
|
|
1035
|
+
store.setEnabled(true);
|
|
1036
|
+
const gitInjector = new GitConfigInjector(store);
|
|
1037
|
+
const sshInjector = new SshInjector(store);
|
|
1038
|
+
const gitRes = gitInjector.inject();
|
|
1039
|
+
const sshRes = sshInjector.inject();
|
|
1040
|
+
logger.success("GitBridge enabled successfully!");
|
|
1041
|
+
if (gitRes.backupPath) {
|
|
1042
|
+
logger.debug(`Created backup of .gitconfig at ${gitRes.backupPath}`);
|
|
1043
|
+
}
|
|
1044
|
+
if (sshRes.backupPath) {
|
|
1045
|
+
logger.debug(`Created backup of .ssh/config at ${sshRes.backupPath}`);
|
|
1046
|
+
}
|
|
1047
|
+
console.log(import_picocolors5.default.green("\n\u2714 GitBridge integration is now active."));
|
|
1048
|
+
console.log(import_picocolors5.default.gray(" \u2022 Native Git commands (commit, push, pull) will now inherit GitBridge context."));
|
|
1049
|
+
console.log(import_picocolors5.default.gray(" \u2022 Run 'gitbridge status' or 'gitbridge context' to inspect current setup.\n"));
|
|
1050
|
+
}
|
|
1051
|
+
async function handleDisableCommand(store = defaultConfigStore) {
|
|
1052
|
+
store.setEnabled(false);
|
|
1053
|
+
const gitInjector = new GitConfigInjector(store);
|
|
1054
|
+
const sshInjector = new SshInjector(store);
|
|
1055
|
+
gitInjector.remove();
|
|
1056
|
+
sshInjector.remove();
|
|
1057
|
+
logger.success("GitBridge disabled successfully!");
|
|
1058
|
+
console.log(import_picocolors5.default.yellow("\n\u26A0 GitBridge integration has been safely removed."));
|
|
1059
|
+
console.log(import_picocolors5.default.gray(" \u2022 ~/.gitconfig and ~/.ssh/config have been restored to native settings."));
|
|
1060
|
+
console.log(import_picocolors5.default.gray(" \u2022 Run 'gitbridge enable' to reactivate whenever you want.\n"));
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// src/cli/ui/prompts.ts
|
|
1064
|
+
var p = __toESM(require("@clack/prompts"));
|
|
1065
|
+
var import_picocolors6 = __toESM(require("picocolors"));
|
|
1066
|
+
function handleCancel() {
|
|
1067
|
+
p.cancel(import_picocolors6.default.yellow("Operation cancelled."));
|
|
1068
|
+
process.exit(0);
|
|
1069
|
+
}
|
|
1070
|
+
async function promptText(options) {
|
|
1071
|
+
const res = await p.text({
|
|
1072
|
+
message: options.message,
|
|
1073
|
+
placeholder: options.placeholder,
|
|
1074
|
+
defaultValue: options.defaultValue,
|
|
1075
|
+
validate: options.validate
|
|
1076
|
+
});
|
|
1077
|
+
if (p.isCancel(res)) handleCancel();
|
|
1078
|
+
return String(res);
|
|
1079
|
+
}
|
|
1080
|
+
async function promptSelect(options) {
|
|
1081
|
+
const res = await p.select({
|
|
1082
|
+
message: options.message,
|
|
1083
|
+
options: options.options,
|
|
1084
|
+
initialValue: options.initialValue
|
|
1085
|
+
});
|
|
1086
|
+
if (p.isCancel(res)) handleCancel();
|
|
1087
|
+
return res;
|
|
1088
|
+
}
|
|
1089
|
+
async function promptMultiSelect(options) {
|
|
1090
|
+
const res = await p.multiselect({
|
|
1091
|
+
message: options.message,
|
|
1092
|
+
options: options.options,
|
|
1093
|
+
required: options.required
|
|
1094
|
+
});
|
|
1095
|
+
if (p.isCancel(res)) handleCancel();
|
|
1096
|
+
return res;
|
|
1097
|
+
}
|
|
1098
|
+
async function promptConfirm(options) {
|
|
1099
|
+
const res = await p.confirm({
|
|
1100
|
+
message: options.message,
|
|
1101
|
+
active: options.active,
|
|
1102
|
+
inactive: options.inactive,
|
|
1103
|
+
initialValue: options.initialValue
|
|
1104
|
+
});
|
|
1105
|
+
if (p.isCancel(res)) handleCancel();
|
|
1106
|
+
return Boolean(res);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/cli/commands/identity.ts
|
|
1110
|
+
var import_picocolors7 = __toESM(require("picocolors"));
|
|
1111
|
+
async function handleIdentityList(store = defaultConfigStore) {
|
|
1112
|
+
const identities = store.loadIdentities();
|
|
1113
|
+
const config = store.loadConfig();
|
|
1114
|
+
console.log(import_picocolors7.default.bold("\n GIT IDENTITIES"));
|
|
1115
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1116
|
+
console.log(renderIdentitiesTable(identities, config.defaultIdentityId));
|
|
1117
|
+
}
|
|
1118
|
+
async function handleIdentityAdd(options = {}, store = defaultConfigStore) {
|
|
1119
|
+
const isInteractive = !options.id && !options.name && !options.email;
|
|
1120
|
+
let id = options.id;
|
|
1121
|
+
let name = options.name;
|
|
1122
|
+
let email = options.email;
|
|
1123
|
+
let signingKey = options.signingKey || null;
|
|
1124
|
+
let isDefault = options.default;
|
|
1125
|
+
if (!id) {
|
|
1126
|
+
id = await promptText({
|
|
1127
|
+
message: "Enter an identity ID (e.g. personal, work, client-acme):",
|
|
1128
|
+
validate: (val) => {
|
|
1129
|
+
if (!val || !val.trim()) return "Identity ID is required.";
|
|
1130
|
+
if (store.getIdentity(val.trim())) return "An identity with this ID already exists.";
|
|
1131
|
+
return void 0;
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
if (!name) {
|
|
1136
|
+
name = await promptText({
|
|
1137
|
+
message: "Enter full name for Git commits (e.g. Fuad Tesfaye):",
|
|
1138
|
+
validate: (val) => !val || !val.trim() ? "Name is required." : void 0
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
if (!email) {
|
|
1142
|
+
email = await promptText({
|
|
1143
|
+
message: "Enter email address for Git commits:",
|
|
1144
|
+
validate: (val) => !val || !val.includes("@") ? "Please enter a valid email address." : void 0
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
if (isInteractive && signingKey === null) {
|
|
1148
|
+
const addKey = await promptConfirm({
|
|
1149
|
+
message: "Do you want to configure an SSH or GPG commit signing key for this identity?",
|
|
1150
|
+
initialValue: false
|
|
1151
|
+
});
|
|
1152
|
+
if (addKey) {
|
|
1153
|
+
signingKey = await promptText({
|
|
1154
|
+
message: "Enter signing key (SSH public key or GPG Key ID):"
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
if (isDefault === void 0) {
|
|
1159
|
+
const existing = store.loadIdentities();
|
|
1160
|
+
if (existing.length === 0) {
|
|
1161
|
+
isDefault = true;
|
|
1162
|
+
} else if (isInteractive) {
|
|
1163
|
+
isDefault = await promptConfirm({
|
|
1164
|
+
message: "Set this identity as your global default identity?",
|
|
1165
|
+
initialValue: false
|
|
1166
|
+
});
|
|
1167
|
+
} else {
|
|
1168
|
+
isDefault = false;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const created = store.addIdentity({
|
|
1172
|
+
id: id.trim(),
|
|
1173
|
+
name: name.trim(),
|
|
1174
|
+
email: email.trim(),
|
|
1175
|
+
signingKey: signingKey ? signingKey.trim() : null,
|
|
1176
|
+
isDefault
|
|
1177
|
+
});
|
|
1178
|
+
const generator = new GitConfigGenerator(store);
|
|
1179
|
+
generator.generate();
|
|
1180
|
+
logger.success(`Identity '${created.id}' created successfully!`);
|
|
1181
|
+
console.log(import_picocolors7.default.gray(` Name: ${created.name}`));
|
|
1182
|
+
console.log(import_picocolors7.default.gray(` Email: ${created.email}`));
|
|
1183
|
+
if (created.isDefault) {
|
|
1184
|
+
console.log(import_picocolors7.default.green(" Set as global default identity."));
|
|
1185
|
+
}
|
|
1186
|
+
console.log("");
|
|
1187
|
+
}
|
|
1188
|
+
async function handleIdentityUse(id, store = defaultConfigStore) {
|
|
1189
|
+
const target = store.getIdentity(id);
|
|
1190
|
+
if (!target) {
|
|
1191
|
+
logger.error(`Identity with ID '${id}' does not exist.`);
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
store.setDefaultIdentity(id);
|
|
1195
|
+
const generator = new GitConfigGenerator(store);
|
|
1196
|
+
generator.generate();
|
|
1197
|
+
logger.success(`Switched global default Git identity to '${target.id}' (${target.email})`);
|
|
1198
|
+
}
|
|
1199
|
+
async function handleIdentityRemove(id, store = defaultConfigStore) {
|
|
1200
|
+
const removed = store.removeIdentity(id);
|
|
1201
|
+
if (!removed) {
|
|
1202
|
+
logger.error(`Identity with ID '${id}' not found.`);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const generator = new GitConfigGenerator(store);
|
|
1206
|
+
generator.generate();
|
|
1207
|
+
logger.success(`Removed identity '${id}'.`);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// src/core/storage/linux-keyring.ts
|
|
1211
|
+
var LinuxKeyringCredentialStore = class {
|
|
1212
|
+
name = "Linux Secret Service (secret-tool)";
|
|
1213
|
+
async isAvailable() {
|
|
1214
|
+
try {
|
|
1215
|
+
const proc = Bun.spawn(["which", "secret-tool"], {
|
|
1216
|
+
stdout: "pipe",
|
|
1217
|
+
stderr: "pipe"
|
|
1218
|
+
});
|
|
1219
|
+
const code = await proc.exited;
|
|
1220
|
+
return code === 0;
|
|
1221
|
+
} catch {
|
|
1222
|
+
return false;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async set(service, account, secret) {
|
|
1226
|
+
try {
|
|
1227
|
+
const proc = Bun.spawn(
|
|
1228
|
+
["secret-tool", "store", "--label", `GitBridge (${service}:${account})`, "service", service, "account", account],
|
|
1229
|
+
{
|
|
1230
|
+
stdin: "pipe",
|
|
1231
|
+
stdout: "pipe",
|
|
1232
|
+
stderr: "pipe"
|
|
1233
|
+
}
|
|
1234
|
+
);
|
|
1235
|
+
proc.stdin.write(secret);
|
|
1236
|
+
proc.stdin.end();
|
|
1237
|
+
const code = await proc.exited;
|
|
1238
|
+
if (code !== 0) {
|
|
1239
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1240
|
+
throw new Error(stderr.trim() || `secret-tool store exited with code ${code}`);
|
|
1241
|
+
}
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
throw new CredentialStoreError(
|
|
1244
|
+
`Failed to store credential in Linux Keyring: ${err instanceof Error ? err.message : String(err)}`
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
async get(service, account) {
|
|
1249
|
+
try {
|
|
1250
|
+
const proc = Bun.spawn(["secret-tool", "lookup", "service", service, "account", account], {
|
|
1251
|
+
stdout: "pipe",
|
|
1252
|
+
stderr: "pipe"
|
|
1253
|
+
});
|
|
1254
|
+
const stdout = await new Response(proc.stdout).text();
|
|
1255
|
+
const code = await proc.exited;
|
|
1256
|
+
if (code === 0 && stdout.trim().length > 0) {
|
|
1257
|
+
return stdout.trim();
|
|
1258
|
+
}
|
|
1259
|
+
return null;
|
|
1260
|
+
} catch {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
async delete(service, account) {
|
|
1265
|
+
try {
|
|
1266
|
+
const proc = Bun.spawn(["secret-tool", "clear", "service", service, "account", account], {
|
|
1267
|
+
stdout: "pipe",
|
|
1268
|
+
stderr: "pipe"
|
|
1269
|
+
});
|
|
1270
|
+
await proc.exited;
|
|
1271
|
+
} catch {
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
// src/core/storage/macos-keychain.ts
|
|
1277
|
+
var MacOSKeychainCredentialStore = class {
|
|
1278
|
+
name = "macOS Keychain";
|
|
1279
|
+
async isAvailable() {
|
|
1280
|
+
if (process.platform !== "darwin") return false;
|
|
1281
|
+
try {
|
|
1282
|
+
const proc = Bun.spawn(["which", "security"], {
|
|
1283
|
+
stdout: "pipe",
|
|
1284
|
+
stderr: "pipe"
|
|
1285
|
+
});
|
|
1286
|
+
const code = await proc.exited;
|
|
1287
|
+
return code === 0;
|
|
1288
|
+
} catch {
|
|
1289
|
+
return false;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
async set(service, account, secret) {
|
|
1293
|
+
try {
|
|
1294
|
+
await this.delete(service, account);
|
|
1295
|
+
const proc = Bun.spawn(
|
|
1296
|
+
["security", "add-generic-password", "-a", account, "-s", service, "-w", secret, "-U"],
|
|
1297
|
+
{
|
|
1298
|
+
stdout: "pipe",
|
|
1299
|
+
stderr: "pipe"
|
|
1300
|
+
}
|
|
1301
|
+
);
|
|
1302
|
+
const code = await proc.exited;
|
|
1303
|
+
if (code !== 0) {
|
|
1304
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1305
|
+
throw new Error(stderr.trim() || `security add-generic-password exited with code ${code}`);
|
|
1306
|
+
}
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
throw new CredentialStoreError(
|
|
1309
|
+
`Failed to store credential in macOS Keychain: ${err instanceof Error ? err.message : String(err)}`
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
async get(service, account) {
|
|
1314
|
+
try {
|
|
1315
|
+
const proc = Bun.spawn(["security", "find-generic-password", "-a", account, "-s", service, "-w"], {
|
|
1316
|
+
stdout: "pipe",
|
|
1317
|
+
stderr: "pipe"
|
|
1318
|
+
});
|
|
1319
|
+
const stdout = await new Response(proc.stdout).text();
|
|
1320
|
+
const code = await proc.exited;
|
|
1321
|
+
if (code === 0 && stdout.trim().length > 0) {
|
|
1322
|
+
return stdout.trim();
|
|
1323
|
+
}
|
|
1324
|
+
return null;
|
|
1325
|
+
} catch {
|
|
1326
|
+
return null;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
async delete(service, account) {
|
|
1330
|
+
try {
|
|
1331
|
+
const proc = Bun.spawn(["security", "delete-generic-password", "-a", account, "-s", service], {
|
|
1332
|
+
stdout: "pipe",
|
|
1333
|
+
stderr: "pipe"
|
|
1334
|
+
});
|
|
1335
|
+
await proc.exited;
|
|
1336
|
+
} catch {
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
// src/core/storage/windows-cred.ts
|
|
1342
|
+
var WindowsCredentialStore = class {
|
|
1343
|
+
name = "Windows Credential Manager";
|
|
1344
|
+
async isAvailable() {
|
|
1345
|
+
return process.platform === "win32";
|
|
1346
|
+
}
|
|
1347
|
+
targetName(service, account) {
|
|
1348
|
+
return `gitbridge:${service}:${account}`;
|
|
1349
|
+
}
|
|
1350
|
+
async set(service, account, secret) {
|
|
1351
|
+
const target = this.targetName(service, account);
|
|
1352
|
+
try {
|
|
1353
|
+
const proc = Bun.spawn(["cmdkey", `/generic:${target}`, `/user:${account}`, `/pass:${secret}`], {
|
|
1354
|
+
stdout: "pipe",
|
|
1355
|
+
stderr: "pipe"
|
|
1356
|
+
});
|
|
1357
|
+
const code = await proc.exited;
|
|
1358
|
+
if (code !== 0) {
|
|
1359
|
+
const stderr = await new Response(proc.stderr).text();
|
|
1360
|
+
throw new Error(stderr.trim() || `cmdkey exited with code ${code}`);
|
|
1361
|
+
}
|
|
1362
|
+
} catch (err) {
|
|
1363
|
+
throw new CredentialStoreError(
|
|
1364
|
+
`Failed to store credential in Windows Credential Manager: ${err instanceof Error ? err.message : String(err)}`
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
async get(service, account) {
|
|
1369
|
+
const target = this.targetName(service, account);
|
|
1370
|
+
try {
|
|
1371
|
+
const script = `
|
|
1372
|
+
Add-Type -AssemblyName System.Security
|
|
1373
|
+
$target = "${target}"
|
|
1374
|
+
$cred = [System.Net.CredentialCache]::DefaultCredentials
|
|
1375
|
+
# Note: full extraction on Windows uses powershell credential lookup
|
|
1376
|
+
`;
|
|
1377
|
+
const proc = Bun.spawn(["powershell", "-NoProfile", "-NonInteractive", "-Command", script], {
|
|
1378
|
+
stdout: "pipe",
|
|
1379
|
+
stderr: "pipe"
|
|
1380
|
+
});
|
|
1381
|
+
const stdout = await new Response(proc.stdout).text();
|
|
1382
|
+
const code = await proc.exited;
|
|
1383
|
+
if (code === 0 && stdout.trim()) {
|
|
1384
|
+
return stdout.trim();
|
|
1385
|
+
}
|
|
1386
|
+
return null;
|
|
1387
|
+
} catch {
|
|
1388
|
+
return null;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
async delete(service, account) {
|
|
1392
|
+
const target = this.targetName(service, account);
|
|
1393
|
+
try {
|
|
1394
|
+
const proc = Bun.spawn(["cmdkey", `/delete:${target}`], {
|
|
1395
|
+
stdout: "pipe",
|
|
1396
|
+
stderr: "pipe"
|
|
1397
|
+
});
|
|
1398
|
+
await proc.exited;
|
|
1399
|
+
} catch {
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
// src/core/storage/encrypted-vault.ts
|
|
1405
|
+
var import_node_crypto = __toESM(require("node:crypto"));
|
|
1406
|
+
var import_node_fs6 = __toESM(require("node:fs"));
|
|
1407
|
+
var import_node_path6 = __toESM(require("node:path"));
|
|
1408
|
+
var import_node_os2 = __toESM(require("node:os"));
|
|
1409
|
+
var EncryptedVaultCredentialStore = class {
|
|
1410
|
+
name = "Encrypted Vault (AES-256-GCM)";
|
|
1411
|
+
paths;
|
|
1412
|
+
constructor(paths) {
|
|
1413
|
+
this.paths = paths;
|
|
1414
|
+
}
|
|
1415
|
+
async isAvailable() {
|
|
1416
|
+
return true;
|
|
1417
|
+
}
|
|
1418
|
+
getMachineSecret() {
|
|
1419
|
+
const hostname = import_node_os2.default.hostname();
|
|
1420
|
+
const user = import_node_os2.default.userInfo().username;
|
|
1421
|
+
const homedir = import_node_os2.default.homedir();
|
|
1422
|
+
return `gitbridge-vault-key:${hostname}:${user}:${homedir}`;
|
|
1423
|
+
}
|
|
1424
|
+
deriveKey(salt) {
|
|
1425
|
+
const secret = this.getMachineSecret();
|
|
1426
|
+
return import_node_crypto.default.pbkdf2Sync(secret, salt, 1e5, 32, "sha256");
|
|
1427
|
+
}
|
|
1428
|
+
readVault() {
|
|
1429
|
+
const file = this.paths.getEncryptedVaultFile();
|
|
1430
|
+
if (!import_node_fs6.default.existsSync(file)) {
|
|
1431
|
+
return {};
|
|
1432
|
+
}
|
|
1433
|
+
try {
|
|
1434
|
+
const buffer = import_node_fs6.default.readFileSync(file);
|
|
1435
|
+
if (buffer.length < 16 + 12 + 16) {
|
|
1436
|
+
return {};
|
|
1437
|
+
}
|
|
1438
|
+
const salt = buffer.subarray(0, 16);
|
|
1439
|
+
const iv = buffer.subarray(16, 28);
|
|
1440
|
+
const tag = buffer.subarray(28, 44);
|
|
1441
|
+
const ciphertext = buffer.subarray(44);
|
|
1442
|
+
const key = this.deriveKey(salt);
|
|
1443
|
+
const decipher = import_node_crypto.default.createDecipheriv("aes-256-gcm", key, iv);
|
|
1444
|
+
decipher.setAuthTag(tag);
|
|
1445
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
1446
|
+
return JSON.parse(decrypted.toString("utf-8"));
|
|
1447
|
+
} catch {
|
|
1448
|
+
return {};
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
writeVault(data) {
|
|
1452
|
+
const file = this.paths.getEncryptedVaultFile();
|
|
1453
|
+
try {
|
|
1454
|
+
const dir = import_node_path6.default.dirname(file);
|
|
1455
|
+
if (!import_node_fs6.default.existsSync(dir)) {
|
|
1456
|
+
import_node_fs6.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1457
|
+
}
|
|
1458
|
+
const salt = import_node_crypto.default.randomBytes(16);
|
|
1459
|
+
const iv = import_node_crypto.default.randomBytes(12);
|
|
1460
|
+
const key = this.deriveKey(salt);
|
|
1461
|
+
const cipher = import_node_crypto.default.createCipheriv("aes-256-gcm", key, iv);
|
|
1462
|
+
const plaintext = Buffer.from(JSON.stringify(data), "utf-8");
|
|
1463
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
|
1464
|
+
const tag = cipher.getAuthTag();
|
|
1465
|
+
const payload = Buffer.concat([salt, iv, tag, ciphertext]);
|
|
1466
|
+
const tempFile = `${file}.tmp.${Date.now()}`;
|
|
1467
|
+
import_node_fs6.default.writeFileSync(tempFile, payload, { mode: 384 });
|
|
1468
|
+
import_node_fs6.default.renameSync(tempFile, file);
|
|
1469
|
+
} catch (err) {
|
|
1470
|
+
throw new CredentialStoreError(
|
|
1471
|
+
`Failed to write encrypted vault: ${err instanceof Error ? err.message : String(err)}`
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
makeKey(service, account) {
|
|
1476
|
+
return `${service}:::${account}`;
|
|
1477
|
+
}
|
|
1478
|
+
async set(service, account, secret) {
|
|
1479
|
+
const vault = this.readVault();
|
|
1480
|
+
vault[this.makeKey(service, account)] = secret;
|
|
1481
|
+
this.writeVault(vault);
|
|
1482
|
+
}
|
|
1483
|
+
async get(service, account) {
|
|
1484
|
+
const vault = this.readVault();
|
|
1485
|
+
return vault[this.makeKey(service, account)] ?? null;
|
|
1486
|
+
}
|
|
1487
|
+
async delete(service, account) {
|
|
1488
|
+
const vault = this.readVault();
|
|
1489
|
+
delete vault[this.makeKey(service, account)];
|
|
1490
|
+
this.writeVault(vault);
|
|
1491
|
+
}
|
|
1492
|
+
};
|
|
1493
|
+
|
|
1494
|
+
// src/core/storage/store-factory.ts
|
|
1495
|
+
var CompositeCredentialStore = class {
|
|
1496
|
+
name;
|
|
1497
|
+
primary;
|
|
1498
|
+
fallback;
|
|
1499
|
+
constructor(primary, fallback) {
|
|
1500
|
+
this.primary = primary;
|
|
1501
|
+
this.fallback = fallback;
|
|
1502
|
+
this.name = `${primary.name} (with Encrypted Vault fallback)`;
|
|
1503
|
+
}
|
|
1504
|
+
async isAvailable() {
|
|
1505
|
+
return true;
|
|
1506
|
+
}
|
|
1507
|
+
async set(service, account, secret) {
|
|
1508
|
+
try {
|
|
1509
|
+
await this.primary.set(service, account, secret);
|
|
1510
|
+
} catch {
|
|
1511
|
+
await this.fallback.set(service, account, secret);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
async get(service, account) {
|
|
1515
|
+
try {
|
|
1516
|
+
const val = await this.primary.get(service, account);
|
|
1517
|
+
if (val !== null && val.length > 0) {
|
|
1518
|
+
return val;
|
|
1519
|
+
}
|
|
1520
|
+
} catch {
|
|
1521
|
+
}
|
|
1522
|
+
return this.fallback.get(service, account);
|
|
1523
|
+
}
|
|
1524
|
+
async delete(service, account) {
|
|
1525
|
+
try {
|
|
1526
|
+
await this.primary.delete(service, account);
|
|
1527
|
+
} catch {
|
|
1528
|
+
}
|
|
1529
|
+
await this.fallback.delete(service, account);
|
|
1530
|
+
}
|
|
1531
|
+
};
|
|
1532
|
+
var StoreFactory = class {
|
|
1533
|
+
static async getStore(paths = defaultPathResolver, forceEncrypted = false) {
|
|
1534
|
+
const vault = new EncryptedVaultCredentialStore(paths);
|
|
1535
|
+
if (forceEncrypted || process.env.GITBRIDGE_USE_VAULT === "1" || process.env.NODE_ENV === "test") {
|
|
1536
|
+
return vault;
|
|
1537
|
+
}
|
|
1538
|
+
if (process.platform === "darwin") {
|
|
1539
|
+
const macStore = new MacOSKeychainCredentialStore();
|
|
1540
|
+
if (await macStore.isAvailable()) {
|
|
1541
|
+
return new CompositeCredentialStore(macStore, vault);
|
|
1542
|
+
}
|
|
1543
|
+
} else if (process.platform === "win32") {
|
|
1544
|
+
const winStore = new WindowsCredentialStore();
|
|
1545
|
+
if (await winStore.isAvailable()) {
|
|
1546
|
+
return new CompositeCredentialStore(winStore, vault);
|
|
1547
|
+
}
|
|
1548
|
+
} else if (process.platform === "linux") {
|
|
1549
|
+
const linuxStore = new LinuxKeyringCredentialStore();
|
|
1550
|
+
if (await linuxStore.isAvailable()) {
|
|
1551
|
+
return new CompositeCredentialStore(linuxStore, vault);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
return vault;
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
|
|
1558
|
+
// src/cli/commands/account.ts
|
|
1559
|
+
var import_picocolors8 = __toESM(require("picocolors"));
|
|
1560
|
+
async function handleAccountList(store = defaultConfigStore) {
|
|
1561
|
+
const accounts = store.loadAccounts();
|
|
1562
|
+
console.log(import_picocolors8.default.bold("\n AUTHENTICATED PROVIDER ACCOUNTS"));
|
|
1563
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
1564
|
+
console.log(renderAccountsTable(accounts));
|
|
1565
|
+
}
|
|
1566
|
+
async function handleAccountRemove(id, store = defaultConfigStore) {
|
|
1567
|
+
const account = store.getAccount(id);
|
|
1568
|
+
if (!account) {
|
|
1569
|
+
logger.error(`Account with ID '${id}' not found.`);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
const credStore = await StoreFactory.getStore(store.getPathResolver());
|
|
1573
|
+
await credStore.delete(account.host, account.id);
|
|
1574
|
+
store.removeAccount(id);
|
|
1575
|
+
const sshGen = new SshConfigGenerator(store);
|
|
1576
|
+
sshGen.generate();
|
|
1577
|
+
logger.success(`Removed account '${id}' and erased credentials.`);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
// src/cli/commands/auth.ts
|
|
1581
|
+
var import_ora = __toESM(require("ora"));
|
|
1582
|
+
var import_picocolors9 = __toESM(require("picocolors"));
|
|
1583
|
+
|
|
1584
|
+
// src/utils/http.ts
|
|
1585
|
+
async function requestJson(url, method = "GET", body, options = {}) {
|
|
1586
|
+
let targetUrl = url;
|
|
1587
|
+
if (options.params) {
|
|
1588
|
+
const searchParams = new URLSearchParams();
|
|
1589
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
1590
|
+
if (value !== void 0) {
|
|
1591
|
+
searchParams.append(key, String(value));
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
const queryString = searchParams.toString();
|
|
1595
|
+
if (queryString) {
|
|
1596
|
+
targetUrl += (url.includes("?") ? "&" : "?") + queryString;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
const controller = new AbortController();
|
|
1600
|
+
const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? 15e3);
|
|
1601
|
+
try {
|
|
1602
|
+
const headers = {
|
|
1603
|
+
"User-Agent": "GitBridge-CLI/0.1.0",
|
|
1604
|
+
Accept: "application/json",
|
|
1605
|
+
...options.headers
|
|
1606
|
+
};
|
|
1607
|
+
let serializedBody;
|
|
1608
|
+
if (body !== void 0) {
|
|
1609
|
+
if (typeof body === "string") {
|
|
1610
|
+
serializedBody = body;
|
|
1611
|
+
} else {
|
|
1612
|
+
serializedBody = JSON.stringify(body);
|
|
1613
|
+
if (!headers["Content-Type"]) {
|
|
1614
|
+
headers["Content-Type"] = "application/json";
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
const res = await fetch(targetUrl, {
|
|
1619
|
+
method,
|
|
1620
|
+
headers,
|
|
1621
|
+
body: serializedBody,
|
|
1622
|
+
signal: controller.signal
|
|
1623
|
+
});
|
|
1624
|
+
clearTimeout(timeout);
|
|
1625
|
+
let responseData;
|
|
1626
|
+
const contentType = res.headers.get("content-type") || "";
|
|
1627
|
+
if (contentType.includes("application/json")) {
|
|
1628
|
+
responseData = await res.json();
|
|
1629
|
+
} else {
|
|
1630
|
+
const text2 = await res.text();
|
|
1631
|
+
try {
|
|
1632
|
+
responseData = JSON.parse(text2);
|
|
1633
|
+
} catch {
|
|
1634
|
+
responseData = text2;
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
return {
|
|
1638
|
+
data: responseData,
|
|
1639
|
+
status: res.status,
|
|
1640
|
+
headers: res.headers
|
|
1641
|
+
};
|
|
1642
|
+
} catch (err) {
|
|
1643
|
+
clearTimeout(timeout);
|
|
1644
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1645
|
+
throw new Error(`Request to ${url} timed out after ${options.timeoutMs ?? 15e3}ms`);
|
|
1646
|
+
}
|
|
1647
|
+
throw err;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// src/core/providers/github.provider.ts
|
|
1652
|
+
var GITHUB_CLIENT_ID = process.env.GITBRIDGE_GITHUB_CLIENT_ID || "Iv1.b507a08c87ecfe98";
|
|
1653
|
+
var GitHubProvider = class {
|
|
1654
|
+
id = "github";
|
|
1655
|
+
name = "GitHub";
|
|
1656
|
+
defaultHost = "github.com";
|
|
1657
|
+
getApiUrl(host) {
|
|
1658
|
+
const targetHost = host || this.defaultHost;
|
|
1659
|
+
if (targetHost === "github.com") {
|
|
1660
|
+
return "https://api.github.com";
|
|
1661
|
+
}
|
|
1662
|
+
return `https://${targetHost}/api/v3`;
|
|
1663
|
+
}
|
|
1664
|
+
async validateToken(token, host) {
|
|
1665
|
+
try {
|
|
1666
|
+
const api = this.getApiUrl(host);
|
|
1667
|
+
const res = await requestJson(`${api}/user`, "GET", void 0, {
|
|
1668
|
+
headers: {
|
|
1669
|
+
Authorization: `Bearer ${token}`,
|
|
1670
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1671
|
+
}
|
|
1672
|
+
});
|
|
1673
|
+
return res.status === 200;
|
|
1674
|
+
} catch {
|
|
1675
|
+
return false;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
async getUser(token, host) {
|
|
1679
|
+
const api = this.getApiUrl(host);
|
|
1680
|
+
const res = await requestJson(`${api}/user`, "GET", void 0, {
|
|
1681
|
+
headers: {
|
|
1682
|
+
Authorization: `Bearer ${token}`,
|
|
1683
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
if (res.status !== 200 || !res.data.login) {
|
|
1687
|
+
throw new ProviderError(res.data.message || `Failed to fetch user (status: ${res.status})`, this.name);
|
|
1688
|
+
}
|
|
1689
|
+
return {
|
|
1690
|
+
id: String(res.data.id),
|
|
1691
|
+
username: res.data.login,
|
|
1692
|
+
displayName: res.data.name || res.data.login,
|
|
1693
|
+
email: res.data.email,
|
|
1694
|
+
avatarUrl: res.data.avatar_url
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
async listRepositories(token, host) {
|
|
1698
|
+
const api = this.getApiUrl(host);
|
|
1699
|
+
const res = await requestJson(`${api}/user/repos?per_page=50&sort=updated`, "GET", void 0, {
|
|
1700
|
+
headers: {
|
|
1701
|
+
Authorization: `Bearer ${token}`,
|
|
1702
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1703
|
+
}
|
|
1704
|
+
});
|
|
1705
|
+
if (res.status !== 200 || !Array.isArray(res.data)) {
|
|
1706
|
+
throw new ProviderError(`Failed to fetch repositories (status: ${res.status})`, this.name);
|
|
1707
|
+
}
|
|
1708
|
+
return res.data.map((r) => ({
|
|
1709
|
+
id: String(r.id),
|
|
1710
|
+
name: r.name,
|
|
1711
|
+
fullName: r.full_name,
|
|
1712
|
+
sshUrl: r.ssh_url,
|
|
1713
|
+
httpsUrl: r.clone_url,
|
|
1714
|
+
isPrivate: r.private,
|
|
1715
|
+
defaultBranch: r.default_branch || "main",
|
|
1716
|
+
htmlUrl: r.html_url
|
|
1717
|
+
}));
|
|
1718
|
+
}
|
|
1719
|
+
async checkHealth(host) {
|
|
1720
|
+
const api = this.getApiUrl(host);
|
|
1721
|
+
const start = Date.now();
|
|
1722
|
+
try {
|
|
1723
|
+
const res = await requestJson(`${api}/rate_limit`, "GET", void 0, { timeoutMs: 8e3 });
|
|
1724
|
+
const pingMs = Date.now() - start;
|
|
1725
|
+
return {
|
|
1726
|
+
apiOk: res.status === 200 || res.status === 401 || res.status === 403,
|
|
1727
|
+
pingMs,
|
|
1728
|
+
message: "GitHub API reachable"
|
|
1729
|
+
};
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
return {
|
|
1732
|
+
apiOk: false,
|
|
1733
|
+
pingMs: Date.now() - start,
|
|
1734
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
async startDeviceFlow() {
|
|
1739
|
+
const res = await requestJson("https://github.com/login/device/code", "POST", {
|
|
1740
|
+
client_id: GITHUB_CLIENT_ID,
|
|
1741
|
+
scope: "repo,read:user,user:email"
|
|
1742
|
+
});
|
|
1743
|
+
if (!res.data.device_code) {
|
|
1744
|
+
throw new ProviderError(res.data.error || "Failed to start GitHub device flow", this.name);
|
|
1745
|
+
}
|
|
1746
|
+
return {
|
|
1747
|
+
deviceCode: res.data.device_code,
|
|
1748
|
+
userCode: res.data.user_code,
|
|
1749
|
+
verificationUri: res.data.verification_uri,
|
|
1750
|
+
expiresIn: res.data.expires_in,
|
|
1751
|
+
interval: res.data.interval || 5
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
async pollDeviceFlow(deviceCode, interval = 5) {
|
|
1755
|
+
const pollIntervalMs = (interval + 1) * 1e3;
|
|
1756
|
+
const maxAttempts = 60;
|
|
1757
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
1758
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
1759
|
+
const res = await requestJson("https://github.com/login/oauth/access_token", "POST", {
|
|
1760
|
+
client_id: GITHUB_CLIENT_ID,
|
|
1761
|
+
device_code: deviceCode,
|
|
1762
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code"
|
|
1763
|
+
});
|
|
1764
|
+
if (res.data.access_token) {
|
|
1765
|
+
return { token: res.data.access_token };
|
|
1766
|
+
}
|
|
1767
|
+
if (res.data.error === "authorization_pending") {
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
if (res.data.error === "slow_down") {
|
|
1771
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
if (res.data.error) {
|
|
1775
|
+
throw new ProviderError(res.data.error_description || res.data.error, this.name);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
throw new ProviderError("Device authorization timed out.", this.name);
|
|
1779
|
+
}
|
|
1780
|
+
};
|
|
1781
|
+
|
|
1782
|
+
// src/core/providers/gitlab.provider.ts
|
|
1783
|
+
var GitLabProvider = class {
|
|
1784
|
+
id = "gitlab";
|
|
1785
|
+
name = "GitLab";
|
|
1786
|
+
defaultHost = "gitlab.com";
|
|
1787
|
+
getApiUrl(host) {
|
|
1788
|
+
const targetHost = host || this.defaultHost;
|
|
1789
|
+
return `https://${targetHost}/api/v4`;
|
|
1790
|
+
}
|
|
1791
|
+
getAuthHeader(token) {
|
|
1792
|
+
if (token.startsWith("glpat-") || token.length < 30) {
|
|
1793
|
+
return { "PRIVATE-TOKEN": token };
|
|
1794
|
+
}
|
|
1795
|
+
return { Authorization: `Bearer ${token}` };
|
|
1796
|
+
}
|
|
1797
|
+
async validateToken(token, host) {
|
|
1798
|
+
try {
|
|
1799
|
+
const api = this.getApiUrl(host);
|
|
1800
|
+
const res = await requestJson(`${api}/user`, "GET", void 0, {
|
|
1801
|
+
headers: this.getAuthHeader(token)
|
|
1802
|
+
});
|
|
1803
|
+
return res.status === 200;
|
|
1804
|
+
} catch {
|
|
1805
|
+
return false;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
async getUser(token, host) {
|
|
1809
|
+
const api = this.getApiUrl(host);
|
|
1810
|
+
const res = await requestJson(`${api}/user`, "GET", void 0, {
|
|
1811
|
+
headers: this.getAuthHeader(token)
|
|
1812
|
+
});
|
|
1813
|
+
if (res.status !== 200 || !res.data.username) {
|
|
1814
|
+
throw new ProviderError(res.data.message || `Failed to fetch GitLab user (status: ${res.status})`, this.name);
|
|
1815
|
+
}
|
|
1816
|
+
return {
|
|
1817
|
+
id: String(res.data.id),
|
|
1818
|
+
username: res.data.username,
|
|
1819
|
+
displayName: res.data.name || res.data.username,
|
|
1820
|
+
email: res.data.email,
|
|
1821
|
+
avatarUrl: res.data.avatar_url
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
async listRepositories(token, host) {
|
|
1825
|
+
const api = this.getApiUrl(host);
|
|
1826
|
+
const res = await requestJson(`${api}/projects?membership=true&simple=true&per_page=50&order_by=updated_at`, "GET", void 0, {
|
|
1827
|
+
headers: this.getAuthHeader(token)
|
|
1828
|
+
});
|
|
1829
|
+
if (res.status !== 200 || !Array.isArray(res.data)) {
|
|
1830
|
+
throw new ProviderError(`Failed to fetch GitLab projects (status: ${res.status})`, this.name);
|
|
1831
|
+
}
|
|
1832
|
+
return res.data.map((r) => ({
|
|
1833
|
+
id: String(r.id),
|
|
1834
|
+
name: r.name,
|
|
1835
|
+
fullName: r.path_with_namespace,
|
|
1836
|
+
sshUrl: r.ssh_url_to_repo,
|
|
1837
|
+
httpsUrl: r.http_url_to_repo,
|
|
1838
|
+
isPrivate: r.visibility === "private",
|
|
1839
|
+
defaultBranch: r.default_branch || "main",
|
|
1840
|
+
htmlUrl: r.web_url
|
|
1841
|
+
}));
|
|
1842
|
+
}
|
|
1843
|
+
async checkHealth(host) {
|
|
1844
|
+
const api = this.getApiUrl(host);
|
|
1845
|
+
const start = Date.now();
|
|
1846
|
+
try {
|
|
1847
|
+
const res = await requestJson(`${api}/version`, "GET", void 0, { timeoutMs: 8e3 });
|
|
1848
|
+
const pingMs = Date.now() - start;
|
|
1849
|
+
return {
|
|
1850
|
+
apiOk: res.status === 200 || res.status === 401,
|
|
1851
|
+
// 401 means API endpoint reached but unauthenticated
|
|
1852
|
+
pingMs,
|
|
1853
|
+
message: "GitLab API reachable"
|
|
1854
|
+
};
|
|
1855
|
+
} catch (err) {
|
|
1856
|
+
return {
|
|
1857
|
+
apiOk: false,
|
|
1858
|
+
pingMs: Date.now() - start,
|
|
1859
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
|
|
1865
|
+
// src/core/providers/bitbucket.provider.ts
|
|
1866
|
+
var BitbucketProvider = class {
|
|
1867
|
+
id = "bitbucket";
|
|
1868
|
+
name = "Bitbucket";
|
|
1869
|
+
defaultHost = "bitbucket.org";
|
|
1870
|
+
getApiUrl(host) {
|
|
1871
|
+
const targetHost = host || this.defaultHost;
|
|
1872
|
+
if (targetHost === "bitbucket.org") {
|
|
1873
|
+
return "https://api.bitbucket.org/2.0";
|
|
1874
|
+
}
|
|
1875
|
+
return `https://${targetHost}/rest/api/1.0`;
|
|
1876
|
+
}
|
|
1877
|
+
getAuthHeader(token) {
|
|
1878
|
+
if (token.includes(":")) {
|
|
1879
|
+
const encoded = Buffer.from(token).toString("base64");
|
|
1880
|
+
return { Authorization: `Basic ${encoded}` };
|
|
1881
|
+
}
|
|
1882
|
+
return { Authorization: `Bearer ${token}` };
|
|
1883
|
+
}
|
|
1884
|
+
async validateToken(token, host) {
|
|
1885
|
+
try {
|
|
1886
|
+
const api = this.getApiUrl(host);
|
|
1887
|
+
const res = await requestJson(`${api}/user`, "GET", void 0, {
|
|
1888
|
+
headers: this.getAuthHeader(token)
|
|
1889
|
+
});
|
|
1890
|
+
return res.status === 200;
|
|
1891
|
+
} catch {
|
|
1892
|
+
return false;
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
async getUser(token, host) {
|
|
1896
|
+
const api = this.getApiUrl(host);
|
|
1897
|
+
const res = await requestJson(`${api}/user`, "GET", void 0, {
|
|
1898
|
+
headers: this.getAuthHeader(token)
|
|
1899
|
+
});
|
|
1900
|
+
if (res.status !== 200 || !res.data.username && !res.data.display_name) {
|
|
1901
|
+
throw new ProviderError(res.data.error?.message || `Failed to fetch Bitbucket user`, this.name);
|
|
1902
|
+
}
|
|
1903
|
+
const username = res.data.username || res.data.display_name?.replace(/\s+/g, "").toLowerCase() || "user";
|
|
1904
|
+
return {
|
|
1905
|
+
id: res.data.account_id || res.data.uuid || username,
|
|
1906
|
+
username,
|
|
1907
|
+
displayName: res.data.display_name || username,
|
|
1908
|
+
avatarUrl: res.data.links?.avatar?.href
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
async listRepositories(token, host) {
|
|
1912
|
+
const user = await this.getUser(token, host);
|
|
1913
|
+
const api = this.getApiUrl(host);
|
|
1914
|
+
const res = await requestJson(`${api}/repositories/${user.username}?pagelen=50&sort=-updated_on`, "GET", void 0, {
|
|
1915
|
+
headers: this.getAuthHeader(token)
|
|
1916
|
+
});
|
|
1917
|
+
if (res.status !== 200 || !res.data.values) {
|
|
1918
|
+
return [];
|
|
1919
|
+
}
|
|
1920
|
+
return res.data.values.map((r) => {
|
|
1921
|
+
const sshClone = r.links?.clone?.find((c) => c.name === "ssh")?.href || `git@bitbucket.org:${r.full_name}.git`;
|
|
1922
|
+
const httpsClone = r.links?.clone?.find((c) => c.name === "https")?.href || `https://bitbucket.org/${r.full_name}.git`;
|
|
1923
|
+
return {
|
|
1924
|
+
id: r.uuid,
|
|
1925
|
+
name: r.name,
|
|
1926
|
+
fullName: r.full_name,
|
|
1927
|
+
sshUrl: sshClone,
|
|
1928
|
+
httpsUrl: httpsClone,
|
|
1929
|
+
isPrivate: r.is_private,
|
|
1930
|
+
defaultBranch: r.mainbranch?.name || "main",
|
|
1931
|
+
htmlUrl: r.links?.html?.href
|
|
1932
|
+
};
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
async checkHealth(host) {
|
|
1936
|
+
const api = this.getApiUrl(host);
|
|
1937
|
+
const start = Date.now();
|
|
1938
|
+
try {
|
|
1939
|
+
const res = await requestJson(`${api}/user`, "GET", void 0, { timeoutMs: 8e3 });
|
|
1940
|
+
const pingMs = Date.now() - start;
|
|
1941
|
+
return {
|
|
1942
|
+
apiOk: res.status === 200 || res.status === 401,
|
|
1943
|
+
pingMs,
|
|
1944
|
+
message: "Bitbucket API reachable"
|
|
1945
|
+
};
|
|
1946
|
+
} catch (err) {
|
|
1947
|
+
return {
|
|
1948
|
+
apiOk: false,
|
|
1949
|
+
pingMs: Date.now() - start,
|
|
1950
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
|
|
1956
|
+
// src/core/providers/provider-registry.ts
|
|
1957
|
+
var ProviderRegistry = class {
|
|
1958
|
+
providers = /* @__PURE__ */ new Map();
|
|
1959
|
+
constructor() {
|
|
1960
|
+
this.register(new GitHubProvider());
|
|
1961
|
+
this.register(new GitLabProvider());
|
|
1962
|
+
this.register(new BitbucketProvider());
|
|
1963
|
+
}
|
|
1964
|
+
register(provider) {
|
|
1965
|
+
this.providers.set(provider.id, provider);
|
|
1966
|
+
}
|
|
1967
|
+
get(idOrType) {
|
|
1968
|
+
return this.providers.get(idOrType);
|
|
1969
|
+
}
|
|
1970
|
+
getByHost(host) {
|
|
1971
|
+
const clean = host.toLowerCase();
|
|
1972
|
+
if (clean.includes("github.com") || clean.startsWith("github")) {
|
|
1973
|
+
return this.providers.get("github");
|
|
1974
|
+
}
|
|
1975
|
+
if (clean.includes("gitlab.com") || clean.startsWith("gitlab")) {
|
|
1976
|
+
return this.providers.get("gitlab");
|
|
1977
|
+
}
|
|
1978
|
+
if (clean.includes("bitbucket.org") || clean.startsWith("bitbucket")) {
|
|
1979
|
+
return this.providers.get("bitbucket");
|
|
1980
|
+
}
|
|
1981
|
+
for (const provider of this.providers.values()) {
|
|
1982
|
+
if (provider.defaultHost === host) {
|
|
1983
|
+
return provider;
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
return void 0;
|
|
1987
|
+
}
|
|
1988
|
+
list() {
|
|
1989
|
+
return Array.from(this.providers.values());
|
|
1990
|
+
}
|
|
1991
|
+
};
|
|
1992
|
+
var defaultProviderRegistry = new ProviderRegistry();
|
|
1993
|
+
|
|
1994
|
+
// src/core/ssh/ssh-key-detector.ts
|
|
1995
|
+
var import_node_fs7 = __toESM(require("node:fs"));
|
|
1996
|
+
var import_node_path7 = __toESM(require("node:path"));
|
|
1997
|
+
var SshKeyDetector = class {
|
|
1998
|
+
static listAvailableKeys(sshDir) {
|
|
1999
|
+
const targetDir = sshDir || import_node_path7.default.join(getHomeDir(), ".ssh");
|
|
2000
|
+
if (!import_node_fs7.default.existsSync(targetDir)) {
|
|
2001
|
+
return [];
|
|
2002
|
+
}
|
|
2003
|
+
const files = import_node_fs7.default.readdirSync(targetDir);
|
|
2004
|
+
const pubFiles = files.filter((f) => f.endsWith(".pub"));
|
|
2005
|
+
const keys = [];
|
|
2006
|
+
for (const pub of pubFiles) {
|
|
2007
|
+
const pubPath = import_node_path7.default.join(targetDir, pub);
|
|
2008
|
+
const privName = pub.slice(0, -4);
|
|
2009
|
+
const privPath = import_node_path7.default.join(targetDir, privName);
|
|
2010
|
+
try {
|
|
2011
|
+
const content = import_node_fs7.default.readFileSync(pubPath, "utf-8").trim();
|
|
2012
|
+
const parts = content.split(/\s+/);
|
|
2013
|
+
const type = parts[0] || "unknown";
|
|
2014
|
+
const comment = parts.length > 2 ? parts.slice(2).join(" ") : void 0;
|
|
2015
|
+
keys.push({
|
|
2016
|
+
name: privName,
|
|
2017
|
+
privateKeyPath: collapseTilde(privPath),
|
|
2018
|
+
publicKeyPath: collapseTilde(pubPath),
|
|
2019
|
+
type,
|
|
2020
|
+
comment
|
|
2021
|
+
});
|
|
2022
|
+
} catch {
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
return keys;
|
|
2026
|
+
}
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
// src/cli/commands/auth.ts
|
|
2030
|
+
async function handleAuthLogin(providerName, options = {}, store = defaultConfigStore) {
|
|
2031
|
+
let targetProvider = providerName?.toLowerCase();
|
|
2032
|
+
if (!targetProvider) {
|
|
2033
|
+
targetProvider = await promptSelect({
|
|
2034
|
+
message: "Select Git Provider to authenticate with:",
|
|
2035
|
+
options: [
|
|
2036
|
+
{ value: "github", label: "GitHub (github.com / Enterprise)" },
|
|
2037
|
+
{ value: "gitlab", label: "GitLab (gitlab.com / Self-Hosted)" },
|
|
2038
|
+
{ value: "bitbucket", label: "Bitbucket (bitbucket.org / Server)" }
|
|
2039
|
+
]
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
const provider = defaultProviderRegistry.get(targetProvider);
|
|
2043
|
+
if (!provider) {
|
|
2044
|
+
logger.error(`Unknown provider: '${targetProvider}'`);
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const host = options.host || provider.defaultHost;
|
|
2048
|
+
let token = options.token;
|
|
2049
|
+
let username = "";
|
|
2050
|
+
if (!token) {
|
|
2051
|
+
if (targetProvider === "github" && provider.startDeviceFlow && provider.pollDeviceFlow) {
|
|
2052
|
+
const method = await promptSelect({
|
|
2053
|
+
message: "Choose authentication method for GitHub:",
|
|
2054
|
+
options: [
|
|
2055
|
+
{ value: "device", label: "Web Browser (Device Code Authorization)" },
|
|
2056
|
+
{ value: "pat", label: "Personal Access Token (PAT)" }
|
|
2057
|
+
]
|
|
2058
|
+
});
|
|
2059
|
+
if (method === "device") {
|
|
2060
|
+
const spinner2 = (0, import_ora.default)("Initiating GitHub Device Authorization...").start();
|
|
2061
|
+
try {
|
|
2062
|
+
const deviceFlow = await provider.startDeviceFlow();
|
|
2063
|
+
spinner2.stop();
|
|
2064
|
+
console.log("\n " + import_picocolors9.default.bold(import_picocolors9.default.cyan("GitHub Device Authorization")));
|
|
2065
|
+
console.log(` 1. Open your browser: ${import_picocolors9.default.underline(import_picocolors9.default.cyan(deviceFlow.verificationUri))}`);
|
|
2066
|
+
console.log(` 2. Enter the one-time code: ${import_picocolors9.default.bold(import_picocolors9.default.green(deviceFlow.userCode))}
|
|
2067
|
+
`);
|
|
2068
|
+
const pollSpinner = (0, import_ora.default)("Waiting for browser authorization...").start();
|
|
2069
|
+
const authResult = await provider.pollDeviceFlow(deviceFlow.deviceCode, deviceFlow.interval);
|
|
2070
|
+
token = authResult.token;
|
|
2071
|
+
pollSpinner.succeed("Authorization granted!");
|
|
2072
|
+
} catch (err) {
|
|
2073
|
+
spinner2.fail("Device flow failed.");
|
|
2074
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
if (!token) {
|
|
2080
|
+
token = await promptText({
|
|
2081
|
+
message: `Enter your Personal Access Token (PAT) for ${provider.name} (${host}):`,
|
|
2082
|
+
validate: (val) => !val || !val.trim() ? "Token cannot be empty." : void 0
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
const spinner = (0, import_ora.default)(`Validating credentials with ${provider.name}...`).start();
|
|
2087
|
+
try {
|
|
2088
|
+
const user = await provider.getUser(token, host);
|
|
2089
|
+
username = user.username;
|
|
2090
|
+
spinner.succeed(`Authenticated as ${import_picocolors9.default.green(user.displayName || user.username)} (@${user.username})`);
|
|
2091
|
+
let sshKeyPath = options.sshKey;
|
|
2092
|
+
if (!sshKeyPath) {
|
|
2093
|
+
const availableKeys = SshKeyDetector.listAvailableKeys();
|
|
2094
|
+
if (availableKeys.length > 0) {
|
|
2095
|
+
const linkKey = await promptConfirm({
|
|
2096
|
+
message: `Do you want to link an SSH key for this ${provider.name} account?`,
|
|
2097
|
+
initialValue: true
|
|
2098
|
+
});
|
|
2099
|
+
if (linkKey) {
|
|
2100
|
+
sshKeyPath = await promptSelect({
|
|
2101
|
+
message: "Select SSH key to link:",
|
|
2102
|
+
options: availableKeys.map((k) => ({
|
|
2103
|
+
value: k.privateKeyPath,
|
|
2104
|
+
label: `${k.name} (${k.type})`,
|
|
2105
|
+
hint: k.comment
|
|
2106
|
+
}))
|
|
2107
|
+
});
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
const accountId = `${targetProvider}_${username.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
2112
|
+
store.addAccount({
|
|
2113
|
+
id: accountId,
|
|
2114
|
+
providerId: targetProvider,
|
|
2115
|
+
host,
|
|
2116
|
+
username,
|
|
2117
|
+
displayName: user.displayName,
|
|
2118
|
+
authType: "oauth",
|
|
2119
|
+
sshKeyPath
|
|
2120
|
+
});
|
|
2121
|
+
const credStore = await StoreFactory.getStore(store.getPathResolver());
|
|
2122
|
+
await credStore.set(host, accountId, token);
|
|
2123
|
+
const sshGen = new SshConfigGenerator(store);
|
|
2124
|
+
sshGen.generate();
|
|
2125
|
+
logger.success(`Account '${accountId}' registered and token secured in OS Keychain/Keyring!`);
|
|
2126
|
+
console.log(import_picocolors9.default.gray(` Host: ${host}`));
|
|
2127
|
+
console.log(import_picocolors9.default.gray(` Username: ${username}`));
|
|
2128
|
+
if (sshKeyPath) {
|
|
2129
|
+
console.log(import_picocolors9.default.gray(` SSH Alias: ${host}-${accountId}`));
|
|
2130
|
+
}
|
|
2131
|
+
console.log("");
|
|
2132
|
+
} catch (err) {
|
|
2133
|
+
spinner.fail(`Authentication failed.`);
|
|
2134
|
+
logger.error(err instanceof Error ? err.message : String(err));
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
async function handleAuthLogout(providerName, username, store = defaultConfigStore) {
|
|
2138
|
+
const accounts = store.loadAccounts().filter((a) => a.providerId === providerName);
|
|
2139
|
+
if (accounts.length === 0) {
|
|
2140
|
+
logger.warn(`No accounts found for provider '${providerName}'.`);
|
|
2141
|
+
return;
|
|
2142
|
+
}
|
|
2143
|
+
let targetAccount = accounts[0];
|
|
2144
|
+
if (username) {
|
|
2145
|
+
const found = accounts.find((a) => a.username === username);
|
|
2146
|
+
if (!found) {
|
|
2147
|
+
logger.error(`Account '${username}' not found for provider '${providerName}'.`);
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
targetAccount = found;
|
|
2151
|
+
}
|
|
2152
|
+
const credStore = await StoreFactory.getStore(store.getPathResolver());
|
|
2153
|
+
await credStore.delete(targetAccount.host, targetAccount.id);
|
|
2154
|
+
store.removeAccount(targetAccount.id);
|
|
2155
|
+
const sshGen = new SshConfigGenerator(store);
|
|
2156
|
+
sshGen.generate();
|
|
2157
|
+
logger.success(`Logged out of ${targetAccount.providerId} (${targetAccount.username}). Credentials removed.`);
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
// src/cli/commands/provider.ts
|
|
2161
|
+
var import_cli_table32 = __toESM(require("cli-table3"));
|
|
2162
|
+
var import_picocolors10 = __toESM(require("picocolors"));
|
|
2163
|
+
async function handleProviderList() {
|
|
2164
|
+
const providers = defaultProviderRegistry.list();
|
|
2165
|
+
console.log(import_picocolors10.default.bold("\n GIT PROVIDERS"));
|
|
2166
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2167
|
+
const table = new import_cli_table32.default({
|
|
2168
|
+
head: [import_picocolors10.default.bold("ID"), import_picocolors10.default.bold("Name"), import_picocolors10.default.bold("Default Host"), import_picocolors10.default.bold("Status")],
|
|
2169
|
+
style: { head: ["cyan"] }
|
|
2170
|
+
});
|
|
2171
|
+
for (const p2 of providers) {
|
|
2172
|
+
table.push([import_picocolors10.default.cyan(p2.id), p2.name, p2.defaultHost, import_picocolors10.default.green("\u2714 supported")]);
|
|
2173
|
+
}
|
|
2174
|
+
console.log(table.toString());
|
|
2175
|
+
console.log("");
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/cli/commands/rule.ts
|
|
2179
|
+
var import_node_path8 = __toESM(require("node:path"));
|
|
2180
|
+
var import_picocolors11 = __toESM(require("picocolors"));
|
|
2181
|
+
async function handleRuleList(store = defaultConfigStore) {
|
|
2182
|
+
const rules = store.loadRules();
|
|
2183
|
+
console.log(import_picocolors11.default.bold("\n DIRECTORY ROUTING RULES"));
|
|
2184
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2185
|
+
console.log(renderRulesTable(rules));
|
|
2186
|
+
}
|
|
2187
|
+
async function handleRuleAdd(dirPath, identityId, options = {}, store = defaultConfigStore) {
|
|
2188
|
+
const identities = store.loadIdentities();
|
|
2189
|
+
const accounts = store.loadAccounts();
|
|
2190
|
+
if (identities.length === 0) {
|
|
2191
|
+
logger.warn("No identities found. Please create an identity first with 'gitbridge identity add'.");
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
let targetPath = dirPath;
|
|
2195
|
+
if (!targetPath) {
|
|
2196
|
+
targetPath = await promptText({
|
|
2197
|
+
message: "Enter the directory path to map (e.g. ~/Projects/company or /path/to/work):",
|
|
2198
|
+
validate: (val) => !val || !val.trim() ? "Directory path is required." : void 0
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
targetPath = collapseTilde(import_node_path8.default.resolve(expandTilde(targetPath.trim())));
|
|
2202
|
+
let targetIdentity = identityId;
|
|
2203
|
+
if (!targetIdentity) {
|
|
2204
|
+
targetIdentity = await promptSelect({
|
|
2205
|
+
message: `Select identity to assign to '${targetPath}':`,
|
|
2206
|
+
options: identities.map((i) => ({
|
|
2207
|
+
value: i.id,
|
|
2208
|
+
label: `${i.id} (${i.name} <${i.email}>)`
|
|
2209
|
+
}))
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
const ruleId = options.id || `rule_${import_node_path8.default.basename(targetPath).replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
2213
|
+
let defaultProvider = options.provider;
|
|
2214
|
+
let defaultAccountId = options.account;
|
|
2215
|
+
if (accounts.length > 0 && !defaultAccountId) {
|
|
2216
|
+
const matchingAccounts = accounts.filter((a) => a.username.includes(targetIdentity) || a.id.includes(targetIdentity));
|
|
2217
|
+
if (matchingAccounts.length > 0) {
|
|
2218
|
+
defaultAccountId = matchingAccounts[0].id;
|
|
2219
|
+
defaultProvider = matchingAccounts[0].providerId;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
store.addRule({
|
|
2223
|
+
id: ruleId,
|
|
2224
|
+
path: targetPath,
|
|
2225
|
+
identityId: targetIdentity,
|
|
2226
|
+
defaultProvider,
|
|
2227
|
+
defaultAccountId
|
|
2228
|
+
});
|
|
2229
|
+
const generator = new GitConfigGenerator(store);
|
|
2230
|
+
generator.generate();
|
|
2231
|
+
logger.success(`Directory rule '${ruleId}' added successfully!`);
|
|
2232
|
+
console.log(import_picocolors11.default.gray(` Path: ${targetPath}`));
|
|
2233
|
+
console.log(import_picocolors11.default.gray(` Identity: ${targetIdentity}`));
|
|
2234
|
+
if (defaultAccountId) {
|
|
2235
|
+
console.log(import_picocolors11.default.gray(` Account: ${defaultAccountId}`));
|
|
2236
|
+
}
|
|
2237
|
+
console.log(import_picocolors11.default.green(" Git includeIf rules regenerated."));
|
|
2238
|
+
console.log("");
|
|
2239
|
+
}
|
|
2240
|
+
async function handleRuleRemove(idOrPath, store = defaultConfigStore) {
|
|
2241
|
+
const removed = store.removeRule(idOrPath);
|
|
2242
|
+
if (!removed) {
|
|
2243
|
+
logger.error(`Rule '${idOrPath}' not found.`);
|
|
2244
|
+
return;
|
|
2245
|
+
}
|
|
2246
|
+
const generator = new GitConfigGenerator(store);
|
|
2247
|
+
generator.generate();
|
|
2248
|
+
logger.success(`Directory rule '${idOrPath}' removed.`);
|
|
2249
|
+
}
|
|
2250
|
+
|
|
2251
|
+
// src/cli/commands/repo.ts
|
|
2252
|
+
var import_node_path11 = __toESM(require("node:path"));
|
|
2253
|
+
|
|
2254
|
+
// src/core/git/url-parser.ts
|
|
2255
|
+
function detectProviderType(host) {
|
|
2256
|
+
const cleanHost = host.toLowerCase();
|
|
2257
|
+
if (cleanHost.includes("github.com") || cleanHost === "github") {
|
|
2258
|
+
return "github";
|
|
2259
|
+
}
|
|
2260
|
+
if (cleanHost.includes("gitlab.com") || cleanHost === "gitlab") {
|
|
2261
|
+
return "gitlab";
|
|
2262
|
+
}
|
|
2263
|
+
if (cleanHost.includes("bitbucket.org") || cleanHost === "bitbucket") {
|
|
2264
|
+
return "bitbucket";
|
|
2265
|
+
}
|
|
2266
|
+
if (cleanHost.includes("gitea") || cleanHost.includes("codeberg")) {
|
|
2267
|
+
return "gitea";
|
|
2268
|
+
}
|
|
2269
|
+
return "custom";
|
|
2270
|
+
}
|
|
2271
|
+
function extractHostAndAlias(rawHost) {
|
|
2272
|
+
const knownDomains = [".com-", ".org-", ".net-", ".io-", ".dev-", ".co-"];
|
|
2273
|
+
for (const kd of knownDomains) {
|
|
2274
|
+
const idx = rawHost.indexOf(kd);
|
|
2275
|
+
if (idx !== -1) {
|
|
2276
|
+
const extLen = kd.length - 1;
|
|
2277
|
+
const host = rawHost.substring(0, idx + extLen);
|
|
2278
|
+
const accountAlias = rawHost.substring(idx + kd.length);
|
|
2279
|
+
if (accountAlias) {
|
|
2280
|
+
return { host, accountAlias };
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
const shortPrefixes = ["github-", "gitlab-", "bitbucket-"];
|
|
2285
|
+
for (const sp of shortPrefixes) {
|
|
2286
|
+
if (rawHost.startsWith(sp)) {
|
|
2287
|
+
const provider = sp.slice(0, -1);
|
|
2288
|
+
const host = provider === "bitbucket" ? "bitbucket.org" : `${provider}.com`;
|
|
2289
|
+
const accountAlias = rawHost.slice(sp.length);
|
|
2290
|
+
return { host, accountAlias };
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
return { host: rawHost };
|
|
2294
|
+
}
|
|
2295
|
+
function parseRemoteUrl(url) {
|
|
2296
|
+
if (!url || typeof url !== "string") return null;
|
|
2297
|
+
const trimmed = url.trim();
|
|
2298
|
+
const scpMatch = trimmed.match(/^(?:([a-zA-Z0-9._-]+)@)?([^:]+):(.+)$/);
|
|
2299
|
+
if (scpMatch && !trimmed.startsWith("http://") && !trimmed.startsWith("https://") && !trimmed.startsWith("ssh://") && !trimmed.startsWith("file://")) {
|
|
2300
|
+
const [, , rawHost, pathPart] = scpMatch;
|
|
2301
|
+
const { host, accountAlias } = extractHostAndAlias(rawHost);
|
|
2302
|
+
const cleanPath = pathPart.replace(/^\/+/, "").replace(/\.git$/, "");
|
|
2303
|
+
const pathSegments = cleanPath.split("/");
|
|
2304
|
+
const repo = pathSegments.pop() || "";
|
|
2305
|
+
const owner = pathSegments.join("/");
|
|
2306
|
+
return {
|
|
2307
|
+
rawUrl: trimmed,
|
|
2308
|
+
protocol: "ssh",
|
|
2309
|
+
providerId: detectProviderType(host),
|
|
2310
|
+
host,
|
|
2311
|
+
rawHost,
|
|
2312
|
+
accountAlias,
|
|
2313
|
+
owner,
|
|
2314
|
+
repo,
|
|
2315
|
+
fullName: owner ? `${owner}/${repo}` : repo
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
try {
|
|
2319
|
+
const parsed = new URL(trimmed);
|
|
2320
|
+
const rawHost = parsed.hostname;
|
|
2321
|
+
const { host, accountAlias } = extractHostAndAlias(rawHost);
|
|
2322
|
+
let protocol = "unknown";
|
|
2323
|
+
if (parsed.protocol === "https:" || parsed.protocol === "http:") {
|
|
2324
|
+
protocol = "https";
|
|
2325
|
+
} else if (parsed.protocol === "ssh:" || parsed.protocol === "git+ssh:") {
|
|
2326
|
+
protocol = "ssh";
|
|
2327
|
+
} else if (parsed.protocol === "file:") {
|
|
2328
|
+
protocol = "file";
|
|
2329
|
+
}
|
|
2330
|
+
const cleanPath = parsed.pathname.replace(/^\/+/, "").replace(/\.git$/, "");
|
|
2331
|
+
const pathSegments = cleanPath.split("/");
|
|
2332
|
+
const repo = pathSegments.pop() || "";
|
|
2333
|
+
const owner = pathSegments.join("/");
|
|
2334
|
+
return {
|
|
2335
|
+
rawUrl: trimmed,
|
|
2336
|
+
protocol,
|
|
2337
|
+
providerId: detectProviderType(host),
|
|
2338
|
+
host,
|
|
2339
|
+
rawHost,
|
|
2340
|
+
accountAlias,
|
|
2341
|
+
owner,
|
|
2342
|
+
repo,
|
|
2343
|
+
fullName: owner ? `${owner}/${repo}` : repo
|
|
2344
|
+
};
|
|
2345
|
+
} catch {
|
|
2346
|
+
return null;
|
|
2347
|
+
}
|
|
2348
|
+
}
|
|
2349
|
+
function buildSshUrl(host, owner, repo, accountId) {
|
|
2350
|
+
const targetHost = accountId ? `${host}-${accountId}` : host;
|
|
2351
|
+
const cleanRepo = repo.endsWith(".git") ? repo : `${repo}.git`;
|
|
2352
|
+
return `git@${targetHost}:${owner}/${cleanRepo}`;
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// src/core/git/git-cli.ts
|
|
2356
|
+
var GitCli = class {
|
|
2357
|
+
cwd;
|
|
2358
|
+
constructor(cwd = process.cwd()) {
|
|
2359
|
+
this.cwd = cwd;
|
|
2360
|
+
}
|
|
2361
|
+
getCwd() {
|
|
2362
|
+
return this.cwd;
|
|
2363
|
+
}
|
|
2364
|
+
async exec(args, options = {}) {
|
|
2365
|
+
const targetCwd = options.cwd ?? this.cwd;
|
|
2366
|
+
try {
|
|
2367
|
+
const proc = Bun.spawn(["git", ...args], {
|
|
2368
|
+
cwd: targetCwd,
|
|
2369
|
+
stdout: "pipe",
|
|
2370
|
+
stderr: "pipe",
|
|
2371
|
+
env: { ...process.env, ...options.env }
|
|
2372
|
+
});
|
|
2373
|
+
const [stdout, stderr] = await Promise.all([
|
|
2374
|
+
new Response(proc.stdout).text(),
|
|
2375
|
+
new Response(proc.stderr).text()
|
|
2376
|
+
]);
|
|
2377
|
+
const exitCode = await proc.exited;
|
|
2378
|
+
if (exitCode !== 0 && !options.allowFailure) {
|
|
2379
|
+
throw new GitCliError(
|
|
2380
|
+
stderr.trim() || stdout.trim() || `git ${args.join(" ")} failed`,
|
|
2381
|
+
exitCode,
|
|
2382
|
+
stderr.trim()
|
|
2383
|
+
);
|
|
2384
|
+
}
|
|
2385
|
+
return {
|
|
2386
|
+
stdout: stdout.trim(),
|
|
2387
|
+
stderr: stderr.trim(),
|
|
2388
|
+
exitCode
|
|
2389
|
+
};
|
|
2390
|
+
} catch (err) {
|
|
2391
|
+
if (err instanceof GitCliError) throw err;
|
|
2392
|
+
throw new GitCliError(
|
|
2393
|
+
`Failed to run 'git ${args.join(" ")}': ${err instanceof Error ? err.message : String(err)}`,
|
|
2394
|
+
1,
|
|
2395
|
+
String(err)
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
async isGitRepo() {
|
|
2400
|
+
try {
|
|
2401
|
+
const res = await this.exec(["rev-parse", "--is-inside-work-tree"], { allowFailure: true });
|
|
2402
|
+
return res.exitCode === 0 && res.stdout === "true";
|
|
2403
|
+
} catch {
|
|
2404
|
+
return false;
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
async getRepoRoot() {
|
|
2408
|
+
try {
|
|
2409
|
+
const res = await this.exec(["rev-parse", "--show-toplevel"], { allowFailure: true });
|
|
2410
|
+
if (res.exitCode === 0 && res.stdout) {
|
|
2411
|
+
return res.stdout.trim();
|
|
2412
|
+
}
|
|
2413
|
+
return null;
|
|
2414
|
+
} catch {
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
async getConfig(key, scope) {
|
|
2419
|
+
const args = ["config"];
|
|
2420
|
+
if (scope) args.push(`--${scope}`);
|
|
2421
|
+
args.push("--get", key);
|
|
2422
|
+
const res = await this.exec(args, { allowFailure: true });
|
|
2423
|
+
if (res.exitCode === 0 && res.stdout) {
|
|
2424
|
+
return res.stdout.trim();
|
|
2425
|
+
}
|
|
2426
|
+
return null;
|
|
2427
|
+
}
|
|
2428
|
+
async setConfig(key, value, scope = "local") {
|
|
2429
|
+
await this.exec(["config", `--${scope}`, key, value]);
|
|
2430
|
+
}
|
|
2431
|
+
async unsetConfig(key, scope = "local") {
|
|
2432
|
+
await this.exec(["config", `--${scope}`, "--unset", key], { allowFailure: true });
|
|
2433
|
+
}
|
|
2434
|
+
async getRemotes() {
|
|
2435
|
+
const res = await this.exec(["remote", "-v"], { allowFailure: true });
|
|
2436
|
+
if (res.exitCode !== 0 || !res.stdout) return [];
|
|
2437
|
+
const lines = res.stdout.split("\n");
|
|
2438
|
+
const remoteMap = /* @__PURE__ */ new Map();
|
|
2439
|
+
for (const line of lines) {
|
|
2440
|
+
const match = line.match(/^(\S+)\s+(\S+)\s+\((fetch|push)\)$/);
|
|
2441
|
+
if (match) {
|
|
2442
|
+
const [, name, url, type] = match;
|
|
2443
|
+
const current = remoteMap.get(name) || { fetchUrl: "", pushUrl: "" };
|
|
2444
|
+
if (type === "fetch") current.fetchUrl = url;
|
|
2445
|
+
if (type === "push") current.pushUrl = url;
|
|
2446
|
+
remoteMap.set(name, current);
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
const result = [];
|
|
2450
|
+
for (const [name, urls] of remoteMap.entries()) {
|
|
2451
|
+
result.push({
|
|
2452
|
+
name,
|
|
2453
|
+
fetchUrl: urls.fetchUrl,
|
|
2454
|
+
pushUrl: urls.pushUrl,
|
|
2455
|
+
parsedFetch: parseRemoteUrl(urls.fetchUrl),
|
|
2456
|
+
parsedPush: parseRemoteUrl(urls.pushUrl)
|
|
2457
|
+
});
|
|
2458
|
+
}
|
|
2459
|
+
return result;
|
|
2460
|
+
}
|
|
2461
|
+
async setRemoteUrl(name, url) {
|
|
2462
|
+
const remotes = await this.getRemotes();
|
|
2463
|
+
const exists = remotes.some((r) => r.name === name);
|
|
2464
|
+
if (exists) {
|
|
2465
|
+
await this.exec(["remote", "set-url", name, url]);
|
|
2466
|
+
} else {
|
|
2467
|
+
await this.exec(["remote", "add", name, url]);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
async getCurrentBranch() {
|
|
2471
|
+
const res = await this.exec(["branch", "--show-current"], { allowFailure: true });
|
|
2472
|
+
if (res.exitCode === 0 && res.stdout) {
|
|
2473
|
+
return res.stdout.trim();
|
|
2474
|
+
}
|
|
2475
|
+
return null;
|
|
2476
|
+
}
|
|
2477
|
+
async getGitVersion() {
|
|
2478
|
+
const res = await this.exec(["--version"], { allowFailure: true });
|
|
2479
|
+
if (res.exitCode === 0 && res.stdout) {
|
|
2480
|
+
const match = res.stdout.match(/git version ([\d.]+)/);
|
|
2481
|
+
return match ? match[1] : res.stdout;
|
|
2482
|
+
}
|
|
2483
|
+
return null;
|
|
2484
|
+
}
|
|
2485
|
+
};
|
|
2486
|
+
|
|
2487
|
+
// src/core/safety/identity-guard.ts
|
|
2488
|
+
var import_node_fs8 = __toESM(require("node:fs"));
|
|
2489
|
+
var import_node_path10 = __toESM(require("node:path"));
|
|
2490
|
+
|
|
2491
|
+
// src/core/identity/identity-resolver.ts
|
|
2492
|
+
var import_node_path9 = __toESM(require("node:path"));
|
|
2493
|
+
var IdentityResolver = class {
|
|
2494
|
+
store;
|
|
2495
|
+
constructor(store) {
|
|
2496
|
+
this.store = store;
|
|
2497
|
+
}
|
|
2498
|
+
async resolve(cwd = process.cwd()) {
|
|
2499
|
+
const git = new GitCli(cwd);
|
|
2500
|
+
const isGitRepo = await git.isGitRepo();
|
|
2501
|
+
const repoRoot = isGitRepo ? await git.getRepoRoot() : null;
|
|
2502
|
+
const targetPath = repoRoot || import_node_path9.default.resolve(cwd);
|
|
2503
|
+
const identities = this.store.loadIdentities();
|
|
2504
|
+
const accounts = this.store.loadAccounts();
|
|
2505
|
+
const rules = this.store.loadRules();
|
|
2506
|
+
const config = this.store.loadConfig();
|
|
2507
|
+
let localGitEmail = null;
|
|
2508
|
+
let localGitName = null;
|
|
2509
|
+
let remotes = [];
|
|
2510
|
+
if (isGitRepo) {
|
|
2511
|
+
localGitEmail = await git.getConfig("user.email");
|
|
2512
|
+
localGitName = await git.getConfig("user.name");
|
|
2513
|
+
remotes = await git.getRemotes();
|
|
2514
|
+
}
|
|
2515
|
+
let resolvedIdentity = null;
|
|
2516
|
+
let resolvedAccount = null;
|
|
2517
|
+
let matchedRule = null;
|
|
2518
|
+
let repoProfile = null;
|
|
2519
|
+
let source = "unconfigured";
|
|
2520
|
+
repoProfile = this.store.getRepository(targetPath) || (repoRoot ? this.store.getRepository(repoRoot) : null) || null;
|
|
2521
|
+
if (repoProfile && repoProfile.identityId) {
|
|
2522
|
+
const found = identities.find((i) => i.id === repoProfile.identityId);
|
|
2523
|
+
if (found) {
|
|
2524
|
+
resolvedIdentity = found;
|
|
2525
|
+
source = "repo_profile";
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
if (!resolvedIdentity) {
|
|
2529
|
+
let bestMatchLength = -1;
|
|
2530
|
+
for (const rule of rules) {
|
|
2531
|
+
const expandedRulePath = import_node_path9.default.resolve(expandTilde(rule.path));
|
|
2532
|
+
if (targetPath === expandedRulePath || targetPath.startsWith(expandedRulePath + import_node_path9.default.sep)) {
|
|
2533
|
+
if (expandedRulePath.length > bestMatchLength) {
|
|
2534
|
+
bestMatchLength = expandedRulePath.length;
|
|
2535
|
+
matchedRule = rule;
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
if (matchedRule) {
|
|
2540
|
+
const found = identities.find((i) => i.id === matchedRule.identityId);
|
|
2541
|
+
if (found) {
|
|
2542
|
+
resolvedIdentity = found;
|
|
2543
|
+
source = "directory_rule";
|
|
2544
|
+
if (matchedRule.defaultAccountId) {
|
|
2545
|
+
resolvedAccount = accounts.find((a) => a.id === matchedRule.defaultAccountId) || null;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
if (!resolvedIdentity) {
|
|
2551
|
+
const defaultId = config.defaultIdentityId || identities.find((i) => i.isDefault)?.id;
|
|
2552
|
+
if (defaultId) {
|
|
2553
|
+
resolvedIdentity = identities.find((i) => i.id === defaultId) || null;
|
|
2554
|
+
if (resolvedIdentity) {
|
|
2555
|
+
source = "global_default";
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (!resolvedIdentity && localGitEmail) {
|
|
2560
|
+
source = "system_fallback";
|
|
2561
|
+
}
|
|
2562
|
+
if (!resolvedAccount && remotes.length > 0) {
|
|
2563
|
+
const firstRemote = remotes[0];
|
|
2564
|
+
const parsed = firstRemote.parsedPush || firstRemote.parsedFetch;
|
|
2565
|
+
if (parsed) {
|
|
2566
|
+
if (parsed.accountAlias) {
|
|
2567
|
+
resolvedAccount = accounts.find((a) => a.id === parsed.accountAlias || a.username === parsed.accountAlias) || null;
|
|
2568
|
+
}
|
|
2569
|
+
if (!resolvedAccount) {
|
|
2570
|
+
resolvedAccount = accounts.find((a) => a.host === parsed.host) || null;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
let isMismatched = false;
|
|
2575
|
+
if (resolvedIdentity && localGitEmail && localGitEmail !== resolvedIdentity.email) {
|
|
2576
|
+
isMismatched = true;
|
|
2577
|
+
}
|
|
2578
|
+
return {
|
|
2579
|
+
cwd,
|
|
2580
|
+
isGitRepo,
|
|
2581
|
+
repoRoot,
|
|
2582
|
+
source,
|
|
2583
|
+
identity: resolvedIdentity,
|
|
2584
|
+
account: resolvedAccount,
|
|
2585
|
+
matchedRule,
|
|
2586
|
+
repoProfile,
|
|
2587
|
+
remotes,
|
|
2588
|
+
localGitEmail,
|
|
2589
|
+
localGitName,
|
|
2590
|
+
isMismatched
|
|
2591
|
+
};
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
|
|
2595
|
+
// src/core/safety/identity-guard.ts
|
|
2596
|
+
var IdentityGuard = class {
|
|
2597
|
+
store;
|
|
2598
|
+
resolver;
|
|
2599
|
+
constructor(store) {
|
|
2600
|
+
this.store = store;
|
|
2601
|
+
this.resolver = new IdentityResolver(store);
|
|
2602
|
+
}
|
|
2603
|
+
async check(cwd = process.cwd()) {
|
|
2604
|
+
const ctx = await this.resolver.resolve(cwd);
|
|
2605
|
+
if (!ctx.isGitRepo) {
|
|
2606
|
+
return { allowed: true, expectedEmail: null, currentEmail: null };
|
|
2607
|
+
}
|
|
2608
|
+
const expectedEmail = ctx.identity?.email || null;
|
|
2609
|
+
const currentEmail = ctx.localGitEmail || null;
|
|
2610
|
+
if (expectedEmail && currentEmail && expectedEmail !== currentEmail) {
|
|
2611
|
+
return {
|
|
2612
|
+
allowed: false,
|
|
2613
|
+
expectedEmail,
|
|
2614
|
+
currentEmail,
|
|
2615
|
+
message: `Mismatched Git commit identity! Current: '${currentEmail}', Expected: '${expectedEmail}' (${ctx.identity?.name}).`
|
|
2616
|
+
};
|
|
2617
|
+
}
|
|
2618
|
+
return {
|
|
2619
|
+
allowed: true,
|
|
2620
|
+
expectedEmail,
|
|
2621
|
+
currentEmail
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
isInstalled(repoPath) {
|
|
2625
|
+
const hookFile = import_node_path10.default.join(repoPath, ".git", "hooks", "pre-commit");
|
|
2626
|
+
if (!import_node_fs8.default.existsSync(hookFile)) return false;
|
|
2627
|
+
const content = import_node_fs8.default.readFileSync(hookFile, "utf-8");
|
|
2628
|
+
return content.includes("gitbridge hook pre-commit") || content.includes("gb hook pre-commit");
|
|
2629
|
+
}
|
|
2630
|
+
async install(repoPath) {
|
|
2631
|
+
return this.installPreCommitHook(repoPath);
|
|
2632
|
+
}
|
|
2633
|
+
async uninstall(repoPath) {
|
|
2634
|
+
return this.uninstallPreCommitHook(repoPath);
|
|
2635
|
+
}
|
|
2636
|
+
async installPreCommitHook(repoPath) {
|
|
2637
|
+
const git = new GitCli(repoPath);
|
|
2638
|
+
const root = await git.getRepoRoot();
|
|
2639
|
+
if (!root) return false;
|
|
2640
|
+
const hooksDir = import_node_path10.default.join(root, ".git", "hooks");
|
|
2641
|
+
if (!import_node_fs8.default.existsSync(hooksDir)) {
|
|
2642
|
+
import_node_fs8.default.mkdirSync(hooksDir, { recursive: true });
|
|
2643
|
+
}
|
|
2644
|
+
const hookFile = import_node_path10.default.join(hooksDir, "pre-commit");
|
|
2645
|
+
const hookScript = `#!/usr/bin/env bash
|
|
2646
|
+
# GitBridge Pre-Commit Identity Guard
|
|
2647
|
+
if command -v gitbridge >/dev/null 2>&1; then
|
|
2648
|
+
gitbridge hook pre-commit
|
|
2649
|
+
elif command -v gb >/dev/null 2>&1; then
|
|
2650
|
+
gb hook pre-commit
|
|
2651
|
+
fi
|
|
2652
|
+
`;
|
|
2653
|
+
if (import_node_fs8.default.existsSync(hookFile)) {
|
|
2654
|
+
const existing = import_node_fs8.default.readFileSync(hookFile, "utf-8");
|
|
2655
|
+
if (!existing.includes("gitbridge hook pre-commit")) {
|
|
2656
|
+
import_node_fs8.default.appendFileSync(hookFile, `
|
|
2657
|
+
${hookScript}
|
|
2658
|
+
`, { mode: 493 });
|
|
2659
|
+
}
|
|
2660
|
+
} else {
|
|
2661
|
+
import_node_fs8.default.writeFileSync(hookFile, hookScript, { encoding: "utf-8", mode: 493 });
|
|
2662
|
+
}
|
|
2663
|
+
return true;
|
|
2664
|
+
}
|
|
2665
|
+
async uninstallPreCommitHook(repoPath) {
|
|
2666
|
+
const git = new GitCli(repoPath);
|
|
2667
|
+
const root = await git.getRepoRoot();
|
|
2668
|
+
if (!root) return false;
|
|
2669
|
+
const hookFile = import_node_path10.default.join(root, ".git", "hooks", "pre-commit");
|
|
2670
|
+
if (!import_node_fs8.default.existsSync(hookFile)) return true;
|
|
2671
|
+
const content = import_node_fs8.default.readFileSync(hookFile, "utf-8");
|
|
2672
|
+
if (content.includes("GitBridge Pre-Commit Identity Guard") || content.includes("gitbridge hook pre-commit")) {
|
|
2673
|
+
const nonCommentLines = content.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
|
|
2674
|
+
const isOnlyGitBridge = nonCommentLines.every(
|
|
2675
|
+
(l) => l.startsWith("if ") || l.startsWith("elif ") || l === "fi" || l.includes("gitbridge") || l.includes("gb hook")
|
|
2676
|
+
);
|
|
2677
|
+
if (isOnlyGitBridge) {
|
|
2678
|
+
import_node_fs8.default.unlinkSync(hookFile);
|
|
2679
|
+
} else {
|
|
2680
|
+
const filtered = content.split("\n").filter(
|
|
2681
|
+
(l) => !l.includes("GitBridge") && !l.includes("gitbridge hook") && !l.includes("gb hook") && !l.includes("command -v gitbridge") && !l.includes("command -v gb")
|
|
2682
|
+
).join("\n").trim();
|
|
2683
|
+
import_node_fs8.default.writeFileSync(hookFile, `${filtered}
|
|
2684
|
+
`, { encoding: "utf-8", mode: 493 });
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
return true;
|
|
2688
|
+
}
|
|
2689
|
+
};
|
|
2690
|
+
|
|
2691
|
+
// src/cli/commands/repo.ts
|
|
2692
|
+
var import_picocolors12 = __toESM(require("picocolors"));
|
|
2693
|
+
async function handleRepoInit(store = defaultConfigStore) {
|
|
2694
|
+
const cwd = process.cwd();
|
|
2695
|
+
const git = new GitCli(cwd);
|
|
2696
|
+
let isGitRepo = await git.isGitRepo();
|
|
2697
|
+
if (!isGitRepo) {
|
|
2698
|
+
const initialize = await promptConfirm({
|
|
2699
|
+
message: `Current directory is not a Git repository. Run 'git init'?`,
|
|
2700
|
+
initialValue: true
|
|
2701
|
+
});
|
|
2702
|
+
if (initialize) {
|
|
2703
|
+
await git.exec(["init"]);
|
|
2704
|
+
isGitRepo = true;
|
|
2705
|
+
} else {
|
|
2706
|
+
logger.warn("Initialization aborted.");
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
const repoRoot = await git.getRepoRoot() || cwd;
|
|
2711
|
+
const repoName = import_node_path11.default.basename(repoRoot);
|
|
2712
|
+
console.log(import_picocolors12.default.bold(`
|
|
2713
|
+
CONFIGURING REPOSITORY: ${import_picocolors12.default.cyan(repoName)}`));
|
|
2714
|
+
console.log(import_picocolors12.default.gray(` Path: ${repoRoot}
|
|
2715
|
+
`));
|
|
2716
|
+
const identities = store.loadIdentities();
|
|
2717
|
+
const accounts = store.loadAccounts();
|
|
2718
|
+
if (identities.length === 0) {
|
|
2719
|
+
logger.warn("No GitBridge identities configured yet. Run 'gitbridge identity add' first.");
|
|
2720
|
+
return;
|
|
2721
|
+
}
|
|
2722
|
+
const selectedIdentityId = await promptSelect({
|
|
2723
|
+
message: "Select Git identity for this repository:",
|
|
2724
|
+
options: identities.map((i) => ({
|
|
2725
|
+
value: i.id,
|
|
2726
|
+
label: `${i.id} (${i.name} <${i.email}>)`
|
|
2727
|
+
}))
|
|
2728
|
+
});
|
|
2729
|
+
const identity = store.getIdentity(selectedIdentityId);
|
|
2730
|
+
await git.setConfig("user.name", identity.name, "local");
|
|
2731
|
+
await git.setConfig("user.email", identity.email, "local");
|
|
2732
|
+
if (identity.signingKey) {
|
|
2733
|
+
await git.setConfig("user.signingkey", identity.signingKey, "local");
|
|
2734
|
+
}
|
|
2735
|
+
const existingRemotes = await git.getRemotes();
|
|
2736
|
+
const repositoryRemotes = [];
|
|
2737
|
+
if (existingRemotes.length > 0) {
|
|
2738
|
+
console.log(import_picocolors12.default.bold("\n Detected Remotes:"));
|
|
2739
|
+
for (const r of existingRemotes) {
|
|
2740
|
+
const parsed = r.parsedPush || r.parsedFetch;
|
|
2741
|
+
console.log(` \u2022 ${import_picocolors12.default.cyan(r.name)}: ${r.fetchUrl}`);
|
|
2742
|
+
if (parsed) {
|
|
2743
|
+
let accountId = parsed.accountAlias;
|
|
2744
|
+
if (!accountId && accounts.length > 0) {
|
|
2745
|
+
const matching = accounts.find((a) => a.host === parsed.host);
|
|
2746
|
+
if (matching) {
|
|
2747
|
+
accountId = matching.id;
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
repositoryRemotes.push({
|
|
2751
|
+
name: r.name,
|
|
2752
|
+
providerId: parsed.providerId,
|
|
2753
|
+
host: parsed.host,
|
|
2754
|
+
accountId,
|
|
2755
|
+
url: r.fetchUrl,
|
|
2756
|
+
rawUrl: r.fetchUrl
|
|
2757
|
+
});
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
}
|
|
2761
|
+
const installHook = await promptConfirm({
|
|
2762
|
+
message: "Install GitBridge pre-commit identity guard hook in this repo?",
|
|
2763
|
+
initialValue: true
|
|
2764
|
+
});
|
|
2765
|
+
if (installHook) {
|
|
2766
|
+
const guard = new IdentityGuard(store);
|
|
2767
|
+
await guard.installPreCommitHook(repoRoot);
|
|
2768
|
+
}
|
|
2769
|
+
store.saveRepositoryProfile({
|
|
2770
|
+
path: repoRoot,
|
|
2771
|
+
identityId: selectedIdentityId,
|
|
2772
|
+
remotes: repositoryRemotes,
|
|
2773
|
+
safetyHookInstalled: installHook
|
|
2774
|
+
});
|
|
2775
|
+
logger.success(`Repository '${repoName}' initialized with GitBridge!`);
|
|
2776
|
+
console.log(import_picocolors12.default.gray(` Identity: ${identity.name} <${identity.email}>`));
|
|
2777
|
+
console.log(import_picocolors12.default.gray(` Pre-commit guard: ${installHook ? import_picocolors12.default.green("installed") : import_picocolors12.default.gray("skipped")}`));
|
|
2778
|
+
console.log(import_picocolors12.default.green("\nYou can now make commits and push normally with native Git!\n"));
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
// src/cli/commands/context.ts
|
|
2782
|
+
var import_node_path12 = __toESM(require("node:path"));
|
|
2783
|
+
var import_picocolors13 = __toESM(require("picocolors"));
|
|
2784
|
+
async function handleContextCommand(store = defaultConfigStore) {
|
|
2785
|
+
const resolver = new IdentityResolver(store);
|
|
2786
|
+
const ctx = await resolver.resolve();
|
|
2787
|
+
console.log(import_picocolors13.default.bold("\n GITBRIDGE CONTEXT"));
|
|
2788
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2789
|
+
if (ctx.isGitRepo) {
|
|
2790
|
+
const repoName = ctx.repoRoot ? import_node_path12.default.basename(ctx.repoRoot) : "unknown";
|
|
2791
|
+
console.log(` Repository: ${import_picocolors13.default.cyan(repoName)} (${ctx.repoRoot})`);
|
|
2792
|
+
} else {
|
|
2793
|
+
console.log(` Directory: ${import_picocolors13.default.blue(ctx.cwd)} ${import_picocolors13.default.gray("(not a git repo)")}`);
|
|
2794
|
+
}
|
|
2795
|
+
console.log(` Resolution Source: ${formatBadge(ctx.source.toUpperCase(), "cyan")}`);
|
|
2796
|
+
if (ctx.identity) {
|
|
2797
|
+
console.log(` Resolved Identity: ${import_picocolors13.default.green(ctx.identity.name)} <${import_picocolors13.default.green(ctx.identity.email)}>`);
|
|
2798
|
+
if (ctx.identity.signingKey) {
|
|
2799
|
+
console.log(` Signing Key: ${import_picocolors13.default.yellow(ctx.identity.signingKey)}`);
|
|
2800
|
+
}
|
|
2801
|
+
} else {
|
|
2802
|
+
console.log(` Resolved Identity: ${import_picocolors13.default.yellow("None configured")}`);
|
|
2803
|
+
}
|
|
2804
|
+
if (ctx.isGitRepo) {
|
|
2805
|
+
console.log(` Local Git Email: ${ctx.localGitEmail ? import_picocolors13.default.cyan(ctx.localGitEmail) : import_picocolors13.default.gray("not set in .git/config")}`);
|
|
2806
|
+
if (ctx.isMismatched) {
|
|
2807
|
+
console.log(
|
|
2808
|
+
import_picocolors13.default.red(` \u26A0 WARNING: Local git email '${ctx.localGitEmail}' does not match resolved identity '${ctx.identity?.email}'!`)
|
|
2809
|
+
);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
if (ctx.account) {
|
|
2813
|
+
console.log(` Target Account: ${import_picocolors13.default.magenta(ctx.account.providerId.toUpperCase())} (${ctx.account.username})`);
|
|
2814
|
+
if (ctx.account.sshKeyPath) {
|
|
2815
|
+
console.log(` SSH Key: ${import_picocolors13.default.yellow(ctx.account.sshKeyPath)}`);
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
if (ctx.matchedRule) {
|
|
2819
|
+
console.log(` Matched Rule: ${import_picocolors13.default.blue(ctx.matchedRule.id)} (${ctx.matchedRule.path})`);
|
|
2820
|
+
}
|
|
2821
|
+
if (ctx.isGitRepo && ctx.remotes.length > 0) {
|
|
2822
|
+
console.log("\n " + import_picocolors13.default.bold("Configured Remotes:"));
|
|
2823
|
+
console.log(renderRemotesTable(ctx.remotes));
|
|
2824
|
+
} else {
|
|
2825
|
+
console.log("");
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
// src/cli/commands/remote.ts
|
|
2830
|
+
var import_picocolors14 = __toESM(require("picocolors"));
|
|
2831
|
+
async function handleRemoteList() {
|
|
2832
|
+
const git = new GitCli();
|
|
2833
|
+
if (!await git.isGitRepo()) {
|
|
2834
|
+
logger.error("Current directory is not a Git repository.");
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2837
|
+
const remotes = await git.getRemotes();
|
|
2838
|
+
console.log(import_picocolors14.default.bold("\n GIT REMOTES"));
|
|
2839
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2840
|
+
console.log(renderRemotesTable(remotes));
|
|
2841
|
+
}
|
|
2842
|
+
async function handleRemoteAdd(name, url, options = {}, store = defaultConfigStore) {
|
|
2843
|
+
const git = new GitCli();
|
|
2844
|
+
if (!await git.isGitRepo()) {
|
|
2845
|
+
logger.error("Current directory is not a Git repository.");
|
|
2846
|
+
return;
|
|
2847
|
+
}
|
|
2848
|
+
const parsed = parseRemoteUrl(url);
|
|
2849
|
+
if (!parsed) {
|
|
2850
|
+
logger.error("Invalid remote URL format.");
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
let finalUrl = url;
|
|
2854
|
+
if (options.account && parsed.protocol === "ssh") {
|
|
2855
|
+
finalUrl = buildSshUrl(parsed.host, parsed.owner, parsed.repo, options.account);
|
|
2856
|
+
}
|
|
2857
|
+
await git.setRemoteUrl(name, finalUrl);
|
|
2858
|
+
const repoRoot = await git.getRepoRoot();
|
|
2859
|
+
if (repoRoot) {
|
|
2860
|
+
const existing = store.getRepository(repoRoot);
|
|
2861
|
+
const remotes = existing ? [...existing.remotes] : [];
|
|
2862
|
+
const existingIdx = remotes.findIndex((r) => r.name === name);
|
|
2863
|
+
const remoteData = {
|
|
2864
|
+
name,
|
|
2865
|
+
providerId: parsed.providerId,
|
|
2866
|
+
host: parsed.host,
|
|
2867
|
+
accountId: options.account,
|
|
2868
|
+
url: finalUrl,
|
|
2869
|
+
rawUrl: url
|
|
2870
|
+
};
|
|
2871
|
+
if (existingIdx >= 0) {
|
|
2872
|
+
remotes[existingIdx] = remoteData;
|
|
2873
|
+
} else {
|
|
2874
|
+
remotes.push(remoteData);
|
|
2875
|
+
}
|
|
2876
|
+
const profile = {
|
|
2877
|
+
path: repoRoot,
|
|
2878
|
+
identityId: existing?.identityId,
|
|
2879
|
+
remotes,
|
|
2880
|
+
safetyHookInstalled: existing?.safetyHookInstalled || false,
|
|
2881
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2882
|
+
};
|
|
2883
|
+
store.saveRepositoryProfile(profile);
|
|
2884
|
+
}
|
|
2885
|
+
logger.success(`Remote '${name}' added: ${finalUrl}`);
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// src/cli/commands/push.ts
|
|
2889
|
+
var import_ora2 = __toESM(require("ora"));
|
|
2890
|
+
var import_picocolors15 = __toESM(require("picocolors"));
|
|
2891
|
+
async function handlePushCommand(targetRemoteOrProvider, options = {}) {
|
|
2892
|
+
const git = new GitCli();
|
|
2893
|
+
if (!await git.isGitRepo()) {
|
|
2894
|
+
logger.error("Current directory is not a Git repository.");
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
const currentBranch = await git.getCurrentBranch();
|
|
2898
|
+
if (!currentBranch) {
|
|
2899
|
+
logger.error("Unable to determine current Git branch (HEAD might be detached).");
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
const remotes = await git.getRemotes();
|
|
2903
|
+
if (remotes.length === 0) {
|
|
2904
|
+
logger.error("No remotes configured for this repository.");
|
|
2905
|
+
return;
|
|
2906
|
+
}
|
|
2907
|
+
let targets = remotes;
|
|
2908
|
+
if (targetRemoteOrProvider && !options.all) {
|
|
2909
|
+
targets = remotes.filter(
|
|
2910
|
+
(r) => r.name === targetRemoteOrProvider || r.parsedPush?.providerId === targetRemoteOrProvider || r.parsedFetch?.providerId === targetRemoteOrProvider
|
|
2911
|
+
);
|
|
2912
|
+
if (targets.length === 0) {
|
|
2913
|
+
logger.error(`No remote matching '${targetRemoteOrProvider}' found.`);
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
}
|
|
2917
|
+
console.log(import_picocolors15.default.bold(`
|
|
2918
|
+
Pushing branch '${import_picocolors15.default.cyan(currentBranch)}' to ${targets.length} remote(s)...
|
|
2919
|
+
`));
|
|
2920
|
+
const results = await Promise.allSettled(
|
|
2921
|
+
targets.map(async (remote) => {
|
|
2922
|
+
const spinner = (0, import_ora2.default)(`Pushing to ${import_picocolors15.default.cyan(remote.name)} (${remote.pushUrl})...`).start();
|
|
2923
|
+
try {
|
|
2924
|
+
const pushArgs = ["push", remote.name, currentBranch];
|
|
2925
|
+
if (options.tags) pushArgs.push("--tags");
|
|
2926
|
+
if (options.force) pushArgs.push("--force");
|
|
2927
|
+
await git.exec(pushArgs);
|
|
2928
|
+
spinner.succeed(`Pushed to ${import_picocolors15.default.green(remote.name)} [${remote.parsedPush?.providerId.toUpperCase() || "GIT"}]`);
|
|
2929
|
+
return { remote: remote.name, success: true };
|
|
2930
|
+
} catch (err) {
|
|
2931
|
+
spinner.fail(`Failed push to ${import_picocolors15.default.red(remote.name)}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2932
|
+
return { remote: remote.name, success: false, error: String(err) };
|
|
2933
|
+
}
|
|
2934
|
+
})
|
|
2935
|
+
);
|
|
2936
|
+
const successful = results.filter((r) => r.status === "fulfilled" && r.value.success).length;
|
|
2937
|
+
console.log("");
|
|
2938
|
+
if (successful === targets.length) {
|
|
2939
|
+
logger.success(`All ${successful} remote(s) updated successfully!`);
|
|
2940
|
+
} else {
|
|
2941
|
+
logger.warn(`Push finished with ${successful}/${targets.length} remotes succeeded.`);
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
// src/cli/commands/switch.ts
|
|
2946
|
+
var import_picocolors16 = __toESM(require("picocolors"));
|
|
2947
|
+
async function handleSwitchCommand(targetIdentityId, options = {}, store = defaultConfigStore) {
|
|
2948
|
+
const identities = store.loadIdentities();
|
|
2949
|
+
if (identities.length === 0) {
|
|
2950
|
+
logger.warn("No identities configured. Create one with 'gitbridge identity add'.");
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
let selectedId = targetIdentityId;
|
|
2954
|
+
if (!selectedId) {
|
|
2955
|
+
selectedId = await promptSelect({
|
|
2956
|
+
message: "Switch to identity:",
|
|
2957
|
+
options: identities.map((i) => ({
|
|
2958
|
+
value: i.id,
|
|
2959
|
+
label: `${i.id} (${i.name} <${i.email}>)`,
|
|
2960
|
+
hint: i.isDefault ? "current global default" : void 0
|
|
2961
|
+
}))
|
|
2962
|
+
});
|
|
2963
|
+
}
|
|
2964
|
+
const identity = store.getIdentity(selectedId);
|
|
2965
|
+
if (!identity) {
|
|
2966
|
+
logger.error(`Identity '${selectedId}' not found.`);
|
|
2967
|
+
return;
|
|
2968
|
+
}
|
|
2969
|
+
const git = new GitCli();
|
|
2970
|
+
const isGitRepo = await git.isGitRepo();
|
|
2971
|
+
if (isGitRepo && !options.global) {
|
|
2972
|
+
await git.setConfig("user.name", identity.name, "local");
|
|
2973
|
+
await git.setConfig("user.email", identity.email, "local");
|
|
2974
|
+
if (identity.signingKey) {
|
|
2975
|
+
await git.setConfig("user.signingkey", identity.signingKey, "local");
|
|
2976
|
+
}
|
|
2977
|
+
const repoRoot = await git.getRepoRoot();
|
|
2978
|
+
if (repoRoot) {
|
|
2979
|
+
const existing = store.getRepository(repoRoot);
|
|
2980
|
+
const profile = {
|
|
2981
|
+
path: repoRoot,
|
|
2982
|
+
identityId: identity.id,
|
|
2983
|
+
remotes: existing?.remotes || [],
|
|
2984
|
+
safetyHookInstalled: existing?.safetyHookInstalled || false,
|
|
2985
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2986
|
+
};
|
|
2987
|
+
store.saveRepositoryProfile(profile);
|
|
2988
|
+
}
|
|
2989
|
+
logger.success(`Switched repository identity to '${identity.id}'!`);
|
|
2990
|
+
console.log(import_picocolors16.default.gray(` Name: ${identity.name}`));
|
|
2991
|
+
console.log(import_picocolors16.default.gray(` Email: ${identity.email}`));
|
|
2992
|
+
console.log(import_picocolors16.default.gray(" Applied locally to current repository (.git/config)."));
|
|
2993
|
+
} else {
|
|
2994
|
+
store.setDefaultIdentity(identity.id);
|
|
2995
|
+
const generator = new GitConfigGenerator(store);
|
|
2996
|
+
generator.generate();
|
|
2997
|
+
logger.success(`Switched global default Git identity to '${identity.id}'!`);
|
|
2998
|
+
console.log(import_picocolors16.default.gray(` Name: ${identity.name}`));
|
|
2999
|
+
console.log(import_picocolors16.default.gray(` Email: ${identity.email}`));
|
|
3000
|
+
console.log(import_picocolors16.default.green(" Applied globally across GitBridge configuration."));
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
// src/cli/commands/doctor.ts
|
|
3005
|
+
var import_ora3 = __toESM(require("ora"));
|
|
3006
|
+
var import_picocolors17 = __toESM(require("picocolors"));
|
|
3007
|
+
async function handleDoctorCommand(store = defaultConfigStore) {
|
|
3008
|
+
console.log(import_picocolors17.default.bold("\n GITBRIDGE DOCTOR - SYSTEM DIAGNOSTICS"));
|
|
3009
|
+
console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
3010
|
+
const git = new GitCli();
|
|
3011
|
+
const gitInjector = new GitConfigInjector(store);
|
|
3012
|
+
const sshInjector = new SshInjector(store);
|
|
3013
|
+
const gitVersion = await git.getGitVersion();
|
|
3014
|
+
const bunVersion = typeof Bun !== "undefined" ? Bun.version : "N/A";
|
|
3015
|
+
const platform = getPlatform();
|
|
3016
|
+
console.log(import_picocolors17.default.bold(" 1. Toolchain & Environment"));
|
|
3017
|
+
if (gitVersion) {
|
|
3018
|
+
console.log(` ${import_picocolors17.default.green("\u2714")} Git CLI: ${import_picocolors17.default.cyan(gitVersion)}`);
|
|
3019
|
+
} else {
|
|
3020
|
+
console.log(` ${import_picocolors17.default.red("\u2716")} Git CLI: ${import_picocolors17.default.red("Not found on PATH")}`);
|
|
3021
|
+
}
|
|
3022
|
+
console.log(` ${import_picocolors17.default.green("\u2714")} Bun Runtime: ${import_picocolors17.default.cyan(bunVersion)}`);
|
|
3023
|
+
console.log(` ${import_picocolors17.default.green("\u2714")} Platform: ${import_picocolors17.default.cyan(platform)} (${process.arch})`);
|
|
3024
|
+
console.log(import_picocolors17.default.bold("\n 2. Git & SSH Integrations"));
|
|
3025
|
+
const gitInstalled = gitInjector.isInstalled();
|
|
3026
|
+
const sshInstalled = sshInjector.isInstalled();
|
|
3027
|
+
if (gitInstalled) {
|
|
3028
|
+
console.log(` ${import_picocolors17.default.green("\u2714")} ~/.gitconfig: ${import_picocolors17.default.green("Managed block active")}`);
|
|
3029
|
+
} else {
|
|
3030
|
+
console.log(` ${import_picocolors17.default.yellow("\u26A0")} ~/.gitconfig: ${import_picocolors17.default.yellow("Not enabled (run 'gitbridge enable')")}`);
|
|
3031
|
+
}
|
|
3032
|
+
if (sshInstalled) {
|
|
3033
|
+
console.log(` ${import_picocolors17.default.green("\u2714")} ~/.ssh/config: ${import_picocolors17.default.green("Include directive active")}`);
|
|
3034
|
+
} else {
|
|
3035
|
+
console.log(` ${import_picocolors17.default.gray("\u25CB")} ~/.ssh/config: ${import_picocolors17.default.gray("Not enabled")}`);
|
|
3036
|
+
}
|
|
3037
|
+
console.log(import_picocolors17.default.bold("\n 3. Secure Credential Storage"));
|
|
3038
|
+
const credStore = await StoreFactory.getStore(store.getPathResolver());
|
|
3039
|
+
const storeAvailable = await credStore.isAvailable();
|
|
3040
|
+
if (storeAvailable) {
|
|
3041
|
+
console.log(` ${import_picocolors17.default.green("\u2714")} Active Backend: ${import_picocolors17.default.cyan(credStore.name)}`);
|
|
3042
|
+
} else {
|
|
3043
|
+
console.log(` ${import_picocolors17.default.red("\u2716")} Active Backend: ${import_picocolors17.default.red("Unavailable")}`);
|
|
3044
|
+
}
|
|
3045
|
+
const sshKeys = SshKeyDetector.listAvailableKeys();
|
|
3046
|
+
console.log(import_picocolors17.default.bold("\n 4. Discovered SSH Keys"));
|
|
3047
|
+
if (sshKeys.length > 0) {
|
|
3048
|
+
for (const key of sshKeys) {
|
|
3049
|
+
console.log(` ${import_picocolors17.default.green("\u2714")} ${key.name}: ${import_picocolors17.default.gray(key.publicKeyPath)} (${key.type})`);
|
|
3050
|
+
}
|
|
3051
|
+
} else {
|
|
3052
|
+
console.log(` ${import_picocolors17.default.gray("\u25CB")} No .pub keys found in ~/.ssh`);
|
|
3053
|
+
}
|
|
3054
|
+
console.log(import_picocolors17.default.bold("\n 5. Provider Connectivity"));
|
|
3055
|
+
const providers = defaultProviderRegistry.list();
|
|
3056
|
+
for (const provider of providers) {
|
|
3057
|
+
const spinner = (0, import_ora3.default)(` Checking ${provider.name} (${provider.defaultHost})...`).start();
|
|
3058
|
+
const health = await provider.checkHealth();
|
|
3059
|
+
if (health.apiOk) {
|
|
3060
|
+
spinner.succeed(` ${import_picocolors17.default.green("\u2714")} ${provider.name}: API reachable (${health.pingMs}ms)`);
|
|
3061
|
+
} else {
|
|
3062
|
+
spinner.warn(` ${import_picocolors17.default.yellow("\u26A0")} ${provider.name}: ${health.error || "Unreachable"}`);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
console.log(import_picocolors17.default.green("\n\u2714 Diagnostics complete.\n"));
|
|
3066
|
+
}
|
|
3067
|
+
|
|
3068
|
+
// src/cli/commands/setup.ts
|
|
3069
|
+
var import_picocolors18 = __toESM(require("picocolors"));
|
|
3070
|
+
async function handleSetupCommand(store = defaultConfigStore) {
|
|
3071
|
+
showBanner();
|
|
3072
|
+
console.log(import_picocolors18.default.bold(" Welcome to GitBridge Interactive Onboarding!\n"));
|
|
3073
|
+
console.log(import_picocolors18.default.gray(" Let's set up your Git identities, provider connections, and workspace rules.\n"));
|
|
3074
|
+
console.log(import_picocolors18.default.cyan("\u2500\u2500 Step 1: Create your Primary Git Identity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
3075
|
+
await handleIdentityAdd({ id: "personal", default: true }, store);
|
|
3076
|
+
const addWork = await promptConfirm({
|
|
3077
|
+
message: "Do you also want to create a Work or secondary identity now?",
|
|
3078
|
+
initialValue: true
|
|
3079
|
+
});
|
|
3080
|
+
if (addWork) {
|
|
3081
|
+
console.log(import_picocolors18.default.cyan("\n\u2500\u2500 Step 1b: Create Work Git Identity \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
3082
|
+
await handleIdentityAdd({ id: "work", default: false }, store);
|
|
3083
|
+
}
|
|
3084
|
+
console.log(import_picocolors18.default.cyan("\n\u2500\u2500 Step 2: Connect Git Providers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
3085
|
+
const selectedProviders = await promptMultiSelect({
|
|
3086
|
+
message: "Which Git providers do you use? (Select with spacebar):",
|
|
3087
|
+
options: [
|
|
3088
|
+
{ value: "github", label: "GitHub" },
|
|
3089
|
+
{ value: "gitlab", label: "GitLab" },
|
|
3090
|
+
{ value: "bitbucket", label: "Bitbucket" }
|
|
3091
|
+
]
|
|
3092
|
+
});
|
|
3093
|
+
for (const prov of selectedProviders) {
|
|
3094
|
+
const loginNow = await promptConfirm({
|
|
3095
|
+
message: `Do you want to log in to ${prov.toUpperCase()} right now?`,
|
|
3096
|
+
initialValue: true
|
|
3097
|
+
});
|
|
3098
|
+
if (loginNow) {
|
|
3099
|
+
await handleAuthLogin(prov, {}, store);
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
console.log(import_picocolors18.default.cyan("\n\u2500\u2500 Step 3: Workspace Directory Routing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
3103
|
+
const addDirRule = await promptConfirm({
|
|
3104
|
+
message: "Do you want to map a directory (e.g. ~/Projects/work) to an identity?",
|
|
3105
|
+
initialValue: true
|
|
3106
|
+
});
|
|
3107
|
+
if (addDirRule) {
|
|
3108
|
+
await handleRuleAdd(void 0, void 0, {}, store);
|
|
3109
|
+
}
|
|
3110
|
+
console.log(import_picocolors18.default.cyan("\n\u2500\u2500 Step 4: Activate Git Integration \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
3111
|
+
await handleEnableCommand(store);
|
|
3112
|
+
logger.success("GitBridge setup completed successfully!");
|
|
3113
|
+
console.log(import_picocolors18.default.bold("\n You're all set! Try running:"));
|
|
3114
|
+
console.log(import_picocolors18.default.cyan(" gitbridge status") + import_picocolors18.default.gray(" # view configured status"));
|
|
3115
|
+
console.log(import_picocolors18.default.cyan(" gitbridge context") + import_picocolors18.default.gray(" # inspect current directory"));
|
|
3116
|
+
console.log(import_picocolors18.default.cyan(" gitbridge doctor") + import_picocolors18.default.gray(" # run system diagnostics\n"));
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// src/cli/commands/credential.ts
|
|
3120
|
+
function parseGitCredentialInput(raw) {
|
|
3121
|
+
const lines = raw.split("\n");
|
|
3122
|
+
const payload = {};
|
|
3123
|
+
for (const line of lines) {
|
|
3124
|
+
const trimmed = line.trim();
|
|
3125
|
+
if (!trimmed) continue;
|
|
3126
|
+
const eqIdx = trimmed.indexOf("=");
|
|
3127
|
+
if (eqIdx !== -1) {
|
|
3128
|
+
const key = trimmed.slice(0, eqIdx);
|
|
3129
|
+
const val = trimmed.slice(eqIdx + 1);
|
|
3130
|
+
if (key === "protocol") payload.protocol = val;
|
|
3131
|
+
if (key === "host") payload.host = val;
|
|
3132
|
+
if (key === "path") payload.path = val;
|
|
3133
|
+
if (key === "username") payload.username = val;
|
|
3134
|
+
if (key === "password") payload.password = val;
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
return payload;
|
|
3138
|
+
}
|
|
3139
|
+
var GitCredentialHelperHandler = class {
|
|
3140
|
+
store;
|
|
3141
|
+
resolver;
|
|
3142
|
+
constructor(store = defaultConfigStore) {
|
|
3143
|
+
this.store = store;
|
|
3144
|
+
this.resolver = new IdentityResolver(store);
|
|
3145
|
+
}
|
|
3146
|
+
async handleGet(input, cwd = process.cwd()) {
|
|
3147
|
+
const payload = parseGitCredentialInput(input);
|
|
3148
|
+
if (!payload.host) return "";
|
|
3149
|
+
const accounts = this.store.loadAccounts();
|
|
3150
|
+
const config = this.store.loadConfig();
|
|
3151
|
+
if (!config.enabled) return "";
|
|
3152
|
+
const ctx = await this.resolver.resolve(cwd);
|
|
3153
|
+
let targetAccount = ctx.account;
|
|
3154
|
+
if (!targetAccount) {
|
|
3155
|
+
if (payload.username) {
|
|
3156
|
+
targetAccount = accounts.find((a) => a.host === payload.host && a.username === payload.username) || null;
|
|
3157
|
+
}
|
|
3158
|
+
if (!targetAccount) {
|
|
3159
|
+
targetAccount = accounts.find((a) => a.host === payload.host) || null;
|
|
3160
|
+
}
|
|
3161
|
+
}
|
|
3162
|
+
if (!targetAccount) return "";
|
|
3163
|
+
const credStore = await StoreFactory.getStore(this.store.getPathResolver());
|
|
3164
|
+
const token = await credStore.get(targetAccount.host, targetAccount.id);
|
|
3165
|
+
if (!token) return "";
|
|
3166
|
+
const lines = [
|
|
3167
|
+
`username=${targetAccount.username}`,
|
|
3168
|
+
`password=${token}`,
|
|
3169
|
+
""
|
|
3170
|
+
// trailing newline required by Git protocol
|
|
3171
|
+
];
|
|
3172
|
+
return lines.join("\n");
|
|
3173
|
+
}
|
|
3174
|
+
async handleStore(input) {
|
|
3175
|
+
const payload = parseGitCredentialInput(input);
|
|
3176
|
+
if (!payload.host || !payload.username || !payload.password) return;
|
|
3177
|
+
const credStore = await StoreFactory.getStore(this.store.getPathResolver());
|
|
3178
|
+
const accountId = `${payload.host.replace(/[^a-zA-Z0-9]/g, "_")}_${payload.username}`;
|
|
3179
|
+
await credStore.set(payload.host, accountId, payload.password);
|
|
3180
|
+
}
|
|
3181
|
+
async handleErase(input) {
|
|
3182
|
+
const payload = parseGitCredentialInput(input);
|
|
3183
|
+
if (!payload.host || !payload.username) return;
|
|
3184
|
+
const credStore = await StoreFactory.getStore(this.store.getPathResolver());
|
|
3185
|
+
const accountId = `${payload.host.replace(/[^a-zA-Z0-9]/g, "_")}_${payload.username}`;
|
|
3186
|
+
await credStore.delete(payload.host, accountId);
|
|
3187
|
+
}
|
|
3188
|
+
};
|
|
3189
|
+
async function handleCredentialCommand(action, stdinData) {
|
|
3190
|
+
const handler = new GitCredentialHelperHandler();
|
|
3191
|
+
let input = stdinData;
|
|
3192
|
+
if (input === void 0) {
|
|
3193
|
+
input = await new Response(Bun.stdin.stream()).text();
|
|
3194
|
+
}
|
|
3195
|
+
if (action === "get") {
|
|
3196
|
+
const output = await handler.handleGet(input);
|
|
3197
|
+
if (output) {
|
|
3198
|
+
process.stdout.write(output);
|
|
3199
|
+
}
|
|
3200
|
+
} else if (action === "store") {
|
|
3201
|
+
await handler.handleStore(input);
|
|
3202
|
+
} else if (action === "erase") {
|
|
3203
|
+
await handler.handleErase(input);
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
// src/cli/commands/hook.ts
|
|
3208
|
+
var import_picocolors19 = __toESM(require("picocolors"));
|
|
3209
|
+
async function handleHookCommand(hookType) {
|
|
3210
|
+
if (hookType !== "pre-commit") {
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
const guard = new IdentityGuard(defaultConfigStore);
|
|
3214
|
+
const result = await guard.check();
|
|
3215
|
+
if (!result.allowed) {
|
|
3216
|
+
console.error(import_picocolors19.default.red("\n\u2716 [GitBridge Identity Guard] Commit Blocked!"));
|
|
3217
|
+
console.error(import_picocolors19.default.yellow(` ${result.message}`));
|
|
3218
|
+
console.error(
|
|
3219
|
+
import_picocolors19.default.gray(" To fix this commit identity, run: ") + import_picocolors19.default.cyan(`git config user.email "${result.expectedEmail}"`)
|
|
3220
|
+
);
|
|
3221
|
+
console.error(
|
|
3222
|
+
import_picocolors19.default.gray(" Or update your GitBridge profile with: ") + import_picocolors19.default.cyan("gitbridge repo init\n")
|
|
3223
|
+
);
|
|
3224
|
+
process.exit(1);
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
// src/cli/index.ts
|
|
3229
|
+
function createProgram(name = "gitbridge") {
|
|
3230
|
+
const program2 = new import_commander.Command();
|
|
3231
|
+
program2.name(name).description("Universal Git Identity & Multi-Account Management Layer").version("0.1.0");
|
|
3232
|
+
program2.command("setup").description("Interactive onboarding wizard to configure identities, providers, and rules").action(() => handleSetupCommand());
|
|
3233
|
+
program2.command("status").alias("st").description("Show overall GitBridge status, active identities, accounts, and rules").action(() => handleStatusCommand());
|
|
3234
|
+
program2.command("context").alias("ctx").description("Display GitBridge and Git identity context for current directory or repo").action(() => handleContextCommand());
|
|
3235
|
+
program2.command("enable").description("Enable GitBridge integration in ~/.gitconfig and ~/.ssh/config").action(() => handleEnableCommand());
|
|
3236
|
+
program2.command("disable").description("Disable GitBridge integration and safely restore original Git config").action(() => handleDisableCommand());
|
|
3237
|
+
program2.command("switch [identityId]").alias("sw").description("Quickly switch active Git identity (globally or for current repository)").option("-g, --global", "Switch global default identity instead of local repo").action((identityId, opts) => handleSwitchCommand(identityId, opts));
|
|
3238
|
+
program2.command("init").description("Initialize GitBridge profile and identity for the current Git repository").action(() => handleRepoInit());
|
|
3239
|
+
const identityCmd = program2.command("identity").alias("id").description("Manage Git commit identities (name, email, signing key)");
|
|
3240
|
+
identityCmd.command("list").alias("ls").description("List all configured identities").action(() => handleIdentityList());
|
|
3241
|
+
identityCmd.command("add").description("Create a new Git identity").option("--id <id>", "Identity ID (e.g. personal, work)").option("--name <name>", "Full name for Git commits").option("--email <email>", "Email address for Git commits").option("--signing-key <key>", "SSH/GPG commit signing key").option("--default", "Set as global default identity").action((opts) => handleIdentityAdd(opts));
|
|
3242
|
+
identityCmd.command("use <id>").description("Set global default Git identity").action((id) => handleIdentityUse(id));
|
|
3243
|
+
identityCmd.command("remove <id>").alias("rm").description("Remove an identity").action((id) => handleIdentityRemove(id));
|
|
3244
|
+
const accountCmd = program2.command("account").alias("acc").description("Manage authenticated provider accounts");
|
|
3245
|
+
accountCmd.command("list").alias("ls").description("List logged-in provider accounts").action(() => handleAccountList());
|
|
3246
|
+
accountCmd.command("remove <id>").alias("rm").description("Remove an account and erase credentials").action((id) => handleAccountRemove(id));
|
|
3247
|
+
const authCmd = program2.command("auth").description("Authenticate with Git providers");
|
|
3248
|
+
authCmd.command("login [provider]").description("Log in to a Git provider (GitHub, GitLab, Bitbucket)").option("-t, --token <token>", "Personal access token").option("--host <host>", "Custom host for enterprise/self-hosted instances").option("--ssh-key <path>", "Path to SSH private key to associate").action((prov, opts) => handleAuthLogin(prov, opts));
|
|
3249
|
+
authCmd.command("logout <provider> [username]").description("Log out of a provider and remove stored tokens").action((prov, user) => handleAuthLogout(prov, user));
|
|
3250
|
+
const providerCmd = program2.command("provider").alias("prov").description("Inspect supported Git providers");
|
|
3251
|
+
providerCmd.command("list").alias("ls").description("List supported Git providers").action(() => handleProviderList());
|
|
3252
|
+
const ruleCmd = program2.command("rule").alias("rules").description("Manage directory routing rules");
|
|
3253
|
+
ruleCmd.command("list").alias("ls").description("List directory routing rules").action(() => handleRuleList());
|
|
3254
|
+
ruleCmd.command("add [path] [identityId]").description("Add a directory routing rule (maps a folder to an identity)").option("--id <id>", "Custom rule ID").option("--provider <provider>", "Default provider for this directory").option("--account <accountId>", "Default account for this directory").action((p2, id, opts) => handleRuleAdd(p2, id, opts));
|
|
3255
|
+
ruleCmd.command("remove <idOrPath>").alias("rm").description("Remove a directory routing rule").action((target) => handleRuleRemove(target));
|
|
3256
|
+
const remoteCmd = program2.command("remote").alias("rem").description("Manage repository remotes across providers");
|
|
3257
|
+
remoteCmd.command("list").alias("ls").description("List remotes for current repository").action(() => handleRemoteList());
|
|
3258
|
+
remoteCmd.command("add <name> <url>").description("Add a remote with optional provider account routing").option("-a, --account <accountId>", "Account ID to route SSH alias to").action((name2, url, opts) => handleRemoteAdd(name2, url, opts));
|
|
3259
|
+
program2.command("push [remoteOrProvider]").description("Push current branch to configured remotes or multiple providers").option("--all", "Push to all configured remotes simultaneously").option("--tags", "Push tags as well").option("-f, --force", "Force push").action((target, opts) => handlePushCommand(target, opts));
|
|
3260
|
+
program2.command("doctor").alias("doc").description("Run comprehensive health and diagnostic checks").action(() => handleDoctorCommand());
|
|
3261
|
+
const credCmd = program2.command("credential").description("Git credential helper bridge (invoked by git)");
|
|
3262
|
+
credCmd.command("get").description("Retrieve credentials for git").action(() => handleCredentialCommand("get"));
|
|
3263
|
+
credCmd.command("store").description("Store credentials for git").action(() => handleCredentialCommand("store"));
|
|
3264
|
+
credCmd.command("erase").description("Erase credentials for git").action(() => handleCredentialCommand("erase"));
|
|
3265
|
+
const hookCmd = program2.command("hook").description("Internal Git hook runner");
|
|
3266
|
+
hookCmd.command("pre-commit").description("Pre-commit identity validation").action(() => handleHookCommand("pre-commit"));
|
|
3267
|
+
return program2;
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
// bin/gb.ts
|
|
3271
|
+
var program = createProgram("gb");
|
|
3272
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
3273
|
+
console.error("Error:", err instanceof Error ? err.message : String(err));
|
|
3274
|
+
process.exit(1);
|
|
3275
|
+
});
|