githits 0.3.0 → 0.3.2

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/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  version
3
- } from "./shared/chunk-gxya097b.js";
3
+ } from "./shared/chunk-w5kw8sef.js";
4
4
  export {
5
5
  version
6
6
  };
@@ -2,8 +2,8 @@ import {
2
2
  createAuthCommandDependencies,
3
3
  createAuthStatusDependencies,
4
4
  createContainer
5
- } from "./chunk-5mk9k2gv.js";
6
- import"./chunk-gxya097b.js";
5
+ } from "./chunk-zarrkzvq.js";
6
+ import"./chunk-w5kw8sef.js";
7
7
  export {
8
8
  createContainer,
9
9
  createAuthStatusDependencies,
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from "node:module";
2
2
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
  // package.json
4
- var version = "0.3.0";
4
+ var version = "0.3.2";
5
5
 
6
6
  export { __require, version };
@@ -1,6 +1,7 @@
1
1
  import {
2
+ __require,
2
3
  version
3
- } from "./chunk-gxya097b.js";
4
+ } from "./chunk-w5kw8sef.js";
4
5
 
5
6
  // src/services/app-config-paths.ts
6
7
  var APP_DIR = "githits";
@@ -250,7 +251,7 @@ class AuthServiceImpl {
250
251
  const error = await response.text();
251
252
  throw new Error(`Token refresh failed: ${error}`);
252
253
  }
253
- return parseTokenResponse(await response.json());
254
+ return parseRefreshTokenResponse(await response.json());
254
255
  }
255
256
  }
256
257
  function parseTokenResponse(data) {
@@ -264,6 +265,17 @@ function parseTokenResponse(data) {
264
265
  expiresIn: d.expires_in || 3600
265
266
  };
266
267
  }
268
+ function parseRefreshTokenResponse(data) {
269
+ const d = data;
270
+ if (!d.access_token) {
271
+ throw new Error("Token response missing required fields");
272
+ }
273
+ return {
274
+ accessToken: d.access_token,
275
+ refreshToken: typeof d.refresh_token === "string" ? d.refresh_token : undefined,
276
+ expiresIn: d.expires_in || 3600
277
+ };
278
+ }
267
279
  function successHtml(title = "Authentication successful") {
268
280
  return `<!DOCTYPE html>
269
281
  <html><head><title>GitHits CLI</title>
@@ -429,6 +441,13 @@ class AuthStorageImpl {
429
441
  await this.fs.ensureDir(this.configDir, DIR_MODE);
430
442
  await this.fs.atomicWriteFile(this.authPath, JSON.stringify(stored, null, 2));
431
443
  }
444
+ async saveTokensIfUnchanged(baseUrl, expected, data) {
445
+ const current = await this.loadTokens(baseUrl);
446
+ if (!sameTokenData(current, expected))
447
+ return false;
448
+ await this.saveTokens(baseUrl, data);
449
+ return true;
450
+ }
432
451
  async clearTokens(baseUrl) {
433
452
  const stored = await this.loadAuthFile();
434
453
  if (!stored)
@@ -440,6 +459,13 @@ class AuthStorageImpl {
440
459
  await this.fs.atomicWriteFile(this.authPath, JSON.stringify(stored, null, 2));
441
460
  }
442
461
  }
462
+ async clearTokensIfUnchanged(baseUrl, expected) {
463
+ const current = await this.loadTokens(baseUrl);
464
+ if (!sameTokenData(current, expected))
465
+ return false;
466
+ await this.clearTokens(baseUrl);
467
+ return true;
468
+ }
443
469
  async loadClient(baseUrl) {
444
470
  const stored = await this.loadClientFile();
445
471
  if (!stored)
@@ -466,6 +492,13 @@ class AuthStorageImpl {
466
492
  await this.fs.ensureDir(this.configDir, DIR_MODE);
467
493
  await this.fs.atomicWriteFile(this.clientPath, JSON.stringify(stored, null, 2));
468
494
  }
495
+ async saveAuthSession(baseUrl, client, tokens) {
496
+ await this.saveClient(baseUrl, client);
497
+ await this.saveTokens(baseUrl, tokens);
498
+ }
499
+ async clearAuthSession(baseUrl) {
500
+ await clearAuthSessionBestEffort(() => this.clearTokens(baseUrl), () => this.clearClient(baseUrl));
501
+ }
469
502
  async loadAuthFile() {
470
503
  if (!await this.fs.exists(this.authPath))
471
504
  return null;
@@ -496,6 +529,26 @@ class AuthStorageImpl {
496
529
  function normalizeBaseUrl(url) {
497
530
  return url.replace(/\/+$/, "");
498
531
  }
532
+ function sameTokenData(a, b) {
533
+ if (a === null || b === null)
534
+ return a === b;
535
+ return a.accessToken === b.accessToken && a.refreshToken === b.refreshToken && a.expiresAt === b.expiresAt && a.createdAt === b.createdAt;
536
+ }
537
+ async function clearAuthSessionBestEffort(clearTokens, clearClient) {
538
+ let firstError;
539
+ try {
540
+ await clearTokens();
541
+ } catch (error) {
542
+ firstError = error;
543
+ }
544
+ try {
545
+ await clearClient();
546
+ } catch (error) {
547
+ firstError ??= error;
548
+ }
549
+ if (firstError)
550
+ throw firstError;
551
+ }
499
552
  // src/services/browser-service.ts
500
553
  import open from "open";
501
554
 
@@ -1617,6 +1670,16 @@ query ListRepoFiles(
1617
1670
  $gitRef: String
1618
1671
  $version: String
1619
1672
  $pathPrefix: String
1673
+ $pathSelectors: [FilePathSelectorInput!]
1674
+ $extensions: [String!]
1675
+ $fileTypes: [String!]
1676
+ $languages: [String!]
1677
+ $fileIntent: FileIntent
1678
+ $fileIntents: [FileIntent!]
1679
+ $excludeFileIntents: [FileIntent!]
1680
+ $excludeDocFiles: Boolean
1681
+ $excludeTestFiles: Boolean
1682
+ $includeHidden: Boolean
1620
1683
  $limit: Int
1621
1684
  $waitTimeoutMs: Int
1622
1685
  ) {
@@ -1627,6 +1690,16 @@ query ListRepoFiles(
1627
1690
  gitRef: $gitRef
1628
1691
  version: $version
1629
1692
  pathPrefix: $pathPrefix
1693
+ pathSelectors: $pathSelectors
1694
+ extensions: $extensions
1695
+ fileTypes: $fileTypes
1696
+ languages: $languages
1697
+ fileIntent: $fileIntent
1698
+ fileIntents: $fileIntents
1699
+ excludeFileIntents: $excludeFileIntents
1700
+ excludeDocFiles: $excludeDocFiles
1701
+ excludeTestFiles: $excludeTestFiles
1702
+ includeHidden: $includeHidden
1630
1703
  limit: $limit
1631
1704
  waitTimeoutMs: $waitTimeoutMs
1632
1705
  ) {
@@ -2241,6 +2314,19 @@ class CodeNavigationServiceImpl {
2241
2314
  gitRef: params.target.gitRef,
2242
2315
  version: params.target.version,
2243
2316
  pathPrefix: params.pathPrefix,
2317
+ pathSelectors: params.pathSelectors?.map((entry) => ({
2318
+ kind: entry.kind,
2319
+ value: entry.value
2320
+ })),
2321
+ extensions: params.extensions,
2322
+ fileTypes: params.fileTypes,
2323
+ languages: params.languages,
2324
+ fileIntent: params.fileIntent,
2325
+ fileIntents: params.fileIntents,
2326
+ excludeFileIntents: params.excludeFileIntents,
2327
+ excludeDocFiles: params.excludeDocFiles,
2328
+ excludeTestFiles: params.excludeTestFiles,
2329
+ includeHidden: params.includeHidden,
2244
2330
  limit: params.limit,
2245
2331
  waitTimeoutMs: params.waitTimeoutMs
2246
2332
  },
@@ -2698,10 +2784,24 @@ class KeychainAuthStorage {
2698
2784
  const key = `${TOKEN_PREFIX}${normalizeBaseUrl(baseUrl2)}`;
2699
2785
  this.keyring.setPassword(SERVICE_NAME, key, JSON.stringify(data));
2700
2786
  }
2787
+ async saveTokensIfUnchanged(baseUrl2, expected, data) {
2788
+ const current = await this.loadTokens(baseUrl2);
2789
+ if (!sameTokenData(current, expected))
2790
+ return false;
2791
+ await this.saveTokens(baseUrl2, data);
2792
+ return true;
2793
+ }
2701
2794
  async clearTokens(baseUrl2) {
2702
2795
  const key = `${TOKEN_PREFIX}${normalizeBaseUrl(baseUrl2)}`;
2703
2796
  this.keyring.deletePassword(SERVICE_NAME, key);
2704
2797
  }
2798
+ async clearTokensIfUnchanged(baseUrl2, expected) {
2799
+ const current = await this.loadTokens(baseUrl2);
2800
+ if (!sameTokenData(current, expected))
2801
+ return false;
2802
+ await this.clearTokens(baseUrl2);
2803
+ return true;
2804
+ }
2705
2805
  async loadClient(baseUrl2) {
2706
2806
  const key = `${CLIENT_PREFIX}${normalizeBaseUrl(baseUrl2)}`;
2707
2807
  const json = this.keyring.getPassword(SERVICE_NAME, key);
@@ -2718,6 +2818,13 @@ class KeychainAuthStorage {
2718
2818
  const key = `${CLIENT_PREFIX}${normalizeBaseUrl(baseUrl2)}`;
2719
2819
  this.keyring.deletePassword(SERVICE_NAME, key);
2720
2820
  }
2821
+ async saveAuthSession(baseUrl2, client, tokens) {
2822
+ await this.saveClient(baseUrl2, client);
2823
+ await this.saveTokens(baseUrl2, tokens);
2824
+ }
2825
+ async clearAuthSession(baseUrl2) {
2826
+ await clearAuthSessionBestEffort(() => this.clearTokens(baseUrl2), () => this.clearClient(baseUrl2));
2827
+ }
2721
2828
  getStorageLocation() {
2722
2829
  switch (process.platform) {
2723
2830
  case "darwin":
@@ -2767,6 +2874,223 @@ class KeyringServiceImpl {
2767
2874
  }
2768
2875
  }
2769
2876
  }
2877
+ // src/services/locked-auth-storage.ts
2878
+ import { execFile } from "node:child_process";
2879
+ import { randomUUID as randomUUID3 } from "node:crypto";
2880
+ import { mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile2 } from "node:fs/promises";
2881
+ import { dirname as dirname2 } from "node:path";
2882
+ import { promisify } from "node:util";
2883
+ var LOCK_DIR = "auth.lock";
2884
+ var LOCK_TIMEOUT_MS = 1e4;
2885
+ var LOCK_RETRY_MS = 25;
2886
+ var ORPHANED_LOCK_MS = 5000;
2887
+ var OWNER_FILE = "owner.json";
2888
+ var execFileAsync = promisify(execFile);
2889
+
2890
+ class AuthStorageLockTimeoutError extends Error {
2891
+ constructor(message) {
2892
+ super(message);
2893
+ this.name = "AuthStorageLockTimeoutError";
2894
+ }
2895
+ }
2896
+
2897
+ class LockedAuthStorage {
2898
+ storage;
2899
+ lockPath;
2900
+ lockTimeoutMs;
2901
+ isOwnerAlive;
2902
+ currentOwner = null;
2903
+ constructor(storage, fileSystemService, options = {}) {
2904
+ this.storage = storage;
2905
+ this.lockTimeoutMs = options.lockTimeoutMs ?? LOCK_TIMEOUT_MS;
2906
+ this.isOwnerAlive = options.isOwnerAlive ?? isOriginalProcessAlive;
2907
+ this.lockPath = fileSystemService.joinPath(getAppConfigDir(fileSystemService), LOCK_DIR);
2908
+ }
2909
+ loadTokens(baseUrl2) {
2910
+ return this.storage.loadTokens(baseUrl2);
2911
+ }
2912
+ saveTokens(baseUrl2, data) {
2913
+ return this.withLock(() => this.storage.saveTokens(baseUrl2, data));
2914
+ }
2915
+ saveTokensIfUnchanged(baseUrl2, expected, data) {
2916
+ return this.withLock(() => this.storage.saveTokensIfUnchanged(baseUrl2, expected, data));
2917
+ }
2918
+ clearTokens(baseUrl2) {
2919
+ return this.withLock(() => this.storage.clearTokens(baseUrl2));
2920
+ }
2921
+ clearTokensIfUnchanged(baseUrl2, expected) {
2922
+ return this.withLock(() => this.storage.clearTokensIfUnchanged(baseUrl2, expected));
2923
+ }
2924
+ loadClient(baseUrl2) {
2925
+ return this.storage.loadClient(baseUrl2);
2926
+ }
2927
+ saveClient(baseUrl2, data) {
2928
+ return this.withLock(() => this.storage.saveClient(baseUrl2, data));
2929
+ }
2930
+ clearClient(baseUrl2) {
2931
+ return this.withLock(() => this.storage.clearClient(baseUrl2));
2932
+ }
2933
+ saveAuthSession(baseUrl2, client, tokens) {
2934
+ return this.withLock(() => this.storage.saveAuthSession(baseUrl2, client, tokens));
2935
+ }
2936
+ clearAuthSession(baseUrl2) {
2937
+ return this.withLock(() => this.storage.clearAuthSession(baseUrl2));
2938
+ }
2939
+ getStorageLocation() {
2940
+ return this.storage.getStorageLocation();
2941
+ }
2942
+ async withLock(fn) {
2943
+ await this.acquireLock();
2944
+ try {
2945
+ return await fn();
2946
+ } finally {
2947
+ await this.releaseLock();
2948
+ }
2949
+ }
2950
+ async acquireLock() {
2951
+ const startedAt = Date.now();
2952
+ await mkdir2(dirname2(this.lockPath), { recursive: true, mode: 448 });
2953
+ while (true) {
2954
+ try {
2955
+ await mkdir2(this.lockPath, { recursive: false, mode: 448 });
2956
+ try {
2957
+ await this.writeOwner();
2958
+ } catch (error) {
2959
+ await rm(this.lockPath, { recursive: true, force: true }).catch(() => {
2960
+ return;
2961
+ });
2962
+ throw error;
2963
+ }
2964
+ return;
2965
+ } catch (error) {
2966
+ if (error.code !== "EEXIST")
2967
+ throw error;
2968
+ await this.reclaimStaleLock();
2969
+ if (Date.now() - startedAt >= this.lockTimeoutMs) {
2970
+ throw new AuthStorageLockTimeoutError(`Timed out waiting for GitHits auth storage lock at ${this.lockPath}. If no githits process is running, remove this directory and retry.`);
2971
+ }
2972
+ await sleep(LOCK_RETRY_MS);
2973
+ }
2974
+ }
2975
+ }
2976
+ async writeOwner() {
2977
+ const owner = {
2978
+ id: randomUUID3(),
2979
+ pid: process.pid,
2980
+ createdAt: new Date().toISOString(),
2981
+ processStartedAt: await getProcessStartedAt(process.pid)
2982
+ };
2983
+ this.currentOwner = owner;
2984
+ await writeFile2(this.ownerPath(), JSON.stringify(owner), { mode: 384 });
2985
+ }
2986
+ async reclaimStaleLock() {
2987
+ const owner = await this.readOwner();
2988
+ if (!owner) {
2989
+ await this.reclaimOldOwnerlessLock();
2990
+ return;
2991
+ }
2992
+ const ownerDead = !await this.isOwnerAlive(owner.pid, owner.processStartedAt);
2993
+ if (!ownerDead)
2994
+ return;
2995
+ const currentOwner = await this.readOwner();
2996
+ if (!currentOwner || currentOwner.id !== owner.id)
2997
+ return;
2998
+ await rm(this.lockPath, { recursive: true, force: true }).catch(() => {
2999
+ return;
3000
+ });
3001
+ }
3002
+ async reclaimOldOwnerlessLock() {
3003
+ const createdAtMs = await lockCreatedAtMs(this.lockPath);
3004
+ if (Date.now() - createdAtMs < ORPHANED_LOCK_MS)
3005
+ return;
3006
+ await rm(this.lockPath, { recursive: true, force: true }).catch(() => {
3007
+ return;
3008
+ });
3009
+ }
3010
+ async readOwner() {
3011
+ try {
3012
+ const raw = await readFile2(this.ownerPath(), "utf8");
3013
+ const parsed = JSON.parse(raw);
3014
+ if (typeof parsed.id !== "string" || typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string" || !(typeof parsed.processStartedAt === "string" || parsed.processStartedAt === null)) {
3015
+ return null;
3016
+ }
3017
+ return {
3018
+ id: parsed.id,
3019
+ pid: parsed.pid,
3020
+ createdAt: parsed.createdAt,
3021
+ processStartedAt: parsed.processStartedAt
3022
+ };
3023
+ } catch {
3024
+ return null;
3025
+ }
3026
+ }
3027
+ async releaseLock() {
3028
+ const owner = this.currentOwner;
3029
+ this.currentOwner = null;
3030
+ if (!owner)
3031
+ return;
3032
+ const currentOwner = await this.readOwner();
3033
+ if (!currentOwner || currentOwner.id !== owner.id)
3034
+ return;
3035
+ await rm(this.lockPath, { recursive: true, force: true }).catch(() => {
3036
+ return;
3037
+ });
3038
+ }
3039
+ ownerPath() {
3040
+ return `${this.lockPath}/${OWNER_FILE}`;
3041
+ }
3042
+ }
3043
+ function sleep(ms) {
3044
+ return new Promise((resolve) => setTimeout(resolve, ms));
3045
+ }
3046
+ async function isOriginalProcessAlive(pid, processStartedAt) {
3047
+ if (!isProcessAlive(pid))
3048
+ return false;
3049
+ if (!processStartedAt)
3050
+ return true;
3051
+ return await getProcessStartedAt(pid) === processStartedAt;
3052
+ }
3053
+ function isProcessAlive(pid) {
3054
+ if (pid <= 0)
3055
+ return false;
3056
+ try {
3057
+ process.kill(pid, 0);
3058
+ return true;
3059
+ } catch (error) {
3060
+ const code = error.code;
3061
+ return code === "EPERM";
3062
+ }
3063
+ }
3064
+ async function getProcessStartedAt(pid) {
3065
+ try {
3066
+ if (process.platform === "win32") {
3067
+ const { stdout: stdout2 } = await execFileAsync("powershell.exe", [
3068
+ "-NoProfile",
3069
+ "-Command",
3070
+ `(Get-Process -Id ${pid}).StartTime.ToUniversalTime().ToString('o')`
3071
+ ]);
3072
+ return stdout2.trim() || null;
3073
+ }
3074
+ const { stdout } = await execFileAsync("ps", [
3075
+ "-p",
3076
+ String(pid),
3077
+ "-o",
3078
+ "lstart="
3079
+ ]);
3080
+ const parsed = Date.parse(stdout.trim());
3081
+ return Number.isNaN(parsed) ? null : new Date(parsed).toISOString();
3082
+ } catch {
3083
+ return null;
3084
+ }
3085
+ }
3086
+ async function lockCreatedAtMs(path) {
3087
+ try {
3088
+ const { stat: stat2 } = await import("node:fs/promises");
3089
+ return (await stat2(path)).mtimeMs;
3090
+ } catch {
3091
+ return 0;
3092
+ }
3093
+ }
2770
3094
  // src/services/mode-aware-file-auth-storage.ts
2771
3095
  class AuthStoragePolicyError extends Error {
2772
3096
  constructor(message) {
@@ -2806,9 +3130,16 @@ class ModeAwareFileAuthStorage {
2806
3130
  this.assertFileMode();
2807
3131
  await this.storage.saveTokens(baseUrl2, data);
2808
3132
  }
3133
+ async saveTokensIfUnchanged(baseUrl2, expected, data) {
3134
+ this.assertFileMode();
3135
+ return this.storage.saveTokensIfUnchanged(baseUrl2, expected, data);
3136
+ }
2809
3137
  clearTokens(baseUrl2) {
2810
3138
  return this.storage.clearTokens(baseUrl2);
2811
3139
  }
3140
+ clearTokensIfUnchanged(baseUrl2, expected) {
3141
+ return this.storage.clearTokensIfUnchanged(baseUrl2, expected);
3142
+ }
2812
3143
  loadClient(baseUrl2) {
2813
3144
  return this.storage.loadClient(baseUrl2);
2814
3145
  }
@@ -2819,6 +3150,13 @@ class ModeAwareFileAuthStorage {
2819
3150
  clearClient(baseUrl2) {
2820
3151
  return this.storage.clearClient(baseUrl2);
2821
3152
  }
3153
+ async saveAuthSession(baseUrl2, client, tokens) {
3154
+ this.assertFileMode();
3155
+ await this.storage.saveAuthSession(baseUrl2, client, tokens);
3156
+ }
3157
+ clearAuthSession(baseUrl2) {
3158
+ return this.storage.clearAuthSession(baseUrl2);
3159
+ }
2822
3160
  getStorageLocation() {
2823
3161
  return this.storage.getStorageLocation();
2824
3162
  }
@@ -2864,6 +3202,13 @@ class MigratingAuthStorage {
2864
3202
  throw this.toPolicyError(error);
2865
3203
  }
2866
3204
  }
3205
+ async saveTokensIfUnchanged(baseUrl2, expected, data) {
3206
+ const current = await this.loadTokens(baseUrl2);
3207
+ if (!this.sameTokenData(current, expected))
3208
+ return false;
3209
+ await this.saveTokens(baseUrl2, data);
3210
+ return true;
3211
+ }
2867
3212
  async clearTokens(baseUrl2) {
2868
3213
  const primaryError = await this.clearBestEffort(() => this.primary.clearTokens(baseUrl2));
2869
3214
  await this.clearBestEffort(() => this.file.clearTokens(baseUrl2));
@@ -2872,6 +3217,13 @@ class MigratingAuthStorage {
2872
3217
  throw primaryError;
2873
3218
  }
2874
3219
  }
3220
+ async clearTokensIfUnchanged(baseUrl2, expected) {
3221
+ const current = await this.loadTokens(baseUrl2);
3222
+ if (!this.sameTokenData(current, expected))
3223
+ return false;
3224
+ await this.clearTokens(baseUrl2);
3225
+ return true;
3226
+ }
2875
3227
  async loadClient(baseUrl2) {
2876
3228
  if (this.mode === "file") {
2877
3229
  return this.loadClientFileMode(baseUrl2);
@@ -2897,6 +3249,25 @@ class MigratingAuthStorage {
2897
3249
  throw primaryError;
2898
3250
  }
2899
3251
  }
3252
+ async saveAuthSession(baseUrl2, client, tokens) {
3253
+ if (this.mode === "file") {
3254
+ await this.file.saveAuthSession(baseUrl2, client, tokens);
3255
+ return;
3256
+ }
3257
+ try {
3258
+ await this.primary.saveAuthSession(baseUrl2, client, tokens);
3259
+ } catch (error) {
3260
+ throw this.toPolicyError(error);
3261
+ }
3262
+ }
3263
+ async clearAuthSession(baseUrl2) {
3264
+ const primaryError = await this.clearBestEffort(() => this.primary.clearAuthSession(baseUrl2));
3265
+ await this.clearBestEffort(() => this.file.clearAuthSession(baseUrl2));
3266
+ await this.clearBestEffort(() => this.legacy.clearAuthSession(baseUrl2));
3267
+ if (primaryError && !(primaryError instanceof KeychainUnavailableError)) {
3268
+ throw primaryError;
3269
+ }
3270
+ }
2900
3271
  getStorageLocation() {
2901
3272
  return this.mode === "file" ? this.file.getStorageLocation() : this.primary.getStorageLocation();
2902
3273
  }
@@ -3103,6 +3474,11 @@ class MigratingAuthStorage {
3103
3474
  this.warnedAmbiguousPlaintext = true;
3104
3475
  this.onWarning("Warning: multiple plaintext auth entries exist with ambiguous timestamps; using the new config auth path and leaving the other entry intact.");
3105
3476
  }
3477
+ sameTokenData(a, b) {
3478
+ if (a === null || b === null)
3479
+ return a === b;
3480
+ return a.accessToken === b.accessToken && a.refreshToken === b.refreshToken && a.expiresAt === b.expiresAt && a.createdAt === b.createdAt;
3481
+ }
3106
3482
  }
3107
3483
  // src/services/package-intelligence-service.ts
3108
3484
  import { z as z3 } from "zod";
@@ -4283,12 +4659,12 @@ class PackageIntelligenceServiceImpl {
4283
4659
  return this.normaliseChangelogReport(data, params);
4284
4660
  }
4285
4661
  normaliseChangelogReport(data, params) {
4286
- const source = data.source ?? undefined;
4287
- if (!source) {
4662
+ const source = data.source && data.source.trim() ? data.source : undefined;
4663
+ const rawEntries = data.entries ?? [];
4664
+ if (!source && rawEntries.length === 0) {
4288
4665
  const target = params.repoUrl ?? (params.registry && params.packageName ? `${params.registry.toLowerCase()}:${params.packageName}` : "package");
4289
4666
  throw new PackageIntelligenceChangelogSourceNotFoundError(`No changelog source available for ${target} (tried GitHub Releases, CHANGELOG.md, and HexDocs).`);
4290
4667
  }
4291
- const rawEntries = data.entries ?? [];
4292
4668
  const entries = rawEntries.map((entry) => ({
4293
4669
  version: entry.version ?? undefined,
4294
4670
  normalizedVersion: entry.normalizedVersion ?? undefined,
@@ -4629,24 +5005,62 @@ class TokenManager {
4629
5005
  refreshToken: tokens.refreshToken
4630
5006
  }));
4631
5007
  } catch {
5008
+ const reloadedToken = await this.loadExternallyUpdatedToken(tokens);
5009
+ if (reloadedToken)
5010
+ return reloadedToken.accessToken;
4632
5011
  const isExpired = tokens.expiresAt ? new Date >= new Date(tokens.expiresAt) : false;
4633
5012
  if (isExpired) {
5013
+ const currentStoredTokens = await this.loadExternallyUpdatedToken(tokens);
5014
+ if (currentStoredTokens)
5015
+ return currentStoredTokens.accessToken;
5016
+ const cleared = await withTelemetrySpan("token-manager.clear-tokens-if-unchanged", () => this.authStorage.clearTokensIfUnchanged(this.mcpUrl, tokens));
5017
+ if (!cleared) {
5018
+ const currentToken = await this.authStorage.loadTokens(this.mcpUrl);
5019
+ this.cachedToken = currentToken;
5020
+ return currentToken?.accessToken;
5021
+ }
4634
5022
  this.cachedToken = null;
4635
- await withTelemetrySpan("token-manager.clear-tokens", () => this.authStorage.clearTokens(this.mcpUrl));
4636
5023
  }
4637
5024
  return;
4638
5025
  }
4639
5026
  const newTokenData = {
4640
5027
  accessToken: response.accessToken,
4641
- refreshToken: response.refreshToken,
5028
+ refreshToken: response.refreshToken ?? tokens.refreshToken,
4642
5029
  expiresAt: new Date(Date.now() + response.expiresIn * 1000).toISOString(),
4643
5030
  createdAt: new Date().toISOString()
4644
5031
  };
4645
- await withTelemetrySpan("token-manager.save-tokens", () => this.authStorage.saveTokens(this.mcpUrl, newTokenData));
5032
+ const externallyUpdatedToken = await this.loadExternallyUpdatedToken(tokens, {
5033
+ treatMissingAsExternalUpdate: true
5034
+ });
5035
+ if (externallyUpdatedToken === null)
5036
+ return;
5037
+ if (externallyUpdatedToken)
5038
+ return externallyUpdatedToken.accessToken;
5039
+ const saved = await withTelemetrySpan("token-manager.save-tokens", () => this.authStorage.saveTokensIfUnchanged(this.mcpUrl, tokens, newTokenData));
5040
+ if (!saved) {
5041
+ const currentToken = await this.authStorage.loadTokens(this.mcpUrl);
5042
+ this.cachedToken = currentToken;
5043
+ return currentToken?.accessToken;
5044
+ }
4646
5045
  this.cachedToken = newTokenData;
4647
5046
  return response.accessToken;
4648
5047
  });
4649
5048
  }
5049
+ async loadExternallyUpdatedToken(failedTokens, options = {}) {
5050
+ const storedTokens = await withTelemetrySpan("token-manager.reload-tokens", () => this.authStorage.loadTokens(this.mcpUrl));
5051
+ if (!storedTokens) {
5052
+ if (options.treatMissingAsExternalUpdate)
5053
+ this.cachedToken = null;
5054
+ return options.treatMissingAsExternalUpdate ? null : undefined;
5055
+ }
5056
+ if (areSameTokenData(storedTokens, failedTokens))
5057
+ return;
5058
+ this.cachedToken = storedTokens;
5059
+ return storedTokens;
5060
+ }
5061
+ }
5062
+ function areSameTokenData(a, b) {
5063
+ return a.accessToken === b.accessToken && a.refreshToken === b.refreshToken && a.expiresAt === b.expiresAt && a.createdAt === b.createdAt;
4650
5064
  }
4651
5065
  // src/services/update-check-service.ts
4652
5066
  import semver from "semver";
@@ -4984,7 +5398,7 @@ function createAuthStorageForMode(fileSystemService, mode, configPath = "your Gi
4984
5398
  const rawKeyring = new KeyringServiceImpl;
4985
5399
  const keyring = process.platform === "win32" ? new ChunkingKeyringService(rawKeyring, WINDOWS_MAX_ENTRY_SIZE) : rawKeyring;
4986
5400
  const keychainStorage = new KeychainAuthStorage(keyring);
4987
- return new MigratingAuthStorage(keychainStorage, fileStorage, legacyStorage, mode, configPath, (message) => console.error(message));
5401
+ return new LockedAuthStorage(new MigratingAuthStorage(keychainStorage, fileStorage, legacyStorage, mode, configPath, (message) => console.error(message)), fileSystemService);
4988
5402
  }
4989
5403
  async function createAuthCommandDependencies() {
4990
5404
  return withTelemetrySpan("container.create-auth-command", async () => {
@@ -5076,4 +5490,4 @@ async function createContainer() {
5076
5490
  });
5077
5491
  }
5078
5492
 
5079
- export { AuthConfigError, CLIENT_UPDATE_REQUIRED_REASON, ClientUpdateRequiredError, debugLog, setMcpClientVersionProvider, setClientMode, isTelemetryEnabled, withTelemetrySpan, startTelemetrySpan, endTelemetrySpan, flushTelemetry, AuthenticationError, CodeNavigationAccessError, CodeNavigationGraphQLError, CodeNavigationIndexingError, CodeNavigationUnresolvableError, MalformedCodeNavigationResponseError, CodeNavigationTargetNotFoundError, CodeNavigationFileNotFoundError, CodeNavigationVersionNotFoundError, CodeNavigationValidationError, CodeNavigationFeatureFlagRequiredError, CodeNavigationNetworkError, CodeNavigationBackendError, getCodeNavigationUrl, ExecServiceImpl, FileSystemServiceImpl, AuthStoragePolicyError, PackageIntelligenceAccessError, PackageIntelligenceFeatureFlagRequiredError, PackageIntelligenceNetworkError, PackageIntelligenceBackendError, PackageIntelligenceGraphQLError, PackageIntelligenceTargetNotFoundError, PackageIntelligenceValidationError, PackageIntelligenceVersionNotFoundError, MalformedPackageIntelligenceResponseError, PackageIntelligenceChangelogSourceNotFoundError, PromptServiceImpl, refreshExpiredToken, NpmRegistryUpdateCheckService, shouldRunUpdateCheck, shouldRunRequiredUpdateEnforcement, formatUpdateNotice, formatRequiredUpdateNotice, createAuthCommandDependencies, createAuthStatusDependencies, createContainer };
5493
+ export { AuthConfigError, CLIENT_UPDATE_REQUIRED_REASON, ClientUpdateRequiredError, debugLog, setMcpClientVersionProvider, setClientMode, isTelemetryEnabled, withTelemetrySpan, startTelemetrySpan, endTelemetrySpan, flushTelemetry, AuthenticationError, CodeNavigationAccessError, CodeNavigationGraphQLError, CodeNavigationIndexingError, CodeNavigationUnresolvableError, MalformedCodeNavigationResponseError, CodeNavigationTargetNotFoundError, CodeNavigationFileNotFoundError, CodeNavigationVersionNotFoundError, CodeNavigationValidationError, CodeNavigationFeatureFlagRequiredError, CodeNavigationNetworkError, CodeNavigationBackendError, getCodeNavigationUrl, ExecServiceImpl, FileSystemServiceImpl, AuthStorageLockTimeoutError, AuthStoragePolicyError, PackageIntelligenceAccessError, PackageIntelligenceFeatureFlagRequiredError, PackageIntelligenceNetworkError, PackageIntelligenceBackendError, PackageIntelligenceGraphQLError, PackageIntelligenceTargetNotFoundError, PackageIntelligenceValidationError, PackageIntelligenceVersionNotFoundError, MalformedPackageIntelligenceResponseError, PackageIntelligenceChangelogSourceNotFoundError, PromptServiceImpl, refreshExpiredToken, NpmRegistryUpdateCheckService, shouldRunUpdateCheck, shouldRunRequiredUpdateEnforcement, formatUpdateNotice, formatRequiredUpdateNotice, createAuthCommandDependencies, createAuthStatusDependencies, createContainer };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "githits",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Code examples from global open source for developers and AI assistants.",
5
5
  "mcpServers": {
6
6
  "githits": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "githits",
3
3
  "description": "CLI companion for GitHits - code examples from global open source for developers and AI assistants",
4
- "version": "0.3.0",
4
+ "version": "0.3.2",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "dist",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "githits",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Code examples from global open source for developers and AI assistants",
5
5
  "author": {
6
6
  "name": "GitHits"