@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 CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@abhaydesu/git-pilot",
3
- "version": "2.0.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 --test \"test/*.test.js\"",
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.10.0"
34
+ "shell-quote": "^1.12.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@eslint/js": "^10.0.1",
@@ -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
- "[Windows.Security.Credentials.PasswordVault,Windows.Security.Credentials,ContentType=WindowsRuntime] | Out-Null; 'OK'",
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 / PasswordVault is unavailable.",
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
- script,
281
+ windowsCredentialScript("get", service, account),
215
282
  ]);
216
- return stdout.trim() || null;
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
- const script = `
265
- $service = '${service}';
266
- $account = '${account}';
267
- $pass = [Console]::In.ReadToEnd();
268
- $vault = New-Object Windows.Security.Credentials.PasswordVault;
269
- $cred = New-Object Windows.Security.Credentials.PasswordCredential($service, $account, $pass);
270
- $vault.Add($cred);
271
- `;
272
- await this.execFn("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], {
273
- input: password,
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", ["-NoProfile", "-NonInteractive", "-Command", script]);
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.split("\0").filter((s) => s.length > 0);
74
+ return parseStagedFileList(output);
71
75
  }
72
76
 
73
77
  /**