Skip to content

Commit 6b45d38

Browse files
committed
fix cli
1 parent 4ee3059 commit 6b45d38

2 files changed

Lines changed: 310 additions & 0 deletions

File tree

src/cli/index.ts

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import { program } from 'commander';
33
import { prettyPrintTree } from './prettyPrint';
44
import { getProcessor } from '../core/analyze';
55
import { 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';
613
import path from 'path';
714
import 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
377596
if (process.argv.length <= 2) {
378597
program.help();

src/utilities/analytics/history.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,97 @@ export interface HistoryEntry {
5555

5656
export { dotNetTicksToDate };
5757

58+
export interface BatonExportMetadata {
59+
timestamp: string;
60+
latitude?: number | null;
61+
longitude?: number | null;
62+
}
63+
64+
export interface BatonExportSentence {
65+
uuid: string;
66+
anonymousUUID: string;
67+
content: string;
68+
metadata: BatonExportMetadata[];
69+
source: HistorySource;
70+
}
71+
72+
export interface BatonExport {
73+
version: string;
74+
exportDate: string;
75+
encryption: string;
76+
sentenceCount: number;
77+
sentences: BatonExportSentence[];
78+
}
79+
80+
const generateUuid = (): string => {
81+
if (typeof globalThis.crypto?.randomUUID === 'function') {
82+
return globalThis.crypto.randomUUID();
83+
}
84+
// RFC4122-ish fallback for Node without crypto.randomUUID
85+
const hex = '0123456789abcdef';
86+
const bytes = Array.from({ length: 16 }, () => Math.floor(Math.random() * 256));
87+
bytes[6] = (bytes[6] & 0x0f) | 0x40;
88+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
89+
const toHex = (b: number): string => hex[(b >> 4) & 0x0f] + hex[b & 0x0f];
90+
return (
91+
toHex(bytes[0]) +
92+
toHex(bytes[1]) +
93+
toHex(bytes[2]) +
94+
toHex(bytes[3]) +
95+
'-' +
96+
toHex(bytes[4]) +
97+
toHex(bytes[5]) +
98+
'-' +
99+
toHex(bytes[6]) +
100+
toHex(bytes[7]) +
101+
'-' +
102+
toHex(bytes[8]) +
103+
toHex(bytes[9]) +
104+
'-' +
105+
toHex(bytes[10]) +
106+
toHex(bytes[11]) +
107+
toHex(bytes[12]) +
108+
toHex(bytes[13]) +
109+
toHex(bytes[14]) +
110+
toHex(bytes[15])
111+
);
112+
};
113+
114+
export function exportHistoryToBaton(
115+
entries: HistoryEntry[],
116+
options?: {
117+
version?: string;
118+
exportDate?: string | Date;
119+
encryption?: string;
120+
anonymousUUID?: string;
121+
}
122+
): BatonExport {
123+
const exportDate =
124+
options?.exportDate instanceof Date
125+
? options.exportDate.toISOString()
126+
: options?.exportDate || new Date().toISOString();
127+
const anonymousUUID = options?.anonymousUUID || generateUuid();
128+
const sentences = entries.map((entry) => ({
129+
uuid: generateUuid(),
130+
anonymousUUID,
131+
content: entry.content,
132+
metadata: entry.occurrences.map((occ) => ({
133+
timestamp: occ.timestamp.toISOString(),
134+
latitude: occ.latitude ?? null,
135+
longitude: occ.longitude ?? null,
136+
})),
137+
source: entry.source,
138+
}));
139+
140+
return {
141+
version: options?.version || '1.0',
142+
exportDate,
143+
encryption: options?.encryption || 'none',
144+
sentenceCount: sentences.length,
145+
sentences,
146+
};
147+
}
148+
58149
/**
59150
* Read Grid 3 phrase history from a history.sqlite database and tag entries with their source.
60151
*/

0 commit comments

Comments
 (0)