kodingo-cli 1.3.3 → 1.3.5
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__/inference.test.js +78 -0
- package/dist/__tests__/repo-identity.test.js +120 -0
- package/dist/adapters/ai/inference.js +5 -2
- package/dist/commands/init.js +11 -10
- package/dist/commands/scan-git.js +5 -20
- package/dist/commands/scan-repo.js +2 -1
- package/dist/commands/security-scan.js +2 -1
- package/dist/commands/sync-dependencies.js +2 -5
- package/dist/ports/cloud-event-port.js +4 -0
- package/dist/ports/db-event-port.js +4 -0
- package/dist/utils/persistence-config.js +27 -0
- package/dist/utils/repo-identity.js +83 -0
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Real tests for adapters/ai/inference.ts's auth fix — build brief "Item 8
|
|
4
|
+
* — the temporal lobe", 2026-10-01.
|
|
5
|
+
*
|
|
6
|
+
* FINDING this closes: inferDecisionSummary (the git-commit-triggered
|
|
7
|
+
* capture surface, via cloud-event-port.ts/db-event-port.ts) was calling
|
|
8
|
+
* POST /infer with no auth header at all — every real call would 401 and
|
|
9
|
+
* silently fall back to the non-LLM template text, meaning this surface
|
|
10
|
+
* never actually reached the shared, semantically-reasoning-enriched
|
|
11
|
+
* prompt built into routes/infer.ts (kodingo-api). These tests prove the
|
|
12
|
+
* real fix: a real token now reaches the request as X-Kodingo-Token, and
|
|
13
|
+
* the existing, honest fallback behavior is completely unchanged when no
|
|
14
|
+
* token is available.
|
|
15
|
+
*/
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
const node_test_1 = require("node:test");
|
|
21
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
22
|
+
const inference_1 = require("../adapters/ai/inference");
|
|
23
|
+
(0, node_test_1.describe)("inferDecisionSummary — real auth fix", () => {
|
|
24
|
+
const originalFetch = globalThis.fetch;
|
|
25
|
+
(0, node_test_1.afterEach)(() => {
|
|
26
|
+
globalThis.fetch = originalFetch;
|
|
27
|
+
});
|
|
28
|
+
(0, node_test_1.it)("sends the real X-Kodingo-Token header when a real token is available", async () => {
|
|
29
|
+
let capturedHeaders;
|
|
30
|
+
let capturedUrl;
|
|
31
|
+
globalThis.fetch = (async (url, init) => {
|
|
32
|
+
capturedUrl = String(url);
|
|
33
|
+
capturedHeaders = init.headers;
|
|
34
|
+
return new Response(JSON.stringify({ title: "t", content: "A real, LLM-authored decision summary.", type: "decision", tags: [] }), { status: 200 });
|
|
35
|
+
});
|
|
36
|
+
const result = await (0, inference_1.inferDecisionSummary)({
|
|
37
|
+
diff: "commit abc1234\n+ real change",
|
|
38
|
+
changedFiles: ["src/real.ts"],
|
|
39
|
+
symbol: "realFunction",
|
|
40
|
+
commitMessage: "real commit",
|
|
41
|
+
repoName: "real-repo",
|
|
42
|
+
token: "real-project-token",
|
|
43
|
+
apiUrl: "https://example.invalid",
|
|
44
|
+
});
|
|
45
|
+
strict_1.default.equal(result, "A real, LLM-authored decision summary.");
|
|
46
|
+
strict_1.default.equal(capturedUrl, "https://example.invalid/infer");
|
|
47
|
+
strict_1.default.equal(capturedHeaders?.["X-Kodingo-Token"], "real-project-token");
|
|
48
|
+
});
|
|
49
|
+
(0, node_test_1.it)("falls back to the honest template — never calls the network at all — when no real token is available (the pre-existing, unchanged behavior)", async () => {
|
|
50
|
+
let fetchCalled = false;
|
|
51
|
+
globalThis.fetch = (async () => {
|
|
52
|
+
fetchCalled = true;
|
|
53
|
+
throw new Error("should never be called without a real token");
|
|
54
|
+
});
|
|
55
|
+
const result = await (0, inference_1.inferDecisionSummary)({
|
|
56
|
+
diff: "commit abc1234ff\n+ real change",
|
|
57
|
+
changedFiles: ["src/a.ts", "src/b.ts"],
|
|
58
|
+
symbol: "realFunction",
|
|
59
|
+
commitMessage: "a real commit message",
|
|
60
|
+
repoName: "real-repo",
|
|
61
|
+
});
|
|
62
|
+
strict_1.default.equal(fetchCalled, false);
|
|
63
|
+
strict_1.default.match(result, /Changes to realFunction across 2 file\(s\) in commit abc1234f/);
|
|
64
|
+
strict_1.default.match(result, /a real commit message/);
|
|
65
|
+
});
|
|
66
|
+
(0, node_test_1.it)("falls back honestly when the real request itself fails (e.g. a genuine 401)", async () => {
|
|
67
|
+
globalThis.fetch = (async () => new Response("", { status: 401 }));
|
|
68
|
+
const result = await (0, inference_1.inferDecisionSummary)({
|
|
69
|
+
diff: "commit def5678\n+ change",
|
|
70
|
+
changedFiles: ["src/x.ts"],
|
|
71
|
+
symbol: "x",
|
|
72
|
+
commitMessage: "msg",
|
|
73
|
+
repoName: "real-repo",
|
|
74
|
+
token: "a-token-the-server-happens-to-reject",
|
|
75
|
+
});
|
|
76
|
+
strict_1.default.match(result, /Changes to x across 1 file\(s\)/);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Real tests for repo-identity.ts — build brief "Fix repo-label
|
|
4
|
+
* contamination at the source, platform-wide" (2026-09-30).
|
|
5
|
+
*
|
|
6
|
+
* Uses real temporary git repos (real `git remote add`, real .git dirs)
|
|
7
|
+
* rather than mocking simple-git, matching this repo's own testing style
|
|
8
|
+
* (see vendoredCodeDetector.test.ts). Only the network call to the API is
|
|
9
|
+
* mocked, since there's no real server to hit in a unit test — the
|
|
10
|
+
* server-side resolve/register logic itself is covered in kodingo-api's
|
|
11
|
+
* repoIdentityResolve.test.ts.
|
|
12
|
+
*/
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
const node_test_1 = require("node:test");
|
|
18
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
19
|
+
const node_child_process_1 = require("node:child_process");
|
|
20
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
21
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
22
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
23
|
+
const repo_identity_1 = require("../utils/repo-identity");
|
|
24
|
+
function makeTempDir() {
|
|
25
|
+
return node_fs_1.default.mkdtempSync(node_path_1.default.join(node_os_1.default.tmpdir(), "kodingo-repo-identity-"));
|
|
26
|
+
}
|
|
27
|
+
(0, node_test_1.describe)("parseRepoNameFromRemote", () => {
|
|
28
|
+
for (const [url, expected] of [
|
|
29
|
+
["git@github.com:abokidev/kodingo-cli.git", "kodingo-cli"],
|
|
30
|
+
["https://github.com/abokidev/kodingo-cli.git", "kodingo-cli"],
|
|
31
|
+
["https://github.com/abokidev/kodingo-cli", "kodingo-cli"],
|
|
32
|
+
["ssh://git@github.com/abokidev/veriport.git", "veriport"],
|
|
33
|
+
["https://github.com/abokidev/NDDC-BoardRoom.git/", "NDDC-BoardRoom"],
|
|
34
|
+
]) {
|
|
35
|
+
(0, node_test_1.it)(`extracts "${expected}" from ${url}`, () => {
|
|
36
|
+
strict_1.default.equal((0, repo_identity_1.parseRepoNameFromRemote)(url), expected);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
(0, node_test_1.describe)("getRemoteUrl — against real temporary git repos", () => {
|
|
41
|
+
let dir;
|
|
42
|
+
(0, node_test_1.beforeEach)(() => {
|
|
43
|
+
dir = makeTempDir();
|
|
44
|
+
(0, node_child_process_1.execSync)("git init -q", { cwd: dir });
|
|
45
|
+
});
|
|
46
|
+
(0, node_test_1.afterEach)(() => {
|
|
47
|
+
node_fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
48
|
+
});
|
|
49
|
+
(0, node_test_1.it)("returns the real origin fetch URL when one is configured", async () => {
|
|
50
|
+
(0, node_child_process_1.execSync)("git remote add origin git@github.com:abokidev/achieva.git", { cwd: dir });
|
|
51
|
+
const url = await (0, repo_identity_1.getRemoteUrl)(dir);
|
|
52
|
+
strict_1.default.equal(url, "git@github.com:abokidev/achieva.git");
|
|
53
|
+
});
|
|
54
|
+
(0, node_test_1.it)("returns null when the repo has no remote at all", async () => {
|
|
55
|
+
const url = await (0, repo_identity_1.getRemoteUrl)(dir);
|
|
56
|
+
strict_1.default.equal(url, null);
|
|
57
|
+
});
|
|
58
|
+
(0, node_test_1.it)("returns null for a path that isn't a git repo", async () => {
|
|
59
|
+
const bare = makeTempDir();
|
|
60
|
+
try {
|
|
61
|
+
const url = await (0, repo_identity_1.getRemoteUrl)(bare);
|
|
62
|
+
strict_1.default.equal(url, null);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
node_fs_1.default.rmSync(bare, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
(0, node_test_1.describe)("deriveCandidateRepoName", () => {
|
|
70
|
+
let dir;
|
|
71
|
+
(0, node_test_1.beforeEach)(() => {
|
|
72
|
+
dir = makeTempDir();
|
|
73
|
+
(0, node_child_process_1.execSync)("git init -q", { cwd: dir });
|
|
74
|
+
});
|
|
75
|
+
(0, node_test_1.afterEach)(() => {
|
|
76
|
+
node_fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
77
|
+
});
|
|
78
|
+
(0, node_test_1.it)("prefers the git-remote-derived name over the local folder name — this is the whole fix", async () => {
|
|
79
|
+
(0, node_child_process_1.execSync)("git remote add origin https://github.com/abokidev/veriport.git", { cwd: dir });
|
|
80
|
+
// The local folder name is deliberately unrelated to the real repo name —
|
|
81
|
+
// this is exactly the VeriPort/"mac" shape of the original bug.
|
|
82
|
+
const { candidateName, hasRemote } = await (0, repo_identity_1.deriveCandidateRepoName)(dir);
|
|
83
|
+
strict_1.default.equal(candidateName, "veriport");
|
|
84
|
+
strict_1.default.equal(hasRemote, true);
|
|
85
|
+
strict_1.default.notEqual(candidateName, node_path_1.default.basename(dir));
|
|
86
|
+
});
|
|
87
|
+
(0, node_test_1.it)("falls back to the local folder name only when there's truly no remote", async () => {
|
|
88
|
+
const { candidateName, hasRemote } = await (0, repo_identity_1.deriveCandidateRepoName)(dir);
|
|
89
|
+
strict_1.default.equal(candidateName, node_path_1.default.basename(dir));
|
|
90
|
+
strict_1.default.equal(hasRemote, false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
(0, node_test_1.describe)("resolveCanonicalRepoName", () => {
|
|
94
|
+
let dir;
|
|
95
|
+
const originalFetch = globalThis.fetch;
|
|
96
|
+
(0, node_test_1.beforeEach)(() => {
|
|
97
|
+
dir = makeTempDir();
|
|
98
|
+
(0, node_child_process_1.execSync)("git init -q", { cwd: dir });
|
|
99
|
+
(0, node_child_process_1.execSync)("git remote add origin https://github.com/abokidev/fot.git", { cwd: dir });
|
|
100
|
+
});
|
|
101
|
+
(0, node_test_1.afterEach)(() => {
|
|
102
|
+
node_fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
103
|
+
globalThis.fetch = originalFetch;
|
|
104
|
+
});
|
|
105
|
+
(0, node_test_1.it)("returns the server's resolved canonical name", async () => {
|
|
106
|
+
globalThis.fetch = (async () => new Response(JSON.stringify({ repoName: "fot", registered: false, matched: "single-registered" }), { status: 200 }));
|
|
107
|
+
const result = await (0, repo_identity_1.resolveCanonicalRepoName)({ repoRoot: dir, apiUrl: "https://example.invalid", token: "t" });
|
|
108
|
+
strict_1.default.equal(result, "fot");
|
|
109
|
+
});
|
|
110
|
+
(0, node_test_1.it)("fails open to the local candidate when the server errors", async () => {
|
|
111
|
+
globalThis.fetch = (async () => new Response("", { status: 500 }));
|
|
112
|
+
const result = await (0, repo_identity_1.resolveCanonicalRepoName)({ repoRoot: dir, apiUrl: "https://example.invalid", token: "t" });
|
|
113
|
+
strict_1.default.equal(result, "fot"); // git-remote-derived candidate, still better than the old folder-name behaviour
|
|
114
|
+
});
|
|
115
|
+
(0, node_test_1.it)("fails open to the local candidate when the server is unreachable", async () => {
|
|
116
|
+
globalThis.fetch = (async () => { throw new Error("network down"); });
|
|
117
|
+
const result = await (0, repo_identity_1.resolveCanonicalRepoName)({ repoRoot: dir, apiUrl: "https://example.invalid", token: "t" });
|
|
118
|
+
strict_1.default.equal(result, "fot");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -6,9 +6,12 @@ const KODINGO_API_URL = process.env.KODINGO_API_URL ?? "https://kodingo-api.onre
|
|
|
6
6
|
async function inferDecisionSummary(input) {
|
|
7
7
|
try {
|
|
8
8
|
const code = input.diff.slice(0, 2000);
|
|
9
|
-
|
|
9
|
+
if (!input.token)
|
|
10
|
+
return buildFallback(input);
|
|
11
|
+
const apiUrl = input.apiUrl ?? KODINGO_API_URL;
|
|
12
|
+
const res = await fetch(`${apiUrl}/infer`, {
|
|
10
13
|
method: "POST",
|
|
11
|
-
headers: { "Content-Type": "application/json" },
|
|
14
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": input.token },
|
|
12
15
|
body: JSON.stringify({ symbol: input.symbol, code }),
|
|
13
16
|
});
|
|
14
17
|
if (!res.ok) {
|
package/dist/commands/init.js
CHANGED
|
@@ -49,6 +49,7 @@ const path = __importStar(require("path"));
|
|
|
49
49
|
const os = __importStar(require("os"));
|
|
50
50
|
// open is imported dynamically to avoid ESM/CJS compatibility issues on Windows
|
|
51
51
|
const persistence_config_1 = require("../utils/persistence-config");
|
|
52
|
+
const repo_identity_1 = require("../utils/repo-identity");
|
|
52
53
|
const API_BASE = "https://kodingo-api.onrender.com";
|
|
53
54
|
const APP_URL = "https://kodingo.xyz";
|
|
54
55
|
const CONFIG_DIR = path.join(os.homedir(), ".kodingo");
|
|
@@ -271,17 +272,17 @@ function registerInitCommand(program) {
|
|
|
271
272
|
(0, persistence_config_1.writeConfig)({ ...existingConfig, mode: "cloud", apiUrl, machineId });
|
|
272
273
|
(0, persistence_config_1.writeWorkspaceConfig)(repoRoot, { mode: "cloud", apiUrl, token: selectedProject.token });
|
|
273
274
|
ensureGitignoreEntry(repoRoot, ".kortex/");
|
|
274
|
-
// Register this repo with the project on the API
|
|
275
|
-
// This is the authoritative binding that prevents cross-project
|
|
275
|
+
// Register this repo with the project on the API.
|
|
276
|
+
// This is the authoritative binding that prevents cross-project
|
|
277
|
+
// contamination — resolved from the git remote (stable across
|
|
278
|
+
// machines and folder names), not the local folder name itself,
|
|
279
|
+
// so the registration this creates is trustworthy for every future
|
|
280
|
+
// scan to resolve against.
|
|
276
281
|
try {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
"Content-Type": "application/json",
|
|
282
|
-
"Authorization": `Bearer ${authToken}`,
|
|
283
|
-
},
|
|
284
|
-
body: JSON.stringify({ repoName }),
|
|
282
|
+
await (0, repo_identity_1.resolveCanonicalRepoName)({
|
|
283
|
+
repoRoot,
|
|
284
|
+
apiUrl,
|
|
285
|
+
token: selectedProject.token,
|
|
285
286
|
});
|
|
286
287
|
}
|
|
287
288
|
catch { }
|
|
@@ -14,26 +14,11 @@ const git_listener_1 = require("../adapters/git-adapter/git-listener");
|
|
|
14
14
|
const db_event_port_1 = require("../ports/db-event-port");
|
|
15
15
|
const cloud_event_port_1 = require("../ports/cloud-event-port");
|
|
16
16
|
const persistence_config_2 = require("../utils/persistence-config");
|
|
17
|
-
const
|
|
18
|
-
function
|
|
19
|
-
const
|
|
20
|
-
const normRoot = repoRoot.replace(/\/+$/, "");
|
|
21
|
-
let wsEntry = config.workspaces?.[normRoot] ?? config.workspaces?.[normRoot + "/"];
|
|
22
|
-
// Walk parent directories
|
|
23
|
-
let current = normRoot;
|
|
24
|
-
while (!wsEntry && current !== path_1.default.dirname(current)) {
|
|
25
|
-
current = path_1.default.dirname(current);
|
|
26
|
-
wsEntry = config.workspaces?.[current] ?? config.workspaces?.[current + "/"];
|
|
27
|
-
}
|
|
28
|
-
const token = wsEntry?.token ?? "";
|
|
29
|
-
const apiUrl = wsEntry?.apiUrl ?? config.apiUrl ?? API_BASE_DEFAULT;
|
|
30
|
-
return { token, apiUrl };
|
|
31
|
-
}
|
|
32
|
-
async function detectContextDecay(repoRoot, changedFiles) {
|
|
33
|
-
const { token, apiUrl } = getCredentials(repoRoot);
|
|
17
|
+
const repo_identity_1 = require("../utils/repo-identity");
|
|
18
|
+
async function detectContextDecay(repoRoot, changedFiles, repoName) {
|
|
19
|
+
const { token, apiUrl } = (0, persistence_config_1.getCredentials)(repoRoot);
|
|
34
20
|
if (!token)
|
|
35
21
|
return;
|
|
36
|
-
const repoName = path_1.default.basename(repoRoot);
|
|
37
22
|
for (const filePath of changedFiles) {
|
|
38
23
|
if (!fs_1.default.existsSync(filePath))
|
|
39
24
|
continue;
|
|
@@ -106,11 +91,11 @@ async function scanGitCommand(options) {
|
|
|
106
91
|
console.log(`Git scan complete (latest commit) for repo: ${repoRoot} commit: ${commitHash.slice(0, 8)}`);
|
|
107
92
|
// Send code_change signals for functions touched in this commit that already have memory
|
|
108
93
|
try {
|
|
109
|
-
const { token, apiUrl } = getCredentials(repoRoot);
|
|
94
|
+
const { token, apiUrl } = (0, persistence_config_1.getCredentials)(repoRoot);
|
|
110
95
|
const diffOutput = await git.diff([`${commitHash}^`, commitHash, '--name-only']).catch(() => '');
|
|
111
96
|
const commitChangedFiles = diffOutput.split('\n').filter(Boolean).map((f) => path_1.default.join(repoRoot, f)).filter((f) => /\.(ts|tsx|js|jsx|py|go|rs|php|java|kt|rb)$/.test(f));
|
|
112
97
|
if (token && commitChangedFiles.length > 0) {
|
|
113
|
-
const repoName =
|
|
98
|
+
const repoName = await (0, repo_identity_1.resolveCanonicalRepoName)({ repoRoot, apiUrl, token });
|
|
114
99
|
for (const filePath of commitChangedFiles) {
|
|
115
100
|
if (!fs_1.default.existsSync(filePath))
|
|
116
101
|
continue;
|
|
@@ -60,6 +60,7 @@ const ast_parser_1 = require("../utils/ast-parser");
|
|
|
60
60
|
const graph_builder_1 = require("../utils/graph-builder");
|
|
61
61
|
const context_assembler_1 = require("../utils/context-assembler");
|
|
62
62
|
const vendoredCodeDetector_1 = require("../utils/vendoredCodeDetector");
|
|
63
|
+
const repo_identity_1 = require("../utils/repo-identity");
|
|
63
64
|
const API_BASE = "https://kodingo-api.onrender.com";
|
|
64
65
|
const SUPPORTED_EXTENSIONS = [
|
|
65
66
|
".ts", ".tsx", ".js", ".jsx",
|
|
@@ -253,7 +254,7 @@ async function scanRepoCommand(options = {}) {
|
|
|
253
254
|
const repoRoot = options.repo
|
|
254
255
|
? path_1.default.resolve(options.repo)
|
|
255
256
|
: findRepoRoot(process.cwd());
|
|
256
|
-
const repoName =
|
|
257
|
+
const repoName = await (0, repo_identity_1.resolveCanonicalRepoName)({ repoRoot, apiUrl, token });
|
|
257
258
|
if (!options.silent) {
|
|
258
259
|
console.log(`\n🔍 Kortex — scanning ${repoName} for existing functions...\n`);
|
|
259
260
|
}
|
|
@@ -18,6 +18,7 @@ const graph_builder_1 = require("../utils/graph-builder");
|
|
|
18
18
|
const security_code_analyzer_1 = require("../utils/security-code-analyzer");
|
|
19
19
|
const context_assembler_1 = require("../utils/context-assembler");
|
|
20
20
|
const ast_parser_1 = require("../utils/ast-parser");
|
|
21
|
+
const repo_identity_1 = require("../utils/repo-identity");
|
|
21
22
|
const IGNORED_DIRS = new Set(["node_modules", ".git", ".kortex", "dist", "build", "vendor", "out", ".next", ".venv", "venv", "env", "site-packages", "virtualenv", ".tox", "target", ".gradle", ".idea", ".vscode", "bin", "obj"]);
|
|
22
23
|
const SUPPORTED_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".php", ".java", ".kt", ".rb", ".c", ".cpp", ".html", ".phtml", ".twig", ".blade.php"];
|
|
23
24
|
function walkFiles(dir) {
|
|
@@ -53,7 +54,7 @@ async function securityScanCommand(options = {}) {
|
|
|
53
54
|
console.error("✖ This repo is not connected to Kodingo. Run `kodingo init` first.");
|
|
54
55
|
return false;
|
|
55
56
|
}
|
|
56
|
-
const repoName =
|
|
57
|
+
const repoName = await (0, repo_identity_1.resolveCanonicalRepoName)({ repoRoot, apiUrl, token });
|
|
57
58
|
if (!options.silent)
|
|
58
59
|
console.log(`\n🔒 Kortex Security — scanning ${repoName} for known vulnerabilities...\n`);
|
|
59
60
|
try {
|
|
@@ -7,15 +7,12 @@
|
|
|
7
7
|
* Run this before 'kodingo security-scan' — the scan works off whatever was
|
|
8
8
|
* last synced here.
|
|
9
9
|
*/
|
|
10
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
11
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
12
|
-
};
|
|
13
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
11
|
exports.syncDependenciesCommand = syncDependenciesCommand;
|
|
15
|
-
const path_1 = __importDefault(require("path"));
|
|
16
12
|
const persistence_config_1 = require("../utils/persistence-config");
|
|
17
13
|
const repo_scope_1 = require("../utils/repo-scope");
|
|
18
14
|
const dependency_parser_1 = require("../utils/dependency-parser");
|
|
15
|
+
const repo_identity_1 = require("../utils/repo-identity");
|
|
19
16
|
async function syncDependenciesCommand(options = {}) {
|
|
20
17
|
const repoRoot = (0, repo_scope_1.resolveRepoScope)(options.repo ? { repo: options.repo } : {}) ?? process.cwd();
|
|
21
18
|
const config = (0, persistence_config_1.readConfig)();
|
|
@@ -27,7 +24,7 @@ async function syncDependenciesCommand(options = {}) {
|
|
|
27
24
|
console.error("✖ This repo is not connected to Kodingo. Run `kodingo init` first.");
|
|
28
25
|
return false;
|
|
29
26
|
}
|
|
30
|
-
const repoName =
|
|
27
|
+
const repoName = await (0, repo_identity_1.resolveCanonicalRepoName)({ repoRoot, apiUrl, token });
|
|
31
28
|
if (!options.silent)
|
|
32
29
|
console.log(`\n🔒 Kortex Security — scanning dependencies for ${repoName}...\n`);
|
|
33
30
|
const deps = (0, dependency_parser_1.extractDependencies)(repoRoot);
|
|
@@ -7,6 +7,7 @@ exports.CloudEventPort = void 0;
|
|
|
7
7
|
const kodingo_core_1 = require("kodingo-core");
|
|
8
8
|
const persistence_config_1 = require("../utils/persistence-config");
|
|
9
9
|
const inference_1 = require("../adapters/ai/inference");
|
|
10
|
+
const persistence_config_2 = require("../utils/persistence-config");
|
|
10
11
|
const path_1 = __importDefault(require("path"));
|
|
11
12
|
/**
|
|
12
13
|
* EventPort implementation that persists via getPersistence().
|
|
@@ -152,6 +153,7 @@ class CloudEventPort {
|
|
|
152
153
|
const filePaths = extractFilePaths(filesChanged);
|
|
153
154
|
const repoName = path_1.default.basename(this.repoPath);
|
|
154
155
|
const shortHash = String(commitHash).slice(0, 8);
|
|
156
|
+
const { token, apiUrl } = (0, persistence_config_2.getCredentials)(this.repoPath);
|
|
155
157
|
const content = await (0, inference_1.inferDecisionSummary)({
|
|
156
158
|
diff: typeof summary?.rawDiff === "string"
|
|
157
159
|
? summary.rawDiff
|
|
@@ -160,6 +162,8 @@ class CloudEventPort {
|
|
|
160
162
|
symbol: symbol ?? "unknown",
|
|
161
163
|
commitMessage: decision.rationale ?? "",
|
|
162
164
|
repoName,
|
|
165
|
+
token,
|
|
166
|
+
apiUrl,
|
|
163
167
|
});
|
|
164
168
|
const title = symbol
|
|
165
169
|
? `Decision: changes to ${symbol}`
|
|
@@ -7,6 +7,7 @@ exports.DbEventPort = void 0;
|
|
|
7
7
|
const kodingo_core_1 = require("kodingo-core");
|
|
8
8
|
const memory_persistence_1 = require("../adapters/db-adapter/memory-persistence");
|
|
9
9
|
const inference_1 = require("../adapters/ai/inference");
|
|
10
|
+
const persistence_config_1 = require("../utils/persistence-config");
|
|
10
11
|
const path_1 = __importDefault(require("path"));
|
|
11
12
|
/**
|
|
12
13
|
* Minimal EventPort implementation for the CLI.
|
|
@@ -113,6 +114,7 @@ class DbEventPort {
|
|
|
113
114
|
const filePaths = extractFilePaths(filesChanged);
|
|
114
115
|
const repoName = path_1.default.basename(this.repoPath);
|
|
115
116
|
const shortHash = String(commitHash).slice(0, 8);
|
|
117
|
+
const { token, apiUrl } = (0, persistence_config_1.getCredentials)(this.repoPath);
|
|
116
118
|
const content = await (0, inference_1.inferDecisionSummary)({
|
|
117
119
|
diff: typeof summary?.rawDiff === "string"
|
|
118
120
|
? summary.rawDiff
|
|
@@ -121,6 +123,8 @@ class DbEventPort {
|
|
|
121
123
|
symbol: symbol ?? "unknown",
|
|
122
124
|
commitMessage: decision.rationale ?? "",
|
|
123
125
|
repoName,
|
|
126
|
+
token,
|
|
127
|
+
apiUrl,
|
|
124
128
|
});
|
|
125
129
|
const title = symbol
|
|
126
130
|
? `Decision: changes to ${symbol}`
|
|
@@ -9,6 +9,7 @@ exports.writeConfig = writeConfig;
|
|
|
9
9
|
exports.writeWorkspaceConfig = writeWorkspaceConfig;
|
|
10
10
|
exports.readWorkspaceConfig = readWorkspaceConfig;
|
|
11
11
|
exports.isCloudMode = isCloudMode;
|
|
12
|
+
exports.getCredentials = getCredentials;
|
|
12
13
|
exports.getPersistence = getPersistence;
|
|
13
14
|
/**
|
|
14
15
|
* Persistence config switcher.
|
|
@@ -66,6 +67,32 @@ function readWorkspaceConfig(repoPath) {
|
|
|
66
67
|
function isCloudMode() {
|
|
67
68
|
return readConfig().mode === "cloud";
|
|
68
69
|
}
|
|
70
|
+
const API_BASE_DEFAULT = "https://kodingo-api.onrender.com";
|
|
71
|
+
/**
|
|
72
|
+
* Real, workspace-scoped project credentials, walking up to parent
|
|
73
|
+
* directories when the exact repoRoot isn't a registered workspace key.
|
|
74
|
+
* Moved here from scan-git.ts (build brief "Item 8 — the temporal lobe",
|
|
75
|
+
* 2026-10-01) — it originally lived there as a private helper, but
|
|
76
|
+
* cloud-event-port.ts/db-event-port.ts now need this SAME real lookup too
|
|
77
|
+
* (see those files' own doc comments — inferDecisionSummary was calling
|
|
78
|
+
* POST /infer with no auth header at all). Importing it back from
|
|
79
|
+
* scan-git.ts would create a real circular dependency (scan-git.ts already
|
|
80
|
+
* imports both event-port classes); this lower-level module has no such
|
|
81
|
+
* cycle. Zero behavior change from the original — a pure relocation.
|
|
82
|
+
*/
|
|
83
|
+
function getCredentials(repoRoot) {
|
|
84
|
+
const config = readConfig();
|
|
85
|
+
const normRoot = repoRoot.replace(/\/+$/, "");
|
|
86
|
+
let wsEntry = config.workspaces?.[normRoot] ?? config.workspaces?.[normRoot + "/"];
|
|
87
|
+
let current = normRoot;
|
|
88
|
+
while (!wsEntry && current !== node_path_1.default.dirname(current)) {
|
|
89
|
+
current = node_path_1.default.dirname(current);
|
|
90
|
+
wsEntry = config.workspaces?.[current] ?? config.workspaces?.[current + "/"];
|
|
91
|
+
}
|
|
92
|
+
const token = wsEntry?.token ?? "";
|
|
93
|
+
const apiUrl = wsEntry?.apiUrl ?? config.apiUrl ?? API_BASE_DEFAULT;
|
|
94
|
+
return { token, apiUrl };
|
|
95
|
+
}
|
|
69
96
|
/**
|
|
70
97
|
* Returns the correct persistence adapter based on config.
|
|
71
98
|
* Import this in commands instead of importing the adapters directly.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.parseRepoNameFromRemote = parseRepoNameFromRemote;
|
|
7
|
+
exports.getRemoteUrl = getRemoteUrl;
|
|
8
|
+
exports.deriveCandidateRepoName = deriveCandidateRepoName;
|
|
9
|
+
exports.resolveCanonicalRepoName = resolveCanonicalRepoName;
|
|
10
|
+
/**
|
|
11
|
+
* Resolves the canonical `repo` identity sent to the server, instead of
|
|
12
|
+
* every scan command inventing its own label from the local checkout.
|
|
13
|
+
*
|
|
14
|
+
* Root cause (build brief "Fix repo-label contamination at the source,
|
|
15
|
+
* platform-wide", 2026-09-30): every local capture path — scan-repo,
|
|
16
|
+
* security-scan, sync-dependencies, scan-git, and `kodingo init`'s own
|
|
17
|
+
* repo-registration call — independently computed `path.basename(repoRoot)`
|
|
18
|
+
* as the repo label. That's the local folder name, never a stable
|
|
19
|
+
* identifier: two developers with two different folder names for the same
|
|
20
|
+
* clone produce two different labels for the same real repo, and `init`
|
|
21
|
+
* seeded `kodingo_project_repos` (the intended source of truth) with that
|
|
22
|
+
* same unstable value, contaminating it at the first write.
|
|
23
|
+
*
|
|
24
|
+
* Fix: derive a candidate name from the git remote URL (stable across
|
|
25
|
+
* machines and folder names) and resolve it against the server's real,
|
|
26
|
+
* registered repos for this project via POST /cli/repo/resolve — which
|
|
27
|
+
* owns the single-repo / multi-repo / first-registration disambiguation.
|
|
28
|
+
* Falls back to the local folder name only when there's truly no git
|
|
29
|
+
* remote to read (e.g. a brand-new local-only repo).
|
|
30
|
+
*/
|
|
31
|
+
const path_1 = __importDefault(require("path"));
|
|
32
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
33
|
+
function parseRepoNameFromRemote(remoteUrl) {
|
|
34
|
+
const cleaned = remoteUrl.trim().replace(/\/+$/, "").replace(/\.git$/, "");
|
|
35
|
+
const match = cleaned.match(/[/:]([^/:]+)$/);
|
|
36
|
+
return match?.[1] || null;
|
|
37
|
+
}
|
|
38
|
+
async function getRemoteUrl(repoRoot) {
|
|
39
|
+
try {
|
|
40
|
+
const git = (0, simple_git_1.default)(repoRoot);
|
|
41
|
+
const remotes = await git.getRemotes(true);
|
|
42
|
+
if (!remotes.length)
|
|
43
|
+
return null;
|
|
44
|
+
const origin = remotes.find(r => r.name === "origin") ?? remotes[0];
|
|
45
|
+
return origin?.refs?.fetch || null;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function deriveCandidateRepoName(repoRoot) {
|
|
52
|
+
const remoteUrl = await getRemoteUrl(repoRoot);
|
|
53
|
+
const fromRemote = remoteUrl ? parseRepoNameFromRemote(remoteUrl) : null;
|
|
54
|
+
if (fromRemote)
|
|
55
|
+
return { candidateName: fromRemote, hasRemote: true };
|
|
56
|
+
return { candidateName: path_1.default.basename(repoRoot), hasRemote: false };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Returns the canonical repo label to send with this scan. Fails open to
|
|
60
|
+
* the locally derived candidate if the server is unreachable — a scan
|
|
61
|
+
* should never be blocked by this resolution, and the git-remote-derived
|
|
62
|
+
* candidate is already more stable than the old folder-basename behaviour
|
|
63
|
+
* even without a round-trip to the server.
|
|
64
|
+
*/
|
|
65
|
+
async function resolveCanonicalRepoName(params) {
|
|
66
|
+
const { candidateName, hasRemote } = await deriveCandidateRepoName(params.repoRoot);
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetch(`${params.apiUrl}/cli/repo/resolve`, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: { "Content-Type": "application/json", "X-Kodingo-Token": params.token },
|
|
71
|
+
body: JSON.stringify({ candidateName, hasRemote }),
|
|
72
|
+
});
|
|
73
|
+
if (res.ok) {
|
|
74
|
+
const data = (await res.json());
|
|
75
|
+
if (data.repoName)
|
|
76
|
+
return data.repoName;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// offline / server unreachable — fail open below
|
|
81
|
+
}
|
|
82
|
+
return candidateName;
|
|
83
|
+
}
|