@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.
- package/.dockerignore +7 -0
- package/.eslintrc.json +23 -0
- package/.prettierrc +7 -0
- package/DOCKER.md +36 -0
- package/Dockerfile +18 -0
- package/README.md +69 -0
- package/dist/commands/capabilities.d.ts +9 -0
- package/dist/commands/capabilities.d.ts.map +1 -0
- package/dist/commands/capabilities.js +194 -0
- package/dist/commands/capabilities.js.map +1 -0
- package/dist/commands/conformance.d.ts +15 -0
- package/dist/commands/conformance.d.ts.map +1 -0
- package/dist/commands/conformance.js +348 -0
- package/dist/commands/conformance.js.map +1 -0
- package/dist/commands/convert.d.ts +21 -0
- package/dist/commands/convert.d.ts.map +1 -0
- package/dist/commands/convert.js +134 -0
- package/dist/commands/convert.js.map +1 -0
- package/dist/commands/pod/export.d.ts +8 -0
- package/dist/commands/pod/export.d.ts.map +1 -0
- package/dist/commands/pod/export.js +72 -0
- package/dist/commands/pod/export.js.map +1 -0
- package/dist/commands/pod/helpers.d.ts +79 -0
- package/dist/commands/pod/helpers.d.ts.map +1 -0
- package/dist/commands/pod/helpers.js +369 -0
- package/dist/commands/pod/helpers.js.map +1 -0
- package/dist/commands/pod/index.d.ts +20 -0
- package/dist/commands/pod/index.d.ts.map +1 -0
- package/dist/commands/pod/index.js +29 -0
- package/dist/commands/pod/index.js.map +1 -0
- package/dist/commands/pod/info.d.ts +9 -0
- package/dist/commands/pod/info.d.ts.map +1 -0
- package/dist/commands/pod/info.js +196 -0
- package/dist/commands/pod/info.js.map +1 -0
- package/dist/commands/pod/init.d.ts +9 -0
- package/dist/commands/pod/init.d.ts.map +1 -0
- package/dist/commands/pod/init.js +251 -0
- package/dist/commands/pod/init.js.map +1 -0
- package/dist/commands/pod/query.d.ts +9 -0
- package/dist/commands/pod/query.d.ts.map +1 -0
- package/dist/commands/pod/query.js +169 -0
- package/dist/commands/pod/query.js.map +1 -0
- package/dist/commands/pod 2.js +1017 -0
- package/dist/commands/pod.d.ts +28 -0
- package/dist/commands/pod.d.ts 2.map +1 -0
- package/dist/commands/pod.d.ts.map +1 -0
- package/dist/commands/pod.js +1031 -0
- package/dist/commands/pod.js 2.map +1 -0
- package/dist/commands/pod.js.map +1 -0
- package/dist/commands/serve.d.ts +33 -0
- package/dist/commands/serve.d.ts.map +1 -0
- package/dist/commands/serve.js +74 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/validate.d.ts +18 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +275 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.d.ts +17 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.d.ts.map +1 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.js +358 -0
- package/dist/lib/fhir-converter/cascade-to-fhir.js.map +1 -0
- package/dist/lib/fhir-converter/converters-clinical.d.ts +29 -0
- package/dist/lib/fhir-converter/converters-clinical.d.ts.map +1 -0
- package/dist/lib/fhir-converter/converters-clinical.js +391 -0
- package/dist/lib/fhir-converter/converters-clinical.js.map +1 -0
- package/dist/lib/fhir-converter/converters-demographics.d.ts +20 -0
- package/dist/lib/fhir-converter/converters-demographics.d.ts.map +1 -0
- package/dist/lib/fhir-converter/converters-demographics.js +242 -0
- package/dist/lib/fhir-converter/converters-demographics.js.map +1 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.d.ts +17 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.d.ts.map +1 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.js +63 -0
- package/dist/lib/fhir-converter/fhir-to-cascade.js.map +1 -0
- package/dist/lib/fhir-converter/index.d.ts +36 -0
- package/dist/lib/fhir-converter/index.d.ts.map +1 -0
- package/dist/lib/fhir-converter/index.js +187 -0
- package/dist/lib/fhir-converter/index.js.map +1 -0
- package/dist/lib/fhir-converter/types.d.ts +77 -0
- package/dist/lib/fhir-converter/types.d.ts.map +1 -0
- package/dist/lib/fhir-converter/types.js +236 -0
- package/dist/lib/fhir-converter/types.js.map +1 -0
- package/dist/lib/fhir-converter.d.ts +62 -0
- package/dist/lib/fhir-converter.d.ts.map +1 -0
- package/dist/lib/fhir-converter.js +1474 -0
- package/dist/lib/fhir-converter.js.map +1 -0
- package/dist/lib/mcp/audit.d.ts +24 -0
- package/dist/lib/mcp/audit.d.ts.map +1 -0
- package/dist/lib/mcp/audit.js +85 -0
- package/dist/lib/mcp/audit.js.map +1 -0
- package/dist/lib/mcp/server.d.ts +38 -0
- package/dist/lib/mcp/server.d.ts.map +1 -0
- package/dist/lib/mcp/server.js +172 -0
- package/dist/lib/mcp/server.js.map +1 -0
- package/dist/lib/mcp/tools.d.ts +47 -0
- package/dist/lib/mcp/tools.d.ts.map +1 -0
- package/dist/lib/mcp/tools.js +547 -0
- package/dist/lib/mcp/tools.js.map +1 -0
- package/dist/lib/output.d.ts +26 -0
- package/dist/lib/output.d.ts.map +1 -0
- package/dist/lib/output.js +64 -0
- package/dist/lib/output.js.map +1 -0
- package/dist/lib/shacl-validator.d.ts +53 -0
- package/dist/lib/shacl-validator.d.ts.map +1 -0
- package/dist/lib/shacl-validator.js +245 -0
- package/dist/lib/shacl-validator.js.map +1 -0
- package/dist/lib/turtle-parser.d.ts +64 -0
- package/dist/lib/turtle-parser.d.ts.map +1 -0
- package/dist/lib/turtle-parser.js +236 -0
- package/dist/lib/turtle-parser.js.map +1 -0
- package/dist/shapes/checkup.shapes.ttl +1459 -0
- package/dist/shapes/clinical.shapes.ttl +1350 -0
- package/dist/shapes/clinical.ttl +1369 -0
- package/dist/shapes/core.shapes.ttl +450 -0
- package/dist/shapes/core.ttl +603 -0
- package/dist/shapes/coverage.shapes.ttl +214 -0
- package/dist/shapes/coverage.ttl +182 -0
- package/dist/shapes/health.shapes.ttl +697 -0
- package/dist/shapes/health.ttl +859 -0
- package/dist/shapes/pots.shapes.ttl +481 -0
- package/package.json +54 -0
- package/src/commands/capabilities.ts +235 -0
- package/src/commands/conformance.ts +447 -0
- package/src/commands/convert.ts +164 -0
- package/src/commands/pod/export.ts +85 -0
- package/src/commands/pod/helpers.ts +449 -0
- package/src/commands/pod/index.ts +32 -0
- package/src/commands/pod/info.ts +239 -0
- package/src/commands/pod/init.ts +273 -0
- package/src/commands/pod/query.ts +224 -0
- package/src/commands/serve.ts +92 -0
- package/src/commands/validate.ts +303 -0
- package/src/index.ts +58 -0
- package/src/lib/fhir-converter/cascade-to-fhir.ts +369 -0
- package/src/lib/fhir-converter/converters-clinical.ts +446 -0
- package/src/lib/fhir-converter/converters-demographics.ts +270 -0
- package/src/lib/fhir-converter/fhir-to-cascade.ts +82 -0
- package/src/lib/fhir-converter/index.ts +215 -0
- package/src/lib/fhir-converter/types.ts +318 -0
- package/src/lib/mcp/audit.ts +107 -0
- package/src/lib/mcp/server.ts +192 -0
- package/src/lib/mcp/tools.ts +668 -0
- package/src/lib/output.ts +76 -0
- package/src/lib/shacl-validator.ts +314 -0
- package/src/lib/turtle-parser.ts +277 -0
- package/src/shapes/checkup.shapes.ttl +1459 -0
- package/src/shapes/clinical.shapes.ttl +1350 -0
- package/src/shapes/clinical.ttl +1369 -0
- package/src/shapes/core.shapes.ttl +450 -0
- package/src/shapes/core.ttl +603 -0
- package/src/shapes/coverage.shapes.ttl +214 -0
- package/src/shapes/coverage.ttl +182 -0
- package/src/shapes/health.shapes.ttl +697 -0
- package/src/shapes/health.ttl +859 -0
- package/src/shapes/pots.shapes.ttl +481 -0
- package/test-fixtures/fhir-bundle-example.json +216 -0
- package/test-fixtures/fhir-medication-example.json +18 -0
- package/tests/cli.test.ts +126 -0
- package/tests/fhir-converter.test.ts +874 -0
- package/tests/mcp-server.test.ts +396 -0
- package/tests/pod.test.ts +400 -0
- 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
|
+
}
|