@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.
Files changed (46) hide show
  1. package/README.md +44 -50
  2. package/index.ts +6 -42
  3. package/package.json +20 -39
  4. package/prompts/codewalker.md +7 -0
  5. package/skills/codewalker/SKILL.md +43 -0
  6. package/src/cards.test.ts +88 -0
  7. package/src/cards.ts +87 -0
  8. package/src/db.test.ts +343 -0
  9. package/src/db.ts +363 -0
  10. package/src/extract/ctags-parse.test.ts +108 -0
  11. package/src/extract/ctags-parse.ts +112 -0
  12. package/src/extract/ctags.ts +51 -0
  13. package/src/extract/docs.test.ts +81 -0
  14. package/src/extract/docs.ts +169 -0
  15. package/src/extract/regex.test.ts +202 -0
  16. package/src/extract/regex.ts +192 -0
  17. package/src/format.test.ts +123 -0
  18. package/src/format.ts +69 -0
  19. package/src/git.test.ts +75 -0
  20. package/src/git.ts +62 -0
  21. package/src/index.contract.test.ts +145 -0
  22. package/src/index.ts +173 -0
  23. package/src/indexer.test.ts +138 -0
  24. package/src/indexer.ts +352 -0
  25. package/src/libs/cards.test.ts +86 -0
  26. package/src/libs/cards.ts +53 -0
  27. package/src/libs/dts.test.ts +269 -0
  28. package/src/libs/dts.ts +213 -0
  29. package/src/libs/indexer.test.ts +236 -0
  30. package/src/libs/indexer.ts +291 -0
  31. package/src/libs/resolve.test.ts +218 -0
  32. package/src/libs/resolve.ts +120 -0
  33. package/src/project.test.ts +115 -0
  34. package/src/project.ts +206 -0
  35. package/src/query.test.ts +169 -0
  36. package/src/query.ts +89 -0
  37. package/src/sync.test.ts +116 -0
  38. package/src/types.ts +117 -0
  39. package/vitest.config.ts +28 -0
  40. package/LICENSE +0 -21
  41. package/agents.ts +0 -126
  42. package/compat.ts +0 -217
  43. package/detect.ts +0 -188
  44. package/docs/PRD.md +0 -78
  45. package/prd.ts +0 -106
  46. package/skills/learn-this/SKILL.md +0 -325
@@ -0,0 +1,236 @@
1
+ /**
2
+ * Tests for libs/indexer.ts — library indexer.
3
+ *
4
+ * Integration tests using a fixture project with a fake node_modules.
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import * as os from "node:os";
11
+ import { indexLibraries, rebuildLibDbFromCards } from "./indexer.ts";
12
+ import { openDb, searchLibSymbols, getMeta } from "../db.ts";
13
+
14
+ describe("indexLibraries", () => {
15
+ let tmpDir: string;
16
+ let projectRoot: string;
17
+ let libsDir: string;
18
+ let dbPath: string;
19
+ let nodeModulesDir: string;
20
+
21
+ beforeEach(() => {
22
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cw-idx-"));
23
+ projectRoot = path.join(tmpDir, "project");
24
+ libsDir = path.join(tmpDir, "codewalker", "entries", "libs");
25
+ dbPath = path.join(tmpDir, "codewalker", "index.db");
26
+ nodeModulesDir = path.join(projectRoot, "node_modules");
27
+
28
+ fs.mkdirSync(nodeModulesDir, { recursive: true });
29
+ fs.mkdirSync(libsDir, { recursive: true });
30
+
31
+ // Write a project package.json
32
+ fs.writeFileSync(
33
+ path.join(projectRoot, "package.json"),
34
+ JSON.stringify({
35
+ name: "test-project",
36
+ version: "1.0.0",
37
+ dependencies: {
38
+ "typed-pkg": "^1.0.0",
39
+ "no-dts-pkg": "^2.0.0",
40
+ },
41
+ }),
42
+ );
43
+ });
44
+
45
+ afterEach(() => {
46
+ fs.rmSync(tmpDir, { recursive: true, force: true });
47
+ });
48
+
49
+ function installTypedPkg(): void {
50
+ const pkgDir = path.join(nodeModulesDir, "typed-pkg");
51
+ fs.mkdirSync(path.join(pkgDir, "dist"), { recursive: true });
52
+ fs.writeFileSync(
53
+ path.join(pkgDir, "package.json"),
54
+ JSON.stringify({ name: "typed-pkg", version: "1.2.0", types: "dist/index.d.ts" }),
55
+ );
56
+ fs.writeFileSync(
57
+ path.join(pkgDir, "dist", "index.d.ts"),
58
+ [
59
+ "/** A typed greeting function. */",
60
+ "export declare function greet(name: string): string;",
61
+ "/** Configuration options. */",
62
+ "export interface Config { port: number; }",
63
+ "export const VERSION: string;",
64
+ ].join("\n"),
65
+ );
66
+ fs.writeFileSync(pkgDir + "/README.md", "# typed-pkg\nA typed package for testing.\n");
67
+ }
68
+
69
+ function installNoDtsPkg(): void {
70
+ const pkgDir = path.join(nodeModulesDir, "no-dts-pkg");
71
+ fs.mkdirSync(pkgDir);
72
+ fs.writeFileSync(
73
+ path.join(pkgDir, "package.json"),
74
+ JSON.stringify({ name: "no-dts-pkg", version: "2.1.0", main: "index.js" }),
75
+ );
76
+ fs.writeFileSync(path.join(pkgDir, "index.js"), "module.exports = {};\n");
77
+ fs.writeFileSync(path.join(pkgDir, "README.md"), "# no-dts-pkg\nA JS-only package.\n");
78
+ }
79
+
80
+ it("indexes typed dependency: writes cards and populates DB", async () => {
81
+ installTypedPkg();
82
+
83
+ const result = await indexLibraries({ projectRoot, libsDir, dbPath });
84
+ expect(result.indexed).toBe(1);
85
+ expect(result.symbols).toBe(3);
86
+ expect(result.errors).toBe(0);
87
+
88
+ // Check cards exist
89
+ const pkgCardDir = path.join(libsDir, "typed-pkg@1.2.0");
90
+ expect(fs.existsSync(pkgCardDir)).toBe(true);
91
+ const cardFiles = fs.readdirSync(pkgCardDir);
92
+ // greet, Config, VERSION
93
+ expect(cardFiles).toHaveLength(3);
94
+
95
+ // Check DB
96
+ const db = openDb(dbPath);
97
+ const symbols = searchLibSymbols(db, "", undefined, 10);
98
+ expect(symbols).toHaveLength(3);
99
+ expect(symbols.map(s => s.name)).toContain("greet");
100
+ expect(symbols.map(s => s.name)).toContain("Config");
101
+ expect(symbols.map(s => s.name)).toContain("VERSION");
102
+ expect(symbols[0]!.lib).toBe("typed-pkg");
103
+ expect(symbols[0]!.version).toBe("1.2.0");
104
+ db.close();
105
+ });
106
+
107
+ it("indexes a README-only dependency (no .d.ts)", async () => {
108
+ installNoDtsPkg();
109
+
110
+ const result = await indexLibraries({ projectRoot, libsDir, dbPath });
111
+ // Only no-dts-pkg is installed; typed-pkg missing from node_modules so skipped
112
+ expect(result.indexed).toBe(1);
113
+ expect(result.symbols).toBeGreaterThanOrEqual(1); // at least the module card
114
+ expect(result.errors).toBe(0);
115
+
116
+ // Check README-only package got a module card
117
+ const pkgCardDir = path.join(libsDir, "no-dts-pkg@2.1.0");
118
+ expect(fs.existsSync(pkgCardDir)).toBe(true);
119
+ const cardFiles = fs.readdirSync(pkgCardDir);
120
+ // Should have a module card (README summary)
121
+ expect(cardFiles.length).toBeGreaterThanOrEqual(1);
122
+ });
123
+
124
+ it("is idempotent: re-running produces no duplicates", async () => {
125
+ installTypedPkg();
126
+
127
+ await indexLibraries({ projectRoot, libsDir, dbPath });
128
+ await indexLibraries({ projectRoot, libsDir, dbPath });
129
+
130
+ const db = openDb(dbPath);
131
+ const symbols = searchLibSymbols(db, "", undefined, 10);
132
+ expect(symbols).toHaveLength(3);
133
+ db.close();
134
+ });
135
+
136
+ it("handles missing node_modules gracefully", async () => {
137
+ // Remove node_modules
138
+ fs.rmSync(nodeModulesDir, { recursive: true, force: true });
139
+
140
+ const result = await indexLibraries({ projectRoot, libsDir, dbPath });
141
+ expect(result.indexed).toBe(0);
142
+ expect(result.symbols).toBe(0);
143
+ expect(result.errors).toBe(0);
144
+ });
145
+
146
+ it("handles a dep that is missing from node_modules with a logged note", async () => {
147
+ // Only install one of the two deps
148
+ installTypedPkg();
149
+ // no-dts-pkg is missing from node_modules
150
+
151
+ const result = await indexLibraries({ projectRoot, libsDir, dbPath });
152
+ // Should index typed-pkg, skip no-dts-pkg
153
+ expect(result.indexed).toBe(1);
154
+ expect(result.errors).toBe(0);
155
+ });
156
+
157
+ it("includes devDependencies when includeDev=true", async () => {
158
+ // Add a dev dependency
159
+ const pkgJson = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf-8"));
160
+ pkgJson.devDependencies = { "dev-pkg": "^3.0.0" };
161
+ fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify(pkgJson));
162
+
163
+ // Install dev-pkg (typed)
164
+ const devPkgDir = path.join(nodeModulesDir, "dev-pkg");
165
+ fs.mkdirSync(devPkgDir);
166
+ fs.writeFileSync(
167
+ path.join(devPkgDir, "package.json"),
168
+ JSON.stringify({ name: "dev-pkg", version: "3.0.0" }),
169
+ );
170
+ fs.writeFileSync(path.join(devPkgDir, "index.d.ts"), "export const devVar: number;\n");
171
+
172
+ // Install other deps too
173
+ installTypedPkg();
174
+
175
+ const result = await indexLibraries({ projectRoot, libsDir, dbPath, includeDev: true });
176
+ // typed-pkg + dev-pkg (no-dts-pkg missing from node_modules so skipped silently)
177
+ expect(result.indexed).toBe(2);
178
+ expect(result.symbols).toBeGreaterThanOrEqual(4); // 3 from typed-pkg + 1 from dev-pkg
179
+ });
180
+
181
+ it("rebuildLibDbFromCards repopulates lib_symbols from cards", async () => {
182
+ installTypedPkg();
183
+ await indexLibraries({ projectRoot, libsDir, dbPath });
184
+
185
+ // Delete the DB and recreate from cards
186
+ const oldDb = openDb(dbPath);
187
+ oldDb.exec("DELETE FROM lib_symbols; DELETE FROM lib_symbols_fts; DELETE FROM libraries;");
188
+ oldDb.close();
189
+
190
+ rebuildLibDbFromCards(dbPath, libsDir);
191
+
192
+ const db = openDb(dbPath);
193
+ const symbols = searchLibSymbols(db, "", undefined, 10);
194
+ expect(symbols).toHaveLength(3);
195
+ db.close();
196
+ });
197
+
198
+ it("version bump prunes old cards+rows and adds new", async () => {
199
+ // Install v1
200
+ const pkgDir = path.join(nodeModulesDir, "typed-pkg");
201
+ fs.mkdirSync(path.join(pkgDir, "dist"), { recursive: true });
202
+ fs.writeFileSync(
203
+ path.join(pkgDir, "package.json"),
204
+ JSON.stringify({ name: "typed-pkg", version: "1.0.0", types: "dist/index.d.ts" }),
205
+ );
206
+ fs.writeFileSync(path.join(pkgDir, "dist", "index.d.ts"), "export const OLD: number;\n");
207
+
208
+ await indexLibraries({ projectRoot, libsDir, dbPath });
209
+
210
+ // Check v1 card exists
211
+ expect(fs.existsSync(path.join(libsDir, "typed-pkg@1.0.0"))).toBe(true);
212
+
213
+ // Now "upgrade" to v2
214
+ fs.writeFileSync(
215
+ path.join(pkgDir, "package.json"),
216
+ JSON.stringify({ name: "typed-pkg", version: "2.0.0", types: "dist/index.d.ts" }),
217
+ );
218
+ fs.writeFileSync(path.join(pkgDir, "dist", "index.d.ts"), "export const NEW: number;\n");
219
+
220
+ await indexLibraries({ projectRoot, libsDir, dbPath });
221
+
222
+ // Old card dir should be gone
223
+ expect(fs.existsSync(path.join(libsDir, "typed-pkg@1.0.0"))).toBe(false);
224
+
225
+ // New card dir exists
226
+ expect(fs.existsSync(path.join(libsDir, "typed-pkg@2.0.0"))).toBe(true);
227
+
228
+ // DB has new symbols only
229
+ const db = openDb(dbPath);
230
+ const symbols = searchLibSymbols(db, "", undefined, 10);
231
+ expect(symbols).toHaveLength(1);
232
+ expect(symbols[0]!.name).toBe("NEW");
233
+ expect(symbols[0]!.version).toBe("2.0.0");
234
+ db.close();
235
+ });
236
+ });
@@ -0,0 +1,291 @@
1
+ /**
2
+ * Library indexer for codewalker v1.2.
3
+ *
4
+ * Orchestrates discovery → extraction → card writing → DB population
5
+ * for third-party library dependencies installed in node_modules.
6
+ *
7
+ * - `indexLibraries()`: full pipeline — idempotent
8
+ * - `rebuildLibDbFromCards()`: disposable-index rebuild
9
+ *
10
+ * Uses the same atomic write pattern (tmp + rename, 0o600) as the v1.1 indexer.
11
+ */
12
+
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import { openDb, upsertLibrary, upsertLibSymbol, deleteLibrary, setMeta } from "../db.ts";
16
+ import { locateLibrary } from "./resolve.ts";
17
+ import { extractDtsSymbols } from "./dts.ts";
18
+ import { renderLibCard } from "./cards.ts";
19
+ import { parseCard } from "../cards.ts";
20
+ import type { LibSymbol } from "../types.ts";
21
+
22
+ export interface IndexLibrariesOptions {
23
+ projectRoot: string;
24
+ libsDir: string;
25
+ dbPath: string;
26
+ includeDev?: boolean;
27
+ }
28
+
29
+ export interface IndexResult {
30
+ indexed: number; // library count indexed
31
+ symbols: number; // total symbols extracted
32
+ errors: number; // libraries that failed
33
+ }
34
+
35
+ /**
36
+ * Full library index pipeline:
37
+ * 1. Read project package.json → dependency names
38
+ * 2. For each dep, locate installed package (version, .d.ts, README)
39
+ * 3. Extract symbols from .d.ts
40
+ * 4. Write cards under entries/libs/<pkg>@<version>/
41
+ * 5. Populate libraries + lib_symbols tables
42
+ *
43
+ * Idempotent: version changes prune old data; re-running with no changes is stable.
44
+ */
45
+ export async function indexLibraries(options: IndexLibrariesOptions): Promise<IndexResult> {
46
+ const { projectRoot, libsDir, dbPath, includeDev } = options;
47
+
48
+ // Read project package.json
49
+ const pkgJsonPath = path.join(projectRoot, "package.json");
50
+ if (!fs.existsSync(pkgJsonPath)) {
51
+ return { indexed: 0, symbols: 0, errors: 0 };
52
+ }
53
+
54
+ let pkgJson: Record<string, any>;
55
+ try {
56
+ pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
57
+ } catch {
58
+ return { indexed: 0, symbols: 0, errors: 0 };
59
+ }
60
+
61
+ const depNames = parseDependenciesFromPkg(pkgJson, includeDev ?? false);
62
+ if (depNames.length === 0) {
63
+ return { indexed: 0, symbols: 0, errors: 0 };
64
+ }
65
+
66
+ const db = openDb(dbPath);
67
+ let indexedCount = 0;
68
+ let symbolCount = 0;
69
+ let errorCount = 0;
70
+
71
+ try {
72
+ for (const name of depNames) {
73
+ const lib = locateLibrary(projectRoot, name);
74
+ if (!lib) {
75
+ // Dep missing from node_modules — skip silently
76
+ continue;
77
+ }
78
+
79
+ const version = lib.version;
80
+ const libDirName = `${name}@${version}`;
81
+ const libCardDir = path.join(libsDir, libDirName);
82
+
83
+ // Check if this exact version is already indexed (idempotency check)
84
+ if (fs.existsSync(libCardDir) && isVersionIndexed(db, name, version)) {
85
+ // Already up-to-date — skip
86
+ indexedCount++;
87
+ continue;
88
+ }
89
+
90
+ // Prune old version if the version changed
91
+ pruneOldVersion(db, libsDir, name, version);
92
+
93
+ // Create card directory
94
+ fs.mkdirSync(libCardDir, { recursive: true });
95
+
96
+ // Upsert library record
97
+ upsertLibrary(db, {
98
+ name,
99
+ version,
100
+ source: "node_modules",
101
+ dts_path: lib.dtsPath,
102
+ readme: lib.readmePath ? readFirstLines(lib.readmePath, 5) : null,
103
+ });
104
+
105
+ const symbols: LibSymbol[] = [];
106
+
107
+ // Extract from .d.ts if available
108
+ if (lib.dtsPath) {
109
+ try {
110
+ const source = fs.readFileSync(lib.dtsPath, "utf-8");
111
+ const extracted = extractDtsSymbols(source, name, version);
112
+ symbols.push(...extracted);
113
+ } catch {
114
+ // d.ts parse error — skip silently
115
+ }
116
+ }
117
+
118
+ // If no .d.ts symbols and we have README, create a module overview card
119
+ if (symbols.length === 0 && lib.readmePath) {
120
+ const readmeText = fs.readFileSync(lib.readmePath, "utf-8");
121
+ const summary = readmeText.split("\n").slice(0, 5).join("\n").trim() || `${name} library`;
122
+ symbols.push({
123
+ lib: name,
124
+ version,
125
+ name,
126
+ kind: "module",
127
+ signature: "",
128
+ doc: readmeText.slice(0, 2000),
129
+ summary,
130
+ card_path: "",
131
+ });
132
+ }
133
+
134
+ // Write cards and insert symbols
135
+ for (const sym of symbols) {
136
+ const cardFileName = `${sanitizeName(sym.name)}.md`;
137
+ const cardPath = path.join(libCardDir, cardFileName);
138
+
139
+ const card = renderLibCard(sym);
140
+ const tmpPath = cardPath + ".tmp";
141
+ fs.writeFileSync(tmpPath, card, { encoding: "utf-8", mode: 0o600 });
142
+ fs.renameSync(tmpPath, cardPath);
143
+
144
+ sym.card_path = cardPath;
145
+ upsertLibSymbol(db, sym);
146
+ }
147
+
148
+ indexedCount++;
149
+ symbolCount += symbols.length;
150
+ }
151
+
152
+ setMeta(db, "last_libs_index", new Date().toISOString());
153
+ } catch (e) {
154
+ errorCount++;
155
+ } finally {
156
+ db.close();
157
+ }
158
+
159
+ return { indexed: indexedCount, symbols: symbolCount, errors: errorCount };
160
+ }
161
+
162
+ /**
163
+ * Rebuild the lib_symbols DB tables from cards alone.
164
+ * Used for the disposable-index property.
165
+ */
166
+ export function rebuildLibDbFromCards(
167
+ dbPath: string,
168
+ libsDir: string,
169
+ ): void {
170
+ if (!fs.existsSync(libsDir)) return;
171
+
172
+ const db = openDb(dbPath);
173
+
174
+ try {
175
+ db.exec("BEGIN TRANSACTION");
176
+
177
+ // Walk all <pkg>@<version>/ directories
178
+ for (const libDir of fs.readdirSync(libsDir, { withFileTypes: true })) {
179
+ if (!libDir.isDirectory()) continue;
180
+
181
+ const fullLibDir = path.join(libsDir, libDir.name);
182
+ const atIndex = libDir.name.lastIndexOf("@");
183
+ if (atIndex < 0) continue;
184
+
185
+ const libName = libDir.name.slice(0, atIndex);
186
+ const version = libDir.name.slice(atIndex + 1);
187
+
188
+ // Upsert library record
189
+ upsertLibrary(db, { name: libName, version, source: "node_modules", dts_path: null, readme: null });
190
+
191
+ // Walk .md cards
192
+ for (const entry of fs.readdirSync(fullLibDir, { withFileTypes: true })) {
193
+ if (!entry.isFile() || !entry.name.endsWith(".md")) continue;
194
+
195
+ const cardPath = path.join(fullLibDir, entry.name);
196
+ const cardText = fs.readFileSync(cardPath, "utf-8");
197
+ const parsed = parseCard(cardText);
198
+ if (!parsed) continue;
199
+
200
+ const { head } = parsed;
201
+
202
+ upsertLibSymbol(db, {
203
+ lib: libName,
204
+ version,
205
+ name: head.name,
206
+ kind: head.kind,
207
+ signature: head.signature,
208
+ doc: parsed.body,
209
+ summary: head.summary,
210
+ card_path: cardPath,
211
+ });
212
+ }
213
+ }
214
+
215
+ db.exec("COMMIT");
216
+ } catch (e) {
217
+ db.exec("ROLLBACK");
218
+ throw e;
219
+ } finally {
220
+ db.close();
221
+ }
222
+ }
223
+
224
+ // ── Internal helpers ───────────────────────────────────────────
225
+
226
+ function parseDependenciesFromPkg(
227
+ pkgJson: Record<string, any>,
228
+ includeDev: boolean,
229
+ ): string[] {
230
+ const deps: string[] = [];
231
+ if (pkgJson.dependencies) {
232
+ deps.push(...Object.keys(pkgJson.dependencies));
233
+ }
234
+ if (includeDev && pkgJson.devDependencies) {
235
+ deps.push(...Object.keys(pkgJson.devDependencies));
236
+ }
237
+ return deps;
238
+ }
239
+
240
+ /** Check if a library+version is already in the DB (idempotent guard). */
241
+ function isVersionIndexed(db: any, libName: string, version: string): boolean {
242
+ try {
243
+ const row = db.prepare(
244
+ "SELECT 1 FROM libraries WHERE name = ? AND version = ?",
245
+ ).get(libName, version);
246
+ return !!row;
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+
252
+ /** Remove old card dir and DB rows for a library when version changes. */
253
+ function pruneOldVersion(
254
+ db: any,
255
+ libsDir: string,
256
+ libName: string,
257
+ newVersion: string,
258
+ ): void {
259
+ // Find any existing card dir for this lib with a different version
260
+ if (!fs.existsSync(libsDir)) return;
261
+
262
+ for (const dir of fs.readdirSync(libsDir, { withFileTypes: true })) {
263
+ if (!dir.isDirectory()) continue;
264
+ const prefix = `${libName}@`;
265
+ if (dir.name.startsWith(prefix) && dir.name !== `${libName}@${newVersion}`) {
266
+ // Remove old card dir
267
+ const oldDir = path.join(libsDir, dir.name);
268
+ try {
269
+ fs.rmSync(oldDir, { recursive: true, force: true });
270
+ } catch { /* best effort */ }
271
+ }
272
+ }
273
+
274
+ // Delete old DB rows for this lib (all versions — we'll re-insert with new version)
275
+ try {
276
+ deleteLibrary(db, libName);
277
+ } catch { /* best effort */ }
278
+ }
279
+
280
+ function sanitizeName(name: string): string {
281
+ return name.replace(/[^a-zA-Z0-9_$]/g, "_");
282
+ }
283
+
284
+ function readFirstLines(filePath: string, n: number): string {
285
+ try {
286
+ const content = fs.readFileSync(filePath, "utf-8");
287
+ return content.split("\n").slice(0, n).join("\n").trim();
288
+ } catch {
289
+ return "";
290
+ }
291
+ }