@atomic-ehr/fhir-canonical-manager 0.0.7 → 0.0.8

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/src/index.ts CHANGED
@@ -3,11 +3,11 @@
3
3
  * A package manager for FHIR resources with canonical URL resolution
4
4
  */
5
5
 
6
- import * as path from 'path';
7
- import * as fs from 'fs/promises';
8
- import { createHash } from 'crypto';
9
- import { exec } from 'child_process';
10
- import { promisify } from 'util';
6
+ import * as path from "path";
7
+ import * as fs from "fs/promises";
8
+ import { createHash } from "crypto";
9
+ import { exec } from "child_process";
10
+ import { promisify } from "util";
11
11
 
12
12
  // Shell command utilities
13
13
  const execAsync = promisify(exec);
@@ -18,9 +18,14 @@ class ShellError extends Error {
18
18
  stdout: string;
19
19
  stderr: string;
20
20
 
21
- constructor(message: string, exitCode: number, stdout: string, stderr: string) {
21
+ constructor(
22
+ message: string,
23
+ exitCode: number,
24
+ stdout: string,
25
+ stderr: string,
26
+ ) {
22
27
  super(message);
23
- this.name = 'ShellError';
28
+ this.name = "ShellError";
24
29
  this.exitCode = exitCode;
25
30
  this.stdout = stdout;
26
31
  this.stderr = stderr;
@@ -43,8 +48,8 @@ interface ShellPromise extends Promise<ShellResult> {
43
48
  */
44
49
  function $(strings: TemplateStringsArray, ...values: any[]): ShellPromise {
45
50
  const command = strings.reduce((acc, str, i) => {
46
- return acc + str + (values[i] || '');
47
- }, '');
51
+ return acc + str + (values[i] || "");
52
+ }, "");
48
53
 
49
54
  let envVars: Record<string, string> = {};
50
55
 
@@ -55,38 +60,33 @@ function $(strings: TemplateStringsArray, ...values: any[]): ShellPromise {
55
60
  shell: true,
56
61
  maxBuffer: 10 * 1024 * 1024, // 10MB buffer
57
62
  };
58
-
63
+
59
64
  // Apply environment variables if set
60
65
  if (Object.keys(envVars).length > 0) {
61
66
  execOptions.env = { ...process.env, ...envVars };
62
67
  }
63
-
68
+
64
69
  const { stdout, stderr } = await execAsync(command, execOptions);
65
-
70
+
66
71
  return {
67
- stdout: stdout?.toString() || '',
68
- stderr: stderr?.toString() || '',
69
- exitCode: 0
72
+ stdout: stdout?.toString() || "",
73
+ stderr: stderr?.toString() || "",
74
+ exitCode: 0,
70
75
  };
71
76
  } catch (error: any) {
72
77
  // Extract error details
73
78
  const code = error.code || 1;
74
- const stdout = error.stdout?.toString() || '';
75
- const stderr = error.stderr?.toString() || error.message || '';
76
-
77
- throw new ShellError(
78
- `Command failed: ${command}`,
79
- code,
80
- stdout,
81
- stderr
82
- );
79
+ const stdout = error.stdout?.toString() || "";
80
+ const stderr = error.stderr?.toString() || error.message || "";
81
+
82
+ throw new ShellError(`Command failed: ${command}`, code, stdout, stderr);
83
83
  }
84
84
  };
85
85
 
86
86
  // Create a lazy promise that only executes when awaited
87
87
  let executed = false;
88
88
  let resultPromise: Promise<ShellResult> | null = null;
89
-
89
+
90
90
  const lazyPromise = {
91
91
  then(onFulfilled?: any, onRejected?: any) {
92
92
  if (!executed) {
@@ -107,7 +107,7 @@ function $(strings: TemplateStringsArray, ...values: any[]): ShellPromise {
107
107
  (reason: any) => {
108
108
  onFinally?.();
109
109
  throw reason;
110
- }
110
+ },
111
111
  );
112
112
  },
113
113
  quiet() {
@@ -118,25 +118,25 @@ function $(strings: TemplateStringsArray, ...values: any[]): ShellPromise {
118
118
  return this;
119
119
  },
120
120
  // Add Symbol.toStringTag to satisfy Promise interface
121
- [Symbol.toStringTag]: 'ShellPromise' as const
121
+ [Symbol.toStringTag]: "ShellPromise" as const,
122
122
  };
123
-
123
+
124
124
  return lazyPromise as any as ShellPromise;
125
125
  }
126
126
 
127
127
  /**
128
128
  * Detect available package manager
129
129
  */
130
- async function detectPackageManager(): Promise<'bun' | 'npm' | null> {
130
+ async function detectPackageManager(): Promise<"bun" | "npm" | null> {
131
131
  try {
132
132
  // Check for bun first
133
133
  await $`bun --version`.quiet();
134
- return 'bun';
134
+ return "bun";
135
135
  } catch {
136
136
  try {
137
137
  // Fall back to npm
138
138
  await $`npm --version`.quiet();
139
- return 'npm';
139
+ return "npm";
140
140
  } catch {
141
141
  return null;
142
142
  }
@@ -189,36 +189,51 @@ export interface CanonicalManager {
189
189
  init(): Promise<void>;
190
190
  destroy(): Promise<void>;
191
191
  packages(): Promise<PackageId[]>;
192
- resolveEntry(canonicalUrl: string, options?: {
193
- package?: string,
194
- version?: string,
195
- sourceContext?: SourceContext
196
- }): Promise<IndexEntry>;
197
- resolve(canonicalUrl: string, options?: {
198
- package?: string,
199
- version?: string,
200
- sourceContext?: SourceContext
201
- }): Promise<Resource>;
192
+ resolveEntry(
193
+ canonicalUrl: string,
194
+ options?: {
195
+ package?: string;
196
+ version?: string;
197
+ sourceContext?: SourceContext;
198
+ },
199
+ ): Promise<IndexEntry>;
200
+ resolve(
201
+ canonicalUrl: string,
202
+ options?: {
203
+ package?: string;
204
+ version?: string;
205
+ sourceContext?: SourceContext;
206
+ },
207
+ ): Promise<Resource>;
202
208
  read(reference: Reference): Promise<Resource>;
203
209
  searchEntries(params: {
204
- kind?: string,
205
- url?: string,
206
- type?: string,
207
- version?: string,
208
- package?: PackageId
210
+ kind?: string;
211
+ url?: string;
212
+ type?: string;
213
+ version?: string;
214
+ package?: PackageId;
209
215
  }): Promise<IndexEntry[]>;
210
216
  search(params: {
211
- kind?: string,
212
- url?: string,
213
- type?: string,
214
- version?: string,
215
- package?: PackageId
217
+ kind?: string;
218
+ url?: string;
219
+ type?: string;
220
+ version?: string;
221
+ package?: PackageId;
216
222
  }): Promise<Resource[]>;
223
+ smartSearch(
224
+ searchTerms: string[],
225
+ filters?: {
226
+ resourceType?: string;
227
+ type?: string;
228
+ kind?: string;
229
+ package?: PackageId;
230
+ }
231
+ ): Promise<IndexEntry[]>;
217
232
  }
218
233
 
219
234
  // Internal types
220
235
  interface IndexFile {
221
- 'index-version': number;
236
+ "index-version": number;
222
237
  files: IndexFileEntry[];
223
238
  }
224
239
 
@@ -287,7 +302,7 @@ const generateReferenceId = (metadata: {
287
302
  filePath: string;
288
303
  }): string => {
289
304
  const input = `${metadata.packageName}@${metadata.packageVersion}:${metadata.filePath}`;
290
- return createHash('sha256').update(input).digest('base64url');
305
+ return createHash("sha256").update(input).digest("base64url");
291
306
  };
292
307
 
293
308
  export const ReferenceManager = (): ReferenceStore & {
@@ -313,8 +328,8 @@ export const ReferenceManager = (): ReferenceStore & {
313
328
  };
314
329
 
315
330
  const clear = (): void => {
316
- Object.keys(references).forEach(key => delete references[key]);
317
- Object.keys(urlToIds).forEach(key => delete urlToIds[key]);
331
+ Object.keys(references).forEach((key) => delete references[key]);
332
+ Object.keys(urlToIds).forEach((key) => delete urlToIds[key]);
318
333
  };
319
334
 
320
335
  return {
@@ -327,32 +342,34 @@ export const ReferenceManager = (): ReferenceStore & {
327
342
  getIdsByUrl: (url: string) => urlToIds[url] || [],
328
343
  createReference: (id: string, metadata: ReferenceMetadata): Reference => ({
329
344
  id,
330
- resourceType: metadata.resourceType
345
+ resourceType: metadata.resourceType,
331
346
  }),
332
- getAllReferences: () => references
347
+ getAllReferences: () => references,
333
348
  };
334
349
  };
335
350
 
336
351
  // Parser functions
337
352
  const isValidFileEntry = (entry: any): boolean => {
338
- if (!entry || typeof entry !== 'object') return false;
339
- if (!entry.filename || typeof entry.filename !== 'string') return false;
340
- if (!entry.resourceType || typeof entry.resourceType !== 'string') return false;
341
- if (!entry.id || typeof entry.id !== 'string') return false;
342
-
343
- const optionalStringFields = ['url', 'version', 'kind', 'type'];
353
+ if (!entry || typeof entry !== "object") return false;
354
+ if (!entry.filename || typeof entry.filename !== "string") return false;
355
+ if (!entry.resourceType || typeof entry.resourceType !== "string")
356
+ return false;
357
+ if (!entry.id || typeof entry.id !== "string") return false;
358
+
359
+ const optionalStringFields = ["url", "version", "kind", "type"];
344
360
  for (const field of optionalStringFields) {
345
- if (entry[field] !== undefined && typeof entry[field] !== 'string') {
361
+ if (entry[field] !== undefined && typeof entry[field] !== "string") {
346
362
  return false;
347
363
  }
348
364
  }
349
-
365
+
350
366
  return true;
351
367
  };
352
368
 
353
369
  const isValidIndexFile = (data: any): boolean => {
354
- if (!data || typeof data !== 'object') return false;
355
- if (!data['index-version'] || typeof data['index-version'] !== 'number') return false;
370
+ if (!data || typeof data !== "object") return false;
371
+ if (!data["index-version"] || typeof data["index-version"] !== "number")
372
+ return false;
356
373
  if (!Array.isArray(data.files)) return false;
357
374
  return data.files.every((file: any) => isValidFileEntry(file));
358
375
  };
@@ -388,7 +405,7 @@ const ensureDir = async (dirPath: string): Promise<void> => {
388
405
  };
389
406
 
390
407
  const isFhirPackage = async (dirPath: string): Promise<boolean> => {
391
- const indexPath = path.join(dirPath, '.index.json');
408
+ const indexPath = path.join(dirPath, ".index.json");
392
409
  return fileExists(indexPath);
393
410
  };
394
411
 
@@ -401,23 +418,25 @@ const createCache = (): IndexCache & {
401
418
  entries: {},
402
419
  packages: {},
403
420
  references: {},
404
- referenceManager
421
+ referenceManager,
405
422
  };
406
423
  };
407
424
 
408
425
  // Compute hash of package-lock.json or bun.lock for cache validation
409
- const computePackageLockHash = async (workingDir: string): Promise<string | null> => {
426
+ const computePackageLockHash = async (
427
+ workingDir: string,
428
+ ): Promise<string | null> => {
410
429
  try {
411
430
  // Try package-lock.json first
412
- const packageLockPath = path.join(workingDir, 'package-lock.json');
431
+ const packageLockPath = path.join(workingDir, "package-lock.json");
413
432
  try {
414
- const content = await fs.readFile(packageLockPath, 'utf-8');
415
- return createHash('sha256').update(content).digest('hex');
433
+ const content = await fs.readFile(packageLockPath, "utf-8");
434
+ return createHash("sha256").update(content).digest("hex");
416
435
  } catch {
417
436
  // Try bun.lock if package-lock.json doesn't exist
418
- const bunLockPath = path.join(workingDir, 'bun.lock');
419
- const content = await fs.readFile(bunLockPath, 'utf-8');
420
- return createHash('sha256').update(content).digest('hex');
437
+ const bunLockPath = path.join(workingDir, "bun.lock");
438
+ const content = await fs.readFile(bunLockPath, "utf-8");
439
+ return createHash("sha256").update(content).digest("hex");
421
440
  }
422
441
  } catch {
423
442
  return null;
@@ -425,24 +444,30 @@ const computePackageLockHash = async (workingDir: string): Promise<string | null
425
444
  };
426
445
 
427
446
  // Cache persistence functions
428
- const saveCacheToDisk = async (cache: ReturnType<typeof createCache>, cacheDir: string, workingDir: string): Promise<void> => {
447
+ const saveCacheToDisk = async (
448
+ cache: ReturnType<typeof createCache>,
449
+ cacheDir: string,
450
+ workingDir: string,
451
+ ): Promise<void> => {
429
452
  const packageLockHash = await computePackageLockHash(workingDir);
430
-
453
+
431
454
  const cacheData: CacheData = {
432
455
  entries: cache.entries,
433
456
  packages: cache.packages,
434
457
  references: cache.referenceManager.getAllReferences(),
435
- packageLockHash: packageLockHash || undefined
458
+ packageLockHash: packageLockHash || undefined,
436
459
  };
437
-
438
- const cachePath = path.join(cacheDir, 'index.json');
460
+
461
+ const cachePath = path.join(cacheDir, "index.json");
439
462
  await fs.writeFile(cachePath, JSON.stringify(cacheData, null, 2));
440
463
  };
441
464
 
442
- const loadCacheFromDisk = async (cacheDir: string): Promise<CacheData | null> => {
465
+ const loadCacheFromDisk = async (
466
+ cacheDir: string,
467
+ ): Promise<CacheData | null> => {
443
468
  try {
444
- const cachePath = path.join(cacheDir, 'index.json');
445
- const content = await fs.readFile(cachePath, 'utf-8');
469
+ const cachePath = path.join(cacheDir, "index.json");
470
+ const content = await fs.readFile(cachePath, "utf-8");
446
471
  return JSON.parse(content) as CacheData;
447
472
  } catch {
448
473
  return null;
@@ -450,49 +475,56 @@ const loadCacheFromDisk = async (cacheDir: string): Promise<CacheData | null> =>
450
475
  };
451
476
 
452
477
  // Package management functions
453
- const installPackages = async (packages: string[], workingDir: string, registry?: string): Promise<void> => {
478
+ const installPackages = async (
479
+ packages: string[],
480
+ workingDir: string,
481
+ registry?: string,
482
+ ): Promise<void> => {
454
483
  await ensureDir(workingDir);
455
-
484
+
456
485
  // Check if package.json exists
457
- const packageJsonPath = path.join(workingDir, 'package.json');
486
+ const packageJsonPath = path.join(workingDir, "package.json");
458
487
  if (!(await fileExists(packageJsonPath))) {
459
488
  // Create minimal package.json
460
489
  const minimalPackageJson = {
461
490
  name: "fhir-canonical-manager-workspace",
462
491
  version: "1.0.0",
463
492
  private: true,
464
- dependencies: {}
493
+ dependencies: {},
465
494
  };
466
- await fs.writeFile(packageJsonPath, JSON.stringify(minimalPackageJson, null, 2));
495
+ await fs.writeFile(
496
+ packageJsonPath,
497
+ JSON.stringify(minimalPackageJson, null, 2),
498
+ );
467
499
  }
468
-
500
+
469
501
  // Detect available package manager
470
502
  const packageManager = await detectPackageManager();
471
503
  if (!packageManager) {
472
- throw new Error('No package manager found. Please install npm or bun.');
504
+ throw new Error("No package manager found. Please install npm or bun.");
473
505
  }
474
-
506
+
475
507
  // Install packages
476
508
  for (const pkg of packages) {
477
509
  try {
478
- if (packageManager === 'bun') {
510
+ if (packageManager === "bun") {
479
511
  // Use bun with auth bypass trick for FHIR registry
480
512
  const env = {
481
- HOME: workingDir, // Prevent reading user's .npmrc
482
- NPM_CONFIG_USERCONFIG: '/dev/null' // Extra safety
513
+ HOME: workingDir, // Prevent reading user's .npmrc
514
+ NPM_CONFIG_USERCONFIG: "/dev/null", // Extra safety
483
515
  };
484
-
485
- const cmd = registry
516
+
517
+ const cmd = registry
486
518
  ? `cd ${workingDir} && bun add ${pkg} --registry ${registry}`
487
519
  : `cd ${workingDir} && bun add ${pkg}`;
488
-
520
+
489
521
  await $`${cmd}`.env(env);
490
522
  } else {
491
523
  // Use npm (handles auth correctly)
492
524
  const cmd = registry
493
525
  ? `cd ${workingDir} && npm add ${pkg} --registry ${registry}`
494
526
  : `cd ${workingDir} && npm add ${pkg}`;
495
-
527
+
496
528
  await $`${cmd}`;
497
529
  }
498
530
  } catch (err) {
@@ -506,50 +538,50 @@ const installPackages = async (packages: string[], workingDir: string, registry?
506
538
  const processIndex = async (
507
539
  basePath: string,
508
540
  packageJson: PackageJson,
509
- cache: ReturnType<typeof createCache>
541
+ cache: ReturnType<typeof createCache>,
510
542
  ): Promise<void> => {
511
- const indexPath = path.join(basePath, '.index.json');
512
-
543
+ const indexPath = path.join(basePath, ".index.json");
544
+
513
545
  try {
514
- const indexContent = await fs.readFile(indexPath, 'utf-8');
546
+ const indexContent = await fs.readFile(indexPath, "utf-8");
515
547
  const index = parseIndex(indexContent, indexPath);
516
-
548
+
517
549
  if (!index) return;
518
-
550
+
519
551
  for (const file of index.files) {
520
552
  if (!file.url) continue;
521
-
553
+
522
554
  const filePath = path.join(basePath, file.filename);
523
-
555
+
524
556
  const id = cache.referenceManager.generateId({
525
557
  packageName: packageJson.name,
526
558
  packageVersion: packageJson.version,
527
- filePath
559
+ filePath,
528
560
  });
529
-
561
+
530
562
  cache.referenceManager.set(id, {
531
563
  packageName: packageJson.name,
532
564
  packageVersion: packageJson.version,
533
565
  filePath,
534
566
  resourceType: file.resourceType,
535
567
  url: file.url,
536
- version: file.version
568
+ version: file.version,
537
569
  });
538
-
570
+
539
571
  const entry: IndexEntry = {
540
572
  id,
541
573
  resourceType: file.resourceType,
542
- indexVersion: index['index-version'],
574
+ indexVersion: index["index-version"],
543
575
  url: file.url,
544
576
  version: file.version,
545
577
  kind: file.kind,
546
578
  type: file.type,
547
579
  package: {
548
580
  name: packageJson.name,
549
- version: packageJson.version
550
- }
581
+ version: packageJson.version,
582
+ },
551
583
  };
552
-
584
+
553
585
  if (!cache.entries[file.url]) {
554
586
  cache.entries[file.url] = [];
555
587
  }
@@ -565,25 +597,25 @@ const processIndex = async (
565
597
 
566
598
  const scanPackage = async (
567
599
  packagePath: string,
568
- cache: ReturnType<typeof createCache>
600
+ cache: ReturnType<typeof createCache>,
569
601
  ): Promise<void> => {
570
602
  try {
571
- const packageJsonPath = path.join(packagePath, 'package.json');
572
- const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
603
+ const packageJsonPath = path.join(packagePath, "package.json");
604
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
573
605
  const packageJson: PackageJson = JSON.parse(packageJsonContent);
574
-
606
+
575
607
  const packageInfo: PackageInfo = {
576
608
  id: { name: packageJson.name, version: packageJson.version },
577
609
  path: packagePath,
578
610
  canonical: packageJson.canonical,
579
- fhirVersions: packageJson.fhirVersions
611
+ fhirVersions: packageJson.fhirVersions,
580
612
  };
581
613
  cache.packages[packageJson.name] = packageInfo;
582
-
614
+
583
615
  await processIndex(packagePath, packageJson, cache);
584
-
585
- const examplesPath = path.join(packagePath, 'examples');
586
- if (await fileExists(path.join(examplesPath, '.index.json'))) {
616
+
617
+ const examplesPath = path.join(packagePath, "examples");
618
+ if (await fileExists(path.join(examplesPath, ".index.json"))) {
587
619
  await processIndex(examplesPath, packageJson, cache);
588
620
  }
589
621
  } catch {
@@ -593,21 +625,23 @@ const scanPackage = async (
593
625
 
594
626
  const scanDirectory = async (
595
627
  dirPath: string,
596
- cache: ReturnType<typeof createCache>
628
+ cache: ReturnType<typeof createCache>,
597
629
  ): Promise<void> => {
598
630
  try {
599
631
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
600
-
632
+
601
633
  for (const entry of entries) {
602
634
  if (!entry.isDirectory()) continue;
603
-
635
+
604
636
  const fullPath = path.join(dirPath, entry.name);
605
-
606
- if (entry.name.startsWith('@')) {
607
- const scopedEntries = await fs.readdir(fullPath, { withFileTypes: true });
637
+
638
+ if (entry.name.startsWith("@")) {
639
+ const scopedEntries = await fs.readdir(fullPath, {
640
+ withFileTypes: true,
641
+ });
608
642
  for (const scopedEntry of scopedEntries) {
609
643
  if (!scopedEntry.isDirectory()) continue;
610
-
644
+
611
645
  const scopedPath = path.join(fullPath, scopedEntry.name);
612
646
  if (await isFhirPackage(scopedPath)) {
613
647
  await scanPackage(scopedPath, cache);
@@ -627,13 +661,13 @@ const resolveWithContext = async (
627
661
  url: string,
628
662
  context: SourceContext,
629
663
  cache: ReturnType<typeof createCache>,
630
- resolveEntry: (url: string, options?: any) => Promise<IndexEntry>
664
+ resolveEntry: (url: string, options?: any) => Promise<IndexEntry>,
631
665
  ): Promise<IndexEntry | null> => {
632
666
  if (context.package) {
633
667
  try {
634
668
  return await resolveEntry(url, {
635
669
  package: context.package.name,
636
- version: context.package.version
670
+ version: context.package.version,
637
671
  });
638
672
  } catch {
639
673
  // Fall through to global resolution
@@ -643,41 +677,46 @@ const resolveWithContext = async (
643
677
  };
644
678
 
645
679
  // Default FHIR package registry
646
- const DEFAULT_REGISTRY = 'https://fs.get-ig.org/pkgs';
680
+ const DEFAULT_REGISTRY = "https://fs.get-ig.org/pkgs/";
647
681
 
648
682
  // Main implementation
649
683
  export const CanonicalManager = (config: Config): CanonicalManager => {
650
- const { packages, workingDir, registry = DEFAULT_REGISTRY } = config;
651
- const nodeModulesPath = path.join(workingDir, 'node_modules');
652
- const cacheDir = path.join(workingDir, '.fcm', 'cache');
653
-
684
+ const { packages, workingDir } = config;
685
+ // Ensure registry URL ends with /
686
+ const registry = config.registry
687
+ ? config.registry.endsWith('/') ? config.registry : `${config.registry}/`
688
+ : DEFAULT_REGISTRY;
689
+ const nodeModulesPath = path.join(workingDir, "node_modules");
690
+ const cacheDir = path.join(workingDir, ".fcm", "cache");
691
+
654
692
  let cache = createCache();
655
693
  let initialized = false;
656
694
 
657
695
  const ensureInitialized = (): void => {
658
696
  if (!initialized) {
659
- throw new Error('CanonicalManager not initialized. Call init() first.');
697
+ throw new Error("CanonicalManager not initialized. Call init() first.");
660
698
  }
661
699
  };
662
700
 
663
701
  const init = async (): Promise<void> => {
664
702
  if (initialized) return;
665
-
703
+
666
704
  // Ensure directories exist
667
705
  await ensureDir(workingDir);
668
706
  await ensureDir(cacheDir);
669
-
707
+
670
708
  // Get current package-lock.json hash
671
709
  const currentPackageLockHash = await computePackageLockHash(workingDir);
672
-
710
+
673
711
  // Try to load cache from disk
674
712
  const cachedData = await loadCacheFromDisk(cacheDir);
675
-
713
+
676
714
  // Check if cache is valid (exists and package-lock.json hasn't changed)
677
- const cacheValid = cachedData &&
715
+ const cacheValid =
716
+ cachedData &&
678
717
  cachedData.packageLockHash === currentPackageLockHash &&
679
718
  currentPackageLockHash !== null;
680
-
719
+
681
720
  if (cacheValid) {
682
721
  // Restore cache from disk
683
722
  cache.entries = cachedData.entries;
@@ -688,22 +727,22 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
688
727
  } else {
689
728
  // Cache is invalid or doesn't exist - rebuild it
690
729
  if (cachedData && cachedData.packageLockHash !== currentPackageLockHash) {
691
- console.log('Package dependencies have changed, rebuilding index...');
730
+ console.log("Package dependencies have changed, rebuilding index...");
692
731
  }
693
-
732
+
694
733
  // Install packages if needed
695
734
  await installPackages(packages, workingDir, registry);
696
-
735
+
697
736
  // Clear cache before scanning
698
737
  cache = createCache();
699
-
738
+
700
739
  // Scan installed packages
701
740
  await scanDirectory(nodeModulesPath, cache);
702
-
741
+
703
742
  // Save cache to disk with current package-lock hash
704
743
  await saveCacheToDisk(cache, cacheDir, workingDir);
705
744
  }
706
-
745
+
707
746
  initialized = true;
708
747
  };
709
748
 
@@ -716,7 +755,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
716
755
 
717
756
  const getPackages = async (): Promise<PackageId[]> => {
718
757
  ensureInitialized();
719
- return Object.values(cache.packages).map(p => p.id);
758
+ return Object.values(cache.packages).map((p) => p.id);
720
759
  };
721
760
 
722
761
  const resolveEntry = async (
@@ -725,7 +764,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
725
764
  package?: string;
726
765
  version?: string;
727
766
  sourceContext?: SourceContext;
728
- }
767
+ },
729
768
  ): Promise<IndexEntry> => {
730
769
  ensureInitialized();
731
770
 
@@ -734,7 +773,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
734
773
  canonicalUrl,
735
774
  options.sourceContext,
736
775
  cache,
737
- resolveEntry
776
+ resolveEntry,
738
777
  );
739
778
  if (contextResolved) {
740
779
  return contextResolved;
@@ -742,23 +781,25 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
742
781
  }
743
782
 
744
783
  const entries = cache.entries[canonicalUrl] || [];
745
-
784
+
746
785
  if (entries.length === 0) {
747
786
  throw new Error(`Cannot resolve canonical URL: ${canonicalUrl}`);
748
787
  }
749
788
 
750
789
  let filtered = [...entries];
751
-
790
+
752
791
  if (options?.package) {
753
- filtered = filtered.filter(e => e.package?.name === options.package);
792
+ filtered = filtered.filter((e) => e.package?.name === options.package);
754
793
  }
755
-
794
+
756
795
  if (options?.version) {
757
- filtered = filtered.filter(e => e.version === options.version);
796
+ filtered = filtered.filter((e) => e.version === options.version);
758
797
  }
759
798
 
760
799
  if (filtered.length === 0) {
761
- throw new Error(`No matching resource found for ${canonicalUrl} with given options`);
800
+ throw new Error(
801
+ `No matching resource found for ${canonicalUrl} with given options`,
802
+ );
762
803
  }
763
804
 
764
805
  return filtered[0]!;
@@ -770,7 +811,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
770
811
  package?: string;
771
812
  version?: string;
772
813
  sourceContext?: SourceContext;
773
- }
814
+ },
774
815
  ): Promise<Resource> => {
775
816
  const entry = await resolveEntry(canonicalUrl, options);
776
817
  return read(entry);
@@ -778,20 +819,20 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
778
819
 
779
820
  const read = async (reference: Reference): Promise<Resource> => {
780
821
  ensureInitialized();
781
-
822
+
782
823
  const metadata = cache.referenceManager.get(reference.id);
783
824
  if (!metadata) {
784
825
  throw new Error(`Invalid reference ID: ${reference.id}`);
785
826
  }
786
827
 
787
828
  try {
788
- const content = await fs.readFile(metadata.filePath, 'utf-8');
829
+ const content = await fs.readFile(metadata.filePath, "utf-8");
789
830
  const resource = JSON.parse(content);
790
-
831
+
791
832
  return {
792
833
  ...resource,
793
834
  id: reference.id,
794
- resourceType: reference.resourceType
835
+ resourceType: reference.resourceType,
795
836
  };
796
837
  } catch (err) {
797
838
  throw new Error(`Failed to read resource: ${err}`);
@@ -806,9 +847,9 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
806
847
  package?: PackageId;
807
848
  }): Promise<IndexEntry[]> => {
808
849
  ensureInitialized();
809
-
850
+
810
851
  let results: IndexEntry[] = [];
811
-
852
+
812
853
  if (params.url) {
813
854
  results = cache.entries[params.url] || [];
814
855
  } else {
@@ -816,27 +857,27 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
816
857
  results.push(...entries);
817
858
  }
818
859
  }
819
-
860
+
820
861
  if (params.kind !== undefined) {
821
- results = results.filter(e => e.kind === params.kind);
862
+ results = results.filter((e) => e.kind === params.kind);
822
863
  }
823
-
864
+
824
865
  if (params.type !== undefined) {
825
- results = results.filter(e => e.type === params.type);
866
+ results = results.filter((e) => e.type === params.type);
826
867
  }
827
-
868
+
828
869
  if (params.version !== undefined) {
829
- results = results.filter(e => e.version === params.version);
870
+ results = results.filter((e) => e.version === params.version);
830
871
  }
831
-
872
+
832
873
  if (params.package) {
833
874
  const pkg = params.package;
834
- results = results.filter(e =>
835
- e.package?.name === pkg.name &&
836
- e.package?.version === pkg.version
875
+ results = results.filter(
876
+ (e) =>
877
+ e.package?.name === pkg.name && e.package?.version === pkg.version,
837
878
  );
838
879
  }
839
-
880
+
840
881
  return results;
841
882
  };
842
883
 
@@ -848,10 +889,98 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
848
889
  package?: PackageId;
849
890
  }): Promise<Resource[]> => {
850
891
  const entries = await searchEntries(params);
851
- const resources = await Promise.all(entries.map(entry => read(entry)));
892
+ const resources = await Promise.all(entries.map((entry) => read(entry)));
852
893
  return resources;
853
894
  };
854
895
 
896
+ const smartSearch = async (
897
+ searchTerms: string[],
898
+ filters?: {
899
+ resourceType?: string;
900
+ type?: string;
901
+ kind?: string;
902
+ package?: PackageId;
903
+ }
904
+ ): Promise<IndexEntry[]> => {
905
+ ensureInitialized();
906
+
907
+ // Start with base search using filters
908
+ let results = await searchEntries({
909
+ kind: filters?.kind,
910
+ package: filters?.package,
911
+ });
912
+
913
+ // Apply resourceType filter
914
+ if (filters?.resourceType) {
915
+ results = results.filter(entry => entry.resourceType === filters.resourceType);
916
+ }
917
+
918
+ // Apply type filter
919
+ if (filters?.type) {
920
+ results = results.filter(entry => entry.type === filters.type);
921
+ }
922
+
923
+ // Apply smart search terms if provided
924
+ if (searchTerms.length > 0) {
925
+ const terms = searchTerms.map(t => t.toLowerCase());
926
+
927
+ results = results.filter(entry => {
928
+ if (!entry.url) return false;
929
+ const urlLower = entry.url.toLowerCase();
930
+
931
+ // Also check type and resourceType for matching
932
+ const fullText = [
933
+ urlLower,
934
+ entry.type?.toLowerCase() || '',
935
+ entry.resourceType?.toLowerCase() || ''
936
+ ].join(' ');
937
+
938
+ // Check if all search terms match
939
+ return terms.every(term => {
940
+ // Split the text into parts (by /, -, _, ., spaces)
941
+ const allParts = fullText.split(/[\/\-_\.\s]+/);
942
+
943
+ // Check if any part starts with the search term
944
+ const directMatch = allParts.some(part => part.startsWith(term));
945
+ if (directMatch) return true;
946
+
947
+ // Smart matching for common abbreviations
948
+ const expandedTerms: Record<string, string[]> = {
949
+ 'str': ['structure'],
950
+ 'struct': ['structure'],
951
+ 'def': ['definition'],
952
+ 'pati': ['patient'],
953
+ 'obs': ['observation'],
954
+ 'org': ['organization'],
955
+ 'pract': ['practitioner'],
956
+ 'med': ['medication', 'medicinal'],
957
+ 'req': ['request'],
958
+ 'resp': ['response'],
959
+ 'ref': ['reference'],
960
+ 'val': ['value'],
961
+ 'code': ['codesystem', 'code'],
962
+ 'cs': ['codesystem'],
963
+ 'vs': ['valueset'],
964
+ 'sd': ['structuredefinition'],
965
+ };
966
+
967
+ // Check if the term is an abbreviation
968
+ const expansions = expandedTerms[term] || [];
969
+ for (const expansion of expansions) {
970
+ if (allParts.some(part => part.startsWith(expansion))) {
971
+ return true;
972
+ }
973
+ }
974
+
975
+ // If still no match, check if the term appears anywhere (substring match)
976
+ return fullText.includes(term);
977
+ });
978
+ });
979
+ }
980
+
981
+ return results;
982
+ };
983
+
855
984
  return {
856
985
  init,
857
986
  destroy,
@@ -860,9 +989,10 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
860
989
  resolve,
861
990
  read,
862
991
  searchEntries,
863
- search
992
+ search,
993
+ smartSearch,
864
994
  };
865
995
  };
866
996
 
867
997
  // Default export
868
- export default CanonicalManager;
998
+ export default CanonicalManager;