@principal-ai/principal-studio-cli 0.35.3
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/README.md +172 -0
- package/dist/commands/agent-session.d.ts +3 -0
- package/dist/commands/agent-session.d.ts.map +1 -0
- package/dist/commands/agent-session.js +67 -0
- package/dist/commands/agent-sessions.d.ts +11 -0
- package/dist/commands/agent-sessions.d.ts.map +1 -0
- package/dist/commands/agent-sessions.js +39 -0
- package/dist/commands/auxiliary/index.d.ts +3 -0
- package/dist/commands/auxiliary/index.d.ts.map +1 -0
- package/dist/commands/auxiliary/index.js +9 -0
- package/dist/commands/auxiliary/validate.d.ts +3 -0
- package/dist/commands/auxiliary/validate.d.ts.map +1 -0
- package/dist/commands/auxiliary/validate.js +123 -0
- package/dist/commands/collector/check.d.ts +6 -0
- package/dist/commands/collector/check.d.ts.map +1 -0
- package/dist/commands/collector/check.js +153 -0
- package/dist/commands/collector/diagnose.d.ts +6 -0
- package/dist/commands/collector/diagnose.d.ts.map +1 -0
- package/dist/commands/collector/diagnose.js +229 -0
- package/dist/commands/collector/index.d.ts +6 -0
- package/dist/commands/collector/index.d.ts.map +1 -0
- package/dist/commands/collector/index.js +16 -0
- package/dist/commands/collector/status.d.ts +6 -0
- package/dist/commands/collector/status.d.ts.map +1 -0
- package/dist/commands/collector/status.js +77 -0
- package/dist/commands/collector/utils.d.ts +92 -0
- package/dist/commands/collector/utils.d.ts.map +1 -0
- package/dist/commands/collector/utils.js +154 -0
- package/dist/commands/coverage.d.ts +9 -0
- package/dist/commands/coverage.d.ts.map +1 -0
- package/dist/commands/coverage.js +252 -0
- package/dist/commands/create.d.ts +6 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +50 -0
- package/dist/commands/events/index.d.ts +3 -0
- package/dist/commands/events/index.d.ts.map +1 -0
- package/dist/commands/events/index.js +9 -0
- package/dist/commands/events/validate.d.ts +3 -0
- package/dist/commands/events/validate.d.ts.map +1 -0
- package/dist/commands/events/validate.js +124 -0
- package/dist/commands/formats.d.ts +6 -0
- package/dist/commands/formats.d.ts.map +1 -0
- package/dist/commands/formats.js +1118 -0
- package/dist/commands/hooks.d.ts +9 -0
- package/dist/commands/hooks.d.ts.map +1 -0
- package/dist/commands/hooks.js +295 -0
- package/dist/commands/inbox.d.ts +13 -0
- package/dist/commands/inbox.d.ts.map +1 -0
- package/dist/commands/inbox.js +179 -0
- package/dist/commands/init.d.ts +6 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +220 -0
- package/dist/commands/lint.d.ts +11 -0
- package/dist/commands/lint.d.ts.map +1 -0
- package/dist/commands/lint.js +71 -0
- package/dist/commands/list.d.ts +6 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +78 -0
- package/dist/commands/migrate-nodes.d.ts +9 -0
- package/dist/commands/migrate-nodes.d.ts.map +1 -0
- package/dist/commands/migrate-nodes.js +363 -0
- package/dist/commands/migrate-scopes-to-canvas.d.ts +3 -0
- package/dist/commands/migrate-scopes-to-canvas.d.ts.map +1 -0
- package/dist/commands/migrate-scopes-to-canvas.js +143 -0
- package/dist/commands/migration.d.ts +6 -0
- package/dist/commands/migration.d.ts.map +1 -0
- package/dist/commands/migration.js +247 -0
- package/dist/commands/open-studio.d.ts +10 -0
- package/dist/commands/open-studio.d.ts.map +1 -0
- package/dist/commands/open-studio.js +32 -0
- package/dist/commands/opencode/fetch.d.ts +3 -0
- package/dist/commands/opencode/fetch.d.ts.map +1 -0
- package/dist/commands/opencode/fetch.js +25 -0
- package/dist/commands/opencode/index.d.ts +3 -0
- package/dist/commands/opencode/index.d.ts.map +1 -0
- package/dist/commands/opencode/index.js +11 -0
- package/dist/commands/opencode/list-sessions.d.ts +3 -0
- package/dist/commands/opencode/list-sessions.d.ts.map +1 -0
- package/dist/commands/opencode/list-sessions.js +20 -0
- package/dist/commands/repo.d.ts +18 -0
- package/dist/commands/repo.d.ts.map +1 -0
- package/dist/commands/repo.js +233 -0
- package/dist/commands/schema.d.ts +6 -0
- package/dist/commands/schema.d.ts.map +1 -0
- package/dist/commands/schema.js +367 -0
- package/dist/commands/scopes/index.d.ts +3 -0
- package/dist/commands/scopes/index.d.ts.map +1 -0
- package/dist/commands/scopes/index.js +9 -0
- package/dist/commands/scopes/validate.d.ts +3 -0
- package/dist/commands/scopes/validate.d.ts.map +1 -0
- package/dist/commands/scopes/validate.js +187 -0
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +406 -0
- package/dist/commands/starred-collections.d.ts +12 -0
- package/dist/commands/starred-collections.d.ts.map +1 -0
- package/dist/commands/starred-collections.js +310 -0
- package/dist/commands/subsystem-model.d.ts +11 -0
- package/dist/commands/subsystem-model.d.ts.map +1 -0
- package/dist/commands/subsystem-model.js +249 -0
- package/dist/commands/topic.d.ts +15 -0
- package/dist/commands/topic.d.ts.map +1 -0
- package/dist/commands/topic.js +683 -0
- package/dist/commands/tour.d.ts +23 -0
- package/dist/commands/tour.d.ts.map +1 -0
- package/dist/commands/tour.js +783 -0
- package/dist/commands/trace/flow.d.ts +6 -0
- package/dist/commands/trace/flow.d.ts.map +1 -0
- package/dist/commands/trace/flow.js +158 -0
- package/dist/commands/trace/index.d.ts +6 -0
- package/dist/commands/trace/index.d.ts.map +1 -0
- package/dist/commands/trace/index.js +20 -0
- package/dist/commands/trace/inspect.d.ts +6 -0
- package/dist/commands/trace/inspect.d.ts.map +1 -0
- package/dist/commands/trace/inspect.js +187 -0
- package/dist/commands/trace/list.d.ts +6 -0
- package/dist/commands/trace/list.d.ts.map +1 -0
- package/dist/commands/trace/list.js +145 -0
- package/dist/commands/trace/registrations.d.ts +6 -0
- package/dist/commands/trace/registrations.d.ts.map +1 -0
- package/dist/commands/trace/registrations.js +81 -0
- package/dist/commands/trace/validate.d.ts +6 -0
- package/dist/commands/trace/validate.d.ts.map +1 -0
- package/dist/commands/trace/validate.js +276 -0
- package/dist/commands/trail.d.ts +36 -0
- package/dist/commands/trail.d.ts.map +1 -0
- package/dist/commands/trail.js +622 -0
- package/dist/commands/validate.d.ts +12 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +3334 -0
- package/dist/commands/workflow/implementation.d.ts +3 -0
- package/dist/commands/workflow/implementation.d.ts.map +1 -0
- package/dist/commands/workflow/implementation.js +51 -0
- package/dist/commands/workflow/index.d.ts +3 -0
- package/dist/commands/workflow/index.d.ts.map +1 -0
- package/dist/commands/workflow/index.js +19 -0
- package/dist/commands/workflow/inspect.d.ts +3 -0
- package/dist/commands/workflow/inspect.d.ts.map +1 -0
- package/dist/commands/workflow/inspect.js +123 -0
- package/dist/commands/workflow/list.d.ts +3 -0
- package/dist/commands/workflow/list.d.ts.map +1 -0
- package/dist/commands/workflow/list.js +95 -0
- package/dist/commands/workflow/render.d.ts +3 -0
- package/dist/commands/workflow/render.d.ts.map +1 -0
- package/dist/commands/workflow/render.js +88 -0
- package/dist/commands/workflow/test.d.ts +3 -0
- package/dist/commands/workflow/test.d.ts.map +1 -0
- package/dist/commands/workflow/test.js +141 -0
- package/dist/commands/workflow/utils.d.ts +49 -0
- package/dist/commands/workflow/utils.d.ts.map +1 -0
- package/dist/commands/workflow/utils.js +164 -0
- package/dist/commands/workflow/validate.d.ts +3 -0
- package/dist/commands/workflow/validate.d.ts.map +1 -0
- package/dist/commands/workflow/validate.js +344 -0
- package/dist/file-utils.d.ts +15 -0
- package/dist/file-utils.d.ts.map +1 -0
- package/dist/file-utils.js +29 -0
- package/dist/index.cjs +285718 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/lib/bridge-ipc.d.ts +29 -0
- package/dist/lib/bridge-ipc.d.ts.map +1 -0
- package/dist/lib/bridge-ipc.js +128 -0
- package/dist/lib/github-user.d.ts +17 -0
- package/dist/lib/github-user.d.ts.map +1 -0
- package/dist/lib/github-user.js +53 -0
- package/dist/lib/open-url.d.ts +11 -0
- package/dist/lib/open-url.d.ts.map +1 -0
- package/dist/lib/open-url.js +45 -0
- package/dist/lib/subsystem-model-store.d.ts +74 -0
- package/dist/lib/subsystem-model-store.d.ts.map +1 -0
- package/dist/lib/subsystem-model-store.js +292 -0
- package/dist/lib/tour-cache.d.ts +26 -0
- package/dist/lib/tour-cache.d.ts.map +1 -0
- package/dist/lib/tour-cache.js +46 -0
- package/dist/lib/trail-cache.d.ts +40 -0
- package/dist/lib/trail-cache.d.ts.map +1 -0
- package/dist/lib/trail-cache.js +97 -0
- package/dist/lib/viewer-ipc.d.ts +44 -0
- package/dist/lib/viewer-ipc.d.ts.map +1 -0
- package/dist/lib/viewer-ipc.js +68 -0
- package/package.json +68 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { ExecutionValidator } from '@principal-ai/subsystems-core';
|
|
4
|
+
/**
|
|
5
|
+
* Load a workflow template from a file
|
|
6
|
+
*/
|
|
7
|
+
export async function loadWorkflow(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
const content = await readFile(filePath, 'utf-8');
|
|
10
|
+
return JSON.parse(content);
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
if (error.code === 'ENOENT') {
|
|
14
|
+
throw new Error(`Workflow file not found: ${filePath}`);
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Failed to parse workflow file: ${error.message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Load execution data from a .otel.json file
|
|
21
|
+
*
|
|
22
|
+
* Automatically handles OTLP standard format conversion via ExecutionValidator.
|
|
23
|
+
*/
|
|
24
|
+
export async function loadExecution(filePath) {
|
|
25
|
+
try {
|
|
26
|
+
const content = await readFile(filePath, 'utf-8');
|
|
27
|
+
const parsed = JSON.parse(content);
|
|
28
|
+
// Use ExecutionValidator to handle OTLP format conversion
|
|
29
|
+
const validator = new ExecutionValidator();
|
|
30
|
+
return validator.validateOrThrow(parsed, filePath);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
if (error.code === 'ENOENT') {
|
|
34
|
+
throw new Error(`Execution file not found: ${filePath}`);
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Failed to parse execution file: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Convert execution data to OtelEvent array format expected by workflow APIs
|
|
41
|
+
*/
|
|
42
|
+
export function executionToEvents(execution) {
|
|
43
|
+
const events = [];
|
|
44
|
+
for (const span of execution.spans) {
|
|
45
|
+
for (const event of span.events) {
|
|
46
|
+
events.push({
|
|
47
|
+
name: event.name,
|
|
48
|
+
timestamp: event.time,
|
|
49
|
+
type: 'log',
|
|
50
|
+
attributes: {
|
|
51
|
+
...event.attributes,
|
|
52
|
+
'span.id': span.id,
|
|
53
|
+
'span.name': span.name,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return events;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Resolve a file path relative to a base directory
|
|
62
|
+
*/
|
|
63
|
+
export function resolvePath(filePath, baseDir) {
|
|
64
|
+
if (baseDir) {
|
|
65
|
+
return resolve(baseDir, filePath);
|
|
66
|
+
}
|
|
67
|
+
return resolve(filePath);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Format a timestamp as human-readable date/time
|
|
71
|
+
*/
|
|
72
|
+
export function formatTimestamp(timestamp) {
|
|
73
|
+
const date = new Date(timestamp);
|
|
74
|
+
return date.toLocaleString('en-US', {
|
|
75
|
+
year: 'numeric',
|
|
76
|
+
month: '2-digit',
|
|
77
|
+
day: '2-digit',
|
|
78
|
+
hour: '2-digit',
|
|
79
|
+
minute: '2-digit',
|
|
80
|
+
second: '2-digit',
|
|
81
|
+
hour12: false,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Format duration in milliseconds
|
|
86
|
+
*/
|
|
87
|
+
export function formatDuration(ms) {
|
|
88
|
+
if (ms < 1000) {
|
|
89
|
+
return `${ms}ms`;
|
|
90
|
+
}
|
|
91
|
+
const seconds = (ms / 1000).toFixed(1);
|
|
92
|
+
return `${seconds}s`;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Group attributes by prefix (e.g., 'auth.method' -> 'auth')
|
|
96
|
+
*/
|
|
97
|
+
export function groupAttributesByPrefix(attributes) {
|
|
98
|
+
const grouped = {};
|
|
99
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
100
|
+
const parts = key.split('.');
|
|
101
|
+
if (parts.length > 1) {
|
|
102
|
+
const prefix = parts[0];
|
|
103
|
+
if (!grouped[prefix]) {
|
|
104
|
+
grouped[prefix] = {};
|
|
105
|
+
}
|
|
106
|
+
grouped[prefix][key] = value;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
if (!grouped['Other']) {
|
|
110
|
+
grouped['Other'] = {};
|
|
111
|
+
}
|
|
112
|
+
grouped['Other'][key] = value;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return grouped;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Filter attributes by pattern (supports glob-like patterns)
|
|
119
|
+
*/
|
|
120
|
+
export function filterAttributes(attributes, pattern) {
|
|
121
|
+
const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
|
|
122
|
+
const filtered = {};
|
|
123
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
124
|
+
if (regex.test(key)) {
|
|
125
|
+
filtered[key] = value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return filtered;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Count events by type/name
|
|
132
|
+
*/
|
|
133
|
+
export function countEventsByType(events) {
|
|
134
|
+
const counts = new Map();
|
|
135
|
+
for (const event of events) {
|
|
136
|
+
const count = counts.get(event.name) || 0;
|
|
137
|
+
counts.set(event.name, count + 1);
|
|
138
|
+
}
|
|
139
|
+
return counts;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Format attribute value for display
|
|
143
|
+
*/
|
|
144
|
+
export function formatValue(value) {
|
|
145
|
+
if (typeof value === 'string') {
|
|
146
|
+
return `"${value}"`;
|
|
147
|
+
}
|
|
148
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
149
|
+
return String(value);
|
|
150
|
+
}
|
|
151
|
+
if (value === null || value === undefined) {
|
|
152
|
+
return 'null';
|
|
153
|
+
}
|
|
154
|
+
if (typeof value === 'object') {
|
|
155
|
+
return JSON.stringify(value);
|
|
156
|
+
}
|
|
157
|
+
return String(value);
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Capitalize first letter of a string
|
|
161
|
+
*/
|
|
162
|
+
export function capitalize(str) {
|
|
163
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
164
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../../src/commands/workflow/validate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA4BpC,wBAAgB,qBAAqB,IAAI,OAAO,CAkY/C"}
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { resolve, dirname } from 'node:path';
|
|
4
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
// Node.js-specific imports (validator)
|
|
7
|
+
import { WorkflowValidator, CanvasDiscovery, EventRegistry, getScopeNames, } from '@principal-ai/subsystems-core/node';
|
|
8
|
+
import { FilesystemService, NodeFileSystemAdapter } from '@principal-ai/codebase-composition/node';
|
|
9
|
+
import { NodeFileSystemAdapter as RepoFsAdapter } from '@principal-ai/repository-abstraction/node';
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
// Browser-safe imports
|
|
12
|
+
import { computeAggregates } from '@principal-ai/subsystems-core';
|
|
13
|
+
import { loadWorkflow, resolvePath, loadExecution, executionToEvents } from './utils.js';
|
|
14
|
+
export function createValidateCommand() {
|
|
15
|
+
const command = new Command('validate');
|
|
16
|
+
command
|
|
17
|
+
.description('Validate workflow template syntax, schema, and references')
|
|
18
|
+
.argument('<file>', 'Path to .workflow.json file')
|
|
19
|
+
.option('--canvas <path>', 'Override canvas file path for validation')
|
|
20
|
+
.option('--execution <path>', 'Execution file (.otel.json) for validating attribute references')
|
|
21
|
+
.option('--json', 'Output violations as JSON')
|
|
22
|
+
.option('-q, --quiet', 'Only show errors, suppress warnings')
|
|
23
|
+
.option('-d, --dir <path>', 'Project directory (default: cwd)')
|
|
24
|
+
.action(async (file, options) => {
|
|
25
|
+
try {
|
|
26
|
+
const baseDir = options.dir || process.cwd();
|
|
27
|
+
const workflowPath = resolvePath(file, baseDir);
|
|
28
|
+
// Load workflow
|
|
29
|
+
const workflow = await loadWorkflow(workflowPath);
|
|
30
|
+
// Resolve canvas path
|
|
31
|
+
// Canvas paths are always relative to repository root
|
|
32
|
+
let canvasPath;
|
|
33
|
+
let canvas;
|
|
34
|
+
if (options.canvas) {
|
|
35
|
+
canvasPath = resolvePath(options.canvas, baseDir);
|
|
36
|
+
}
|
|
37
|
+
else if (workflow.canvas) {
|
|
38
|
+
canvasPath = resolve(baseDir, workflow.canvas);
|
|
39
|
+
}
|
|
40
|
+
// Load canvas if path exists
|
|
41
|
+
if (canvasPath) {
|
|
42
|
+
try {
|
|
43
|
+
const canvasContent = readFileSync(canvasPath, 'utf-8');
|
|
44
|
+
canvas = JSON.parse(canvasContent);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
// Canvas not found or invalid - will be flagged by validator
|
|
48
|
+
canvas = undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Load execution data if provided
|
|
52
|
+
let executionData;
|
|
53
|
+
if (options.execution) {
|
|
54
|
+
try {
|
|
55
|
+
const executionPath = resolvePath(options.execution, baseDir);
|
|
56
|
+
const execution = await loadExecution(executionPath);
|
|
57
|
+
const events = executionToEvents(execution);
|
|
58
|
+
const aggregates = computeAggregates(events);
|
|
59
|
+
// Build event-specific attribute map
|
|
60
|
+
const eventAttributes = new Map();
|
|
61
|
+
for (const event of events) {
|
|
62
|
+
if (!eventAttributes.has(event.name)) {
|
|
63
|
+
eventAttributes.set(event.name, {});
|
|
64
|
+
}
|
|
65
|
+
const attrs = eventAttributes.get(event.name);
|
|
66
|
+
// Merge attributes from this event occurrence
|
|
67
|
+
if (event.attributes) {
|
|
68
|
+
Object.assign(attrs, event.attributes);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
executionData = { aggregates, eventAttributes };
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
console.error(chalk.yellow('Warning:'), `Failed to load execution file: ${error.message}`);
|
|
75
|
+
console.error(chalk.gray(' Attribute validation will be skipped'));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Discover co-located execution files for template completeness validation
|
|
79
|
+
const workflowDir = dirname(workflowPath);
|
|
80
|
+
const executionFiles = [];
|
|
81
|
+
try {
|
|
82
|
+
const filesInDir = readdirSync(workflowDir);
|
|
83
|
+
for (const file of filesInDir) {
|
|
84
|
+
if (file.endsWith('.otel.json')) {
|
|
85
|
+
executionFiles.push(resolve(workflowDir, file));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// Directory not readable, skip co-located file discovery
|
|
91
|
+
}
|
|
92
|
+
// Build EventRegistry and discover workflows for cross-canvas event lookup
|
|
93
|
+
let eventRegistry;
|
|
94
|
+
let allWorkflowEvents;
|
|
95
|
+
try {
|
|
96
|
+
// Load library
|
|
97
|
+
let library;
|
|
98
|
+
const libraryPath = resolve(baseDir, '.principal-views/library.yaml');
|
|
99
|
+
if (existsSync(libraryPath)) {
|
|
100
|
+
try {
|
|
101
|
+
const libraryContent = readFileSync(libraryPath, 'utf-8');
|
|
102
|
+
library = yaml.load(libraryContent);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Library failed to parse, continue without it
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Discover all canvases and workflows in the project
|
|
109
|
+
const service = new FilesystemService(new NodeFileSystemAdapter());
|
|
110
|
+
const fileTree = await service.buildFileSystemTreeFromPath(baseDir);
|
|
111
|
+
const fileReader = async (path) => readFile(resolve(baseDir, path), 'utf-8');
|
|
112
|
+
const discovery = new CanvasDiscovery();
|
|
113
|
+
const discoveryResult = await discovery.discover(fileTree, {
|
|
114
|
+
fileReader,
|
|
115
|
+
includeContent: true,
|
|
116
|
+
});
|
|
117
|
+
// Parse all canvases for EventRegistry
|
|
118
|
+
const parsedCanvases = new Map();
|
|
119
|
+
for (const discoveredCanvas of discoveryResult.canvases) {
|
|
120
|
+
if (discoveredCanvas.type === 'otel') {
|
|
121
|
+
try {
|
|
122
|
+
const canvasFullPath = resolve(baseDir, discoveredCanvas.path);
|
|
123
|
+
if (existsSync(canvasFullPath)) {
|
|
124
|
+
const content = readFileSync(canvasFullPath, 'utf-8');
|
|
125
|
+
const parsed = JSON.parse(content);
|
|
126
|
+
parsedCanvases.set(discoveredCanvas.path, parsed);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// Skip canvases that fail to parse
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// Build the registry
|
|
135
|
+
eventRegistry = EventRegistry.build(library, parsedCanvases, library ? libraryPath : undefined);
|
|
136
|
+
// Find all workflows for the same canvas (multi-workflow pattern support)
|
|
137
|
+
if (workflow.canvas) {
|
|
138
|
+
// Find the storyboard(s) that use this canvas
|
|
139
|
+
const storyboardsForCanvas = discoveryResult.storyboards.filter(sb => sb.canvas.path === workflow.canvas);
|
|
140
|
+
// If multiple workflows reference this canvas, collect all their events
|
|
141
|
+
const allWorkflows = storyboardsForCanvas.flatMap(sb => sb.workflows);
|
|
142
|
+
if (allWorkflows.length > 1) {
|
|
143
|
+
allWorkflowEvents = new Set();
|
|
144
|
+
for (const discoveredWorkflow of allWorkflows) {
|
|
145
|
+
// Read workflow content
|
|
146
|
+
try {
|
|
147
|
+
const workflowFullPath = resolve(baseDir, discoveredWorkflow.path);
|
|
148
|
+
const workflowContent = readFileSync(workflowFullPath, 'utf-8');
|
|
149
|
+
const wf = JSON.parse(workflowContent);
|
|
150
|
+
if (wf.scenarios) {
|
|
151
|
+
for (const scenario of wf.scenarios) {
|
|
152
|
+
// From template.events
|
|
153
|
+
if (scenario.template?.events) {
|
|
154
|
+
for (const eventName of Object.keys(scenario.template.events)) {
|
|
155
|
+
if (!eventName.includes('*')) {
|
|
156
|
+
allWorkflowEvents.add(eventName);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// Skip workflows that fail to parse
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
// Discovery failed - continue without it
|
|
172
|
+
// Validation will still work, just without cross-canvas suggestions
|
|
173
|
+
}
|
|
174
|
+
// Extract owned-scopes from library resources
|
|
175
|
+
let ownedScopes;
|
|
176
|
+
if (eventRegistry) {
|
|
177
|
+
// Get library from the registry's internal state
|
|
178
|
+
// The registry was built with the library, so we can extract scopes from resources
|
|
179
|
+
const libraryPath = resolve(baseDir, '.principal-views/library.yaml');
|
|
180
|
+
if (existsSync(libraryPath)) {
|
|
181
|
+
try {
|
|
182
|
+
const libraryContent = readFileSync(libraryPath, 'utf-8');
|
|
183
|
+
const lib = yaml.load(libraryContent);
|
|
184
|
+
if (lib?.resources) {
|
|
185
|
+
ownedScopes = [];
|
|
186
|
+
for (const [_resourceKey, attrs] of Object.entries(lib.resources)) {
|
|
187
|
+
// Handle both legacy (string[]) and new (Record<string, ScopeDefinition>) formats
|
|
188
|
+
const scopes = attrs['owned-scopes'];
|
|
189
|
+
const scopeNames = getScopeNames(scopes);
|
|
190
|
+
ownedScopes.push(...scopeNames);
|
|
191
|
+
// Also add the service name itself as a valid scope
|
|
192
|
+
const serviceName = attrs['service.name'];
|
|
193
|
+
if (typeof serviceName === 'string') {
|
|
194
|
+
ownedScopes.push(serviceName);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Remove duplicates
|
|
198
|
+
ownedScopes = [...new Set(ownedScopes)];
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// Failed to extract scopes, continue without
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Create validator
|
|
207
|
+
const validator = new WorkflowValidator(new RepoFsAdapter());
|
|
208
|
+
// Validate
|
|
209
|
+
const context = {
|
|
210
|
+
workflow,
|
|
211
|
+
workflowPath,
|
|
212
|
+
canvasPath,
|
|
213
|
+
canvas,
|
|
214
|
+
basePath: baseDir,
|
|
215
|
+
executionData,
|
|
216
|
+
executionFiles,
|
|
217
|
+
eventRegistry,
|
|
218
|
+
allWorkflowEvents,
|
|
219
|
+
ownedScopes,
|
|
220
|
+
};
|
|
221
|
+
const result = await validator.validate(context);
|
|
222
|
+
// Filter violations if quiet mode
|
|
223
|
+
const violations = options.quiet
|
|
224
|
+
? result.violations.filter((v) => v.severity === 'error')
|
|
225
|
+
: result.violations;
|
|
226
|
+
const errors = violations.filter((v) => v.severity === 'error');
|
|
227
|
+
const warnings = violations.filter((v) => v.severity === 'warn');
|
|
228
|
+
// Output
|
|
229
|
+
if (options.json) {
|
|
230
|
+
const output = {
|
|
231
|
+
file: file,
|
|
232
|
+
valid: errors.length === 0,
|
|
233
|
+
violations: violations.map((v) => ({
|
|
234
|
+
severity: v.severity,
|
|
235
|
+
ruleId: v.ruleId,
|
|
236
|
+
file: v.file,
|
|
237
|
+
path: v.path,
|
|
238
|
+
message: v.message,
|
|
239
|
+
impact: v.impact,
|
|
240
|
+
suggestion: v.suggestion,
|
|
241
|
+
fixable: v.fixable,
|
|
242
|
+
})),
|
|
243
|
+
summary: {
|
|
244
|
+
errors: errors.length,
|
|
245
|
+
warnings: warnings.length,
|
|
246
|
+
scenarioCount: workflow.scenarios.length,
|
|
247
|
+
scope: workflow.scope || null,
|
|
248
|
+
status: workflow.status || 'draft',
|
|
249
|
+
attributeValidation: executionData ? 'enabled' : 'skipped',
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
console.log(JSON.stringify(output, null, 2));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Text output
|
|
256
|
+
console.log(chalk.bold(`\nValidating: ${file}\n`));
|
|
257
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
258
|
+
console.log(chalk.green('✓'), 'Schema validation passed');
|
|
259
|
+
console.log(chalk.green('✓'), `${workflow.scenarios.length} scenarios found`);
|
|
260
|
+
const priorities = workflow.scenarios.map((s) => s.priority);
|
|
261
|
+
const allUnique = new Set(priorities).size === priorities.length;
|
|
262
|
+
console.log(chalk.green('✓'), allUnique ? 'All priorities unique' : 'Duplicate priorities found');
|
|
263
|
+
if (canvasPath) {
|
|
264
|
+
console.log(chalk.green('✓'), `Canvas: ${workflow.canvas || canvasPath} ✓`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
else {
|
|
268
|
+
// Show violations
|
|
269
|
+
for (const violation of violations) {
|
|
270
|
+
const label = violation.severity === 'error'
|
|
271
|
+
? chalk.red('error')
|
|
272
|
+
: chalk.yellow('warning');
|
|
273
|
+
console.log(`\n${label}: ${violation.message}`);
|
|
274
|
+
if (violation.path) {
|
|
275
|
+
console.log(chalk.gray(` Location: ${violation.path}`));
|
|
276
|
+
}
|
|
277
|
+
if (violation.impact) {
|
|
278
|
+
console.log(chalk.gray(` Impact: ${violation.impact}`));
|
|
279
|
+
}
|
|
280
|
+
if (violation.suggestion) {
|
|
281
|
+
console.log(chalk.cyan(` Suggestion: ${violation.suggestion}`));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Summary
|
|
286
|
+
console.log(chalk.bold('\nSummary:'));
|
|
287
|
+
if (errors.length > 0) {
|
|
288
|
+
console.log(chalk.red(` • ${errors.length} error(s)`));
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
console.log(chalk.green(' • 0 errors'));
|
|
292
|
+
}
|
|
293
|
+
if (warnings.length > 0) {
|
|
294
|
+
console.log(chalk.yellow(` • ${warnings.length} warning(s)`));
|
|
295
|
+
}
|
|
296
|
+
else if (!options.quiet) {
|
|
297
|
+
console.log(chalk.green(' • 0 warnings'));
|
|
298
|
+
}
|
|
299
|
+
console.log(chalk.gray(` • ${workflow.scenarios.length} scenario(s)`));
|
|
300
|
+
if (canvasPath) {
|
|
301
|
+
console.log(chalk.gray(` • Canvas: ${workflow.canvas || canvasPath}`));
|
|
302
|
+
}
|
|
303
|
+
// Show scope information
|
|
304
|
+
if (workflow.scope) {
|
|
305
|
+
console.log(chalk.gray(` • Scope: ${workflow.scope}`));
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
const status = workflow.status || 'draft';
|
|
309
|
+
if (status === 'approved' || status === 'implemented') {
|
|
310
|
+
console.log(chalk.yellow(` • Scope: not specified (required for ${status} status)`));
|
|
311
|
+
}
|
|
312
|
+
else {
|
|
313
|
+
console.log(chalk.gray(' • Scope: not specified'));
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Show status
|
|
317
|
+
const status = workflow.status || 'draft';
|
|
318
|
+
const statusColor = status === 'implemented' ? chalk.green :
|
|
319
|
+
status === 'approved' ? chalk.cyan :
|
|
320
|
+
chalk.gray;
|
|
321
|
+
console.log(chalk.gray(' • Status:'), statusColor(status));
|
|
322
|
+
if (executionData) {
|
|
323
|
+
console.log(chalk.gray(' • Attribute validation:'), chalk.green('enabled'));
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
console.log(chalk.gray(' • Attribute validation:'), chalk.gray('skipped (use --execution to enable)'));
|
|
327
|
+
}
|
|
328
|
+
if (executionFiles.length > 0) {
|
|
329
|
+
console.log(chalk.gray(` • Co-located executions: ${executionFiles.length} file(s) checked`));
|
|
330
|
+
}
|
|
331
|
+
console.log();
|
|
332
|
+
}
|
|
333
|
+
// Exit with error code if validation failed
|
|
334
|
+
if (errors.length > 0) {
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch (error) {
|
|
339
|
+
console.error(chalk.red('Error:'), error.message);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
return command;
|
|
344
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared file utilities for CLI commands
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* File types recognized by the CLI
|
|
6
|
+
*/
|
|
7
|
+
export type FileType = 'canvas' | 'workflow' | 'testTrace' | 'library' | 'dashboard' | 'unknown';
|
|
8
|
+
/**
|
|
9
|
+
* Determine file type based on naming convention
|
|
10
|
+
*
|
|
11
|
+
* This function is shared between validate and lint commands to ensure
|
|
12
|
+
* consistent file type detection across the CLI.
|
|
13
|
+
*/
|
|
14
|
+
export declare function determineFileType(filePath: string): FileType;
|
|
15
|
+
//# sourceMappingURL=file-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-utils.d.ts","sourceRoot":"","sources":["../src/file-utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,WAAW,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;AAEjG;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAmB5D"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared file utilities for CLI commands
|
|
3
|
+
*/
|
|
4
|
+
import { basename } from 'node:path';
|
|
5
|
+
/**
|
|
6
|
+
* Determine file type based on naming convention
|
|
7
|
+
*
|
|
8
|
+
* This function is shared between validate and lint commands to ensure
|
|
9
|
+
* consistent file type detection across the CLI.
|
|
10
|
+
*/
|
|
11
|
+
export function determineFileType(filePath) {
|
|
12
|
+
const name = basename(filePath).toLowerCase();
|
|
13
|
+
if (name.startsWith('library.')) {
|
|
14
|
+
return 'library';
|
|
15
|
+
}
|
|
16
|
+
if (name.endsWith('.workflow.json')) {
|
|
17
|
+
return 'workflow';
|
|
18
|
+
}
|
|
19
|
+
if (name.endsWith('.dashboard.json')) {
|
|
20
|
+
return 'dashboard';
|
|
21
|
+
}
|
|
22
|
+
if (name.endsWith('.otel.json')) {
|
|
23
|
+
return 'testTrace';
|
|
24
|
+
}
|
|
25
|
+
if (name.endsWith('.canvas') || name.endsWith('.otel.canvas')) {
|
|
26
|
+
return 'canvas';
|
|
27
|
+
}
|
|
28
|
+
return 'unknown';
|
|
29
|
+
}
|