codiedev 0.7.6 → 0.7.8
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/__tests__/connectFlow.test.d.ts +1 -0
- package/dist/__tests__/connectFlow.test.js +33 -0
- package/dist/__tests__/detection.test.d.ts +1 -0
- package/dist/__tests__/detection.test.js +18 -0
- package/dist/__tests__/prUrl.test.d.ts +1 -0
- package/dist/__tests__/prUrl.test.js +50 -0
- package/dist/__tests__/repoResolver.test.d.ts +1 -0
- package/dist/__tests__/repoResolver.test.js +72 -0
- package/dist/__tests__/versionCheck.test.d.ts +1 -0
- package/dist/__tests__/versionCheck.test.js +85 -0
- package/dist/cli.js +1 -1
- package/dist/commands/doctor.js +28 -8
- package/dist/commands/reverseTicket.js +109 -48
- package/dist/commands/shared.js +2 -0
- package/dist/connect.d.ts +1 -1
- package/dist/connect.js +35 -15
- package/dist/connectFlow.d.ts +13 -0
- package/dist/connectFlow.js +21 -0
- package/dist/detection.d.ts +12 -0
- package/dist/detection.js +16 -0
- package/dist/hook.js +7 -7
- package/dist/prUrl.d.ts +9 -0
- package/dist/prUrl.js +33 -0
- package/dist/repoResolver.d.ts +17 -0
- package/dist/repoResolver.js +41 -0
- package/dist/upgradeNudge.d.ts +7 -0
- package/dist/upgradeNudge.js +123 -0
- package/dist/utils.js +14 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +67 -0
- package/dist/versionCheck.d.ts +28 -0
- package/dist/versionCheck.js +49 -0
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const connectFlow_1 = require("../connectFlow");
|
|
5
|
+
const SAVED = {
|
|
6
|
+
token: "cdv_savedToken_xxxx",
|
|
7
|
+
companyId: "co_1",
|
|
8
|
+
companyName: "Sonifi",
|
|
9
|
+
repos: [],
|
|
10
|
+
lastSynced: new Date().toISOString(),
|
|
11
|
+
backendUrl: "https://example.test",
|
|
12
|
+
};
|
|
13
|
+
(0, vitest_1.describe)("resolveConnectToken", () => {
|
|
14
|
+
(0, vitest_1.it)("uses saved token when present and force=false", () => {
|
|
15
|
+
const res = (0, connectFlow_1.resolveConnectToken)({ savedConfig: SAVED, force: false });
|
|
16
|
+
(0, vitest_1.expect)(res).toEqual({ source: "reused", token: SAVED.token });
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.it)("requires a prompt when no config exists", () => {
|
|
19
|
+
const res = (0, connectFlow_1.resolveConnectToken)({ savedConfig: null, force: false });
|
|
20
|
+
(0, vitest_1.expect)(res).toEqual({ source: "prompt-required" });
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.it)("requires a prompt when --force is set, even if a saved token exists", () => {
|
|
23
|
+
const res = (0, connectFlow_1.resolveConnectToken)({ savedConfig: SAVED, force: true });
|
|
24
|
+
(0, vitest_1.expect)(res).toEqual({ source: "prompt-required" });
|
|
25
|
+
});
|
|
26
|
+
(0, vitest_1.it)("requires a prompt when saved config has empty token", () => {
|
|
27
|
+
const res = (0, connectFlow_1.resolveConnectToken)({
|
|
28
|
+
savedConfig: { ...SAVED, token: "" },
|
|
29
|
+
force: false,
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.expect)(res).toEqual({ source: "prompt-required" });
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const detection_1 = require("../detection");
|
|
5
|
+
(0, vitest_1.describe)("detectClaudeCode", () => {
|
|
6
|
+
(0, vitest_1.it)("returns false when neither ~/.claude nor `claude` binary present", () => {
|
|
7
|
+
(0, vitest_1.expect)((0, detection_1.detectClaudeCode)({ dirExists: false, binOnPath: false })).toBe(false);
|
|
8
|
+
});
|
|
9
|
+
(0, vitest_1.it)("returns true when only ~/.claude exists", () => {
|
|
10
|
+
(0, vitest_1.expect)((0, detection_1.detectClaudeCode)({ dirExists: true, binOnPath: false })).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
(0, vitest_1.it)("returns true when only `claude` is on PATH (CC installed but never launched)", () => {
|
|
13
|
+
(0, vitest_1.expect)((0, detection_1.detectClaudeCode)({ dirExists: false, binOnPath: true })).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
(0, vitest_1.it)("returns true when both are present", () => {
|
|
16
|
+
(0, vitest_1.expect)((0, detection_1.detectClaudeCode)({ dirExists: true, binOnPath: true })).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const prUrl_1 = require("../prUrl");
|
|
5
|
+
(0, vitest_1.describe)("parsePrOrMrUrl", () => {
|
|
6
|
+
(0, vitest_1.it)("parses a GitHub PR url", () => {
|
|
7
|
+
(0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://github.com/foo/bar/pull/123")).toEqual({
|
|
8
|
+
provider: "github",
|
|
9
|
+
host: "github.com",
|
|
10
|
+
owner: "foo",
|
|
11
|
+
repo: "bar",
|
|
12
|
+
number: 123,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
(0, vitest_1.it)("parses a GitLab.com MR url", () => {
|
|
16
|
+
(0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://gitlab.com/sonifi/platform/-/merge_requests/42")).toEqual({
|
|
17
|
+
provider: "gitlab",
|
|
18
|
+
host: "gitlab.com",
|
|
19
|
+
owner: "sonifi",
|
|
20
|
+
repo: "platform",
|
|
21
|
+
number: 42,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.it)("parses a self-hosted GitLab MR url", () => {
|
|
25
|
+
(0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://git.sonifi.com/internal/platform/-/merge_requests/7")).toEqual({
|
|
26
|
+
provider: "gitlab",
|
|
27
|
+
host: "git.sonifi.com",
|
|
28
|
+
owner: "internal",
|
|
29
|
+
repo: "platform",
|
|
30
|
+
number: 7,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
(0, vitest_1.it)("parses a GitLab MR url with a nested group path", () => {
|
|
34
|
+
(0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://gitlab.com/sonifi/team-a/platform/-/merge_requests/9")).toEqual({
|
|
35
|
+
provider: "gitlab",
|
|
36
|
+
host: "gitlab.com",
|
|
37
|
+
owner: "sonifi/team-a",
|
|
38
|
+
repo: "platform",
|
|
39
|
+
number: 9,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
(0, vitest_1.it)("returns null for unrecognized URLs", () => {
|
|
43
|
+
(0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://example.com/foo")).toBe(null);
|
|
44
|
+
(0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("not a url")).toBe(null);
|
|
45
|
+
(0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("")).toBe(null);
|
|
46
|
+
});
|
|
47
|
+
(0, vitest_1.it)("returns null when the number segment isn't an integer", () => {
|
|
48
|
+
(0, vitest_1.expect)((0, prUrl_1.parsePrOrMrUrl)("https://github.com/foo/bar/pull/abc")).toBe(null);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const repoResolver_1 = require("../repoResolver");
|
|
5
|
+
const ghRepo = { _id: "rep_gh1", owner: "Signal-and-Code", name: "vm-demo", gitProvider: "github" };
|
|
6
|
+
const glRepo = { _id: "rep_gl1", owner: "sonifi", name: "platform", gitProvider: "gitlab" };
|
|
7
|
+
(0, vitest_1.describe)("resolveRepoForCapture", () => {
|
|
8
|
+
(0, vitest_1.it)("returns the provided repoId when it matches a current repo", () => {
|
|
9
|
+
const res = (0, repoResolver_1.resolveRepoForCapture)({
|
|
10
|
+
remoteUrl: "https://github.com/Signal-and-Code/vm-demo.git",
|
|
11
|
+
providedRepoId: "rep_gh1",
|
|
12
|
+
companyRepos: [ghRepo],
|
|
13
|
+
});
|
|
14
|
+
(0, vitest_1.expect)(res).toEqual({ repoId: "rep_gh1", source: "provided" });
|
|
15
|
+
});
|
|
16
|
+
(0, vitest_1.it)("falls back to remoteUrl resolution when providedRepoId is stale", () => {
|
|
17
|
+
const res = (0, repoResolver_1.resolveRepoForCapture)({
|
|
18
|
+
remoteUrl: "https://github.com/Signal-and-Code/vm-demo.git",
|
|
19
|
+
providedRepoId: "rep_deleted",
|
|
20
|
+
companyRepos: [ghRepo],
|
|
21
|
+
});
|
|
22
|
+
(0, vitest_1.expect)(res).toEqual({ repoId: "rep_gh1", source: "resolved" });
|
|
23
|
+
});
|
|
24
|
+
(0, vitest_1.it)("resolves from remoteUrl when no providedRepoId is given (HTTPS form)", () => {
|
|
25
|
+
const res = (0, repoResolver_1.resolveRepoForCapture)({
|
|
26
|
+
remoteUrl: "https://github.com/Signal-and-Code/vm-demo.git",
|
|
27
|
+
providedRepoId: null,
|
|
28
|
+
companyRepos: [ghRepo],
|
|
29
|
+
});
|
|
30
|
+
(0, vitest_1.expect)(res).toEqual({ repoId: "rep_gh1", source: "resolved" });
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)("resolves from remoteUrl when no providedRepoId is given (SSH form)", () => {
|
|
33
|
+
const res = (0, repoResolver_1.resolveRepoForCapture)({
|
|
34
|
+
remoteUrl: "git@github.com:Signal-and-Code/vm-demo.git",
|
|
35
|
+
providedRepoId: null,
|
|
36
|
+
companyRepos: [ghRepo],
|
|
37
|
+
});
|
|
38
|
+
(0, vitest_1.expect)(res).toEqual({ repoId: "rep_gh1", source: "resolved" });
|
|
39
|
+
});
|
|
40
|
+
(0, vitest_1.it)("resolves a GitLab remote against a gitlab-provider repo", () => {
|
|
41
|
+
const res = (0, repoResolver_1.resolveRepoForCapture)({
|
|
42
|
+
remoteUrl: "https://gitlab.com/sonifi/platform.git",
|
|
43
|
+
providedRepoId: null,
|
|
44
|
+
companyRepos: [ghRepo, glRepo],
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.expect)(res).toEqual({ repoId: "rep_gl1", source: "resolved" });
|
|
47
|
+
});
|
|
48
|
+
(0, vitest_1.it)("resolves SSH-form GitLab URL", () => {
|
|
49
|
+
const res = (0, repoResolver_1.resolveRepoForCapture)({
|
|
50
|
+
remoteUrl: "git@gitlab.com:sonifi/platform.git",
|
|
51
|
+
providedRepoId: null,
|
|
52
|
+
companyRepos: [glRepo],
|
|
53
|
+
});
|
|
54
|
+
(0, vitest_1.expect)(res).toEqual({ repoId: "rep_gl1", source: "resolved" });
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)("returns unmatched when remoteUrl matches no repo (don't drop the session — let backend decide)", () => {
|
|
57
|
+
const res = (0, repoResolver_1.resolveRepoForCapture)({
|
|
58
|
+
remoteUrl: "https://github.com/some/other-repo.git",
|
|
59
|
+
providedRepoId: null,
|
|
60
|
+
companyRepos: [ghRepo, glRepo],
|
|
61
|
+
});
|
|
62
|
+
(0, vitest_1.expect)(res).toEqual({ repoId: null, source: "unmatched" });
|
|
63
|
+
});
|
|
64
|
+
(0, vitest_1.it)("matches case-insensitively on owner/name (GitHub is case-insensitive on org/repo names)", () => {
|
|
65
|
+
const res = (0, repoResolver_1.resolveRepoForCapture)({
|
|
66
|
+
remoteUrl: "https://github.com/SIGNAL-AND-CODE/vm-demo.git",
|
|
67
|
+
providedRepoId: null,
|
|
68
|
+
companyRepos: [ghRepo],
|
|
69
|
+
});
|
|
70
|
+
(0, vitest_1.expect)(res).toEqual({ repoId: "rep_gh1", source: "resolved" });
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const versionCheck_1 = require("../versionCheck");
|
|
5
|
+
(0, vitest_1.describe)("parseSemver", () => {
|
|
6
|
+
(0, vitest_1.it)("parses a normal version", () => {
|
|
7
|
+
(0, vitest_1.expect)((0, versionCheck_1.parseSemver)("0.7.7")).toEqual({ major: 0, minor: 7, patch: 7 });
|
|
8
|
+
});
|
|
9
|
+
(0, vitest_1.it)("strips a leading v", () => {
|
|
10
|
+
(0, vitest_1.expect)((0, versionCheck_1.parseSemver)("v1.2.3")).toEqual({ major: 1, minor: 2, patch: 3 });
|
|
11
|
+
});
|
|
12
|
+
(0, vitest_1.it)("ignores prerelease/build suffixes", () => {
|
|
13
|
+
(0, vitest_1.expect)((0, versionCheck_1.parseSemver)("1.2.3-beta.1+build.99")).toEqual({
|
|
14
|
+
major: 1,
|
|
15
|
+
minor: 2,
|
|
16
|
+
patch: 3,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
(0, vitest_1.it)("returns null for malformed input", () => {
|
|
20
|
+
(0, vitest_1.expect)((0, versionCheck_1.parseSemver)("")).toBe(null);
|
|
21
|
+
(0, vitest_1.expect)((0, versionCheck_1.parseSemver)("not.a.version")).toBe(null);
|
|
22
|
+
(0, vitest_1.expect)((0, versionCheck_1.parseSemver)("1.2")).toBe(null);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
(0, vitest_1.describe)("compareSemver", () => {
|
|
26
|
+
(0, vitest_1.it)("returns -1 when current is older (patch)", () => {
|
|
27
|
+
(0, vitest_1.expect)((0, versionCheck_1.compareSemver)("0.7.6", "0.7.7")).toBe(-1);
|
|
28
|
+
});
|
|
29
|
+
(0, vitest_1.it)("returns -1 when current is older (minor)", () => {
|
|
30
|
+
(0, vitest_1.expect)((0, versionCheck_1.compareSemver)("0.6.99", "0.7.0")).toBe(-1);
|
|
31
|
+
});
|
|
32
|
+
(0, vitest_1.it)("returns -1 when current is older (major)", () => {
|
|
33
|
+
(0, vitest_1.expect)((0, versionCheck_1.compareSemver)("0.99.99", "1.0.0")).toBe(-1);
|
|
34
|
+
});
|
|
35
|
+
(0, vitest_1.it)("returns 0 when equal", () => {
|
|
36
|
+
(0, vitest_1.expect)((0, versionCheck_1.compareSemver)("0.7.7", "0.7.7")).toBe(0);
|
|
37
|
+
});
|
|
38
|
+
(0, vitest_1.it)("returns 1 when current is ahead of latest (e.g. local dev build)", () => {
|
|
39
|
+
(0, vitest_1.expect)((0, versionCheck_1.compareSemver)("0.8.0", "0.7.7")).toBe(1);
|
|
40
|
+
});
|
|
41
|
+
(0, vitest_1.it)("treats malformed inputs as equal — never warn on garbage", () => {
|
|
42
|
+
(0, vitest_1.expect)((0, versionCheck_1.compareSemver)("garbage", "0.7.7")).toBe(0);
|
|
43
|
+
(0, vitest_1.expect)((0, versionCheck_1.compareSemver)("0.7.7", "garbage")).toBe(0);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
(0, vitest_1.describe)("shouldShowUpdateWarning", () => {
|
|
47
|
+
const now = 1_777_777_777_000;
|
|
48
|
+
const dayMs = 24 * 60 * 60 * 1000;
|
|
49
|
+
(0, vitest_1.it)("warns when current is older AND cache is fresh", () => {
|
|
50
|
+
(0, vitest_1.expect)((0, versionCheck_1.shouldShowUpdateWarning)({
|
|
51
|
+
current: "0.7.6",
|
|
52
|
+
latest: "0.7.7",
|
|
53
|
+
lastChecked: now - 1000,
|
|
54
|
+
nowMs: now,
|
|
55
|
+
cacheTtlMs: dayMs,
|
|
56
|
+
})).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
(0, vitest_1.it)("does not warn when current matches latest", () => {
|
|
59
|
+
(0, vitest_1.expect)((0, versionCheck_1.shouldShowUpdateWarning)({
|
|
60
|
+
current: "0.7.7",
|
|
61
|
+
latest: "0.7.7",
|
|
62
|
+
lastChecked: now,
|
|
63
|
+
nowMs: now,
|
|
64
|
+
cacheTtlMs: dayMs,
|
|
65
|
+
})).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
(0, vitest_1.it)("does not warn when latest is unknown (no fetch yet)", () => {
|
|
68
|
+
(0, vitest_1.expect)((0, versionCheck_1.shouldShowUpdateWarning)({
|
|
69
|
+
current: "0.7.6",
|
|
70
|
+
latest: null,
|
|
71
|
+
lastChecked: 0,
|
|
72
|
+
nowMs: now,
|
|
73
|
+
cacheTtlMs: dayMs,
|
|
74
|
+
})).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
(0, vitest_1.it)("does not warn when current is ahead of latest (dev build)", () => {
|
|
77
|
+
(0, vitest_1.expect)((0, versionCheck_1.shouldShowUpdateWarning)({
|
|
78
|
+
current: "0.8.0",
|
|
79
|
+
latest: "0.7.7",
|
|
80
|
+
lastChecked: now,
|
|
81
|
+
nowMs: now,
|
|
82
|
+
cacheTtlMs: dayMs,
|
|
83
|
+
})).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
});
|
package/dist/cli.js
CHANGED
package/dist/commands/doctor.js
CHANGED
|
@@ -40,6 +40,8 @@ const os = __importStar(require("os"));
|
|
|
40
40
|
const child_process_1 = require("child_process");
|
|
41
41
|
const shared_1 = require("./shared");
|
|
42
42
|
const utils_1 = require("../utils");
|
|
43
|
+
const version_1 = require("../version");
|
|
44
|
+
const upgradeNudge_1 = require("../upgradeNudge");
|
|
43
45
|
const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), ".claude", "settings.json");
|
|
44
46
|
const CLAUDE_INSTRUCTIONS_PATH = path.join(os.homedir(), ".claude", "CLAUDE.md");
|
|
45
47
|
const CODEX_HOOKS_PATH = path.join(os.homedir(), ".codex", "hooks.json");
|
|
@@ -54,9 +56,6 @@ function symbol(status) {
|
|
|
54
56
|
return "!";
|
|
55
57
|
return "✗";
|
|
56
58
|
}
|
|
57
|
-
function claudeCodeInstalled() {
|
|
58
|
-
return fs.existsSync(path.join(os.homedir(), ".claude"));
|
|
59
|
-
}
|
|
60
59
|
function codexInstalled() {
|
|
61
60
|
return fs.existsSync(path.join(os.homedir(), ".codex"));
|
|
62
61
|
}
|
|
@@ -142,9 +141,19 @@ function hasGhCli() {
|
|
|
142
141
|
return false;
|
|
143
142
|
}
|
|
144
143
|
}
|
|
144
|
+
function hasGlabCli() {
|
|
145
|
+
try {
|
|
146
|
+
(0, child_process_1.execSync)("glab --version", { stdio: ["pipe", "pipe", "pipe"] });
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
145
153
|
async function runDoctor(_args) {
|
|
146
154
|
const checks = [];
|
|
147
155
|
console.log("\nCodieDev doctor — checking your setup…\n");
|
|
156
|
+
await (0, upgradeNudge_1.maybePrintUpgradeNudge)();
|
|
148
157
|
// 1. Config file
|
|
149
158
|
const config = (0, utils_1.readConfig)();
|
|
150
159
|
if (!config) {
|
|
@@ -165,7 +174,10 @@ async function runDoctor(_args) {
|
|
|
165
174
|
try {
|
|
166
175
|
const res = await fetch(`${config.backendUrl}/api/cli/validateToken`, {
|
|
167
176
|
method: "POST",
|
|
168
|
-
headers: {
|
|
177
|
+
headers: {
|
|
178
|
+
"Content-Type": "application/json",
|
|
179
|
+
"X-Codiedev-Cli-Version": version_1.CLI_VERSION,
|
|
180
|
+
},
|
|
169
181
|
body: JSON.stringify({ token: config.token }),
|
|
170
182
|
});
|
|
171
183
|
if (res.ok) {
|
|
@@ -217,7 +229,7 @@ async function runDoctor(_args) {
|
|
|
217
229
|
: "0 — link repos in the portal so sessions get captured",
|
|
218
230
|
});
|
|
219
231
|
// 5. Claude Code setup (if present)
|
|
220
|
-
if (claudeCodeInstalled()) {
|
|
232
|
+
if ((0, utils_1.claudeCodeInstalled)()) {
|
|
221
233
|
checks.push({
|
|
222
234
|
name: "Claude Code detected",
|
|
223
235
|
status: "pass",
|
|
@@ -310,13 +322,21 @@ async function runDoctor(_args) {
|
|
|
310
322
|
detail: "not installed on this machine",
|
|
311
323
|
});
|
|
312
324
|
}
|
|
313
|
-
// 8. gh
|
|
325
|
+
// 8. gh / glab CLIs (optional — only needed for reverse-ticket from a
|
|
326
|
+
// specific PR/MR URL; branch mode works without either).
|
|
314
327
|
checks.push({
|
|
315
|
-
name: "GitHub CLI (`gh`) for reverse-ticket
|
|
328
|
+
name: "GitHub CLI (`gh`) for reverse-ticket from a PR url",
|
|
316
329
|
status: hasGhCli() ? "pass" : "warn",
|
|
317
330
|
detail: hasGhCli()
|
|
318
331
|
? "installed"
|
|
319
|
-
: "not installed — only needed
|
|
332
|
+
: "not installed — only needed for `codiedev reverse-ticket <github-pr-url>`",
|
|
333
|
+
});
|
|
334
|
+
checks.push({
|
|
335
|
+
name: "GitLab CLI (`glab`) for reverse-ticket from an MR url",
|
|
336
|
+
status: hasGlabCli() ? "pass" : "warn",
|
|
337
|
+
detail: hasGlabCli()
|
|
338
|
+
? "installed"
|
|
339
|
+
: "not installed — only needed for `codiedev reverse-ticket <gitlab-mr-url>`",
|
|
320
340
|
});
|
|
321
341
|
report(checks);
|
|
322
342
|
const failures = checks.filter((c) => c.status === "fail");
|
|
@@ -3,15 +3,90 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.runReverseTicket = runReverseTicket;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
5
|
const shared_1 = require("./shared");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
const prUrl_1 = require("../prUrl");
|
|
7
|
+
async function fetchPrOrMrBundle(parsed, rawUrl, noTruncate) {
|
|
8
|
+
console.log(`Fetching ${parsed.provider === "gitlab" ? "MR" : "PR"} ${parsed.owner}/${parsed.repo}#${parsed.number}…`);
|
|
9
|
+
if (parsed.provider === "github") {
|
|
10
|
+
let prJson;
|
|
11
|
+
let filesJson;
|
|
12
|
+
let diff;
|
|
13
|
+
try {
|
|
14
|
+
prJson = ghApi(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`);
|
|
15
|
+
filesJson = ghApi(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}/files`);
|
|
16
|
+
try {
|
|
17
|
+
const rawDiff = ghRaw(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`);
|
|
18
|
+
diff = noTruncate ? rawDiff : rawDiff.slice(0, 12_000);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// diff is nice-to-have
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
console.error(`Failed to fetch PR via gh api: ${err.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
repo: `${parsed.owner}/${parsed.repo}`,
|
|
30
|
+
number: parsed.number,
|
|
31
|
+
title: prJson.title ?? "",
|
|
32
|
+
body: prJson.body ?? "",
|
|
33
|
+
author: prJson.user?.login ?? "unknown",
|
|
34
|
+
authorEmail: prJson.user?.email ?? undefined,
|
|
35
|
+
mergedAt: prJson.merged_at
|
|
36
|
+
? Date.parse(prJson.merged_at)
|
|
37
|
+
: prJson.closed_at
|
|
38
|
+
? Date.parse(prJson.closed_at)
|
|
39
|
+
: Date.now(),
|
|
40
|
+
filesChanged: (filesJson ?? []).map((f) => f.filename),
|
|
41
|
+
diff,
|
|
42
|
+
htmlUrl: prJson.html_url ?? rawUrl,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
// GitLab path. `glab api projects/<encoded-path>/...` returns the same
|
|
46
|
+
// MR shape as the REST API. We pull the diff from `merge_requests/<n>/changes`
|
|
47
|
+
// and reassemble a unified diff out of each file's `diff` blob.
|
|
48
|
+
const projectPath = `${parsed.owner}/${parsed.repo}`;
|
|
49
|
+
let mrJson;
|
|
50
|
+
let changesJson;
|
|
51
|
+
try {
|
|
52
|
+
mrJson = glabApi(projectPath, `merge_requests/${parsed.number}`);
|
|
53
|
+
changesJson = glabApi(projectPath, `merge_requests/${parsed.number}/changes`);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
console.error(`Failed to fetch MR via glab api: ${err.message}`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
const changes = (changesJson?.changes ?? []);
|
|
60
|
+
const filesChanged = changes
|
|
61
|
+
.map((c) => c.new_path ?? c.old_path)
|
|
62
|
+
.filter((p) => Boolean(p));
|
|
63
|
+
const rawDiff = changes
|
|
64
|
+
.map((c) => {
|
|
65
|
+
const header = `diff --git a/${c.old_path ?? c.new_path} b/${c.new_path ?? c.old_path}`;
|
|
66
|
+
return [header, c.diff ?? ""].join("\n");
|
|
67
|
+
})
|
|
68
|
+
.join("\n");
|
|
69
|
+
const diff = noTruncate ? rawDiff : rawDiff.slice(0, 12_000);
|
|
70
|
+
return {
|
|
71
|
+
repo: projectPath,
|
|
72
|
+
number: parsed.number,
|
|
73
|
+
title: mrJson.title ?? "",
|
|
74
|
+
body: mrJson.description ?? "",
|
|
75
|
+
author: mrJson.author?.username ?? "unknown",
|
|
76
|
+
authorEmail: undefined,
|
|
77
|
+
mergedAt: mrJson.merged_at
|
|
78
|
+
? Date.parse(mrJson.merged_at)
|
|
79
|
+
: mrJson.closed_at
|
|
80
|
+
? Date.parse(mrJson.closed_at)
|
|
81
|
+
: Date.now(),
|
|
82
|
+
filesChanged,
|
|
83
|
+
diff,
|
|
84
|
+
htmlUrl: mrJson.web_url ?? rawUrl,
|
|
85
|
+
};
|
|
11
86
|
}
|
|
12
|
-
function
|
|
87
|
+
function checkCli(bin) {
|
|
13
88
|
try {
|
|
14
|
-
(0, child_process_1.execSync)(
|
|
89
|
+
(0, child_process_1.execSync)(`${bin} --version`, { stdio: ["pipe", "pipe", "pipe"] });
|
|
15
90
|
return true;
|
|
16
91
|
}
|
|
17
92
|
catch {
|
|
@@ -31,6 +106,17 @@ function ghRaw(path) {
|
|
|
31
106
|
maxBuffer: 50 * 1024 * 1024,
|
|
32
107
|
}).toString("utf8");
|
|
33
108
|
}
|
|
109
|
+
// glab api takes a project path (URL-encoded) or numeric id and a sub-path.
|
|
110
|
+
// For self-hosted GitLab, glab respects `GITLAB_HOST` env if the user has
|
|
111
|
+
// configured glab against their internal instance.
|
|
112
|
+
function glabApi(projectPath, subpath) {
|
|
113
|
+
const encoded = encodeURIComponent(projectPath);
|
|
114
|
+
const out = (0, child_process_1.execSync)(`glab api "projects/${encoded}/${subpath}"`, {
|
|
115
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
116
|
+
maxBuffer: 50 * 1024 * 1024,
|
|
117
|
+
}).toString("utf8");
|
|
118
|
+
return JSON.parse(out);
|
|
119
|
+
}
|
|
34
120
|
function parseArgs(args) {
|
|
35
121
|
let prUrl;
|
|
36
122
|
let base;
|
|
@@ -65,56 +151,31 @@ async function runReverseTicket(args) {
|
|
|
65
151
|
if (!prUrl) {
|
|
66
152
|
return runBranchMode({ explicitBase: base, forcedKey });
|
|
67
153
|
}
|
|
68
|
-
const parsed =
|
|
154
|
+
const parsed = (0, prUrl_1.parsePrOrMrUrl)(prUrl);
|
|
69
155
|
if (!parsed) {
|
|
70
|
-
console.error("Couldn't parse PR URL. Expected
|
|
156
|
+
console.error("Couldn't parse PR/MR URL. Expected formats:");
|
|
157
|
+
console.error(" https://github.com/<owner>/<repo>/pull/<number>");
|
|
158
|
+
console.error(" https://gitlab.com/<group>/<repo>/-/merge_requests/<number>");
|
|
159
|
+
console.error(" (self-hosted GitLab works too — same shape, your host)");
|
|
71
160
|
console.error("");
|
|
72
161
|
console.error("Run with no arguments for branch mode (uses your current local branch).");
|
|
73
162
|
process.exit(1);
|
|
74
163
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
console.error(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const config = (0, shared_1.requireConfig)();
|
|
82
|
-
console.log(`Fetching PR ${parsed.owner}/${parsed.repo}#${parsed.number}…`);
|
|
83
|
-
let prJson;
|
|
84
|
-
let filesJson;
|
|
85
|
-
let diff;
|
|
86
|
-
try {
|
|
87
|
-
prJson = ghApi(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`);
|
|
88
|
-
filesJson = ghApi(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}/files`);
|
|
89
|
-
// Diff is nice-to-have; truncate if huge.
|
|
90
|
-
try {
|
|
91
|
-
const rawDiff = ghRaw(`/repos/${parsed.owner}/${parsed.repo}/pulls/${parsed.number}`);
|
|
92
|
-
diff = noTruncate ? rawDiff : rawDiff.slice(0, 12_000);
|
|
164
|
+
const requiredCli = parsed.provider === "gitlab" ? "glab" : "gh";
|
|
165
|
+
if (!checkCli(requiredCli)) {
|
|
166
|
+
console.error(`This command needs the \`${requiredCli}\` CLI installed and authenticated.`);
|
|
167
|
+
if (requiredCli === "gh") {
|
|
168
|
+
console.error(" brew install gh (macOS)");
|
|
169
|
+
console.error(" gh auth login");
|
|
93
170
|
}
|
|
94
|
-
|
|
95
|
-
|
|
171
|
+
else {
|
|
172
|
+
console.error(" brew install glab (macOS)");
|
|
173
|
+
console.error(" glab auth login");
|
|
96
174
|
}
|
|
97
|
-
}
|
|
98
|
-
catch (err) {
|
|
99
|
-
console.error(`Failed to fetch PR via gh api: ${err.message}`);
|
|
100
175
|
process.exit(1);
|
|
101
176
|
}
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
number: parsed.number,
|
|
105
|
-
title: prJson.title ?? "",
|
|
106
|
-
body: prJson.body ?? "",
|
|
107
|
-
author: prJson.user?.login ?? "unknown",
|
|
108
|
-
authorEmail: prJson.user?.email ?? undefined,
|
|
109
|
-
mergedAt: prJson.merged_at
|
|
110
|
-
? Date.parse(prJson.merged_at)
|
|
111
|
-
: prJson.closed_at
|
|
112
|
-
? Date.parse(prJson.closed_at)
|
|
113
|
-
: Date.now(),
|
|
114
|
-
filesChanged: (filesJson ?? []).map((f) => f.filename),
|
|
115
|
-
diff,
|
|
116
|
-
htmlUrl: prJson.html_url ?? prUrl,
|
|
117
|
-
};
|
|
177
|
+
const config = (0, shared_1.requireConfig)();
|
|
178
|
+
const bundle = await fetchPrOrMrBundle(parsed, prUrl, noTruncate);
|
|
118
179
|
console.log(` title: ${bundle.title}`);
|
|
119
180
|
console.log(` author: ${bundle.author}`);
|
|
120
181
|
console.log(` files: ${bundle.filesChanged.length}`);
|
package/dist/commands/shared.js
CHANGED
|
@@ -40,6 +40,7 @@ exports.stripQuotes = stripQuotes;
|
|
|
40
40
|
const https = __importStar(require("https"));
|
|
41
41
|
const http = __importStar(require("http"));
|
|
42
42
|
const utils_1 = require("../utils");
|
|
43
|
+
const version_1 = require("../version");
|
|
43
44
|
/**
|
|
44
45
|
* Require a connected CodieDev config. Exits the process with a helpful
|
|
45
46
|
* message if the user hasn't run `codiedev connect` yet.
|
|
@@ -75,6 +76,7 @@ function apiRequest(method, path, options) {
|
|
|
75
76
|
method,
|
|
76
77
|
headers: {
|
|
77
78
|
Authorization: `Bearer ${options.config.token}`,
|
|
79
|
+
"X-Codiedev-Cli-Version": version_1.CLI_VERSION,
|
|
78
80
|
...(data
|
|
79
81
|
? {
|
|
80
82
|
"Content-Type": "application/json",
|
package/dist/connect.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function runConnect(): Promise<void>;
|
|
1
|
+
export declare function runConnect(args?: string[]): Promise<void>;
|
package/dist/connect.js
CHANGED
|
@@ -38,6 +38,9 @@ const readline = __importStar(require("readline"));
|
|
|
38
38
|
const https = __importStar(require("https"));
|
|
39
39
|
const http = __importStar(require("http"));
|
|
40
40
|
const utils_1 = require("./utils");
|
|
41
|
+
const connectFlow_1 = require("./connectFlow");
|
|
42
|
+
const version_1 = require("./version");
|
|
43
|
+
const upgradeNudge_1 = require("./upgradeNudge");
|
|
41
44
|
const BACKEND_URL = process.env.CODIEDEV_URL || "https://judicious-falcon-861.convex.site";
|
|
42
45
|
function prompt(rl, question) {
|
|
43
46
|
return new Promise((resolve) => {
|
|
@@ -58,6 +61,7 @@ function postJson(url, body) {
|
|
|
58
61
|
headers: {
|
|
59
62
|
"Content-Type": "application/json",
|
|
60
63
|
"Content-Length": Buffer.byteLength(data),
|
|
64
|
+
"X-Codiedev-Cli-Version": version_1.CLI_VERSION,
|
|
61
65
|
},
|
|
62
66
|
};
|
|
63
67
|
const lib = parsed.protocol === "https:" ? https : http;
|
|
@@ -86,24 +90,39 @@ function postJson(url, body) {
|
|
|
86
90
|
req.end();
|
|
87
91
|
});
|
|
88
92
|
}
|
|
89
|
-
async function runConnect() {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
async function runConnect(args = []) {
|
|
94
|
+
// `--force` (or `-f`) re-prompts for a token even if `~/.codiedev/config.json`
|
|
95
|
+
// already has one. Default behavior reuses the saved token so users can
|
|
96
|
+
// re-run `connect` (e.g., after installing Claude Code) without having to
|
|
97
|
+
// mint a fresh token from the portal — which is what bit Sonifi's first
|
|
98
|
+
// engineer when he interpreted "no visible token" as "the old one is gone".
|
|
99
|
+
const force = args.includes("--force") || args.includes("-f");
|
|
94
100
|
console.log("\nWelcome to CodieDev CLI\n");
|
|
101
|
+
await (0, upgradeNudge_1.maybePrintUpgradeNudge)();
|
|
95
102
|
console.log("This will connect your coding agent to your CodieDev workspace and install");
|
|
96
103
|
console.log("the session capture hook for org-wide session sharing.\n");
|
|
104
|
+
const saved = (0, utils_1.readConfig)();
|
|
105
|
+
const decision = (0, connectFlow_1.resolveConnectToken)({ savedConfig: saved, force });
|
|
97
106
|
let token;
|
|
98
|
-
|
|
99
|
-
token =
|
|
100
|
-
|
|
101
|
-
finally {
|
|
102
|
-
rl.close();
|
|
107
|
+
if (decision.source === "reused") {
|
|
108
|
+
token = decision.token;
|
|
109
|
+
console.log(`Reusing API token from ~/.codiedev/config.json (run with --force to use a new one).\n`);
|
|
103
110
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
else {
|
|
112
|
+
const rl = readline.createInterface({
|
|
113
|
+
input: process.stdin,
|
|
114
|
+
output: process.stdout,
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
token = await prompt(rl, "Enter your CodieDev API token: ");
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
rl.close();
|
|
121
|
+
}
|
|
122
|
+
if (!token) {
|
|
123
|
+
console.error("Error: API token is required.");
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
107
126
|
}
|
|
108
127
|
console.log("\nValidating token...");
|
|
109
128
|
let responseData;
|
|
@@ -225,8 +244,9 @@ async function runConnect() {
|
|
|
225
244
|
}
|
|
226
245
|
}
|
|
227
246
|
if (!hasClaude && !hasCodex && !hasCursor && !hasVSCodeCopilot) {
|
|
228
|
-
console.warn("\nNo Claude Code
|
|
229
|
-
console.warn("
|
|
247
|
+
console.warn("\nNo Claude Code, Codex, Cursor, or VS Code Copilot install detected.");
|
|
248
|
+
console.warn("If you just installed Claude Code, launch it once (so ~/.claude is created),");
|
|
249
|
+
console.warn("then re-run `npx codiedev connect` to wire up the capture hook.");
|
|
230
250
|
}
|
|
231
251
|
console.log(`\nConnected to ${companyName}`);
|
|
232
252
|
console.log(`Tracking ${repos.length} repo${repos.length === 1 ? "" : "s"}`);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CodiedevConfig } from "./utils";
|
|
2
|
+
export type ConnectTokenSource = "reused" | "prompt-required";
|
|
3
|
+
export interface ConnectTokenInput {
|
|
4
|
+
savedConfig: CodiedevConfig | null;
|
|
5
|
+
force: boolean;
|
|
6
|
+
}
|
|
7
|
+
export type ConnectTokenResult = {
|
|
8
|
+
source: "reused";
|
|
9
|
+
token: string;
|
|
10
|
+
} | {
|
|
11
|
+
source: "prompt-required";
|
|
12
|
+
};
|
|
13
|
+
export declare function resolveConnectToken(input: ConnectTokenInput): ConnectTokenResult;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure helper for `npx codiedev connect` — decides where the API token
|
|
3
|
+
// comes from. Splitting this out lets us TDD the policy (re-use vs. re-prompt)
|
|
4
|
+
// without touching readline or fs at all.
|
|
5
|
+
//
|
|
6
|
+
// Motivation: Jackson hit a confusing flow where he had a working token in
|
|
7
|
+
// `~/.codiedev/config.json`, re-ran `connect` (to pick up Claude Code that
|
|
8
|
+
// he'd just installed), and was forced to paste a token again. Since the
|
|
9
|
+
// portal only displays the plaintext on creation, his only option was to
|
|
10
|
+
// generate a new one — leaving him thinking the old one had been lost.
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.resolveConnectToken = resolveConnectToken;
|
|
13
|
+
function resolveConnectToken(input) {
|
|
14
|
+
if (input.force)
|
|
15
|
+
return { source: "prompt-required" };
|
|
16
|
+
if (!input.savedConfig)
|
|
17
|
+
return { source: "prompt-required" };
|
|
18
|
+
if (!input.savedConfig.token)
|
|
19
|
+
return { source: "prompt-required" };
|
|
20
|
+
return { source: "reused", token: input.savedConfig.token };
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ClaudeCodeProbe {
|
|
2
|
+
dirExists: boolean;
|
|
3
|
+
binOnPath: boolean;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Claude Code is "installed" if either ~/.claude exists (the user has
|
|
7
|
+
* launched the app at least once) OR the `claude` binary is on PATH (the
|
|
8
|
+
* user installed via npm/brew but hasn't launched yet). Either signal is
|
|
9
|
+
* sufficient — we want to wire up the hook the moment the user has
|
|
10
|
+
* obviously committed to the tool.
|
|
11
|
+
*/
|
|
12
|
+
export declare function detectClaudeCode(probe: ClaudeCodeProbe): boolean;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure detection helpers — runtime probes (fs / PATH lookup) live in utils.ts
|
|
3
|
+
// and feed these. Splitting them out makes the truth-table testable without
|
|
4
|
+
// mocking fs or process.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.detectClaudeCode = detectClaudeCode;
|
|
7
|
+
/**
|
|
8
|
+
* Claude Code is "installed" if either ~/.claude exists (the user has
|
|
9
|
+
* launched the app at least once) OR the `claude` binary is on PATH (the
|
|
10
|
+
* user installed via npm/brew but hasn't launched yet). Either signal is
|
|
11
|
+
* sufficient — we want to wire up the hook the moment the user has
|
|
12
|
+
* obviously committed to the tool.
|
|
13
|
+
*/
|
|
14
|
+
function detectClaudeCode(probe) {
|
|
15
|
+
return probe.dirExists || probe.binOnPath;
|
|
16
|
+
}
|
package/dist/hook.js
CHANGED
|
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
|
|
|
38
38
|
const https = __importStar(require("https"));
|
|
39
39
|
const http = __importStar(require("http"));
|
|
40
40
|
const utils_1 = require("./utils");
|
|
41
|
+
const version_1 = require("./version");
|
|
41
42
|
function postJson(url, body, bearerToken) {
|
|
42
43
|
return new Promise((resolve, reject) => {
|
|
43
44
|
const parsed = new URL(url);
|
|
@@ -51,6 +52,7 @@ function postJson(url, body, bearerToken) {
|
|
|
51
52
|
"Content-Type": "application/json",
|
|
52
53
|
"Content-Length": Buffer.byteLength(data),
|
|
53
54
|
Authorization: `Bearer ${bearerToken}`,
|
|
55
|
+
"X-Codiedev-Cli-Version": version_1.CLI_VERSION,
|
|
54
56
|
},
|
|
55
57
|
};
|
|
56
58
|
const lib = parsed.protocol === "https:" ? https : http;
|
|
@@ -133,13 +135,11 @@ async function main() {
|
|
|
133
135
|
if (!remoteUrl) {
|
|
134
136
|
process.exit(0);
|
|
135
137
|
}
|
|
138
|
+
// Best-effort local match — backend re-resolves from `remoteUrl` against
|
|
139
|
+
// the company's current repo list, so a stale or empty local cache
|
|
140
|
+
// doesn't drop the session. (Pre-0.7.7 we exited 0 here, which silently
|
|
141
|
+
// dropped sessions for repos added after the user's last `connect`.)
|
|
136
142
|
const matchedRepo = (0, utils_1.matchRepo)(remoteUrl, config.repos);
|
|
137
|
-
if (!matchedRepo) {
|
|
138
|
-
process.exit(0);
|
|
139
|
-
}
|
|
140
|
-
// Cursor's transcript format is undocumented — skip stats parsing for now
|
|
141
|
-
// and let the backend handle it once we have a real session to inspect.
|
|
142
|
-
// The transcript itself still uploads.
|
|
143
143
|
const stats = mode === "codex"
|
|
144
144
|
? (0, utils_1.parseCodexStats)(transcriptContent)
|
|
145
145
|
: mode === "cursor"
|
|
@@ -149,7 +149,7 @@ async function main() {
|
|
|
149
149
|
const payload = {
|
|
150
150
|
sessionId: session_id,
|
|
151
151
|
repoUrl: remoteUrl,
|
|
152
|
-
repoId: matchedRepo
|
|
152
|
+
repoId: matchedRepo?.repoId,
|
|
153
153
|
companyId: config.companyId,
|
|
154
154
|
transcript: transcriptBase64,
|
|
155
155
|
messageCount: stats.messageCount,
|
package/dist/prUrl.d.ts
ADDED
package/dist/prUrl.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// PR/MR URL parser. GitHub uses `/pull/<n>`, GitLab uses `/-/merge_requests/<n>`.
|
|
3
|
+
// Self-hosted GitLab keeps the same path shape on a different host (Sonifi
|
|
4
|
+
// runs git.sonifi.com behind a Cloudflare tunnel, for example). Nested groups
|
|
5
|
+
// land in `owner` as a slash-joined path so we can rebuild the full project
|
|
6
|
+
// path when calling the GitLab API.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.parsePrOrMrUrl = parsePrOrMrUrl;
|
|
9
|
+
function parsePrOrMrUrl(url) {
|
|
10
|
+
if (!url)
|
|
11
|
+
return null;
|
|
12
|
+
const gh = url.match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/]+)\/pull\/(\d+)(?:[/?#]|$)/);
|
|
13
|
+
if (gh && /github/i.test(gh[1])) {
|
|
14
|
+
return {
|
|
15
|
+
provider: "github",
|
|
16
|
+
host: gh[1],
|
|
17
|
+
owner: gh[2],
|
|
18
|
+
repo: gh[3],
|
|
19
|
+
number: Number(gh[4]),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
const gl = url.match(/^https?:\/\/([^/]+)\/(.+)\/([^/]+)\/-\/merge_requests\/(\d+)(?:[/?#]|$)/);
|
|
23
|
+
if (gl) {
|
|
24
|
+
return {
|
|
25
|
+
provider: "gitlab",
|
|
26
|
+
host: gl[1],
|
|
27
|
+
owner: gl[2],
|
|
28
|
+
repo: gl[3],
|
|
29
|
+
number: Number(gl[4]),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type ResolveSource = "provided" | "resolved" | "unmatched";
|
|
2
|
+
export interface RepoLite {
|
|
3
|
+
_id: string;
|
|
4
|
+
owner: string;
|
|
5
|
+
name: string;
|
|
6
|
+
gitProvider?: "github" | "gitlab";
|
|
7
|
+
}
|
|
8
|
+
export interface ResolveInput {
|
|
9
|
+
remoteUrl: string;
|
|
10
|
+
providedRepoId: string | null;
|
|
11
|
+
companyRepos: ReadonlyArray<RepoLite>;
|
|
12
|
+
}
|
|
13
|
+
export interface ResolveResult {
|
|
14
|
+
repoId: string | null;
|
|
15
|
+
source: ResolveSource;
|
|
16
|
+
}
|
|
17
|
+
export declare function resolveRepoForCapture(input: ResolveInput): ResolveResult;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure resolver — given a captured remoteUrl, an optional client-cached
|
|
3
|
+
// repoId, and the company's current repo list, decide which repoId (if any)
|
|
4
|
+
// the session belongs to. Used both inline by the CLI hook and (mirrored
|
|
5
|
+
// in convex/repoResolver.ts) by the /api/captureSession endpoint so that
|
|
6
|
+
// repos added after the user's last `connect` still get matched.
|
|
7
|
+
//
|
|
8
|
+
// "unmatched" is intentional — the backend will accept the session anyway
|
|
9
|
+
// and surface it for triage rather than silently dropping it.
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.resolveRepoForCapture = resolveRepoForCapture;
|
|
12
|
+
function parseRemoteUrl(url) {
|
|
13
|
+
let s = url.trim();
|
|
14
|
+
if (s.endsWith(".git"))
|
|
15
|
+
s = s.slice(0, -4);
|
|
16
|
+
const ssh = s.match(/^git@([^:]+):(.+)$/);
|
|
17
|
+
if (ssh) {
|
|
18
|
+
return { host: ssh[1].toLowerCase(), ownerName: ssh[2].toLowerCase() };
|
|
19
|
+
}
|
|
20
|
+
const https = s.match(/^https?:\/\/([^/]+)\/(.+)$/);
|
|
21
|
+
if (https) {
|
|
22
|
+
return { host: https[1].toLowerCase(), ownerName: https[2].toLowerCase() };
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
function resolveRepoForCapture(input) {
|
|
27
|
+
const { remoteUrl, providedRepoId, companyRepos } = input;
|
|
28
|
+
if (providedRepoId) {
|
|
29
|
+
const hit = companyRepos.find((r) => r._id === providedRepoId);
|
|
30
|
+
if (hit)
|
|
31
|
+
return { repoId: hit._id, source: "provided" };
|
|
32
|
+
}
|
|
33
|
+
const parsed = parseRemoteUrl(remoteUrl);
|
|
34
|
+
if (!parsed) {
|
|
35
|
+
return { repoId: null, source: "unmatched" };
|
|
36
|
+
}
|
|
37
|
+
const match = companyRepos.find((r) => `${r.owner}/${r.name}`.toLowerCase() === parsed.ownerName);
|
|
38
|
+
if (match)
|
|
39
|
+
return { repoId: match._id, source: "resolved" };
|
|
40
|
+
return { repoId: null, source: "unmatched" };
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-silent — never throws, never blocks on network. If the registry is
|
|
3
|
+
* unreachable or the cache is corrupt, the warning just doesn't print. The
|
|
4
|
+
* worst outcome is "user keeps running an old version" — same as before
|
|
5
|
+
* this feature existed.
|
|
6
|
+
*/
|
|
7
|
+
export declare function maybePrintUpgradeNudge(): Promise<void>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Client-side "you're outdated" warning. Prints at the top of connect/doctor
|
|
3
|
+
// when there's a newer `codiedev` published on npm. Pure helpers tested in
|
|
4
|
+
// __tests__/versionCheck.test.ts; this module does the IO (registry fetch +
|
|
5
|
+
// disk cache) and is exercised by manual smoke.
|
|
6
|
+
//
|
|
7
|
+
// Cache lives at ~/.codiedev/.update-cache.json. 24h TTL — long enough that
|
|
8
|
+
// the registry isn't hit on every command, short enough that newly-published
|
|
9
|
+
// versions surface the day after release.
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.maybePrintUpgradeNudge = maybePrintUpgradeNudge;
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const os = __importStar(require("os"));
|
|
48
|
+
const version_1 = require("./version");
|
|
49
|
+
const versionCheck_1 = require("./versionCheck");
|
|
50
|
+
const CACHE_PATH = path.join(os.homedir(), ".codiedev", ".update-cache.json");
|
|
51
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
52
|
+
const REGISTRY_URL = "https://registry.npmjs.org/codiedev/latest";
|
|
53
|
+
const FETCH_TIMEOUT_MS = 1500;
|
|
54
|
+
function readCache() {
|
|
55
|
+
try {
|
|
56
|
+
const raw = fs.readFileSync(CACHE_PATH, "utf8");
|
|
57
|
+
const parsed = JSON.parse(raw);
|
|
58
|
+
if (typeof parsed.fetchedAt !== "number")
|
|
59
|
+
return null;
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function writeCache(entry) {
|
|
67
|
+
try {
|
|
68
|
+
const dir = path.dirname(CACHE_PATH);
|
|
69
|
+
if (!fs.existsSync(dir))
|
|
70
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify(entry), "utf8");
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// ignore — cache is best-effort
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function fetchLatestFromNpm() {
|
|
78
|
+
try {
|
|
79
|
+
const controller = new AbortController();
|
|
80
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
81
|
+
const res = await fetch(REGISTRY_URL, {
|
|
82
|
+
headers: { Accept: "application/json" },
|
|
83
|
+
signal: controller.signal,
|
|
84
|
+
});
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
if (!res.ok)
|
|
87
|
+
return null;
|
|
88
|
+
const json = (await res.json());
|
|
89
|
+
return json.version ?? null;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Fail-silent — never throws, never blocks on network. If the registry is
|
|
97
|
+
* unreachable or the cache is corrupt, the warning just doesn't print. The
|
|
98
|
+
* worst outcome is "user keeps running an old version" — same as before
|
|
99
|
+
* this feature existed.
|
|
100
|
+
*/
|
|
101
|
+
async function maybePrintUpgradeNudge() {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
const cache = readCache();
|
|
104
|
+
let latest = cache?.latest ?? null;
|
|
105
|
+
const cacheStale = !cache || now - cache.fetchedAt > CACHE_TTL_MS;
|
|
106
|
+
if (cacheStale) {
|
|
107
|
+
latest = await fetchLatestFromNpm();
|
|
108
|
+
writeCache({ latest, fetchedAt: now });
|
|
109
|
+
}
|
|
110
|
+
const warn = (0, versionCheck_1.shouldShowUpdateWarning)({
|
|
111
|
+
current: version_1.CLI_VERSION,
|
|
112
|
+
latest,
|
|
113
|
+
lastChecked: cache?.fetchedAt ?? 0,
|
|
114
|
+
nowMs: now,
|
|
115
|
+
cacheTtlMs: CACHE_TTL_MS,
|
|
116
|
+
});
|
|
117
|
+
if (warn && latest) {
|
|
118
|
+
console.log("");
|
|
119
|
+
console.log(`[33m⚠️ codiedev ${latest} available (you're on ${version_1.CLI_VERSION})[0m`);
|
|
120
|
+
console.log(` Upgrade: [36mnpm i -g codiedev@latest[0m`);
|
|
121
|
+
console.log("");
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/utils.js
CHANGED
|
@@ -60,6 +60,7 @@ const fs = __importStar(require("fs"));
|
|
|
60
60
|
const path = __importStar(require("path"));
|
|
61
61
|
const os = __importStar(require("os"));
|
|
62
62
|
const child_process_1 = require("child_process");
|
|
63
|
+
const detection_1 = require("./detection");
|
|
63
64
|
const CONFIG_DIR = path.join(os.homedir(), ".codiedev");
|
|
64
65
|
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
65
66
|
function readConfig() {
|
|
@@ -190,8 +191,20 @@ function isCodiedevHookCommand(cmd) {
|
|
|
190
191
|
return false;
|
|
191
192
|
return cmd.includes("codiedev-hook") || /codiedev[\\/]dist[\\/]hook/.test(cmd);
|
|
192
193
|
}
|
|
194
|
+
function claudeBinOnPath() {
|
|
195
|
+
try {
|
|
196
|
+
(0, child_process_1.execSync)("command -v claude", { stdio: ["pipe", "pipe", "pipe"] });
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
193
203
|
function claudeCodeInstalled() {
|
|
194
|
-
return
|
|
204
|
+
return (0, detection_1.detectClaudeCode)({
|
|
205
|
+
dirExists: fs.existsSync(CLAUDE_DIR),
|
|
206
|
+
binOnPath: claudeBinOnPath(),
|
|
207
|
+
});
|
|
195
208
|
}
|
|
196
209
|
function codexInstalled() {
|
|
197
210
|
return fs.existsSync(CODEX_DIR);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const CLI_VERSION: string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Resolves the running CLI version. Read once at startup so subsequent
|
|
3
|
+
// auth calls don't pay an fs.readFile per request.
|
|
4
|
+
//
|
|
5
|
+
// We could `import packageJson from "../package.json"` but that requires
|
|
6
|
+
// `resolveJsonModule` and emits the JSON into dist. Reading at runtime
|
|
7
|
+
// keeps the bundle smaller and works regardless of tsconfig settings.
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.CLI_VERSION = void 0;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
function resolveCliVersion() {
|
|
46
|
+
// dist/version.js sits alongside dist/cli.js. The package.json is one
|
|
47
|
+
// directory up from dist/, i.e. `../package.json` relative to this file.
|
|
48
|
+
// In TS source we're at src/version.ts so the path is `../package.json`.
|
|
49
|
+
const candidates = [
|
|
50
|
+
path.join(__dirname, "..", "package.json"),
|
|
51
|
+
path.join(__dirname, "..", "..", "package.json"),
|
|
52
|
+
];
|
|
53
|
+
for (const p of candidates) {
|
|
54
|
+
try {
|
|
55
|
+
const raw = fs.readFileSync(p, "utf8");
|
|
56
|
+
const json = JSON.parse(raw);
|
|
57
|
+
if (json.name === "codiedev" && typeof json.version === "string") {
|
|
58
|
+
return json.version;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// try next candidate
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return "0.0.0";
|
|
66
|
+
}
|
|
67
|
+
exports.CLI_VERSION = resolveCliVersion();
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface Semver {
|
|
2
|
+
major: number;
|
|
3
|
+
minor: number;
|
|
4
|
+
patch: number;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseSemver(input: string): Semver | null;
|
|
7
|
+
/**
|
|
8
|
+
* Returns -1 if a < b, 0 if equal, 1 if a > b.
|
|
9
|
+
*
|
|
10
|
+
* Malformed input on either side returns 0 — we never want to nag a user
|
|
11
|
+
* because of garbage version strings (corrupt cache, registry returned
|
|
12
|
+
* something weird, prerelease format we don't recognize).
|
|
13
|
+
*/
|
|
14
|
+
export declare function compareSemver(a: string, b: string): -1 | 0 | 1;
|
|
15
|
+
export interface UpdateWarningInput {
|
|
16
|
+
current: string;
|
|
17
|
+
latest: string | null;
|
|
18
|
+
lastChecked: number;
|
|
19
|
+
nowMs: number;
|
|
20
|
+
cacheTtlMs: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Warning policy: only show when we have a known-newer `latest` from a
|
|
24
|
+
* recent-enough cache. Stale-cache freshness is enforced by the *caller*
|
|
25
|
+
* (which decides whether to refetch); this helper takes the cache state
|
|
26
|
+
* as-given and answers a single question — should the user see a nag?
|
|
27
|
+
*/
|
|
28
|
+
export declare function shouldShowUpdateWarning(input: UpdateWarningInput): boolean;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Pure version-comparison helpers for the "is your CLI outdated" warning.
|
|
3
|
+
// The IO (fetching npm registry, reading/writing the cache file) lives in
|
|
4
|
+
// connect.ts/doctor.ts and is exercised by manual smoke. Keeping the policy
|
|
5
|
+
// pure means the truth table is unit-testable without mocking fetch or fs.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.parseSemver = parseSemver;
|
|
8
|
+
exports.compareSemver = compareSemver;
|
|
9
|
+
exports.shouldShowUpdateWarning = shouldShowUpdateWarning;
|
|
10
|
+
function parseSemver(input) {
|
|
11
|
+
if (!input)
|
|
12
|
+
return null;
|
|
13
|
+
const stripped = input.replace(/^v/i, "");
|
|
14
|
+
const m = stripped.match(/^(\d+)\.(\d+)\.(\d+)/);
|
|
15
|
+
if (!m)
|
|
16
|
+
return null;
|
|
17
|
+
return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Returns -1 if a < b, 0 if equal, 1 if a > b.
|
|
21
|
+
*
|
|
22
|
+
* Malformed input on either side returns 0 — we never want to nag a user
|
|
23
|
+
* because of garbage version strings (corrupt cache, registry returned
|
|
24
|
+
* something weird, prerelease format we don't recognize).
|
|
25
|
+
*/
|
|
26
|
+
function compareSemver(a, b) {
|
|
27
|
+
const pa = parseSemver(a);
|
|
28
|
+
const pb = parseSemver(b);
|
|
29
|
+
if (!pa || !pb)
|
|
30
|
+
return 0;
|
|
31
|
+
if (pa.major !== pb.major)
|
|
32
|
+
return pa.major < pb.major ? -1 : 1;
|
|
33
|
+
if (pa.minor !== pb.minor)
|
|
34
|
+
return pa.minor < pb.minor ? -1 : 1;
|
|
35
|
+
if (pa.patch !== pb.patch)
|
|
36
|
+
return pa.patch < pb.patch ? -1 : 1;
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Warning policy: only show when we have a known-newer `latest` from a
|
|
41
|
+
* recent-enough cache. Stale-cache freshness is enforced by the *caller*
|
|
42
|
+
* (which decides whether to refetch); this helper takes the cache state
|
|
43
|
+
* as-given and answers a single question — should the user see a nag?
|
|
44
|
+
*/
|
|
45
|
+
function shouldShowUpdateWarning(input) {
|
|
46
|
+
if (!input.latest)
|
|
47
|
+
return false;
|
|
48
|
+
return compareSemver(input.current, input.latest) === -1;
|
|
49
|
+
}
|
package/package.json
CHANGED