@principal-ai/principal-view-cli 0.1.19 → 0.1.21

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.
@@ -17,13 +17,13 @@ function loadLibrary(principalViewsDir) {
17
17
  if (existsSync(libraryPath)) {
18
18
  try {
19
19
  const content = readFileSync(libraryPath, 'utf8');
20
- const library = fileName.endsWith('.json')
21
- ? JSON.parse(content)
22
- : yaml.load(content);
20
+ const library = fileName.endsWith('.json') ? JSON.parse(content) : yaml.load(content);
23
21
  if (library && typeof library === 'object') {
24
22
  return {
25
- nodeComponents: library.nodeComponents || {},
26
- edgeComponents: library.edgeComponents || {},
23
+ nodeComponents: library.nodeComponents ||
24
+ {},
25
+ edgeComponents: library.edgeComponents ||
26
+ {},
27
27
  raw: library,
28
28
  path: libraryPath,
29
29
  };
@@ -51,6 +51,8 @@ function validateLibrary(library) {
51
51
  if (compDef && typeof compDef === 'object') {
52
52
  const comp = compDef;
53
53
  checkUnknownFields(comp, ALLOWED_LIBRARY_FIELDS.nodeComponent, `nodeComponents.${compId}`, issues);
54
+ // Validate icon name format (must be PascalCase for Lucide icons)
55
+ validateIconName(comp.icon, `nodeComponents.${compId}.icon`, issues);
54
56
  // Check nested fields
55
57
  if (comp.size && typeof comp.size === 'object') {
56
58
  checkUnknownFields(comp.size, ALLOWED_LIBRARY_FIELDS.nodeComponentSize, `nodeComponents.${compId}.size`, issues);
@@ -59,6 +61,9 @@ function validateLibrary(library) {
59
61
  for (const [stateId, stateDef] of Object.entries(comp.states)) {
60
62
  if (stateDef && typeof stateDef === 'object') {
61
63
  checkUnknownFields(stateDef, ALLOWED_LIBRARY_FIELDS.nodeComponentState, `nodeComponents.${compId}.states.${stateId}`, issues);
64
+ // Validate state icon name format
65
+ const state = stateDef;
66
+ validateIconName(state.icon, `nodeComponents.${compId}.states.${stateId}.icon`, issues);
62
67
  }
63
68
  }
64
69
  }
@@ -121,6 +126,55 @@ const STANDARD_CANVAS_TYPES = ['text', 'group', 'file', 'link'];
121
126
  */
122
127
  const VALID_NODE_SHAPES = ['circle', 'rectangle', 'hexagon', 'diamond', 'custom'];
123
128
  // ============================================================================
129
+ // Icon Validation
130
+ // ============================================================================
131
+ /**
132
+ * Convert kebab-case to PascalCase
133
+ * e.g., "file-text" -> "FileText", "alert-circle" -> "AlertCircle"
134
+ */
135
+ function kebabToPascalCase(str) {
136
+ return str
137
+ .split('-')
138
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
139
+ .join('');
140
+ }
141
+ /**
142
+ * Check if a string looks like kebab-case (has hyphens and lowercase)
143
+ */
144
+ function isKebabCase(str) {
145
+ return str.includes('-') && str === str.toLowerCase();
146
+ }
147
+ /**
148
+ * Validate an icon name and return issues if invalid
149
+ * Icons should be in PascalCase (e.g., "FileText", "Database", "AlertCircle")
150
+ */
151
+ function validateIconName(iconValue, path, issues) {
152
+ if (typeof iconValue !== 'string' || !iconValue) {
153
+ return; // No icon specified, that's fine
154
+ }
155
+ // Check if it looks like kebab-case
156
+ if (isKebabCase(iconValue)) {
157
+ const suggested = kebabToPascalCase(iconValue);
158
+ issues.push({
159
+ type: 'error',
160
+ message: `Invalid icon name "${iconValue}" - icons must be in PascalCase`,
161
+ path,
162
+ suggestion: `Use "${suggested}" instead of "${iconValue}"`,
163
+ });
164
+ return;
165
+ }
166
+ // Check if first character is lowercase (common mistake)
167
+ if (iconValue[0] === iconValue[0].toLowerCase() && iconValue[0] !== iconValue[0].toUpperCase()) {
168
+ const suggested = iconValue.charAt(0).toUpperCase() + iconValue.slice(1);
169
+ issues.push({
170
+ type: 'error',
171
+ message: `Invalid icon name "${iconValue}" - icons must start with uppercase`,
172
+ path,
173
+ suggestion: `Use "${suggested}" instead of "${iconValue}"`,
174
+ });
175
+ }
176
+ }
177
+ // ============================================================================
124
178
  // Allowed Fields Definitions
125
179
  // ============================================================================
126
180
  /**
@@ -129,12 +183,27 @@ const VALID_NODE_SHAPES = ['circle', 'rectangle', 'hexagon', 'diamond', 'custom'
129
183
  const ALLOWED_CANVAS_FIELDS = {
130
184
  root: ['nodes', 'edges', 'pv'],
131
185
  pv: ['version', 'name', 'description', 'nodeTypes', 'edgeTypes', 'pathConfig', 'display'],
132
- pvPathConfig: ['projectRoot', 'captureSource', 'enableActionPatterns', 'logLevel', 'ignoreUnsourced'],
186
+ pvPathConfig: [
187
+ 'projectRoot',
188
+ 'captureSource',
189
+ 'enableActionPatterns',
190
+ 'logLevel',
191
+ 'ignoreUnsourced',
192
+ ],
133
193
  pvDisplay: ['layout', 'theme', 'animations'],
134
194
  pvDisplayTheme: ['primary', 'success', 'warning', 'danger', 'info'],
135
195
  pvDisplayAnimations: ['enabled', 'speed'],
136
196
  pvNodeType: ['label', 'description', 'color', 'icon', 'shape'],
137
- pvEdgeType: ['label', 'style', 'color', 'width', 'directed', 'animation', 'labelConfig', 'activatedBy'],
197
+ pvEdgeType: [
198
+ 'label',
199
+ 'style',
200
+ 'color',
201
+ 'width',
202
+ 'directed',
203
+ 'animation',
204
+ 'labelConfig',
205
+ 'activatedBy',
206
+ ],
138
207
  pvEdgeTypeAnimation: ['type', 'duration', 'color'],
139
208
  pvEdgeTypeLabelConfig: ['field', 'position'],
140
209
  // Base node fields from JSON Canvas spec
@@ -145,13 +214,36 @@ const ALLOWED_CANVAS_FIELDS = {
145
214
  nodeLink: ['url'],
146
215
  nodeGroup: ['label', 'background', 'backgroundStyle'],
147
216
  // Node pv extension
148
- nodePv: ['nodeType', 'description', 'shape', 'icon', 'fill', 'stroke', 'states', 'sources', 'actions', 'dataSchema', 'layout'],
217
+ nodePv: [
218
+ 'nodeType',
219
+ 'description',
220
+ 'shape',
221
+ 'icon',
222
+ 'fill',
223
+ 'stroke',
224
+ 'states',
225
+ 'sources',
226
+ 'actions',
227
+ 'dataSchema',
228
+ 'layout',
229
+ ],
149
230
  nodePvState: ['color', 'icon', 'label'],
150
231
  nodePvAction: ['pattern', 'event', 'state', 'metadata', 'triggerEdges'],
151
232
  nodePvDataSchemaField: ['type', 'required', 'displayInLabel'],
152
233
  nodePvLayout: ['layer', 'cluster'],
153
234
  // Edge fields
154
- edge: ['id', 'fromNode', 'toNode', 'fromSide', 'toSide', 'fromEnd', 'toEnd', 'color', 'label', 'pv'],
235
+ edge: [
236
+ 'id',
237
+ 'fromNode',
238
+ 'toNode',
239
+ 'fromSide',
240
+ 'toSide',
241
+ 'fromEnd',
242
+ 'toEnd',
243
+ 'color',
244
+ 'label',
245
+ 'pv',
246
+ ],
155
247
  edgePv: ['edgeType', 'style', 'width', 'animation', 'activatedBy'],
156
248
  edgePvAnimation: ['type', 'duration', 'color'],
157
249
  edgePvActivatedBy: ['action', 'animation', 'direction', 'duration'],
@@ -161,13 +253,35 @@ const ALLOWED_CANVAS_FIELDS = {
161
253
  */
162
254
  const ALLOWED_LIBRARY_FIELDS = {
163
255
  root: ['version', 'name', 'description', 'nodeComponents', 'edgeComponents', 'connectionRules'],
164
- nodeComponent: ['description', 'tags', 'defaultLabel', 'shape', 'icon', 'color', 'size', 'states', 'sources', 'actions', 'dataSchema', 'layout'],
256
+ nodeComponent: [
257
+ 'description',
258
+ 'tags',
259
+ 'defaultLabel',
260
+ 'shape',
261
+ 'icon',
262
+ 'color',
263
+ 'size',
264
+ 'states',
265
+ 'sources',
266
+ 'actions',
267
+ 'dataSchema',
268
+ 'layout',
269
+ ],
165
270
  nodeComponentSize: ['width', 'height'],
166
271
  nodeComponentState: ['color', 'icon', 'label'],
167
272
  nodeComponentAction: ['pattern', 'event', 'state', 'metadata', 'triggerEdges'],
168
273
  nodeComponentDataSchemaField: ['type', 'required', 'displayInLabel', 'label', 'displayInInfo'],
169
274
  nodeComponentLayout: ['layer', 'cluster'],
170
- edgeComponent: ['description', 'tags', 'style', 'color', 'width', 'directed', 'animation', 'label'],
275
+ edgeComponent: [
276
+ 'description',
277
+ 'tags',
278
+ 'style',
279
+ 'color',
280
+ 'width',
281
+ 'directed',
282
+ 'animation',
283
+ 'label',
284
+ ],
171
285
  edgeComponentAnimation: ['type', 'duration', 'color'],
172
286
  edgeComponentLabel: ['field', 'position'],
173
287
  connectionRule: ['from', 'to', 'via', 'constraints'],
@@ -293,6 +407,9 @@ function validateCanvas(canvas, filePath, library) {
293
407
  for (const [typeId, typeDef] of Object.entries(pv.nodeTypes)) {
294
408
  if (typeDef && typeof typeDef === 'object') {
295
409
  checkUnknownFields(typeDef, ALLOWED_CANVAS_FIELDS.pvNodeType, `pv.nodeTypes.${typeId}`, issues);
410
+ // Validate icon name format
411
+ const nodeType = typeDef;
412
+ validateIconName(nodeType.icon, `pv.nodeTypes.${typeId}.icon`, issues);
296
413
  }
297
414
  }
298
415
  }
@@ -323,7 +440,11 @@ function validateCanvas(canvas, filePath, library) {
323
440
  else {
324
441
  c.nodes.forEach((node, index) => {
325
442
  if (!node || typeof node !== 'object') {
326
- issues.push({ type: 'error', message: `Node at index ${index} must be an object`, path: `nodes[${index}]` });
443
+ issues.push({
444
+ type: 'error',
445
+ message: `Node at index ${index} must be an object`,
446
+ path: `nodes[${index}]`,
447
+ });
327
448
  return;
328
449
  }
329
450
  const n = node;
@@ -347,23 +468,72 @@ function validateCanvas(canvas, filePath, library) {
347
468
  // Custom types can have any base fields
348
469
  checkUnknownFields(n, allowedNodeFields, nodePath, issues);
349
470
  if (typeof n.id !== 'string' || !n.id) {
350
- issues.push({ type: 'error', message: `Node at index ${index} must have a string "id"`, path: `${nodePath}.id` });
471
+ issues.push({
472
+ type: 'error',
473
+ message: `Node at index ${index} must have a string "id"`,
474
+ path: `${nodePath}.id`,
475
+ });
351
476
  }
352
477
  if (typeof n.type !== 'string') {
353
- issues.push({ type: 'error', message: `Node "${nodeLabel}" must have a string "type"`, path: `${nodePath}.type` });
478
+ issues.push({
479
+ type: 'error',
480
+ message: `Node "${nodeLabel}" must have a string "type"`,
481
+ path: `${nodePath}.type`,
482
+ });
354
483
  }
355
484
  if (typeof n.x !== 'number') {
356
- issues.push({ type: 'error', message: `Node "${nodeLabel}" must have a numeric "x" position`, path: `${nodePath}.x` });
485
+ issues.push({
486
+ type: 'error',
487
+ message: `Node "${nodeLabel}" must have a numeric "x" position`,
488
+ path: `${nodePath}.x`,
489
+ });
357
490
  }
358
491
  if (typeof n.y !== 'number') {
359
- issues.push({ type: 'error', message: `Node "${nodeLabel}" must have a numeric "y" position`, path: `${nodePath}.y` });
492
+ issues.push({
493
+ type: 'error',
494
+ message: `Node "${nodeLabel}" must have a numeric "y" position`,
495
+ path: `${nodePath}.y`,
496
+ });
360
497
  }
361
498
  // Width and height are now REQUIRED (was warning)
362
499
  if (typeof n.width !== 'number') {
363
- issues.push({ type: 'error', message: `Node "${nodeLabel}" must have a numeric "width"`, path: `${nodePath}.width` });
500
+ issues.push({
501
+ type: 'error',
502
+ message: `Node "${nodeLabel}" must have a numeric "width"`,
503
+ path: `${nodePath}.width`,
504
+ });
364
505
  }
365
506
  if (typeof n.height !== 'number') {
366
- issues.push({ type: 'error', message: `Node "${nodeLabel}" must have a numeric "height"`, path: `${nodePath}.height` });
507
+ issues.push({
508
+ type: 'error',
509
+ message: `Node "${nodeLabel}" must have a numeric "height"`,
510
+ path: `${nodePath}.height`,
511
+ });
512
+ }
513
+ // Validate required fields for standard canvas types
514
+ if (nodeType === 'text' && (typeof n.text !== 'string' || !n.text)) {
515
+ issues.push({
516
+ type: 'error',
517
+ message: `Node "${nodeLabel}" has type "text" but is missing required "text" field`,
518
+ path: `${nodePath}.text`,
519
+ suggestion: 'Add a "text" field with markdown content, or change the node type',
520
+ });
521
+ }
522
+ if (nodeType === 'file' && (typeof n.file !== 'string' || !n.file)) {
523
+ issues.push({
524
+ type: 'error',
525
+ message: `Node "${nodeLabel}" has type "file" but is missing required "file" field`,
526
+ path: `${nodePath}.file`,
527
+ suggestion: 'Add a "file" field with a file path, or change the node type',
528
+ });
529
+ }
530
+ if (nodeType === 'link' && (typeof n.url !== 'string' || !n.url)) {
531
+ issues.push({
532
+ type: 'error',
533
+ message: `Node "${nodeLabel}" has type "link" but is missing required "url" field`,
534
+ path: `${nodePath}.url`,
535
+ suggestion: 'Add a "url" field with a URL, or change the node type',
536
+ });
367
537
  }
368
538
  // Validate node type - must be standard canvas type OR have pv metadata
369
539
  const isStandardType = STANDARD_CANVAS_TYPES.includes(nodeType);
@@ -386,7 +556,8 @@ function validateCanvas(canvas, filePath, library) {
386
556
  path: `nodes[${index}].pv.nodeType`,
387
557
  });
388
558
  }
389
- if (typeof nodePv.shape !== 'string' || !VALID_NODE_SHAPES.includes(nodePv.shape)) {
559
+ if (typeof nodePv.shape !== 'string' ||
560
+ !VALID_NODE_SHAPES.includes(nodePv.shape)) {
390
561
  issues.push({
391
562
  type: 'error',
392
563
  message: `Node "${n.id || index}" must have a valid "pv.shape"`,
@@ -401,11 +572,16 @@ function validateCanvas(canvas, filePath, library) {
401
572
  const nodePv = n.pv;
402
573
  // Check unknown fields in node pv extension
403
574
  checkUnknownFields(nodePv, ALLOWED_CANVAS_FIELDS.nodePv, `${nodePath}.pv`, issues);
575
+ // Validate icon name format (must be PascalCase for Lucide icons)
576
+ validateIconName(nodePv.icon, `${nodePath}.pv.icon`, issues);
404
577
  // Check nested pv fields
405
578
  if (nodePv.states && typeof nodePv.states === 'object') {
406
579
  for (const [stateId, stateDef] of Object.entries(nodePv.states)) {
407
580
  if (stateDef && typeof stateDef === 'object') {
408
581
  checkUnknownFields(stateDef, ALLOWED_CANVAS_FIELDS.nodePvState, `${nodePath}.pv.states.${stateId}`, issues);
582
+ // Validate state icon name format
583
+ const state = stateDef;
584
+ validateIconName(state.icon, `${nodePath}.pv.states.${stateId}.icon`, issues);
409
585
  }
410
586
  }
411
587
  }
@@ -464,10 +640,14 @@ function validateCanvas(canvas, filePath, library) {
464
640
  issues.push({ type: 'error', message: '"edges" must be an array if present' });
465
641
  }
466
642
  else if (Array.isArray(c.edges)) {
467
- const nodeIds = new Set(c.nodes?.map(n => n.id) || []);
643
+ const nodeIds = new Set(c.nodes?.map((n) => n.id) || []);
468
644
  c.edges.forEach((edge, index) => {
469
645
  if (!edge || typeof edge !== 'object') {
470
- issues.push({ type: 'error', message: `Edge at index ${index} must be an object`, path: `edges[${index}]` });
646
+ issues.push({
647
+ type: 'error',
648
+ message: `Edge at index ${index} must be an object`,
649
+ path: `edges[${index}]`,
650
+ });
471
651
  return;
472
652
  }
473
653
  const e = edge;
@@ -476,19 +656,73 @@ function validateCanvas(canvas, filePath, library) {
476
656
  // Check unknown fields on edge
477
657
  checkUnknownFields(e, ALLOWED_CANVAS_FIELDS.edge, edgePath, issues);
478
658
  if (typeof e.id !== 'string' || !e.id) {
479
- issues.push({ type: 'error', message: `Edge at index ${index} must have a string "id"`, path: `${edgePath}.id` });
659
+ issues.push({
660
+ type: 'error',
661
+ message: `Edge at index ${index} must have a string "id"`,
662
+ path: `${edgePath}.id`,
663
+ });
480
664
  }
481
665
  if (typeof e.fromNode !== 'string') {
482
- issues.push({ type: 'error', message: `Edge "${edgeLabel}" must have a string "fromNode"`, path: `${edgePath}.fromNode` });
666
+ issues.push({
667
+ type: 'error',
668
+ message: `Edge "${edgeLabel}" must have a string "fromNode"`,
669
+ path: `${edgePath}.fromNode`,
670
+ });
483
671
  }
484
672
  else if (!nodeIds.has(e.fromNode)) {
485
- issues.push({ type: 'error', message: `Edge "${edgeLabel}" references unknown node "${e.fromNode}"`, path: `${edgePath}.fromNode` });
673
+ issues.push({
674
+ type: 'error',
675
+ message: `Edge "${edgeLabel}" references unknown node "${e.fromNode}"`,
676
+ path: `${edgePath}.fromNode`,
677
+ });
486
678
  }
487
679
  if (typeof e.toNode !== 'string') {
488
- issues.push({ type: 'error', message: `Edge "${edgeLabel}" must have a string "toNode"`, path: `${edgePath}.toNode` });
680
+ issues.push({
681
+ type: 'error',
682
+ message: `Edge "${edgeLabel}" must have a string "toNode"`,
683
+ path: `${edgePath}.toNode`,
684
+ });
489
685
  }
490
686
  else if (!nodeIds.has(e.toNode)) {
491
- issues.push({ type: 'error', message: `Edge "${edgeLabel}" references unknown node "${e.toNode}"`, path: `${edgePath}.toNode` });
687
+ issues.push({
688
+ type: 'error',
689
+ message: `Edge "${edgeLabel}" references unknown node "${e.toNode}"`,
690
+ path: `${edgePath}.toNode`,
691
+ });
692
+ }
693
+ // Validate fromSide and toSide are present and valid
694
+ const VALID_SIDES = ['top', 'right', 'bottom', 'left'];
695
+ if (typeof e.fromSide !== 'string') {
696
+ issues.push({
697
+ type: 'error',
698
+ message: `Edge "${edgeLabel}" must have a "fromSide" field`,
699
+ path: `${edgePath}.fromSide`,
700
+ suggestion: `Specify which side of the source node the edge starts from: ${VALID_SIDES.join(', ')}`,
701
+ });
702
+ }
703
+ else if (!VALID_SIDES.includes(e.fromSide)) {
704
+ issues.push({
705
+ type: 'error',
706
+ message: `Edge "${edgeLabel}" has invalid fromSide "${e.fromSide}"`,
707
+ path: `${edgePath}.fromSide`,
708
+ suggestion: `Valid values: ${VALID_SIDES.join(', ')}`,
709
+ });
710
+ }
711
+ if (typeof e.toSide !== 'string') {
712
+ issues.push({
713
+ type: 'error',
714
+ message: `Edge "${edgeLabel}" must have a "toSide" field`,
715
+ path: `${edgePath}.toSide`,
716
+ suggestion: `Specify which side of the target node the edge connects to: ${VALID_SIDES.join(', ')}`,
717
+ });
718
+ }
719
+ else if (!VALID_SIDES.includes(e.toSide)) {
720
+ issues.push({
721
+ type: 'error',
722
+ message: `Edge "${edgeLabel}" has invalid toSide "${e.toSide}"`,
723
+ path: `${edgePath}.toSide`,
724
+ suggestion: `Valid values: ${VALID_SIDES.join(', ')}`,
725
+ });
492
726
  }
493
727
  // Validate edge pv extension fields
494
728
  if (e.pv && typeof e.pv === 'object') {
@@ -558,7 +792,7 @@ function validateFile(filePath, library) {
558
792
  const content = readFileSync(absolutePath, 'utf8');
559
793
  const canvas = JSON.parse(content);
560
794
  const issues = validateCanvas(canvas, relativePath, library);
561
- const hasErrors = issues.some(i => i.type === 'error');
795
+ const hasErrors = issues.some((i) => i.type === 'error');
562
796
  return {
563
797
  file: relativePath,
564
798
  isValid: !hasErrors,
@@ -607,7 +841,7 @@ export function createValidateCommand() {
607
841
  let libraryResult = null;
608
842
  if (library && Object.keys(library.raw).length > 0) {
609
843
  const libraryIssues = validateLibrary(library);
610
- const libraryHasErrors = libraryIssues.some(i => i.type === 'error');
844
+ const libraryHasErrors = libraryIssues.some((i) => i.type === 'error');
611
845
  libraryResult = {
612
846
  file: relative(process.cwd(), library.path),
613
847
  isValid: !libraryHasErrors,
@@ -615,10 +849,10 @@ export function createValidateCommand() {
615
849
  };
616
850
  }
617
851
  // Validate all canvas files
618
- const results = matchedFiles.map(f => validateFile(f, library));
852
+ const results = matchedFiles.map((f) => validateFile(f, library));
619
853
  // Combine results
620
854
  const allResults = libraryResult ? [libraryResult, ...results] : results;
621
- const validCount = allResults.filter(r => r.isValid).length;
855
+ const validCount = allResults.filter((r) => r.isValid).length;
622
856
  const invalidCount = allResults.length - validCount;
623
857
  // Output results
624
858
  if (options.json) {
@@ -629,16 +863,18 @@ export function createValidateCommand() {
629
863
  }
630
864
  else {
631
865
  if (!options.quiet) {
632
- const fileCount = libraryResult ? `${results.length} canvas file(s) + library` : `${results.length} canvas file(s)`;
866
+ const fileCount = libraryResult
867
+ ? `${results.length} canvas file(s) + library`
868
+ : `${results.length} canvas file(s)`;
633
869
  console.log(chalk.bold(`\nValidating ${fileCount}...\n`));
634
870
  }
635
871
  for (const result of allResults) {
636
872
  if (result.isValid) {
637
873
  if (!options.quiet) {
638
874
  console.log(chalk.green(`✓ ${result.file}`));
639
- const warnings = result.issues.filter(i => i.type === 'warning');
875
+ const warnings = result.issues.filter((i) => i.type === 'warning');
640
876
  if (warnings.length > 0) {
641
- warnings.forEach(w => {
877
+ warnings.forEach((w) => {
642
878
  console.log(chalk.yellow(` ⚠ ${w.message}`));
643
879
  });
644
880
  }
@@ -646,7 +882,7 @@ export function createValidateCommand() {
646
882
  }
647
883
  else {
648
884
  console.log(chalk.red(`✗ ${result.file}`));
649
- result.issues.forEach(issue => {
885
+ result.issues.forEach((issue) => {
650
886
  const icon = issue.type === 'error' ? '✗' : '⚠';
651
887
  const color = issue.type === 'error' ? chalk.red : chalk.yellow;
652
888
  console.log(color(` ${icon} ${issue.message}`));