codetrap 0.1.2 → 0.1.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.
Files changed (53) hide show
  1. package/.agents/plugins/marketplace.json +20 -0
  2. package/README.md +112 -33
  3. package/docs/installation.md +18 -10
  4. package/package.json +4 -1
  5. package/plugins/codetrap-agent/.codex-plugin/plugin.json +34 -0
  6. package/plugins/codetrap-agent/hooks/post-flight-capture.example.md +25 -0
  7. package/plugins/codetrap-agent/hooks/pre-edit.example.sh +10 -0
  8. package/plugins/codetrap-agent/hooks.json +11 -0
  9. package/plugins/codetrap-agent/skills/codetrap-capture/SKILL.md +19 -0
  10. package/plugins/codetrap-agent/skills/codetrap-check/SKILL.md +14 -0
  11. package/plugins/codetrap-agent/templates/AGENTS.codetrap.md +25 -0
  12. package/scripts/release-preflight.ts +55 -0
  13. package/skills/codetrap-add/SKILL.md +4 -1
  14. package/skills/codetrap-check/SKILL.md +24 -4
  15. package/skills/codetrap-search/SKILL.md +32 -12
  16. package/src/commands/command-result.ts +29 -0
  17. package/src/commands/router.ts +6 -400
  18. package/src/commands/workflow.ts +419 -0
  19. package/src/db/embedding-queries.ts +33 -0
  20. package/src/db/queries.ts +165 -48
  21. package/src/db/repository.ts +72 -15
  22. package/src/db/schema.ts +35 -0
  23. package/src/domain/trap.ts +38 -10
  24. package/src/index.ts +13 -1
  25. package/src/lib/command-requests.ts +133 -0
  26. package/src/lib/config.ts +102 -0
  27. package/src/lib/constants.ts +1 -1
  28. package/src/lib/doctor.ts +86 -0
  29. package/src/lib/embedding-health.ts +49 -0
  30. package/src/lib/embedding-index.ts +53 -0
  31. package/src/lib/format.ts +6 -2
  32. package/src/lib/output-json.ts +141 -0
  33. package/src/lib/scope-context.ts +118 -0
  34. package/src/lib/scope-maintenance.ts +71 -0
  35. package/src/lib/scope-migration.ts +315 -0
  36. package/src/lib/scope-path.ts +99 -0
  37. package/src/lib/scope.ts +16 -11
  38. package/src/lib/search-normalizer.ts +6 -0
  39. package/src/lib/search-policy.ts +365 -0
  40. package/src/lib/search-result-card.ts +2 -7
  41. package/src/lib/search-service.ts +67 -120
  42. package/src/lib/store.ts +129 -108
  43. package/src/lib/trap-archive.ts +9 -42
  44. package/src/lib/trap-codec.ts +113 -0
  45. package/src/lib/trap-json-fields.ts +12 -0
  46. package/src/lib/trap-lifecycle.ts +37 -0
  47. package/src/lib/trap-mutation-result.ts +36 -0
  48. package/src/lib/trap-operations.ts +30 -9
  49. package/src/lib/trap-scope-match.ts +112 -0
  50. package/src/lib/trap-search-document.ts +8 -1
  51. package/src/lib/trap-transfer.ts +88 -0
  52. package/src/mcp/server.ts +77 -72
  53. package/src/mcp/tools.ts +32 -5
@@ -0,0 +1,118 @@
1
+ import { openGlobal, openProject } from "../db/connection";
2
+ import { TrapRepository } from "../db/repository";
3
+ import { CODETRAP_DIR, TRAPS_DB_FILE } from "./constants";
4
+ import type { Scope } from "./constants";
5
+ import type { EmbeddingProvider } from "./embedder";
6
+ import { findProjectRoot, getGlobalDB, resolveScopePath } from "./scope";
7
+ import { ScopePathResolver } from "./scope-path";
8
+
9
+ const scopePath = new ScopePathResolver();
10
+
11
+ export type ScopeContext = {
12
+ cwd: string;
13
+ project_root: string | null;
14
+ project_db: string | null;
15
+ global_db: string;
16
+ };
17
+
18
+ export function createScopeContext(cwd = process.cwd()): ScopeContext {
19
+ const resolvedCwd = resolveScopePath(cwd);
20
+ const projectRoot = findProjectRoot(resolvedCwd);
21
+ return {
22
+ cwd: resolvedCwd,
23
+ project_root: projectRoot,
24
+ project_db: projectRoot ? scopePath.join(projectRoot, CODETRAP_DIR, TRAPS_DB_FILE) : null,
25
+ global_db: getGlobalDB(),
26
+ };
27
+ }
28
+
29
+ export type ScopedRepository = {
30
+ scope: Scope;
31
+ repository: TrapRepository;
32
+ };
33
+
34
+ export class ScopedRepositoryContext {
35
+ private readonly context: ScopeContext;
36
+ private globalRepository?: TrapRepository;
37
+ private projectRepository?: TrapRepository;
38
+
39
+ constructor(cwd = process.cwd(), private readonly embedder?: EmbeddingProvider) {
40
+ this.context = createScopeContext(cwd);
41
+ }
42
+
43
+ hasProject(): boolean {
44
+ return this.context.project_root !== null;
45
+ }
46
+
47
+ projectRoot(): string | null {
48
+ return this.context.project_root;
49
+ }
50
+
51
+ repositoriesForRead(scope?: string): ScopedRepository[] {
52
+ const resolvedScope = optionalScope(scope);
53
+ if (resolvedScope) {
54
+ const entry = this.repositoryEntry(resolvedScope);
55
+ return entry ? [entry] : [];
56
+ }
57
+
58
+ const repositories: ScopedRepository[] = [];
59
+ const project = this.repositoryEntry("project");
60
+ if (project) repositories.push(project);
61
+ repositories.push({ scope: "global", repository: this.globalRepo() });
62
+ return repositories;
63
+ }
64
+
65
+ repositoriesForWrite(scope?: string): ScopedRepository[] {
66
+ return this.repositoriesForRead(scope);
67
+ }
68
+
69
+ repositoryFor(scope: Scope): TrapRepository {
70
+ const entry = this.repositoryEntry(scope);
71
+ if (!entry) {
72
+ throw new Error("Not in a project. Run 'codetrap init' first, or use --scope global.");
73
+ }
74
+ return entry.repository;
75
+ }
76
+
77
+ repositoryEntry(scope: Scope): ScopedRepository | null {
78
+ if (scope === "project") {
79
+ return this.context.project_root ? { scope, repository: this.projectRepo() } : null;
80
+ }
81
+ return { scope, repository: this.globalRepo() };
82
+ }
83
+
84
+ private projectRepo(): TrapRepository {
85
+ const projectRoot = this.context.project_root;
86
+ if (!projectRoot) {
87
+ throw new Error("Not in a project. Run 'codetrap init' first, or use --scope global.");
88
+ }
89
+ if (!this.projectRepository) {
90
+ this.projectRepository = new TrapRepository(openProject(projectRoot), this.embedder);
91
+ }
92
+ return this.projectRepository;
93
+ }
94
+
95
+ private globalRepo(): TrapRepository {
96
+ if (!this.globalRepository) {
97
+ this.globalRepository = new TrapRepository(openGlobal(), this.embedder);
98
+ }
99
+ return this.globalRepository;
100
+ }
101
+ }
102
+
103
+ export function normalizeScope(scope: string): Scope {
104
+ if (scope === "project" || scope === "global") return scope;
105
+ throw new Error(`Invalid scope: ${scope}`);
106
+ }
107
+
108
+ export function optionalScope(scope?: string): Scope | null {
109
+ if (!scope) return null;
110
+ return normalizeScope(scope);
111
+ }
112
+
113
+ export function storeForScopeContext<T extends { forCwd(cwd: string): T }>(store: T, cwd?: unknown): T {
114
+ if (typeof cwd === "string" && cwd.trim() !== "") {
115
+ return store.forCwd(cwd);
116
+ }
117
+ return store;
118
+ }
@@ -0,0 +1,71 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { TrapRepository } from "../db/repository";
5
+ import { CODETRAP_DIR, TRAPS_DB_FILE } from "./constants";
6
+ import { ScopePathResolver } from "./scope-path";
7
+
8
+ const scopePath = new ScopePathResolver();
9
+
10
+ export type ScopeMaintenanceCommand = "repair-scope" | "migrate-project";
11
+
12
+ export type ScopeMaintenancePaths = {
13
+ command: ScopeMaintenanceCommand;
14
+ fromProjectPath: string;
15
+ toProjectPath: string;
16
+ sourceDb: string;
17
+ destinationDb: string;
18
+ };
19
+
20
+ export function buildScopeMaintenancePaths(input: {
21
+ command: ScopeMaintenanceCommand;
22
+ fromProjectPath: string;
23
+ toProjectPath: string;
24
+ }): ScopeMaintenancePaths {
25
+ return {
26
+ command: input.command,
27
+ fromProjectPath: input.fromProjectPath,
28
+ toProjectPath: input.toProjectPath,
29
+ sourceDb: projectDbPath(input.fromProjectPath),
30
+ destinationDb: projectDbPath(input.toProjectPath),
31
+ };
32
+ }
33
+
34
+ export function validateScopeMaintenancePaths(input: ScopeMaintenancePaths): void {
35
+ if (input.command === "migrate-project" && scopePath.same(input.fromProjectPath, input.toProjectPath)) {
36
+ throw new Error("--from-project-path and --to-project-path must be different.");
37
+ }
38
+ if (scopePath.same(input.sourceDb, input.destinationDb)) {
39
+ throw new Error("Source and destination database paths are the same.");
40
+ }
41
+ if (!existsSync(scopePath.join(input.toProjectPath, CODETRAP_DIR))) {
42
+ throw new Error(`Destination project is not initialized: ${input.toProjectPath}. Run 'codetrap init' first.`);
43
+ }
44
+ }
45
+
46
+ export function projectDbPath(projectPath: string): string {
47
+ return scopePath.join(projectPath, CODETRAP_DIR, TRAPS_DB_FILE);
48
+ }
49
+
50
+ export function withReadOnlyScopeRepository<T>(dbPath: string, callback: (repository: TrapRepository) => T): T {
51
+ const db = new Database(dbPath, { readonly: true });
52
+ try {
53
+ return callback(new TrapRepository(db));
54
+ } finally {
55
+ db.close();
56
+ }
57
+ }
58
+
59
+ export function backupScopeDatabase(dbPath: string, label: string): string {
60
+ const backupDir = join(dirname(dbPath), "backups");
61
+ mkdirSync(backupDir, { recursive: true });
62
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
63
+ const backupPath = join(backupDir, `${basename(dbPath)}.${label}.${timestamp}.backup`);
64
+ const db = new Database(dbPath, { readonly: true });
65
+ try {
66
+ writeFileSync(backupPath, db.serialize());
67
+ } finally {
68
+ db.close();
69
+ }
70
+ return backupPath;
71
+ }
@@ -0,0 +1,315 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { openDatabase } from "../db/connection";
4
+ import { TrapRepository } from "../db/repository";
5
+ import type { TrapExportRecord } from "../domain/trap";
6
+ import { findProjectRoot, resolveScopePath } from "./scope";
7
+ import {
8
+ backupScopeDatabase,
9
+ buildScopeMaintenancePaths,
10
+ validateScopeMaintenancePaths,
11
+ withReadOnlyScopeRepository,
12
+ type ScopeMaintenanceCommand,
13
+ } from "./scope-maintenance";
14
+ import {
15
+ deleteTransferredSourceTraps,
16
+ importProjectTrapTransfer,
17
+ type TrapTransferMapping,
18
+ } from "./trap-transfer";
19
+
20
+ export type ScopeMigrationCommand = ScopeMaintenanceCommand;
21
+ export type ScopeMigrationMode = "dry-run" | "apply";
22
+
23
+ export type ScopeMigrationCandidate = {
24
+ id: number;
25
+ title: string;
26
+ category: string;
27
+ severity: string;
28
+ status: string;
29
+ project_path: string | null;
30
+ };
31
+
32
+ export type ScopeMigrationCounts = {
33
+ source_before: number;
34
+ source_after: number;
35
+ destination_before: number;
36
+ destination_after: number;
37
+ candidates: number;
38
+ moved: number;
39
+ };
40
+
41
+ export type ScopeMigrationBackups = {
42
+ source_db: string | null;
43
+ destination_db: string | null;
44
+ };
45
+
46
+ export type ScopeMigrationResult = {
47
+ command: ScopeMigrationCommand;
48
+ mode: ScopeMigrationMode;
49
+ from_project_path: string;
50
+ to_project_path: string;
51
+ source_db: string;
52
+ destination_db: string;
53
+ source_db_exists: boolean;
54
+ destination_db_exists: boolean;
55
+ candidates: ScopeMigrationCandidate[];
56
+ moved: TrapTransferMapping[];
57
+ backups: ScopeMigrationBackups;
58
+ counts: ScopeMigrationCounts;
59
+ next_action: { command: string } | null;
60
+ };
61
+
62
+ export type ScopeMigrationOptions = {
63
+ command: ScopeMigrationCommand;
64
+ cwd?: string;
65
+ fromProjectPath?: string;
66
+ toProjectPath?: string;
67
+ apply?: boolean;
68
+ };
69
+
70
+ type ResolvedScopeMigration = {
71
+ command: ScopeMigrationCommand;
72
+ mode: ScopeMigrationMode;
73
+ apply: boolean;
74
+ fromProjectPath: string;
75
+ toProjectPath: string;
76
+ sourceDb: string;
77
+ destinationDb: string;
78
+ };
79
+
80
+ type ScopeMigrationPlan = ResolvedScopeMigration & {
81
+ sourceDbExists: boolean;
82
+ destinationDbExists: boolean;
83
+ records: TrapExportRecord[];
84
+ sourceBefore: number;
85
+ destinationBefore: number;
86
+ };
87
+
88
+ type ScopeMigrationApplyResult = {
89
+ moved: TrapTransferMapping[];
90
+ backups: ScopeMigrationBackups;
91
+ sourceAfter: number;
92
+ destinationAfter: number;
93
+ };
94
+
95
+ export function runScopeMigration(options: ScopeMigrationOptions): ScopeMigrationResult {
96
+ const plan = buildScopeMigrationPlan(options);
97
+
98
+ if (!plan.apply || plan.records.length === 0) {
99
+ return buildScopeMigrationResult(plan, {
100
+ moved: [],
101
+ backups: { source_db: null, destination_db: null },
102
+ sourceAfter: plan.sourceBefore,
103
+ destinationAfter: plan.destinationBefore,
104
+ });
105
+ }
106
+
107
+ return buildScopeMigrationResult(plan, applyScopeMigrationPlan(plan));
108
+ }
109
+
110
+ export function formatScopeMigrationText(result: ScopeMigrationResult): string {
111
+ const lines = [
112
+ `${result.command} ${result.mode}`,
113
+ `from_project_path: ${result.from_project_path}`,
114
+ `to_project_path: ${result.to_project_path}`,
115
+ `source_db: ${result.source_db}${result.source_db_exists ? "" : " (missing)"}`,
116
+ `destination_db: ${result.destination_db}${result.destination_db_exists ? "" : " (missing)"}`,
117
+ `candidates: ${result.counts.candidates}`,
118
+ ];
119
+
120
+ if (result.candidates.length > 0) {
121
+ lines.push("candidate traps:");
122
+ lines.push(...result.candidates.map((trap) =>
123
+ ` #${trap.id} [${trap.severity}] [${trap.category}] [${trap.status}] ${trap.title}`
124
+ ));
125
+ }
126
+
127
+ if (result.mode === "apply") {
128
+ lines.push(`moved: ${result.counts.moved}`);
129
+ if (result.backups.source_db || result.backups.destination_db) {
130
+ lines.push("backups:");
131
+ if (result.backups.source_db) lines.push(` source_db: ${result.backups.source_db}`);
132
+ if (result.backups.destination_db) lines.push(` destination_db: ${result.backups.destination_db}`);
133
+ }
134
+ if (result.moved.length > 0) {
135
+ lines.push("id mapping:");
136
+ lines.push(...result.moved.map((item) =>
137
+ ` #${item.source_id} -> #${item.destination_id} ${item.title}`
138
+ ));
139
+ }
140
+ }
141
+
142
+ lines.push(
143
+ `source_count: ${result.counts.source_before} -> ${result.counts.source_after}`,
144
+ `destination_count: ${result.counts.destination_before} -> ${result.counts.destination_after}`
145
+ );
146
+
147
+ if (result.next_action) {
148
+ lines.push(`Next: ${result.next_action.command}`);
149
+ }
150
+ if (result.candidates.length === 0) {
151
+ lines.push("No matching project traps found.");
152
+ }
153
+ return lines.join("\n");
154
+ }
155
+
156
+ function buildScopeMigrationPlan(options: ScopeMigrationOptions): ScopeMigrationPlan {
157
+ const resolved = resolveScopeMigrationOptions(options);
158
+ validateScopeMaintenancePaths(resolved);
159
+ const sourceDbExists = existsSync(resolved.sourceDb);
160
+ const destinationDbExists = existsSync(resolved.destinationDb);
161
+ const records = sourceDbExists
162
+ ? withReadOnlyScopeRepository(resolved.sourceDb, (repository) =>
163
+ repository.exportProjectTrapsByPath(resolved.fromProjectPath)
164
+ )
165
+ : [];
166
+ const destinationBefore = destinationDbExists
167
+ ? withReadOnlyScopeRepository(resolved.destinationDb, (repository) =>
168
+ repository.countProjectTrapsByPath(resolved.toProjectPath)
169
+ )
170
+ : 0;
171
+
172
+ return {
173
+ ...resolved,
174
+ sourceDbExists,
175
+ destinationDbExists,
176
+ records,
177
+ sourceBefore: records.length,
178
+ destinationBefore,
179
+ };
180
+ }
181
+
182
+ function applyScopeMigrationPlan(plan: ScopeMigrationPlan): ScopeMigrationApplyResult {
183
+ const backups = {
184
+ source_db: backupScopeDatabase(plan.sourceDb, "source"),
185
+ destination_db: plan.destinationDbExists ? backupScopeDatabase(plan.destinationDb, "destination") : null,
186
+ };
187
+
188
+ const applyResult = applyScopeMigration(
189
+ plan.sourceDb,
190
+ plan.destinationDb,
191
+ plan.fromProjectPath,
192
+ plan.toProjectPath
193
+ );
194
+
195
+ return {
196
+ moved: applyResult.moved,
197
+ backups,
198
+ sourceAfter: applyResult.sourceAfter,
199
+ destinationAfter: applyResult.destinationAfter,
200
+ };
201
+ }
202
+
203
+ function resolveScopeMigrationOptions(options: ScopeMigrationOptions): ResolvedScopeMigration {
204
+ const cwd = resolveScopePath(options.cwd ?? process.cwd());
205
+ const projectRoot = findProjectRoot(cwd);
206
+ const fromProjectPath = resolveScopePath(options.fromProjectPath ?? homedir());
207
+ const rawToProjectPath = options.toProjectPath ?? projectRoot;
208
+
209
+ if (!rawToProjectPath) {
210
+ throw new Error("Destination project not found. Run 'codetrap init' first, or pass --to-project-path.");
211
+ }
212
+ const toProjectPath = resolveScopePath(rawToProjectPath);
213
+
214
+ return {
215
+ ...buildScopeMaintenancePaths({
216
+ command: options.command,
217
+ fromProjectPath,
218
+ toProjectPath,
219
+ }),
220
+ mode: options.apply ? "apply" : "dry-run",
221
+ apply: options.apply === true,
222
+ };
223
+ }
224
+
225
+ function applyScopeMigration(
226
+ sourceDbPath: string,
227
+ destinationDbPath: string,
228
+ fromProjectPath: string,
229
+ toProjectPath: string
230
+ ): { moved: TrapTransferMapping[]; sourceAfter: number; destinationAfter: number } {
231
+ const sourceDb = openDatabase(sourceDbPath);
232
+ const destinationDb = openDatabase(destinationDbPath);
233
+ try {
234
+ const sourceRepository = new TrapRepository(sourceDb);
235
+ const destinationRepository = new TrapRepository(destinationDb);
236
+ const records = sourceRepository.exportProjectTrapsByPath(fromProjectPath);
237
+ const moved = importProjectTrapTransfer(destinationRepository, records, toProjectPath);
238
+ const deleted = deleteTransferredSourceTraps(sourceRepository, records);
239
+ if (deleted !== records.length) {
240
+ throw new Error(`Expected to delete ${records.length} source traps, deleted ${deleted}.`);
241
+ }
242
+
243
+ return {
244
+ moved,
245
+ sourceAfter: sourceRepository.countProjectTrapsByPath(fromProjectPath),
246
+ destinationAfter: destinationRepository.countProjectTrapsByPath(toProjectPath),
247
+ };
248
+ } finally {
249
+ sourceDb.close();
250
+ destinationDb.close();
251
+ }
252
+ }
253
+
254
+ function buildScopeMigrationResult(
255
+ plan: ScopeMigrationPlan,
256
+ applied: ScopeMigrationApplyResult
257
+ ): ScopeMigrationResult {
258
+ const candidates = plan.records.map(toCandidate);
259
+ return {
260
+ command: plan.command,
261
+ mode: plan.mode,
262
+ from_project_path: plan.fromProjectPath,
263
+ to_project_path: plan.toProjectPath,
264
+ source_db: plan.sourceDb,
265
+ destination_db: plan.destinationDb,
266
+ source_db_exists: plan.sourceDbExists,
267
+ destination_db_exists: plan.destinationDbExists,
268
+ candidates,
269
+ moved: applied.moved,
270
+ backups: applied.backups,
271
+ counts: {
272
+ source_before: plan.sourceBefore,
273
+ source_after: applied.sourceAfter,
274
+ destination_before: plan.destinationBefore,
275
+ destination_after: applied.destinationAfter,
276
+ candidates: candidates.length,
277
+ moved: applied.moved.length,
278
+ },
279
+ next_action: plan.mode === "dry-run" && candidates.length > 0
280
+ ? { command: buildApplyCommand(plan.command, plan.fromProjectPath, plan.toProjectPath) }
281
+ : null,
282
+ };
283
+ }
284
+
285
+ function toCandidate(record: TrapExportRecord): ScopeMigrationCandidate {
286
+ return {
287
+ id: record.id,
288
+ title: record.title,
289
+ category: record.category,
290
+ severity: record.severity,
291
+ status: record.status,
292
+ project_path: record.project_path,
293
+ };
294
+ }
295
+
296
+ function buildApplyCommand(
297
+ command: ScopeMigrationCommand,
298
+ fromProjectPath: string,
299
+ toProjectPath: string
300
+ ): string {
301
+ return [
302
+ "codetrap",
303
+ command,
304
+ "--from-project-path",
305
+ shellQuote(fromProjectPath),
306
+ "--to-project-path",
307
+ shellQuote(toProjectPath),
308
+ "--apply",
309
+ "--json",
310
+ ].join(" ");
311
+ }
312
+
313
+ function shellQuote(value: string): string {
314
+ return `'${value.replace(/'/g, "'\\''")}'`;
315
+ }
@@ -0,0 +1,99 @@
1
+ import { existsSync, realpathSync } from "node:fs";
2
+ import { posix, win32 } from "node:path";
3
+
4
+ export type ScopePathFlavor = "posix" | "win32";
5
+
6
+ export type ScopePathFs = {
7
+ exists(path: string): boolean;
8
+ realpath(path: string): string;
9
+ };
10
+
11
+ export type ScopePathResolverOptions = {
12
+ fs?: ScopePathFs;
13
+ platform?: NodeJS.Platform;
14
+ };
15
+
16
+ const nodeFs: ScopePathFs = {
17
+ exists: existsSync,
18
+ realpath: realpathSync,
19
+ };
20
+
21
+ export class ScopePathResolver {
22
+ private readonly fs: ScopePathFs;
23
+ private readonly platform: NodeJS.Platform;
24
+
25
+ constructor(options: ScopePathResolverOptions = {}) {
26
+ this.fs = options.fs ?? nodeFs;
27
+ this.platform = options.platform ?? process.platform;
28
+ }
29
+
30
+ resolve(path: string, relatedPath = path): string {
31
+ return this.resolveWithFlavor(path, this.flavorFor(path, relatedPath));
32
+ }
33
+
34
+ canonical(path: string, relatedPath = path): string {
35
+ const flavor = this.flavorFor(path, relatedPath);
36
+ const resolved = this.resolveWithFlavor(path, flavor);
37
+ const real = this.fs.exists(resolved) ? this.fs.realpath(resolved) : resolved;
38
+ const normalized = pathApi(flavor).normalize(real);
39
+ return flavor === "win32" ? normalized.toLowerCase() : normalized;
40
+ }
41
+
42
+ same(left: string, right: string): boolean {
43
+ const flavor = this.flavorFor(left, right);
44
+ return this.canonicalWithFlavor(left, flavor) === this.canonicalWithFlavor(right, flavor);
45
+ }
46
+
47
+ join(base: string, ...segments: string[]): string {
48
+ return pathApi(this.flavorFor(base)).join(base, ...segments);
49
+ }
50
+
51
+ exists(path: string): boolean {
52
+ return this.fs.exists(path);
53
+ }
54
+
55
+ dirname(path: string): string {
56
+ return pathApi(this.flavorFor(path)).dirname(path);
57
+ }
58
+
59
+ flavorFor(...paths: string[]): ScopePathFlavor {
60
+ if (this.platform === "win32") return "win32";
61
+ return paths.some((path) => isWindowsAbsolutePath(path) || isMsysAbsolutePath(path)) ? "win32" : "posix";
62
+ }
63
+
64
+ private canonicalWithFlavor(path: string, flavor: ScopePathFlavor): string {
65
+ const resolved = this.resolveWithFlavor(path, flavor);
66
+ const real = this.fs.exists(resolved) ? this.fs.realpath(resolved) : resolved;
67
+ const normalized = pathApi(flavor).normalize(real);
68
+ return flavor === "win32" ? normalized.toLowerCase() : normalized;
69
+ }
70
+
71
+ private resolveWithFlavor(path: string, flavor: ScopePathFlavor): string {
72
+ return pathApi(flavor).resolve(flavor === "win32" ? msysToWindowsPath(path) : path);
73
+ }
74
+ }
75
+
76
+ export const defaultScopePathResolver = new ScopePathResolver();
77
+
78
+ export function resolveScopePath(path: string, relatedPath = path): string {
79
+ return defaultScopePathResolver.resolve(path, relatedPath);
80
+ }
81
+
82
+ function pathApi(flavor: ScopePathFlavor): typeof posix {
83
+ return flavor === "win32" ? win32 : posix;
84
+ }
85
+
86
+ function msysToWindowsPath(path: string): string {
87
+ const match = path.match(/^\/([a-zA-Z])(?:\/(.*))?$/);
88
+ if (!match) return path;
89
+ const [, drive, rest = ""] = match;
90
+ return `${drive.toUpperCase()}:\\${rest.replaceAll("/", "\\")}`;
91
+ }
92
+
93
+ function isWindowsAbsolutePath(path: string): boolean {
94
+ return /^[a-zA-Z]:[\\/]/.test(path) || /^\\\\/.test(path);
95
+ }
96
+
97
+ function isMsysAbsolutePath(path: string): boolean {
98
+ return /^\/[a-zA-Z](?:\/|$)/.test(path);
99
+ }
package/src/lib/scope.ts CHANGED
@@ -1,32 +1,37 @@
1
1
  import { existsSync, mkdirSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
3
2
  import { homedir } from "node:os";
4
3
  import { CODETRAP_DIR, TRAPS_DB_FILE } from "./constants";
4
+ import { defaultScopePathResolver, resolveScopePath, ScopePathResolver } from "./scope-path";
5
+
6
+ export { resolveScopePath, ScopePathResolver } from "./scope-path";
5
7
 
6
8
  export function getGlobalDir(): string {
7
- const dir = join(homedir(), CODETRAP_DIR);
9
+ const dir = defaultScopePathResolver.join(homedir(), CODETRAP_DIR);
8
10
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
9
11
  return dir;
10
12
  }
11
13
 
12
14
  export function getGlobalDB(): string {
13
- return join(getGlobalDir(), TRAPS_DB_FILE);
15
+ return defaultScopePathResolver.join(getGlobalDir(), TRAPS_DB_FILE);
14
16
  }
15
17
 
16
- export function findProjectRoot(cwd: string, homeDir = homedir()): string | null {
17
- let dir = resolve(cwd);
18
- const home = resolve(homeDir);
18
+ export function findProjectRoot(
19
+ cwd: string,
20
+ homeDir = homedir(),
21
+ resolver: ScopePathResolver = defaultScopePathResolver
22
+ ): string | null {
23
+ let dir = resolver.resolve(cwd, homeDir);
19
24
  while (true) {
20
- if (dir === home) return null;
21
- if (existsSync(join(dir, CODETRAP_DIR))) return dir;
22
- const parent = dirname(dir);
25
+ if (resolver.same(dir, homeDir)) return null;
26
+ if (resolver.exists(resolver.join(dir, CODETRAP_DIR))) return dir;
27
+ const parent = resolver.dirname(dir);
23
28
  if (parent === dir) return null;
24
29
  dir = parent;
25
30
  }
26
31
  }
27
32
 
28
33
  export function getProjectDB(root: string): string {
29
- const dir = join(root, CODETRAP_DIR);
34
+ const dir = defaultScopePathResolver.join(root, CODETRAP_DIR);
30
35
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
31
- return join(dir, TRAPS_DB_FILE);
36
+ return defaultScopePathResolver.join(dir, TRAPS_DB_FILE);
32
37
  }
@@ -18,6 +18,9 @@ export type SearchTextFields = {
18
18
  tags?: string | string[];
19
19
  before_code?: string | null;
20
20
  after_code?: string | null;
21
+ path_globs?: string | string[] | null;
22
+ module?: string | null;
23
+ owner?: string | null;
21
24
  };
22
25
 
23
26
  export const SEARCH_TEXT_FIELD_NAMES = [
@@ -28,6 +31,9 @@ export const SEARCH_TEXT_FIELD_NAMES = [
28
31
  "tags",
29
32
  "before_code",
30
33
  "after_code",
34
+ "path_globs",
35
+ "module",
36
+ "owner",
31
37
  ] as const;
32
38
 
33
39
  export function bigramCJK(input: string): string[] {