@aprimediet/codewalker 1.0.0 → 1.2.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 +343 -0
- package/src/db.ts +363 -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 +123 -0
- package/src/format.ts +69 -0
- package/src/git.test.ts +75 -0
- package/src/git.ts +62 -0
- package/src/index.contract.test.ts +145 -0
- package/src/index.ts +173 -0
- package/src/indexer.test.ts +138 -0
- package/src/indexer.ts +352 -0
- package/src/libs/cards.test.ts +86 -0
- package/src/libs/cards.ts +53 -0
- package/src/libs/dts.test.ts +269 -0
- package/src/libs/dts.ts +213 -0
- package/src/libs/indexer.test.ts +236 -0
- package/src/libs/indexer.ts +291 -0
- package/src/libs/resolve.test.ts +218 -0
- package/src/libs/resolve.ts +120 -0
- package/src/project.test.ts +115 -0
- package/src/project.ts +206 -0
- package/src/query.test.ts +169 -0
- package/src/query.ts +89 -0
- package/src/sync.test.ts +116 -0
- package/src/types.ts +117 -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.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,145 @@
|
|
|
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 tool has a source parameter
|
|
62
|
+
const toolParams = (queryTool!.parameters as any);
|
|
63
|
+
expect(toolParams.properties).toHaveProperty('source');
|
|
64
|
+
|
|
65
|
+
// Check command registered
|
|
66
|
+
const cmd = stub.commands.find(c => c.name === 'codewalker');
|
|
67
|
+
expect(cmd).toBeDefined();
|
|
68
|
+
expect(cmd!.description).toContain('libs');
|
|
69
|
+
expect(cmd!.description).toContain('lib');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('tool.execute returns { content, details } with compact text content', async () => {
|
|
73
|
+
// Create a temporary project dir so the DB path exists
|
|
74
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cw-contract-'));
|
|
75
|
+
const piDir = path.join(tmpDir, '.pi');
|
|
76
|
+
fs.mkdirSync(piDir, { recursive: true });
|
|
77
|
+
const markerId = 'test-project-contract-' + Math.random().toString(36).slice(2, 8);
|
|
78
|
+
fs.writeFileSync(path.join(piDir, markerId + '.md'), `---\npi-project: true\nid: ${markerId}\n---\n`);
|
|
79
|
+
|
|
80
|
+
// Pre-create the codewalker global dir so the DB can be opened
|
|
81
|
+
const homePi = path.join(os.homedir(), '.pi', 'projects', markerId, 'codewalker');
|
|
82
|
+
fs.mkdirSync(homePi, { recursive: true });
|
|
83
|
+
|
|
84
|
+
const mod = await import('./index.ts');
|
|
85
|
+
const stub = createPiStub();
|
|
86
|
+
|
|
87
|
+
mod.default(stub.api as any);
|
|
88
|
+
|
|
89
|
+
const tool = stub.tools.find(t => t.name === 'codewalker_query')!;
|
|
90
|
+
const result = await tool.execute(
|
|
91
|
+
'test-id',
|
|
92
|
+
{ query: 'test' },
|
|
93
|
+
new AbortController().signal,
|
|
94
|
+
() => {},
|
|
95
|
+
{ cwd: tmpDir },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
expect(result).toHaveProperty('content');
|
|
99
|
+
expect(result).toHaveProperty('details');
|
|
100
|
+
expect(Array.isArray(result.content)).toBe(true);
|
|
101
|
+
expect(result.content[0]!.type).toBe('text');
|
|
102
|
+
|
|
103
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('tool.execute with source="libs" returns valid result even with no lib data', async () => {
|
|
107
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cw-contract-libs-'));
|
|
108
|
+
const piDir = path.join(tmpDir, '.pi');
|
|
109
|
+
fs.mkdirSync(piDir, { recursive: true });
|
|
110
|
+
const markerId = 'test-project-libs-' + Math.random().toString(36).slice(2, 8);
|
|
111
|
+
fs.writeFileSync(path.join(piDir, markerId + '.md'), `---\npi-project: true\nid: ${markerId}\n---\n`);
|
|
112
|
+
|
|
113
|
+
const homePi = path.join(os.homedir(), '.pi', 'projects', markerId, 'codewalker');
|
|
114
|
+
fs.mkdirSync(homePi, { recursive: true });
|
|
115
|
+
|
|
116
|
+
const mod = await import('./index.ts');
|
|
117
|
+
const stub = createPiStub();
|
|
118
|
+
mod.default(stub.api as any);
|
|
119
|
+
|
|
120
|
+
const tool = stub.tools.find(t => t.name === 'codewalker_query')!;
|
|
121
|
+
const result = await tool.execute(
|
|
122
|
+
'test-id',
|
|
123
|
+
{ query: 'test', source: 'libs' },
|
|
124
|
+
new AbortController().signal,
|
|
125
|
+
() => {},
|
|
126
|
+
{ cwd: tmpDir },
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(result).toHaveProperty('content');
|
|
130
|
+
expect(result.content[0]!.text).toContain('No matches');
|
|
131
|
+
expect(result.details.rows).toEqual([]);
|
|
132
|
+
|
|
133
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('command description mentions libs and lib subcommands', async () => {
|
|
137
|
+
const mod = await import('./index.ts');
|
|
138
|
+
const stub = createPiStub();
|
|
139
|
+
mod.default(stub.api as any);
|
|
140
|
+
|
|
141
|
+
const cmd = stub.commands.find(c => c.name === 'codewalker')!;
|
|
142
|
+
expect(cmd.description).toContain('libs [--dev]');
|
|
143
|
+
expect(cmd.description).toContain('lib <pkg>');
|
|
144
|
+
});
|
|
145
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
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) — now with `source` param
|
|
8
|
+
* - `/codewalker` command (human-facing) with subcommands scan, sync, query, libs, lib
|
|
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 { indexLibraries } from "./libs/indexer.ts";
|
|
17
|
+
import { formatCompact } from "./format.ts";
|
|
18
|
+
|
|
19
|
+
export default function codewalkerExtension(pi: ExtensionAPI): void {
|
|
20
|
+
// ----------------------------------------------------------------- tool
|
|
21
|
+
pi.registerTool({
|
|
22
|
+
name: "codewalker_query",
|
|
23
|
+
label: "Codewalker Query",
|
|
24
|
+
description:
|
|
25
|
+
"Search the project's code index for symbols (functions, consts, classes, types). " +
|
|
26
|
+
"Returns compact facts (name, kind, file:line, one-line summary) — use this BEFORE grepping/reading files. " +
|
|
27
|
+
"Optionally search libraries (source='libs') or both (source='all').",
|
|
28
|
+
parameters: Type.Object({
|
|
29
|
+
query: Type.String({ description: "Search text — symbol name or concept keywords." }),
|
|
30
|
+
kind: Type.Optional(Type.String({ description: "Filter: function|const|class|type|method|enum" })),
|
|
31
|
+
limit: Type.Optional(Type.Number({ description: "Max hits (default 10)." })),
|
|
32
|
+
source: Type.Optional(Type.String({ description: "Where to search: code | libs | all (default code)." })),
|
|
33
|
+
}),
|
|
34
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
35
|
+
const p = params as any;
|
|
36
|
+
const project = resolveProject(ctx.cwd);
|
|
37
|
+
const { rows, staleness } = runQuery(
|
|
38
|
+
project.dbPath,
|
|
39
|
+
{
|
|
40
|
+
query: p.query as string,
|
|
41
|
+
kind: p.kind as string | undefined,
|
|
42
|
+
limit: p.limit as number | undefined,
|
|
43
|
+
source: (p.source as "code" | "libs" | "all") ?? "code",
|
|
44
|
+
},
|
|
45
|
+
project.root,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const text = formatCompact(rows, staleness);
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text" as const, text }],
|
|
51
|
+
details: { rows },
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// ----------------------------------------------------------------- command
|
|
57
|
+
pi.registerCommand("codewalker", {
|
|
58
|
+
description:
|
|
59
|
+
"codewalker: scan | sync | query <text> | libs [--dev] | lib <pkg> [query] | help\n" +
|
|
60
|
+
" scan Full (re)build of the code index\n" +
|
|
61
|
+
" sync Git-anchored incremental update\n" +
|
|
62
|
+
" query <text> Search the code index for symbols\n" +
|
|
63
|
+
" libs [--dev] Index all direct dependencies (--dev includes devDependencies)\n" +
|
|
64
|
+
" lib <pkg> [query] Search a specific library's API symbols\n" +
|
|
65
|
+
" help Show this help",
|
|
66
|
+
handler: async (args, ctx) => {
|
|
67
|
+
const tokens = (args ?? "").trim().split(/\s+/).filter(Boolean);
|
|
68
|
+
const sub = tokens[0] ?? "help";
|
|
69
|
+
const notify = (msg: string, level: "info" | "error" = "info") => {
|
|
70
|
+
if ((ctx as any).hasUI) (ctx as any).ui.notify(msg, level);
|
|
71
|
+
else console.log(msg);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const project = await ensureProject(ctx.cwd);
|
|
76
|
+
|
|
77
|
+
switch (sub) {
|
|
78
|
+
case "scan": {
|
|
79
|
+
notify("Starting codewalker full scan…");
|
|
80
|
+
await scan({
|
|
81
|
+
projectRoot: project.root,
|
|
82
|
+
globalCodewalkerDir: project.codewalkerDir,
|
|
83
|
+
dbPath: project.dbPath,
|
|
84
|
+
entriesDir: project.entriesDir,
|
|
85
|
+
symbolsDir: project.symbolsDir,
|
|
86
|
+
});
|
|
87
|
+
notify("Codewalker scan complete.");
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
case "sync": {
|
|
92
|
+
notify("Starting codewalker incremental sync…");
|
|
93
|
+
await sync({
|
|
94
|
+
projectRoot: project.root,
|
|
95
|
+
globalCodewalkerDir: project.codewalkerDir,
|
|
96
|
+
dbPath: project.dbPath,
|
|
97
|
+
entriesDir: project.entriesDir,
|
|
98
|
+
symbolsDir: project.symbolsDir,
|
|
99
|
+
});
|
|
100
|
+
notify("Codewalker sync complete.");
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
case "query": {
|
|
105
|
+
const q = tokens.slice(1).join(" ");
|
|
106
|
+
if (!q) {
|
|
107
|
+
notify("Usage: /codewalker query <text>", "error");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const { rows, staleness } = runQuery(project.dbPath, { query: q, limit: 10 }, project.root);
|
|
111
|
+
const text = formatCompact(rows, staleness);
|
|
112
|
+
notify(text);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case "libs": {
|
|
117
|
+
const includeDev = tokens.includes("--dev");
|
|
118
|
+
notify(`Indexing libraries${includeDev ? " (including devDependencies)" : ""}…`);
|
|
119
|
+
const result = await indexLibraries({
|
|
120
|
+
projectRoot: project.root,
|
|
121
|
+
libsDir: project.libsDir,
|
|
122
|
+
dbPath: project.dbPath,
|
|
123
|
+
includeDev,
|
|
124
|
+
});
|
|
125
|
+
if (result.indexed === 0 && result.symbols === 0) {
|
|
126
|
+
notify("No libraries indexed. Ensure node_modules exists and has dependencies installed.");
|
|
127
|
+
} else {
|
|
128
|
+
notify(`Indexed ${result.indexed} libraries, ${result.symbols} symbols${result.errors > 0 ? ` (${result.errors} errors)` : ""}.`);
|
|
129
|
+
}
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
case "lib": {
|
|
134
|
+
const pkg = tokens[1];
|
|
135
|
+
if (!pkg) {
|
|
136
|
+
notify("Usage: /codewalker lib <pkg> [query]", "error");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const q = tokens.slice(2).join(" ");
|
|
140
|
+
const { rows, staleness } = runQuery(
|
|
141
|
+
project.dbPath,
|
|
142
|
+
{ query: q || "", source: "libs", limit: 20 },
|
|
143
|
+
project.root,
|
|
144
|
+
);
|
|
145
|
+
// Filter to the requested package
|
|
146
|
+
const pkgRows = rows.filter(r => r.lib === pkg || r.name === pkg);
|
|
147
|
+
if (pkgRows.length === 0) {
|
|
148
|
+
notify(`No API symbols found for "${pkg}". Run /codewalker libs first to index libraries.`);
|
|
149
|
+
} else {
|
|
150
|
+
const text = formatCompact(pkgRows, staleness);
|
|
151
|
+
notify(text);
|
|
152
|
+
}
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
default: {
|
|
157
|
+
notify(
|
|
158
|
+
"codewalker: scan | sync | query <text> | libs [--dev] | lib <pkg> [query] | help\n" +
|
|
159
|
+
" scan Full (re)build of the code index\n" +
|
|
160
|
+
" sync Git-anchored incremental update\n" +
|
|
161
|
+
" query <text> Search the code index for symbols\n" +
|
|
162
|
+
" libs [--dev] Index all direct dependencies (--dev includes devDependencies)\n" +
|
|
163
|
+
" lib <pkg> [query] Search a specific library's API symbols\n" +
|
|
164
|
+
" help Show this help",
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (e: any) {
|
|
169
|
+
notify(`codewalker error: ${e?.message ?? String(e)}`, "error");
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
@@ -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
|
+
});
|