carto-md 2.0.4 → 2.0.6
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/CONTRIBUTING.md +5 -5
- package/README.md +13 -20
- package/index.js +17 -7
- package/package.json +1 -1
- package/src/acp/session.js +9 -11
- package/src/cli/check.js +2 -0
- package/src/cli/impact.js +7 -5
- package/src/cli/init.js +3 -19
- package/src/cli/serve.js +13 -12
- package/src/cli/sync.js +2 -14
- package/src/cli/update-check.js +50 -7
- package/src/store/sqlite-store.js +35 -6
- package/src/store/store-adapter.js +169 -0
- package/src/store/sync-v2.js +37 -4
- package/src/cache/file-hash.js +0 -84
- package/src/cache/graph-cache.js +0 -77
- package/src/detector/files.js +0 -289
- package/src/engine/carto.js +0 -606
- package/src/engine/incremental.js +0 -149
- package/src/mcp/server.js +0 -431
- package/src/sync.js +0 -317
package/CONTRIBUTING.md
CHANGED
|
@@ -44,9 +44,9 @@ Framework-specific route and model extraction lives inside the language plugins.
|
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
-
## How to add a language (
|
|
47
|
+
## How to add a language (tree-sitter based)
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Carto uses tree-sitter for import and symbol extraction. Babel is only used for deep JS/TS route/model extraction on API handler files.
|
|
50
50
|
|
|
51
51
|
### Step 1: Install the grammar
|
|
52
52
|
|
|
@@ -177,7 +177,7 @@ Test on at least 2 real open-source projects using the framework.
|
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
|
|
180
|
-
## How domain clustering works
|
|
180
|
+
## How domain clustering works
|
|
181
181
|
|
|
182
182
|
Domain detection uses **Leiden+CPM graph clustering** (`src/agents/leiden.js`). Files that import each other heavily cluster together. Domain names are inferred from path tokens, with keyword hints for well-known patterns.
|
|
183
183
|
|
|
@@ -215,7 +215,7 @@ cd carto
|
|
|
215
215
|
npm install
|
|
216
216
|
node src/cli/index.js init # test in any project
|
|
217
217
|
node src/cli/index.js serve # test MCP server
|
|
218
|
-
npm test # run test suite (
|
|
218
|
+
npm test # run test suite (62 tests)
|
|
219
219
|
node test/correctness.js # run correctness tests (31 tests)
|
|
220
220
|
node test/benchmark.js # run benchmarks against real repos
|
|
221
221
|
```
|
|
@@ -258,7 +258,7 @@ npm run test:bench-ci # self-bench (matches what bench.yml runs)
|
|
|
258
258
|
- [ ] Extension added to `CODE_EXTS` and `detectLanguage()` in `sync-v2.js`
|
|
259
259
|
- [ ] No changes to merger logic (unless explicitly fixing a merger bug)
|
|
260
260
|
- [ ] No network calls added
|
|
261
|
-
- [ ] `npm test` passes (
|
|
261
|
+
- [ ] `npm test` passes (62/62)
|
|
262
262
|
- [ ] `node test/correctness.js` passes (31/31)
|
|
263
263
|
|
|
264
264
|
---
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ Reads `tsconfig.json` / `jsconfig.json` for `paths` config. `@/components/Button
|
|
|
138
138
|
|
|
139
139
|
## Domain detection
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
Carto uses **Leiden+CPM graph clustering** — files that import each other heavily cluster together. Domain names are inferred from path tokens, with keyword hints for well-known patterns (AUTH, PAYMENTS, DATABASE, etc.).
|
|
142
142
|
|
|
143
143
|
Works on any repo — not just SaaS apps. vscode gets AUTH/EVENTS/DATABASE. zed (Rust) gets DATABASE/AUTH/EVENTS. A game engine would get RENDERER/PHYSICS/AUDIO.
|
|
144
144
|
|
|
@@ -227,27 +227,10 @@ Other agents are smart but blind. Carto sees the architecture:
|
|
|
227
227
|
|
|
228
228
|
---
|
|
229
229
|
|
|
230
|
-
## V1 → V2 migration
|
|
231
|
-
|
|
232
|
-
Run `carto sync` — it auto-migrates your existing `graph-cache.json` and `map.json` into SQLite and renames them to `.bak`. No manual steps.
|
|
233
|
-
|
|
234
|
-
What changed under the hood:
|
|
235
|
-
|
|
236
|
-
| | V1 | V2 |
|
|
237
|
-
|---|---|---|
|
|
238
|
-
| Storage | JSON blobs | SQLite (WAL mode, indexed queries) |
|
|
239
|
-
| Parsing | Babel-only | tree-sitter for all languages, Babel only for deep route/model extraction |
|
|
240
|
-
| File limit | 300 cap | Unlimited |
|
|
241
|
-
| Languages | JS/TS/Python/Go/R | + Rust/Java/C++/C#/Ruby |
|
|
242
|
-
| Domain detection | Hardcoded keywords | Leiden+CPM graph clustering |
|
|
243
|
-
| Watcher | Per-file chokidar | Single recursive directory watch (<20 file descriptors) |
|
|
244
|
-
| MCP startup | Re-indexes on start | Opens SQLite instantly (<10ms) |
|
|
245
|
-
| Path aliases | Not resolved | `@/`, `~/` resolved via tsconfig |
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
230
|
## MCP config (if auto-wire missed your IDE)
|
|
250
231
|
|
|
232
|
+
`carto init` auto-wires into Kiro, Cursor, and Claude Desktop. If it missed yours, add manually:
|
|
233
|
+
|
|
251
234
|
**Kiro** — `~/.kiro/settings/mcp.json`
|
|
252
235
|
```json
|
|
253
236
|
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
@@ -263,6 +246,16 @@ What changed under the hood:
|
|
|
263
246
|
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
264
247
|
```
|
|
265
248
|
|
|
249
|
+
**VS Code Copilot** — `.vscode/mcp.json` in your project root, or Command Palette → `MCP: Add Server`
|
|
250
|
+
```json
|
|
251
|
+
{ "servers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Windsurf** — `~/.windsurf/mcp.json`
|
|
255
|
+
```json
|
|
256
|
+
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
257
|
+
```
|
|
258
|
+
|
|
266
259
|
---
|
|
267
260
|
|
|
268
261
|
## What Carto never does
|
package/index.js
CHANGED
|
@@ -4,17 +4,27 @@
|
|
|
4
4
|
* carto-md — public module API
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* const {
|
|
8
|
-
* const carto = new
|
|
7
|
+
* const { StoreAdapter } = require('carto-md');
|
|
8
|
+
* const carto = new StoreAdapter();
|
|
9
9
|
* await carto.index('/path/to/project');
|
|
10
10
|
*
|
|
11
|
-
* // Get everything
|
|
11
|
+
* // Get everything an editor agent needs for a file
|
|
12
12
|
* const ctx = carto.getContextForFile('src/auth/auth.service.ts');
|
|
13
13
|
*
|
|
14
|
-
* //
|
|
15
|
-
* carto.
|
|
14
|
+
* // Read structural facts
|
|
15
|
+
* const routes = carto.getRoutes();
|
|
16
|
+
* const radius = carto.getBlastRadius('src/auth/auth.service.ts');
|
|
17
|
+
* carto.close();
|
|
18
|
+
*
|
|
19
|
+
* Back-compat: `Carto` is exported as an alias for `StoreAdapter`.
|
|
20
|
+
* Existing programs that did `const { Carto } = require('carto-md')`
|
|
21
|
+
* continue to work without changes. The alias (and the `terminate()`
|
|
22
|
+
* method on StoreAdapter) will be removed in 3.0.0.
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
|
-
const {
|
|
25
|
+
const { StoreAdapter } = require('./src/store/store-adapter');
|
|
19
26
|
|
|
20
|
-
module.exports = {
|
|
27
|
+
module.exports = {
|
|
28
|
+
StoreAdapter,
|
|
29
|
+
Carto: StoreAdapter, // deprecated alias, removed in 3.0.0
|
|
30
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carto-md",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"description": "Structural intelligence layer for AI coding tools. Indexes your codebase into SQLite — routes, models, import graph, blast radius, domains — and exposes 16 MCP tools for Kiro, Cursor, and Claude.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"carto": "src/cli/index.js"
|
package/src/acp/session.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const {
|
|
6
|
+
const { StoreAdapter } = require('../store/store-adapter');
|
|
7
7
|
|
|
8
8
|
class Session {
|
|
9
9
|
constructor(id, workingDir) {
|
|
@@ -23,20 +23,16 @@ class Session {
|
|
|
23
23
|
if (this._indexed) return null;
|
|
24
24
|
|
|
25
25
|
const dbPath = path.join(this.workingDir, '.carto', 'carto.db');
|
|
26
|
-
|
|
26
|
+
const wasIndexed = fs.existsSync(dbPath);
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
// DB exists — just load the index (fast path, <10ms)
|
|
30
|
-
await this.carto.index(this.workingDir);
|
|
31
|
-
this._indexed = true;
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// No DB — run full indexing
|
|
28
|
+
this.carto = new StoreAdapter();
|
|
36
29
|
const start = Date.now();
|
|
37
|
-
await this.carto.index(this.workingDir, {
|
|
30
|
+
await this.carto.index(this.workingDir, { writeOutputs: false });
|
|
38
31
|
this._indexed = true;
|
|
39
32
|
|
|
33
|
+
if (wasIndexed) return null;
|
|
34
|
+
|
|
35
|
+
// Fresh index — surface status message
|
|
40
36
|
const meta = this.carto.getMeta();
|
|
41
37
|
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
|
42
38
|
const domains = this.carto.getDomainsList();
|
|
@@ -64,6 +60,8 @@ class SessionManager {
|
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
delete(id) {
|
|
63
|
+
const s = this._sessions.get(id);
|
|
64
|
+
if (s && s.carto) s.carto.close();
|
|
67
65
|
this._sessions.delete(id);
|
|
68
66
|
}
|
|
69
67
|
}
|
package/src/cli/check.js
CHANGED
|
@@ -4,8 +4,10 @@ const { execSync } = require('child_process');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const { SQLiteStore } = require('../store/sqlite-store');
|
|
7
|
+
const { checkForUpdate } = require('./update-check');
|
|
7
8
|
|
|
8
9
|
async function run(projectRoot) {
|
|
10
|
+
checkForUpdate(); // fire and forget
|
|
9
11
|
const dbPath = path.join(projectRoot, '.carto', 'carto.db');
|
|
10
12
|
|
|
11
13
|
if (!fs.existsSync(dbPath)) {
|
package/src/cli/impact.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const {
|
|
3
|
+
const { StoreAdapter } = require('../store/store-adapter');
|
|
4
|
+
const { checkForUpdate } = require('./update-check');
|
|
5
5
|
|
|
6
6
|
async function run(projectRoot, fileArg) {
|
|
7
|
+
checkForUpdate(); // fire and forget
|
|
7
8
|
if (!fileArg) {
|
|
8
9
|
console.error('[CARTO] Usage: carto impact <file>');
|
|
9
10
|
process.exit(1);
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const carto = new
|
|
13
|
+
const carto = new StoreAdapter();
|
|
13
14
|
try {
|
|
14
|
-
await carto.index(projectRoot
|
|
15
|
+
await carto.index(projectRoot);
|
|
15
16
|
} catch (err) {
|
|
16
17
|
console.error(`[CARTO] Error loading index: ${err.message}`);
|
|
17
18
|
process.exit(1);
|
|
@@ -20,6 +21,7 @@ async function run(projectRoot, fileArg) {
|
|
|
20
21
|
const br = carto.getBlastRadius(fileArg);
|
|
21
22
|
if (!br) {
|
|
22
23
|
console.error(`[CARTO] File not found in project graph: ${fileArg}`);
|
|
24
|
+
carto.close();
|
|
23
25
|
process.exit(1);
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -53,7 +55,7 @@ async function run(projectRoot, fileArg) {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
console.log('');
|
|
56
|
-
carto.
|
|
58
|
+
carto.close();
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
module.exports = { run };
|
package/src/cli/init.js
CHANGED
|
@@ -3,8 +3,10 @@ const path = require('path');
|
|
|
3
3
|
const { detectFramework } = require('../detector/framework');
|
|
4
4
|
const { parseCartoIgnore } = require('../security/ignore');
|
|
5
5
|
const { runSyncV2, discoverFiles: discoverFilesV2 } = require('../store/sync-v2');
|
|
6
|
+
const { checkForUpdate } = require('./update-check');
|
|
6
7
|
|
|
7
8
|
async function run(projectRoot) {
|
|
9
|
+
checkForUpdate(); // fire and forget
|
|
8
10
|
console.log('[CARTO] Detecting project...');
|
|
9
11
|
|
|
10
12
|
const detection = detectFramework(projectRoot);
|
|
@@ -52,8 +54,6 @@ async function run(projectRoot) {
|
|
|
52
54
|
installGitHook(projectRoot);
|
|
53
55
|
|
|
54
56
|
// Run first sync — V2 SQLite-backed indexer.
|
|
55
|
-
// (Previously: V1 runFullSync with empty file lists from resolveConfig — produced
|
|
56
|
-
// a 23ms no-op that left .carto/carto.db missing and AGENTS.md unpopulated.)
|
|
57
57
|
await runSyncV2({
|
|
58
58
|
projectRoot,
|
|
59
59
|
output: path.resolve(projectRoot, config.output || 'AGENTS.md')
|
|
@@ -65,22 +65,6 @@ async function run(projectRoot) {
|
|
|
65
65
|
console.log('[CARTO] AGENTS.md generated. Carto will sync on every git commit.');
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
/**
|
|
69
|
-
* Resolves config paths to absolute paths.
|
|
70
|
-
* V2: no watch file lists — sync-v2 discovers files itself.
|
|
71
|
-
*/
|
|
72
|
-
function resolveConfig(projectRoot, config) {
|
|
73
|
-
return {
|
|
74
|
-
watch: {
|
|
75
|
-
routeFiles: (config.watch && config.watch.routeFiles || []).map(f => path.resolve(projectRoot, f)),
|
|
76
|
-
modelFiles: (config.watch && config.watch.modelFiles || []).map(f => path.resolve(projectRoot, f)),
|
|
77
|
-
frontendFiles: (config.watch && config.watch.frontendFiles || []).map(f => path.resolve(projectRoot, f))
|
|
78
|
-
},
|
|
79
|
-
output: path.resolve(projectRoot, config.output || 'AGENTS.md'),
|
|
80
|
-
projectRoot
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
68
|
function installGitHook(projectRoot) {
|
|
85
69
|
const gitDir = path.join(projectRoot, '.git');
|
|
86
70
|
if (!fs.existsSync(gitDir)) return;
|
|
@@ -161,4 +145,4 @@ function wireIDEs(projectRoot) {
|
|
|
161
145
|
}
|
|
162
146
|
}
|
|
163
147
|
|
|
164
|
-
module.exports = { run
|
|
148
|
+
module.exports = { run };
|
package/src/cli/serve.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
+
const { checkForUpdate } = require('./update-check');
|
|
3
4
|
|
|
4
5
|
function run(projectRoot) {
|
|
5
|
-
//
|
|
6
|
+
checkForUpdate(); // fire and forget
|
|
6
7
|
const dbPath = path.join(projectRoot, '.carto', 'carto.db');
|
|
7
|
-
if (fs.existsSync(dbPath)) {
|
|
8
|
-
console.error('[CARTO] MCP server starting (V2 SQLite)...');
|
|
9
|
-
require('../mcp/server-v2');
|
|
10
|
-
return;
|
|
11
|
-
}
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
if (!fs.existsSync(dbPath)) {
|
|
10
|
+
// Could be pre-2.0.4 install with only map.json, or never initialized.
|
|
11
|
+
const mapPath = path.join(projectRoot, '.carto', 'map.json');
|
|
12
|
+
if (fs.existsSync(mapPath)) {
|
|
13
|
+
console.error('[CARTO] Legacy index found (map.json) but no SQLite DB. Run `carto init` to upgrade your index.');
|
|
14
|
+
} else {
|
|
15
|
+
console.error('[CARTO] No index found. Run `carto init` first.');
|
|
16
|
+
}
|
|
17
17
|
process.exit(1);
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
|
|
20
|
+
console.error('[CARTO] MCP server starting...');
|
|
21
|
+
require('../mcp/server-v2');
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
module.exports = { run };
|
package/src/cli/sync.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { runSyncV2 } = require('../store/sync-v2');
|
|
4
|
-
const { runFullSync } = require('../sync');
|
|
5
|
-
const { resolveConfig } = require('./init');
|
|
6
4
|
const { checkForUpdate } = require('./update-check');
|
|
7
5
|
|
|
8
6
|
async function run(projectRoot) {
|
|
@@ -22,20 +20,10 @@ async function run(projectRoot) {
|
|
|
22
20
|
process.exit(1);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
const v2Config = {
|
|
23
|
+
await runSyncV2({
|
|
27
24
|
projectRoot,
|
|
28
25
|
output: path.resolve(projectRoot, config.output || 'AGENTS.md')
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
await runSyncV2(v2Config);
|
|
33
|
-
} catch (err) {
|
|
34
|
-
// Fallback to V1 if V2 fails (e.g., better-sqlite3 not available)
|
|
35
|
-
console.warn(`[CARTO] V2 sync failed (${err.message}), falling back to V1`);
|
|
36
|
-
const resolved = resolveConfig(projectRoot, config);
|
|
37
|
-
await runFullSync(resolved);
|
|
38
|
-
}
|
|
26
|
+
});
|
|
39
27
|
|
|
40
28
|
console.log('[CARTO] Sync complete.');
|
|
41
29
|
}
|
package/src/cli/update-check.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
1
3
|
const https = require('https');
|
|
2
4
|
const pkg = require('../../package.json');
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Fire-and-forget version check against the npm registry.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
*
|
|
9
|
+
* Prints a visually distinct notice to stderr if a newer version is on npm.
|
|
10
|
+
* Never throws, never blocks (3s socket timeout, ignores all errors) —
|
|
11
|
+
* safe to call without await from any CLI command's entry point.
|
|
12
|
+
*
|
|
13
|
+
* Set `CARTO_NO_UPDATE_CHECK=1` to disable entirely (used in test runs and
|
|
14
|
+
* by users who don't want the egress).
|
|
8
15
|
*/
|
|
9
16
|
function checkForUpdate() {
|
|
17
|
+
if (process.env.CARTO_NO_UPDATE_CHECK) return;
|
|
18
|
+
|
|
10
19
|
const req = https.get('https://registry.npmjs.org/carto-md/latest', {
|
|
11
20
|
timeout: 3000,
|
|
12
21
|
}, (res) => {
|
|
@@ -16,10 +25,8 @@ function checkForUpdate() {
|
|
|
16
25
|
try {
|
|
17
26
|
const data = JSON.parse(body);
|
|
18
27
|
const latest = data.version;
|
|
19
|
-
if (latest &&
|
|
20
|
-
process.stderr.write(
|
|
21
|
-
`[CARTO] Update available: ${pkg.version} → ${latest} | npm install -g carto-md\n`
|
|
22
|
-
);
|
|
28
|
+
if (latest && isNewer(latest, pkg.version)) {
|
|
29
|
+
process.stderr.write(formatNotice(pkg.version, latest));
|
|
23
30
|
}
|
|
24
31
|
} catch (_) {
|
|
25
32
|
// malformed JSON — ignore
|
|
@@ -27,6 +34,10 @@ function checkForUpdate() {
|
|
|
27
34
|
});
|
|
28
35
|
});
|
|
29
36
|
|
|
37
|
+
// unref so the pending request never delays process exit on its own —
|
|
38
|
+
// if the main command finishes first, the check is silently dropped.
|
|
39
|
+
if (typeof req.unref === 'function') req.unref();
|
|
40
|
+
|
|
30
41
|
req.on('timeout', () => { req.destroy(); });
|
|
31
42
|
req.on('error', () => { /* offline / DNS failure — ignore */ });
|
|
32
43
|
}
|
|
@@ -45,4 +56,36 @@ function isNewer(a, b) {
|
|
|
45
56
|
return false;
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
|
|
59
|
+
/**
|
|
60
|
+
* formatNotice(current, latest) → multi-line stderr string.
|
|
61
|
+
*
|
|
62
|
+
* TTY-aware: emits ANSI yellow + bold when stderr is a TTY, plain text
|
|
63
|
+
* otherwise (so piped output and CI logs stay clean).
|
|
64
|
+
*
|
|
65
|
+
* Format (TTY example, ★ in yellow, version + command in bold):
|
|
66
|
+
*
|
|
67
|
+
* <blank line>
|
|
68
|
+
* [CARTO] ★ Update available: 2.0.1 → 2.0.5
|
|
69
|
+
* [CARTO] Run: npm install -g carto-md
|
|
70
|
+
* <blank line>
|
|
71
|
+
*
|
|
72
|
+
* Two blank lines (one before, one after) separate the notice from
|
|
73
|
+
* regular [CARTO] log output so the upgrade prompt stands out even when
|
|
74
|
+
* it lands mid-progress (the check is fire-and-forget, so the response
|
|
75
|
+
* arrives whenever the network resolves).
|
|
76
|
+
*/
|
|
77
|
+
function formatNotice(current, latest) {
|
|
78
|
+
const useColor = !!process.stderr.isTTY && !process.env.NO_COLOR;
|
|
79
|
+
const yellow = (s) => useColor ? `\x1b[33m${s}\x1b[0m` : s;
|
|
80
|
+
const bold = (s) => useColor ? `\x1b[1m${s}\x1b[0m` : s;
|
|
81
|
+
|
|
82
|
+
return [
|
|
83
|
+
'',
|
|
84
|
+
`${yellow('[CARTO] ★ Update available:')} ${bold(`${current} → ${latest}`)}`,
|
|
85
|
+
`${yellow('[CARTO] Run:')} ${bold('npm install -g carto-md')}`,
|
|
86
|
+
'',
|
|
87
|
+
''
|
|
88
|
+
].join('\n');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { checkForUpdate, isNewer, formatNotice };
|
|
@@ -6,6 +6,34 @@ const fs = require('fs');
|
|
|
6
6
|
|
|
7
7
|
const SCHEMA_VERSION = '1';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* normalizePath(p) — Canonicalize a relative path for storage and query.
|
|
11
|
+
*
|
|
12
|
+
* Converts backslashes to forward slashes (Windows → POSIX), strips leading
|
|
13
|
+
* './', and rejects absolute paths (callers must `path.relative(projectRoot, p)`
|
|
14
|
+
* before passing in).
|
|
15
|
+
*
|
|
16
|
+
* Single-source-of-truth for "what does a row in `files.path` look like?".
|
|
17
|
+
* Apply this at every boundary that writes or queries the column, and every
|
|
18
|
+
* downstream join (imports.from_file_id → files.path) stays consistent.
|
|
19
|
+
*
|
|
20
|
+
* Cross-platform invariant: same code, same DB, same query results on macOS,
|
|
21
|
+
* Linux, and Windows. Closes Bug 2 (carto impact path normalization) and the
|
|
22
|
+
* Windows-only "0 dependents" failures in the Store adapter test suite.
|
|
23
|
+
*/
|
|
24
|
+
function normalizePath(p) {
|
|
25
|
+
if (typeof p !== 'string' || p.length === 0) return p;
|
|
26
|
+
let out = p;
|
|
27
|
+
// Backslash → forward slash (Windows → POSIX). path.sep is '\\' on Windows.
|
|
28
|
+
if (path.sep !== '/') out = out.split(path.sep).join('/');
|
|
29
|
+
// Some inputs may already use mixed separators (e.g., `src\utils/foo.js`).
|
|
30
|
+
// Normalize aggressively.
|
|
31
|
+
out = out.replace(/\\/g, '/');
|
|
32
|
+
// Strip leading './'
|
|
33
|
+
if (out.startsWith('./')) out = out.slice(2);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
class SQLiteStore {
|
|
10
38
|
constructor(projectRoot) {
|
|
11
39
|
this._projectRoot = projectRoot;
|
|
@@ -213,7 +241,7 @@ class SQLiteStore {
|
|
|
213
241
|
// ─── File operations ───────────────────────────────────────────────────
|
|
214
242
|
|
|
215
243
|
getFileByPath(relPath) {
|
|
216
|
-
return this._db.prepare('SELECT * FROM files WHERE path = ?').get(relPath);
|
|
244
|
+
return this._db.prepare('SELECT * FROM files WHERE path = ?').get(normalizePath(relPath));
|
|
217
245
|
}
|
|
218
246
|
|
|
219
247
|
getFileById(id) {
|
|
@@ -230,7 +258,8 @@ class SQLiteStore {
|
|
|
230
258
|
}
|
|
231
259
|
|
|
232
260
|
upsertFile(relPath, { language, hash, mtime, size }) {
|
|
233
|
-
const
|
|
261
|
+
const norm = normalizePath(relPath);
|
|
262
|
+
const existing = this.getFileByPath(norm);
|
|
234
263
|
if (existing) {
|
|
235
264
|
this._db.prepare(
|
|
236
265
|
'UPDATE files SET language=?, hash=?, mtime=?, size=?, last_indexed_at=? WHERE id=?'
|
|
@@ -239,18 +268,18 @@ class SQLiteStore {
|
|
|
239
268
|
}
|
|
240
269
|
const info = this._db.prepare(
|
|
241
270
|
'INSERT INTO files (path, language, hash, mtime, size, last_indexed_at) VALUES (?,?,?,?,?,?)'
|
|
242
|
-
).run(
|
|
271
|
+
).run(norm, language, hash, mtime, size, Date.now());
|
|
243
272
|
return info.lastInsertRowid;
|
|
244
273
|
}
|
|
245
274
|
|
|
246
275
|
updateFileMtime(relPath, mtime, size) {
|
|
247
276
|
this._db.prepare(
|
|
248
277
|
'UPDATE files SET mtime=?, size=? WHERE path=?'
|
|
249
|
-
).run(mtime, size, relPath);
|
|
278
|
+
).run(mtime, size, normalizePath(relPath));
|
|
250
279
|
}
|
|
251
280
|
|
|
252
281
|
removeFile(relPath) {
|
|
253
|
-
this._db.prepare('DELETE FROM files WHERE path = ?').run(relPath);
|
|
282
|
+
this._db.prepare('DELETE FROM files WHERE path = ?').run(normalizePath(relPath));
|
|
254
283
|
}
|
|
255
284
|
|
|
256
285
|
removeStaleFiles(currentPaths) {
|
|
@@ -744,4 +773,4 @@ class SQLiteStore {
|
|
|
744
773
|
}
|
|
745
774
|
}
|
|
746
775
|
|
|
747
|
-
module.exports = { SQLiteStore };
|
|
776
|
+
module.exports = { SQLiteStore, normalizePath };
|