or: "my kingdom for a progress bar!"
npm i @battis/gas-lighter
npx clasp login # if not already logged in
npx --package=@battis/gas-lighter -- setup
Less verbose CardService apps.
import g from '@battis/gas-lighter';
global.onHomepage = () => {
  return g.CardService.Card.$({
    header: 'Welcome!',
    widgets: [
      'Lorem ipsum dolor.',
      g.CardService.Widget.TextButton.$({
        text: 'Example',
        url: 'https://example.com'
      })
    ]
  });
};
Nicer UI (with longer-running scripts).
import g, {import, getProgress} from '@battis/gas-lighter';
global.include = include;
global.getProgress = getProgress;
// ...
global.jobThatExceedsScriptTimoutLimit(job?: string) {
  const progress = new g.HtmlService.Component.Progress({
    job,
    onComplete: g.HtmlService.Page.Message.close(),
    paging: {
      loader: ({ page, progress }) => {
        // ...return an iterable object starting at `page` of your data to be processed by `handler()`
        progress.max = 1000;
      },
      handler: ({ data, progress }) => {
        // ...process a page of your data
        progress.value++;
        progress.status = `Working on it`;
      },
      callback: 'jobThatExceedsScriptTimoutLimit'
    }
  });
  if (!job) {
    progress.getPage().modal({
      root: SpreadsheetApp,
      height: 100,
      data: { title: 'The Big One' }
    });
  }
  progress.run();
}
// ...
More in examples.