11cb0ef41Sopenharmony_ci#!/usr/bin/env node
21cb0ef41Sopenharmony_ci
31cb0ef41Sopenharmony_ciimport { execSync, spawn } from 'node:child_process';
41cb0ef41Sopenharmony_ciimport { promises as fs, readdirSync, statSync } from 'node:fs';
51cb0ef41Sopenharmony_ciimport { extname, join, relative, resolve } from 'node:path';
61cb0ef41Sopenharmony_ciimport process from 'node:process';
71cb0ef41Sopenharmony_ci
81cb0ef41Sopenharmony_ciconst FIX_MODE_ENABLED = process.argv.includes('--fix');
91cb0ef41Sopenharmony_ciconst USE_NPX = process.argv.includes('--from-npx');
101cb0ef41Sopenharmony_ci
111cb0ef41Sopenharmony_ciconst SHELLCHECK_EXE_NAME = 'shellcheck';
121cb0ef41Sopenharmony_ciconst SHELLCHECK_OPTIONS = ['--shell=sh', '--severity=info', '--enable=all'];
131cb0ef41Sopenharmony_ciif (FIX_MODE_ENABLED) SHELLCHECK_OPTIONS.push('--format=diff');
141cb0ef41Sopenharmony_cielse if (process.env.GITHUB_ACTIONS) SHELLCHECK_OPTIONS.push('--format=json');
151cb0ef41Sopenharmony_ci
161cb0ef41Sopenharmony_ciconst SPAWN_OPTIONS = {
171cb0ef41Sopenharmony_ci  cwd: null,
181cb0ef41Sopenharmony_ci  shell: false,
191cb0ef41Sopenharmony_ci  stdio: ['pipe', 'pipe', 'inherit'],
201cb0ef41Sopenharmony_ci};
211cb0ef41Sopenharmony_ci
221cb0ef41Sopenharmony_cifunction* findScriptFilesRecursively(dirPath) {
231cb0ef41Sopenharmony_ci  const entries = readdirSync(dirPath, { withFileTypes: true });
241cb0ef41Sopenharmony_ci
251cb0ef41Sopenharmony_ci  for (const entry of entries) {
261cb0ef41Sopenharmony_ci    const path = join(dirPath, entry.name);
271cb0ef41Sopenharmony_ci
281cb0ef41Sopenharmony_ci    if (
291cb0ef41Sopenharmony_ci      entry.isDirectory() &&
301cb0ef41Sopenharmony_ci      entry.name !== 'build' &&
311cb0ef41Sopenharmony_ci      entry.name !== 'changelogs' &&
321cb0ef41Sopenharmony_ci      entry.name !== 'deps' &&
331cb0ef41Sopenharmony_ci      entry.name !== 'fixtures' &&
341cb0ef41Sopenharmony_ci      entry.name !== 'gyp' &&
351cb0ef41Sopenharmony_ci      entry.name !== 'inspector_protocol' &&
361cb0ef41Sopenharmony_ci      entry.name !== 'node_modules' &&
371cb0ef41Sopenharmony_ci      entry.name !== 'out' &&
381cb0ef41Sopenharmony_ci      entry.name !== 'tmp'
391cb0ef41Sopenharmony_ci    ) {
401cb0ef41Sopenharmony_ci      yield* findScriptFilesRecursively(path);
411cb0ef41Sopenharmony_ci    } else if (entry.isFile() && extname(entry.name) === '.sh') {
421cb0ef41Sopenharmony_ci      yield path;
431cb0ef41Sopenharmony_ci    }
441cb0ef41Sopenharmony_ci  }
451cb0ef41Sopenharmony_ci}
461cb0ef41Sopenharmony_ci
471cb0ef41Sopenharmony_ciconst expectedHashBang = Buffer.from('#!/bin/sh\n');
481cb0ef41Sopenharmony_ciasync function hasInvalidHashBang(fd) {
491cb0ef41Sopenharmony_ci  const { length } = expectedHashBang;
501cb0ef41Sopenharmony_ci
511cb0ef41Sopenharmony_ci  const actual = Buffer.allocUnsafe(length);
521cb0ef41Sopenharmony_ci  await fd.read(actual, 0, length, 0);
531cb0ef41Sopenharmony_ci
541cb0ef41Sopenharmony_ci  return Buffer.compare(actual, expectedHashBang);
551cb0ef41Sopenharmony_ci}
561cb0ef41Sopenharmony_ci
571cb0ef41Sopenharmony_ciasync function checkFiles(...files) {
581cb0ef41Sopenharmony_ci  const flags = FIX_MODE_ENABLED ? 'r+' : 'r';
591cb0ef41Sopenharmony_ci  await Promise.all(
601cb0ef41Sopenharmony_ci    files.map(async (file) => {
611cb0ef41Sopenharmony_ci      const fd = await fs.open(file, flags);
621cb0ef41Sopenharmony_ci      if (await hasInvalidHashBang(fd)) {
631cb0ef41Sopenharmony_ci        if (FIX_MODE_ENABLED) {
641cb0ef41Sopenharmony_ci          const file = await fd.readFile();
651cb0ef41Sopenharmony_ci
661cb0ef41Sopenharmony_ci          const fileContent =
671cb0ef41Sopenharmony_ci            file[0] === '#'.charCodeAt() ?
681cb0ef41Sopenharmony_ci              file.subarray(file.indexOf('\n') + 1) :
691cb0ef41Sopenharmony_ci              file;
701cb0ef41Sopenharmony_ci
711cb0ef41Sopenharmony_ci          const toWrite = Buffer.concat([expectedHashBang, fileContent]);
721cb0ef41Sopenharmony_ci          await fd.truncate(toWrite.length);
731cb0ef41Sopenharmony_ci          await fd.write(toWrite, 0, toWrite.length, 0);
741cb0ef41Sopenharmony_ci        } else {
751cb0ef41Sopenharmony_ci          if (!process.exitCode) process.exitCode = 1;
761cb0ef41Sopenharmony_ci          console.error(
771cb0ef41Sopenharmony_ci            (process.env.GITHUB_ACTIONS ?
781cb0ef41Sopenharmony_ci              `::error file=${file},line=1,col=1::` :
791cb0ef41Sopenharmony_ci              'Fixable with --fix: ') +
801cb0ef41Sopenharmony_ci              `Invalid hashbang for ${file} (expected /bin/sh).`,
811cb0ef41Sopenharmony_ci          );
821cb0ef41Sopenharmony_ci        }
831cb0ef41Sopenharmony_ci      }
841cb0ef41Sopenharmony_ci      await fd.close();
851cb0ef41Sopenharmony_ci    }),
861cb0ef41Sopenharmony_ci  );
871cb0ef41Sopenharmony_ci
881cb0ef41Sopenharmony_ci  const stdout = await new Promise((resolve, reject) => {
891cb0ef41Sopenharmony_ci    const SHELLCHECK_EXE =
901cb0ef41Sopenharmony_ci      process.env.SHELLCHECK ||
911cb0ef41Sopenharmony_ci      execSync('command -v ' + (USE_NPX ? 'npx' : SHELLCHECK_EXE_NAME))
921cb0ef41Sopenharmony_ci        .toString()
931cb0ef41Sopenharmony_ci        .trim();
941cb0ef41Sopenharmony_ci    const NPX_OPTIONS = USE_NPX ? [SHELLCHECK_EXE_NAME] : [];
951cb0ef41Sopenharmony_ci
961cb0ef41Sopenharmony_ci    const shellcheck = spawn(
971cb0ef41Sopenharmony_ci      SHELLCHECK_EXE,
981cb0ef41Sopenharmony_ci      [
991cb0ef41Sopenharmony_ci        ...NPX_OPTIONS,
1001cb0ef41Sopenharmony_ci        ...SHELLCHECK_OPTIONS,
1011cb0ef41Sopenharmony_ci        ...(FIX_MODE_ENABLED ?
1021cb0ef41Sopenharmony_ci          files.map((filePath) => relative(SPAWN_OPTIONS.cwd, filePath)) :
1031cb0ef41Sopenharmony_ci          files),
1041cb0ef41Sopenharmony_ci      ],
1051cb0ef41Sopenharmony_ci      SPAWN_OPTIONS,
1061cb0ef41Sopenharmony_ci    );
1071cb0ef41Sopenharmony_ci    shellcheck.once('error', reject);
1081cb0ef41Sopenharmony_ci
1091cb0ef41Sopenharmony_ci    let json = '';
1101cb0ef41Sopenharmony_ci    let childProcess = shellcheck;
1111cb0ef41Sopenharmony_ci    if (FIX_MODE_ENABLED) {
1121cb0ef41Sopenharmony_ci      const GIT_EXE =
1131cb0ef41Sopenharmony_ci        process.env.GIT || execSync('command -v git').toString().trim();
1141cb0ef41Sopenharmony_ci
1151cb0ef41Sopenharmony_ci      const gitApply = spawn(GIT_EXE, ['apply'], SPAWN_OPTIONS);
1161cb0ef41Sopenharmony_ci      shellcheck.stdout.pipe(gitApply.stdin);
1171cb0ef41Sopenharmony_ci      shellcheck.once('exit', (code) => {
1181cb0ef41Sopenharmony_ci        if (!process.exitCode && code) process.exitCode = code;
1191cb0ef41Sopenharmony_ci      });
1201cb0ef41Sopenharmony_ci      gitApply.stdout.pipe(process.stdout);
1211cb0ef41Sopenharmony_ci
1221cb0ef41Sopenharmony_ci      gitApply.once('error', reject);
1231cb0ef41Sopenharmony_ci      childProcess = gitApply;
1241cb0ef41Sopenharmony_ci    } else if (process.env.GITHUB_ACTIONS) {
1251cb0ef41Sopenharmony_ci      shellcheck.stdout.on('data', (chunk) => {
1261cb0ef41Sopenharmony_ci        json += chunk;
1271cb0ef41Sopenharmony_ci      });
1281cb0ef41Sopenharmony_ci    } else {
1291cb0ef41Sopenharmony_ci      shellcheck.stdout.pipe(process.stdout);
1301cb0ef41Sopenharmony_ci    }
1311cb0ef41Sopenharmony_ci    childProcess.once('exit', (code) => {
1321cb0ef41Sopenharmony_ci      if (!process.exitCode && code) process.exitCode = code;
1331cb0ef41Sopenharmony_ci      resolve(json);
1341cb0ef41Sopenharmony_ci    });
1351cb0ef41Sopenharmony_ci  });
1361cb0ef41Sopenharmony_ci
1371cb0ef41Sopenharmony_ci  if (!FIX_MODE_ENABLED && process.env.GITHUB_ACTIONS) {
1381cb0ef41Sopenharmony_ci    const data = JSON.parse(stdout);
1391cb0ef41Sopenharmony_ci    for (const { file, line, column, message } of data) {
1401cb0ef41Sopenharmony_ci      console.error(
1411cb0ef41Sopenharmony_ci        `::error file=${file},line=${line},col=${column}::${file}:${line}:${column}: ${message}`,
1421cb0ef41Sopenharmony_ci      );
1431cb0ef41Sopenharmony_ci    }
1441cb0ef41Sopenharmony_ci  }
1451cb0ef41Sopenharmony_ci}
1461cb0ef41Sopenharmony_ci
1471cb0ef41Sopenharmony_ciconst USAGE_STR =
1481cb0ef41Sopenharmony_ci  `Usage: ${process.argv[1]} <path> [--fix] [--from-npx]\n` +
1491cb0ef41Sopenharmony_ci  '\n' +
1501cb0ef41Sopenharmony_ci  'Environment variables:\n' +
1511cb0ef41Sopenharmony_ci  ' - SHELLCHECK: absolute path to `shellcheck`. If not provided, the\n' +
1521cb0ef41Sopenharmony_ci  '   script will use the result of `command -v shellcheck`, or\n' +
1531cb0ef41Sopenharmony_ci  '   `$(command -v npx) shellcheck` if the flag `--from-npx` is provided\n' +
1541cb0ef41Sopenharmony_ci  '   (may require an internet connection).\n' +
1551cb0ef41Sopenharmony_ci  ' - GIT: absolute path to `git`. If not provided, the \n' +
1561cb0ef41Sopenharmony_ci  '   script will use the result of `command -v git`.\n';
1571cb0ef41Sopenharmony_ci
1581cb0ef41Sopenharmony_ciif (
1591cb0ef41Sopenharmony_ci  process.argv.length < 3 ||
1601cb0ef41Sopenharmony_ci  process.argv.includes('-h') ||
1611cb0ef41Sopenharmony_ci  process.argv.includes('--help')
1621cb0ef41Sopenharmony_ci) {
1631cb0ef41Sopenharmony_ci  console.log(USAGE_STR);
1641cb0ef41Sopenharmony_ci} else {
1651cb0ef41Sopenharmony_ci  console.log('Running Shell scripts checker...');
1661cb0ef41Sopenharmony_ci  const entryPoint = resolve(process.argv[2]);
1671cb0ef41Sopenharmony_ci  const stats = statSync(entryPoint, { throwIfNoEntry: false });
1681cb0ef41Sopenharmony_ci
1691cb0ef41Sopenharmony_ci  const onError = (e) => {
1701cb0ef41Sopenharmony_ci    console.log(USAGE_STR);
1711cb0ef41Sopenharmony_ci    console.error(e);
1721cb0ef41Sopenharmony_ci    process.exitCode = 1;
1731cb0ef41Sopenharmony_ci  };
1741cb0ef41Sopenharmony_ci  if (stats?.isDirectory()) {
1751cb0ef41Sopenharmony_ci    SPAWN_OPTIONS.cwd = entryPoint;
1761cb0ef41Sopenharmony_ci    checkFiles(...findScriptFilesRecursively(entryPoint)).catch(onError);
1771cb0ef41Sopenharmony_ci  } else if (stats?.isFile()) {
1781cb0ef41Sopenharmony_ci    SPAWN_OPTIONS.cwd = process.cwd();
1791cb0ef41Sopenharmony_ci    checkFiles(entryPoint).catch(onError);
1801cb0ef41Sopenharmony_ci  } else {
1811cb0ef41Sopenharmony_ci    onError(new Error('You must provide a valid directory or file path. ' +
1821cb0ef41Sopenharmony_ci                      `Received '${process.argv[2]}'.`));
1831cb0ef41Sopenharmony_ci  }
1841cb0ef41Sopenharmony_ci}
185