kodingo-cli 1.3.2 → 1.3.4

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,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
+ });
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * Real tests for vendoredCodeDetector.ts — build brief "Stop capturing
4
+ * vendored/third-party code as if it were the client's own" (2026-09-22).
5
+ * Mirrors kodingo-api's own test coverage for the same, identically-
6
+ * designed module.
7
+ */
8
+ var __importDefault = (this && this.__importDefault) || function (mod) {
9
+ return (mod && mod.__esModule) ? mod : { "default": mod };
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ const node_test_1 = require("node:test");
13
+ const strict_1 = __importDefault(require("node:assert/strict"));
14
+ const vendoredCodeDetector_1 = require("../utils/vendoredCodeDetector");
15
+ (0, node_test_1.describe)("isVendoredPath", () => {
16
+ (0, node_test_1.describe)("POSITIVE — real files confirmed polluting TestHaven/FOTX", () => {
17
+ for (const p of [
18
+ "wwwroot/lib/jquery/jquery.js",
19
+ "wwwroot/lib/jquery/jquery.min.js",
20
+ "Scripts/WebUIValidation.js",
21
+ "Scripts/WebForms.js",
22
+ "Scripts/MicrosoftAjaxWebForms.js",
23
+ ]) {
24
+ (0, node_test_1.it)(`flags ${p}`, () => {
25
+ strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredPath)(p).vendored, true);
26
+ });
27
+ }
28
+ });
29
+ (0, node_test_1.describe)("POSITIVE — general path/directory/filename conventions", () => {
30
+ for (const p of [
31
+ "node_modules/react/index.js",
32
+ "vendor/autoload.php",
33
+ "bower_components/angular/angular.js",
34
+ "jspm_packages/npm/lodash@4.js",
35
+ "Pods/Alamofire/Source/Request.swift",
36
+ ".venv/lib/python3.11/site-packages/flask/app.py",
37
+ "packages/Newtonsoft.Json.13.0.1/lib/net45/Newtonsoft.Json.cs",
38
+ "src/assets/app.min.js",
39
+ "src/assets/theme.min.css",
40
+ "jquery-3.6.0.js",
41
+ "bootstrap.bundle.min.js",
42
+ "modernizr-2.8.3.js",
43
+ ]) {
44
+ (0, node_test_1.it)(`flags ${p}`, () => {
45
+ strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredPath)(p).vendored, true);
46
+ });
47
+ }
48
+ });
49
+ (0, node_test_1.describe)("NEGATIVE — real, legitimate application files must NOT be flagged", () => {
50
+ for (const p of [
51
+ "src/validators/UserValidation.js",
52
+ "src/utils/MicrosoftAjaxHelper.cs",
53
+ "packages/api/src/OrderService.cs",
54
+ "src/lib/formatCurrency.ts",
55
+ "Scripts/OrderService.js",
56
+ "Scripts/checkout-page.js",
57
+ "src/services/OrderService.cs",
58
+ "src/controllers/CheckoutController.cs",
59
+ ]) {
60
+ (0, node_test_1.it)(`does not flag ${p}`, () => {
61
+ const result = (0, vendoredCodeDetector_1.isVendoredPath)(p);
62
+ strict_1.default.equal(result.vendored, false);
63
+ strict_1.default.equal(result.reason, null);
64
+ });
65
+ }
66
+ });
67
+ });
68
+ (0, node_test_1.describe)("isVendoredContent", () => {
69
+ (0, node_test_1.it)("flags genuinely minified-shaped content", () => {
70
+ const content = Array.from({ length: 5 }, () => "x".repeat(4000)).join("\n");
71
+ strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredContent)(content).vendored, true);
72
+ });
73
+ (0, node_test_1.it)("does not flag a real, dense, hand-written file with normal line breaks", () => {
74
+ const lines = [];
75
+ for (let i = 0; i < 300; i++) {
76
+ lines.push(` public Order GetOrder${i}(int id) { return _repo.Find(id, "status", "active"); }`);
77
+ }
78
+ const content = `namespace Acme.Orders {\n public class OrderService {\n${lines.join("\n")}\n }\n}`;
79
+ strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredContent)(content).vendored, false);
80
+ });
81
+ (0, node_test_1.it)("flags an explicit auto-generated header", () => {
82
+ const content = "// <auto-generated>\n// This code was generated by a tool.\nnamespace Acme { public class Foo {} }";
83
+ strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredContent)(content).vendored, true);
84
+ });
85
+ (0, node_test_1.it)("does not flag a small real file with one long line", () => {
86
+ const content = "const CONFIG = " + JSON.stringify({ a: 1, b: 2 });
87
+ strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredContent)(content).vendored, false);
88
+ });
89
+ });
90
+ (0, node_test_1.describe)("isVendoredCode", () => {
91
+ (0, node_test_1.it)("flags on path alone without content", () => {
92
+ strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredCode)("node_modules/react/index.js", null).vendored, true);
93
+ });
94
+ (0, node_test_1.it)("real, legitimate file is never flagged", () => {
95
+ const content = `export function calculateTotal(items) {\n return items.reduce((sum, i) => sum + i.price, 0);\n}`;
96
+ strict_1.default.equal((0, vendoredCodeDetector_1.isVendoredCode)("src/services/OrderService.ts", content).vendored, false);
97
+ });
98
+ });
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+ /**
3
+ * Real, end-to-end proof that vendored files never reach symbol
4
+ * extraction in either real local capture path — build brief "Stop
5
+ * capturing vendored/third-party code as if it were the client's own"
6
+ * (2026-09-22). Uses a real temp directory on disk (no mocking needed —
7
+ * these are pure filesystem + static-analysis functions), following the
8
+ * exact real symptom shape confirmed for TestHaven/FOTX.
9
+ */
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
44
+ return (mod && mod.__esModule) ? mod : { "default": mod };
45
+ };
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ const node_test_1 = require("node:test");
48
+ const strict_1 = __importDefault(require("node:assert/strict"));
49
+ const fs = __importStar(require("fs"));
50
+ const os = __importStar(require("os"));
51
+ const path = __importStar(require("path"));
52
+ const scan_repo_1 = require("../commands/scan-repo");
53
+ const graph_builder_1 = require("../utils/graph-builder");
54
+ let repoRoot;
55
+ function write(relPath, content) {
56
+ const full = path.join(repoRoot, relPath);
57
+ fs.mkdirSync(path.dirname(full), { recursive: true });
58
+ fs.writeFileSync(full, content, "utf-8");
59
+ }
60
+ function realFunctionSource(name) {
61
+ return `function ${name}(x) {\n const result = x + 1;\n return result;\n}\n`;
62
+ }
63
+ (0, node_test_1.before)(() => {
64
+ repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), "kortex-vendored-test-"));
65
+ // Real client business logic — must always survive.
66
+ write("src/OrderService.ts", realFunctionSource("getOrder"));
67
+ write("src/CheckoutController.ts", realFunctionSource("processCheckout"));
68
+ // Real jQuery, checked in under ASP.NET Core's real wwwroot/lib/ convention.
69
+ write("wwwroot/lib/jquery/jquery.js", `function domManip(a, b) { return a; }\nfunction isWindow(obj) { return obj === obj.window; }\n`);
70
+ // Real WebForms framework file, checked in under an ordinary Scripts/ folder.
71
+ write("Scripts/WebForms.js", `function WebForm_GetElementByTagName(tag) { return document.getElementsByTagName(tag); }\n`);
72
+ // Vendored via node_modules — already excluded at the directory-walk
73
+ // level by IGNORED_DIRS, included here to confirm it's genuinely never
74
+ // reached either way.
75
+ write("node_modules/some-lib/index.js", realFunctionSource("someLibInternal"));
76
+ // Genuinely minified, ordinary-looking filename — only real content
77
+ // reveals it.
78
+ write("src/thirdPartyBundle.js", Array.from({ length: 5 }, () => "x".repeat(4000)).join("\n"));
79
+ });
80
+ (0, node_test_1.after)(() => {
81
+ fs.rmSync(repoRoot, { recursive: true, force: true });
82
+ });
83
+ (0, node_test_1.describe)("scan-repo.ts — real walkFiles + extractSymbols never surface vendored files", () => {
84
+ (0, node_test_1.it)("walkFiles excludes the real jQuery file (path-level) but includes the client's own real files", () => {
85
+ const files = (0, scan_repo_1.walkFiles)(repoRoot).map(f => path.relative(repoRoot, f).split(path.sep).join("/"));
86
+ strict_1.default.ok(!files.includes("wwwroot/lib/jquery/jquery.js"), `jquery.js should have been excluded, got: ${JSON.stringify(files)}`);
87
+ strict_1.default.ok(!files.includes("Scripts/WebForms.js"), `WebForms.js should have been excluded, got: ${JSON.stringify(files)}`);
88
+ strict_1.default.ok(!files.includes("node_modules/some-lib/index.js"));
89
+ strict_1.default.ok(files.includes("src/OrderService.ts"));
90
+ strict_1.default.ok(files.includes("src/CheckoutController.ts"));
91
+ // The minified bundle passes the PATH check (ordinary name) — content
92
+ // filtering happens later, in extractSymbols.
93
+ strict_1.default.ok(files.includes("src/thirdPartyBundle.js"));
94
+ });
95
+ (0, node_test_1.it)("extractSymbols never returns real jQuery/WebForms symbols, and returns [] for the minified bundle via content-level detection", async () => {
96
+ const files = (0, scan_repo_1.walkFiles)(repoRoot);
97
+ const allSymbols = (await Promise.all(files.map(f => (0, scan_repo_1.extractSymbols)(f)))).flat();
98
+ const names = allSymbols.map(s => s.name);
99
+ strict_1.default.ok(!names.includes("domManip"), `domManip should never have been extracted, got: ${JSON.stringify(names)}`);
100
+ strict_1.default.ok(!names.includes("isWindow"));
101
+ strict_1.default.ok(!names.includes("WebForm_GetElementByTagName"));
102
+ strict_1.default.ok(names.includes("getOrder"));
103
+ strict_1.default.ok(names.includes("processCheckout"));
104
+ const bundleFile = files.find(f => f.endsWith("thirdPartyBundle.js"));
105
+ strict_1.default.ok(bundleFile);
106
+ const bundleSymbols = await (0, scan_repo_1.extractSymbols)(bundleFile);
107
+ strict_1.default.deepEqual(bundleSymbols, []);
108
+ });
109
+ });
110
+ (0, node_test_1.describe)("graph-builder.ts — real buildGraph never indexes vendored symbols", () => {
111
+ (0, node_test_1.it)("the real, built graph contains the client's own real functions and none of the vendored ones", async () => {
112
+ const graph = await (0, graph_builder_1.buildGraph)(repoRoot);
113
+ const names = Object.values(graph.nodes).map(n => n.name);
114
+ strict_1.default.ok(!names.includes("domManip"), `domManip should never appear in the graph, got: ${JSON.stringify(names)}`);
115
+ strict_1.default.ok(!names.includes("isWindow"));
116
+ strict_1.default.ok(!names.includes("WebForm_GetElementByTagName"));
117
+ strict_1.default.ok(!names.includes("someLibInternal"));
118
+ strict_1.default.ok(names.includes("getOrder"));
119
+ strict_1.default.ok(names.includes("processCheckout"));
120
+ });
121
+ });
@@ -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 contamination
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
- const repoName = path.basename(repoRoot);
278
- await fetch(`${apiUrl}/org/projects/${selectedProject.id}/repos`, {
279
- method: "POST",
280
- headers: {
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,6 +14,7 @@ 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 repo_identity_1 = require("../utils/repo-identity");
17
18
  const API_BASE_DEFAULT = "https://kodingo-api.onrender.com";
18
19
  function getCredentials(repoRoot) {
19
20
  const config = (0, persistence_config_1.readConfig)();
@@ -29,11 +30,10 @@ function getCredentials(repoRoot) {
29
30
  const apiUrl = wsEntry?.apiUrl ?? config.apiUrl ?? API_BASE_DEFAULT;
30
31
  return { token, apiUrl };
31
32
  }
32
- async function detectContextDecay(repoRoot, changedFiles) {
33
+ async function detectContextDecay(repoRoot, changedFiles, repoName) {
33
34
  const { token, apiUrl } = getCredentials(repoRoot);
34
35
  if (!token)
35
36
  return;
36
- const repoName = path_1.default.basename(repoRoot);
37
37
  for (const filePath of changedFiles) {
38
38
  if (!fs_1.default.existsSync(filePath))
39
39
  continue;
@@ -110,7 +110,7 @@ async function scanGitCommand(options) {
110
110
  const diffOutput = await git.diff([`${commitHash}^`, commitHash, '--name-only']).catch(() => '');
111
111
  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
112
  if (token && commitChangedFiles.length > 0) {
113
- const repoName = path_1.default.basename(repoRoot);
113
+ const repoName = await (0, repo_identity_1.resolveCanonicalRepoName)({ repoRoot, apiUrl, token });
114
114
  for (const filePath of commitChangedFiles) {
115
115
  if (!fs_1.default.existsSync(filePath))
116
116
  continue;
@@ -49,6 +49,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
49
49
  return (mod && mod.__esModule) ? mod : { "default": mod };
50
50
  };
51
51
  Object.defineProperty(exports, "__esModule", { value: true });
52
+ exports.walkFiles = walkFiles;
53
+ exports.extractSymbols = extractSymbols;
52
54
  exports.scanRepoCommand = scanRepoCommand;
53
55
  const fs_1 = __importDefault(require("fs"));
54
56
  const path_1 = __importDefault(require("path"));
@@ -57,6 +59,8 @@ const os = __importStar(require("os"));
57
59
  const ast_parser_1 = require("../utils/ast-parser");
58
60
  const graph_builder_1 = require("../utils/graph-builder");
59
61
  const context_assembler_1 = require("../utils/context-assembler");
62
+ const vendoredCodeDetector_1 = require("../utils/vendoredCodeDetector");
63
+ const repo_identity_1 = require("../utils/repo-identity");
60
64
  const API_BASE = "https://kodingo-api.onrender.com";
61
65
  const SUPPORTED_EXTENSIONS = [
62
66
  ".ts", ".tsx", ".js", ".jsx",
@@ -84,7 +88,7 @@ function findRepoRoot(startPath) {
84
88
  current = parent;
85
89
  }
86
90
  }
87
- function walkFiles(dir) {
91
+ function walkFiles(dir, repoRoot = dir) {
88
92
  const results = [];
89
93
  try {
90
94
  const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
@@ -93,9 +97,20 @@ function walkFiles(dir) {
93
97
  continue;
94
98
  const fullPath = path_1.default.join(dir, entry.name);
95
99
  if (entry.isDirectory()) {
96
- results.push(...walkFiles(fullPath));
100
+ results.push(...walkFiles(fullPath, repoRoot));
97
101
  }
98
102
  else if (entry.isFile() && SUPPORTED_EXTENSIONS.includes(path_1.default.extname(entry.name))) {
103
+ // Real, general vendored-code detection (build brief "Stop
104
+ // capturing vendored/third-party code as if it were the client's
105
+ // own", 2026-09-22) — IGNORED_DIRS above already prunes
106
+ // single-segment conventions (node_modules, vendor, .venv, ...)
107
+ // at real traversal speed; this catches the rest (two-segment
108
+ // conventions like wwwroot/lib/, minified-filename conventions,
109
+ // and known bundled framework files) which need the real,
110
+ // relative path to detect correctly.
111
+ const relativePath = path_1.default.relative(repoRoot, fullPath).split(path_1.default.sep).join("/");
112
+ if ((0, vendoredCodeDetector_1.isVendoredPath)(relativePath).vendored)
113
+ continue;
99
114
  results.push(fullPath);
100
115
  }
101
116
  }
@@ -106,6 +121,12 @@ function walkFiles(dir) {
106
121
  async function extractSymbols(filePath) {
107
122
  try {
108
123
  const source = fs_1.default.readFileSync(filePath, "utf-8");
124
+ // Content-level check — catches minified/auto-generated files that
125
+ // pass the path check (e.g. a bundled framework script checked in
126
+ // directly, not under a vendored-directory convention). Recognized
127
+ // and skipped BEFORE symbol extraction, not filtered after the fact.
128
+ if ((0, vendoredCodeDetector_1.isVendoredContent)(source).vendored)
129
+ return [];
109
130
  const nodes = await (0, ast_parser_1.parseFile)(filePath, source);
110
131
  return nodes.map(n => ({
111
132
  name: n.name,
@@ -233,7 +254,7 @@ async function scanRepoCommand(options = {}) {
233
254
  const repoRoot = options.repo
234
255
  ? path_1.default.resolve(options.repo)
235
256
  : findRepoRoot(process.cwd());
236
- const repoName = path_1.default.basename(repoRoot);
257
+ const repoName = await (0, repo_identity_1.resolveCanonicalRepoName)({ repoRoot, apiUrl, token });
237
258
  if (!options.silent) {
238
259
  console.log(`\n🔍 Kortex — scanning ${repoName} for existing functions...\n`);
239
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 = path_1.default.basename(repoRoot);
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 = path_1.default.basename(repoRoot);
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);
@@ -61,6 +61,7 @@ const fs_1 = __importDefault(require("fs"));
61
61
  const path_1 = __importDefault(require("path"));
62
62
  const ts = __importStar(require("typescript"));
63
63
  const ast_parser_1 = require("./ast-parser");
64
+ const vendoredCodeDetector_1 = require("./vendoredCodeDetector");
64
65
  const SUPPORTED_EXTENSIONS = [
65
66
  ".ts", ".tsx", ".js", ".jsx",
66
67
  ".py", ".go", ".rs", ".php",
@@ -73,7 +74,7 @@ const IGNORED_DIRS = [
73
74
  ".venv", "venv", "env", "site-packages", "virtualenv",
74
75
  ".tox", "target", ".gradle", ".idea", ".vscode", "bin", "obj",
75
76
  ];
76
- function walkFiles(dir) {
77
+ function walkFiles(dir, repoRoot = dir) {
77
78
  const results = [];
78
79
  try {
79
80
  const entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
@@ -82,9 +83,17 @@ function walkFiles(dir) {
82
83
  continue;
83
84
  const fullPath = path_1.default.join(dir, entry.name);
84
85
  if (entry.isDirectory()) {
85
- results.push(...walkFiles(fullPath));
86
+ results.push(...walkFiles(fullPath, repoRoot));
86
87
  }
87
88
  else if (entry.isFile() && SUPPORTED_EXTENSIONS.includes(path_1.default.extname(entry.name))) {
89
+ // Real, general vendored-code detection (build brief "Stop
90
+ // capturing vendored/third-party code as if it were the client's
91
+ // own", 2026-09-22) — a polluted graph corrupts real centrality/
92
+ // reachability scoring (used by attack-surface reasoning), the
93
+ // same real underlying problem as polluted memory capture.
94
+ const relativePath = path_1.default.relative(repoRoot, fullPath).split(path_1.default.sep).join("/");
95
+ if ((0, vendoredCodeDetector_1.isVendoredPath)(relativePath).vendored)
96
+ continue;
88
97
  results.push(fullPath);
89
98
  }
90
99
  }
@@ -109,6 +118,8 @@ async function collectAllSymbols(repoRoot) {
109
118
  for (const file of files) {
110
119
  try {
111
120
  const source = fs_1.default.readFileSync(file, "utf-8");
121
+ if ((0, vendoredCodeDetector_1.isVendoredContent)(source).vendored)
122
+ continue;
112
123
  const nodes = await (0, ast_parser_1.parseFile)(file, source);
113
124
  for (const n of nodes) {
114
125
  all.push({ ...n, language: languageForFile(file) });
@@ -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
+ }
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /**
3
+ * vendoredCodeDetector.ts
4
+ *
5
+ * Real, general, structural detection of vendored/third-party/bundled code
6
+ * — NOT a hardcoded library-name list as the primary mechanism. Build
7
+ * brief: "Stop capturing vendored/third-party code as if it were the
8
+ * client's own" (2026-09-22). Mirrored, identically-designed module in
9
+ * kodingo-api (utils/vendoredCodeDetector.ts) and kodingo-vscode
10
+ * (src/utils/vendoredCodeDetector.ts) — each repo has its own real,
11
+ * separate walk/extract pipeline with no shared low-level utility package
12
+ * between them (kodingo-core is explicitly scoped to decision/memory
13
+ * lifecycle domain logic, not general file classification), so this is
14
+ * duplicated by design, matching this codebase's own established
15
+ * precedent (e.g. IGNORED_DIRS is already independently duplicated across
16
+ * scan-repo.ts and security-scan.ts within this same repo). See this
17
+ * brief's own final report for the full reasoning.
18
+ *
19
+ * Real, confirmed evidence this fixes: an admin deep scan of a real
20
+ * .NET/WebForms project (TestHaven/FOTX) captured real jQuery internals
21
+ * (domManip, isWindow, movePrevious) and real ASP.NET WebForms framework
22
+ * internals (WebPartManager_InitiateWebPartDragDrop,
23
+ * WebForm_GetElementByTagName, Zone_UpdatePosition) as if they were the
24
+ * client's own business logic — diluting a real answer to "what is this
25
+ * project" into near-nothing.
26
+ *
27
+ * Three real, independent layers, checked cheapest/most-certain first:
28
+ * 1. Path/directory conventions — reliable across languages/ecosystems,
29
+ * needs only the file path.
30
+ * 2. File-naming conventions (.min.js/.min.css) — same, path-only.
31
+ * 3. Content heuristics (minified shape, an explicit auto-generated
32
+ * header) — needs real file content.
33
+ * A fourth, explicitly-acknowledged-INCOMPLETE layer: a real, growing list
34
+ * of specific, well-known bundled framework files. This list will NEVER be
35
+ * complete — it exists because layers 1-2, by design, cannot catch every
36
+ * real case: these specific files are frequently checked in un-minified,
37
+ * under an ordinarily-legitimate-looking folder name (e.g. "Scripts/",
38
+ * which is NOT treated as a vendored-directory convention here precisely
39
+ * because plenty of real WebForms projects also put their OWN
40
+ * app-specific JS there — see isVendoredPath's own reasoning below).
41
+ */
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.isVendoredPath = isVendoredPath;
44
+ exports.isVendoredContent = isVendoredContent;
45
+ exports.isVendoredCode = isVendoredCode;
46
+ const NOT_VENDORED = { vendored: false, reason: null };
47
+ // ── Layer 1: path/directory conventions ─────────────────────────────────────
48
+ const VENDORED_DIR_SEGMENTS = [
49
+ "node_modules", "vendor", "bower_components", "jspm_packages", "pods",
50
+ ".venv", "venv", "site-packages",
51
+ ];
52
+ // "wwwroot/lib/" specifically is ASP.NET Core's real, well-known convention
53
+ // for client-side libraries pulled in via LibMan/npm — directly relevant
54
+ // to this exact client's real stack. "lib/" alone is NOT treated as
55
+ // vendored — an extremely common real application folder name on its own.
56
+ const VENDORED_PATH_PAIRS = [
57
+ ["wwwroot", "lib"],
58
+ ];
59
+ // Classic NuGet packages/ convention: packages/<PackageName>.<Version>/...
60
+ // Deliberately NOT a bare "packages/" match — that would false-positive on
61
+ // the equally common JS/TS monorepo convention (Lerna/Yarn/npm workspaces)
62
+ // where packages/<name>/ holds the CLIENT'S OWN first-party code.
63
+ const NUGET_VERSIONED_DIR = /^[A-Za-z0-9_.-]+\.\d+\.\d+(\.\d+)?(-[A-Za-z0-9.]+)?$/;
64
+ function pathSegments(filePath) {
65
+ return filePath.replace(/\\/g, "/").split("/").filter(Boolean);
66
+ }
67
+ function isVendoredPath(filePath) {
68
+ const segments = pathSegments(filePath);
69
+ const lowerSegments = segments.map(s => s.toLowerCase());
70
+ for (const dir of VENDORED_DIR_SEGMENTS) {
71
+ if (lowerSegments.includes(dir)) {
72
+ return { vendored: true, reason: `Path contains the "${dir}/" vendored-dependency directory convention.` };
73
+ }
74
+ }
75
+ for (const [first, second] of VENDORED_PATH_PAIRS) {
76
+ for (let i = 0; i < lowerSegments.length - 1; i++) {
77
+ if (lowerSegments[i] === first && lowerSegments[i + 1] === second) {
78
+ return { vendored: true, reason: `Path contains the "${first}/${second}/" vendored-dependency directory convention.` };
79
+ }
80
+ }
81
+ }
82
+ const packagesIdx = lowerSegments.indexOf("packages");
83
+ if (packagesIdx !== -1 && packagesIdx + 1 < segments.length && NUGET_VERSIONED_DIR.test(segments[packagesIdx + 1])) {
84
+ return { vendored: true, reason: `Path matches the classic NuGet "packages/<Name>.<Version>/" vendored-package convention.` };
85
+ }
86
+ const basename = segments[segments.length - 1] ?? "";
87
+ if (/\.min\.(js|css)$/i.test(basename) || /-min\.(js|css)$/i.test(basename)) {
88
+ return { vendored: true, reason: `Filename "${basename}" follows the minified-distribution naming convention (.min.js/.min.css).` };
89
+ }
90
+ const knownFileReason = matchKnownVendoredFile(basename);
91
+ if (knownFileReason)
92
+ return { vendored: true, reason: knownFileReason };
93
+ return NOT_VENDORED;
94
+ }
95
+ // ── Layer 2: content heuristics ─────────────────────────────────────────────
96
+ const MIN_CONTENT_LENGTH_FOR_MINIFICATION_CHECK = 5000;
97
+ const MINIFIED_AVG_LINE_LENGTH_THRESHOLD = 500;
98
+ const AUTO_GENERATED_HEADER_PATTERN = /<auto-generated|@generated\b|do not (edit|modify) this file|this file (is|was) auto-generated|machine[- ]generated/i;
99
+ function isVendoredContent(content) {
100
+ const header = content.slice(0, 500);
101
+ if (AUTO_GENERATED_HEADER_PATTERN.test(header)) {
102
+ return { vendored: true, reason: "File header contains an explicit auto-generated marker." };
103
+ }
104
+ if (content.length >= MIN_CONTENT_LENGTH_FOR_MINIFICATION_CHECK) {
105
+ const lineCount = content.split("\n").length;
106
+ const avgLineLength = content.length / Math.max(1, lineCount);
107
+ if (avgLineLength > MINIFIED_AVG_LINE_LENGTH_THRESHOLD) {
108
+ return {
109
+ vendored: true,
110
+ reason: `Unusually long average line length (${Math.round(avgLineLength)} chars/line across ${lineCount} lines) consistent with minified/bundled code, not hand-written source.`,
111
+ };
112
+ }
113
+ }
114
+ return NOT_VENDORED;
115
+ }
116
+ // ── Layer 3 (secondary, explicitly incomplete): known bundled framework files ──
117
+ const KNOWN_VENDORED_FILENAME_PATTERNS = [
118
+ /^jquery(-\d+(\.\d+){1,2})?(\.min)?\.js$/i,
119
+ /^jquery-ui(-\d+(\.\d+){1,2})?(\.min)?\.js$/i,
120
+ /^jquery\.validate(\.unobtrusive)?(\.min)?\.js$/i,
121
+ /^jquery\.unobtrusive-ajax(\.min)?\.js$/i,
122
+ /^WebUIValidation\.js$/i,
123
+ /^WebForms\.js$/i,
124
+ /^MicrosoftAjax(WebForms|Timer)?(\.debug)?\.js$/i,
125
+ /^SmartNav\.js$/i,
126
+ /^bootstrap(\.bundle)?(\.min)?\.(js|css)$/i,
127
+ /^modernizr(-\d+(\.\d+){1,2})?(\.min)?\.js$/i,
128
+ ];
129
+ function matchKnownVendoredFile(basename) {
130
+ for (const pattern of KNOWN_VENDORED_FILENAME_PATTERNS) {
131
+ if (pattern.test(basename)) {
132
+ return `Filename "${basename}" matches a real, known bundled/vendored framework file (secondary, explicitly-incomplete list — see this module's own file header).`;
133
+ }
134
+ }
135
+ return null;
136
+ }
137
+ // ── Combined convenience ─────────────────────────────────────────────────────
138
+ function isVendoredCode(filePath, content) {
139
+ const pathResult = isVendoredPath(filePath);
140
+ if (pathResult.vendored)
141
+ return pathResult;
142
+ if (content !== null)
143
+ return isVendoredContent(content);
144
+ return NOT_VENDORED;
145
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kodingo-cli",
3
- "version": "1.3.2",
3
+ "version": "1.3.4",
4
4
  "description": "Kodingo CLI",
5
5
  "license": "MIT",
6
6
  "private": false,