@the-cascade-protocol/cli 0.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 (165) hide show
  1. package/.dockerignore +7 -0
  2. package/.eslintrc.json +23 -0
  3. package/.prettierrc +7 -0
  4. package/DOCKER.md +36 -0
  5. package/Dockerfile +18 -0
  6. package/README.md +69 -0
  7. package/dist/commands/capabilities.d.ts +9 -0
  8. package/dist/commands/capabilities.d.ts.map +1 -0
  9. package/dist/commands/capabilities.js +194 -0
  10. package/dist/commands/capabilities.js.map +1 -0
  11. package/dist/commands/conformance.d.ts +15 -0
  12. package/dist/commands/conformance.d.ts.map +1 -0
  13. package/dist/commands/conformance.js +348 -0
  14. package/dist/commands/conformance.js.map +1 -0
  15. package/dist/commands/convert.d.ts +21 -0
  16. package/dist/commands/convert.d.ts.map +1 -0
  17. package/dist/commands/convert.js +134 -0
  18. package/dist/commands/convert.js.map +1 -0
  19. package/dist/commands/pod/export.d.ts +8 -0
  20. package/dist/commands/pod/export.d.ts.map +1 -0
  21. package/dist/commands/pod/export.js +72 -0
  22. package/dist/commands/pod/export.js.map +1 -0
  23. package/dist/commands/pod/helpers.d.ts +79 -0
  24. package/dist/commands/pod/helpers.d.ts.map +1 -0
  25. package/dist/commands/pod/helpers.js +369 -0
  26. package/dist/commands/pod/helpers.js.map +1 -0
  27. package/dist/commands/pod/index.d.ts +20 -0
  28. package/dist/commands/pod/index.d.ts.map +1 -0
  29. package/dist/commands/pod/index.js +29 -0
  30. package/dist/commands/pod/index.js.map +1 -0
  31. package/dist/commands/pod/info.d.ts +9 -0
  32. package/dist/commands/pod/info.d.ts.map +1 -0
  33. package/dist/commands/pod/info.js +196 -0
  34. package/dist/commands/pod/info.js.map +1 -0
  35. package/dist/commands/pod/init.d.ts +9 -0
  36. package/dist/commands/pod/init.d.ts.map +1 -0
  37. package/dist/commands/pod/init.js +251 -0
  38. package/dist/commands/pod/init.js.map +1 -0
  39. package/dist/commands/pod/query.d.ts +9 -0
  40. package/dist/commands/pod/query.d.ts.map +1 -0
  41. package/dist/commands/pod/query.js +169 -0
  42. package/dist/commands/pod/query.js.map +1 -0
  43. package/dist/commands/pod 2.js +1017 -0
  44. package/dist/commands/pod.d.ts +28 -0
  45. package/dist/commands/pod.d.ts 2.map +1 -0
  46. package/dist/commands/pod.d.ts.map +1 -0
  47. package/dist/commands/pod.js +1031 -0
  48. package/dist/commands/pod.js 2.map +1 -0
  49. package/dist/commands/pod.js.map +1 -0
  50. package/dist/commands/serve.d.ts +33 -0
  51. package/dist/commands/serve.d.ts.map +1 -0
  52. package/dist/commands/serve.js +74 -0
  53. package/dist/commands/serve.js.map +1 -0
  54. package/dist/commands/validate.d.ts +18 -0
  55. package/dist/commands/validate.d.ts.map +1 -0
  56. package/dist/commands/validate.js +275 -0
  57. package/dist/commands/validate.js.map +1 -0
  58. package/dist/index.d.ts +19 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +49 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/lib/fhir-converter/cascade-to-fhir.d.ts +17 -0
  63. package/dist/lib/fhir-converter/cascade-to-fhir.d.ts.map +1 -0
  64. package/dist/lib/fhir-converter/cascade-to-fhir.js +358 -0
  65. package/dist/lib/fhir-converter/cascade-to-fhir.js.map +1 -0
  66. package/dist/lib/fhir-converter/converters-clinical.d.ts +29 -0
  67. package/dist/lib/fhir-converter/converters-clinical.d.ts.map +1 -0
  68. package/dist/lib/fhir-converter/converters-clinical.js +391 -0
  69. package/dist/lib/fhir-converter/converters-clinical.js.map +1 -0
  70. package/dist/lib/fhir-converter/converters-demographics.d.ts +20 -0
  71. package/dist/lib/fhir-converter/converters-demographics.d.ts.map +1 -0
  72. package/dist/lib/fhir-converter/converters-demographics.js +242 -0
  73. package/dist/lib/fhir-converter/converters-demographics.js.map +1 -0
  74. package/dist/lib/fhir-converter/fhir-to-cascade.d.ts +17 -0
  75. package/dist/lib/fhir-converter/fhir-to-cascade.d.ts.map +1 -0
  76. package/dist/lib/fhir-converter/fhir-to-cascade.js +63 -0
  77. package/dist/lib/fhir-converter/fhir-to-cascade.js.map +1 -0
  78. package/dist/lib/fhir-converter/index.d.ts +36 -0
  79. package/dist/lib/fhir-converter/index.d.ts.map +1 -0
  80. package/dist/lib/fhir-converter/index.js +187 -0
  81. package/dist/lib/fhir-converter/index.js.map +1 -0
  82. package/dist/lib/fhir-converter/types.d.ts +77 -0
  83. package/dist/lib/fhir-converter/types.d.ts.map +1 -0
  84. package/dist/lib/fhir-converter/types.js +236 -0
  85. package/dist/lib/fhir-converter/types.js.map +1 -0
  86. package/dist/lib/fhir-converter.d.ts +62 -0
  87. package/dist/lib/fhir-converter.d.ts.map +1 -0
  88. package/dist/lib/fhir-converter.js +1474 -0
  89. package/dist/lib/fhir-converter.js.map +1 -0
  90. package/dist/lib/mcp/audit.d.ts +24 -0
  91. package/dist/lib/mcp/audit.d.ts.map +1 -0
  92. package/dist/lib/mcp/audit.js +85 -0
  93. package/dist/lib/mcp/audit.js.map +1 -0
  94. package/dist/lib/mcp/server.d.ts +38 -0
  95. package/dist/lib/mcp/server.d.ts.map +1 -0
  96. package/dist/lib/mcp/server.js +172 -0
  97. package/dist/lib/mcp/server.js.map +1 -0
  98. package/dist/lib/mcp/tools.d.ts +47 -0
  99. package/dist/lib/mcp/tools.d.ts.map +1 -0
  100. package/dist/lib/mcp/tools.js +547 -0
  101. package/dist/lib/mcp/tools.js.map +1 -0
  102. package/dist/lib/output.d.ts +26 -0
  103. package/dist/lib/output.d.ts.map +1 -0
  104. package/dist/lib/output.js +64 -0
  105. package/dist/lib/output.js.map +1 -0
  106. package/dist/lib/shacl-validator.d.ts +53 -0
  107. package/dist/lib/shacl-validator.d.ts.map +1 -0
  108. package/dist/lib/shacl-validator.js +245 -0
  109. package/dist/lib/shacl-validator.js.map +1 -0
  110. package/dist/lib/turtle-parser.d.ts +64 -0
  111. package/dist/lib/turtle-parser.d.ts.map +1 -0
  112. package/dist/lib/turtle-parser.js +236 -0
  113. package/dist/lib/turtle-parser.js.map +1 -0
  114. package/dist/shapes/checkup.shapes.ttl +1459 -0
  115. package/dist/shapes/clinical.shapes.ttl +1350 -0
  116. package/dist/shapes/clinical.ttl +1369 -0
  117. package/dist/shapes/core.shapes.ttl +450 -0
  118. package/dist/shapes/core.ttl +603 -0
  119. package/dist/shapes/coverage.shapes.ttl +214 -0
  120. package/dist/shapes/coverage.ttl +182 -0
  121. package/dist/shapes/health.shapes.ttl +697 -0
  122. package/dist/shapes/health.ttl +859 -0
  123. package/dist/shapes/pots.shapes.ttl +481 -0
  124. package/package.json +54 -0
  125. package/src/commands/capabilities.ts +235 -0
  126. package/src/commands/conformance.ts +447 -0
  127. package/src/commands/convert.ts +164 -0
  128. package/src/commands/pod/export.ts +85 -0
  129. package/src/commands/pod/helpers.ts +449 -0
  130. package/src/commands/pod/index.ts +32 -0
  131. package/src/commands/pod/info.ts +239 -0
  132. package/src/commands/pod/init.ts +273 -0
  133. package/src/commands/pod/query.ts +224 -0
  134. package/src/commands/serve.ts +92 -0
  135. package/src/commands/validate.ts +303 -0
  136. package/src/index.ts +58 -0
  137. package/src/lib/fhir-converter/cascade-to-fhir.ts +369 -0
  138. package/src/lib/fhir-converter/converters-clinical.ts +446 -0
  139. package/src/lib/fhir-converter/converters-demographics.ts +270 -0
  140. package/src/lib/fhir-converter/fhir-to-cascade.ts +82 -0
  141. package/src/lib/fhir-converter/index.ts +215 -0
  142. package/src/lib/fhir-converter/types.ts +318 -0
  143. package/src/lib/mcp/audit.ts +107 -0
  144. package/src/lib/mcp/server.ts +192 -0
  145. package/src/lib/mcp/tools.ts +668 -0
  146. package/src/lib/output.ts +76 -0
  147. package/src/lib/shacl-validator.ts +314 -0
  148. package/src/lib/turtle-parser.ts +277 -0
  149. package/src/shapes/checkup.shapes.ttl +1459 -0
  150. package/src/shapes/clinical.shapes.ttl +1350 -0
  151. package/src/shapes/clinical.ttl +1369 -0
  152. package/src/shapes/core.shapes.ttl +450 -0
  153. package/src/shapes/core.ttl +603 -0
  154. package/src/shapes/coverage.shapes.ttl +214 -0
  155. package/src/shapes/coverage.ttl +182 -0
  156. package/src/shapes/health.shapes.ttl +697 -0
  157. package/src/shapes/health.ttl +859 -0
  158. package/src/shapes/pots.shapes.ttl +481 -0
  159. package/test-fixtures/fhir-bundle-example.json +216 -0
  160. package/test-fixtures/fhir-medication-example.json +18 -0
  161. package/tests/cli.test.ts +126 -0
  162. package/tests/fhir-converter.test.ts +874 -0
  163. package/tests/mcp-server.test.ts +396 -0
  164. package/tests/pod.test.ts +400 -0
  165. package/tsconfig.json +24 -0
@@ -0,0 +1,1031 @@
1
+ /**
2
+ * cascade pod <subcommand>
3
+ *
4
+ * Manage Cascade Pod structures.
5
+ *
6
+ * Subcommands:
7
+ * init <directory> Initialize a new Cascade Pod
8
+ * query <pod-dir> Query data within a pod
9
+ * export <pod-dir> Export pod data
10
+ * info <pod-dir> Show pod metadata and statistics
11
+ *
12
+ * Query options:
13
+ * --medications Query medications
14
+ * --conditions Query conditions
15
+ * --allergies Query allergies
16
+ * --lab-results Query lab results
17
+ * --immunizations Query immunizations
18
+ * --vital-signs Query vital signs
19
+ * --all Query all data
20
+ * --json Output as JSON
21
+ *
22
+ * Export options:
23
+ * --format <fmt> Export format (zip|directory) [default: zip]
24
+ * --output <path> Output path for export
25
+ */
26
+ import * as fs from 'fs/promises';
27
+ import * as path from 'path';
28
+ import { printResult, printError, printVerbose } from '../lib/output.js';
29
+ import { parseTurtleFile, getSubjectsByType, getProperties, shortenIRI, extractLabel, CASCADE_NAMESPACES, } from '../lib/turtle-parser.js';
30
+ // ─── Pod Init Templates ──────────────────────────────────────────────────────
31
+ function wellKnownSolid(absPath) {
32
+ return JSON.stringify({
33
+ '@context': 'https://www.w3.org/ns/solid/terms',
34
+ pod_root: '/',
35
+ profile: '/profile/card.ttl#me',
36
+ storage: '/',
37
+ publicTypeIndex: '/settings/publicTypeIndex.ttl',
38
+ privateTypeIndex: '/settings/privateTypeIndex.ttl',
39
+ podUri: `file://${absPath}/`,
40
+ version: '1.0',
41
+ }, null, 2);
42
+ }
43
+ const PROFILE_CARD_TTL = `@prefix cascade: <https://ns.cascadeprotocol.org/core/v1#> .
44
+ @prefix foaf: <http://xmlns.com/foaf/0.1/> .
45
+ @prefix solid: <http://www.w3.org/ns/solid/terms#> .
46
+ @prefix pim: <http://www.w3.org/ns/pim/space#> .
47
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
48
+
49
+ # =============================================================================
50
+ # WebID Profile Card
51
+ # =============================================================================
52
+ # This is a Solid-compatible WebID profile for the Pod owner.
53
+ # Edit this file to add patient demographics and identity information.
54
+ #
55
+ # The <#me> fragment serves as the WebID for this Pod.
56
+ # =============================================================================
57
+
58
+ <#me> a foaf:Person ;
59
+ # ── Edit the fields below to personalize your Pod ──
60
+ # foaf:name "First Last" ;
61
+ # foaf:givenName "First" ;
62
+ # foaf:familyName "Last" ;
63
+ # cascade:dateOfBirth "1990-01-01"^^xsd:date ;
64
+
65
+ # ── Discovery links (do not remove) ──
66
+ solid:publicTypeIndex </settings/publicTypeIndex.ttl> ;
67
+ solid:privateTypeIndex </settings/privateTypeIndex.ttl> ;
68
+ pim:storage </> ;
69
+
70
+ cascade:schemaVersion "1.3" .
71
+ `;
72
+ const PUBLIC_TYPE_INDEX_TTL = `@prefix solid: <http://www.w3.org/ns/solid/terms#> .
73
+ @prefix cascade: <https://ns.cascadeprotocol.org/core/v1#> .
74
+ @prefix health: <https://ns.cascadeprotocol.org/health/v1#> .
75
+ @prefix clinical: <https://ns.cascadeprotocol.org/clinical/v1#> .
76
+
77
+ # =============================================================================
78
+ # Public Type Index
79
+ # =============================================================================
80
+ # Maps data types to their storage locations in the Pod.
81
+ # Public registrations are visible to authorized applications.
82
+ #
83
+ # As you populate your Pod with data, add type registrations here so that
84
+ # agents and applications can discover where to find each data type.
85
+ # =============================================================================
86
+
87
+ <> a solid:TypeIndex, solid:ListedDocument .
88
+
89
+ # Type registrations will be added as data is populated.
90
+ # Example:
91
+ # <#medications> a solid:TypeRegistration ;
92
+ # solid:forClass health:MedicationRecord ;
93
+ # solid:instance </clinical/medications.ttl> .
94
+ #
95
+ # <#conditions> a solid:TypeRegistration ;
96
+ # solid:forClass health:ConditionRecord ;
97
+ # solid:instance </clinical/conditions.ttl> .
98
+ `;
99
+ const PRIVATE_TYPE_INDEX_TTL = `@prefix solid: <http://www.w3.org/ns/solid/terms#> .
100
+ @prefix health: <https://ns.cascadeprotocol.org/health/v1#> .
101
+ @prefix clinical: <https://ns.cascadeprotocol.org/clinical/v1#> .
102
+
103
+ # =============================================================================
104
+ # Private Type Index
105
+ # =============================================================================
106
+ # Maps wellness and device data types to their storage locations.
107
+ # Private registrations require explicit authorization to access.
108
+ #
109
+ # Wellness data (heart rate, activity, sleep, etc.) is typically registered
110
+ # here rather than in the public type index.
111
+ # =============================================================================
112
+
113
+ <> a solid:TypeIndex, solid:UnlistedDocument .
114
+
115
+ # Type registrations will be added as wellness data is populated.
116
+ # Example:
117
+ # <#heartRate> a solid:TypeRegistration ;
118
+ # solid:forClass health:HeartRateData ;
119
+ # solid:instance </wellness/heart-rate.ttl> .
120
+ `;
121
+ function indexTtl(dirName) {
122
+ const now = new Date().toISOString();
123
+ return `@prefix ldp: <http://www.w3.org/ns/ldp#> .
124
+ @prefix dcterms: <http://purl.org/dc/terms/> .
125
+ @prefix cascade: <https://ns.cascadeprotocol.org/core/v1#> .
126
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
127
+
128
+ # =============================================================================
129
+ # Root LDP Container
130
+ # =============================================================================
131
+ # This is the root index of this Cascade Pod, structured as an LDP Basic
132
+ # Container per Solid Protocol conventions. It enumerates every resource
133
+ # in the Pod for discoverability by agents and applications.
134
+ #
135
+ # Update this file as you add or remove resources.
136
+ # =============================================================================
137
+
138
+ <> a ldp:BasicContainer ;
139
+ dcterms:title "${dirName} -- Cascade Pod" ;
140
+ dcterms:description "A Cascade Protocol Pod initialized with the cascade CLI." ;
141
+ dcterms:created "${now}"^^xsd:dateTime ;
142
+ cascade:schemaVersion "1.3" ;
143
+
144
+ # ── Profile & Settings ──
145
+ ldp:contains
146
+ <profile/card.ttl> ,
147
+ <settings/publicTypeIndex.ttl> ,
148
+ <settings/privateTypeIndex.ttl> .
149
+
150
+ # ── Add clinical and wellness resources below as they are created ──
151
+ # ldp:contains <clinical/medications.ttl> .
152
+ # ldp:contains <wellness/heart-rate.ttl> .
153
+ `;
154
+ }
155
+ const README_MD = `# Cascade Protocol Pod
156
+
157
+ This directory is a **Cascade Protocol Pod** -- a portable, self-describing collection of personal health data serialized as RDF/Turtle files.
158
+
159
+ ## Structure
160
+
161
+ \`\`\`
162
+ .well-known/
163
+ solid # Pod discovery document (JSON)
164
+ profile/
165
+ card.ttl # WebID profile (identity + discovery links)
166
+ settings/
167
+ publicTypeIndex.ttl # Maps clinical data types to file locations
168
+ privateTypeIndex.ttl # Maps wellness data types to file locations
169
+ clinical/ # Clinical records (EHR-sourced data)
170
+ wellness/ # Wellness records (device and self-reported data)
171
+ index.ttl # Root LDP container listing all resources
172
+ \`\`\`
173
+
174
+ ## Getting Started
175
+
176
+ 1. Edit \`profile/card.ttl\` to set the Pod owner's name and demographics.
177
+ 2. Add clinical data files (e.g., \`clinical/medications.ttl\`) and register them in \`settings/publicTypeIndex.ttl\`.
178
+ 3. Add wellness data files (e.g., \`wellness/heart-rate.ttl\`) and register them in \`settings/privateTypeIndex.ttl\`.
179
+ 4. Update \`index.ttl\` to list all resources.
180
+
181
+ ## Useful Commands
182
+
183
+ \`\`\`bash
184
+ cascade pod info . # Show Pod summary
185
+ cascade pod query . --all # Query all data in the Pod
186
+ cascade pod export . --format zip # Export as ZIP archive
187
+ cascade validate . # Validate against SHACL shapes
188
+ \`\`\`
189
+
190
+ ## Learn More
191
+
192
+ - Cascade Protocol: https://cascadeprotocol.org
193
+ - Pod Structure Spec: https://cascadeprotocol.org/docs/spec/pod-structure
194
+ - Cascade SDK: https://github.com/nickthorpe71/cascade-sdk-swift
195
+ `;
196
+ const DATA_TYPES = {
197
+ medications: {
198
+ label: 'Medications',
199
+ rdfTypes: [CASCADE_NAMESPACES.health + 'MedicationRecord'],
200
+ directory: 'clinical',
201
+ filename: 'medications.ttl',
202
+ },
203
+ conditions: {
204
+ label: 'Conditions',
205
+ rdfTypes: [CASCADE_NAMESPACES.health + 'ConditionRecord'],
206
+ directory: 'clinical',
207
+ filename: 'conditions.ttl',
208
+ },
209
+ allergies: {
210
+ label: 'Allergies',
211
+ rdfTypes: [CASCADE_NAMESPACES.health + 'AllergyRecord'],
212
+ directory: 'clinical',
213
+ filename: 'allergies.ttl',
214
+ },
215
+ 'lab-results': {
216
+ label: 'Lab Results',
217
+ rdfTypes: [CASCADE_NAMESPACES.health + 'LabResultRecord'],
218
+ directory: 'clinical',
219
+ filename: 'lab-results.ttl',
220
+ },
221
+ immunizations: {
222
+ label: 'Immunizations',
223
+ rdfTypes: [CASCADE_NAMESPACES.health + 'ImmunizationRecord'],
224
+ directory: 'clinical',
225
+ filename: 'immunizations.ttl',
226
+ },
227
+ 'vital-signs': {
228
+ label: 'Vital Signs',
229
+ rdfTypes: [CASCADE_NAMESPACES.clinical + 'VitalSign'],
230
+ directory: 'clinical',
231
+ filename: 'vital-signs.ttl',
232
+ },
233
+ insurance: {
234
+ label: 'Insurance',
235
+ rdfTypes: [CASCADE_NAMESPACES.clinical + 'CoverageRecord'],
236
+ directory: 'clinical',
237
+ filename: 'insurance.ttl',
238
+ },
239
+ 'patient-profile': {
240
+ label: 'Patient Profile',
241
+ rdfTypes: [CASCADE_NAMESPACES.cascade + 'PatientProfile'],
242
+ directory: 'clinical',
243
+ filename: 'patient-profile.ttl',
244
+ },
245
+ 'heart-rate': {
246
+ label: 'Heart Rate',
247
+ rdfTypes: [CASCADE_NAMESPACES.health + 'DailyVitalReading', CASCADE_NAMESPACES.health + 'HeartRateData'],
248
+ directory: 'wellness',
249
+ filename: 'heart-rate.ttl',
250
+ },
251
+ 'blood-pressure': {
252
+ label: 'Blood Pressure',
253
+ rdfTypes: [
254
+ 'http://hl7.org/fhir/Observation',
255
+ CASCADE_NAMESPACES.health + 'BloodPressureData',
256
+ ],
257
+ directory: 'wellness',
258
+ filename: 'blood-pressure.ttl',
259
+ },
260
+ activity: {
261
+ label: 'Activity',
262
+ rdfTypes: [CASCADE_NAMESPACES.health + 'DailyActivitySnapshot', CASCADE_NAMESPACES.health + 'ActivityData'],
263
+ directory: 'wellness',
264
+ filename: 'activity.ttl',
265
+ },
266
+ sleep: {
267
+ label: 'Sleep',
268
+ rdfTypes: [CASCADE_NAMESPACES.health + 'DailySleepSnapshot', CASCADE_NAMESPACES.health + 'SleepData'],
269
+ directory: 'wellness',
270
+ filename: 'sleep.ttl',
271
+ },
272
+ supplements: {
273
+ label: 'Supplements',
274
+ rdfTypes: [CASCADE_NAMESPACES.clinical + 'Supplement'],
275
+ directory: 'wellness',
276
+ filename: 'supplements.ttl',
277
+ },
278
+ };
279
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
280
+ /**
281
+ * Resolve a pod directory path to an absolute path.
282
+ */
283
+ function resolvePodDir(podDir) {
284
+ return path.resolve(process.cwd(), podDir);
285
+ }
286
+ /**
287
+ * Check if a path exists and is a directory.
288
+ */
289
+ async function isDirectory(dirPath) {
290
+ try {
291
+ const stat = await fs.stat(dirPath);
292
+ return stat.isDirectory();
293
+ }
294
+ catch {
295
+ return false;
296
+ }
297
+ }
298
+ /**
299
+ * Check if a file exists.
300
+ */
301
+ async function fileExists(filePath) {
302
+ try {
303
+ await fs.access(filePath);
304
+ return true;
305
+ }
306
+ catch {
307
+ return false;
308
+ }
309
+ }
310
+ /**
311
+ * Discover all TTL files in a pod directory recursively.
312
+ */
313
+ async function discoverTtlFiles(podDir) {
314
+ const files = [];
315
+ async function walk(dir) {
316
+ const entries = await fs.readdir(dir, { withFileTypes: true });
317
+ for (const entry of entries) {
318
+ const fullPath = path.join(dir, entry.name);
319
+ if (entry.isDirectory() && !entry.name.startsWith('.')) {
320
+ await walk(fullPath);
321
+ }
322
+ else if (entry.isFile() && entry.name.endsWith('.ttl')) {
323
+ files.push(fullPath);
324
+ }
325
+ }
326
+ }
327
+ await walk(podDir);
328
+ return files.sort();
329
+ }
330
+ /**
331
+ * Parse a single TTL file and extract typed records.
332
+ */
333
+ async function parseDataFile(filePath) {
334
+ const result = await parseTurtleFile(filePath);
335
+ if (!result.success) {
336
+ return { records: [], totalQuads: 0, error: result.errors.join('; ') };
337
+ }
338
+ const records = [];
339
+ for (const subject of result.subjects) {
340
+ // Skip blank nodes that are just structural (e.g., nested blank nodes for provenance)
341
+ // Keep named subjects (URNs, URIs) and typed blank nodes with meaningful types
342
+ const meaningfulTypes = subject.types.filter((t) => !t.startsWith('http://www.w3.org/ns/prov#') &&
343
+ t !== 'http://www.w3.org/ns/solid/terms#TypeRegistration' &&
344
+ t !== 'http://www.w3.org/ns/solid/terms#TypeIndex' &&
345
+ t !== 'http://www.w3.org/ns/solid/terms#ListedDocument' &&
346
+ t !== 'http://www.w3.org/ns/solid/terms#UnlistedDocument' &&
347
+ t !== 'http://www.w3.org/ns/ldp#BasicContainer');
348
+ if (meaningfulTypes.length === 0)
349
+ continue;
350
+ const props = getProperties(result.store, subject.uri);
351
+ const label = extractLabel(props);
352
+ // Flatten properties for display (take first value of each, shorten IRIs)
353
+ const flatProps = {};
354
+ for (const [pred, values] of Object.entries(props)) {
355
+ const shortPred = shortenIRI(pred);
356
+ // Skip rdf:type since we have it separately
357
+ if (pred === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type')
358
+ continue;
359
+ flatProps[shortPred] = values.length === 1 ? values[0] : values.join(', ');
360
+ }
361
+ records.push({
362
+ id: subject.uri,
363
+ type: shortenIRI(meaningfulTypes[0]),
364
+ label,
365
+ properties: flatProps,
366
+ });
367
+ }
368
+ return { records, totalQuads: result.quadCount };
369
+ }
370
+ /**
371
+ * Read the patient profile from a pod to extract name, age, schema version.
372
+ */
373
+ async function readPatientProfile(podDir) {
374
+ // Try clinical/patient-profile.ttl first, then profile/card.ttl
375
+ const profilePaths = [
376
+ path.join(podDir, 'clinical', 'patient-profile.ttl'),
377
+ path.join(podDir, 'profile', 'card.ttl'),
378
+ ];
379
+ let name;
380
+ let age;
381
+ let schemaVersion;
382
+ let dateOfBirth;
383
+ for (const profilePath of profilePaths) {
384
+ if (!(await fileExists(profilePath)))
385
+ continue;
386
+ const result = await parseTurtleFile(profilePath);
387
+ if (!result.success)
388
+ continue;
389
+ for (const subject of result.subjects) {
390
+ const props = getProperties(result.store, subject.uri);
391
+ if (!name) {
392
+ name = props['http://xmlns.com/foaf/0.1/name']?.[0];
393
+ }
394
+ if (!age) {
395
+ age = props[CASCADE_NAMESPACES.cascade + 'computedAge']?.[0];
396
+ }
397
+ if (!schemaVersion) {
398
+ schemaVersion = props[CASCADE_NAMESPACES.cascade + 'schemaVersion']?.[0];
399
+ }
400
+ if (!dateOfBirth) {
401
+ dateOfBirth = props[CASCADE_NAMESPACES.cascade + 'dateOfBirth']?.[0];
402
+ }
403
+ }
404
+ }
405
+ return { name, age, schemaVersion, dateOfBirth };
406
+ }
407
+ // ─── Command Registration ────────────────────────────────────────────────────
408
+ export function registerPodCommand(program) {
409
+ const pod = program.command('pod').description('Manage Cascade Pod structures');
410
+ // ── cascade pod init <directory> ───────────────────────────────────────────
411
+ pod
412
+ .command('init')
413
+ .description('Initialize a new Cascade Pod')
414
+ .argument('<directory>', 'Directory to initialize as a Cascade Pod')
415
+ .action(async (directory) => {
416
+ const globalOpts = program.opts();
417
+ const absDir = resolvePodDir(directory);
418
+ const dirName = path.basename(absDir);
419
+ printVerbose(`Initializing pod at: ${absDir}`, globalOpts);
420
+ try {
421
+ // Check if directory already has pod structure
422
+ if (await fileExists(path.join(absDir, 'index.ttl'))) {
423
+ printError(`Directory already contains a Cascade Pod: ${absDir}`, globalOpts);
424
+ process.exitCode = 1;
425
+ return;
426
+ }
427
+ // Create directory structure
428
+ const dirs = [
429
+ path.join(absDir, '.well-known'),
430
+ path.join(absDir, 'profile'),
431
+ path.join(absDir, 'settings'),
432
+ path.join(absDir, 'clinical'),
433
+ path.join(absDir, 'wellness'),
434
+ ];
435
+ for (const dir of dirs) {
436
+ await fs.mkdir(dir, { recursive: true });
437
+ }
438
+ // Write template files
439
+ await fs.writeFile(path.join(absDir, '.well-known', 'solid'), wellKnownSolid(absDir));
440
+ await fs.writeFile(path.join(absDir, 'profile', 'card.ttl'), PROFILE_CARD_TTL);
441
+ await fs.writeFile(path.join(absDir, 'settings', 'publicTypeIndex.ttl'), PUBLIC_TYPE_INDEX_TTL);
442
+ await fs.writeFile(path.join(absDir, 'settings', 'privateTypeIndex.ttl'), PRIVATE_TYPE_INDEX_TTL);
443
+ await fs.writeFile(path.join(absDir, 'index.ttl'), indexTtl(dirName));
444
+ await fs.writeFile(path.join(absDir, 'README.md'), README_MD);
445
+ const filesCreated = [
446
+ '.well-known/solid',
447
+ 'profile/card.ttl',
448
+ 'settings/publicTypeIndex.ttl',
449
+ 'settings/privateTypeIndex.ttl',
450
+ 'clinical/',
451
+ 'wellness/',
452
+ 'index.ttl',
453
+ 'README.md',
454
+ ];
455
+ if (globalOpts.json) {
456
+ printResult({
457
+ status: 'created',
458
+ directory: absDir,
459
+ files: filesCreated,
460
+ message: 'Cascade Pod initialized successfully.',
461
+ }, globalOpts);
462
+ }
463
+ else {
464
+ console.log(`Cascade Pod initialized at: ${absDir}\n`);
465
+ console.log('Created:');
466
+ for (const f of filesCreated) {
467
+ console.log(` ${f}`);
468
+ }
469
+ console.log('\nNext steps:');
470
+ console.log(' 1. Edit profile/card.ttl to set patient name and demographics');
471
+ console.log(' 2. Add data files to clinical/ and wellness/ directories');
472
+ console.log(' 3. Register data types in settings/publicTypeIndex.ttl');
473
+ console.log(` 4. Run: cascade pod info ${directory}`);
474
+ }
475
+ }
476
+ catch (err) {
477
+ const message = err instanceof Error ? err.message : String(err);
478
+ printError(`Failed to initialize pod: ${message}`, globalOpts);
479
+ process.exitCode = 1;
480
+ }
481
+ });
482
+ // ── cascade pod query <pod-dir> ────────────────────────────────────────────
483
+ pod
484
+ .command('query')
485
+ .description('Query data within a pod')
486
+ .argument('<pod-dir>', 'Path to the Cascade Pod')
487
+ .option('--medications', 'Query medications')
488
+ .option('--conditions', 'Query conditions')
489
+ .option('--allergies', 'Query allergies')
490
+ .option('--lab-results', 'Query lab results')
491
+ .option('--immunizations', 'Query immunizations')
492
+ .option('--vital-signs', 'Query vital signs')
493
+ .option('--supplements', 'Query supplements')
494
+ .option('--all', 'Query all data')
495
+ .action(async (podDir, options) => {
496
+ const globalOpts = program.opts();
497
+ const absDir = resolvePodDir(podDir);
498
+ printVerbose(`Querying pod: ${absDir}`, globalOpts);
499
+ printVerbose(`Filters: ${JSON.stringify(options)}`, globalOpts);
500
+ // Validate pod exists
501
+ if (!(await isDirectory(absDir))) {
502
+ printError(`Pod directory not found: ${absDir}`, globalOpts);
503
+ process.exitCode = 1;
504
+ return;
505
+ }
506
+ try {
507
+ // Determine which data types to query
508
+ let requestedTypes;
509
+ if (options.all) {
510
+ // Discover all TTL files in the pod
511
+ requestedTypes = Object.keys(DATA_TYPES);
512
+ }
513
+ else {
514
+ requestedTypes = [];
515
+ if (options.medications)
516
+ requestedTypes.push('medications');
517
+ if (options.conditions)
518
+ requestedTypes.push('conditions');
519
+ if (options.allergies)
520
+ requestedTypes.push('allergies');
521
+ if (options.labResults)
522
+ requestedTypes.push('lab-results');
523
+ if (options.immunizations)
524
+ requestedTypes.push('immunizations');
525
+ if (options.vitalSigns)
526
+ requestedTypes.push('vital-signs');
527
+ if (options.supplements)
528
+ requestedTypes.push('supplements');
529
+ }
530
+ if (requestedTypes.length === 0) {
531
+ printError('No query filter specified. Use --medications, --conditions, --all, etc.', globalOpts);
532
+ process.exitCode = 1;
533
+ return;
534
+ }
535
+ // Process each requested data type
536
+ const queryResults = {};
537
+ // If --all, also discover any TTL files not in the registry
538
+ const extraFiles = [];
539
+ if (options.all) {
540
+ const allTtlFiles = await discoverTtlFiles(absDir);
541
+ const knownPaths = new Set(Object.values(DATA_TYPES).map((dt) => path.join(absDir, dt.directory, dt.filename)));
542
+ // Also exclude index.ttl, manifest.ttl, profile/card.ttl, type indexes
543
+ const excludePaths = new Set([
544
+ path.join(absDir, 'index.ttl'),
545
+ path.join(absDir, 'manifest.ttl'),
546
+ path.join(absDir, 'profile', 'card.ttl'),
547
+ path.join(absDir, 'settings', 'publicTypeIndex.ttl'),
548
+ path.join(absDir, 'settings', 'privateTypeIndex.ttl'),
549
+ ]);
550
+ for (const f of allTtlFiles) {
551
+ if (!knownPaths.has(f) && !excludePaths.has(f)) {
552
+ extraFiles.push(f);
553
+ }
554
+ }
555
+ }
556
+ for (const typeName of requestedTypes) {
557
+ const typeInfo = DATA_TYPES[typeName];
558
+ if (!typeInfo)
559
+ continue;
560
+ const filePath = path.join(absDir, typeInfo.directory, typeInfo.filename);
561
+ if (!(await fileExists(filePath))) {
562
+ printVerbose(`Skipping ${typeName}: file not found at ${filePath}`, globalOpts);
563
+ continue;
564
+ }
565
+ const { records, error } = await parseDataFile(filePath);
566
+ queryResults[typeName] = {
567
+ count: records.length,
568
+ file: `${typeInfo.directory}/${typeInfo.filename}`,
569
+ records: records.map((r) => ({
570
+ id: r.id,
571
+ type: r.type,
572
+ properties: r.properties,
573
+ })),
574
+ error,
575
+ };
576
+ }
577
+ // Process extra files found in --all mode
578
+ for (const extraFile of extraFiles) {
579
+ const relPath = path.relative(absDir, extraFile);
580
+ const baseName = path.basename(extraFile, '.ttl');
581
+ const { records, error } = await parseDataFile(extraFile);
582
+ if (records.length > 0) {
583
+ queryResults[baseName] = {
584
+ count: records.length,
585
+ file: relPath,
586
+ records: records.map((r) => ({
587
+ id: r.id,
588
+ type: r.type,
589
+ properties: r.properties,
590
+ })),
591
+ error,
592
+ };
593
+ }
594
+ }
595
+ // Output results
596
+ if (globalOpts.json) {
597
+ printResult({
598
+ pod: podDir,
599
+ dataTypes: queryResults,
600
+ }, globalOpts);
601
+ }
602
+ else {
603
+ // Human-readable output
604
+ const typeKeys = Object.keys(queryResults);
605
+ if (typeKeys.length === 0) {
606
+ console.log('No data found for the specified query filters.');
607
+ return;
608
+ }
609
+ for (const typeName of typeKeys) {
610
+ const data = queryResults[typeName];
611
+ const typeInfo = DATA_TYPES[typeName];
612
+ const displayLabel = typeInfo?.label ?? typeName;
613
+ console.log(`\n=== ${displayLabel} (${data.count} records) ===`);
614
+ if (data.error) {
615
+ console.log(` Error: ${data.error}`);
616
+ continue;
617
+ }
618
+ console.log(` File: ${data.file}\n`);
619
+ for (let i = 0; i < data.records.length; i++) {
620
+ const rec = data.records[i];
621
+ const label = extractLabelFromProps(rec.properties);
622
+ const idShort = rec.id.length > 40 ? rec.id.substring(0, 40) + '...' : rec.id;
623
+ console.log(` ${i + 1}. ${label ?? rec.type} (${idShort})`);
624
+ // Show key properties
625
+ const keyProps = selectKeyProperties(typeName, rec.properties);
626
+ for (const [key, value] of Object.entries(keyProps)) {
627
+ console.log(` ${key}: ${value}`);
628
+ }
629
+ console.log('');
630
+ }
631
+ }
632
+ }
633
+ }
634
+ catch (err) {
635
+ const message = err instanceof Error ? err.message : String(err);
636
+ printError(`Failed to query pod: ${message}`, globalOpts);
637
+ process.exitCode = 1;
638
+ }
639
+ });
640
+ // ── cascade pod export <pod-dir> ───────────────────────────────────────────
641
+ pod
642
+ .command('export')
643
+ .description('Export pod data')
644
+ .argument('<pod-dir>', 'Path to the Cascade Pod')
645
+ .option('--format <fmt>', 'Export format (zip|directory)', 'zip')
646
+ .option('--output <path>', 'Output path for export')
647
+ .action(async (podDir, options) => {
648
+ const globalOpts = program.opts();
649
+ const absDir = resolvePodDir(podDir);
650
+ printVerbose(`Exporting pod: ${absDir} as ${options.format}`, globalOpts);
651
+ // Validate pod exists
652
+ if (!(await isDirectory(absDir))) {
653
+ printError(`Pod directory not found: ${absDir}`, globalOpts);
654
+ process.exitCode = 1;
655
+ return;
656
+ }
657
+ try {
658
+ if (options.format === 'directory') {
659
+ // Copy to new directory
660
+ const outputDir = options.output ?? `${absDir}-export`;
661
+ await copyDirectory(absDir, outputDir);
662
+ if (globalOpts.json) {
663
+ printResult({
664
+ status: 'exported',
665
+ format: 'directory',
666
+ source: absDir,
667
+ output: outputDir,
668
+ }, globalOpts);
669
+ }
670
+ else {
671
+ console.log(`Pod exported to directory: ${outputDir}`);
672
+ }
673
+ }
674
+ else if (options.format === 'zip') {
675
+ // Create ZIP archive
676
+ const outputZip = options.output ?? `${path.basename(absDir)}.zip`;
677
+ const absOutputZip = path.resolve(process.cwd(), outputZip);
678
+ await createZipArchive(absDir, absOutputZip);
679
+ if (globalOpts.json) {
680
+ printResult({
681
+ status: 'exported',
682
+ format: 'zip',
683
+ source: absDir,
684
+ output: absOutputZip,
685
+ }, globalOpts);
686
+ }
687
+ else {
688
+ console.log(`Pod exported to ZIP: ${absOutputZip}`);
689
+ }
690
+ }
691
+ else {
692
+ printError(`Unknown export format: ${options.format}. Use 'zip' or 'directory'.`, globalOpts);
693
+ process.exitCode = 1;
694
+ }
695
+ }
696
+ catch (err) {
697
+ const message = err instanceof Error ? err.message : String(err);
698
+ printError(`Failed to export pod: ${message}`, globalOpts);
699
+ process.exitCode = 1;
700
+ }
701
+ });
702
+ // ── cascade pod info <pod-dir> ─────────────────────────────────────────────
703
+ pod
704
+ .command('info')
705
+ .description('Show pod metadata and statistics')
706
+ .argument('<pod-dir>', 'Path to the Cascade Pod')
707
+ .action(async (podDir) => {
708
+ const globalOpts = program.opts();
709
+ const absDir = resolvePodDir(podDir);
710
+ printVerbose(`Getting info for pod: ${absDir}`, globalOpts);
711
+ // Validate pod exists
712
+ if (!(await isDirectory(absDir))) {
713
+ printError(`Pod directory not found: ${absDir}`, globalOpts);
714
+ process.exitCode = 1;
715
+ return;
716
+ }
717
+ try {
718
+ // Read patient profile info
719
+ const profile = await readPatientProfile(absDir);
720
+ // Scan data files
721
+ const clinicalSummary = [];
722
+ const wellnessSummary = [];
723
+ const provenanceSources = new Set();
724
+ // Get last modified time of the pod
725
+ let lastModified;
726
+ const allTtlFiles = await discoverTtlFiles(absDir);
727
+ for (const filePath of allTtlFiles) {
728
+ const stat = await fs.stat(filePath);
729
+ if (!lastModified || stat.mtime > lastModified) {
730
+ lastModified = stat.mtime;
731
+ }
732
+ }
733
+ // Analyze each known data type
734
+ for (const [, typeInfo] of Object.entries(DATA_TYPES)) {
735
+ const filePath = path.join(absDir, typeInfo.directory, typeInfo.filename);
736
+ if (!(await fileExists(filePath)))
737
+ continue;
738
+ const result = await parseTurtleFile(filePath);
739
+ if (!result.success)
740
+ continue;
741
+ // Count records by type
742
+ let recordCount = 0;
743
+ for (const rdfType of typeInfo.rdfTypes) {
744
+ recordCount += getSubjectsByType(result.store, rdfType).length;
745
+ }
746
+ // If no records found by type, count all typed subjects
747
+ if (recordCount === 0 && result.subjects.length > 0) {
748
+ recordCount = result.subjects.length;
749
+ }
750
+ // Detect provenance
751
+ const provenanceValues = new Set();
752
+ for (const subject of result.subjects) {
753
+ const props = getProperties(result.store, subject.uri);
754
+ const prov = props[CASCADE_NAMESPACES.cascade + 'dataProvenance'];
755
+ if (prov) {
756
+ for (const p of prov) {
757
+ const shortProv = normalizeProvenanceLabel(shortenIRI(p));
758
+ provenanceValues.add(shortProv);
759
+ provenanceSources.add(shortProv);
760
+ }
761
+ }
762
+ }
763
+ // For wellness files, also check for prov:wasGeneratedBy / cascade:sourceType
764
+ // which indicates DeviceGenerated provenance
765
+ if (provenanceValues.size === 0) {
766
+ const allQuads = result.quads;
767
+ const hasDeviceSource = allQuads.some((q) => (q.predicate.value === CASCADE_NAMESPACES.cascade + 'sourceType' &&
768
+ (q.object.value === 'healthKit' || q.object.value === 'bluetoothDevice')) ||
769
+ // Also detect device provenance from prov:wasGeneratedBy patterns
770
+ (q.predicate.value === 'http://www.w3.org/ns/prov#wasGeneratedBy'));
771
+ // If in wellness directory and has device data patterns, infer DeviceGenerated
772
+ if (hasDeviceSource || typeInfo.directory === 'wellness') {
773
+ const hasDeviceTypes = allQuads.some((q) => q.predicate.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' &&
774
+ (q.object.value.includes('HeartRateData') ||
775
+ q.object.value.includes('BloodPressureData') ||
776
+ q.object.value.includes('ActivityData') ||
777
+ q.object.value.includes('SleepData') ||
778
+ q.object.value.includes('DailyVitalReading') ||
779
+ q.object.value.includes('DailyActivitySnapshot') ||
780
+ q.object.value.includes('DailySleepSnapshot') ||
781
+ q.object.value === 'http://hl7.org/fhir/Observation'));
782
+ if (hasDeviceSource || hasDeviceTypes) {
783
+ provenanceValues.add('cascade:DeviceGenerated');
784
+ provenanceSources.add('cascade:DeviceGenerated');
785
+ }
786
+ }
787
+ }
788
+ const provenanceStr = provenanceValues.size > 0
789
+ ? Array.from(provenanceValues).join(', ')
790
+ : 'Unknown';
791
+ // Determine record description
792
+ let recordDesc;
793
+ // For time-series data (vital signs, heart rate, etc.), show as "X days" if applicable
794
+ const isTimeSeries = ['vital-signs', 'heart-rate', 'blood-pressure', 'activity', 'sleep'].some((ts) => typeInfo.filename.includes(ts.replace('-', '-')));
795
+ if (isTimeSeries && recordCount >= 28) {
796
+ recordDesc = `${recordCount} days`;
797
+ }
798
+ else if (recordCount === 1) {
799
+ recordDesc = '1 record';
800
+ }
801
+ else {
802
+ recordDesc = `${recordCount} records`;
803
+ }
804
+ const entry = {
805
+ file: typeInfo.filename,
806
+ records: recordCount,
807
+ provenance: provenanceStr,
808
+ label: `${typeInfo.filename.padEnd(22)} ${recordDesc.padEnd(16)} (${provenanceStr})`,
809
+ };
810
+ if (typeInfo.directory === 'clinical') {
811
+ clinicalSummary.push(entry);
812
+ }
813
+ else {
814
+ wellnessSummary.push(entry);
815
+ }
816
+ }
817
+ if (globalOpts.json) {
818
+ printResult({
819
+ pod: podDir,
820
+ patient: {
821
+ name: profile.name,
822
+ age: profile.age,
823
+ dateOfBirth: profile.dateOfBirth,
824
+ },
825
+ schemaVersion: profile.schemaVersion,
826
+ lastModified: lastModified?.toISOString(),
827
+ clinical: clinicalSummary.map((s) => ({
828
+ file: s.file,
829
+ records: s.records,
830
+ provenance: s.provenance,
831
+ })),
832
+ wellness: wellnessSummary.map((s) => ({
833
+ file: s.file,
834
+ records: s.records,
835
+ provenance: s.provenance,
836
+ })),
837
+ provenanceSources: Array.from(provenanceSources),
838
+ }, globalOpts);
839
+ }
840
+ else {
841
+ // Human-readable output
842
+ console.log(`\nCascade Pod: ${podDir}\n`);
843
+ if (profile.name) {
844
+ const ageStr = profile.age ? ` (age ${profile.age})` : '';
845
+ console.log(`Patient: ${profile.name}${ageStr}`);
846
+ }
847
+ if (profile.schemaVersion) {
848
+ console.log(`Schema Version: ${profile.schemaVersion}`);
849
+ }
850
+ if (lastModified) {
851
+ console.log(`Last Modified: ${lastModified.toISOString().split('T')[0]}`);
852
+ }
853
+ if (clinicalSummary.length > 0) {
854
+ console.log('\nData Summary:');
855
+ console.log(' Clinical:');
856
+ for (const entry of clinicalSummary) {
857
+ console.log(` ${entry.label}`);
858
+ }
859
+ }
860
+ if (wellnessSummary.length > 0) {
861
+ if (clinicalSummary.length === 0) {
862
+ console.log('\nData Summary:');
863
+ }
864
+ console.log(' Wellness:');
865
+ for (const entry of wellnessSummary) {
866
+ console.log(` ${entry.label}`);
867
+ }
868
+ }
869
+ if (provenanceSources.size > 0) {
870
+ console.log(`\nProvenance Sources: ${Array.from(provenanceSources).join(', ')}`);
871
+ }
872
+ if (clinicalSummary.length === 0 && wellnessSummary.length === 0) {
873
+ console.log('\nThis pod has no data files yet.');
874
+ console.log('Add TTL files to the clinical/ or wellness/ directories to get started.');
875
+ }
876
+ console.log('');
877
+ }
878
+ }
879
+ catch (err) {
880
+ const message = err instanceof Error ? err.message : String(err);
881
+ printError(`Failed to read pod info: ${message}`, globalOpts);
882
+ process.exitCode = 1;
883
+ }
884
+ });
885
+ }
886
+ // ─── Export Helpers ──────────────────────────────────────────────────────────
887
+ /**
888
+ * Recursively copy a directory.
889
+ */
890
+ async function copyDirectory(src, dest) {
891
+ await fs.mkdir(dest, { recursive: true });
892
+ const entries = await fs.readdir(src, { withFileTypes: true });
893
+ for (const entry of entries) {
894
+ const srcPath = path.join(src, entry.name);
895
+ const destPath = path.join(dest, entry.name);
896
+ if (entry.isDirectory()) {
897
+ await copyDirectory(srcPath, destPath);
898
+ }
899
+ else {
900
+ await fs.copyFile(srcPath, destPath);
901
+ }
902
+ }
903
+ }
904
+ /**
905
+ * Create a ZIP archive of the pod directory using the archiver package.
906
+ */
907
+ async function createZipArchive(sourceDir, outputPath) {
908
+ // Dynamic import of archiver
909
+ let archiverModule;
910
+ try {
911
+ archiverModule = await import('archiver');
912
+ }
913
+ catch {
914
+ throw new Error('The "archiver" package is required for ZIP export. ' +
915
+ 'Install it with: npm install archiver');
916
+ }
917
+ const { createWriteStream } = await import('fs');
918
+ return new Promise((resolve, reject) => {
919
+ const output = createWriteStream(outputPath);
920
+ const archive = archiverModule.default('zip', { zlib: { level: 9 } });
921
+ output.on('close', () => resolve());
922
+ archive.on('error', (err) => reject(err));
923
+ archive.pipe(output);
924
+ archive.directory(sourceDir, path.basename(sourceDir));
925
+ void archive.finalize();
926
+ });
927
+ }
928
+ // ─── Display Helpers ─────────────────────────────────────────────────────────
929
+ /**
930
+ * Normalize provenance label for consistent display.
931
+ * Converts "core:ClinicalGenerated" to "cascade:ClinicalGenerated" since
932
+ * the "core" and "cascade" prefixes map to the same namespace.
933
+ */
934
+ function normalizeProvenanceLabel(label) {
935
+ if (label.startsWith('core:')) {
936
+ return 'cascade:' + label.slice(5);
937
+ }
938
+ return label;
939
+ }
940
+ /**
941
+ * Extract a display label from already-shortened property keys.
942
+ */
943
+ function extractLabelFromProps(properties) {
944
+ const labelKeys = [
945
+ 'health:medicationName',
946
+ 'health:conditionName',
947
+ 'health:allergen',
948
+ 'clinical:supplementName',
949
+ 'clinical:vaccineName',
950
+ 'health:vaccineName',
951
+ 'health:testName',
952
+ 'health:labTestName',
953
+ 'foaf:name',
954
+ 'dcterms:title',
955
+ ];
956
+ for (const key of labelKeys) {
957
+ if (properties[key]) {
958
+ return properties[key];
959
+ }
960
+ }
961
+ return undefined;
962
+ }
963
+ /**
964
+ * Select the most relevant properties for display based on data type.
965
+ */
966
+ function selectKeyProperties(typeName, properties) {
967
+ const result = {};
968
+ // Common properties to always show if present
969
+ const commonKeys = ['cascade:dataProvenance', 'cascade:schemaVersion'];
970
+ // Type-specific key properties
971
+ const typeKeys = {
972
+ medications: [
973
+ 'health:dose',
974
+ 'health:frequency',
975
+ 'health:route',
976
+ 'health:isActive',
977
+ 'health:startDate',
978
+ 'health:prescriber',
979
+ 'health:rxNormCode',
980
+ 'health:medicationClass',
981
+ ],
982
+ conditions: [
983
+ 'health:status',
984
+ 'health:onsetDate',
985
+ 'health:icd10Code',
986
+ 'health:snomedCode',
987
+ 'health:conditionClass',
988
+ ],
989
+ allergies: [
990
+ 'health:allergyCategory',
991
+ 'health:reaction',
992
+ 'health:allergySeverity',
993
+ 'health:onsetDate',
994
+ ],
995
+ 'lab-results': [
996
+ 'health:value',
997
+ 'health:unit',
998
+ 'health:referenceRange',
999
+ 'health:interpretation',
1000
+ 'health:effectiveDate',
1001
+ ],
1002
+ immunizations: [
1003
+ 'health:vaccineDate',
1004
+ 'health:lotNumber',
1005
+ 'health:site',
1006
+ 'health:manufacturer',
1007
+ ],
1008
+ supplements: [
1009
+ 'clinical:dose',
1010
+ 'clinical:frequency',
1011
+ 'clinical:form',
1012
+ 'clinical:isActive',
1013
+ 'clinical:evidenceStrength',
1014
+ ],
1015
+ };
1016
+ const keysToShow = [...(typeKeys[typeName] ?? []), ...commonKeys];
1017
+ for (const key of keysToShow) {
1018
+ if (properties[key]) {
1019
+ result[key] = properties[key];
1020
+ }
1021
+ }
1022
+ // If no specific keys matched, show first few properties
1023
+ if (Object.keys(result).length === 0) {
1024
+ const allKeys = Object.keys(properties);
1025
+ for (const key of allKeys.slice(0, 5)) {
1026
+ result[key] = properties[key];
1027
+ }
1028
+ }
1029
+ return result;
1030
+ }
1031
+ //# sourceMappingURL=pod.js.map