@the-cascade-protocol/cli 0.2.0 → 0.2.1
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/dist/commands/pod/helpers.d.ts +1 -1
- package/dist/commands/pod/helpers.d.ts.map +1 -1
- package/dist/commands/pod/helpers.js +5 -20
- package/dist/commands/pod/helpers.js.map +1 -1
- package/package.json +17 -5
- package/.dockerignore +0 -7
- package/.eslintrc.json +0 -23
- package/.prettierrc +0 -7
- package/Dockerfile +0 -18
- package/src/commands/capabilities.ts +0 -235
- package/src/commands/conformance.ts +0 -447
- package/src/commands/convert.ts +0 -164
- package/src/commands/pod/export.ts +0 -85
- package/src/commands/pod/helpers.ts +0 -449
- package/src/commands/pod/index.ts +0 -32
- package/src/commands/pod/info.ts +0 -239
- package/src/commands/pod/init.ts +0 -273
- package/src/commands/pod/query.ts +0 -224
- package/src/commands/serve.ts +0 -92
- package/src/commands/validate.ts +0 -303
- package/src/index.ts +0 -58
- package/src/lib/fhir-converter/cascade-to-fhir.ts +0 -369
- package/src/lib/fhir-converter/converters-clinical.ts +0 -446
- package/src/lib/fhir-converter/converters-demographics.ts +0 -270
- package/src/lib/fhir-converter/fhir-to-cascade.ts +0 -82
- package/src/lib/fhir-converter/index.ts +0 -215
- package/src/lib/fhir-converter/types.ts +0 -318
- package/src/lib/mcp/audit.ts +0 -107
- package/src/lib/mcp/server.ts +0 -192
- package/src/lib/mcp/tools.ts +0 -668
- package/src/lib/output.ts +0 -76
- package/src/lib/shacl-validator.ts +0 -314
- package/src/lib/turtle-parser.ts +0 -277
- package/src/shapes/checkup.shapes.ttl +0 -1459
- package/src/shapes/clinical.shapes.ttl +0 -1350
- package/src/shapes/clinical.ttl +0 -1369
- package/src/shapes/core.shapes.ttl +0 -450
- package/src/shapes/core.ttl +0 -603
- package/src/shapes/coverage.shapes.ttl +0 -214
- package/src/shapes/coverage.ttl +0 -182
- package/src/shapes/health.shapes.ttl +0 -697
- package/src/shapes/health.ttl +0 -859
- package/src/shapes/pots.shapes.ttl +0 -481
- package/test-fixtures/fhir-bundle-example.json +0 -216
- package/test-fixtures/fhir-medication-example.json +0 -18
- package/tests/cli.test.ts +0 -126
- package/tests/fhir-converter.test.ts +0 -874
- package/tests/mcp-server.test.ts +0 -396
- package/tests/pod.test.ts +0 -400
- package/tsconfig.json +0 -24
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cascade pod query <pod-dir>
|
|
3
|
-
*
|
|
4
|
-
* Query data within a Cascade Pod by type (medications, conditions, etc.)
|
|
5
|
-
* or across all data types.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { Command } from 'commander';
|
|
9
|
-
import * as path from 'path';
|
|
10
|
-
import { printResult, printError, printVerbose, type OutputOptions } from '../../lib/output.js';
|
|
11
|
-
import {
|
|
12
|
-
DATA_TYPES,
|
|
13
|
-
resolvePodDir,
|
|
14
|
-
isDirectory,
|
|
15
|
-
fileExists,
|
|
16
|
-
discoverTtlFiles,
|
|
17
|
-
parseDataFile,
|
|
18
|
-
extractLabelFromProps,
|
|
19
|
-
selectKeyProperties,
|
|
20
|
-
} from './helpers.js';
|
|
21
|
-
|
|
22
|
-
export function registerQuerySubcommand(pod: Command, program: Command): void {
|
|
23
|
-
pod
|
|
24
|
-
.command('query')
|
|
25
|
-
.description('Query data within a pod')
|
|
26
|
-
.argument('<pod-dir>', 'Path to the Cascade Pod')
|
|
27
|
-
.option('--medications', 'Query medications')
|
|
28
|
-
.option('--conditions', 'Query conditions')
|
|
29
|
-
.option('--allergies', 'Query allergies')
|
|
30
|
-
.option('--lab-results', 'Query lab results')
|
|
31
|
-
.option('--immunizations', 'Query immunizations')
|
|
32
|
-
.option('--vital-signs', 'Query vital signs')
|
|
33
|
-
.option('--supplements', 'Query supplements')
|
|
34
|
-
.option('--all', 'Query all data')
|
|
35
|
-
.action(
|
|
36
|
-
async (
|
|
37
|
-
podDir: string,
|
|
38
|
-
options: {
|
|
39
|
-
medications?: boolean;
|
|
40
|
-
conditions?: boolean;
|
|
41
|
-
allergies?: boolean;
|
|
42
|
-
labResults?: boolean;
|
|
43
|
-
immunizations?: boolean;
|
|
44
|
-
vitalSigns?: boolean;
|
|
45
|
-
supplements?: boolean;
|
|
46
|
-
all?: boolean;
|
|
47
|
-
},
|
|
48
|
-
) => {
|
|
49
|
-
const globalOpts = program.opts() as OutputOptions;
|
|
50
|
-
const absDir = resolvePodDir(podDir);
|
|
51
|
-
|
|
52
|
-
printVerbose(`Querying pod: ${absDir}`, globalOpts);
|
|
53
|
-
printVerbose(`Filters: ${JSON.stringify(options)}`, globalOpts);
|
|
54
|
-
|
|
55
|
-
// Validate pod exists
|
|
56
|
-
if (!(await isDirectory(absDir))) {
|
|
57
|
-
printError(`Pod directory not found: ${absDir}`, globalOpts);
|
|
58
|
-
process.exitCode = 1;
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
// Determine which data types to query
|
|
64
|
-
let requestedTypes: string[];
|
|
65
|
-
|
|
66
|
-
if (options.all) {
|
|
67
|
-
// Discover all TTL files in the pod
|
|
68
|
-
requestedTypes = Object.keys(DATA_TYPES);
|
|
69
|
-
} else {
|
|
70
|
-
requestedTypes = [];
|
|
71
|
-
if (options.medications) requestedTypes.push('medications');
|
|
72
|
-
if (options.conditions) requestedTypes.push('conditions');
|
|
73
|
-
if (options.allergies) requestedTypes.push('allergies');
|
|
74
|
-
if (options.labResults) requestedTypes.push('lab-results');
|
|
75
|
-
if (options.immunizations) requestedTypes.push('immunizations');
|
|
76
|
-
if (options.vitalSigns) requestedTypes.push('vital-signs');
|
|
77
|
-
if (options.supplements) requestedTypes.push('supplements');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (requestedTypes.length === 0) {
|
|
81
|
-
printError(
|
|
82
|
-
'No query filter specified. Use --medications, --conditions, --all, etc.',
|
|
83
|
-
globalOpts,
|
|
84
|
-
);
|
|
85
|
-
process.exitCode = 1;
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Process each requested data type
|
|
90
|
-
const queryResults: Record<
|
|
91
|
-
string,
|
|
92
|
-
{
|
|
93
|
-
count: number;
|
|
94
|
-
file: string;
|
|
95
|
-
records: Array<{
|
|
96
|
-
id: string;
|
|
97
|
-
type: string;
|
|
98
|
-
properties: Record<string, string>;
|
|
99
|
-
}>;
|
|
100
|
-
error?: string;
|
|
101
|
-
}
|
|
102
|
-
> = {};
|
|
103
|
-
|
|
104
|
-
// If --all, also discover any TTL files not in the registry
|
|
105
|
-
const extraFiles: string[] = [];
|
|
106
|
-
if (options.all) {
|
|
107
|
-
const allTtlFiles = await discoverTtlFiles(absDir);
|
|
108
|
-
const knownPaths = new Set(
|
|
109
|
-
Object.values(DATA_TYPES).map((dt) =>
|
|
110
|
-
path.join(absDir, dt.directory, dt.filename),
|
|
111
|
-
),
|
|
112
|
-
);
|
|
113
|
-
// Also exclude index.ttl, manifest.ttl, profile/card.ttl, type indexes
|
|
114
|
-
const excludePaths = new Set([
|
|
115
|
-
path.join(absDir, 'index.ttl'),
|
|
116
|
-
path.join(absDir, 'manifest.ttl'),
|
|
117
|
-
path.join(absDir, 'profile', 'card.ttl'),
|
|
118
|
-
path.join(absDir, 'settings', 'publicTypeIndex.ttl'),
|
|
119
|
-
path.join(absDir, 'settings', 'privateTypeIndex.ttl'),
|
|
120
|
-
]);
|
|
121
|
-
for (const f of allTtlFiles) {
|
|
122
|
-
if (!knownPaths.has(f) && !excludePaths.has(f)) {
|
|
123
|
-
extraFiles.push(f);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
for (const typeName of requestedTypes) {
|
|
129
|
-
const typeInfo = DATA_TYPES[typeName];
|
|
130
|
-
if (!typeInfo) continue;
|
|
131
|
-
|
|
132
|
-
const filePath = path.join(absDir, typeInfo.directory, typeInfo.filename);
|
|
133
|
-
if (!(await fileExists(filePath))) {
|
|
134
|
-
printVerbose(`Skipping ${typeName}: file not found at ${filePath}`, globalOpts);
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const { records, error } = await parseDataFile(filePath);
|
|
139
|
-
|
|
140
|
-
queryResults[typeName] = {
|
|
141
|
-
count: records.length,
|
|
142
|
-
file: `${typeInfo.directory}/${typeInfo.filename}`,
|
|
143
|
-
records: records.map((r) => ({
|
|
144
|
-
id: r.id,
|
|
145
|
-
type: r.type,
|
|
146
|
-
properties: r.properties,
|
|
147
|
-
})),
|
|
148
|
-
error,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Process extra files found in --all mode
|
|
153
|
-
for (const extraFile of extraFiles) {
|
|
154
|
-
const relPath = path.relative(absDir, extraFile);
|
|
155
|
-
const baseName = path.basename(extraFile, '.ttl');
|
|
156
|
-
|
|
157
|
-
const { records, error } = await parseDataFile(extraFile);
|
|
158
|
-
if (records.length > 0) {
|
|
159
|
-
queryResults[baseName] = {
|
|
160
|
-
count: records.length,
|
|
161
|
-
file: relPath,
|
|
162
|
-
records: records.map((r) => ({
|
|
163
|
-
id: r.id,
|
|
164
|
-
type: r.type,
|
|
165
|
-
properties: r.properties,
|
|
166
|
-
})),
|
|
167
|
-
error,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Output results
|
|
173
|
-
if (globalOpts.json) {
|
|
174
|
-
printResult(
|
|
175
|
-
{
|
|
176
|
-
pod: podDir,
|
|
177
|
-
dataTypes: queryResults,
|
|
178
|
-
},
|
|
179
|
-
globalOpts,
|
|
180
|
-
);
|
|
181
|
-
} else {
|
|
182
|
-
// Human-readable output
|
|
183
|
-
const typeKeys = Object.keys(queryResults);
|
|
184
|
-
if (typeKeys.length === 0) {
|
|
185
|
-
console.log('No data found for the specified query filters.');
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
for (const typeName of typeKeys) {
|
|
190
|
-
const data = queryResults[typeName];
|
|
191
|
-
const typeInfo = DATA_TYPES[typeName];
|
|
192
|
-
const displayLabel = typeInfo?.label ?? typeName;
|
|
193
|
-
|
|
194
|
-
console.log(`\n=== ${displayLabel} (${data.count} records) ===`);
|
|
195
|
-
if (data.error) {
|
|
196
|
-
console.log(` Error: ${data.error}`);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
console.log(` File: ${data.file}\n`);
|
|
200
|
-
|
|
201
|
-
for (let i = 0; i < data.records.length; i++) {
|
|
202
|
-
const rec = data.records[i];
|
|
203
|
-
const label = extractLabelFromProps(rec.properties);
|
|
204
|
-
const idShort = rec.id.length > 40 ? rec.id.substring(0, 40) + '...' : rec.id;
|
|
205
|
-
|
|
206
|
-
console.log(` ${i + 1}. ${label ?? rec.type} (${idShort})`);
|
|
207
|
-
|
|
208
|
-
// Show key properties
|
|
209
|
-
const keyProps = selectKeyProperties(typeName, rec.properties);
|
|
210
|
-
for (const [key, value] of Object.entries(keyProps)) {
|
|
211
|
-
console.log(` ${key}: ${value}`);
|
|
212
|
-
}
|
|
213
|
-
console.log('');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
} catch (err: unknown) {
|
|
218
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
219
|
-
printError(`Failed to query pod: ${message}`, globalOpts);
|
|
220
|
-
process.exitCode = 1;
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
);
|
|
224
|
-
}
|
package/src/commands/serve.ts
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cascade serve
|
|
3
|
-
*
|
|
4
|
-
* Start a local MCP-compatible agent server that exposes Cascade Protocol
|
|
5
|
-
* tools to AI agents.
|
|
6
|
-
*
|
|
7
|
-
* Transports:
|
|
8
|
-
* stdio — Standard input/output (for Claude Desktop, Claude Code)
|
|
9
|
-
* sse — Server-Sent Events over HTTP (for web-based agents)
|
|
10
|
-
*
|
|
11
|
-
* Options:
|
|
12
|
-
* --mcp Enable MCP (Model Context Protocol) mode
|
|
13
|
-
* --transport <transport> Transport type (stdio|sse) [default: stdio]
|
|
14
|
-
* --port <port> Port for SSE transport [default: 3000]
|
|
15
|
-
* --pod <path> Default Pod directory path
|
|
16
|
-
*
|
|
17
|
-
* Environment Variables:
|
|
18
|
-
* CASCADE_POD_PATH Default Pod directory (overridden by --pod)
|
|
19
|
-
*
|
|
20
|
-
* Claude Desktop configuration:
|
|
21
|
-
* {
|
|
22
|
-
* "mcpServers": {
|
|
23
|
-
* "cascade": {
|
|
24
|
-
* "command": "cascade",
|
|
25
|
-
* "args": ["serve", "--mcp"],
|
|
26
|
-
* "env": { "CASCADE_POD_PATH": "/path/to/pod" }
|
|
27
|
-
* }
|
|
28
|
-
* }
|
|
29
|
-
* }
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import { Command } from 'commander';
|
|
33
|
-
import { printError, printVerbose, type OutputOptions } from '../lib/output.js';
|
|
34
|
-
import { startServer } from '../lib/mcp/server.js';
|
|
35
|
-
|
|
36
|
-
export function registerServeCommand(program: Command): void {
|
|
37
|
-
program
|
|
38
|
-
.command('serve')
|
|
39
|
-
.description('Start local agent server')
|
|
40
|
-
.option('--mcp', 'Enable MCP (Model Context Protocol) mode')
|
|
41
|
-
.option('--transport <transport>', 'Transport type (stdio|sse)', 'stdio')
|
|
42
|
-
.option('--port <port>', 'Port for SSE transport', '3000')
|
|
43
|
-
.option('--pod <path>', 'Default Pod directory path')
|
|
44
|
-
.action(
|
|
45
|
-
async (options: {
|
|
46
|
-
mcp?: boolean;
|
|
47
|
-
transport: string;
|
|
48
|
-
port: string;
|
|
49
|
-
pod?: string;
|
|
50
|
-
}) => {
|
|
51
|
-
const globalOpts = program.opts() as OutputOptions;
|
|
52
|
-
|
|
53
|
-
if (!options.mcp) {
|
|
54
|
-
printError(
|
|
55
|
-
'The --mcp flag is required. Usage: cascade serve --mcp',
|
|
56
|
-
globalOpts,
|
|
57
|
-
);
|
|
58
|
-
console.error('');
|
|
59
|
-
console.error('Examples:');
|
|
60
|
-
console.error(' cascade serve --mcp # stdio transport');
|
|
61
|
-
console.error(' cascade serve --mcp --transport sse --port 3000 # SSE transport');
|
|
62
|
-
console.error(' cascade serve --mcp --pod ./my-pod # with default Pod');
|
|
63
|
-
process.exitCode = 1;
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const transport = options.transport as 'stdio' | 'sse';
|
|
68
|
-
if (transport !== 'stdio' && transport !== 'sse') {
|
|
69
|
-
printError(
|
|
70
|
-
`Invalid transport: "${options.transport}". Must be "stdio" or "sse".`,
|
|
71
|
-
globalOpts,
|
|
72
|
-
);
|
|
73
|
-
process.exitCode = 1;
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
printVerbose(`Starting MCP server with transport: ${transport}`, globalOpts);
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
await startServer({
|
|
81
|
-
transport,
|
|
82
|
-
port: parseInt(options.port, 10),
|
|
83
|
-
podPath: options.pod,
|
|
84
|
-
});
|
|
85
|
-
} catch (err: unknown) {
|
|
86
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
87
|
-
printError(`Failed to start server: ${message}`, globalOpts);
|
|
88
|
-
process.exitCode = 1;
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
);
|
|
92
|
-
}
|
package/src/commands/validate.ts
DELETED
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* cascade validate <file-or-dir>
|
|
3
|
-
*
|
|
4
|
-
* Validate Cascade Protocol data against SHACL shapes.
|
|
5
|
-
*
|
|
6
|
-
* Options:
|
|
7
|
-
* --shapes <shapes-dir> Path to custom SHACL shapes directory
|
|
8
|
-
* --json Output results as JSON
|
|
9
|
-
* --verbose Show detailed validation information
|
|
10
|
-
*
|
|
11
|
-
* Exit codes:
|
|
12
|
-
* 0 = all files pass validation
|
|
13
|
-
* 1 = one or more validation failures
|
|
14
|
-
* 2 = errors (file not found, malformed Turtle, etc.)
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import fs from 'node:fs';
|
|
18
|
-
import path from 'node:path';
|
|
19
|
-
import { Command } from 'commander';
|
|
20
|
-
import { printVerbose, type OutputOptions } from '../lib/output.js';
|
|
21
|
-
import {
|
|
22
|
-
loadShapes,
|
|
23
|
-
validateFile,
|
|
24
|
-
findTurtleFiles,
|
|
25
|
-
type ValidationResult,
|
|
26
|
-
} from '../lib/shacl-validator.js';
|
|
27
|
-
|
|
28
|
-
/** ANSI color codes for terminal output */
|
|
29
|
-
const colors = {
|
|
30
|
-
reset: '\x1b[0m',
|
|
31
|
-
red: '\x1b[31m',
|
|
32
|
-
green: '\x1b[32m',
|
|
33
|
-
yellow: '\x1b[33m',
|
|
34
|
-
blue: '\x1b[34m',
|
|
35
|
-
cyan: '\x1b[36m',
|
|
36
|
-
dim: '\x1b[2m',
|
|
37
|
-
bold: '\x1b[1m',
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
/** Severity icons for human-readable output */
|
|
41
|
-
const severityIcons: Record<string, string> = {
|
|
42
|
-
violation: `${colors.red}FAIL${colors.reset}`,
|
|
43
|
-
warning: `${colors.yellow}WARN${colors.reset}`,
|
|
44
|
-
info: `${colors.blue}INFO${colors.reset}`,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Format a single validation result for human-readable output.
|
|
49
|
-
*/
|
|
50
|
-
function formatResultHuman(result: ValidationResult, verbose: boolean): string {
|
|
51
|
-
const lines: string[] = [];
|
|
52
|
-
const relPath = result.file;
|
|
53
|
-
|
|
54
|
-
if (result.valid) {
|
|
55
|
-
lines.push(`${colors.green}PASS${colors.reset} ${relPath} (${result.quadCount} triples)`);
|
|
56
|
-
if (verbose && result.shapesUsed.length > 0) {
|
|
57
|
-
lines.push(` Shapes: ${result.shapesUsed.join(', ')}`);
|
|
58
|
-
}
|
|
59
|
-
if (verbose && result.subjects.length > 0) {
|
|
60
|
-
const typesSummary = result.subjects
|
|
61
|
-
.flatMap((s) => s.types.map(uriToLocalName))
|
|
62
|
-
.filter((t, i, a) => a.indexOf(t) === i);
|
|
63
|
-
lines.push(` Types: ${typesSummary.join(', ')}`);
|
|
64
|
-
}
|
|
65
|
-
} else {
|
|
66
|
-
const violations = result.results.filter((r) => r.severity === 'violation');
|
|
67
|
-
const warnings = result.results.filter((r) => r.severity === 'warning');
|
|
68
|
-
const infos = result.results.filter((r) => r.severity === 'info');
|
|
69
|
-
|
|
70
|
-
const countParts: string[] = [];
|
|
71
|
-
if (violations.length > 0) countParts.push(`${violations.length} violation${violations.length !== 1 ? 's' : ''}`);
|
|
72
|
-
if (warnings.length > 0) countParts.push(`${warnings.length} warning${warnings.length !== 1 ? 's' : ''}`);
|
|
73
|
-
if (infos.length > 0) countParts.push(`${infos.length} info`);
|
|
74
|
-
|
|
75
|
-
const statusIcon = violations.length > 0
|
|
76
|
-
? `${colors.red}FAIL${colors.reset}`
|
|
77
|
-
: `${colors.yellow}WARN${colors.reset}`;
|
|
78
|
-
|
|
79
|
-
lines.push(`${statusIcon} ${relPath} (${result.quadCount} triples, ${countParts.join(', ')})`);
|
|
80
|
-
|
|
81
|
-
if (result.shapesUsed.length > 0) {
|
|
82
|
-
lines.push(` Shapes: ${result.shapesUsed.join(', ')}`);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Group issues by severity
|
|
86
|
-
for (const issue of result.results) {
|
|
87
|
-
const icon = severityIcons[issue.severity] ?? issue.severity;
|
|
88
|
-
const focusInfo = issue.focusNode ? ` [${uriToLocalName(issue.focusNode)}]` : '';
|
|
89
|
-
lines.push(` ${icon} ${issue.message}${focusInfo}`);
|
|
90
|
-
if (issue.property) {
|
|
91
|
-
lines.push(` Property: ${issue.property}`);
|
|
92
|
-
}
|
|
93
|
-
if (issue.shape) {
|
|
94
|
-
lines.push(` Shape: ${issue.shape}`);
|
|
95
|
-
}
|
|
96
|
-
if (issue.value !== undefined) {
|
|
97
|
-
lines.push(` Value: ${issue.value}`);
|
|
98
|
-
}
|
|
99
|
-
if (issue.specLink) {
|
|
100
|
-
lines.push(` Spec: ${colors.cyan}${issue.specLink}${colors.reset}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
return lines.join('\n');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Extract local name from a URI (fragment or last path segment).
|
|
110
|
-
*/
|
|
111
|
-
function uriToLocalName(uri: string): string {
|
|
112
|
-
if (uri.includes('#')) {
|
|
113
|
-
return uri.split('#').pop() ?? uri;
|
|
114
|
-
}
|
|
115
|
-
return uri.split('/').pop() ?? uri;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Print a summary line for batch validation.
|
|
120
|
-
*/
|
|
121
|
-
function printSummary(
|
|
122
|
-
results: ValidationResult[],
|
|
123
|
-
opts: OutputOptions,
|
|
124
|
-
): void {
|
|
125
|
-
if (opts.json) return; // JSON mode outputs the full array
|
|
126
|
-
|
|
127
|
-
const total = results.length;
|
|
128
|
-
const passed = results.filter((r) => r.valid).length;
|
|
129
|
-
const failed = total - passed;
|
|
130
|
-
|
|
131
|
-
const totalViolations = results.reduce(
|
|
132
|
-
(sum, r) => sum + r.results.filter((i) => i.severity === 'violation').length,
|
|
133
|
-
0,
|
|
134
|
-
);
|
|
135
|
-
const totalWarnings = results.reduce(
|
|
136
|
-
(sum, r) => sum + r.results.filter((i) => i.severity === 'warning').length,
|
|
137
|
-
0,
|
|
138
|
-
);
|
|
139
|
-
const totalInfos = results.reduce(
|
|
140
|
-
(sum, r) => sum + r.results.filter((i) => i.severity === 'info').length,
|
|
141
|
-
0,
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
console.log('');
|
|
145
|
-
console.log(`${colors.bold}Validation Summary${colors.reset}`);
|
|
146
|
-
console.log(` Files: ${total} total, ${colors.green}${passed} passed${colors.reset}, ${failed > 0 ? `${colors.red}${failed} failed${colors.reset}` : '0 failed'}`);
|
|
147
|
-
|
|
148
|
-
const parts: string[] = [];
|
|
149
|
-
if (totalViolations > 0) parts.push(`${colors.red}${totalViolations} violations${colors.reset}`);
|
|
150
|
-
if (totalWarnings > 0) parts.push(`${colors.yellow}${totalWarnings} warnings${colors.reset}`);
|
|
151
|
-
if (totalInfos > 0) parts.push(`${colors.blue}${totalInfos} info${colors.reset}`);
|
|
152
|
-
if (parts.length > 0) {
|
|
153
|
-
console.log(` Issues: ${parts.join(', ')}`);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function registerValidateCommand(program: Command): void {
|
|
158
|
-
program
|
|
159
|
-
.command('validate')
|
|
160
|
-
.description('Validate Cascade data against SHACL shapes')
|
|
161
|
-
.argument('<file-or-dir>', 'Turtle file or directory to validate')
|
|
162
|
-
.option('--shapes <shapes-dir>', 'Path to custom SHACL shapes directory')
|
|
163
|
-
.action(async (fileOrDir: string, options: { shapes?: string }) => {
|
|
164
|
-
const globalOpts = program.opts() as OutputOptions;
|
|
165
|
-
|
|
166
|
-
// Resolve the path
|
|
167
|
-
const targetPath = path.resolve(fileOrDir);
|
|
168
|
-
|
|
169
|
-
// Check if path exists
|
|
170
|
-
if (!fs.existsSync(targetPath)) {
|
|
171
|
-
if (globalOpts.json) {
|
|
172
|
-
console.log(JSON.stringify({ error: `Path not found: ${targetPath}` }, null, 2));
|
|
173
|
-
} else {
|
|
174
|
-
console.error(`${colors.red}ERROR${colors.reset}: Path not found: ${targetPath}`);
|
|
175
|
-
}
|
|
176
|
-
process.exitCode = 2;
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Load SHACL shapes
|
|
181
|
-
printVerbose('Loading SHACL shapes...', globalOpts);
|
|
182
|
-
|
|
183
|
-
let shapesStore;
|
|
184
|
-
let shapeFiles: string[];
|
|
185
|
-
try {
|
|
186
|
-
const loaded = loadShapes(options.shapes);
|
|
187
|
-
shapesStore = loaded.store;
|
|
188
|
-
shapeFiles = loaded.shapeFiles;
|
|
189
|
-
printVerbose(`Loaded ${shapeFiles.length} shape files: ${shapeFiles.join(', ')}`, globalOpts);
|
|
190
|
-
} catch (e: unknown) {
|
|
191
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
192
|
-
if (globalOpts.json) {
|
|
193
|
-
console.log(JSON.stringify({ error: msg }, null, 2));
|
|
194
|
-
} else {
|
|
195
|
-
console.error(`${colors.red}ERROR${colors.reset}: ${msg}`);
|
|
196
|
-
}
|
|
197
|
-
process.exitCode = 2;
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Determine files to validate
|
|
202
|
-
let filesToValidate: string[];
|
|
203
|
-
const stat = fs.statSync(targetPath);
|
|
204
|
-
|
|
205
|
-
if (stat.isDirectory()) {
|
|
206
|
-
filesToValidate = findTurtleFiles(targetPath);
|
|
207
|
-
if (filesToValidate.length === 0) {
|
|
208
|
-
if (globalOpts.json) {
|
|
209
|
-
console.log(JSON.stringify({ error: `No .ttl files found in ${targetPath}` }, null, 2));
|
|
210
|
-
} else {
|
|
211
|
-
console.error(`${colors.yellow}WARNING${colors.reset}: No .ttl files found in ${targetPath}`);
|
|
212
|
-
}
|
|
213
|
-
process.exitCode = 2;
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
printVerbose(`Found ${filesToValidate.length} Turtle files in ${targetPath}`, globalOpts);
|
|
217
|
-
} else if (stat.isFile()) {
|
|
218
|
-
if (!targetPath.endsWith('.ttl')) {
|
|
219
|
-
if (globalOpts.json) {
|
|
220
|
-
console.log(JSON.stringify({ error: `Not a Turtle file: ${targetPath}` }, null, 2));
|
|
221
|
-
} else {
|
|
222
|
-
console.error(`${colors.red}ERROR${colors.reset}: Not a Turtle file (expected .ttl): ${targetPath}`);
|
|
223
|
-
}
|
|
224
|
-
process.exitCode = 2;
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
filesToValidate = [targetPath];
|
|
228
|
-
} else {
|
|
229
|
-
if (globalOpts.json) {
|
|
230
|
-
console.log(JSON.stringify({ error: `Not a file or directory: ${targetPath}` }, null, 2));
|
|
231
|
-
} else {
|
|
232
|
-
console.error(`${colors.red}ERROR${colors.reset}: Not a file or directory: ${targetPath}`);
|
|
233
|
-
}
|
|
234
|
-
process.exitCode = 2;
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Validate each file
|
|
239
|
-
const results: ValidationResult[] = [];
|
|
240
|
-
let hasErrors = false;
|
|
241
|
-
let hasViolations = false;
|
|
242
|
-
|
|
243
|
-
for (const file of filesToValidate) {
|
|
244
|
-
printVerbose(`Validating: ${file}`, globalOpts);
|
|
245
|
-
|
|
246
|
-
try {
|
|
247
|
-
const result = validateFile(file, shapesStore, shapeFiles);
|
|
248
|
-
results.push(result);
|
|
249
|
-
|
|
250
|
-
if (!result.valid) {
|
|
251
|
-
const violations = result.results.filter((r) => r.severity === 'violation');
|
|
252
|
-
if (violations.length > 0) {
|
|
253
|
-
hasViolations = true;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Print result immediately in human-readable mode
|
|
258
|
-
if (!globalOpts.json) {
|
|
259
|
-
console.log(formatResultHuman(result, globalOpts.verbose));
|
|
260
|
-
}
|
|
261
|
-
} catch (e: unknown) {
|
|
262
|
-
hasErrors = true;
|
|
263
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
264
|
-
const errorResult: ValidationResult = {
|
|
265
|
-
valid: false,
|
|
266
|
-
file,
|
|
267
|
-
results: [{
|
|
268
|
-
severity: 'violation',
|
|
269
|
-
shape: '',
|
|
270
|
-
property: '',
|
|
271
|
-
message: `Error processing file: ${msg}`,
|
|
272
|
-
}],
|
|
273
|
-
shapesUsed: [],
|
|
274
|
-
quadCount: 0,
|
|
275
|
-
subjects: [],
|
|
276
|
-
};
|
|
277
|
-
results.push(errorResult);
|
|
278
|
-
if (!globalOpts.json) {
|
|
279
|
-
console.log(formatResultHuman(errorResult, globalOpts.verbose));
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Output JSON
|
|
285
|
-
if (globalOpts.json) {
|
|
286
|
-
console.log(JSON.stringify(results, null, 2));
|
|
287
|
-
} else {
|
|
288
|
-
// Print summary for multiple files
|
|
289
|
-
if (results.length > 1) {
|
|
290
|
-
printSummary(results, globalOpts);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Set exit code
|
|
295
|
-
if (hasErrors) {
|
|
296
|
-
process.exitCode = 2;
|
|
297
|
-
} else if (hasViolations) {
|
|
298
|
-
process.exitCode = 1;
|
|
299
|
-
} else {
|
|
300
|
-
process.exitCode = 0;
|
|
301
|
-
}
|
|
302
|
-
});
|
|
303
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @the-cascade-protocol/cli
|
|
5
|
-
*
|
|
6
|
-
* Cascade Protocol CLI - Validate, convert, and manage health data.
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* cascade <command> [options]
|
|
10
|
-
*
|
|
11
|
-
* Commands:
|
|
12
|
-
* validate Validate Cascade data against SHACL shapes
|
|
13
|
-
* convert Convert between health data formats
|
|
14
|
-
* pod Manage Cascade Pod structures
|
|
15
|
-
* conformance Run conformance test suite
|
|
16
|
-
* serve Start local agent server
|
|
17
|
-
* capabilities Show machine-readable tool descriptions
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { Command } from 'commander';
|
|
21
|
-
import { registerValidateCommand } from './commands/validate.js';
|
|
22
|
-
import { registerConvertCommand } from './commands/convert.js';
|
|
23
|
-
import { registerPodCommand } from './commands/pod/index.js';
|
|
24
|
-
import { registerConformanceCommand } from './commands/conformance.js';
|
|
25
|
-
import { registerServeCommand } from './commands/serve.js';
|
|
26
|
-
import { registerCapabilitiesCommand } from './commands/capabilities.js';
|
|
27
|
-
|
|
28
|
-
const program = new Command();
|
|
29
|
-
|
|
30
|
-
program
|
|
31
|
-
.name('cascade')
|
|
32
|
-
.description('Cascade Protocol CLI')
|
|
33
|
-
.version('0.2.0')
|
|
34
|
-
.option('--verbose', 'Verbose output', false)
|
|
35
|
-
.option('--json', 'Output results as JSON (machine-readable)', false);
|
|
36
|
-
|
|
37
|
-
// Register all commands
|
|
38
|
-
registerValidateCommand(program);
|
|
39
|
-
registerConvertCommand(program);
|
|
40
|
-
registerPodCommand(program);
|
|
41
|
-
registerConformanceCommand(program);
|
|
42
|
-
registerServeCommand(program);
|
|
43
|
-
registerCapabilitiesCommand(program);
|
|
44
|
-
|
|
45
|
-
// Custom help text with examples
|
|
46
|
-
program.addHelpText(
|
|
47
|
-
'after',
|
|
48
|
-
`
|
|
49
|
-
Examples:
|
|
50
|
-
cascade validate record.ttl
|
|
51
|
-
cascade convert --from fhir --to cascade patient.json
|
|
52
|
-
cascade pod init ./my-pod
|
|
53
|
-
cascade capabilities
|
|
54
|
-
cascade capabilities --json
|
|
55
|
-
`,
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
program.parse();
|