@@ -3,6 +3,13 @@ import { program } from 'commander';
33import { prettyPrintTree } from './prettyPrint' ;
44import { getProcessor } from '../core/analyze' ;
55import { ProcessorOptions } from '../core/baseProcessor' ;
6+ import {
7+ exportHistoryToBaton ,
8+ readGrid3History ,
9+ readSnapUsage ,
10+ } from '../utilities/analytics/history' ;
11+ import { ComparisonAnalyzer , MetricsCalculator } from '../utilities/analytics' ;
12+ import { CellScanningOrder , ScanningSelectionMethod } from '../types/aac' ;
613import path from 'path' ;
714import fs from 'fs' ;
815
@@ -373,6 +380,218 @@ program
373380 }
374381 ) ;
375382
383+ program
384+ . command ( 'history <input>' )
385+ . option ( '--format <format>' , 'Output format: raw or baton' , 'raw' )
386+ . option ( '--out <path>' , 'Write output to a file instead of stdout' )
387+ . option ( '--source <source>' , 'History source: auto, grid3, snap' , 'auto' )
388+ . option ( '--anonymous-uuid <uuid>' , 'Anonymous UUID for baton export' )
389+ . option ( '--export-date <iso>' , 'Export date for baton export (ISO string)' )
390+ . option ( '--encryption <mode>' , 'Encryption label for baton export' , 'none' )
391+ . option ( '--version <version>' , 'Baton export version' , '1.0' )
392+ . action (
393+ (
394+ input : string ,
395+ options : {
396+ format ?: string ;
397+ out ?: string ;
398+ source ?: string ;
399+ anonymousUuid ?: string ;
400+ exportDate ?: string ;
401+ encryption ?: string ;
402+ version ?: string ;
403+ }
404+ ) => {
405+ try {
406+ if ( ! fs . existsSync ( input ) ) {
407+ throw new Error ( `File not found: ${ input } ` ) ;
408+ }
409+
410+ const normalizedSource = ( options . source || 'auto' ) . toLowerCase ( ) ;
411+ const ext = path . extname ( input ) . toLowerCase ( ) ;
412+ const isGrid3Db =
413+ ext === '.sqlite' || path . basename ( input ) . toLowerCase ( ) === 'history.sqlite' ;
414+ const isSnap = ext === '.sps' || ext === '.spb' ;
415+
416+ let entries ;
417+ if ( normalizedSource === 'grid3' || ( normalizedSource === 'auto' && isGrid3Db ) ) {
418+ entries = readGrid3History ( input ) ;
419+ } else if ( normalizedSource === 'snap' || ( normalizedSource === 'auto' && isSnap ) ) {
420+ entries = readSnapUsage ( input ) ;
421+ } else {
422+ throw new Error ( 'Unable to detect history source. Use --source grid3 or --source snap.' ) ;
423+ }
424+
425+ const format = ( options . format || 'raw' ) . toLowerCase ( ) ;
426+ let payload : unknown = entries ;
427+
428+ if ( format === 'baton' ) {
429+ payload = exportHistoryToBaton ( entries , {
430+ version : options . version ,
431+ exportDate : options . exportDate ,
432+ encryption : options . encryption ,
433+ anonymousUUID : options . anonymousUuid ,
434+ } ) ;
435+ } else if ( format !== 'raw' ) {
436+ throw new Error ( `Unsupported format: ${ format } ` ) ;
437+ }
438+
439+ const output = JSON . stringify ( payload , null , 2 ) ;
440+ if ( options . out ) {
441+ fs . writeFileSync ( options . out , output ) ;
442+ } else {
443+ console . log ( output ) ;
444+ }
445+ } catch ( error ) {
446+ console . error (
447+ 'Error exporting history:' ,
448+ error instanceof Error ? error . message : String ( error )
449+ ) ;
450+ process . exit ( 1 ) ;
451+ }
452+ }
453+ ) ;
454+
455+ program
456+ . command ( 'metrics <file>' )
457+ . option ( '--format <format>' , 'Format type (auto-detected if not specified)' )
458+ . option ( '--pretty' , 'Pretty print JSON output' )
459+ . option ( '--out <path>' , 'Write output to a file instead of stdout' )
460+ . option ( '--preserve-all-buttons' , 'Preserve all buttons including navigation/system buttons' )
461+ . option ( '--no-exclude-navigation' , "Don't exclude navigation buttons (Home, Back)" )
462+ . option ( '--no-exclude-system' , "Don't exclude system buttons (Delete, Clear, etc.)" )
463+ . option ( '--exclude-buttons <list>' , 'Comma-separated list of button labels/terms to exclude' )
464+ . option ( '--gridset-password <password>' , 'Password for encrypted Grid3 archives (.gridsetx)' )
465+ . option ( '--access-method <method>' , 'direct or scanning' , 'direct' )
466+ . option ( '--scanning-pattern <pattern>' , 'linear, row-column, or block' , 'row-column' )
467+ . option (
468+ '--selection-method <method>' ,
469+ 'auto-1-switch, step-1-switch, or step-2-switch' ,
470+ 'auto-1-switch'
471+ )
472+ . option ( '--error-correction' , 'Enable scanning error correction' , false )
473+ . option ( '--use-prediction' , 'Enable prediction in CARE scoring' , false )
474+ . option ( '--no-smart-grammar' , 'Disable smart grammar word forms' )
475+ . option ( '--care' , 'Include CARE comparison output' , false )
476+ . action (
477+ async (
478+ file : string ,
479+ options : {
480+ format ?: string ;
481+ pretty ?: boolean ;
482+ out ?: string ;
483+ preserveAllButtons ?: boolean ;
484+ excludeNavigation ?: boolean ;
485+ excludeSystem ?: boolean ;
486+ excludeButtons ?: string ;
487+ gridsetPassword ?: string ;
488+ accessMethod ?: string ;
489+ scanningPattern ?: string ;
490+ selectionMethod ?: string ;
491+ errorCorrection ?: boolean ;
492+ usePrediction ?: boolean ;
493+ smartGrammar ?: boolean ;
494+ care ?: boolean ;
495+ }
496+ ) => {
497+ try {
498+ const filteringOptions = parseFilteringOptions ( options ) ;
499+ const format = options . format || detectFormat ( file ) ;
500+ const processor = getProcessor ( format , filteringOptions ) ;
501+ const tree = await processor . loadIntoTree ( file ) ;
502+
503+ const accessMethod = ( options . accessMethod || 'direct' ) . toLowerCase ( ) ;
504+ const scanningPattern = ( options . scanningPattern || 'row-column' ) . toLowerCase ( ) ;
505+ const selectionMethodParam = ( options . selectionMethod || 'auto-1-switch' ) . toLowerCase ( ) ;
506+ const errorCorrection = ! ! options . errorCorrection ;
507+
508+ let scanningConfig = undefined ;
509+ if ( accessMethod === 'scanning' ) {
510+ let cellScanningOrder = CellScanningOrder . SimpleScan ;
511+ let blockScanEnabled = false ;
512+
513+ switch ( scanningPattern ) {
514+ case 'linear' :
515+ cellScanningOrder = CellScanningOrder . SimpleScan ;
516+ break ;
517+ case 'row-column' :
518+ cellScanningOrder = CellScanningOrder . RowColumnScan ;
519+ break ;
520+ case 'block' :
521+ cellScanningOrder = CellScanningOrder . RowColumnScan ;
522+ blockScanEnabled = true ;
523+ break ;
524+ default :
525+ throw new Error ( `Unsupported scanning pattern: ${ scanningPattern } ` ) ;
526+ }
527+
528+ let selectionMethod = ScanningSelectionMethod . AutoScan ;
529+ switch ( selectionMethodParam ) {
530+ case 'auto-1-switch' :
531+ selectionMethod = ScanningSelectionMethod . AutoScan ;
532+ break ;
533+ case 'step-1-switch' :
534+ selectionMethod = ScanningSelectionMethod . StepScan1Switch ;
535+ break ;
536+ case 'step-2-switch' :
537+ selectionMethod = ScanningSelectionMethod . StepScan2Switch ;
538+ break ;
539+ default :
540+ throw new Error ( `Unsupported selection method: ${ selectionMethodParam } ` ) ;
541+ }
542+
543+ scanningConfig = {
544+ cellScanningOrder,
545+ blockScanEnabled,
546+ selectionMethod,
547+ errorCorrectionEnabled : errorCorrection ,
548+ errorRate : errorCorrection ? 0.1 : undefined ,
549+ } ;
550+ }
551+
552+ const calculator = new MetricsCalculator ( ) ;
553+ const metrics = calculator . analyze ( tree , {
554+ scanningConfig,
555+ useSmartGrammar : options . smartGrammar ,
556+ } ) ;
557+
558+ let care = undefined ;
559+ if ( options . care ) {
560+ const comparison = new ComparisonAnalyzer ( ) ;
561+ care = comparison . compare ( metrics , metrics , {
562+ includeSentences : true ,
563+ usePrediction : ! ! options . usePrediction ,
564+ scanningConfig,
565+ } ) ;
566+ }
567+
568+ const result = {
569+ format,
570+ filtering : filteringOptions ,
571+ accessMethod,
572+ scanningPattern,
573+ selectionMethod : selectionMethodParam ,
574+ errorCorrection,
575+ metrics,
576+ care,
577+ } ;
578+
579+ const output = options . pretty ? JSON . stringify ( result , null , 2 ) : JSON . stringify ( result ) ;
580+ if ( options . out ) {
581+ fs . writeFileSync ( options . out , output ) ;
582+ } else {
583+ console . log ( output ) ;
584+ }
585+ } catch ( error ) {
586+ console . error (
587+ 'Error calculating metrics:' ,
588+ error instanceof Error ? error . message : String ( error )
589+ ) ;
590+ process . exit ( 1 ) ;
591+ }
592+ }
593+ ) ;
594+
376595// Show help if no command provided
377596if ( process . argv . length <= 2 ) {
378597 program . help ( ) ;
0 commit comments