@superblocksteam/shared 0.9569.3 → 0.9570.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.
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Git remote URL utilities — GitHub-focused.
3
+ *
4
+ * Parsing, validation, and compare-URL generation for git remote URLs.
5
+ * Used by both the UI and the vite-plugin-file-sync AI agent tooling.
6
+ */
7
+ export declare const GITHUB_PAT_SETTINGS_URL = "https://github.com/settings/tokens";
8
+ export declare function isValidGitUrl(url: string): boolean;
9
+ export declare function isGitHubUrl(url: string): boolean;
10
+ /**
11
+ * Converts a git remote URL (HTTPS or SSH) to a browser-friendly HTTPS URL.
12
+ * Returns `null` when the URL cannot be converted.
13
+ *
14
+ * https://github.com/org/repo.git → https://github.com/org/repo
15
+ * git@github.com:org/repo.git → https://github.com/org/repo
16
+ */
17
+ export declare function gitRemoteUrlToWeb(url: string): string | null;
18
+ /**
19
+ * Returns a URL that opens the "new pull request" compare screen on GitHub.
20
+ * Returns `null` when the remote URL is not a GitHub URL or cannot be parsed.
21
+ */
22
+ export declare function getGitHubCompareUrl({ gitRemoteUrl, fromBranch, toBranch, title, body }: {
23
+ gitRemoteUrl: string;
24
+ fromBranch: string;
25
+ toBranch: string;
26
+ title?: string;
27
+ body?: string;
28
+ }): string | null;
29
+ export declare function isGitCommitHash(value: string): boolean;
30
+ export declare function getCommitUrlForHash(params: {
31
+ hash: string;
32
+ repositoryWebUrl: string;
33
+ }): string | null;
34
+ export declare function isCommitUrlForHash(params: {
35
+ hash: string;
36
+ url: string;
37
+ }): boolean;
38
+ //# sourceMappingURL=git-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-url.d.ts","sourceRoot":"","sources":["../../src/utils/git-url.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,eAAO,MAAM,uBAAuB,uCAAuC,CAAC;AAE5E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGlD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAchD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB5D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,KAAK,EACL,IAAI,EACL,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,GAAG,IAAI,CAchB;AA2CD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAerG;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAoBjF"}
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ /**
3
+ * Git remote URL utilities — GitHub-focused.
4
+ *
5
+ * Parsing, validation, and compare-URL generation for git remote URLs.
6
+ * Used by both the UI and the vite-plugin-file-sync AI agent tooling.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.GITHUB_PAT_SETTINGS_URL = void 0;
10
+ exports.isValidGitUrl = isValidGitUrl;
11
+ exports.isGitHubUrl = isGitHubUrl;
12
+ exports.gitRemoteUrlToWeb = gitRemoteUrlToWeb;
13
+ exports.getGitHubCompareUrl = getGitHubCompareUrl;
14
+ exports.isGitCommitHash = isGitCommitHash;
15
+ exports.getCommitUrlForHash = getCommitUrlForHash;
16
+ exports.isCommitUrlForHash = isCommitUrlForHash;
17
+ const HTTPS_GIT_URL_RE = /^https?:\/\/[^\s/$.?#]+\.[^\s/$.?#]+\/[^\s/$.?#]+(\/[^\s/$.?#]+)+(\.git)?\/?$/i;
18
+ const SSH_GIT_URL_RE = /^[\w-]+@[\w.-]+:[\w./-]+(\.git)?$/;
19
+ exports.GITHUB_PAT_SETTINGS_URL = 'https://github.com/settings/tokens';
20
+ function isValidGitUrl(url) {
21
+ const trimmed = url.trim();
22
+ return HTTPS_GIT_URL_RE.test(trimmed) || SSH_GIT_URL_RE.test(trimmed);
23
+ }
24
+ function isGitHubUrl(url) {
25
+ const trimmed = url.trim();
26
+ try {
27
+ const parsed = new URL(trimmed);
28
+ if (parsed.hostname.toLowerCase() === 'github.com')
29
+ return true;
30
+ }
31
+ catch {
32
+ // Not a valid URL — try SSH format
33
+ }
34
+ const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
35
+ if (sshMatch?.[1]?.toLowerCase() === 'github.com')
36
+ return true;
37
+ return false;
38
+ }
39
+ /**
40
+ * Converts a git remote URL (HTTPS or SSH) to a browser-friendly HTTPS URL.
41
+ * Returns `null` when the URL cannot be converted.
42
+ *
43
+ * https://github.com/org/repo.git → https://github.com/org/repo
44
+ * git@github.com:org/repo.git → https://github.com/org/repo
45
+ */
46
+ function gitRemoteUrlToWeb(url) {
47
+ const trimmed = url.trim();
48
+ try {
49
+ const parsed = new URL(trimmed);
50
+ if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
51
+ return trimmed.replace(/\.git\s*$/, '');
52
+ }
53
+ }
54
+ catch {
55
+ // Not a standard URL — try SSH
56
+ }
57
+ const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):(.+?)(?:\.git)?\s*$/);
58
+ if (sshMatch?.[1] && sshMatch[2]) {
59
+ return `https://${sshMatch[1]}/${sshMatch[2]}`;
60
+ }
61
+ return null;
62
+ }
63
+ /**
64
+ * Returns a URL that opens the "new pull request" compare screen on GitHub.
65
+ * Returns `null` when the remote URL is not a GitHub URL or cannot be parsed.
66
+ */
67
+ function getGitHubCompareUrl({ gitRemoteUrl, fromBranch, toBranch, title, body }) {
68
+ if (!isGitHubUrl(gitRemoteUrl))
69
+ return null;
70
+ const webUrl = gitRemoteUrlToWeb(gitRemoteUrl);
71
+ if (!webUrl)
72
+ return null;
73
+ const base = `${webUrl}/compare/${encodeURIComponent(toBranch)}...${encodeURIComponent(fromBranch)}`;
74
+ if (!title && !body)
75
+ return base;
76
+ const params = new URLSearchParams();
77
+ params.set('expand', '1');
78
+ if (title)
79
+ params.set('title', title);
80
+ if (body)
81
+ params.set('body', body);
82
+ return `${base}?${params.toString()}`;
83
+ }
84
+ const HOST_TO_PROVIDER = {
85
+ 'bitbucket.org': 'bitbucket',
86
+ 'github.com': 'github',
87
+ 'gitlab.com': 'gitlab'
88
+ };
89
+ function detectGitProvider(url) {
90
+ const trimmed = url.trim();
91
+ try {
92
+ const parsed = new URL(trimmed);
93
+ return HOST_TO_PROVIDER[parsed.hostname.toLowerCase()] ?? 'unknown';
94
+ }
95
+ catch {
96
+ // Not a standard URL — try SSH format.
97
+ }
98
+ const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
99
+ if (sshMatch?.[1]) {
100
+ return HOST_TO_PROVIDER[sshMatch[1].toLowerCase()] ?? 'unknown';
101
+ }
102
+ return 'unknown';
103
+ }
104
+ function getCommitPathForProvider(params) {
105
+ const { hash, provider } = params;
106
+ switch (provider) {
107
+ case 'bitbucket':
108
+ return `/commits/${hash}`;
109
+ case 'gitlab':
110
+ return `/-/commit/${hash}`;
111
+ case 'github':
112
+ case 'unknown':
113
+ return `/commit/${hash}`;
114
+ }
115
+ }
116
+ const GIT_COMMIT_HASH_REGEX = /^[0-9a-f]{7,40}$/i;
117
+ function isGitCommitHash(value) {
118
+ return GIT_COMMIT_HASH_REGEX.test(value.trim());
119
+ }
120
+ function getCommitUrlForHash(params) {
121
+ const { hash, repositoryWebUrl } = params;
122
+ const trimmedHash = hash.trim();
123
+ if (!isGitCommitHash(trimmedHash)) {
124
+ return null;
125
+ }
126
+ const trimmedRepositoryUrl = repositoryWebUrl.trim();
127
+ const webUrl = gitRemoteUrlToWeb(trimmedRepositoryUrl) ?? trimmedRepositoryUrl;
128
+ const provider = detectGitProvider(trimmedRepositoryUrl);
129
+ return `${webUrl.replace(/\/+$/, '')}${getCommitPathForProvider({
130
+ hash: trimmedHash,
131
+ provider
132
+ })}`;
133
+ }
134
+ function isCommitUrlForHash(params) {
135
+ const { hash, url } = params;
136
+ const trimmedHash = hash.trim();
137
+ const trimmedUrl = url.trim();
138
+ if (!isGitCommitHash(trimmedHash)) {
139
+ return false;
140
+ }
141
+ try {
142
+ const parsed = new URL(trimmedUrl);
143
+ const normalizedPathname = parsed.pathname.replace(/\/+$/, '');
144
+ return normalizedPathname.endsWith(getCommitPathForProvider({
145
+ hash: trimmedHash,
146
+ provider: detectGitProvider(trimmedUrl)
147
+ }));
148
+ }
149
+ catch {
150
+ return false;
151
+ }
152
+ }
153
+ //# sourceMappingURL=git-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-url.js","sourceRoot":"","sources":["../../src/utils/git-url.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAOH,sCAGC;AAED,kCAcC;AASD,8CAkBC;AAMD,kDA0BC;AA2CD,0CAEC;AAED,kDAeC;AAED,gDAoBC;AAvKD,MAAM,gBAAgB,GAAG,gFAAgF,CAAC;AAC1G,MAAM,cAAc,GAAG,mCAAmC,CAAC;AAE9C,QAAA,uBAAuB,GAAG,oCAAoC,CAAC;AAE5E,SAAgB,aAAa,CAAC,GAAW;IACvC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC3B,OAAO,gBAAgB,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AACxE,CAAC;AAED,SAAgB,WAAW,CAAC,GAAW;IACrC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE3B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,KAAK,YAAY;YAAE,OAAO,IAAI,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,mCAAmC;IACrC,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACrD,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,KAAK,YAAY;QAAE,OAAO,IAAI,CAAC;IAE/D,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,iBAAiB,CAAC,GAAW;IAC3C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE3B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QAChC,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YAChE,OAAO,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,+BAA+B;IACjC,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,uCAAuC,CAAC,CAAC;IACxE,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,OAAO,WAAW,QAAQ,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;IACjD,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,SAAgB,mBAAmB,CAAC,EAClC,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,KAAK,EACL,IAAI,EAOL;IACC,IAAI,CAAC,WAAW,CAAC,YAAY,CAAC;QAAE,OAAO,IAAI,CAAC;IAE5C,MAAM,MAAM,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IAEzB,MAAM,IAAI,GAAG,GAAG,MAAM,YAAY,kBAAkB,CAAC,QAAQ,CAAC,MAAM,kBAAkB,CAAC,UAAU,CAAC,EAAE,CAAC;IACrG,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IAEjC,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;IACrC,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;IAC1B,IAAI,KAAK;QAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;IACtC,IAAI,IAAI;QAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACnC,OAAO,GAAG,IAAI,IAAI,MAAM,CAAC,QAAQ,EAAE,EAAE,CAAC;AACxC,CAAC;AAID,MAAM,gBAAgB,GAAgC;IACpD,eAAe,EAAE,WAAW;IAC5B,YAAY,EAAE,QAAQ;IACtB,YAAY,EAAE,QAAQ;CACvB,CAAC;AAEF,SAAS,iBAAiB,CAAC,GAAW;IACpC,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE3B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,CAAC;QAChC,OAAO,gBAAgB,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,IAAI,SAAS,CAAC;IACtE,CAAC;IAAC,MAAM,CAAC;QACP,uCAAuC;IACzC,CAAC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;IACrD,IAAI,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAClB,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,IAAI,SAAS,CAAC;IAClE,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,SAAS,wBAAwB,CAAC,MAA+C;IAC/E,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IAClC,QAAQ,QAAQ,EAAE,CAAC;QACjB,KAAK,WAAW;YACd,OAAO,YAAY,IAAI,EAAE,CAAC;QAC5B,KAAK,QAAQ;YACX,OAAO,aAAa,IAAI,EAAE,CAAC;QAC7B,KAAK,QAAQ,CAAC;QACd,KAAK,SAAS;YACZ,OAAO,WAAW,IAAI,EAAE,CAAC;IAC7B,CAAC;AACH,CAAC;AAED,MAAM,qBAAqB,GAAG,mBAAmB,CAAC;AAElD,SAAgB,eAAe,CAAC,KAAa;IAC3C,OAAO,qBAAqB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;AAClD,CAAC;AAED,SAAgB,mBAAmB,CAAC,MAAkD;IACpF,MAAM,EAAE,IAAI,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAAC;IAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAChC,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,oBAAoB,GAAG,gBAAgB,CAAC,IAAI,EAAE,CAAC;IACrD,MAAM,MAAM,GAAG,iBAAiB,CAAC,oBAAoB,CAAC,IAAI,oBAAoB,CAAC;IAC/E,MAAM,QAAQ,GAAG,iBAAiB,CAAC,oBAAoB,CAAC,CAAC;IAEzD,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,wBAAwB,CAAC;QAC9D,IAAI,EAAE,WAAW;QACjB,QAAQ;KACT,CAAC,EAAE,CAAC;AACP,CAAC;AAED,SAAgB,kBAAkB,CAAC,MAAqC;IACtE,MAAM,EAAE,IAAI,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC;IAC7B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAChC,MAAM,UAAU,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAC9B,IAAI,CAAC,eAAe,CAAC,WAAW,CAAC,EAAE,CAAC;QAClC,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;QACnC,MAAM,kBAAkB,GAAG,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;QAC/D,OAAO,kBAAkB,CAAC,QAAQ,CAChC,wBAAwB,CAAC;YACvB,IAAI,EAAE,WAAW;YACjB,QAAQ,EAAE,iBAAiB,CAAC,UAAU,CAAC;SACxC,CAAC,CACH,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=git-url.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-url.test.d.ts","sourceRoot":"","sources":["../../src/utils/git-url.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const git_url_js_1 = require("./git-url.js");
5
+ (0, vitest_1.describe)('isValidGitUrl', () => {
6
+ vitest_1.it.each([
7
+ 'https://github.com/org/repo.git',
8
+ 'https://github.com/org/repo',
9
+ 'http://gitlab.com/org/repo.git',
10
+ 'git@github.com:org/repo.git',
11
+ 'git@gitlab.com:org/repo'
12
+ ])('returns true for %s', (url) => {
13
+ (0, vitest_1.expect)((0, git_url_js_1.isValidGitUrl)(url)).toBe(true);
14
+ });
15
+ vitest_1.it.each(['', 'not-a-url', 'ftp://github.com/org/repo', 'https://'])('returns false for %j', (url) => {
16
+ (0, vitest_1.expect)((0, git_url_js_1.isValidGitUrl)(url)).toBe(false);
17
+ });
18
+ (0, vitest_1.it)('trims whitespace', () => {
19
+ (0, vitest_1.expect)((0, git_url_js_1.isValidGitUrl)(' https://github.com/org/repo ')).toBe(true);
20
+ });
21
+ });
22
+ (0, vitest_1.describe)('isGitHubUrl', () => {
23
+ vitest_1.it.each(['https://github.com/org/repo.git', 'https://github.com/org/repo', 'git@github.com:org/repo.git'])('returns true for %s', (url) => {
24
+ (0, vitest_1.expect)((0, git_url_js_1.isGitHubUrl)(url)).toBe(true);
25
+ });
26
+ vitest_1.it.each(['https://gitlab.com/org/repo', 'git@bitbucket.org:org/repo.git', 'https://example.com/org/repo', 'not-a-url'])('returns false for %s', (url) => {
27
+ (0, vitest_1.expect)((0, git_url_js_1.isGitHubUrl)(url)).toBe(false);
28
+ });
29
+ (0, vitest_1.it)('is case-insensitive on the host', () => {
30
+ (0, vitest_1.expect)((0, git_url_js_1.isGitHubUrl)('https://GitHub.COM/org/repo')).toBe(true);
31
+ });
32
+ });
33
+ (0, vitest_1.describe)('gitRemoteUrlToWeb', () => {
34
+ vitest_1.it.each([
35
+ ['https://github.com/org/repo.git', 'https://github.com/org/repo'],
36
+ ['https://github.com/org/repo', 'https://github.com/org/repo'],
37
+ ['http://gitlab.com/org/repo.git', 'http://gitlab.com/org/repo'],
38
+ ['git@github.com:org/repo.git', 'https://github.com/org/repo'],
39
+ ['git@gitlab.com:group/sub/repo.git', 'https://gitlab.com/group/sub/repo']
40
+ ])('converts %s → %s', (input, expected) => {
41
+ (0, vitest_1.expect)((0, git_url_js_1.gitRemoteUrlToWeb)(input)).toBe(expected);
42
+ });
43
+ (0, vitest_1.it)('returns null for unconvertible URLs', () => {
44
+ (0, vitest_1.expect)((0, git_url_js_1.gitRemoteUrlToWeb)('not-a-url')).toBeNull();
45
+ });
46
+ });
47
+ (0, vitest_1.describe)('getGitHubCompareUrl', () => {
48
+ const base = { fromBranch: 'feature/cool', toBranch: 'main' };
49
+ (0, vitest_1.it)('builds a GitHub compare URL', () => {
50
+ (0, vitest_1.expect)((0, git_url_js_1.getGitHubCompareUrl)({
51
+ gitRemoteUrl: 'https://github.com/org/repo.git',
52
+ ...base
53
+ })).toBe('https://github.com/org/repo/compare/main...feature%2Fcool');
54
+ });
55
+ (0, vitest_1.it)('appends title and body', () => {
56
+ (0, vitest_1.expect)((0, git_url_js_1.getGitHubCompareUrl)({
57
+ gitRemoteUrl: 'https://github.com/org/repo.git',
58
+ ...base,
59
+ title: 'My PR',
60
+ body: '## Summary'
61
+ })).toBe('https://github.com/org/repo/compare/main...feature%2Fcool?expand=1&title=My+PR&body=%23%23+Summary');
62
+ });
63
+ (0, vitest_1.it)('returns null for non-GitHub URLs', () => {
64
+ (0, vitest_1.expect)((0, git_url_js_1.getGitHubCompareUrl)({
65
+ gitRemoteUrl: 'https://gitlab.com/org/repo',
66
+ ...base
67
+ })).toBeNull();
68
+ });
69
+ (0, vitest_1.it)('returns null for unconvertible URLs', () => {
70
+ (0, vitest_1.expect)((0, git_url_js_1.getGitHubCompareUrl)({ gitRemoteUrl: 'not-a-url', ...base })).toBeNull();
71
+ });
72
+ });
73
+ (0, vitest_1.describe)('GITHUB_PAT_SETTINGS_URL', () => {
74
+ (0, vitest_1.it)('is a valid URL', () => {
75
+ (0, vitest_1.expect)(git_url_js_1.GITHUB_PAT_SETTINGS_URL).toBe('https://github.com/settings/tokens');
76
+ });
77
+ });
78
+ //# sourceMappingURL=git-url.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-url.test.js","sourceRoot":"","sources":["../../src/utils/git-url.test.ts"],"names":[],"mappings":";;AAAA,mCAA8C;AAC9C,6CAA2H;AAE3H,IAAA,iBAAQ,EAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,WAAE,CAAC,IAAI,CAAC;QACN,iCAAiC;QACjC,6BAA6B;QAC7B,gCAAgC;QAChC,6BAA6B;QAC7B,yBAAyB;KAC1B,CAAC,CAAC,qBAAqB,EAAE,CAAC,GAAG,EAAE,EAAE;QAChC,IAAA,eAAM,EAAC,IAAA,0BAAa,EAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC,CAAC,CAAC;IAEH,WAAE,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,WAAW,EAAE,2BAA2B,EAAE,UAAU,CAAC,CAAC,CAAC,sBAAsB,EAAE,CAAC,GAAG,EAAE,EAAE;QAClG,IAAA,eAAM,EAAC,IAAA,0BAAa,EAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,kBAAkB,EAAE,GAAG,EAAE;QAC1B,IAAA,eAAM,EAAC,IAAA,0BAAa,EAAC,iCAAiC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,WAAE,CAAC,IAAI,CAAC,CAAC,iCAAiC,EAAE,6BAA6B,EAAE,6BAA6B,CAAC,CAAC,CACxG,qBAAqB,EACrB,CAAC,GAAG,EAAE,EAAE;QACN,IAAA,eAAM,EAAC,IAAA,wBAAW,EAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC,CACF,CAAC;IAEF,WAAE,CAAC,IAAI,CAAC,CAAC,6BAA6B,EAAE,gCAAgC,EAAE,8BAA8B,EAAE,WAAW,CAAC,CAAC,CACrH,sBAAsB,EACtB,CAAC,GAAG,EAAE,EAAE;QACN,IAAA,eAAM,EAAC,IAAA,wBAAW,EAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC,CACF,CAAC;IAEF,IAAA,WAAE,EAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,IAAA,eAAM,EAAC,IAAA,wBAAW,EAAC,6BAA6B,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,WAAE,CAAC,IAAI,CAAmB;QACxB,CAAC,iCAAiC,EAAE,6BAA6B,CAAC;QAClE,CAAC,6BAA6B,EAAE,6BAA6B,CAAC;QAC9D,CAAC,gCAAgC,EAAE,4BAA4B,CAAC;QAChE,CAAC,6BAA6B,EAAE,6BAA6B,CAAC;QAC9D,CAAC,mCAAmC,EAAE,mCAAmC,CAAC;KAC3E,CAAC,CAAC,kBAAkB,EAAE,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE;QACzC,IAAA,eAAM,EAAC,IAAA,8BAAiB,EAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,IAAA,eAAM,EAAC,IAAA,8BAAiB,EAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACpD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,MAAM,IAAI,GAAG,EAAE,UAAU,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;IAE9D,IAAA,WAAE,EAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,IAAA,eAAM,EACJ,IAAA,gCAAmB,EAAC;YAClB,YAAY,EAAE,iCAAiC;YAC/C,GAAG,IAAI;SACR,CAAC,CACH,CAAC,IAAI,CAAC,2DAA2D,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,IAAA,eAAM,EACJ,IAAA,gCAAmB,EAAC;YAClB,YAAY,EAAE,iCAAiC;YAC/C,GAAG,IAAI;YACP,KAAK,EAAE,OAAO;YACd,IAAI,EAAE,YAAY;SACnB,CAAC,CACH,CAAC,IAAI,CAAC,oGAAoG,CAAC,CAAC;IAC/G,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,IAAA,eAAM,EACJ,IAAA,gCAAmB,EAAC;YAClB,YAAY,EAAE,6BAA6B;YAC3C,GAAG,IAAI;SACR,CAAC,CACH,CAAC,QAAQ,EAAE,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,IAAA,WAAE,EAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,IAAA,eAAM,EAAC,IAAA,gCAAmB,EAAC,EAAE,YAAY,EAAE,WAAW,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAC;IACjF,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,IAAA,iBAAQ,EAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,IAAA,WAAE,EAAC,gBAAgB,EAAE,GAAG,EAAE;QACxB,IAAA,eAAM,EAAC,oCAAuB,CAAC,CAAC,IAAI,CAAC,oCAAoC,CAAC,CAAC;IAC7E,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -2,6 +2,7 @@ export * from './configuration.js';
2
2
  export * from './attachment-upload.js';
3
3
  export * from './env.js';
4
4
  export * from './environment.js';
5
+ export * from './git-url.js';
5
6
  export * from './string.js';
6
7
  export * from './time.js';
7
8
  export * from './truncate.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
@@ -22,6 +22,7 @@ __exportStar(require("./configuration.js"), exports);
22
22
  __exportStar(require("./attachment-upload.js"), exports);
23
23
  __exportStar(require("./env.js"), exports);
24
24
  __exportStar(require("./environment.js"), exports);
25
+ __exportStar(require("./git-url.js"), exports);
25
26
  __exportStar(require("./string.js"), exports);
26
27
  __exportStar(require("./time.js"), exports);
27
28
  __exportStar(require("./truncate.js"), exports);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,qDAAmC;AACnC,yDAAuC;AACvC,2CAAyB;AACzB,mDAAiC;AACjC,8CAA4B;AAC5B,4CAA0B;AAC1B,gDAA8B;AAC9B,2CAAyB;AACzB,kDAAgC;AAChC,6CAA2B;AAC3B,4CAA0B;AAC1B,iDAA+B;AAC/B,yDAAuC;AAEvC,wEAA+C;AACtC,iBADF,iBAAM,CACE"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA,qDAAmC;AACnC,yDAAuC;AACvC,2CAAyB;AACzB,mDAAiC;AACjC,+CAA6B;AAC7B,8CAA4B;AAC5B,4CAA0B;AAC1B,gDAA8B;AAC9B,2CAAyB;AACzB,kDAAgC;AAChC,6CAA2B;AAC3B,4CAA0B;AAC1B,iDAA+B;AAC/B,yDAAuC;AAEvC,wEAA+C;AACtC,iBADF,iBAAM,CACE"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Git remote URL utilities — GitHub-focused.
3
+ *
4
+ * Parsing, validation, and compare-URL generation for git remote URLs.
5
+ * Used by both the UI and the vite-plugin-file-sync AI agent tooling.
6
+ */
7
+ export declare const GITHUB_PAT_SETTINGS_URL = "https://github.com/settings/tokens";
8
+ export declare function isValidGitUrl(url: string): boolean;
9
+ export declare function isGitHubUrl(url: string): boolean;
10
+ /**
11
+ * Converts a git remote URL (HTTPS or SSH) to a browser-friendly HTTPS URL.
12
+ * Returns `null` when the URL cannot be converted.
13
+ *
14
+ * https://github.com/org/repo.git → https://github.com/org/repo
15
+ * git@github.com:org/repo.git → https://github.com/org/repo
16
+ */
17
+ export declare function gitRemoteUrlToWeb(url: string): string | null;
18
+ /**
19
+ * Returns a URL that opens the "new pull request" compare screen on GitHub.
20
+ * Returns `null` when the remote URL is not a GitHub URL or cannot be parsed.
21
+ */
22
+ export declare function getGitHubCompareUrl({ gitRemoteUrl, fromBranch, toBranch, title, body }: {
23
+ gitRemoteUrl: string;
24
+ fromBranch: string;
25
+ toBranch: string;
26
+ title?: string;
27
+ body?: string;
28
+ }): string | null;
29
+ export declare function isGitCommitHash(value: string): boolean;
30
+ export declare function getCommitUrlForHash(params: {
31
+ hash: string;
32
+ repositoryWebUrl: string;
33
+ }): string | null;
34
+ export declare function isCommitUrlForHash(params: {
35
+ hash: string;
36
+ url: string;
37
+ }): boolean;
38
+ //# sourceMappingURL=git-url.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-url.d.ts","sourceRoot":"","sources":["../../src/utils/git-url.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,eAAO,MAAM,uBAAuB,uCAAuC,CAAC;AAE5E,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGlD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAchD;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkB5D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,KAAK,EACL,IAAI,EACL,EAAE;IACD,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,GAAG,IAAI,CAchB;AA2CD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEtD;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAerG;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAoBjF"}
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Git remote URL utilities — GitHub-focused.
3
+ *
4
+ * Parsing, validation, and compare-URL generation for git remote URLs.
5
+ * Used by both the UI and the vite-plugin-file-sync AI agent tooling.
6
+ */
7
+ const HTTPS_GIT_URL_RE = /^https?:\/\/[^\s/$.?#]+\.[^\s/$.?#]+\/[^\s/$.?#]+(\/[^\s/$.?#]+)+(\.git)?\/?$/i;
8
+ const SSH_GIT_URL_RE = /^[\w-]+@[\w.-]+:[\w./-]+(\.git)?$/;
9
+ export const GITHUB_PAT_SETTINGS_URL = 'https://github.com/settings/tokens';
10
+ export function isValidGitUrl(url) {
11
+ const trimmed = url.trim();
12
+ return HTTPS_GIT_URL_RE.test(trimmed) || SSH_GIT_URL_RE.test(trimmed);
13
+ }
14
+ export function isGitHubUrl(url) {
15
+ const trimmed = url.trim();
16
+ try {
17
+ const parsed = new URL(trimmed);
18
+ if (parsed.hostname.toLowerCase() === 'github.com')
19
+ return true;
20
+ }
21
+ catch {
22
+ // Not a valid URL — try SSH format
23
+ }
24
+ const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
25
+ if (sshMatch?.[1]?.toLowerCase() === 'github.com')
26
+ return true;
27
+ return false;
28
+ }
29
+ /**
30
+ * Converts a git remote URL (HTTPS or SSH) to a browser-friendly HTTPS URL.
31
+ * Returns `null` when the URL cannot be converted.
32
+ *
33
+ * https://github.com/org/repo.git → https://github.com/org/repo
34
+ * git@github.com:org/repo.git → https://github.com/org/repo
35
+ */
36
+ export function gitRemoteUrlToWeb(url) {
37
+ const trimmed = url.trim();
38
+ try {
39
+ const parsed = new URL(trimmed);
40
+ if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
41
+ return trimmed.replace(/\.git\s*$/, '');
42
+ }
43
+ }
44
+ catch {
45
+ // Not a standard URL — try SSH
46
+ }
47
+ const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):(.+?)(?:\.git)?\s*$/);
48
+ if (sshMatch?.[1] && sshMatch[2]) {
49
+ return `https://${sshMatch[1]}/${sshMatch[2]}`;
50
+ }
51
+ return null;
52
+ }
53
+ /**
54
+ * Returns a URL that opens the "new pull request" compare screen on GitHub.
55
+ * Returns `null` when the remote URL is not a GitHub URL or cannot be parsed.
56
+ */
57
+ export function getGitHubCompareUrl({ gitRemoteUrl, fromBranch, toBranch, title, body }) {
58
+ if (!isGitHubUrl(gitRemoteUrl))
59
+ return null;
60
+ const webUrl = gitRemoteUrlToWeb(gitRemoteUrl);
61
+ if (!webUrl)
62
+ return null;
63
+ const base = `${webUrl}/compare/${encodeURIComponent(toBranch)}...${encodeURIComponent(fromBranch)}`;
64
+ if (!title && !body)
65
+ return base;
66
+ const params = new URLSearchParams();
67
+ params.set('expand', '1');
68
+ if (title)
69
+ params.set('title', title);
70
+ if (body)
71
+ params.set('body', body);
72
+ return `${base}?${params.toString()}`;
73
+ }
74
+ const HOST_TO_PROVIDER = {
75
+ 'bitbucket.org': 'bitbucket',
76
+ 'github.com': 'github',
77
+ 'gitlab.com': 'gitlab'
78
+ };
79
+ function detectGitProvider(url) {
80
+ const trimmed = url.trim();
81
+ try {
82
+ const parsed = new URL(trimmed);
83
+ return HOST_TO_PROVIDER[parsed.hostname.toLowerCase()] ?? 'unknown';
84
+ }
85
+ catch {
86
+ // Not a standard URL — try SSH format.
87
+ }
88
+ const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
89
+ if (sshMatch?.[1]) {
90
+ return HOST_TO_PROVIDER[sshMatch[1].toLowerCase()] ?? 'unknown';
91
+ }
92
+ return 'unknown';
93
+ }
94
+ function getCommitPathForProvider(params) {
95
+ const { hash, provider } = params;
96
+ switch (provider) {
97
+ case 'bitbucket':
98
+ return `/commits/${hash}`;
99
+ case 'gitlab':
100
+ return `/-/commit/${hash}`;
101
+ case 'github':
102
+ case 'unknown':
103
+ return `/commit/${hash}`;
104
+ }
105
+ }
106
+ const GIT_COMMIT_HASH_REGEX = /^[0-9a-f]{7,40}$/i;
107
+ export function isGitCommitHash(value) {
108
+ return GIT_COMMIT_HASH_REGEX.test(value.trim());
109
+ }
110
+ export function getCommitUrlForHash(params) {
111
+ const { hash, repositoryWebUrl } = params;
112
+ const trimmedHash = hash.trim();
113
+ if (!isGitCommitHash(trimmedHash)) {
114
+ return null;
115
+ }
116
+ const trimmedRepositoryUrl = repositoryWebUrl.trim();
117
+ const webUrl = gitRemoteUrlToWeb(trimmedRepositoryUrl) ?? trimmedRepositoryUrl;
118
+ const provider = detectGitProvider(trimmedRepositoryUrl);
119
+ return `${webUrl.replace(/\/+$/, '')}${getCommitPathForProvider({
120
+ hash: trimmedHash,
121
+ provider
122
+ })}`;
123
+ }
124
+ export function isCommitUrlForHash(params) {
125
+ const { hash, url } = params;
126
+ const trimmedHash = hash.trim();
127
+ const trimmedUrl = url.trim();
128
+ if (!isGitCommitHash(trimmedHash)) {
129
+ return false;
130
+ }
131
+ try {
132
+ const parsed = new URL(trimmedUrl);
133
+ const normalizedPathname = parsed.pathname.replace(/\/+$/, '');
134
+ return normalizedPathname.endsWith(getCommitPathForProvider({
135
+ hash: trimmedHash,
136
+ provider: detectGitProvider(trimmedUrl)
137
+ }));
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
143
+ //# sourceMappingURL=git-url.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-url.js","sourceRoot":"","sources":["../../src/utils/git-url.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=git-url.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-url.test.d.ts","sourceRoot":"","sources":["../../src/utils/git-url.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { GITHUB_PAT_SETTINGS_URL, getGitHubCompareUrl, gitRemoteUrlToWeb, isGitHubUrl, isValidGitUrl } from './git-url.js';
3
+ describe('isValidGitUrl', () => {
4
+ it.each([
5
+ 'https://github.com/org/repo.git',
6
+ 'https://github.com/org/repo',
7
+ 'http://gitlab.com/org/repo.git',
8
+ 'git@github.com:org/repo.git',
9
+ 'git@gitlab.com:org/repo'
10
+ ])('returns true for %s', (url) => {
11
+ expect(isValidGitUrl(url)).toBe(true);
12
+ });
13
+ it.each(['', 'not-a-url', 'ftp://github.com/org/repo', 'https://'])('returns false for %j', (url) => {
14
+ expect(isValidGitUrl(url)).toBe(false);
15
+ });
16
+ it('trims whitespace', () => {
17
+ expect(isValidGitUrl(' https://github.com/org/repo ')).toBe(true);
18
+ });
19
+ });
20
+ describe('isGitHubUrl', () => {
21
+ it.each(['https://github.com/org/repo.git', 'https://github.com/org/repo', 'git@github.com:org/repo.git'])('returns true for %s', (url) => {
22
+ expect(isGitHubUrl(url)).toBe(true);
23
+ });
24
+ it.each(['https://gitlab.com/org/repo', 'git@bitbucket.org:org/repo.git', 'https://example.com/org/repo', 'not-a-url'])('returns false for %s', (url) => {
25
+ expect(isGitHubUrl(url)).toBe(false);
26
+ });
27
+ it('is case-insensitive on the host', () => {
28
+ expect(isGitHubUrl('https://GitHub.COM/org/repo')).toBe(true);
29
+ });
30
+ });
31
+ describe('gitRemoteUrlToWeb', () => {
32
+ it.each([
33
+ ['https://github.com/org/repo.git', 'https://github.com/org/repo'],
34
+ ['https://github.com/org/repo', 'https://github.com/org/repo'],
35
+ ['http://gitlab.com/org/repo.git', 'http://gitlab.com/org/repo'],
36
+ ['git@github.com:org/repo.git', 'https://github.com/org/repo'],
37
+ ['git@gitlab.com:group/sub/repo.git', 'https://gitlab.com/group/sub/repo']
38
+ ])('converts %s → %s', (input, expected) => {
39
+ expect(gitRemoteUrlToWeb(input)).toBe(expected);
40
+ });
41
+ it('returns null for unconvertible URLs', () => {
42
+ expect(gitRemoteUrlToWeb('not-a-url')).toBeNull();
43
+ });
44
+ });
45
+ describe('getGitHubCompareUrl', () => {
46
+ const base = { fromBranch: 'feature/cool', toBranch: 'main' };
47
+ it('builds a GitHub compare URL', () => {
48
+ expect(getGitHubCompareUrl({
49
+ gitRemoteUrl: 'https://github.com/org/repo.git',
50
+ ...base
51
+ })).toBe('https://github.com/org/repo/compare/main...feature%2Fcool');
52
+ });
53
+ it('appends title and body', () => {
54
+ expect(getGitHubCompareUrl({
55
+ gitRemoteUrl: 'https://github.com/org/repo.git',
56
+ ...base,
57
+ title: 'My PR',
58
+ body: '## Summary'
59
+ })).toBe('https://github.com/org/repo/compare/main...feature%2Fcool?expand=1&title=My+PR&body=%23%23+Summary');
60
+ });
61
+ it('returns null for non-GitHub URLs', () => {
62
+ expect(getGitHubCompareUrl({
63
+ gitRemoteUrl: 'https://gitlab.com/org/repo',
64
+ ...base
65
+ })).toBeNull();
66
+ });
67
+ it('returns null for unconvertible URLs', () => {
68
+ expect(getGitHubCompareUrl({ gitRemoteUrl: 'not-a-url', ...base })).toBeNull();
69
+ });
70
+ });
71
+ describe('GITHUB_PAT_SETTINGS_URL', () => {
72
+ it('is a valid URL', () => {
73
+ expect(GITHUB_PAT_SETTINGS_URL).toBe('https://github.com/settings/tokens');
74
+ });
75
+ });
76
+ //# sourceMappingURL=git-url.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-url.test.js","sourceRoot":"","sources":["../../src/utils/git-url.test.ts"],"names":[],"mappings":"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"}
@@ -2,6 +2,7 @@ export * from './configuration.js';
2
2
  export * from './attachment-upload.js';
3
3
  export * from './env.js';
4
4
  export * from './environment.js';
5
+ export * from './git-url.js';
5
6
  export * from './string.js';
6
7
  export * from './time.js';
7
8
  export * from './truncate.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
@@ -2,6 +2,7 @@ export * from './configuration.js';
2
2
  export * from './attachment-upload.js';
3
3
  export * from './env.js';
4
4
  export * from './environment.js';
5
+ export * from './git-url.js';
5
6
  export * from './string.js';
6
7
  export * from './time.js';
7
8
  export * from './truncate.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/utils/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,UAAU,CAAC;AACzB,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,eAAe,CAAC;AAC9B,cAAc,UAAU,CAAC;AACzB,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,wBAAwB,CAAC;AAEvC,OAAO,MAAM,MAAM,2BAA2B,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superblocksteam/shared",
3
- "version": "0.9569.3",
3
+ "version": "0.9570.0",
4
4
  "description": "Superblocks Shared Resources",
5
5
  "license": "Superblocks Community Software License",
6
6
  "repository": "https://github.com/superblocksteam/shared.git",
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { GITHUB_PAT_SETTINGS_URL, getGitHubCompareUrl, gitRemoteUrlToWeb, isGitHubUrl, isValidGitUrl } from './git-url.js';
3
+
4
+ describe('isValidGitUrl', () => {
5
+ it.each([
6
+ 'https://github.com/org/repo.git',
7
+ 'https://github.com/org/repo',
8
+ 'http://gitlab.com/org/repo.git',
9
+ 'git@github.com:org/repo.git',
10
+ 'git@gitlab.com:org/repo'
11
+ ])('returns true for %s', (url) => {
12
+ expect(isValidGitUrl(url)).toBe(true);
13
+ });
14
+
15
+ it.each(['', 'not-a-url', 'ftp://github.com/org/repo', 'https://'])('returns false for %j', (url) => {
16
+ expect(isValidGitUrl(url)).toBe(false);
17
+ });
18
+
19
+ it('trims whitespace', () => {
20
+ expect(isValidGitUrl(' https://github.com/org/repo ')).toBe(true);
21
+ });
22
+ });
23
+
24
+ describe('isGitHubUrl', () => {
25
+ it.each(['https://github.com/org/repo.git', 'https://github.com/org/repo', 'git@github.com:org/repo.git'])(
26
+ 'returns true for %s',
27
+ (url) => {
28
+ expect(isGitHubUrl(url)).toBe(true);
29
+ }
30
+ );
31
+
32
+ it.each(['https://gitlab.com/org/repo', 'git@bitbucket.org:org/repo.git', 'https://example.com/org/repo', 'not-a-url'])(
33
+ 'returns false for %s',
34
+ (url) => {
35
+ expect(isGitHubUrl(url)).toBe(false);
36
+ }
37
+ );
38
+
39
+ it('is case-insensitive on the host', () => {
40
+ expect(isGitHubUrl('https://GitHub.COM/org/repo')).toBe(true);
41
+ });
42
+ });
43
+
44
+ describe('gitRemoteUrlToWeb', () => {
45
+ it.each<[string, string]>([
46
+ ['https://github.com/org/repo.git', 'https://github.com/org/repo'],
47
+ ['https://github.com/org/repo', 'https://github.com/org/repo'],
48
+ ['http://gitlab.com/org/repo.git', 'http://gitlab.com/org/repo'],
49
+ ['git@github.com:org/repo.git', 'https://github.com/org/repo'],
50
+ ['git@gitlab.com:group/sub/repo.git', 'https://gitlab.com/group/sub/repo']
51
+ ])('converts %s → %s', (input, expected) => {
52
+ expect(gitRemoteUrlToWeb(input)).toBe(expected);
53
+ });
54
+
55
+ it('returns null for unconvertible URLs', () => {
56
+ expect(gitRemoteUrlToWeb('not-a-url')).toBeNull();
57
+ });
58
+ });
59
+
60
+ describe('getGitHubCompareUrl', () => {
61
+ const base = { fromBranch: 'feature/cool', toBranch: 'main' };
62
+
63
+ it('builds a GitHub compare URL', () => {
64
+ expect(
65
+ getGitHubCompareUrl({
66
+ gitRemoteUrl: 'https://github.com/org/repo.git',
67
+ ...base
68
+ })
69
+ ).toBe('https://github.com/org/repo/compare/main...feature%2Fcool');
70
+ });
71
+
72
+ it('appends title and body', () => {
73
+ expect(
74
+ getGitHubCompareUrl({
75
+ gitRemoteUrl: 'https://github.com/org/repo.git',
76
+ ...base,
77
+ title: 'My PR',
78
+ body: '## Summary'
79
+ })
80
+ ).toBe('https://github.com/org/repo/compare/main...feature%2Fcool?expand=1&title=My+PR&body=%23%23+Summary');
81
+ });
82
+
83
+ it('returns null for non-GitHub URLs', () => {
84
+ expect(
85
+ getGitHubCompareUrl({
86
+ gitRemoteUrl: 'https://gitlab.com/org/repo',
87
+ ...base
88
+ })
89
+ ).toBeNull();
90
+ });
91
+
92
+ it('returns null for unconvertible URLs', () => {
93
+ expect(getGitHubCompareUrl({ gitRemoteUrl: 'not-a-url', ...base })).toBeNull();
94
+ });
95
+ });
96
+
97
+ describe('GITHUB_PAT_SETTINGS_URL', () => {
98
+ it('is a valid URL', () => {
99
+ expect(GITHUB_PAT_SETTINGS_URL).toBe('https://github.com/settings/tokens');
100
+ });
101
+ });
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Git remote URL utilities — GitHub-focused.
3
+ *
4
+ * Parsing, validation, and compare-URL generation for git remote URLs.
5
+ * Used by both the UI and the vite-plugin-file-sync AI agent tooling.
6
+ */
7
+
8
+ const HTTPS_GIT_URL_RE = /^https?:\/\/[^\s/$.?#]+\.[^\s/$.?#]+\/[^\s/$.?#]+(\/[^\s/$.?#]+)+(\.git)?\/?$/i;
9
+ const SSH_GIT_URL_RE = /^[\w-]+@[\w.-]+:[\w./-]+(\.git)?$/;
10
+
11
+ export const GITHUB_PAT_SETTINGS_URL = 'https://github.com/settings/tokens';
12
+
13
+ export function isValidGitUrl(url: string): boolean {
14
+ const trimmed = url.trim();
15
+ return HTTPS_GIT_URL_RE.test(trimmed) || SSH_GIT_URL_RE.test(trimmed);
16
+ }
17
+
18
+ export function isGitHubUrl(url: string): boolean {
19
+ const trimmed = url.trim();
20
+
21
+ try {
22
+ const parsed = new URL(trimmed);
23
+ if (parsed.hostname.toLowerCase() === 'github.com') return true;
24
+ } catch {
25
+ // Not a valid URL — try SSH format
26
+ }
27
+
28
+ const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
29
+ if (sshMatch?.[1]?.toLowerCase() === 'github.com') return true;
30
+
31
+ return false;
32
+ }
33
+
34
+ /**
35
+ * Converts a git remote URL (HTTPS or SSH) to a browser-friendly HTTPS URL.
36
+ * Returns `null` when the URL cannot be converted.
37
+ *
38
+ * https://github.com/org/repo.git → https://github.com/org/repo
39
+ * git@github.com:org/repo.git → https://github.com/org/repo
40
+ */
41
+ export function gitRemoteUrlToWeb(url: string): string | null {
42
+ const trimmed = url.trim();
43
+
44
+ try {
45
+ const parsed = new URL(trimmed);
46
+ if (parsed.protocol === 'https:' || parsed.protocol === 'http:') {
47
+ return trimmed.replace(/\.git\s*$/, '');
48
+ }
49
+ } catch {
50
+ // Not a standard URL — try SSH
51
+ }
52
+
53
+ const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):(.+?)(?:\.git)?\s*$/);
54
+ if (sshMatch?.[1] && sshMatch[2]) {
55
+ return `https://${sshMatch[1]}/${sshMatch[2]}`;
56
+ }
57
+
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Returns a URL that opens the "new pull request" compare screen on GitHub.
63
+ * Returns `null` when the remote URL is not a GitHub URL or cannot be parsed.
64
+ */
65
+ export function getGitHubCompareUrl({
66
+ gitRemoteUrl,
67
+ fromBranch,
68
+ toBranch,
69
+ title,
70
+ body
71
+ }: {
72
+ gitRemoteUrl: string;
73
+ fromBranch: string;
74
+ toBranch: string;
75
+ title?: string;
76
+ body?: string;
77
+ }): string | null {
78
+ if (!isGitHubUrl(gitRemoteUrl)) return null;
79
+
80
+ const webUrl = gitRemoteUrlToWeb(gitRemoteUrl);
81
+ if (!webUrl) return null;
82
+
83
+ const base = `${webUrl}/compare/${encodeURIComponent(toBranch)}...${encodeURIComponent(fromBranch)}`;
84
+ if (!title && !body) return base;
85
+
86
+ const params = new URLSearchParams();
87
+ params.set('expand', '1');
88
+ if (title) params.set('title', title);
89
+ if (body) params.set('body', body);
90
+ return `${base}?${params.toString()}`;
91
+ }
92
+
93
+ type GitProvider = 'github' | 'gitlab' | 'bitbucket' | 'unknown';
94
+
95
+ const HOST_TO_PROVIDER: Record<string, GitProvider> = {
96
+ 'bitbucket.org': 'bitbucket',
97
+ 'github.com': 'github',
98
+ 'gitlab.com': 'gitlab'
99
+ };
100
+
101
+ function detectGitProvider(url: string): GitProvider {
102
+ const trimmed = url.trim();
103
+
104
+ try {
105
+ const parsed = new URL(trimmed);
106
+ return HOST_TO_PROVIDER[parsed.hostname.toLowerCase()] ?? 'unknown';
107
+ } catch {
108
+ // Not a standard URL — try SSH format.
109
+ }
110
+
111
+ const sshMatch = trimmed.match(/^[\w-]+@([\w.-]+):/);
112
+ if (sshMatch?.[1]) {
113
+ return HOST_TO_PROVIDER[sshMatch[1].toLowerCase()] ?? 'unknown';
114
+ }
115
+
116
+ return 'unknown';
117
+ }
118
+
119
+ function getCommitPathForProvider(params: { hash: string; provider: GitProvider }): string {
120
+ const { hash, provider } = params;
121
+ switch (provider) {
122
+ case 'bitbucket':
123
+ return `/commits/${hash}`;
124
+ case 'gitlab':
125
+ return `/-/commit/${hash}`;
126
+ case 'github':
127
+ case 'unknown':
128
+ return `/commit/${hash}`;
129
+ }
130
+ }
131
+
132
+ const GIT_COMMIT_HASH_REGEX = /^[0-9a-f]{7,40}$/i;
133
+
134
+ export function isGitCommitHash(value: string): boolean {
135
+ return GIT_COMMIT_HASH_REGEX.test(value.trim());
136
+ }
137
+
138
+ export function getCommitUrlForHash(params: { hash: string; repositoryWebUrl: string }): string | null {
139
+ const { hash, repositoryWebUrl } = params;
140
+ const trimmedHash = hash.trim();
141
+ if (!isGitCommitHash(trimmedHash)) {
142
+ return null;
143
+ }
144
+
145
+ const trimmedRepositoryUrl = repositoryWebUrl.trim();
146
+ const webUrl = gitRemoteUrlToWeb(trimmedRepositoryUrl) ?? trimmedRepositoryUrl;
147
+ const provider = detectGitProvider(trimmedRepositoryUrl);
148
+
149
+ return `${webUrl.replace(/\/+$/, '')}${getCommitPathForProvider({
150
+ hash: trimmedHash,
151
+ provider
152
+ })}`;
153
+ }
154
+
155
+ export function isCommitUrlForHash(params: { hash: string; url: string }): boolean {
156
+ const { hash, url } = params;
157
+ const trimmedHash = hash.trim();
158
+ const trimmedUrl = url.trim();
159
+ if (!isGitCommitHash(trimmedHash)) {
160
+ return false;
161
+ }
162
+
163
+ try {
164
+ const parsed = new URL(trimmedUrl);
165
+ const normalizedPathname = parsed.pathname.replace(/\/+$/, '');
166
+ return normalizedPathname.endsWith(
167
+ getCommitPathForProvider({
168
+ hash: trimmedHash,
169
+ provider: detectGitProvider(trimmedUrl)
170
+ })
171
+ );
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
@@ -2,6 +2,7 @@ export * from './configuration.js';
2
2
  export * from './attachment-upload.js';
3
3
  export * from './env.js';
4
4
  export * from './environment.js';
5
+ export * from './git-url.js';
5
6
  export * from './string.js';
6
7
  export * from './time.js';
7
8
  export * from './truncate.js';