* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import process from 'node:process';
import fs from 'node:fs/promises';
import {
buildCrashReport,
buildNextActionText,
formatCrashReportText,
} from './shared/jscrash-parse.mjs';
import { printKv, toErrorMessage } from './shared/utils.mjs';
function parseArgs(argv) {
const map = new Map();
const flags = new Set();
const optionalKeys = new Set(['bundle-name', 'process-hint', 'device', 'source']);
for (let i = 0; i < argv.length; i += 1) {
const t = argv[i];
if (t === '--include-text') {
flags.add('include-text');
continue;
}
if (!t.startsWith('--')) {
continue;
}
const key = t.slice(2);
const val = argv[i + 1];
if (!val || val.startsWith('--')) {
if (optionalKeys.has(key)) {
map.set(key, '');
continue;
}
throw new Error(`Missing value for --${key}`);
}
map.set(key, val);
i += 1;
}
const logFile = map.get('log-file');
const logText = map.get('log-text');
if ((logFile && logText) || (!logFile && !logText)) {
throw new Error('Provide exactly one of --log-file or --log-text');
}
return {
logFile,
logText,
bundleName: map.get('bundle-name') ?? '',
processHint: map.get('process-hint') ?? '',
source: map.get('source') ?? (logFile ? 'file' : 'text'),
device: map.get('device') ?? 'default',
includeText: flags.has('include-text'),
};
}
async function readLogText(logFile) {
try {
return await fs.readFile(logFile, 'utf8');
} catch {
throw new Error(`log file not found: ${logFile}`);
}
}
async function main() {
try {
const args = parseArgs(process.argv.slice(2));
let raw = '';
if (args.logFile) {
raw = await readLogText(args.logFile);
} else if (args.logText) {
raw = args.logText;
}
const report = buildCrashReport(raw, args.source, args.device, args.bundleName, args.processHint);
const nextAction = buildNextActionText(report);
const topStackJoined = report.topStack.join('|');
const keywordsJoined = report.keywords.join(',');
printKv({
status: report.status === 'detected' ? 'detected' : 'no_crash_signature',
source: args.source,
error_type: report.errorType,
error_message: report.errorMessage,
suspected_file: report.suspectedFile,
top_stack: topStackJoined,
keywords: keywordsJoined,
next_action: nextAction,
});
if (args.includeText) {
console.log('');
console.log(formatCrashReportText(report));
}
process.exit(0);
} catch (err) {
printKv({
status: 'parse_failed',
source: 'text',
error_type: '',
error_message: '',
suspected_file: '',
top_stack: '',
keywords: '',
next_action: toErrorMessage(err),
});
process.exit(1);
}
}
await main();