@abhaydesu/git-pilot 2.0.0 → 2.0.1
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/package.json +3 -3
- package/src/lib/credentials.js +102 -44
- package/src/lib/git.js +5 -1
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abhaydesu/git-pilot",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "AI-powered Git assistant: commit messages, natural-language git commands, undo suggestions and branch names.",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "node
|
|
7
|
+
"test": "node scripts/run-tests.js",
|
|
8
8
|
"test:e2e": "bash test/e2e/run.sh",
|
|
9
9
|
"lint": "eslint .",
|
|
10
10
|
"format": "prettier --write ."
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"execa": "^9.6.0",
|
|
32
32
|
"inquirer": "^12.11.1",
|
|
33
33
|
"ora": "^9.4.1",
|
|
34
|
-
"shell-quote": "^1.
|
|
34
|
+
"shell-quote": "^1.12.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@eslint/js": "^10.0.1",
|
package/src/lib/credentials.js
CHANGED
|
@@ -4,6 +4,87 @@ import process from "node:process";
|
|
|
4
4
|
export const SERVICE_NAME = "git-pilot";
|
|
5
5
|
const SAFE_IDENTIFIER = /^[a-zA-Z0-9._-]+$/;
|
|
6
6
|
|
|
7
|
+
const WINDOWS_CREDENTIAL_MANAGER_SOURCE = `
|
|
8
|
+
using System;
|
|
9
|
+
using System.ComponentModel;
|
|
10
|
+
using System.Runtime.InteropServices;
|
|
11
|
+
|
|
12
|
+
public static class GitPilotCredentialManager {
|
|
13
|
+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
|
14
|
+
private struct NativeCredential {
|
|
15
|
+
public UInt32 Flags;
|
|
16
|
+
public UInt32 Type;
|
|
17
|
+
public string TargetName;
|
|
18
|
+
public string Comment;
|
|
19
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
20
|
+
public UInt32 CredentialBlobSize;
|
|
21
|
+
public IntPtr CredentialBlob;
|
|
22
|
+
public UInt32 Persist;
|
|
23
|
+
public UInt32 AttributeCount;
|
|
24
|
+
public IntPtr Attributes;
|
|
25
|
+
public string TargetAlias;
|
|
26
|
+
public string UserName;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
30
|
+
private static extern bool CredWrite(ref NativeCredential credential, UInt32 flags);
|
|
31
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
32
|
+
private static extern bool CredRead(string target, UInt32 type, UInt32 flags, out IntPtr credential);
|
|
33
|
+
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
34
|
+
private static extern bool CredDelete(string target, UInt32 type, UInt32 flags);
|
|
35
|
+
[DllImport("Advapi32.dll", SetLastError = false)]
|
|
36
|
+
private static extern void CredFree(IntPtr credential);
|
|
37
|
+
|
|
38
|
+
public static bool Probe() { return true; }
|
|
39
|
+
|
|
40
|
+
public static void Write(string target, string username, byte[] secret) {
|
|
41
|
+
IntPtr blob = Marshal.AllocHGlobal(secret.Length);
|
|
42
|
+
try {
|
|
43
|
+
Marshal.Copy(secret, 0, blob, secret.Length);
|
|
44
|
+
var credential = new NativeCredential {
|
|
45
|
+
Type = 1, TargetName = target, CredentialBlobSize = (UInt32)secret.Length,
|
|
46
|
+
CredentialBlob = blob, Persist = 2, UserName = username
|
|
47
|
+
};
|
|
48
|
+
if (!CredWrite(ref credential, 0)) throw new Win32Exception(Marshal.GetLastWin32Error());
|
|
49
|
+
} finally { Marshal.FreeHGlobal(blob); }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public static byte[] Read(string target, out int error) {
|
|
53
|
+
IntPtr pointer;
|
|
54
|
+
if (!CredRead(target, 1, 0, out pointer)) { error = Marshal.GetLastWin32Error(); return null; }
|
|
55
|
+
try {
|
|
56
|
+
var credential = (NativeCredential)Marshal.PtrToStructure(pointer, typeof(NativeCredential));
|
|
57
|
+
var secret = new byte[credential.CredentialBlobSize];
|
|
58
|
+
if (secret.Length > 0) Marshal.Copy(credential.CredentialBlob, secret, 0, secret.Length);
|
|
59
|
+
error = 0;
|
|
60
|
+
return secret;
|
|
61
|
+
} finally { CredFree(pointer); }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public static bool Delete(string target, out int error) {
|
|
65
|
+
if (CredDelete(target, 1, 0)) { error = 0; return true; }
|
|
66
|
+
error = Marshal.GetLastWin32Error();
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
function windowsCredentialScript(action, service, account) {
|
|
73
|
+
const target = `${service}/${account}`;
|
|
74
|
+
const prelude = `$ErrorActionPreference = 'Stop';\nAdd-Type -TypeDefinition @'\n${WINDOWS_CREDENTIAL_MANAGER_SOURCE}\n'@;\n`;
|
|
75
|
+
|
|
76
|
+
if (action === "probe") {
|
|
77
|
+
return `${prelude}[GitPilotCredentialManager]::Probe() | Out-Null; 'OK'`;
|
|
78
|
+
}
|
|
79
|
+
if (action === "get") {
|
|
80
|
+
return `${prelude}$errorCode = 0; $secret = [GitPilotCredentialManager]::Read('${target}', [ref]$errorCode); if ($null -eq $secret) { if ($errorCode -eq 1168) { exit 44 }; throw [ComponentModel.Win32Exception]::new($errorCode) }; [Console]::Out.Write([Convert]::ToBase64String($secret))`;
|
|
81
|
+
}
|
|
82
|
+
if (action === "set") {
|
|
83
|
+
return `${prelude}$encoded = [Console]::In.ReadToEnd().Trim(); $secret = [Convert]::FromBase64String($encoded); [GitPilotCredentialManager]::Write('${target}', '${account}', $secret)`;
|
|
84
|
+
}
|
|
85
|
+
return `${prelude}$errorCode = 0; if ([GitPilotCredentialManager]::Delete('${target}', [ref]$errorCode)) { exit 0 }; if ($errorCode -eq 1168) { exit 44 }; throw [ComponentModel.Win32Exception]::new($errorCode)`;
|
|
86
|
+
}
|
|
87
|
+
|
|
7
88
|
export class CredentialStoreUnavailableError extends Error {
|
|
8
89
|
constructor(message, platform) {
|
|
9
90
|
super(message);
|
|
@@ -119,13 +200,13 @@ export class OsCredentialStore {
|
|
|
119
200
|
"-NoProfile",
|
|
120
201
|
"-NonInteractive",
|
|
121
202
|
"-Command",
|
|
122
|
-
"
|
|
203
|
+
windowsCredentialScript("probe", "git-pilot", "availability"),
|
|
123
204
|
]);
|
|
124
205
|
return { available: true };
|
|
125
206
|
} catch {
|
|
126
207
|
return {
|
|
127
208
|
available: false,
|
|
128
|
-
reason: "Windows Credential Manager
|
|
209
|
+
reason: "Windows Credential Manager is unavailable.",
|
|
129
210
|
};
|
|
130
211
|
}
|
|
131
212
|
}
|
|
@@ -192,28 +273,15 @@ export class OsCredentialStore {
|
|
|
192
273
|
}
|
|
193
274
|
|
|
194
275
|
if (this.platform === "win32") {
|
|
195
|
-
const script = `
|
|
196
|
-
$service = '${service}';
|
|
197
|
-
$account = '${account}';
|
|
198
|
-
try {
|
|
199
|
-
$vault = New-Object Windows.Security.Credentials.PasswordVault;
|
|
200
|
-
$cred = $vault.Retrieve($service, $account);
|
|
201
|
-
$cred.RetrievePassword();
|
|
202
|
-
[Console]::Write($cred.Password);
|
|
203
|
-
} catch {
|
|
204
|
-
if ($_.Exception.Message -match 'Element not found') { exit 44; }
|
|
205
|
-
[Console]::Error.Write($_.Exception.Message);
|
|
206
|
-
exit 1;
|
|
207
|
-
}
|
|
208
|
-
`;
|
|
209
276
|
try {
|
|
210
277
|
const { stdout } = await this.execFn("powershell.exe", [
|
|
211
278
|
"-NoProfile",
|
|
212
279
|
"-NonInteractive",
|
|
213
280
|
"-Command",
|
|
214
|
-
|
|
281
|
+
windowsCredentialScript("get", service, account),
|
|
215
282
|
]);
|
|
216
|
-
|
|
283
|
+
const encoded = stdout.trim();
|
|
284
|
+
return encoded ? Buffer.from(encoded, "base64").toString("utf8") : null;
|
|
217
285
|
} catch (err) {
|
|
218
286
|
if (err.exitCode === 44) return null;
|
|
219
287
|
throw new CredentialStoreUnavailableError(
|
|
@@ -261,17 +329,16 @@ export class OsCredentialStore {
|
|
|
261
329
|
}
|
|
262
330
|
|
|
263
331
|
if (this.platform === "win32") {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
});
|
|
332
|
+
await this.execFn(
|
|
333
|
+
"powershell.exe",
|
|
334
|
+
[
|
|
335
|
+
"-NoProfile",
|
|
336
|
+
"-NonInteractive",
|
|
337
|
+
"-Command",
|
|
338
|
+
windowsCredentialScript("set", service, account),
|
|
339
|
+
],
|
|
340
|
+
{ input: Buffer.from(password, "utf8").toString("base64") }
|
|
341
|
+
);
|
|
275
342
|
}
|
|
276
343
|
} catch (err) {
|
|
277
344
|
const sanitized = redactSecretText(
|
|
@@ -326,22 +393,13 @@ export class OsCredentialStore {
|
|
|
326
393
|
}
|
|
327
394
|
|
|
328
395
|
if (this.platform === "win32") {
|
|
329
|
-
const script = `
|
|
330
|
-
$service = '${service}';
|
|
331
|
-
$account = '${account}';
|
|
332
|
-
try {
|
|
333
|
-
$vault = New-Object Windows.Security.Credentials.PasswordVault;
|
|
334
|
-
$cred = $vault.Retrieve($service, $account);
|
|
335
|
-
$vault.Remove($cred);
|
|
336
|
-
exit 0;
|
|
337
|
-
} catch {
|
|
338
|
-
if ($_.Exception.Message -match 'Element not found') { exit 44; }
|
|
339
|
-
[Console]::Error.Write($_.Exception.Message);
|
|
340
|
-
exit 1;
|
|
341
|
-
}
|
|
342
|
-
`;
|
|
343
396
|
try {
|
|
344
|
-
await this.execFn("powershell.exe", [
|
|
397
|
+
await this.execFn("powershell.exe", [
|
|
398
|
+
"-NoProfile",
|
|
399
|
+
"-NonInteractive",
|
|
400
|
+
"-Command",
|
|
401
|
+
windowsCredentialScript("delete", service, account),
|
|
402
|
+
]);
|
|
345
403
|
return true;
|
|
346
404
|
} catch (err) {
|
|
347
405
|
if (err.exitCode === 44) return false;
|
package/src/lib/git.js
CHANGED
|
@@ -58,6 +58,10 @@ export const getStagedDiff = () => git(["diff", "--staged", "--no-ext-diff", "--
|
|
|
58
58
|
export const getStagedSummary = () =>
|
|
59
59
|
git(["diff", "--staged", "--name-status", "--no-ext-diff", "--no-textconv"]);
|
|
60
60
|
|
|
61
|
+
export function parseStagedFileList(output) {
|
|
62
|
+
return output.split("\0").filter((file) => file.length > 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
61
65
|
/**
|
|
62
66
|
* Returns staged file paths using NUL-delimited (`-z`) output so filenames
|
|
63
67
|
* containing leading/trailing spaces, special characters, or newlines remain intact.
|
|
@@ -67,7 +71,7 @@ export async function getStagedFiles() {
|
|
|
67
71
|
["diff", "--staged", "--name-only", "-z", "--no-ext-diff", "--no-textconv"],
|
|
68
72
|
{ stripFinalNewline: false }
|
|
69
73
|
);
|
|
70
|
-
return output
|
|
74
|
+
return parseStagedFileList(output);
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
/**
|