@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,32 @@
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
+ * This module delegates to focused subcommand modules:
13
+ * - init.ts Pod initialization with templates
14
+ * - query.ts Data querying by type
15
+ * - export.ts Pod export (zip/directory)
16
+ * - info.ts Pod metadata and statistics
17
+ */
18
+
19
+ import { Command } from 'commander';
20
+ import { registerInitSubcommand } from './init.js';
21
+ import { registerQuerySubcommand } from './query.js';
22
+ import { registerExportSubcommand } from './export.js';
23
+ import { registerInfoSubcommand } from './info.js';
24
+
25
+ export function registerPodCommand(program: Command): void {
26
+ const pod = program.command('pod').description('Manage Cascade Pod structures');
27
+
28
+ registerInitSubcommand(pod, program);
29
+ registerQuerySubcommand(pod, program);
30
+ registerExportSubcommand(pod, program);
31
+ registerInfoSubcommand(pod, program);
32
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * cascade pod info <pod-dir>
3
+ *
4
+ * Show pod metadata and statistics, including patient profile,
5
+ * data file summary, and provenance information.
6
+ */
7
+
8
+ import type { Command } from 'commander';
9
+ import * as fs from 'fs/promises';
10
+ import * as path from 'path';
11
+ import { printResult, printError, printVerbose, type OutputOptions } from '../../lib/output.js';
12
+ import {
13
+ parseTurtleFile,
14
+ getSubjectsByType,
15
+ getProperties,
16
+ shortenIRI,
17
+ } from '../../lib/turtle-parser.js';
18
+ import {
19
+ DATA_TYPES,
20
+ CASCADE_NAMESPACES,
21
+ resolvePodDir,
22
+ isDirectory,
23
+ fileExists,
24
+ discoverTtlFiles,
25
+ readPatientProfile,
26
+ normalizeProvenanceLabel,
27
+ } from './helpers.js';
28
+
29
+ export function registerInfoSubcommand(pod: Command, program: Command): void {
30
+ pod
31
+ .command('info')
32
+ .description('Show pod metadata and statistics')
33
+ .argument('<pod-dir>', 'Path to the Cascade Pod')
34
+ .action(async (podDir: string) => {
35
+ const globalOpts = program.opts() as OutputOptions;
36
+ const absDir = resolvePodDir(podDir);
37
+
38
+ printVerbose(`Getting info for pod: ${absDir}`, globalOpts);
39
+
40
+ // Validate pod exists
41
+ if (!(await isDirectory(absDir))) {
42
+ printError(`Pod directory not found: ${absDir}`, globalOpts);
43
+ process.exitCode = 1;
44
+ return;
45
+ }
46
+
47
+ try {
48
+ // Read patient profile info
49
+ const profile = await readPatientProfile(absDir);
50
+
51
+ // Scan data files
52
+ const clinicalSummary: Array<{ file: string; records: number; provenance: string; label: string }> = [];
53
+ const wellnessSummary: Array<{ file: string; records: number; provenance: string; label: string }> = [];
54
+ const provenanceSources = new Set<string>();
55
+
56
+ // Get last modified time of the pod
57
+ let lastModified: Date | undefined;
58
+ const allTtlFiles = await discoverTtlFiles(absDir);
59
+
60
+ for (const filePath of allTtlFiles) {
61
+ const stat = await fs.stat(filePath);
62
+ if (!lastModified || stat.mtime > lastModified) {
63
+ lastModified = stat.mtime;
64
+ }
65
+ }
66
+
67
+ // Analyze each known data type
68
+ for (const [, typeInfo] of Object.entries(DATA_TYPES)) {
69
+ const filePath = path.join(absDir, typeInfo.directory, typeInfo.filename);
70
+ if (!(await fileExists(filePath))) continue;
71
+
72
+ const result = await parseTurtleFile(filePath);
73
+ if (!result.success) continue;
74
+
75
+ // Count records by type
76
+ let recordCount = 0;
77
+ for (const rdfType of typeInfo.rdfTypes) {
78
+ recordCount += getSubjectsByType(result.store, rdfType).length;
79
+ }
80
+
81
+ // If no records found by type, count all typed subjects
82
+ if (recordCount === 0 && result.subjects.length > 0) {
83
+ recordCount = result.subjects.length;
84
+ }
85
+
86
+ // Detect provenance
87
+ const provenanceValues = new Set<string>();
88
+ for (const subject of result.subjects) {
89
+ const props = getProperties(result.store, subject.uri);
90
+ const prov = props[CASCADE_NAMESPACES.cascade + 'dataProvenance'];
91
+ if (prov) {
92
+ for (const p of prov) {
93
+ const shortProv = normalizeProvenanceLabel(shortenIRI(p));
94
+ provenanceValues.add(shortProv);
95
+ provenanceSources.add(shortProv);
96
+ }
97
+ }
98
+ }
99
+
100
+ // For wellness files, also check for prov:wasGeneratedBy / cascade:sourceType
101
+ // which indicates DeviceGenerated provenance
102
+ if (provenanceValues.size === 0) {
103
+ const allQuads = result.quads;
104
+ const hasDeviceSource = allQuads.some(
105
+ (q) =>
106
+ (q.predicate.value === CASCADE_NAMESPACES.cascade + 'sourceType' &&
107
+ (q.object.value === 'healthKit' || q.object.value === 'bluetoothDevice')) ||
108
+ // Also detect device provenance from prov:wasGeneratedBy patterns
109
+ (q.predicate.value === 'http://www.w3.org/ns/prov#wasGeneratedBy'),
110
+ );
111
+ // If in wellness directory and has device data patterns, infer DeviceGenerated
112
+ if (hasDeviceSource || typeInfo.directory === 'wellness') {
113
+ const hasDeviceTypes = allQuads.some(
114
+ (q) =>
115
+ q.predicate.value === 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' &&
116
+ (q.object.value.includes('HeartRateData') ||
117
+ q.object.value.includes('BloodPressureData') ||
118
+ q.object.value.includes('ActivityData') ||
119
+ q.object.value.includes('SleepData') ||
120
+ q.object.value.includes('DailyVitalReading') ||
121
+ q.object.value.includes('DailyActivitySnapshot') ||
122
+ q.object.value.includes('DailySleepSnapshot') ||
123
+ q.object.value === 'http://hl7.org/fhir/Observation'),
124
+ );
125
+ if (hasDeviceSource || hasDeviceTypes) {
126
+ provenanceValues.add('cascade:DeviceGenerated');
127
+ provenanceSources.add('cascade:DeviceGenerated');
128
+ }
129
+ }
130
+ }
131
+
132
+ const provenanceStr = provenanceValues.size > 0
133
+ ? Array.from(provenanceValues).join(', ')
134
+ : 'Unknown';
135
+
136
+ // Determine record description
137
+ let recordDesc: string;
138
+ // For time-series data (vital signs, heart rate, etc.), show as "X days" if applicable
139
+ const isTimeSeries = ['vital-signs', 'heart-rate', 'blood-pressure', 'activity', 'sleep'].some(
140
+ (ts) => typeInfo.filename.includes(ts.replace('-', '-')),
141
+ );
142
+ if (isTimeSeries && recordCount >= 28) {
143
+ recordDesc = `${recordCount} days`;
144
+ } else if (recordCount === 1) {
145
+ recordDesc = '1 record';
146
+ } else {
147
+ recordDesc = `${recordCount} records`;
148
+ }
149
+
150
+ const entry = {
151
+ file: typeInfo.filename,
152
+ records: recordCount,
153
+ provenance: provenanceStr,
154
+ label: `${typeInfo.filename.padEnd(22)} ${recordDesc.padEnd(16)} (${provenanceStr})`,
155
+ };
156
+
157
+ if (typeInfo.directory === 'clinical') {
158
+ clinicalSummary.push(entry);
159
+ } else {
160
+ wellnessSummary.push(entry);
161
+ }
162
+ }
163
+
164
+ if (globalOpts.json) {
165
+ printResult(
166
+ {
167
+ pod: podDir,
168
+ patient: {
169
+ name: profile.name,
170
+ age: profile.age,
171
+ dateOfBirth: profile.dateOfBirth,
172
+ },
173
+ schemaVersion: profile.schemaVersion,
174
+ lastModified: lastModified?.toISOString(),
175
+ clinical: clinicalSummary.map((s) => ({
176
+ file: s.file,
177
+ records: s.records,
178
+ provenance: s.provenance,
179
+ })),
180
+ wellness: wellnessSummary.map((s) => ({
181
+ file: s.file,
182
+ records: s.records,
183
+ provenance: s.provenance,
184
+ })),
185
+ provenanceSources: Array.from(provenanceSources),
186
+ },
187
+ globalOpts,
188
+ );
189
+ } else {
190
+ // Human-readable output
191
+ console.log(`\nCascade Pod: ${podDir}\n`);
192
+
193
+ if (profile.name) {
194
+ const ageStr = profile.age ? ` (age ${profile.age})` : '';
195
+ console.log(`Patient: ${profile.name}${ageStr}`);
196
+ }
197
+ if (profile.schemaVersion) {
198
+ console.log(`Schema Version: ${profile.schemaVersion}`);
199
+ }
200
+ if (lastModified) {
201
+ console.log(`Last Modified: ${lastModified.toISOString().split('T')[0]}`);
202
+ }
203
+
204
+ if (clinicalSummary.length > 0) {
205
+ console.log('\nData Summary:');
206
+ console.log(' Clinical:');
207
+ for (const entry of clinicalSummary) {
208
+ console.log(` ${entry.label}`);
209
+ }
210
+ }
211
+
212
+ if (wellnessSummary.length > 0) {
213
+ if (clinicalSummary.length === 0) {
214
+ console.log('\nData Summary:');
215
+ }
216
+ console.log(' Wellness:');
217
+ for (const entry of wellnessSummary) {
218
+ console.log(` ${entry.label}`);
219
+ }
220
+ }
221
+
222
+ if (provenanceSources.size > 0) {
223
+ console.log(`\nProvenance Sources: ${Array.from(provenanceSources).join(', ')}`);
224
+ }
225
+
226
+ if (clinicalSummary.length === 0 && wellnessSummary.length === 0) {
227
+ console.log('\nThis pod has no data files yet.');
228
+ console.log('Add TTL files to the clinical/ or wellness/ directories to get started.');
229
+ }
230
+
231
+ console.log('');
232
+ }
233
+ } catch (err: unknown) {
234
+ const message = err instanceof Error ? err.message : String(err);
235
+ printError(`Failed to read pod info: ${message}`, globalOpts);
236
+ process.exitCode = 1;
237
+ }
238
+ });
239
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * cascade pod init <directory>
3
+ *
4
+ * Initialize a new Cascade Pod with the standard directory structure,
5
+ * template files, and discovery documents.
6
+ */
7
+
8
+ import type { Command } from 'commander';
9
+ import * as fs from 'fs/promises';
10
+ import * as path from 'path';
11
+ import { printResult, printError, printVerbose, type OutputOptions } from '../../lib/output.js';
12
+ import { resolvePodDir, fileExists } from './helpers.js';
13
+
14
+ // ─── Pod Init Templates ──────────────────────────────────────────────────────
15
+
16
+ function wellKnownSolid(absPath: string): string {
17
+ return JSON.stringify(
18
+ {
19
+ '@context': 'https://www.w3.org/ns/solid/terms',
20
+ pod_root: '/',
21
+ profile: '/profile/card.ttl#me',
22
+ storage: '/',
23
+ publicTypeIndex: '/settings/publicTypeIndex.ttl',
24
+ privateTypeIndex: '/settings/privateTypeIndex.ttl',
25
+ podUri: `file://${absPath}/`,
26
+ version: '1.0',
27
+ },
28
+ null,
29
+ 2,
30
+ );
31
+ }
32
+
33
+ const PROFILE_CARD_TTL = `@prefix cascade: <https://ns.cascadeprotocol.org/core/v1#> .
34
+ @prefix foaf: <http://xmlns.com/foaf/0.1/> .
35
+ @prefix solid: <http://www.w3.org/ns/solid/terms#> .
36
+ @prefix pim: <http://www.w3.org/ns/pim/space#> .
37
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
38
+
39
+ # =============================================================================
40
+ # WebID Profile Card
41
+ # =============================================================================
42
+ # This is a Solid-compatible WebID profile for the Pod owner.
43
+ # Edit this file to add patient demographics and identity information.
44
+ #
45
+ # The <#me> fragment serves as the WebID for this Pod.
46
+ # =============================================================================
47
+
48
+ <#me> a foaf:Person ;
49
+ # ── Edit the fields below to personalize your Pod ──
50
+ # foaf:name "First Last" ;
51
+ # foaf:givenName "First" ;
52
+ # foaf:familyName "Last" ;
53
+ # cascade:dateOfBirth "1990-01-01"^^xsd:date ;
54
+
55
+ # ── Discovery links (do not remove) ──
56
+ solid:publicTypeIndex </settings/publicTypeIndex.ttl> ;
57
+ solid:privateTypeIndex </settings/privateTypeIndex.ttl> ;
58
+ pim:storage </> ;
59
+
60
+ cascade:schemaVersion "1.3" .
61
+ `;
62
+
63
+ const PUBLIC_TYPE_INDEX_TTL = `@prefix solid: <http://www.w3.org/ns/solid/terms#> .
64
+ @prefix cascade: <https://ns.cascadeprotocol.org/core/v1#> .
65
+ @prefix health: <https://ns.cascadeprotocol.org/health/v1#> .
66
+ @prefix clinical: <https://ns.cascadeprotocol.org/clinical/v1#> .
67
+
68
+ # =============================================================================
69
+ # Public Type Index
70
+ # =============================================================================
71
+ # Maps data types to their storage locations in the Pod.
72
+ # Public registrations are visible to authorized applications.
73
+ #
74
+ # As you populate your Pod with data, add type registrations here so that
75
+ # agents and applications can discover where to find each data type.
76
+ # =============================================================================
77
+
78
+ <> a solid:TypeIndex, solid:ListedDocument .
79
+
80
+ # Type registrations will be added as data is populated.
81
+ # Example:
82
+ # <#medications> a solid:TypeRegistration ;
83
+ # solid:forClass health:MedicationRecord ;
84
+ # solid:instance </clinical/medications.ttl> .
85
+ #
86
+ # <#conditions> a solid:TypeRegistration ;
87
+ # solid:forClass health:ConditionRecord ;
88
+ # solid:instance </clinical/conditions.ttl> .
89
+ `;
90
+
91
+ const PRIVATE_TYPE_INDEX_TTL = `@prefix solid: <http://www.w3.org/ns/solid/terms#> .
92
+ @prefix health: <https://ns.cascadeprotocol.org/health/v1#> .
93
+ @prefix clinical: <https://ns.cascadeprotocol.org/clinical/v1#> .
94
+
95
+ # =============================================================================
96
+ # Private Type Index
97
+ # =============================================================================
98
+ # Maps wellness and device data types to their storage locations.
99
+ # Private registrations require explicit authorization to access.
100
+ #
101
+ # Wellness data (heart rate, activity, sleep, etc.) is typically registered
102
+ # here rather than in the public type index.
103
+ # =============================================================================
104
+
105
+ <> a solid:TypeIndex, solid:UnlistedDocument .
106
+
107
+ # Type registrations will be added as wellness data is populated.
108
+ # Example:
109
+ # <#heartRate> a solid:TypeRegistration ;
110
+ # solid:forClass health:HeartRateData ;
111
+ # solid:instance </wellness/heart-rate.ttl> .
112
+ `;
113
+
114
+ function indexTtl(dirName: string): string {
115
+ const now = new Date().toISOString();
116
+ return `@prefix ldp: <http://www.w3.org/ns/ldp#> .
117
+ @prefix dcterms: <http://purl.org/dc/terms/> .
118
+ @prefix cascade: <https://ns.cascadeprotocol.org/core/v1#> .
119
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
120
+
121
+ # =============================================================================
122
+ # Root LDP Container
123
+ # =============================================================================
124
+ # This is the root index of this Cascade Pod, structured as an LDP Basic
125
+ # Container per Solid Protocol conventions. It enumerates every resource
126
+ # in the Pod for discoverability by agents and applications.
127
+ #
128
+ # Update this file as you add or remove resources.
129
+ # =============================================================================
130
+
131
+ <> a ldp:BasicContainer ;
132
+ dcterms:title "${dirName} -- Cascade Pod" ;
133
+ dcterms:description "A Cascade Protocol Pod initialized with the cascade CLI." ;
134
+ dcterms:created "${now}"^^xsd:dateTime ;
135
+ cascade:schemaVersion "1.3" ;
136
+
137
+ # ── Profile & Settings ──
138
+ ldp:contains
139
+ <profile/card.ttl> ,
140
+ <settings/publicTypeIndex.ttl> ,
141
+ <settings/privateTypeIndex.ttl> .
142
+
143
+ # ── Add clinical and wellness resources below as they are created ──
144
+ # ldp:contains <clinical/medications.ttl> .
145
+ # ldp:contains <wellness/heart-rate.ttl> .
146
+ `;
147
+ }
148
+
149
+ const README_MD = `# Cascade Protocol Pod
150
+
151
+ This directory is a **Cascade Protocol Pod** -- a portable, self-describing collection of personal health data serialized as RDF/Turtle files.
152
+
153
+ ## Structure
154
+
155
+ \`\`\`
156
+ .well-known/
157
+ solid # Pod discovery document (JSON)
158
+ profile/
159
+ card.ttl # WebID profile (identity + discovery links)
160
+ settings/
161
+ publicTypeIndex.ttl # Maps clinical data types to file locations
162
+ privateTypeIndex.ttl # Maps wellness data types to file locations
163
+ clinical/ # Clinical records (EHR-sourced data)
164
+ wellness/ # Wellness records (device and self-reported data)
165
+ index.ttl # Root LDP container listing all resources
166
+ \`\`\`
167
+
168
+ ## Getting Started
169
+
170
+ 1. Edit \`profile/card.ttl\` to set the Pod owner's name and demographics.
171
+ 2. Add clinical data files (e.g., \`clinical/medications.ttl\`) and register them in \`settings/publicTypeIndex.ttl\`.
172
+ 3. Add wellness data files (e.g., \`wellness/heart-rate.ttl\`) and register them in \`settings/privateTypeIndex.ttl\`.
173
+ 4. Update \`index.ttl\` to list all resources.
174
+
175
+ ## Useful Commands
176
+
177
+ \`\`\`bash
178
+ cascade pod info . # Show Pod summary
179
+ cascade pod query . --all # Query all data in the Pod
180
+ cascade pod export . --format zip # Export as ZIP archive
181
+ cascade validate . # Validate against SHACL shapes
182
+ \`\`\`
183
+
184
+ ## Learn More
185
+
186
+ - Cascade Protocol: https://cascadeprotocol.org
187
+ - Pod Structure Spec: https://cascadeprotocol.org/docs/spec/pod-structure
188
+ - Cascade SDK: https://github.com/nickthorpe71/cascade-sdk-swift
189
+ `;
190
+
191
+ // ─── Command Registration ────────────────────────────────────────────────────
192
+
193
+ export function registerInitSubcommand(pod: Command, program: Command): void {
194
+ pod
195
+ .command('init')
196
+ .description('Initialize a new Cascade Pod')
197
+ .argument('<directory>', 'Directory to initialize as a Cascade Pod')
198
+ .action(async (directory: string) => {
199
+ const globalOpts = program.opts() as OutputOptions;
200
+ const absDir = resolvePodDir(directory);
201
+ const dirName = path.basename(absDir);
202
+
203
+ printVerbose(`Initializing pod at: ${absDir}`, globalOpts);
204
+
205
+ try {
206
+ // Check if directory already has pod structure
207
+ if (await fileExists(path.join(absDir, 'index.ttl'))) {
208
+ printError(`Directory already contains a Cascade Pod: ${absDir}`, globalOpts);
209
+ process.exitCode = 1;
210
+ return;
211
+ }
212
+
213
+ // Create directory structure
214
+ const dirs = [
215
+ path.join(absDir, '.well-known'),
216
+ path.join(absDir, 'profile'),
217
+ path.join(absDir, 'settings'),
218
+ path.join(absDir, 'clinical'),
219
+ path.join(absDir, 'wellness'),
220
+ ];
221
+
222
+ for (const dir of dirs) {
223
+ await fs.mkdir(dir, { recursive: true });
224
+ }
225
+
226
+ // Write template files
227
+ await fs.writeFile(path.join(absDir, '.well-known', 'solid'), wellKnownSolid(absDir));
228
+ await fs.writeFile(path.join(absDir, 'profile', 'card.ttl'), PROFILE_CARD_TTL);
229
+ await fs.writeFile(path.join(absDir, 'settings', 'publicTypeIndex.ttl'), PUBLIC_TYPE_INDEX_TTL);
230
+ await fs.writeFile(path.join(absDir, 'settings', 'privateTypeIndex.ttl'), PRIVATE_TYPE_INDEX_TTL);
231
+ await fs.writeFile(path.join(absDir, 'index.ttl'), indexTtl(dirName));
232
+ await fs.writeFile(path.join(absDir, 'README.md'), README_MD);
233
+
234
+ const filesCreated = [
235
+ '.well-known/solid',
236
+ 'profile/card.ttl',
237
+ 'settings/publicTypeIndex.ttl',
238
+ 'settings/privateTypeIndex.ttl',
239
+ 'clinical/',
240
+ 'wellness/',
241
+ 'index.ttl',
242
+ 'README.md',
243
+ ];
244
+
245
+ if (globalOpts.json) {
246
+ printResult(
247
+ {
248
+ status: 'created',
249
+ directory: absDir,
250
+ files: filesCreated,
251
+ message: 'Cascade Pod initialized successfully.',
252
+ },
253
+ globalOpts,
254
+ );
255
+ } else {
256
+ console.log(`Cascade Pod initialized at: ${absDir}\n`);
257
+ console.log('Created:');
258
+ for (const f of filesCreated) {
259
+ console.log(` ${f}`);
260
+ }
261
+ console.log('\nNext steps:');
262
+ console.log(' 1. Edit profile/card.ttl to set patient name and demographics');
263
+ console.log(' 2. Add data files to clinical/ and wellness/ directories');
264
+ console.log(' 3. Register data types in settings/publicTypeIndex.ttl');
265
+ console.log(` 4. Run: cascade pod info ${directory}`);
266
+ }
267
+ } catch (err: unknown) {
268
+ const message = err instanceof Error ? err.message : String(err);
269
+ printError(`Failed to initialize pod: ${message}`, globalOpts);
270
+ process.exitCode = 1;
271
+ }
272
+ });
273
+ }