message: cli: build out task lib a bit with command execution, adding deps
diff: @@ -18,9 +18,14 @@ import chalk from 'chalk';
  import fs from 'fs-extra';
  import handlebars from 'handlebars';
  import ora from 'ora';
  +import { promisify } from 'util';
  import { basename, dirname } from 'path';
  import recursive from 'recursive-readdir';
  +import { exec as execCb } from 'child_process';
  import { paths } from './paths';
  +import { assertError } from '@backstage/errors';
  +
  +const exec = promisify(execCb);
  const TASK_NAME_MAX_LENGTH = 14;
  @@ -42,11 +47,11 @@ export class Task {
  process.exit(code);
  }
  - static async forItem(
  + static async forItem<T = void>(
  task: string,
  item: string,
  - taskFunc: () => Promise<void>,
  - ): Promise<void> {
  + taskFunc: () => Promise<T>,
  + ): Promise<T> {
  const paddedTask = chalk.green(task.padEnd(TASK_NAME_MAX_LENGTH));
  const spinner = ora({
  @@ -56,13 +61,40 @@ export class Task {
  }).start();
  try {
  - await taskFunc();
  + const result = await taskFunc();
  spinner.succeed();
  + return result;
  } catch (error) {
  spinner.fail();
  throw error;
  }
  }
  +
  + static async forCommand(
  + command: string,
  + options?: { cwd?: string; optional?: boolean },
  + ) {
  + try {
  + await Task.forItem('executing', command, async () => {
  + await exec(command, { cwd: options?.cwd });
  + });
  + } catch (error) {
  + assertError(error);
  + if (error.stderr) {
  + process.stdout.write(error.stderr as Buffer);
  + }
  + if (error.stdout) {
  + process.stdout.write(error.stdout as Buffer);
  + }
  + if (options?.optional) {
  + Task.error(`Warning: Failed to execute command ${chalk.cyan(command)}`);
  + } else {
  + throw new Error(
  + `Failed to execute command '${chalk.cyan(command)}', ${error}`,
  + );
  + }
  + }
  + }
  }
  export async function templatingTask(
  @@ -122,3 +154,44 @@ export async function templatingTask(
  }
  }
  }
  +
  +export async function addPackageDependency(
  + path: string,
  + options: {
  + dependencies?: Record<string, string>;
  + devDependencies?: Record<string, string>;
  + peerDependencies?: Record<string, string>;
  + },
  +) {
  + try {
  + const pkgJson = await fs.readJson(path);
  +
  + const normalize = (obj: Record<string, string>) => {
  + if (Object.keys(obj).length === 0) {
  + return undefined;
  + }
  + return Object.fromEntries(
  + Object.keys(obj)
  + .sort()
  + .map(key => [key, obj[key]]),
  + );
  + };
  +
  + pkgJson.dependencies = normalize({
  + ...pkgJson.dependencies,
  + ...options.dependencies,
  + });
  + pkgJson.devDependencies = normalize({
  + ...pkgJson.devDependencies,
  + ...options.devDependencies,
  + });
  + pkgJson.peerDependencies = normalize({
  + ...pkgJson.peerDependencies,
  + ...options.peerDependencies,
  + });
  +
  + await fs.writeJson(path, pkgJson, { spaces: 2 });
  + } catch (error) {
  + throw new Error(`Failed to add package dependencies, ${error}`);
  + }
  +}
  