UNPKG

@haxtheweb/create

Version:

CLI for all things HAX the web

1,039 lines 53.8 kB
#!/usr/bin/env node
// forces middleware into CLI mode so we don't automatically perform certain operations like pathing context
"use strict";

var fs = _interopRequireWildcard(require("node:fs"));
var path = _interopRequireWildcard(require("node:path"));
var p = _interopRequireWildcard(require("@clack/prompts"));
var _picocolors = _interopRequireDefault(require("picocolors"));
var _statements = require("./lib/statements.js");
var _logging = require("./lib/logging.js");
var _audit = require("./lib/programs/audit.js");
var _party = require("./lib/programs/party.js");
var _webcomponent = require("./lib/programs/webcomponent.js");
var _site = require("./lib/programs/site.js");
var _skills = require("./lib/programs/skills.js");
var _utils = require("./lib/utils.js");
var haxcmsLib = _interopRequireWildcard(require("@haxtheweb/haxcms-nodejs/dist/lib/HAXCMS.js"));
var _commander = require("commander");
var _package = _interopRequireDefault(require("../package.json"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
process.env.haxcms_middleware = "node-cli";
const HAXCMS = haxcmsLib.HAXCMS;
const systemStructureContext = haxcmsLib.systemStructureContext;

// check the last time this was run
let lastTime = (0, _utils.readConfigFile)('hax-cli-last-run');
// first run in the event it is null assume now
if (!lastTime) {
  lastTime = new Date().toISOString();
}
// check time difference broken down by unit
const timeSince = (0, _utils.getTimeDifference)(new Date().toISOString(), lastTime);
// write last run so that we know last time they were here
(0, _utils.writeConfigFile)('hax-cli-last-run', new Date().toISOString());
let sysGit = true;
(0, _utils.exec)('which git', error => {
  if (error) {
    sysGit = false;
  }
});
async function main() {
  let wcReg = {};
  let regPath = path.join(__dirname, './lib/wc-registry.json');
  if (fs.existsSync(regPath)) {
    try {
      wcReg = JSON.parse(fs.readFileSync(regPath));
    } catch (e) {
      // no registry for testing, probably busted package
    }
  }
  var commandRun = {};
  _commander.program.option('--v', 'Verbose output').option('--debug', 'Output for developers').option('--format <char>', 'Output format; json (default), yaml').option('--path <char>', 'where to perform operation').option('--name <char>', 'name of the project/web component').option('--npm-client <char>', 'npm client to use (must be installed) npm, yarn, pnpm', 'npm').option('--y', 'yes to all questions').option('--skip', 'skip frills like animations').option('--quiet', 'remove console logging').option('--auto', 'yes to all questions, alias of y').option('--no-i', 'prevent interactions / sub-process, good for scripting').option('--to-file <char>', 'redirect command output to a file').option('--no-extras', 'skip all extra / automatic command processing').option('--root <char>', 'root location to execute the command from')

  // options for webcomponent
  .option('--org <char>', 'organization for package.json').option('--author <char>', 'author for site / package.json').option('--template <char>', 'template type to use (compliant, minimal, training, monorepo)').option('--writeHaxProperties', 'Write haxProperties for the element').option('--force', 'force creation even if name exists in registry')

  // options for site
  .option('--import-site <char>', 'URL of site to import').option('--import-structure <char>', `import method to use:\n\rpressbooksToSite\n\relmslnToSite\n\rhaxcmsToSite\n\rnotionToSite\n\rgitbookToSite\n\revolutionToSite\n\rploneToSite\n\rwordpressPagesToSite\n\rdrupalBookToSite\n\rhtmlToSite\n\rdocxToSite`).option('--node-op <char>', 'node operation to perform').option('--item-id <char>', 'node ID to operate on').option('--domain <char>', 'published domain name').option('--title-scrape <char>', 'CSS Selector for `title` in resource').option('--content-scrape <char>', 'CSS Selector for `body` in resource').option('--items-import <char>', 'import items from a file / site').option('--recipe <char>', 'path to recipe file').option('--custom-theme-name <char>', 'custom theme name').option('--custom-theme-template <char>', 'custom theme template; (options: base, polaris-flex, polaris-sidebar)').option('--skeleton-file <char>', 'path to skeleton JSON file').option('--skeleton-machine-name <char>', 'skeleton machine name (installed template) to create a site from').option('--search <char>', 'search query text or simple selector (for site:search)').option('--search-field <char>', 'comma-separated fields for text search (title,slug,description,tags,content,all)').option('--search-case-sensitive', 'case-sensitive text search').option('--search-limit <char>', 'max number of site search matches to return').option('--search-selector', 'treat --search as a selector query (tag, tag[attr], tag[attr=\"value\"], [attr])').option('--search-mode <char>', 'search mode override (text or selector)')

  // options for rsync
  .option('--source <char>', 'rsync source directory or remote path').option('--destination <char>', 'rsync destination directory or remote path').option('--exclude <char>', 'comma-separated patterns to exclude from rsync').option('--dry-run', 'perform rsync dry run').option('--delete', 'delete extraneous files from destination')

  // options for party
  .option('--repos <char...>', 'repositories to clone')

  // options for new site commands
  .option('--export-format <char>', 'export format for site:export (pdf, docx, epub, html, zip, markdown, skeleton)').option('--revision-id <char>', 'revision ID for site:revisions').option('--restore', 'restore a revision (used with site:revisions)').option('--target-path <char>', 'target subdirectory for files-upload').option('--file-uuid <char>', 'file UUID for files-delete').option('--file-path <char>', 'file path (alternative to UUID) for files-delete').option('--replace <char>', 'replacement text for search-replace').option('--confirm', 'confirm destructive search-replace operation').option('--destroy-confirm', 'confirm removing matched text in search-replace').version(_package.default.version).helpCommand(true);

  // default command which runs interactively
  _commander.program.command('start').description('Select which hax sub-program to run').action(() => {
    commandRun = {
      command: 'start',
      arguments: {},
      options: {}
    };
  });
  _commander.program.command('update').description('hax cli self update').option('--y', 'yes to all questions').action(() => {
    commandRun = {
      command: 'update',
      arguments: {},
      options: {}
    };
  });
  _commander.program.command('serve').description('Launch HAXsite in development mode (http://localhost)').action(() => {
    commandRun = {
      command: 'serve',
      arguments: {
        action: 'serve'
      },
      options: {}
    };
  });

  // site operations and actions
  let strActions = '';
  (0, _site.siteActions)().forEach(action => {
    strActions += `${action.value} - ${action.label}` + "\n\r";
  });
  let siteProg = _commander.program.command('site').description('create or administer a HAXsite').argument('[action]', 'Actions to perform on site include:' + "\n\r" + strActions).action(action => {
    commandRun = {
      command: 'site',
      arguments: {},
      options: {}
    };
    if (action) {
      commandRun.arguments.action = action;
      commandRun.options.skip = true;
    }
  }).option('--v', 'Verbose output').option('--debug', 'Output for developers').option('--format <char>', 'Output format; json (default), yaml').option('--path <char>', 'where to perform operation').option('--npm-client <char>', 'npm client to use (must be installed) npm, yarn, pnpm', 'npm').option('--y', 'yes to all questions').option('--skip', 'skip frills like animations').option('--quiet', 'remove console logging').option('--auto', 'yes to all questions, alias of y').option('--no-i', 'prevent interactions / sub-process, good for scripting').option('--to-file <char>', 'redirect command output to a file').option('--no-extras', 'skip all extra / automatic command processing').option('--root <char>', 'root location to execute the command from').option('--import-site <char>', 'URL of site to import').option('--import-structure <char>', `import method to use:\n\rpressbooksToSite\n\relmslnToSite\n\rhaxcmsToSite\n\rnotionToSite\n\rgitbookToSite\n\revolutionToSite\n\rploneToSite\n\rwordpressPagesToSite\n\rdrupalBookToSite\n\rhtmlToSite\n\rdocxToSite`).option('--name <char>', 'name of the site (when creating a new one)').option('--domain <char>', 'published domain name').option('--node-op <char>', 'node operation to perform').option('--title-scrape <char>', 'CSS Selector for `title` in resource').option('--content-scrape <char>', 'CSS Selector for `body` in resource').option('--items-import <char>', 'import items from a file / site').option('--recipe <char>', 'path to recipe file').option('--custom-theme-name <char>', 'custom theme name').option('--custom-theme-template <char>', 'custom theme template (options: base, polaris-flex, polaris-sidebar)').option('--skeleton-file <char>', 'path to skeleton JSON file').option('--skeleton-machine-name <char>', 'skeleton machine name (installed template) to create a site from').option('--search <char>', 'search query text or simple selector').option('--search-field <char>', 'comma-separated fields for text search (title,slug,description,tags,content,all)').option('--search-case-sensitive', 'case-sensitive text search').option('--search-limit <char>', 'max number of site search matches to return').option('--search-selector', 'treat --search as a selector query (tag, tag[attr], tag[attr=\"value\"], [attr])').option('--search-mode <char>', 'search mode override (text or selector)').option('--source <char>', 'rsync source directory or remote path').option('--destination <char>', 'rsync destination directory or remote path').option('--exclude <char>', 'comma-separated patterns to exclude from rsync').option('--dry-run', 'perform rsync dry run').option('--delete', 'delete extraneous files from destination').option('--export-format <char>', 'export format for site:export (pdf, docx, epub, html, zip, markdown, skeleton)').option('--revision-id <char>', 'revision ID for site:revisions').option('--restore', 'restore a revision (used with site:revisions)').option('--target-path <char>', 'target subdirectory for files-upload').option('--file-uuid <char>', 'file UUID for files-delete').option('--file-path <char>', 'file path (alternative to UUID) for files-delete').option('--replace <char>', 'replacement text for search-replace').option('--confirm', 'confirm destructive search-replace operation').option('--destroy-confirm', 'confirm removing matched text in search-replace').version(_package.default.version);
  let siteNodeOps = (0, _site.siteNodeOperations)();
  for (var i in siteNodeOps) {
    _commander.program.option(`--${(0, _utils.camelToDash)(siteNodeOps[i].value)} <char>`, `${siteNodeOps[i].label}`);
    siteProg.option(`--${(0, _utils.camelToDash)(siteNodeOps[i].value)} <char>`, `${siteNodeOps[i].label}`);
  }

  // webcomponent program
  let strWebcomponentActions = '';
  (0, _webcomponent.webcomponentActions)().forEach(action => {
    strWebcomponentActions += `${action.value} - ${action.label}` + "\n\r";
  });
  _commander.program.command('wc').alias('webcomponent').description('Create Lit based web components, with HAX recommendations').argument('[action]', 'Actions to perform on web component include:' + "\n\r" + strWebcomponentActions).action(action => {
    commandRun = {
      command: 'webcomponent',
      arguments: {},
      options: {}
    };
    // if name set, populate
    if (action) {
      commandRun.arguments.action = action;
      commandRun.options.skip = true;
    }
  }).option('--path <char>', 'path the project should be created in').option('--org <char>', 'organization for package.json').option('--author <char>', 'author for site / package.json').option('--name <char>', 'name of the web component').option('--writeHaxProperties', 'Write haxProperties for the element').option('--template <char>', 'template type to use (compliant, minimal, training, monorepo)').option('--to-file <char>', 'redirect command output to a file').option('--no-extras', 'skip all extra / automatic command processing').option('--no-i', 'prevent interactions / sub-process, good for scripting').option('--root <char>', 'root location to execute the command from').option('--force', 'force creation even if name exists in registry').version(_package.default.version);

  // audit program
  _commander.program.command('audit').description('Audits web components for compliance with DDD (HAX design system)').action(() => {
    commandRun = {
      command: 'audit',
      arguments: {},
      options: {}
    };
  }).option('--debug', 'Output for developers').version(_package.default.version);
  _commander.program.command('party').description('Party time! Join the HAX community and get involved!').argument('[action]', 'Actions to perform on web component include:' + "\n\r" + "test").action(action => {
    commandRun = {
      command: 'party',
      arguments: {},
      options: {}
    };
    if (action) {
      commandRun.arguments.action = action;
      commandRun.options.skip = true;
    }
  }).option('--root <char>', 'root location to execute the command from').option('--y', 'yes to all questions').option('--auto', 'yes to all questions, alias of y').option('--repos <char...>', 'repositories to clone').version(_package.default.version);

  // skills program
  let strSkillsActions = '';
  (0, _skills.skillsActions)().forEach(action => {
    strSkillsActions += `${action.value} - ${action.label}` + "\n\r";
  });
  _commander.program.command('skills').description('List and install bundled agent skills for HAX ecosystem agents').argument('[action]', 'Actions to perform:' + "\n\r" + strSkillsActions).action(action => {
    commandRun = {
      command: 'skills',
      arguments: {},
      options: {}
    };
    if (action) {
      commandRun.arguments.action = action;
      commandRun.options.skip = true;
    }
  }).option('--skill-name <char>', 'name of the skill to install (or "all")').option('--path <char>', 'target directory for skill installation (default: ~/.agents/skills/)').option('--y', 'yes to all questions').option('--auto', 'yes to all questions, alias of y').option('--no-i', 'prevent interactions / sub-process, good for scripting').option('--format <char>', 'Output format; json (default), yaml').option('--quiet', 'remove console logging').version(_package.default.version);

  // process program arguments
  _commander.program.parse();
  commandRun.options = {
    ...commandRun.options,
    ..._commander.program.opts()
  };
  // this is a false positive bc of the no-extras flag. no-extras does not imply true if not there
  // but that's how the CLI works. This then bombs things downstream. Its good to be false but NOT
  // good to be true since we need an array of options
  if (commandRun.options.extras === true) {
    delete commandRun.options.extras;
  }
  // Security (M-2): validate the CLI-provided --npm-client early, before the
  // packageData loop below can overwrite it. The loop unconditionally resets
  // npmClient from the local package.json (or 'npm'), so a malicious CLI
  // value would be clobbered before the post-loop check — validate it here
  // while it still reflects the user's --npm-client argument.
  if (commandRun.options.npmClient) {
    try {
      commandRun.options.npmClient = (0, _utils.validateNpmClient)(commandRun.options.npmClient);
    } catch (e) {
      _commander.program.error(_picocolors.default.red(e.message));
    }
  }
  // Security (M-1/L-5): validate --root before chdir so a missing/invalid
  // root fails with a clear message instead of a process.chdir exception.
  // --root is not shell-interpolated (recipe replay now uses spawn args), but
  // the metacharacter check is kept as defense-in-depth.
  if (commandRun.options.root) {
    try {
      (0, _utils.rejectShellMetacharacters)(commandRun.options.root, 'root');
      if (!fs.existsSync(commandRun.options.root) || !fs.statSync(commandRun.options.root).isDirectory()) {
        _commander.program.error(_picocolors.default.red(`Invalid --root: ${commandRun.options.root} is not an existing directory.`));
      }
    } catch (e) {
      _commander.program.error(_picocolors.default.red(e.message));
    }
  }
  // allow execution of the command from a different location
  if (commandRun.options.root) {
    process.chdir(commandRun.options.root);
  }
  // bridge to log so we can respect this setting
  if (commandRun.options.quiet) {
    process.haxquiet = true;
    // don't output to the console any messages on this run since told to be silent
    // logging still happens to log files
    _logging.logger.remove(_logging.consoleTransport);
  }
  if (commandRun.options.debug) {
    (0, _logging.log)(commandRun, 'debug');
  }
  // auto and y assume same thing
  if (commandRun.options.y || commandRun.options.auto) {
    commandRun.options.y = true;
    commandRun.options.auto = true;
  }
  let author = '';
  // should be able to grab if not predefined
  try {
    let value = await (0, _utils.exec)(`git config user.name`);
    author = value.stdout.trim();
  } catch (e) {
    (0, _logging.log)(`
      git user name not configured. Run the following to do this:\n
      git config --global user.name "namehere"\n
      git config --global user.email "email@here`, 'debug');
  }
  // skills install manages its own default (~/.agents/skills/) in skills.js,
  // so don't let the action-triggered cwd default override it
  if (!commandRun.options.path && commandRun.options.skip && commandRun.command !== 'skills') {
    commandRun.options.path = process.cwd();
  }
  // if we skip stuff then set org/author automatically
  if (commandRun.options.skip || commandRun.options.auto) {
    if (!commandRun.options.org) {
      commandRun.options.org = '';
    }
    if (!commandRun.options.author) {
      commandRun.options.author = author;
    }
  }
  // validate theme cli commands
  if (commandRun.options.theme !== 'custom-theme' && (commandRun.options.customThemeName || commandRun.options.customThemeTemplate)) {
    _commander.program.error(_picocolors.default.red('ERROR: You can only use the --custom-theme-name option with --theme custom-theme'));
  }
  let packageData = {};
  let testPackages = [path.join(process.cwd(), 'package.json'), path.join(process.cwd(), '../', 'package.json'), path.join(process.cwd(), '../', '../', 'package.json')];
  // test within reason, for package.json files seeing if anything is available to suggest
  // that we might be in a local package or a monorepo.
  while (testPackages.length > 0) {
    let packLoc = testPackages.shift();
    if (fs.existsSync(packLoc)) {
      try {
        packageData = {
          ...JSON.parse(fs.readFileSync(packLoc)),
          ...packageData
        };
        // assume we are working on a web component / existing if we find this key
        if (packageData.hax && packageData.hax.cli) {
          commandRun.program = 'webcomponent';
        }
        // leverage these values if they exist downstream
        if (packageData.npmClient) {
          commandRun.options.npmClient = packageData.npmClient;
        } else {
          commandRun.options.npmClient = 'npm';
        }
        // see if we're in a monorepo
        if (packageData.useWorkspaces && packageData.workspaces && packageData.workspaces.packages && packageData.workspaces.packages[0]) {
          if (!commandRun.options.quiet) {
            p.intro(`${_picocolors.default.bgBlack(_picocolors.default.white(` Monorepo detected : Setting relative defaults `))}`);
          }
          commandRun.options.isMonorepo = true;
          commandRun.options.auto = true;
          commandRun.options.template = 'monorepo';
          // assumed if monorepo
          if (commandRun.command === "audit") {
            (0, _audit.auditCommandDetected)(commandRun);
          } else {
            commandRun.command = 'webcomponent';
            commandRun.options.path = path.join(process.cwd(), packageData.workspaces.packages[0].replace('/*', ''));
            if (packageData.orgNpm) {
              commandRun.options.org = packageData.orgNpm;
            }
            commandRun.options.gitRepo = packageData.repository.url;
            commandRun.options.author = packageData.author.name ? packageData.author.name : author;
          }
        }
      } catch (err) {
        console.error(err);
      }
    }
  }
  if (commandRun.options.debug) {
    (0, _logging.log)(packageData, 'debug');
  }
  // Security (M-2): validate --npm-client against a strict allowlist before it
  // can be interpolated into any exec() shell string downstream. This runs
  // after the packageData merge so a monorepo's npmClient is also covered.
  if (commandRun.options.npmClient) {
    try {
      commandRun.options.npmClient = (0, _utils.validateNpmClient)(commandRun.options.npmClient);
    } catch (e) {
      _commander.program.error(_picocolors.default.red(e.message));
    }
  }
  // Security (M-1): reject shell metacharacters in options that are
  // interpolated into exec() shell strings across site.js/webcomponent.js/
  // party.js (publish targets, git remote, rsync source/dest, excludes).
  // --domain additionally gets a strict hostname charset. --path must be an
  // existing directory (--root was validated earlier, before chdir). Validates
  // at the boundary so the existing exec(...${opt}...) call sites are no longer
  // exploitable, without a full exec()->spawn() migration.
  try {
    if (commandRun.options.domain) {
      commandRun.options.domain = (0, _utils.validateDomain)(commandRun.options.domain);
    }
    (0, _utils.rejectShellMetacharacters)(commandRun.options.author, 'author');
    (0, _utils.rejectShellMetacharacters)(commandRun.options.gitRepo, 'git-repo');
    (0, _utils.rejectShellMetacharacters)(commandRun.options.source, 'source');
    (0, _utils.rejectShellMetacharacters)(commandRun.options.destination, 'destination');
    if (commandRun.options.exclude) {
      String(commandRun.options.exclude).split(',').forEach(pat => (0, _utils.rejectShellMetacharacters)(pat.trim(), 'exclude'));
    }
    if (commandRun.options.path) {
      (0, _utils.rejectShellMetacharacters)(commandRun.options.path, 'path');
      if (!fs.existsSync(commandRun.options.path) || !fs.statSync(commandRun.options.path).isDirectory()) {
        _commander.program.error(_picocolors.default.red(`Invalid --path: ${commandRun.options.path} is not an existing directory.`));
      }
    }
  } catch (e) {
    _commander.program.error(_picocolors.default.red(e.message));
  }
  // test for updating to latest or just run the command
  if (commandRun.command === "update") {
    await testForUpdates(commandRun);
  } else if (commandRun.command === 'audit') {
    let customPath = null;
    // test for haxcms context
    if ((await systemStructureContext()) && fs.existsSync(`${process.cwd()}/custom`)) {
      customPath = path.join(process.cwd(), 'custom');
    }
    (0, _audit.auditCommandDetected)(commandRun, customPath);
  } else if (commandRun.command === 'party') {
    commandRun.options.author = author;
    await (0, _party.partyCommandDetected)(commandRun);
  } else if (commandRun.command === 'skills') {
    await (0, _skills.skillsCommandDetected)(commandRun);
  }
  // CLI works within context of the site if one is detected, otherwise we can do other things
  else if (await systemStructureContext()) {
    if (commandRun.command === 'serve') {
      commandRun.program = 'serve';
      commandRun.options.skip = true;
      await (0, _site.siteCommandDetected)(commandRun);
    } else {
      commandRun.program = 'site';
      commandRun.options.skip = true;
      await (0, _site.siteCommandDetected)(commandRun);
    }
  } else if (packageData && (packageData.customElements || packageData.hax && packageData.hax.cli) && packageData.scripts.start) {
    if (commandRun.command === 'serve') {
      commandRun.program = 'serve';
      commandRun.options.skip = true;
      await (0, _webcomponent.webcomponentCommandDetected)(commandRun, packageData);
    } else {
      commandRun.program = 'webcomponent';
      commandRun.options.skip = true;
      await (0, _webcomponent.webcomponentCommandDetected)(commandRun, packageData);
    }
  } else {
    if (commandRun.command === 'start' && !commandRun.options.y && !commandRun.options.auto && !commandRun.options.skip && !commandRun.options.quiet) {
      await (0, _statements.haxIntro)();
    }
    // it's been 7 days since last check, do the check and offer command to resolve if needed
    if (timeSince.days > 7) {
      // check for updates
      let execOut = await (0, _utils.exec)(`npm view @haxtheweb/create version`);
      let updateCheck = execOut.stdout.trim();
      if (updateCheck !== _package.default.version && !commandRun.options.quiet) {
        p.intro(`${_picocolors.default.bgBlack(_picocolors.default.white(` HAX cli updates available! `))}`);
        p.intro(`Current version: ${_package.default.version}`);
        p.intro(`Latest version: ${updateCheck}`);
        p.intro(`Run ${_picocolors.default.bold(_picocolors.default.black(_picocolors.default.bgGreen('hax update')))} to update to the latest version`);
      }
    }
    let activeProject = null;
    let project = {
      type: null
    };
    while (project.type !== 'quit' && project.type !== 'update') {
      if (activeProject) {
        if (!commandRun.options.quiet) {
          p.note(` 🧙🪄 BE GONE ${_picocolors.default.bold(_picocolors.default.black(_picocolors.default.bgGreen(activeProject)))} sub-process daemon! 🪄 + ✨ 👹 = 💀 `);
        }
        // ensure if we were automatically running the command we end
        if (commandRun.options.y) {
          if (!commandRun.options.quiet) {
            (0, _statements.communityStatement)();
          }
          process.exit(0);
        }
        // otherwise null to reset the program to run again
        commandRun = {
          command: null,
          arguments: {},
          options: {}
        };
      }
      if (['site', 'webcomponent', 'audit'].includes(commandRun.command)) {
        project = {
          type: commandRun.command
        };
      } else if (commandRun.options.i) {
        let buildOptions = [{
          value: 'webcomponent',
          label: '🏗️ Create a Web Component'
        }, {
          value: 'site',
          label: '🏡 Create a HAXsite'
        }, {
          value: 'party',
          label: '🎉 Join the HAX community'
        }, {
          value: 'update',
          label: '🤓 Check for hax cli updates'
        }, {
          value: 'quit',
          label: '🚪 Quit'
        }];
        project = await p.group({
          type: ({
            results
          }) => p.select({
            message: !activeProject ? `What should we build?` : `Thirsty for more? What should we create now?`,
            initialValue: 'webcomponent',
            required: true,
            options: buildOptions
          })
        }, {
          onCancel: () => {
            if (!commandRun.options.quiet) {
              p.cancel('🧙🪄 Merlin: Canceling cli.. HAX ya later');
              (0, _statements.communityStatement)();
            }
            process.exit(0);
          }
        });
      } else if (!commandRun.options.i) {
        process.exit(0);
      }
      if (project.type === "update") {
        await testForUpdates(commandRun);
      } else if (project.type === "party") {
        await (0, _party.partyCommandDetected)(commandRun);
      }
      // detect being in a haxcms scaffold. easiest way is _sites being in this directory
      // set the path automatically so we skip the question
      else if (commandRun.command === "site" && fs.existsSync(`${process.cwd()}/_sites`)) {
        if (!commandRun.options.quiet) {
          p.intro(`${_picocolors.default.bgBlack(_picocolors.default.white(` HAXcms detected : Path set automatically `))}`);
        }
        commandRun.options.path = `${process.cwd()}/_sites`;
      }
      activeProject = project.type;
      // silly but this way we don't have to take options for quitting
      if (['site', 'webcomponent'].includes(project.type)) {
        // global spot for core themes list
        let coreThemes = await (0, _site.siteThemeList)(true);
        const cleanOneTheme = coreThemes.find(themeOption => themeOption.value === 'clean-one');
        const defaultCoreTheme = cleanOneTheme ? cleanOneTheme : coreThemes[0];
        let availableSkeletons = await (0, _site.siteSkeletonList)(true);
        project = await p.group({
          type: ({
            results
          }) => {
            return new Promise((resolve, reject) => {
              resolve(activeProject);
            });
          },
          path: ({
            results
          }) => {
            let initialPath = `${process.cwd()}`;
            if (!commandRun.options.path && !commandRun.options.auto && !commandRun.options.skip) {
              return p.text({
                message: `What folder will your ${commandRun.command === "webcomponent" || results.type === "webcomponent" ? "project" : "site"} live in?`,
                placeholder: initialPath,
                initialValue: initialPath,
                required: true,
                validate: value => {
                  if (!value) {
                    return "Path is required (Enter accepts default)";
                  }
                  if (!fs.existsSync(value)) {
                    return `${value} does not exist. Select a valid folder`;
                  }
                }
              });
            }
          },
          name: ({
            results
          }) => {
            const reservedNames = ["annotation-xml", "color-profile", "font-face", "font-face-src", "font-face-uri", "font-face-format", "font-face-name", "missing-glyph"];
            if (!commandRun.arguments.action && !commandRun.options.name) {
              if (commandRun.options.auto || commandRun.options.skip || !commandRun.options.i) {
                _commander.program.error(_picocolors.default.red("Name is required when running non-interactively. Pass --name <value>."));
                process.exit(1);
              }
              let placeholder = "mysite";
              let message = "Site name:";
              if (commandRun.command === "webcomponent" || results.type === "webcomponent") {
                placeholder = "my-element";
                message = "Element name:";
              }
              return p.text({
                message: message,
                placeholder: placeholder,
                initialValue: placeholder,
                required: true,
                validate: value => {
                  if (!value) {
                    return "Name is required (Enter accepts default)";
                  }
                  if (reservedNames.includes(value)) {
                    return `Reserved name ${_picocolors.default.bold(value)} cannot be used`;
                  }
                  if (value.toLocaleLowerCase() !== value) {
                    return "Name must be lowercase";
                  }
                  if (/^\d/.test(value)) {
                    return "Name cannot start with a number";
                  }
                  if (/[`~!@#$%^&*()_=+\[\]{}|;:\'",<.>\/?\\]/.test(value)) {
                    return "No special characters allowed in name";
                  }
                  if (value.indexOf(' ') !== -1) {
                    return "No spaces allowed in name";
                  }
                  if (results.type === "webcomponent" && (value.indexOf('-') === -1 || value.replace('--', '') !== value || value[0] === '-' || value[value.length - 1] === '-')) {
                    return "Name must include at least one `-` and must not start or end name.";
                  }
                  // test that this is not an existing element we use in the registry
                  if (results.type === "webcomponent" && wcReg[value] && !commandRun.options.force) {
                    return "Name is already a web component in the wc-registry published for HAX.";
                  }
                  // Check for any other syntax errors
                  if (results.type === "webcomponent" && !/^[a-z][a-z0-9.\-]*\-[a-z0-9.\-]*$/.test(value)) {
                    return `Name must follow the syntax ${_picocolors.default.bold("my-component")}`;
                  }
                  // assumes auto was selected in CLI
                  let joint = process.cwd();
                  if (commandRun.options.path) {
                    joint = commandRun.options.path;
                  } else if (results.path) {
                    joint = results.path;
                  }
                  if (fs.existsSync(path.join(joint, value))) {
                    return `${path.join(joint, value)} exists, rename this project`;
                  }
                }
              });
            }
            if (commandRun.arguments.action || commandRun.options.name) {
              let knownActions = [];
              if (results.type === "webcomponent") {
                knownActions = (0, _webcomponent.webcomponentActions)().map(action => action.value);
              } else if (results.type === "site") {
                knownActions = (0, _site.siteActions)().map(action => action.value);
              }
              let value = knownActions.includes(commandRun.arguments.action) ? commandRun.options.name : commandRun.arguments.action || commandRun.options.name;
              if (!value) {
                _commander.program.error(_picocolors.default.red("Name is required (Enter accepts default)."));
                process.exit(1);
              }
              if (reservedNames.includes(value)) {
                _commander.program.error(_picocolors.default.red(`Reserved name ${_picocolors.default.bold(value)} cannot be used`));
                process.exit(1);
              }
              if (value.toLocaleLowerCase() !== value) {
                _commander.program.error(_picocolors.default.red("Name must be lowercase"));
                process.exit(1);
              }
              if (/^\d/.test(value)) {
                _commander.program.error(_picocolors.default.red("Name cannot start with a number"));
                process.exit(1);
              }
              if (value.indexOf(' ') !== -1) {
                _commander.program.error(_picocolors.default.red("No spaces allowed in name"));
                process.exit(1);
              }
              if (results.type === "webcomponent" && (value.indexOf('-') === -1 || value.replace('--', '') !== value || value[0] === '-' || value[value.length - 1] === '-')) {
                _commander.program.error(_picocolors.default.red("Name must include at least one `-` and must not start or end name."));
                process.exit(1);
              }
              // test that this is not an existing element we use in the registry
              if (results.type === "webcomponent" && wcReg[value] && !commandRun.options.force) {
                _commander.program.error(_picocolors.default.red("Name is already a web component in the wc-registry published for HAX."));
                process.exit(1);
              }
              // Check for any other syntax errors
              if (results.type === "webcomponent" && !/^[a-z][a-z0-9.\-]*\-[a-z0-9.\-]*$/.test(value)) {
                _commander.program.error(_picocolors.default.red(`Name must follow the syntax ${_picocolors.default.bold("my-component")}`));
                process.exit(1);
              }
              // assumes auto was selected in CLI
              let joint = process.cwd();
              if (commandRun.options.path) {
                joint = commandRun.options.path;
              } else if (results.path) {
                joint = results.path;
              }
              if (fs.existsSync(path.join(joint, value))) {
                _commander.program.error(_picocolors.default.red(`${path.join(joint, value)} exists, rename this project`));
                process.exit(1);
              }
            }
          },
          org: ({
            results
          }) => {
            if (results.type === "webcomponent" && !commandRun.options.org && !commandRun.options.auto && !commandRun.options.skip) {
              let initialOrg = '@yourOrganization';
              return p.text({
                message: 'Organization:',
                placeholder: initialOrg,
                initialValue: initialOrg,
                required: false,
                validate: value => {
                  if (value && !value.startsWith('@')) {
                    return "Organizations are not required, but organizations must start with @ if used";
                  }
                }
              });
            }
          },
          author: ({
            results
          }) => {
            if (!commandRun.options.author && !commandRun.options.auto && !commandRun.options.skip) {
              return p.text({
                message: 'Author:',
                required: false,
                initialValue: author
              });
            }
          },
          template: ({
            results
          }) => {
            if (results.type === 'webcomponent' && !commandRun.options.isMonorepo && !commandRun.options.template && !commandRun.options.auto && !commandRun.options.skip && commandRun.options.i) {
              return p.select({
                message: 'Template:',
                required: false,
                options: [{
                  value: 'compliant',
                  label: 'Standard — DDD, i18n, HAX, full build (recommended)'
                }, {
                  value: 'minimal',
                  label: 'Minimal — Lit only, no docs, no build pipeline'
                }, {
                  value: 'training',
                  label: 'Training — Heavily documented tutorial example'
                }],
                initialValue: 'compliant'
              });
            }
          },
          siteCreationMode: async ({
            results
          }) => {
            if (results.type === "site") {
              const hasSkeletonFile = Boolean(commandRun.options.skeletonFile);
              const hasSkeletonMachineName = Boolean(commandRun.options.skeletonMachineName);
              if (hasSkeletonFile && hasSkeletonMachineName) {
                _commander.program.error(_picocolors.default.red('You can only use one skeleton source when creating a site (--skeleton-file or --skeleton-machine-name).'));
                process.exit(1);
              }
              if ((hasSkeletonFile || hasSkeletonMachineName) && commandRun.options.theme) {
                _commander.program.error(_picocolors.default.red('Skeleton template creation and --theme are mutually exclusive. Use Start from Scratch when selecting a theme.'));
                process.exit(1);
              }
              if ((hasSkeletonFile || hasSkeletonMachineName) && commandRun.options.importSite) {
                _commander.program.error(_picocolors.default.red('Skeleton template creation and --import-site are mutually exclusive.'));
                process.exit(1);
              }
              if (hasSkeletonFile || hasSkeletonMachineName) {
                return 'template';
              }
              if (commandRun.options.theme) {
                return 'scratch';
              }
              if (!commandRun.options.i || commandRun.options.auto || commandRun.options.skip) {
                return 'scratch';
              }
              return p.select({
                message: 'How should this site start?',
                required: true,
                options: [{
                  value: 'scratch',
                  label: 'Start from Scratch (pick a theme)'
                }, {
                  value: 'template',
                  label: 'Start from Template (pick a skeleton)'
                }],
                initialValue: 'scratch'
              });
            }
          },
          skeletonMachineName: async ({
            results
          }) => {
            if (results.type === "site" && results.siteCreationMode === 'template') {
              if (commandRun.options.skeletonMachineName) {
                return commandRun.options.skeletonMachineName;
              }
              if (commandRun.options.skeletonFile) {
                return null;
              }
              if (!commandRun.options.i) {
                _commander.program.error(_picocolors.default.red('Template mode requires --skeleton-file or --skeleton-machine-name when running non-interactively.'));
                process.exit(1);
              }
              if (availableSkeletons.length === 0) {
                return null;
              }
              let skeletonOptions = [...availableSkeletons];
              skeletonOptions.push({
                value: '__from_file__',
                label: 'Use skeleton JSON file'
              });
              const selectedSkeleton = await p.select({
                message: 'Template:',
                required: true,
                options: skeletonOptions,
                initialValue: skeletonOptions[0].value
              });
              if (selectedSkeleton === '__from_file__') {
                return null;
              }
              commandRun.options.skeletonMachineName = selectedSkeleton;
              return selectedSkeleton;
            }
          },
          skeletonFile: ({
            results
          }) => {
            if (results.type === "site" && results.siteCreationMode === 'template' && !commandRun.options.skeletonMachineName && !results.skeletonMachineName && !commandRun.options.skeletonFile) {
              return p.text({
                message: 'Template JSON file path:',
                placeholder: `${process.cwd()}/template.json`,
                initialValue: `${process.cwd()}/template.json`,
                required: true,
                validate: value => {
                  if (!value) {
                    return 'Template JSON file path is required';
                  }
                  if (!fs.existsSync(value)) {
                    return `${value} does not exist. Select a valid file`;
                  }
                  if (!value.endsWith('.json')) {
                    return 'Template file must be a JSON file';
                  }
                }
              });
            }
          },
          theme: async ({
            results
          }) => {
            if (results.type === "site" && results.siteCreationMode !== 'template' && !commandRun.options.theme) {
              // support having no theme but autoselecting
              if (commandRun.options.auto && commandRun.options.skip) {
                commandRun.options.theme = defaultCoreTheme.value;
              } else {
                return p.select({
                  message: "Theme:",
                  required: false,
                  options: coreThemes,
                  initialValue: defaultCoreTheme.value
                });
              }
            } else if (results.type === "site" && results.siteCreationMode !== 'template' && commandRun.options.theme) {
              if (coreThemes.filter(item => item.value === commandRun.options.theme).length === 0) {
                _commander.program.error(_picocolors.default.red('Theme is not in the list of valid themes'));
                process.exit(1);
              }
            }
          },
          customThemeName: async ({
            results
          }) => {
            if (results.theme === "custom-theme") {
              let tmpCustomName = await p.text({
                message: 'Theme Name:',
                placeholder: `custom-${commandRun.arguments.action ? commandRun.arguments.action : results.name ? results.name : commandRun.options.name}-theme`,
                initialValue: `custom-${commandRun.arguments.action ? commandRun.arguments.action : results.name ? results.name : commandRun.options.name}-theme`,
                required: false,
                validate: value => {
                  if (!value) {
                    return "Theme name is required (Enter accepts default)";
                  }
                  if (coreThemes.some(theme => theme.value === value)) {
                    return "Theme name is already in use";
                  }
                  if (/^\d/.test(value)) {
                    return "Theme name cannot start with a number";
                  }
                  if (/[A-Z]/.test(value)) {
                    return "No uppercase letters allowed in theme name";
                  }
                  if (value.indexOf(' ') !== -1) {
                    return "No spaces allowed in theme name";
                  }
                }
              });
              return tmpCustomName;
            } else if (results.type === "site") {
              // need to validate theme from CLI arguments
              let value = `${commandRun.options.customThemeName ? commandRun.options.customThemeName : results.name ? results.name : commandRun.arguments.action ? commandRun.arguments.action : commandRun.options.name}`;
              if (!value) {
                _commander.program.error(_picocolors.default.red("Theme name is required (Enter accepts default)"));
              }
              if (coreThemes.some(theme => theme.value === value)) {
                _commander.program.error(_picocolors.default.red("Theme name is already in use"));
              }
              if (/^\d/.test(value)) {
                _commander.program.error(_picocolors.default.red("Theme name cannot start with a number"));
              }
              if (/[A-Z]/.test(value)) {
                _commander.program.error(_picocolors.default.red("No uppercase letters allowed in theme name"));
              }
              if (value.indexOf(' ') !== -1) {
                _commander.program.error(_picocolors.default.red("No spaces allowed in theme name"));
              }
            }
          },
          customThemeTemplate: ({
            results
          }) => {
            if (results.theme === "custom-theme") {
              const options = [{
                value: 'base',
                label: 'Vanilla Theme with Hearty Documentation'
              }, {
                value: 'polaris-flex',
                label: 'Minimalist Theme with Horizontal Nav'
              }, {
                value: 'polaris-sidebar',
                label: 'Content-Focused Theme with Flexible Sidebar'
              }];
              return p.select({
                message: 'Template:',
                required: false,
                options: options,
                initialValue: `${results.theme}`
              });
            }
          },
          extras: ({
            results
          }) => {
            if (!commandRun.options.auto && commandRun.options.i) {
              let options = [];
              let initialValues = [];
              if (commandRun.command === "webcomponent" || results.type === "webcomponent") {
                options = [{
                  value: 'launch',
                  label: 'Launch project',
                  hint: 'recommended'
                }, {
                  value: 'install',
                  label: `Install dependencies via ${commandRun.options.npmClient}`,
                  hint: 'recommended'
                }, {
                  value: 'git',
                  label: 'Apply version control via git',
                  hint: 'recommended'
                }];
                initialValues = ['launch', 'install', 'git'];
                if (!sysGit || commandRun.options.isMonorepo) {
                  options.pop();
                  initialValues.pop();
                }
              } else {
                options = [{
                  value: 'launch',
                  label: 'Launch project on creation',
                  hint: 'recommended'
                }];
                initialValues = ['launch'];
              }
              return p.multiselect({
                message: 'Additional setup',
                initialValues: initialValues,
                options: options,
                required: false
              });
            }
          }
        }, {
          onCancel: () => {
            if (!commandRun.options.quiet) {
              p.cancel('🧙🪄 Merlin: Canceling cli.. HAX ya later');
              (0, _statements.communityStatement)();
            }
            process.exit(0);
          }
        });
        // merge cli options with project options assume this is NOT a monorepo
        // but spread will overwrite if needed
        if (commandRun.command === 'webcomponent' && !commandRun.arguments.action) {
          project = {
            isMonorepo: false,
            ...project,
            ...commandRun.options
          };
        } else {
          project = {
            isMonorepo: false,
            ...project,
            ...commandRun.arguments,
            ...commandRun.options
          };
        }
        project.year = new Date().getFullYear();
        project.version = _package.default.version;
        if (!project.name && commandRun.arguments.action) {
          project.name = commandRun.arguments.action;
        }
        if (project.type === 'webcomponent' && !project.template) {
          project.template = 'compliant';
        }

        // resolve site vs multi-site
        switch (project.type) {
          case 'site':
            // only set path if not already set, normalize across clack and commander
            if (commandRun.options.path && !project.path) {
              project.path = commandRun.options.path;
            }
            if (!project.path) {
              project.path = process.cwd();
            }
            if (!commandRun.options.path) {
              commandRun.options.path = project.path;
            }
            await (0, _site.siteProcess)(commandRun, project);
            break;
          case 'webcomponent':
            await (0, _webcomponent.webcomponentProcess)(commandRun, project);
            break;
        }
      }
    }
    if (!commandRun.options.quiet && project.type !== 'update') {
      (0, _statements.communityStatement)();
    }
  }
}

// check for updates
async function testForUpdates(commandRun) {
  let execOut = await (0, _utils.exec)(`npm view @haxtheweb/create version`);
  let latest = execOut.stdout.trim();
  if (latest !== _package.default.version) {
    if (!commandRun.options.quiet) {
      p.intro(`${_picocolors.default.bgBlack(_picocolors.default.white(` HAX cli updates available! `))}`);
      p.intro(`Current version: ${_package.default.version}`);
      p.intro(`Latest version: ${latest}`);
    }
    let runUpdates = {
      answer: false
    };
    if (!commandRun.options.y) {
      runUpdates = await p.group({
        answer: ({
          results
        }) => p.confirm({
          message: `Do you want to update the cli to the latest version? (${latest})`,
          initialValue: true
        })
      }, {
        onCancel: () => {
          if (!commandRun.options.quiet) {
            p.cancel('🧙🪄 Merlin: Leaving so soon? HAX ya later');
            p.outro(`
                🧙  Upgrade at any time: ${_picocolors.default.yellow('npm install --global @haxtheweb/create')}
                
                💡  ${_picocolors.default.bold(_picocolors.default.white(`Never. Stop. Innovating.`))}
              `);
          }
          process.exit(0);
        }
      });
    } else {
      runUpdates.answer = true; // automatic
    }
    // ensure they wanted to run them
    if (runUpdates.answer) {
      await (0, _utils.interactiveExec)('npm', ['install', '--global', '@haxtheweb/create']);
      if (!commandRun.options.quiet) {
        p.outro(`
          🔮  HAX CLI updated to : ${_picocolors.default.yellow(latest)}
          
          🧙  Type ${_picocolors.default.yellow('hax help')} for latest commands
          
          💡  ${_picocolors.default.bold(_picocolors.default.white(`Never. Stop. Innovating.`))}
        `);
      }
    } else {
      if (!commandRun.options.quiet) {
        p.outro(`
          🧙  Upgrade at any time: ${_picocolors.default.yellow('npm install --global @haxtheweb/create')}
          
          💡  ${_picocolors.default.bold(_picocolors.default.white(`Never. Stop. Innovating.`))}
        `);
      }
    }
  } else {
    if (!commandRun.options.quiet) {
      p.intro(`${_picocolors.default.bgBlack(_picocolors.default.white(` HAX CLI (${_package.default.version}) is up to date `))}`);
    }
  }
}
main().catch(console.error);