@aprimediet/codewalker 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -50
- package/index.ts +6 -42
- package/package.json +20 -39
- package/prompts/codewalker.md +7 -0
- package/skills/codewalker/SKILL.md +43 -0
- package/src/cards.test.ts +88 -0
- package/src/cards.ts +87 -0
- package/src/db.test.ts +197 -0
- package/src/db.ts +196 -0
- package/src/extract/ctags-parse.test.ts +108 -0
- package/src/extract/ctags-parse.ts +112 -0
- package/src/extract/ctags.ts +51 -0
- package/src/extract/docs.test.ts +81 -0
- package/src/extract/docs.ts +169 -0
- package/src/extract/regex.test.ts +202 -0
- package/src/extract/regex.ts +192 -0
- package/src/format.test.ts +71 -0
- package/src/format.ts +63 -0
- package/src/git.test.ts +75 -0
- package/src/git.ts +62 -0
- package/src/index.contract.test.ts +100 -0
- package/src/index.ts +124 -0
- package/src/indexer.test.ts +138 -0
- package/src/indexer.ts +352 -0
- package/src/project.test.ts +115 -0
- package/src/project.ts +204 -0
- package/src/query.test.ts +98 -0
- package/src/query.ts +73 -0
- package/src/sync.test.ts +116 -0
- package/src/types.ts +89 -0
- package/vitest.config.ts +28 -0
- package/LICENSE +0 -21
- package/agents.ts +0 -126
- package/compat.ts +0 -217
- package/detect.ts +0 -188
- package/docs/PRD.md +0 -78
- package/prd.ts +0 -106
- package/skills/learn-this/SKILL.md +0 -325
package/src/git.test.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { getHeadSha, changedFilesSince, parseDiffNameOnly } from './git.ts';
|
|
7
|
+
|
|
8
|
+
describe('parseDiffNameOnly', () => {
|
|
9
|
+
it('parses git diff --name-only output into string[]', () => {
|
|
10
|
+
const output = `src/a.ts\nsrc/b.ts\nREADME.md\n`;
|
|
11
|
+
const files = parseDiffNameOnly(output);
|
|
12
|
+
expect(files).toEqual(['src/a.ts', 'src/b.ts', 'README.md']);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns empty array for empty output', () => {
|
|
16
|
+
expect(parseDiffNameOnly('')).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('trims whitespace from each line', () => {
|
|
20
|
+
const output = ` src/a.ts \n src/b.ts\n`;
|
|
21
|
+
const files = parseDiffNameOnly(output);
|
|
22
|
+
expect(files).toEqual(['src/a.ts', 'src/b.ts']);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe('git operations in a real repo', () => {
|
|
27
|
+
let tmpDir: string;
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cw-git-'));
|
|
31
|
+
execSync('git init', { cwd: tmpDir, stdio: 'ignore' });
|
|
32
|
+
execSync('git config user.email test@test.com', { cwd: tmpDir, stdio: 'ignore' });
|
|
33
|
+
execSync('git config user.name Test', { cwd: tmpDir, stdio: 'ignore' });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('getHeadSha returns the current HEAD commit hash', () => {
|
|
41
|
+
fs.writeFileSync(path.join(tmpDir, 'a.ts'), 'content');
|
|
42
|
+
execSync('git add . && git commit -m "first"', { cwd: tmpDir, stdio: 'ignore' });
|
|
43
|
+
const sha = getHeadSha(tmpDir);
|
|
44
|
+
expect(sha).toBeTruthy();
|
|
45
|
+
expect(sha!.length).toBe(40);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('getHeadSha returns null when there are no commits', () => {
|
|
49
|
+
const sha = getHeadSha(tmpDir);
|
|
50
|
+
expect(sha).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('changedFilesSince returns [] when lastCommit === HEAD', () => {
|
|
54
|
+
fs.writeFileSync(path.join(tmpDir, 'a.ts'), 'content');
|
|
55
|
+
execSync('git add . && git commit -m "first"', { cwd: tmpDir, stdio: 'ignore' });
|
|
56
|
+
const sha = getHeadSha(tmpDir)!;
|
|
57
|
+
const files = changedFilesSince(tmpDir, sha);
|
|
58
|
+
expect(files).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('changedFilesSince returns changed files since a previous commit', () => {
|
|
62
|
+
fs.writeFileSync(path.join(tmpDir, 'a.ts'), 'v1');
|
|
63
|
+
execSync('git add . && git commit -m "first"', { cwd: tmpDir, stdio: 'ignore' });
|
|
64
|
+
const firstSha = getHeadSha(tmpDir)!;
|
|
65
|
+
|
|
66
|
+
// Second commit
|
|
67
|
+
fs.writeFileSync(path.join(tmpDir, 'a.ts'), 'v2');
|
|
68
|
+
fs.writeFileSync(path.join(tmpDir, 'b.ts'), 'new');
|
|
69
|
+
execSync('git add . && git commit -m "second"', { cwd: tmpDir, stdio: 'ignore' });
|
|
70
|
+
|
|
71
|
+
const files = changedFilesSince(tmpDir, firstSha);
|
|
72
|
+
expect(files).toContain('a.ts');
|
|
73
|
+
expect(files).toContain('b.ts');
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/git.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git operations for codewalker's git-anchored incremental sync.
|
|
3
|
+
*
|
|
4
|
+
* Provides getHeadSha, changedFilesSince (git diff --name-only), and
|
|
5
|
+
* the pure parser parseDiffNameOnly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the current HEAD commit SHA for a repo.
|
|
12
|
+
* Returns null if there are no commits yet.
|
|
13
|
+
*/
|
|
14
|
+
export function getHeadSha(repoDir: string): string | null {
|
|
15
|
+
try {
|
|
16
|
+
const sha = execSync("git rev-parse HEAD", {
|
|
17
|
+
cwd: repoDir,
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
20
|
+
timeout: 5000,
|
|
21
|
+
}).trim();
|
|
22
|
+
return sha || null;
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get the list of files changed between two commits.
|
|
30
|
+
* Uses `git diff --name-only <from> <to>`.
|
|
31
|
+
* Returns an empty array if no files changed.
|
|
32
|
+
*/
|
|
33
|
+
export function changedFilesSince(
|
|
34
|
+
repoDir: string,
|
|
35
|
+
sinceCommit: string,
|
|
36
|
+
): string[] {
|
|
37
|
+
try {
|
|
38
|
+
const output = execSync(
|
|
39
|
+
`git diff --name-only "${sinceCommit}" HEAD`,
|
|
40
|
+
{
|
|
41
|
+
cwd: repoDir,
|
|
42
|
+
encoding: "utf-8",
|
|
43
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
44
|
+
timeout: 5000,
|
|
45
|
+
},
|
|
46
|
+
);
|
|
47
|
+
return parseDiffNameOnly(output);
|
|
48
|
+
} catch {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Pure: parse the output of `git diff --name-only` into a string array.
|
|
55
|
+
* Each line is a file path; ignores empty lines.
|
|
56
|
+
*/
|
|
57
|
+
export function parseDiffNameOnly(output: string): string[] {
|
|
58
|
+
return output
|
|
59
|
+
.split("\n")
|
|
60
|
+
.map((line) => line.trim())
|
|
61
|
+
.filter((line) => line.length > 0);
|
|
62
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
|
|
6
|
+
// We test the contract of the extension factory: it must register a tool and a command.
|
|
7
|
+
// We create a minimal pi API stub to verify the shape.
|
|
8
|
+
|
|
9
|
+
interface ToolRegistration {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
parameters: unknown;
|
|
13
|
+
execute: Function;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface CommandRegistration {
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
handler: Function;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createPiStub(): {
|
|
23
|
+
tools: ToolRegistration[];
|
|
24
|
+
commands: CommandRegistration[];
|
|
25
|
+
api: Record<string, Function>;
|
|
26
|
+
} {
|
|
27
|
+
const tools: ToolRegistration[] = [];
|
|
28
|
+
const commands: CommandRegistration[] = [];
|
|
29
|
+
const api: Record<string, Function> = {};
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
tools,
|
|
33
|
+
commands,
|
|
34
|
+
api: {
|
|
35
|
+
registerTool: (t: ToolRegistration) => { tools.push(t); },
|
|
36
|
+
registerCommand: (name: string, cmd: { description: string; handler: Function }) => {
|
|
37
|
+
commands.push({ name, description: cmd.description, handler: cmd.handler });
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('index.ts contract', () => {
|
|
44
|
+
it('the default export is a factory function', async () => {
|
|
45
|
+
const mod = await import('./index.ts');
|
|
46
|
+
expect(typeof mod.default).toBe('function');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('calling the factory registers a tool named codewalker_query and a command named codewalker', async () => {
|
|
50
|
+
const mod = await import('./index.ts');
|
|
51
|
+
const stub = createPiStub();
|
|
52
|
+
|
|
53
|
+
// Call the factory with the stub API
|
|
54
|
+
mod.default(stub.api as any);
|
|
55
|
+
|
|
56
|
+
// Check tool registered
|
|
57
|
+
const queryTool = stub.tools.find(t => t.name === 'codewalker_query');
|
|
58
|
+
expect(queryTool).toBeDefined();
|
|
59
|
+
expect(queryTool!.description).toContain('code index');
|
|
60
|
+
|
|
61
|
+
// Check command registered
|
|
62
|
+
const cmd = stub.commands.find(c => c.name === 'codewalker');
|
|
63
|
+
expect(cmd).toBeDefined();
|
|
64
|
+
expect(cmd!.description).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('tool.execute returns { content, details } with compact text content', async () => {
|
|
68
|
+
// Create a temporary project dir so the DB path exists
|
|
69
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cw-contract-'));
|
|
70
|
+
const piDir = path.join(tmpDir, '.pi');
|
|
71
|
+
fs.mkdirSync(piDir, { recursive: true });
|
|
72
|
+
const markerId = 'test-project-contract-' + Math.random().toString(36).slice(2, 8);
|
|
73
|
+
fs.writeFileSync(path.join(piDir, markerId + '.md'), `---\npi-project: true\nid: ${markerId}\n---\n`);
|
|
74
|
+
|
|
75
|
+
// Pre-create the codewalker global dir so the DB can be opened
|
|
76
|
+
const homePi = path.join(os.homedir(), '.pi', 'projects', markerId, 'codewalker');
|
|
77
|
+
fs.mkdirSync(homePi, { recursive: true });
|
|
78
|
+
|
|
79
|
+
const mod = await import('./index.ts');
|
|
80
|
+
const stub = createPiStub();
|
|
81
|
+
|
|
82
|
+
mod.default(stub.api as any);
|
|
83
|
+
|
|
84
|
+
const tool = stub.tools.find(t => t.name === 'codewalker_query')!;
|
|
85
|
+
const result = await tool.execute(
|
|
86
|
+
'test-id',
|
|
87
|
+
{ query: 'test' },
|
|
88
|
+
new AbortController().signal,
|
|
89
|
+
() => {},
|
|
90
|
+
{ cwd: tmpDir },
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(result).toHaveProperty('content');
|
|
94
|
+
expect(result).toHaveProperty('details');
|
|
95
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
96
|
+
expect(result.content[0]!.type).toBe('text');
|
|
97
|
+
|
|
98
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
99
|
+
});
|
|
100
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @aprimediet/codewalker
|
|
3
|
+
*
|
|
4
|
+
* Queryable, token-economical project & code index for the pi coding agent.
|
|
5
|
+
*
|
|
6
|
+
* Registers:
|
|
7
|
+
* - `codewalker_query` tool (agent-facing, compact results)
|
|
8
|
+
* - `/codewalker` command (human-facing) with subcommands scan, sync, query
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { Type } from "typebox";
|
|
13
|
+
import { resolveProject, ensureProject } from "./project.ts";
|
|
14
|
+
import { runQuery } from "./query.ts";
|
|
15
|
+
import { scan, sync } from "./indexer.ts";
|
|
16
|
+
import { formatCompact, formatCardBody } from "./format.ts";
|
|
17
|
+
|
|
18
|
+
export default function codewalkerExtension(pi: ExtensionAPI): void {
|
|
19
|
+
// ----------------------------------------------------------------- tool
|
|
20
|
+
pi.registerTool({
|
|
21
|
+
name: "codewalker_query",
|
|
22
|
+
label: "Codewalker Query",
|
|
23
|
+
description:
|
|
24
|
+
"Search the project's code index for symbols (functions, consts, classes, types). " +
|
|
25
|
+
"Returns compact facts (name, kind, file:line, one-line summary) — use this BEFORE grepping/reading files.",
|
|
26
|
+
parameters: Type.Object({
|
|
27
|
+
query: Type.String({ description: "Search text — symbol name or concept keywords." }),
|
|
28
|
+
kind: Type.Optional(Type.String({ description: "Filter: function|const|class|type|method|enum" })),
|
|
29
|
+
limit: Type.Optional(Type.Number({ description: "Max hits (default 10)." })),
|
|
30
|
+
}),
|
|
31
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
32
|
+
const project = resolveProject(ctx.cwd);
|
|
33
|
+
const { rows, staleness } = runQuery(
|
|
34
|
+
project.dbPath,
|
|
35
|
+
{
|
|
36
|
+
query: (params as any).query as string,
|
|
37
|
+
kind: (params as any).kind as string | undefined,
|
|
38
|
+
limit: (params as any).limit as number | undefined,
|
|
39
|
+
},
|
|
40
|
+
project.root,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const text = formatCompact(rows, staleness);
|
|
44
|
+
return {
|
|
45
|
+
content: [{ type: "text" as const, text }],
|
|
46
|
+
details: { rows },
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// ----------------------------------------------------------------- command
|
|
52
|
+
pi.registerCommand("codewalker", {
|
|
53
|
+
description:
|
|
54
|
+
"codewalker: scan | sync | query <text> | help\n" +
|
|
55
|
+
" scan Full (re)build of the code index\n" +
|
|
56
|
+
" sync Git-anchored incremental update\n" +
|
|
57
|
+
" query <text> Search the index for symbols\n" +
|
|
58
|
+
" help Show this help",
|
|
59
|
+
handler: async (args, ctx) => {
|
|
60
|
+
const tokens = (args ?? "").trim().split(/\s+/).filter(Boolean);
|
|
61
|
+
const sub = tokens[0] ?? "help";
|
|
62
|
+
const notify = (msg: string, level: "info" | "error" = "info") => {
|
|
63
|
+
if ((ctx as any).hasUI) (ctx as any).ui.notify(msg, level);
|
|
64
|
+
else console.log(msg);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const project = await ensureProject(ctx.cwd);
|
|
69
|
+
|
|
70
|
+
switch (sub) {
|
|
71
|
+
case "scan": {
|
|
72
|
+
notify("Starting codewalker full scan…");
|
|
73
|
+
await scan({
|
|
74
|
+
projectRoot: project.root,
|
|
75
|
+
globalCodewalkerDir: project.codewalkerDir,
|
|
76
|
+
dbPath: project.dbPath,
|
|
77
|
+
entriesDir: project.entriesDir,
|
|
78
|
+
symbolsDir: project.symbolsDir,
|
|
79
|
+
});
|
|
80
|
+
notify("Codewalker scan complete.");
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case "sync": {
|
|
85
|
+
notify("Starting codewalker incremental sync…");
|
|
86
|
+
await sync({
|
|
87
|
+
projectRoot: project.root,
|
|
88
|
+
globalCodewalkerDir: project.codewalkerDir,
|
|
89
|
+
dbPath: project.dbPath,
|
|
90
|
+
entriesDir: project.entriesDir,
|
|
91
|
+
symbolsDir: project.symbolsDir,
|
|
92
|
+
});
|
|
93
|
+
notify("Codewalker sync complete.");
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
case "query": {
|
|
98
|
+
const q = tokens.slice(1).join(" ");
|
|
99
|
+
if (!q) {
|
|
100
|
+
notify("Usage: /codewalker query <text>", "error");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const { rows, staleness } = runQuery(project.dbPath, { query: q, limit: 10 }, project.root);
|
|
104
|
+
const text = formatCompact(rows, staleness);
|
|
105
|
+
notify(text);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
default: {
|
|
110
|
+
notify(
|
|
111
|
+
"codewalker: scan | sync | query <text> | help\n" +
|
|
112
|
+
" scan Full (re)build of the code index\n" +
|
|
113
|
+
" sync Git-anchored incremental update\n" +
|
|
114
|
+
" query <text> Search the index for symbols\n" +
|
|
115
|
+
" help Show this help",
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} catch (e: any) {
|
|
120
|
+
notify(`codewalker error: ${e?.message ?? String(e)}`, "error");
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import * as os from 'node:os';
|
|
5
|
+
import { execSync } from 'node:child_process';
|
|
6
|
+
import { scan, rebuildDbFromCards } from './indexer.ts';
|
|
7
|
+
import { openDb, getMeta, searchSymbols } from './db.ts';
|
|
8
|
+
|
|
9
|
+
describe('indexer.ts', () => {
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
let globalDir: string;
|
|
12
|
+
let cardsDir: string;
|
|
13
|
+
let symbolsDir: string;
|
|
14
|
+
let dbPath: string;
|
|
15
|
+
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cw-indexer-'));
|
|
18
|
+
globalDir = path.join(tmpDir, '.pi-global', 'projects', 'test-project', 'codewalker');
|
|
19
|
+
cardsDir = path.join(globalDir, 'entries');
|
|
20
|
+
symbolsDir = path.join(cardsDir, 'symbols');
|
|
21
|
+
dbPath = path.join(globalDir, 'index.db');
|
|
22
|
+
fs.mkdirSync(symbolsDir, { recursive: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function writeFixture(name: string, content: string): string {
|
|
30
|
+
const p = path.join(tmpDir, name);
|
|
31
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
32
|
+
fs.writeFileSync(p, content, 'utf-8');
|
|
33
|
+
return p;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it('scan indexes TS files using regex fallback when ctags is unavailable', async () => {
|
|
37
|
+
// Write fixture files
|
|
38
|
+
writeFixture('src/hello.ts', 'export function hello(name: string): string { return "hi"; }');
|
|
39
|
+
writeFixture('src/math.ts', 'export const PI = 3.14;\nfunction internal() {}');
|
|
40
|
+
|
|
41
|
+
await scan({
|
|
42
|
+
projectRoot: tmpDir,
|
|
43
|
+
globalCodewalkerDir: globalDir,
|
|
44
|
+
dbPath,
|
|
45
|
+
entriesDir: cardsDir,
|
|
46
|
+
symbolsDir,
|
|
47
|
+
useCtags: false, // force regex fallback
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Check DB was populated
|
|
51
|
+
const db = openDb(dbPath);
|
|
52
|
+
expect(getMeta(db, 'last_full_scan')).toBeTruthy();
|
|
53
|
+
|
|
54
|
+
// Check symbols are findable
|
|
55
|
+
const hello = searchSymbols(db, 'hello', undefined, 10);
|
|
56
|
+
expect(hello.length).toBeGreaterThanOrEqual(1);
|
|
57
|
+
expect(hello.some(s => s.name === 'hello')).toBe(true);
|
|
58
|
+
|
|
59
|
+
// Check cards were written
|
|
60
|
+
const cardFiles = fs.readdirSync(symbolsDir, { recursive: true }).filter(f => String(f).endsWith('.md'));
|
|
61
|
+
expect(cardFiles.length).toBeGreaterThan(0);
|
|
62
|
+
|
|
63
|
+
db.close();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('scan is idempotent — running twice produces no duplicates', async () => {
|
|
67
|
+
writeFixture('src/foo.ts', 'function foo() {}');
|
|
68
|
+
|
|
69
|
+
await scan({
|
|
70
|
+
projectRoot: tmpDir,
|
|
71
|
+
globalCodewalkerDir: globalDir,
|
|
72
|
+
dbPath,
|
|
73
|
+
entriesDir: cardsDir,
|
|
74
|
+
symbolsDir,
|
|
75
|
+
useCtags: false,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
await scan({
|
|
79
|
+
projectRoot: tmpDir,
|
|
80
|
+
globalCodewalkerDir: globalDir,
|
|
81
|
+
dbPath,
|
|
82
|
+
entriesDir: cardsDir,
|
|
83
|
+
symbolsDir,
|
|
84
|
+
useCtags: false,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const db = openDb(dbPath);
|
|
88
|
+
const results = searchSymbols(db, '', undefined, 100);
|
|
89
|
+
const fooCount = results.filter(s => s.name === 'foo').length;
|
|
90
|
+
expect(fooCount).toBe(1);
|
|
91
|
+
db.close();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('rebuildDbFromCards reproduces the DB from cards alone', async () => {
|
|
95
|
+
writeFixture('src/bar.ts', 'function bar() {}');
|
|
96
|
+
|
|
97
|
+
await scan({
|
|
98
|
+
projectRoot: tmpDir,
|
|
99
|
+
globalCodewalkerDir: globalDir,
|
|
100
|
+
dbPath,
|
|
101
|
+
entriesDir: cardsDir,
|
|
102
|
+
symbolsDir,
|
|
103
|
+
useCtags: false,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Delete the DB
|
|
107
|
+
fs.rmSync(dbPath);
|
|
108
|
+
if (fs.existsSync(dbPath + '-wal')) fs.rmSync(dbPath + '-wal');
|
|
109
|
+
|
|
110
|
+
// Rebuild from cards
|
|
111
|
+
rebuildDbFromCards(dbPath, cardsDir);
|
|
112
|
+
|
|
113
|
+
// Check it works
|
|
114
|
+
const db = openDb(dbPath);
|
|
115
|
+
const bar = searchSymbols(db, 'bar', undefined, 10);
|
|
116
|
+
expect(bar.length).toBeGreaterThanOrEqual(1);
|
|
117
|
+
db.close();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('scan with ctags absent still indexes TS/JS via regex', async () => {
|
|
121
|
+
writeFixture('src/test.ts', 'type Result = string;\ninterface Props { x: number; }');
|
|
122
|
+
writeFixture('src/util.js', 'function helper() { return 1; }');
|
|
123
|
+
|
|
124
|
+
await scan({
|
|
125
|
+
projectRoot: tmpDir,
|
|
126
|
+
globalCodewalkerDir: globalDir,
|
|
127
|
+
dbPath,
|
|
128
|
+
entriesDir: cardsDir,
|
|
129
|
+
symbolsDir,
|
|
130
|
+
useCtags: false,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const db = openDb(dbPath);
|
|
134
|
+
const all = searchSymbols(db, '', undefined, 100);
|
|
135
|
+
expect(all.length).toBeGreaterThanOrEqual(3);
|
|
136
|
+
db.close();
|
|
137
|
+
});
|
|
138
|
+
});
|