]> git.wincent.com - wincent.git/commitdiff
feat(vim): add WIP of .editorconfig support
authorGreg Hurrell <greg@hurrell.net>
Fri, 3 Apr 2020 23:56:54 +0000 (01:56 +0200)
committerGreg Hurrell <greg@hurrell.net>
Fri, 3 Apr 2020 23:59:53 +0000 (01:59 +0200)
It works for the simple case shown here, which is changing the tabWidth
to 4 spaces (up from 2) in Markdown, JSON, JS, and TS files.

I prefer to do this minimally and locally without pulling in a large
editorconfig plugin like:

- https://github.com/editorconfig/editorconfig-vim
- https://github.com/sgur/vim-editorconfig

because I want to integrate it with my existing Liferay-specific stuff.

Plenty of unimplemented features though, and I don't intend to reach
feature parity with those plugins, so I might end up having to implement
an "escape hatch" here to blacklist folders when things don't play
nicely. Will cross that bridge when we come to it.

78 files changed:
.editorconfig [new file with mode: 0644]
.prettierrc
CHANGELOG.md
README.md
aspects/launchd/aspect.json
aspects/launchd/index.ts
aspects/terminfo/aspect.json
aspects/terminfo/index.ts
package.json
profiles/defaults.json
profiles/personal.json
profiles/work.json
project.json
roles/automator/files/Open\ in\ Vim.js
roles/dotfiles/files/.clipper.json
roles/dotfiles/files/.vim/autoload/wincent/autocmds.vim
roles/dotfiles/files/.zsh/liferay/bin/portool
roles/dotfiles/support/karabiner-test.js
roles/dotfiles/support/karabiner.js
roles/iterm/files/DynamicProfiles/00-Base.json
roles/iterm/files/DynamicProfiles/30-Mutt-Base.json
roles/iterm/files/DynamicProfiles/60-Vim-Base.json
roles/iterm/files/Sources/10-4K.json
roles/iterm/files/Sources/10-Retina.json
roles/iterm/files/Sources/40-Mutt-4K.json
roles/iterm/files/Sources/40-Mutt-Retina.json
roles/iterm/files/Sources/70-Vim-4K.json
roles/iterm/files/Sources/70-Vim-Retina.json
src/Attributes.ts
src/Compiler.ts
src/ErrorWithMetadata.ts
src/Fig/Context.ts
src/Fig/TaskRegistry.ts
src/Fig/__tests__/compare-test.ts
src/Fig/compare.ts
src/Fig/globToRegExp.ts
src/Fig/index.ts
src/Fig/operations/command.ts
src/Fig/operations/file.ts
src/Fig/operations/template.ts
src/Fig/resource.ts
src/Fig/root.ts
src/Fig/status.ts
src/Fig/task.ts
src/Fig/variable.ts
src/__tests__/merge-test.ts
src/__tests__/regExpFromString-test.ts
src/__tests__/stringify-test.ts
src/__tests__/template-test.ts
src/assert.ts
src/chown.ts
src/console/COLORS.ts
src/console/index.ts
src/dedent.ts
src/escapeRegExpPattern.ts
src/expand.ts
src/getCaller.ts
src/getOptions.ts
src/main.ts
src/merge.ts
src/prompt.ts
src/readAspect.ts
src/readProject.ts
src/regExpFromString.ts
src/run.ts
src/simplify.ts
src/spawn.ts
src/stat.ts
src/stringify.ts
src/tempfile.ts
src/template.ts
src/test/harness.ts
src/test/index.ts
src/types.d.ts
support/typegen/Builder.js
support/typegen/SCHEMAS.js
support/typegen/index.js
tsconfig.json

diff --git a/.editorconfig b/.editorconfig
new file mode 100644 (file)
index 0000000..9d9ccb8
--- /dev/null
@@ -0,0 +1,10 @@
+# See: https://editorconfig.org/
+root = true
+
+[*.{js,json,md,ts}]
+indent_style = space
+indent_size = 4
+
+[portool]
+indent_style = space
+indent_size = 4
index 34cfd521f1ad372a2bd7548ce6f0f2725b70f467..0aad73f1b69638019397066a7ddd83a096df4494 100644 (file)
@@ -1,2 +1,3 @@
 bracketSpacing: false
 singleQuote: true
+tabWidth: 4
index 67e99c315bcd41a2591eddb272314f4a5fc79a25..719bd77c6ab197d8696970448b8553bb4813b613 100644 (file)
 
 ## 2019-10-13
 
-- [Vim]: Switched from [tpope/vim-markdown](https://github.com/tpope/vim-markdown) to [plasticboy/vim-markdown](https://github.com/plasticboy/vim-markdown); note that because this is replacing one submodule with another of the same name you may need to remove the old submodule directory with `rm` as well as removing the corresponding config in your local `.git/config` in order for the next `git submodule update --init` to succeed.
+-   [Vim]: Switched from [tpope/vim-markdown](https://github.com/tpope/vim-markdown) to [plasticboy/vim-markdown](https://github.com/plasticboy/vim-markdown); note that because this is replacing one submodule with another of the same name you may need to remove the old submodule directory with `rm` as well as removing the corresponding config in your local `.git/config` in order for the next `git submodule update --init` to succeed.
 
 ## 2019-10-02
 
-- [Vim]: Add `:OpenOnGitHub` command.
+-   [Vim]: Add `:OpenOnGitHub` command.
 
 ## 2019-09-27
 
-- [Vim]: Add `:Typecheck` command.
+-   [Vim]: Add `:Typecheck` command.
 
 ## 2019-09-24
 
-- [Vim]: Add `:Lint` command.
+-   [Vim]: Add `:Lint` command.
 
 ## 2019-09-19
 
-- [Zsh]: Make tabs visible in `git-diff` output.
+-   [Zsh]: Make tabs visible in `git-diff` output.
 
 ## 2019-09-14
 
-- [Vim]: Implement "smart tab" functionality (ie. in projects that use tabs, use tabs for indentation and spacing for alignment).
+-   [Vim]: Implement "smart tab" functionality (ie. in projects that use tabs, use tabs for indentation and spacing for alignment).
 
 ## 2019-09-14
 
-- [Zsh]: Add `git cp` alias for `git cherry-pick`.
+-   [Zsh]: Add `git cp` alias for `git cherry-pick`.
 
 ## 2019-08-23
 
-- [Zsh]: Add `git wd` alias for `git diff --dirstat` (mnemonic: "[w]hat(changed) [d]irectory").
+-   [Zsh]: Add `git wd` alias for `git diff --dirstat` (mnemonic: "[w]hat(changed) [d]irectory").
 
 ## 2019-08-22
 
-- [Vim]: Use floating preview window for LSP "hover" functionality.
+-   [Vim]: Use floating preview window for LSP "hover" functionality.
 
 ## 2019-07-21
 
-- [Vim]: Switch from javascript-typescript-langserver to typescript-language-server.
+-   [Vim]: Switch from javascript-typescript-langserver to typescript-language-server.
 
 ## 2019-06-26
 
-- [Zsh]: Add `git get` alias equivalent to a `git fresh` (see below) followed by a `git merge`.
+-   [Zsh]: Add `git get` alias equivalent to a `git fresh` (see below) followed by a `git merge`.
 
 ## 2019-06-17
 
-- [Zsh]: Add `subtree` function (eg. `subtree '*.js'`, or `subtree '*.js|*.ts' src`).
+-   [Zsh]: Add `subtree` function (eg. `subtree '*.js'`, or `subtree '*.js|*.ts' src`).
 
 ## 2019-06-07
 
-- [Vim]: Add `table` snippet for use in Markdown files.
+-   [Vim]: Add `table` snippet for use in Markdown files.
 
 ## 2019-05-23
 
-- [Vim]: Moved plug-in submodules into "[~/.vim/pack/bundle/opt](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.vim/pack/bundle/opt)"
+-   [Vim]: Moved plug-in submodules into "[~/.vim/pack/bundle/opt](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.vim/pack/bundle/opt)"
 
 ## 2019-04-26
 
-- [Zsh]: Add `git fresh` alias for `git remote update --prune`.
+-   [Zsh]: Add `git fresh` alias for `git remote update --prune`.
 
 ## 2019-03-29
 
-- [Zsh]: Make Shift-Tab go to previous completion.
-- [Vim]: Force use of NERDTree instead of netrw when opening Vim with a directory argument.
+-   [Zsh]: Make Shift-Tab go to previous completion.
+-   [Vim]: Force use of NERDTree instead of netrw when opening Vim with a directory argument.
 
 ## 2019-03-27
 
-- [Vim]: Improve set-up for doing merge conflict resolution.
+-   [Vim]: Improve set-up for doing merge conflict resolution.
 
 ## 2019-03-08
 
-- [Zsh]: Add `git wc` alias for `git whatchanged`.
+-   [Zsh]: Add `git wc` alias for `git whatchanged`.
 
 ## 2019-02-27
 
-- [Zsh]: Add `git message` (shortcut: `git msg`) alias.
+-   [Zsh]: Add `git message` (shortcut: `git msg`) alias.
 
 ## 2019-02-15
 
-- [Vim]: Make deoplete rank file-path matches above others.
+-   [Vim]: Make deoplete rank file-path matches above others.
 
 ## 2019-02-14
 
-- [Vim]: `<S-Up>`, `<S-Down>` etc can be used to navigate with location list results.
+-   [Vim]: `<S-Up>`, `<S-Down>` etc can be used to navigate with location list results.
 
 ## 2019-02-07
 
-- [Vim]: Apply tweaks for working in repos that use tabs instead of spaces.
+-   [Vim]: Apply tweaks for working in repos that use tabs instead of spaces.
 
 ## 2019-01-25
 
-- [Vim]: Make improvements for working with TypeScript.
+-   [Vim]: Make improvements for working with TypeScript.
 
 ## 2019-01-23
 
-- [Zsh]: Add `git ff` alias for doing fast-forward merges.
+-   [Zsh]: Add `git ff` alias for doing fast-forward merges.
 
 ## 2019-01-10
 
-- [Vim]: Bind `<C-minus>` to `:NERDTreeFind`, to complement the existing `-` binding that does in-place directory navigation (vim-vinegar style).
+-   [Vim]: Bind `<C-minus>` to `:NERDTreeFind`, to complement the existing `-` binding that does in-place directory navigation (vim-vinegar style).
 
 ## 2019-01-03
 
-- [Vim]: `J` and `K` visual mode mappings now take a count.
+-   [Vim]: `J` and `K` visual mode mappings now take a count.
 
 ## 2018-12-24
 
-- [Zsh]: Running `color` without arguments refreshes the color scheme in the current window.
+-   [Zsh]: Running `color` without arguments refreshes the color scheme in the current window.
 
 ## 2018-12-18
 
-- [Vim,Zsh]: Switched default color scheme to "default-dark".
+-   [Vim,Zsh]: Switched default color scheme to "default-dark".
 
 ## 2018-11-25
 
-- [Zsh]: Overhaul prompt.
+-   [Zsh]: Overhaul prompt.
 
 ## 2018-10-20
 
-- [Vim]: Set up [Goyo](https://github.com/junegunn/goyo.vim).
+-   [Vim]: Set up [Goyo](https://github.com/junegunn/goyo.vim).
 
 ## 2018-03-21
 
-- Set up "fn" and "power" key equivalents on external keyboard.
+-   Set up "fn" and "power" key equivalents on external keyboard.
 
 ## 2018-03-08
 
-- [Zsh]: Add `tw` and `tick` shell utilities.
+-   [Zsh]: Add `tw` and `tick` shell utilities.
 
 ## 2018-03-06
 
-- [Vim]: Prevent `'spelllang'` setting from getting blown away in Markdown files.
+-   [Vim]: Prevent `'spelllang'` setting from getting blown away in Markdown files.
 
 ## 2017-12-28
 
-- Make Caps Lock and Return repeat when held down.
-- Add back SpaceFN layer.
-- [Vim]: Add `<LocalLeader>p` mapping to print the highlight groups that apply at the current cursor position.
+-   Make Caps Lock and Return repeat when held down.
+-   Add back SpaceFN layer.
+-   [Vim]: Add `<LocalLeader>p` mapping to print the highlight groups that apply at the current cursor position.
 
 ## 2017-12-22
 
-- [Vim]: Add "J"/"K" bindings to move visual selection up and down.
-- [Vim]: Add convenience `wincent#debug#log()` function for debugging purposes.
+-   [Vim]: Add "J"/"K" bindings to move visual selection up and down.
+-   [Vim]: Add convenience `wincent#debug#log()` function for debugging purposes.
 
 ## 2017-12-15
 
-- [Mutt]: Add "O" macro to save original message (mnemonic: "[O]riginal").
-- [Mutt]: Add "S" macro to save all attachments (mnemonic: "[S]ave").
+-   [Mutt]: Add "O" macro to save original message (mnemonic: "[O]riginal").
+-   [Mutt]: Add "S" macro to save all attachments (mnemonic: "[S]ave").
 
 ## 2017-12-06
 
-- Use iTerm dynamic profiles to change the font size when an external monitor is present.
+-   Use iTerm dynamic profiles to change the font size when an external monitor is present.
 
 ## 2017-11-08
 
-- [Vim]: Dump YouCompleteMe.
+-   [Vim]: Dump YouCompleteMe.
 
 ## 2017-11-01
 
-- [Mutt]: Mutt now uses different configs for work and personal machines.
+-   [Mutt]: Mutt now uses different configs for work and personal machines.
 
 ## 2017-10-20
 
-- [Zsh]: Add `fzf`-powered functions for finding directories and history entries.
+-   [Zsh]: Add `fzf`-powered functions for finding directories and history entries.
 
 ## 2017-06-16
 
-- Switch to Karabiner-Elements.
+-   Switch to Karabiner-Elements.
 
 ## 2017-06-14
 
-- [Zsh]: Start using zsh-autosuggestions plug-in.
+-   [Zsh]: Start using zsh-autosuggestions plug-in.
 
 ## 2017-06-06
 
-- [Vim]: Neovim is now the default `$EDITOR`.
+-   [Vim]: Neovim is now the default `$EDITOR`.
 
 ## 2017-05-03
 
-- [Zsh]: Prompt now shows `$SHLVL` by repeating the `$` or `#` symbol.
-- [Zsh]: Prompt now indicates the presence of background jobs with a `*`.
-- [Vim]: Now turns off syntax highlighting in inactive splits.
+-   [Zsh]: Prompt now shows `$SHLVL` by repeating the `$` or `#` symbol.
+-   [Zsh]: Prompt now indicates the presence of background jobs with a `*`.
+-   [Vim]: Now turns off syntax highlighting in inactive splits.
 
 ## 2017-02-09
 
-- [Vim]: Use `par` to re-wrap text.
+-   [Vim]: Use `par` to re-wrap text.
 
 ## 2017-01-19
 
-- Get emoji working in the pager.
+-   Get emoji working in the pager.
 
 ## 2016-12-24
 
-- [Mutt]: Use Markdown to send HTML email.
+-   [Mutt]: Use Markdown to send HTML email.
 
 ## 2016-12-16
 
-- [Mutt]: Add retry with exponential backoff to mail sync script.
+-   [Mutt]: Add retry with exponential backoff to mail sync script.
 
 ## 2016-12-14
 
-- Colorize man pages.
+-   Colorize man pages.
 
 ## 2016-12-13
 
-- [Mutt]: Switched from `offlineimap` to `mbsync` (in the `isync` package) for mail synchronization.
+-   [Mutt]: Switched from `offlineimap` to `mbsync` (in the `isync` package) for mail synchronization.
 
 ## 2016-12-12
 
-- [Mutt]: Added address autocompletion (via custom YouCompleteMe completer) inside Vim buffers of with filetype "mail".
+-   [Mutt]: Added address autocompletion (via custom YouCompleteMe completer) inside Vim buffers of with filetype "mail".
 
 ## 2016-12-11
 
-- [Mutt]: Switched from `contacts` to `lbdb` for searching contacts.
+-   [Mutt]: Switched from `contacts` to `lbdb` for searching contacts.
 
 ## 2016-12-07
 
-- [Mutt]: Switched from `w3m` to `elinks` for viewing links within emails.
+-   [Mutt]: Switched from `w3m` to `elinks` for viewing links within emails.
 
 ## 2016-12-02
 
-- [Mutt]: Added `mutt` config.
+-   [Mutt]: Added `mutt` config.
 
 ## 2016-11-30
 
-- [Vim]: Fine-tuned startup performance from 500ms down to 150ms.
+-   [Vim]: Fine-tuned startup performance from 500ms down to 150ms.
 
 ## 2016-11-29
 
-- [Vim,Zsh]: Updated base16 dependencies, which means that the existing `dark`/`light` scheme names no longer apply. Instead of `dark tomorrow` (`color dark tomorrow`) or `light tomorrow` (`color light tomorrow`), run `color tomorrow-night` and `color tomorrow`. Note that some schemes [no longer have light variants](https://github.com/chriskempson/base16/issues/42) at all. `color` continues to show currently configured scheme information and `color help` shows a list of all available colors.
+-   [Vim,Zsh]: Updated base16 dependencies, which means that the existing `dark`/`light` scheme names no longer apply. Instead of `dark tomorrow` (`color dark tomorrow`) or `light tomorrow` (`color light tomorrow`), run `color tomorrow-night` and `color tomorrow`. Note that some schemes [no longer have light variants](https://github.com/chriskempson/base16/issues/42) at all. `color` continues to show currently configured scheme information and `color help` shows a list of all available colors.
 
 ## 2016-11-28
 
-- Removed BSD license and replaced with public domain dedication.
+-   Removed BSD license and replaced with public domain dedication.
 
 ## 2016-11-22
 
-- Replaced Karabiner configuration with custom Hammerspoon configuration, because Karabiner does not work on macOS Sierra.
-  - Features that survived translation:
-    - `<Capslock>` and `<Return>` retain their dual-purpose functionalities.
-    - `<Tab>` and `<C-i>` can still be mapped independently in the terminal.
-  - Features that have not yet been ported:
-    - "SpaceFN" layer.
-  - Features unlikely to be ported due to technical constraints:
-    - `<Shift>` control over Caps Lock state.
-  - Features that will not be ported because they can be solved by other means:
-    - Remapping of YubiKey to work with Colemak.
+-   Replaced Karabiner configuration with custom Hammerspoon configuration, because Karabiner does not work on macOS Sierra.
+    -   Features that survived translation:
+        -   `<Capslock>` and `<Return>` retain their dual-purpose functionalities.
+        -   `<Tab>` and `<C-i>` can still be mapped independently in the terminal.
+    -   Features that have not yet been ported:
+        -   "SpaceFN" layer.
+    -   Features unlikely to be ported due to technical constraints:
+        -   `<Shift>` control over Caps Lock state.
+    -   Features that will not be ported because they can be solved by other means:
+        -   Remapping of YubiKey to work with Colemak.
 
 ## 2016-11-14
 
-- [Vim]: Add "breakpoints" to statusline that reduce the amount of information displayed as window width decreases.
-- [Vim]: Extracted macro replay functionality into a separate plug-in, [Replay](https://github.com/wincent/replay).
+-   [Vim]: Add "breakpoints" to statusline that reduce the amount of information displayed as window width decreases.
+-   [Vim]: Extracted macro replay functionality into a separate plug-in, [Replay](https://github.com/wincent/replay).
 
 ## 2016-11-10
 
-- [tmux]: Adding `<Prefix>-b` binding to jump back to previous shell prompt.
+-   [tmux]: Adding `<Prefix>-b` binding to jump back to previous shell prompt.
 
 ## 2016-10-12
 
-- [tmux]: Change color of active/inactive panes to make currently active pane more obvious.
+-   [tmux]: Change color of active/inactive panes to make currently active pane more obvious.
 
 ## 2016-10-11
 
-- Manage most of the macOS preferences via the Ansible `osx_defaults` module, instead of custom Ansible `command` tasks.
+-   Manage most of the macOS preferences via the Ansible `osx_defaults` module, instead of custom Ansible `command` tasks.
 
 ## 2016-10-07
 
-- [Zsh]: Make `C-z` run `fg` at the shell prompt.
+-   [Zsh]: Make `C-z` run `fg` at the shell prompt.
 
 ## 2016-07-05
 
-- [Zsh]: Bounce Dock icon when a shell command finishes running and the terminal is in the background.
+-   [Zsh]: Bounce Dock icon when a shell command finishes running and the terminal is in the background.
 
 ## 2016-06-03
 
-- Set up [Clipper](https://github.com/wincent/clipper) to work via UNIX domain sockets rather than TCP ports for better security.
+-   Set up [Clipper](https://github.com/wincent/clipper) to work via UNIX domain sockets rather than TCP ports for better security.
 
 ## 2016-05-11
 
-- [Vim,Zsh]: Switched default color scheme to "tomorrow-dark" (later renamed to "tomorrow-night").
+-   [Vim,Zsh]: Switched default color scheme to "tomorrow-dark" (later renamed to "tomorrow-night").
 
 ## 2016-05-09
 
-- [Vim]: Use `<Tab>` to toggle folds.
+-   [Vim]: Use `<Tab>` to toggle folds.
 
 ## 2016-05-02
 
-- Work around lengthy hangs running Ansible on macOS.
+-   Work around lengthy hangs running Ansible on macOS.
 
 ## 2016-04-29
 
-- [Vim]: Extracted within-file find-and-replace enhancements into a separate plug-in, [Scalpel](https://github.com/wincent/scalpel).
+-   [Vim]: Extracted within-file find-and-replace enhancements into a separate plug-in, [Scalpel](https://github.com/wincent/scalpel).
 
 ## 2016-04-10
 
-- [Vim]: Use "Powerline" glyphs to make statusline a little prettier.
-- Switch to Adobe Source Code Pro font.
+-   [Vim]: Use "Powerline" glyphs to make statusline a little prettier.
+-   Switch to Adobe Source Code Pro font.
index 1788995d2abb3b4227430afe4b288c30478c165f..071718eb0377abb618c72d18ffaacd64424eb2f2 100644 (file)
--- a/README.md
+++ b/README.md
@@ -4,12 +4,12 @@
 
 ![](https://raw.githubusercontent.com/wincent/wincent/media/screenshot.png)
 
-- Target platforms: macOS and Red Hat-like Linuxes (eg. CentOS).
-- Set-up method: ~~Beautiful and intricate snowflake~~ incredibly over-engineered [Ansible](https://www.ansible.com/) orchestration.
-- Visible in the screenshot:
-  - [default-dark Base16](http://chriskempson.com/projects/base16/) color scheme.
-  - [Adobe Source Code Pro](https://github.com/adobe-fonts/source-code-pro) (Light) font.
-  - Vim, running inside tmux, inside iTerm2, on macOS "High Sierra".
+-   Target platforms: macOS and Red Hat-like Linuxes (eg. CentOS).
+-   Set-up method: ~~Beautiful and intricate snowflake~~ incredibly over-engineered [Ansible](https://www.ansible.com/) orchestration.
+-   Visible in the screenshot:
+    -   [default-dark Base16](http://chriskempson.com/projects/base16/) color scheme.
+    -   [Adobe Source Code Pro](https://github.com/adobe-fonts/source-code-pro) (Light) font.
+    -   Vim, running inside tmux, inside iTerm2, on macOS "High Sierra".
 
 ## Features
 
 
 [A set of dotfiles](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files) that I've been tweaking and twiddling since the early 2000s ([under version control](https://github.com/wincent/wincent/commit/61a7e2a830edb7) since 2009). Characteristics include:
 
-- Sane Vim pasting via bracketed paste mode.
-- Write access to local clipboard from local and remote hosts, inside and outside of tmux (via [Clipper](https://github.com/wincent/clipper)).
-- Full mouse support (pane/split resizing, scrolling, text selection) in Vim and tmux.
-- Focus/lost events for Vim inside tmux.
-- Cursor shape toggles on entering Vim.
-- Italics in the terminal.
-- Bundles a (not-excessive) number of [useful Vim plug-ins](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.vim/pack).
-- Conservative Vim configuration (very few overrides of core functionality; most changes are unobtrusive enhancements; some additional functionality exposed via `<Leader>` and `<LocalLeader>` mappings.
-- Relatively restrained Zsh config, Bash-like but with a few Zsh perks, such as right-side prompt, auto-cd hooks, command elapsed time printing and such.
-- Unified color-handling (across iTerm2 and Vim) via [Base16 Shell](https://github.com/chriskempson/base16-shell).
-- Encrypted versioning of files with sensitive content (via [git-cipher](https://github.com/wincent/git-cipher)).
-- Comprehensive [Hammerspoon](http://www.hammerspoon.org/) [config](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.hammerspoon).
+-   Sane Vim pasting via bracketed paste mode.
+-   Write access to local clipboard from local and remote hosts, inside and outside of tmux (via [Clipper](https://github.com/wincent/clipper)).
+-   Full mouse support (pane/split resizing, scrolling, text selection) in Vim and tmux.
+-   Focus/lost events for Vim inside tmux.
+-   Cursor shape toggles on entering Vim.
+-   Italics in the terminal.
+-   Bundles a (not-excessive) number of [useful Vim plug-ins](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.vim/pack).
+-   Conservative Vim configuration (very few overrides of core functionality; most changes are unobtrusive enhancements; some additional functionality exposed via `<Leader>` and `<LocalLeader>` mappings.
+-   Relatively restrained Zsh config, Bash-like but with a few Zsh perks, such as right-side prompt, auto-cd hooks, command elapsed time printing and such.
+-   Unified color-handling (across iTerm2 and Vim) via [Base16 Shell](https://github.com/chriskempson/base16-shell).
+-   Encrypted versioning of files with sensitive content (via [git-cipher](https://github.com/wincent/git-cipher)).
+-   Comprehensive [Hammerspoon](http://www.hammerspoon.org/) [config](https://github.com/wincent/wincent/tree/master/roles/dotfiles/files/.hammerspoon).
 
 ### Homebrew
 
@@ -38,33 +38,33 @@ On macOS, [the `homebrew` role](https://github.com/wincent/wincent/tree/master/r
 
 On macOS, [Karabiner-Elements](https://github.com/tekezo/Karabiner-Elements/) is used for the following:
 
-- Make Caps Lock serve as Backspace (when tapped) and Left Control (when chorded with another key). When held down alone, Caps Lock fires repeated Backspace events.
-- Make Return serve as Return (when tapped) and Right Control (when chorded with another key). When held down alone, Return fires repeated Return events.
-- Maps Control-I to F6 (only in MacVim and the terminal) so that it can be mapped independently from Tab in Vim.
-- Toggle Caps Lock on by tapping both Shift keys simultaneously.
-- Makes the function keys on my external Realforce keyboard behave like the "media" keys on Apple's keyboards.
-- Swap Option and Command keys on my external Realforce keyboard.
-- Make the "application" key (extra modifier key on right-hand side) behave as "fn" on Realforce keyboard.
-- Make "pause" (at far-right of function key row) behave as "power" (effectively, sleep) on Realforce keyboard.
-- Adds a "SpaceFN" layer that can be activated by holding down Space while hitting other keys; I use this to make the cursor keys available on or near the home row in any app.
+-   Make Caps Lock serve as Backspace (when tapped) and Left Control (when chorded with another key). When held down alone, Caps Lock fires repeated Backspace events.
+-   Make Return serve as Return (when tapped) and Right Control (when chorded with another key). When held down alone, Return fires repeated Return events.
+-   Maps Control-I to F6 (only in MacVim and the terminal) so that it can be mapped independently from Tab in Vim.
+-   Toggle Caps Lock on by tapping both Shift keys simultaneously.
+-   Makes the function keys on my external Realforce keyboard behave like the "media" keys on Apple's keyboards.
+-   Swap Option and Command keys on my external Realforce keyboard.
+-   Make the "application" key (extra modifier key on right-hand side) behave as "fn" on Realforce keyboard.
+-   Make "pause" (at far-right of function key row) behave as "power" (effectively, sleep) on Realforce keyboard.
+-   Adds a "SpaceFN" layer that can be activated by holding down Space while hitting other keys; I use this to make the cursor keys available on or near the home row in any app.
 
 ### Zsh
 
 #### Functions
 
-- `ag`: Transparently wraps the `ag` executable so as to provide a centralized place to set defaults for that command (seeing as it has no "rc" file).
-- `bounce`: bounce the macOS Dock icon if the terminal is not in the foreground.
-- `color`: change terminal and Vim color scheme.
-- `email`: convenience wrapper to spawn (or attach to) a tmux session running `mutt` and `mbsync`.
-- `fd`: "find directory" using fast `bfs` and `sk`; automatically `cd`s into the selected directory.
-- `fh`: "find [in] history"; selecting a history item inserts it into the command line but does not execute it.
-- `history`: overrides the (tiny) default history count.
-- `jump` (aliased to `j`): to jump to hashed directories.
-- `regmv`: bulk-rename files (eg. `regmv '/\.tif$/.tiff/' *`).
-- `scratch`: create a random temporary scratch directory and `cd` into it.
-- `tick`: moves an existing time warp (eg. `tick +1h`); see `tw` below for a description of time warp.
-- `tmux`: wrapper that reattches to pre-existing sessions, or creates new ones based on the current directory name; additionally, looks for a `.tmux` file to set up windows and panes (note that the first time a given `.tmux` file is encountered the wrapper asks the user whether to trust or skip it).
-- `tw` ("time warp"): overrides `GIT_AUTHOR_DATE` and `GIT_COMMITTER_DATE` (eg. `tw -1d`).
+-   `ag`: Transparently wraps the `ag` executable so as to provide a centralized place to set defaults for that command (seeing as it has no "rc" file).
+-   `bounce`: bounce the macOS Dock icon if the terminal is not in the foreground.
+-   `color`: change terminal and Vim color scheme.
+-   `email`: convenience wrapper to spawn (or attach to) a tmux session running `mutt` and `mbsync`.
+-   `fd`: "find directory" using fast `bfs` and `sk`; automatically `cd`s into the selected directory.
+-   `fh`: "find [in] history"; selecting a history item inserts it into the command line but does not execute it.
+-   `history`: overrides the (tiny) default history count.
+-   `jump` (aliased to `j`): to jump to hashed directories.
+-   `regmv`: bulk-rename files (eg. `regmv '/\.tif$/.tiff/' *`).
+-   `scratch`: create a random temporary scratch directory and `cd` into it.
+-   `tick`: moves an existing time warp (eg. `tick +1h`); see `tw` below for a description of time warp.
+-   `tmux`: wrapper that reattches to pre-existing sessions, or creates new ones based on the current directory name; additionally, looks for a `.tmux` file to set up windows and panes (note that the first time a given `.tmux` file is encountered the wrapper asks the user whether to trust or skip it).
+-   `tw` ("time warp"): overrides `GIT_AUTHOR_DATE` and `GIT_COMMITTER_DATE` (eg. `tw -1d`).
 
 #### Prompt
 
@@ -74,17 +74,17 @@ Zsh is configured with the following prompt:
 
 Visible here are:
 
-- Concise left-hand prompt consisting of:
-  - Last component of current directory (abbreviates `$HOME` to `~` if possible).
-  - Prompt marker, `❯`, the "[HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT](https://codepoints.net/U+276F)" (that's `\u276f`, or `e2 9d af` in UTF-8).
-- Extended right-hand size prompt which auto-hides when necessary to make room for long commands and contains:
-  - Duration of previous command in adaptive units (seconds, minutes, hours, days, depending on duration).
-  - Current version control branch name.
-  - Current version control worktree status using colors that match those used in `git status`:
-    - Green dot indicates staged changes.
-    - Red dot indicates unstaged changes.
-    - Blue dot indicates untracked files.
-  - Full version of current working directory (again, abbreviating `$HOME` to `~`).
+-   Concise left-hand prompt consisting of:
+    -   Last component of current directory (abbreviates `$HOME` to `~` if possible).
+    -   Prompt marker, `❯`, the "[HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT](https://codepoints.net/U+276F)" (that's `\u276f`, or `e2 9d af` in UTF-8).
+-   Extended right-hand size prompt which auto-hides when necessary to make room for long commands and contains:
+    -   Duration of previous command in adaptive units (seconds, minutes, hours, days, depending on duration).
+    -   Current version control branch name.
+    -   Current version control worktree status using colors that match those used in `git status`:
+        -   Green dot indicates staged changes.
+        -   Red dot indicates unstaged changes.
+        -   Blue dot indicates untracked files.
+    -   Full version of current working directory (again, abbreviating `$HOME` to `~`).
 
 Nested shells are indicated with additional prompt characters. For example, one nested shell:
 
@@ -118,102 +118,102 @@ If there are background processes, a yellow asterisk is shown:
 
 A number of tools are used to provide command-line access to Gmail and Office IMAP accounts.
 
-- [mutt](http://www.mutt.org/): For reading email.
-- [isync](http://isync.sourceforge.net/): For maintaining a local cache of messages for offline access.
-- [notmuch](https://notmuchmail.org/): For fast search.
-- [msmtp](http://msmtp.sourceforge.net/): For sending email.
-- [elinks](http://elinks.or.cz/): For viewing HTML emails.
-- [urlview](https://packages.debian.org/sid/misc/urlview): For opening URLs from inside mutt.
-- [terminal-notifier](https://github.com/julienXX/terminal-notifier): For notifications.
-- [lbdb](https://www.spinnaker.de/lbdb/): Contact autocompletion drawing from a number of sources, such as previous messages, aliases, and macOS Contacts (which can be configured to synchronize Google contacts as well).
-- [imapfilter](https://github.com/lefcha/imapfilter/): For filtering.
-- [passage](https://github.com/wincent/passage): For mediating interaction with the macOS keychain.
+-   [mutt](http://www.mutt.org/): For reading email.
+-   [isync](http://isync.sourceforge.net/): For maintaining a local cache of messages for offline access.
+-   [notmuch](https://notmuchmail.org/): For fast search.
+-   [msmtp](http://msmtp.sourceforge.net/): For sending email.
+-   [elinks](http://elinks.or.cz/): For viewing HTML emails.
+-   [urlview](https://packages.debian.org/sid/misc/urlview): For opening URLs from inside mutt.
+-   [terminal-notifier](https://github.com/julienXX/terminal-notifier): For notifications.
+-   [lbdb](https://www.spinnaker.de/lbdb/): Contact autocompletion drawing from a number of sources, such as previous messages, aliases, and macOS Contacts (which can be configured to synchronize Google contacts as well).
+-   [imapfilter](https://github.com/lefcha/imapfilter/): For filtering.
+-   [passage](https://github.com/wincent/passage): For mediating interaction with the macOS keychain.
 
 In order for all this to work, a few items have to be stored in the macOS keychain:
 
-- A "generic" (A.K.A. "application") keychain items (that is, without protocols, only hostnames):
-  - "Keychain Item Name": example.net (corresponds to the "host" field in `~/.msmtprc`, and "Host" field in `~/.mbsyncrc`).
-  - "Account Name": username+mutt@example.net (corresponds to the "user" field in `~/.msmtprc`, and "PassCmd" field in `~/.mbsynrc`).
+-   A "generic" (A.K.A. "application") keychain items (that is, without protocols, only hostnames):
+    -   "Keychain Item Name": example.net (corresponds to the "host" field in `~/.msmtprc`, and "Host" field in `~/.mbsyncrc`).
+    -   "Account Name": username+mutt@example.net (corresponds to the "user" field in `~/.msmtprc`, and "PassCmd" field in `~/.mbsynrc`).
 
 The following Gmail-like/Vim-like bindings are configured:
 
-- `e`: Archive (but note: leaves copy of mail in mailbox until next sync; force an immediate sync with `$`).
-- `#`: Trash mail.
-- `!`: Mark as spam.
-- `gi`: Go to inbox.
-- `ga`: Go to archive.
-- `gt`: Go to sent mail.
-- `gd`: Go to drafts.
-- `gs`: Go to starred mail.
-- `gl`: Go to a label (folder).
-- `x`: Toggle selection on entry (see also `t`).
-- `c`: Compose new message.
-- `s`: Toggle star.
-- `*a`: Select all.
-- `*n`: Deselect all (mnemonic: "select none").
-- `*r`: Select read messages.
-- `*u`: Select unread messages.
-- `Shift-U`: Mark as unread.
-- `Shift-I`: Mark as read.
+-   `e`: Archive (but note: leaves copy of mail in mailbox until next sync; force an immediate sync with `$`).
+-   `#`: Trash mail.
+-   `!`: Mark as spam.
+-   `gi`: Go to inbox.
+-   `ga`: Go to archive.
+-   `gt`: Go to sent mail.
+-   `gd`: Go to drafts.
+-   `gs`: Go to starred mail.
+-   `gl`: Go to a label (folder).
+-   `x`: Toggle selection on entry (see also `t`).
+-   `c`: Compose new message.
+-   `s`: Toggle star.
+-   `*a`: Select all.
+-   `*n`: Deselect all (mnemonic: "select none").
+-   `*r`: Select read messages.
+-   `*u`: Select unread messages.
+-   `Shift-U`: Mark as unread.
+-   `Shift-I`: Mark as read.
 
 Standard `mutt` stuff:
 
-- `v`: View attachments (including alternate parts for a multipart message).
+-   `v`: View attachments (including alternate parts for a multipart message).
 
 Non-Gmail extensions:
 
-- `t`: Toggle selection on entire thread (see also `x`).
-- `A`: Show alternate MIME-type in MIME-multipart messages.
-- `O`: Save original message.
-- `S`: Search all using [Xapian query syntax](https://xapian.org/docs/queryparser.html) ([notmuch-specific reference documentation](https://notmuchmail.org/manpages/notmuch-search-terms-7/)):
-  - `+foo`: Must include "foo".
-  - `-bar`: Must not include "bar".
-  - `AND`, `OR`, `NOT`, `XOR`: Self-evident.
-  - `foo NEAR bar`: "foo" within 10 words of "bar" (order-independent).
-  - `foo ADJ bar`: Like `NEAR`, but "foo" must appear earlier than "bar".
-  - `"foo bar"`: Match entire phrase.
-  - `foo*`: Match "foo", "food", "foobar" etc.
-  - `subject:this`, `subject:"one two"` (two consecutive words), `subject:(one two)` (either or both words anywhere in subject), `subject:one AND subject:two` (both words anywhere in subject).
-  - `subject:/regex.*/` (but note, quotes are needed for patterns containing spaces; eg. `subject:"/a b/"`).
-  - `from:john`, `from:me@example.com`
-  - `to:john`, `to:me@example.com`
-  - `date:today`
-  - `date:yesterday`
-  - `date:3d` (exactly 3 days ago)
-  - `date:14d..7d` (a week ago)
-  - `date:10d..` (since 10 days ago)
-  - `date:..3d` (until 3 days ago)
-  - `date:"last week"` (preceding Monday through Sunday)
-  - `date:"this week"` or `date:this_week` or `date:this-week` (Monday to present day)
-  - `date:"last year"` (also works with `years`, `months`, `hours`/`hrs`, `minutes`/`mins`, `seconds`/`secs` etc).
-  - `date:june`
-  - `date:2018-06-01`
-  - `is:{tag}`: eg. `is:unread`, `is:flagged` (ie. starred); to see all tags, run `notmuch search --output=tags '*'`:
-    - `attachment`
-    - `flagged`
-    - `inbox` (not very meaningful as _everything_ gets this tag when indexed via `notmuch new`)
-    - `replied`
-    - `signed`
-    - `unread`
-  - `id:messageId@example.net` (search by Message-Id).
-- `l`: Limit listed messages:
-  - `~f bob` (from bob)
-  - `~s foo` (subject contains "foo"; "Foo" would search case-sensitively)
-  - `~s foo.+bar` (subject contains pattern)
-  - `!~s foo` (subject does not contain "foo")
-  - `~d >1m` (messages more than 1 month old)
-- `\u`: Open list of URLs in message (via `urlview`).
-- `b`: Toggle (mailboxes) sidebar.
-- `m`: Move message(s).
+-   `t`: Toggle selection on entire thread (see also `x`).
+-   `A`: Show alternate MIME-type in MIME-multipart messages.
+-   `O`: Save original message.
+-   `S`: Search all using [Xapian query syntax](https://xapian.org/docs/queryparser.html) ([notmuch-specific reference documentation](https://notmuchmail.org/manpages/notmuch-search-terms-7/)):
+    -   `+foo`: Must include "foo".
+    -   `-bar`: Must not include "bar".
+    -   `AND`, `OR`, `NOT`, `XOR`: Self-evident.
+    -   `foo NEAR bar`: "foo" within 10 words of "bar" (order-independent).
+    -   `foo ADJ bar`: Like `NEAR`, but "foo" must appear earlier than "bar".
+    -   `"foo bar"`: Match entire phrase.
+    -   `foo*`: Match "foo", "food", "foobar" etc.
+    -   `subject:this`, `subject:"one two"` (two consecutive words), `subject:(one two)` (either or both words anywhere in subject), `subject:one AND subject:two` (both words anywhere in subject).
+    -   `subject:/regex.*/` (but note, quotes are needed for patterns containing spaces; eg. `subject:"/a b/"`).
+    -   `from:john`, `from:me@example.com`
+    -   `to:john`, `to:me@example.com`
+    -   `date:today`
+    -   `date:yesterday`
+    -   `date:3d` (exactly 3 days ago)
+    -   `date:14d..7d` (a week ago)
+    -   `date:10d..` (since 10 days ago)
+    -   `date:..3d` (until 3 days ago)
+    -   `date:"last week"` (preceding Monday through Sunday)
+    -   `date:"this week"` or `date:this_week` or `date:this-week` (Monday to present day)
+    -   `date:"last year"` (also works with `years`, `months`, `hours`/`hrs`, `minutes`/`mins`, `seconds`/`secs` etc).
+    -   `date:june`
+    -   `date:2018-06-01`
+    -   `is:{tag}`: eg. `is:unread`, `is:flagged` (ie. starred); to see all tags, run `notmuch search --output=tags '*'`:
+        -   `attachment`
+        -   `flagged`
+        -   `inbox` (not very meaningful as _everything_ gets this tag when indexed via `notmuch new`)
+        -   `replied`
+        -   `signed`
+        -   `unread`
+    -   `id:messageId@example.net` (search by Message-Id).
+-   `l`: Limit listed messages:
+    -   `~f bob` (from bob)
+    -   `~s foo` (subject contains "foo"; "Foo" would search case-sensitively)
+    -   `~s foo.+bar` (subject contains pattern)
+    -   `!~s foo` (subject does not contain "foo")
+    -   `~d >1m` (messages more than 1 month old)
+-   `\u`: Open list of URLs in message (via `urlview`).
+-   `b`: Toggle (mailboxes) sidebar.
+-   `m`: Move message(s).
 
 Other stuff:
 
-- `<Tab>` autocompletes addresses from the lbdb database.
-- `<C-t>` autocompletes aliases.
+-   `<Tab>` autocompletes addresses from the lbdb database.
+-   `<C-t>` autocompletes aliases.
 
 Attachment menu bindings:
 
-- `S`: Save all attachments.
+-   `S`: Save all attachments.
 
 To have `mailto` links open up in `mutt` in iTerm:
 
@@ -222,20 +222,20 @@ To have `mailto` links open up in `mutt` in iTerm:
 
 Notes:
 
-- `$$URL$$` is documented [here](https://groups.google.com/d/msg/iterm2-discuss/TFPl1D_miIU/uDVV2ZZpYWQJ).
-- The convoluted use of `env` and `zsh` is required to get terminal colors working correctly.
+-   `$$URL$$` is documented [here](https://groups.google.com/d/msg/iterm2-discuss/TFPl1D_miIU/uDVV2ZZpYWQJ).
+-   The convoluted use of `env` and `zsh` is required to get terminal colors working correctly.
 
 ## Dependencies
 
-- [tmux](http://tmux.sourceforge.net/) 2.3 or later.
-- [Neovim](https://neovim.io) or [Vim](http://www.vim.org/) 8.0 or later with Ruby and Python support (although there's a reasonable amount of feature detection in order to degrade gracefully).
-- Relatively recent [Zsh](http://www.zsh.org/).
-- Relatively recent [Git](http://git-scm.com/).
-- [Clipper](https://wincent.com/products/clipper) for transparent access to the local system clipboard.
-- On macOS, [iTerm2](http://www.iterm2.com/). Additionally, only the latest version of macOS (although at the time of writing, I'm still on High Sierra) gets actively tested.
-- [Python](https://www.python.org/) to perform setup via the included `install` command.
-- [Ruby](https://www.ruby-lang.org/).
-- [Adobe Source Code Pro](https://github.com/adobe-fonts/source-code-pro) or any other fixed-width font that includes the [Powerline glyphs](http://powerline.readthedocs.io/en/master/installation.html#fonts-installation).
+-   [tmux](http://tmux.sourceforge.net/) 2.3 or later.
+-   [Neovim](https://neovim.io) or [Vim](http://www.vim.org/) 8.0 or later with Ruby and Python support (although there's a reasonable amount of feature detection in order to degrade gracefully).
+-   Relatively recent [Zsh](http://www.zsh.org/).
+-   Relatively recent [Git](http://git-scm.com/).
+-   [Clipper](https://wincent.com/products/clipper) for transparent access to the local system clipboard.
+-   On macOS, [iTerm2](http://www.iterm2.com/). Additionally, only the latest version of macOS (although at the time of writing, I'm still on High Sierra) gets actively tested.
+-   [Python](https://www.python.org/) to perform setup via the included `install` command.
+-   [Ruby](https://www.ruby-lang.org/).
+-   [Adobe Source Code Pro](https://github.com/adobe-fonts/source-code-pro) or any other fixed-width font that includes the [Powerline glyphs](http://powerline.readthedocs.io/en/master/installation.html#fonts-installation).
 
 ## Installation
 
@@ -328,7 +328,7 @@ You can also inspect variables by adding a task that uses the "debug" module in
 
 - name: debugging bad stat info
   debug:
-    var: stat_result
+      var: stat_result
 ```
 
 Note that for convenience, "debug" tasks have already been inserted for all variables that are `register`-ed in the existing roles, with verbosity thresholds of 2, meaning that they will be logged automatically when the install is run using `./install -vv` or more.
@@ -385,8 +385,8 @@ LC_ALL=en_US.UTF-8
 
 The goal was to replace Ansible with some handmade scripts using the smallest dependency graph possible. I original [tried](https://github.com/wincent/wincent/commit/8809a1681cfd8fd02eb40113d2485d7cadc10e4c) out [Deno](https://deno.land/) because that would enable me to use TypeScript with no dependencies outside of Deno itself, however I [gave up on that](https://github.com/wincent/wincent/commit/a213ddf69d3213882808b5c5ff0e000bcd83fe98) when I saw that editor integration was still very nascent. So I went with the following:
 
-- [n](https://github.com/tj/n) ([as a submodule](https://github.com/wincent/wincent/tree/master/vendor)) and some [hand-rolled Bash scripts](https://github.com/wincent/wincent/tree/master/bin) to replace [virtualenv](https://virtualenv.pypa.io/) and friends ([Python](https://www.python.org/), [pip](https://pypi.org/project/pip/)).
-- [Yarn](https://github.com/yarnpkg/yarn/) ([vendored](https://github.com/wincent/wincent/commit/26adf86d4c742390537be4dc1572f93a97bc3e68)) to install [TypeScript](https://www.typescriptlang.org/).
+-   [n](https://github.com/tj/n) ([as a submodule](https://github.com/wincent/wincent/tree/master/vendor)) and some [hand-rolled Bash scripts](https://github.com/wincent/wincent/tree/master/bin) to replace [virtualenv](https://virtualenv.pypa.io/) and friends ([Python](https://www.python.org/), [pip](https://pypi.org/project/pip/)).
+-   [Yarn](https://github.com/yarnpkg/yarn/) ([vendored](https://github.com/wincent/wincent/commit/26adf86d4c742390537be4dc1572f93a97bc3e68)) to install [TypeScript](https://www.typescriptlang.org/).
 
 Beyond that, there are no dependencies outside of the [Node.js](https://nodejs.org/en/) standard library. I use [Prettier](https://prettier.io/) to format code, but I invoke it via `npx` which means the [yarn.lock](https://github.com/wincent/wincent/blob/master/yarn.lock) remains basically empty. Ansible itself is replaced by [a set of self-contained TypeScript scripts](https://github.com/wincent/wincent/tree/master/src). Instead of YAML configuration files containing "declarative" configuration peppered with snippets of Python, we just use TypeScript for everything. Instead of [Jinja templates](https://jinja.palletsprojects.com/), we use ERB/JSP-like templates that use embedded JavaScript when necessary.
 
@@ -417,13 +417,13 @@ Unless otherwise noted, the contents of this repo are in the public domain. See
 
 The repo is written and maintained by Greg Hurrell &lt;[greg@hurrell.net](mailto:greg@hurrell.net)&gt;. Other contributors that have submitted patches include, in alphabetical order:
 
-- Joe Lencioni
-- Jonathan Wilkins
-- Mark Stenglein
-- Matthew Byrne
-- Stone C. Lasley
-- Victor Igor
-- Zac Collier
+-   Joe Lencioni
+-   Jonathan Wilkins
+-   Mark Stenglein
+-   Matthew Byrne
+-   Stone C. Lasley
+-   Victor Igor
+-   Zac Collier
 
 This list produced with:
 
index e47f3803b30fb94aa9f081b43f0de02b9afe2ec3..1e81d51b9fed94804f4dd142e45ed7a1e24c0a47 100644 (file)
@@ -1,3 +1,3 @@
 {
-  "description": "Configures launchd"
+    "description": "Configures launchd"
 }
index 30be38d59fb5f06832a71625327d24a6576734f1..890537f86895849a3289786fbd127ce76b2259d1 100644 (file)
@@ -1,59 +1,59 @@
 import {resource, template, task} from '../../src/Fig';
 
 task('configure (global) LaunchDaemons', async () => {
-  const items = [
-    {
-      path: '/Library/LaunchDaemons/limit.maxfiles.plist',
-      variables: {
-        arguments: ['limit', 'maxfiles', 65536, 65536],
-        label: 'limit.maxfiles',
-      },
-    },
-    {
-      path: '/Library/LaunchDaemons/limit.maxproc.plist',
-      variables: {
-        arguments: ['limit', 'maxproc', 2048, 2048],
-        label: 'limit.maxproc',
-      },
-    },
-  ];
+    const items = [
+        {
+            path: '/Library/LaunchDaemons/limit.maxfiles.plist',
+            variables: {
+                arguments: ['limit', 'maxfiles', 65536, 65536],
+                label: 'limit.maxfiles',
+            },
+        },
+        {
+            path: '/Library/LaunchDaemons/limit.maxproc.plist',
+            variables: {
+                arguments: ['limit', 'maxproc', 2048, 2048],
+                label: 'limit.maxproc',
+            },
+        },
+    ];
 
-  for (const {path, variables} of items) {
-    await template({
-      group: 'wheel',
-      mode: '0644',
-      owner: 'root',
-      path,
-      src: resource.template('run.plist.erb'),
-      variables,
-    });
-  }
+    for (const {path, variables} of items) {
+        await template({
+            group: 'wheel',
+            mode: '0644',
+            owner: 'root',
+            path,
+            src: resource.template('run.plist.erb'),
+            variables,
+        });
+    }
 });
 
 task('configure (local) LaunchAgents', async () => {
-  const items = [
-    {
-      path: '~/Library/LaunchAgents/setenv.lang.plist',
-      variables: {
-        arguments: ['setenv', 'LANG', 'en_US.UTF-8'],
-        label: 'setenv.lang',
-      },
-    },
-    {
-      path: '~/Library/LaunchAgents/setenv.lc_time.plist',
-      variables: {
-        arguments: ['setenv', 'LC_TIME', 'en_AU.UTF-8'],
-        label: 'setenv.lc_time',
-      },
-    },
-  ];
+    const items = [
+        {
+            path: '~/Library/LaunchAgents/setenv.lang.plist',
+            variables: {
+                arguments: ['setenv', 'LANG', 'en_US.UTF-8'],
+                label: 'setenv.lang',
+            },
+        },
+        {
+            path: '~/Library/LaunchAgents/setenv.lc_time.plist',
+            variables: {
+                arguments: ['setenv', 'LC_TIME', 'en_AU.UTF-8'],
+                label: 'setenv.lc_time',
+            },
+        },
+    ];
 
-  for (const {path, variables} of items) {
-    await template({
-      mode: '0644',
-      path,
-      src: resource.template('run.plist.erb'),
-      variables,
-    });
-  }
+    for (const {path, variables} of items) {
+        await template({
+            mode: '0644',
+            path,
+            src: resource.template('run.plist.erb'),
+            variables,
+        });
+    }
 });
index add0b224e47def94616163f9abad57e384c91766..b2ba8a6d7a76abe4c081e1c4e5fd6f43c87e5c2d 100644 (file)
@@ -1,6 +1,6 @@
 {
-  "description": "Sets up terminfo database entries for italics and 256-color support",
-  "variables": {
-    "terminfo_path": "~/.terminfo"
-  }
+    "description": "Sets up terminfo database entries for italics and 256-color support",
+    "variables": {
+        "terminfo_path": "~/.terminfo"
+    }
 }
index 4b1f131320ae496b42bf8e7074262c126113ac18..b7c3e87f838364322b11d57e7257ef445a5bd568 100644 (file)
@@ -1,14 +1,14 @@
 import {command, file, resource, task, variable} from '../../src/Fig';
 
 task('create target directory', async () => {
-  await file({
-    path: variable.string('terminfo_path'),
-    state: 'directory',
-  });
+    await file({
+        path: variable.string('terminfo_path'),
+        state: 'directory',
+    });
 });
 
 task('update terminfo files', async () => {
-  for (const terminfo of resource.files('*.terminfo')) {
-    await command('tic', '-o', variable.string('terminfo_path'), terminfo);
-  }
+    for (const terminfo of resource.files('*.terminfo')) {
+        await command('tic', '-o', variable.string('terminfo_path'), terminfo);
+    }
 });
index 62916bff1c52cb8ec5667f32e26a9c75bdfb6137..ad1d1b47e230dbab00daae698bdab73e63cc00b0 100644 (file)
@@ -1,21 +1,21 @@
 {
-  "name": "wincent",
-  "description": "\"dotfiles\" and system configuration",
-  "version": "0.0.1",
-  "main": "/dev/null",
-  "private": true,
-  "repository": "https://github.com/wincent/wincent.git",
-  "author": "Greg Hurrell <greg@hurrell.net>",
-  "license": "Public Domain",
-  "scripts": {
-    "ci": "yarn format:check",
-    "format:check": "npx prettier --check \"**/*.{js,json,ts}\" \"*.md\" roles/dotfiles/files/.zsh/liferay/bin/portool",
-    "format": "npx prettier --write \"**/*.{js,json,ts}\" \"*.md\" roles/dotfiles/files/.zsh/liferay/bin/portool"
-  },
-  "dependencies": {
-    "typescript": "3.8.3"
-  },
-  "devDependencies": {
-    "@types/node": "*"
-  }
+    "name": "wincent",
+    "description": "\"dotfiles\" and system configuration",
+    "version": "0.0.1",
+    "main": "/dev/null",
+    "private": true,
+    "repository": "https://github.com/wincent/wincent.git",
+    "author": "Greg Hurrell <greg@hurrell.net>",
+    "license": "Public Domain",
+    "scripts": {
+        "ci": "yarn format:check",
+        "format:check": "npx prettier --check \"**/*.{js,json,ts}\" \"*.md\" roles/dotfiles/files/.zsh/liferay/bin/portool",
+        "format": "npx prettier --write \"**/*.{js,json,ts}\" \"*.md\" roles/dotfiles/files/.zsh/liferay/bin/portool"
+    },
+    "dependencies": {
+        "typescript": "3.8.3"
+    },
+    "devDependencies": {
+        "@types/node": "*"
+    }
 }
index 8588f409c1eeca1660944837a0fbeef4ea46779b..fe6b9570c6de19ff83c979ba6cdeb0ec047aaef9 100644 (file)
@@ -1,6 +1,6 @@
 {
-  "variables": {
-    "corpus_notes": "~/Documents/Corpus",
-    "git_cipher_path": "vendor/git-cipher/bin/git-cipher"
-  }
+    "variables": {
+        "corpus_notes": "~/Documents/Corpus",
+        "git_cipher_path": "vendor/git-cipher/bin/git-cipher"
+    }
 }
index 179c28f4a4cee422e08ea08d864c896536669fc7..e138ea49bfc6d9db6eecbd3660b203bff09a69db 100644 (file)
@@ -1,5 +1,5 @@
 {
-  "variables": {
-    "corpus_notes": "~/Sync/Personal/Corpus"
-  }
+    "variables": {
+        "corpus_notes": "~/Sync/Personal/Corpus"
+    }
 }
index 2ab19106236f50533e4dfa1feb7fc6686c8e6e35..37e4f918a53269d7ea5450926a92bec7f23b1b15 100644 (file)
@@ -1,3 +1,3 @@
 {
-  "variables": {}
+    "variables": {}
 }
index 019768f19fbe8c33bf8c350e3141a04926f23d7b..1884fbb60e28972f0063280ca882d0f1d8d3d03f 100644 (file)
@@ -1,24 +1,24 @@
 {
-  "platforms": {
-    "darwin": {
-      "aspects": ["launchd", "terminfo"],
-      "variables": {}
+    "platforms": {
+        "darwin": {
+            "aspects": ["launchd", "terminfo"],
+            "variables": {}
+        },
+        "linux": {
+            "aspects": ["terminfo"],
+            "variables": {
+                "terminfo_path": "~/share/terminfo"
+            }
+        }
     },
-    "linux": {
-      "aspects": ["terminfo"],
-      "variables": {
-        "terminfo_path": "~/share/terminfo"
-      }
+    "profiles": {
+        "personal": {
+            "pattern": "/\\bretiro\\b/i",
+            "variables": {}
+        },
+        "work": {
+            "pattern": "/\\blfrw044\\b/i",
+            "variables": {}
+        }
     }
-  },
-  "profiles": {
-    "personal": {
-      "pattern": "/\\bretiro\\b/i",
-      "variables": {}
-    },
-    "work": {
-      "pattern": "/\\blfrw044\\b/i",
-      "variables": {}
-    }
-  }
 }
index d4dacbf4b723cf1b7f0aadcb8f937eb4c62acb33..578eb87ca81211eb68c56fe6b91129b3948c70df 100644 (file)
@@ -5,35 +5,35 @@
 // 4. Set all JS files to open via this app.
 // 5. Profit.
 function run(input, parameters) {
-  var iTerm = Application('iTerm2');
-  iTerm.activate();
-  var windows = iTerm.windows();
-  var window;
-  var tab;
-  if (windows.length) {
-    window = iTerm.currentWindow();
-    tab = window.createTabWithDefaultProfile();
-  } else {
-    window = iTerm.createWindowWithDefaultProfile();
-    tab = window.currentTab();
-  }
-  var session = tab.currentSession();
-  var files = [];
-  for (var i = 0; i < input.length; i++) {
-    files.push(quotedForm(input[i]));
-  }
-  session.write({text: 'vim ' + files.join(' ')});
+    var iTerm = Application('iTerm2');
+    iTerm.activate();
+    var windows = iTerm.windows();
+    var window;
+    var tab;
+    if (windows.length) {
+        window = iTerm.currentWindow();
+        tab = window.createTabWithDefaultProfile();
+    } else {
+        window = iTerm.createWindowWithDefaultProfile();
+        tab = window.currentTab();
+    }
+    var session = tab.currentSession();
+    var files = [];
+    for (var i = 0; i < input.length; i++) {
+        files.push(quotedForm(input[i]));
+    }
+    session.write({text: 'vim ' + files.join(' ')});
 }
 
 // Based on: https://ruby-doc.org/stdlib-2.3.0/libdoc/shellwords/rdoc/Shellwords.html#method-c-shellescape
 function quotedForm(path) {
-  var string = path.toString();
+    var string = path.toString();
 
-  if (string === '' || string === null) {
-    return "''";
-  }
+    if (string === '' || string === null) {
+        return "''";
+    }
 
-  return string
-    .replace(/([^a-z0-9_\-.,:\/@\n])/gi, '\\$1')
-    .replace(/\n/g, "'\n'");
+    return string
+        .replace(/([^a-z0-9_\-.,:\/@\n])/gi, '\\$1')
+        .replace(/\n/g, "'\n'");
 }
index cc1ff5844da4ee27bad5e1ebf4fcfb5f86a60dc3..bf827d573bb861c997b20890008c7ecd72c2d3f2 100644 (file)
@@ -1,3 +1,3 @@
 {
-  "address": "~/.clipper.sock"
+    "address": "~/.clipper.sock"
 }
index 53f8a07e7566b0ab1ffda304448b86ca71c68bab..f42529429e783f778a43493f04f63c494e20539e 100644 (file)
@@ -215,6 +215,62 @@ let s:wincent_override_filetypes=[
       \ ]
 
 function! wincent#autocmds#apply_overrides(file, type) abort
+  let l:editorconfig=wincent#autocmds#editorconfig(a:file, a:type)
+
+  " TODO: Deal with more kinds of globs
+  " typical examples:
+  "     *           any file
+  "     *.js        any js file
+  "     lib/**.js   any js file under lib (at any level)
+  "     *.{js,ts}   any js or ts file
+  " meaning of each char:
+  "     *           match run of any char except /
+  "     **          match any run of chars
+  "     ?           any single char
+  "     [abc]       any single char of a, b, c
+  "     [!abc]      any single char not in set a, b, c
+  "     {a,b,c}     a or b or c (can be nested, gulp)
+  "     {-10..10}   numbers between -10 and 10
+  "     \\          escape
+
+  let l:overrides={}
+  let l:path=fnamemodify(a:file, ':p')
+  for l:config in l:editorconfig
+    " unsilent echomsg string(l:config)
+    let l:glob=l:config.name
+
+    " TODO: don't backslash-escape already-escaped things
+    let l:glob=substitute(l:glob, '\v\.', '\\.', 'g')
+    let l:glob=substitute(l:glob, '\v\*\*', '.+', 'g')
+    let l:glob=substitute(l:glob, '\v\*', '[^/]+', 'g')
+
+    " TODO: replace this hack with real thing:
+    let l:glob=substitute(l:glob, '\v\{', '(', 'g')
+    let l:glob=substitute(l:glob, '\v\}', ')', 'g')
+    let l:glob=substitute(l:glob, '\v,', '|', 'g')
+
+    if match(l:path, '\v' . l:glob . '$') != -1
+      " BUG: won't handle unsaved files; maybe that is ok
+      for l:pair in items(l:config.pairs)
+        let l:key=l:pair[0]
+        let l:value=l:pair[1]
+        if l:key == 'indent_style'
+          if l:value == 'space'
+            setlocal expandtab
+          else
+            setlocal noexpandtab
+          endif
+        elseif l:key == 'indent_size'
+          let &l:shiftwidth=l:value
+          let &l:tabstop=l:value
+        endif
+      endfor
+    endif
+  endfor
+
+  " TODO: rework this so that if when we check for liferay directories, if
+  " found, they return an "editorconfig object...
+
   let l:pattern=join(s:wincent_override_filetypes, '\|')
   if match(a:type, '\<\(' . l:pattern . '\)\>') != -1
     let l:detected=wincent#liferay#detect(a:file)
@@ -247,6 +303,103 @@ function! wincent#autocmds#apply_overrides(file, type) abort
   endif
 endfunction
 
+" For full list of possible keys, see:
+"
+"     https://editorconfig-specification.readthedocs.io/en/latest/
+"
+" We only support a restrictive subset for now.
+let s:editorconfig_keys={
+      \   'indent_style': ['tab', 'space'],
+      \   'indent_size': ['tab', '1', '2', '3', '4', '5', '6', '7', '8'],
+      \   'insert_final_newline': ['true', 'false'],
+      \   'tab_width': ['tab', '1', '2', '3', '4', '5', '6', '7', '8']
+      \ }
+
+" Traverse upwards looking for an .editorconfig.
+"
+" Implements a subset of the functionality described at: https://editorconfig.org/
+"
+" @param {file} path of current file(either absolute or relative to cwd)
+" @param {type} filetype
+"
+" If there is no filename yet, `file` will be the same as `type`.
+function! wincent#autocmds#editorconfig(file, type) abort
+  let l:path=fnamemodify(a:file, ':p')
+  while 1
+    let l:path=fnamemodify(l:path, ':h')
+    let l:candidate=l:path . '/.editorconfig'
+    if filereadable(l:candidate)
+      break
+    endif
+    if l:path == '' || l:path == '/'
+      return []
+    endif
+  endwhile
+
+  let l:config=[]
+  let l:section=v:null
+  let l:lines=readfile(l:candidate)
+  for l:line in l:lines
+    if match(l:line, '\v^\s*$') != -1
+      " Blank line, skip.
+    elseif match(l:line, '\v^\s*[#;]') != -1
+      " Comment, skip.
+    else
+      let l:header=matchlist(l:line, '\v^\s*\[([^\]]+)\]\s*$')
+      if !empty(l:header)
+        " Starting a section.
+        let l:section={'name': l:header[1], 'pairs': {}}
+        call add(l:config, l:section)
+      else
+        let l:pair=matchlist(l:line, '\v^\s*([^=]{-})\s*\=\s*(\S.{-})\s*$')
+        if !empty(l:pair)
+          " Adding key/value pair to current section.
+          let l:key=l:pair[1]
+          let l:value=l:pair[2]
+          if type(l:section) == type(v:null)
+            if l:key == 'root'
+              " 'root' in preamble.
+              "
+              " Possible values: 'true' or 'false'.
+              "
+              " TODO: actually keep walking upward to see if there are
+              " more files to be merged. (Low-pri: I don't expect to see
+              " .editorconfig files at multiple levels in practice very
+              " often; by far the most common pattern is a single file
+              " at the repo root with 'root = true'.)
+            else
+              " Ignore non-'root' key in preamble.
+            endif
+          else
+            if l:key == 'root'
+              " Ignore 'root' outside of preamble.
+            elseif has_key(s:editorconfig_keys, l:key)
+              " Non-'root' outside of preamble.
+              if l:value == 'unset'
+                " Remove the key, if present.
+                if has_key(l:section.pairs, l:key)
+                  call remove(l:section.pairs, l:key)
+                endif
+              elseif index(get(s:editorconfig_keys, l:key), l:value) != -1
+                " Legit value for this key.
+                let l:section.pairs[l:key]=l:value
+              else
+                " Invalid/unsupported value.
+              endif
+            else
+              " Unknown key.
+            endif
+          endif
+        else
+          " Unknown format, skip.
+        endif
+      endif
+    endif
+  endfor
+
+  return l:config
+endfunction
+
 function! wincent#autocmds#format(motion) abort
   if has('ex_extra')
     let l:v=operator#user#visual_command_from_wise_name(a:motion)
index f379b6f18342c1dbe92d854409ad585801f20cdc..61e26e0daadaee85463d81b5dcec9ff93068c17f 100755 (executable)
@@ -6,37 +6,37 @@ const path = require('path');
 const {createInterface} = require('readline');
 
 function log(...messages) {
-  console.log(...messages);
+    console.log(...messages);
 }
 
 function error(...messages) {
-  log('error: ', ...messages);
+    log('error: ', ...messages);
 }
 
 function die(...messages) {
-  if (messages.length) {
-    log(...messages);
-  }
-  process.exit(1);
+    if (messages.length) {
+        log(...messages);
+    }
+    process.exit(1);
 }
 
 async function confirm(prompt) {
-  const readline = createInterface({
-    input: process.stdin,
-    output: process.stdout,
-  });
-
-  return new Promise((resolve) => {
-    readline.question(`${prompt} [y/n] `, resolve);
-
-    readline.write('y');
-  })
-    .then((result) => {
-      return /^\s*y(es?)?\s*$/i.test(result);
-    })
-    .finally(() => {
-      readline.close();
+    const readline = createInterface({
+        input: process.stdin,
+        output: process.stdout,
     });
+
+    return new Promise((resolve) => {
+        readline.question(`${prompt} [y/n] `, resolve);
+
+        readline.write('y');
+    })
+        .then((result) => {
+            return /^\s*y(es?)?\s*$/i.test(result);
+        })
+        .finally(() => {
+            readline.close();
+        });
 }
 
 // The lowest signal number (via `man 3 signal`).
@@ -46,120 +46,124 @@ const SIGHUP = 1;
 const SIGUSR2 = 31;
 
 function run(command, ...args) {
-  return new Promise((resolve, reject) => {
-    let resolved = false;
-    const child = child_process.spawn(command, args, {stdio: 'inherit'});
-
-    process.on('SIGINT', () => {
-      child.kill('SIGINT');
-    });
-
-    child.on('error', (err) => {
-      if (!resolved) {
-        resolved = true;
-        reject(err);
-      }
-    });
-
-    child.on('exit', (code) => {
-      if (!resolved) {
-        resolved = true;
-        if (code) {
-          let err;
-          const description = `\`${[command, ...args].join(' ')}\``;
-          if (code >= 128 + SIGHUP && code <= 128 + SIGUSR2) {
-            err = new Error(
-              `${description} exited due to signal ${code - 128}`
-            );
-          } else {
-            err = new Error(`${description} exited with status ${code}`);
-          }
-          reject(err);
-        } else {
-          resolve();
-        }
-      }
+    return new Promise((resolve, reject) => {
+        let resolved = false;
+        const child = child_process.spawn(command, args, {stdio: 'inherit'});
+
+        process.on('SIGINT', () => {
+            child.kill('SIGINT');
+        });
+
+        child.on('error', (err) => {
+            if (!resolved) {
+                resolved = true;
+                reject(err);
+            }
+        });
+
+        child.on('exit', (code) => {
+            if (!resolved) {
+                resolved = true;
+                if (code) {
+                    let err;
+                    const description = `\`${[command, ...args].join(' ')}\``;
+                    if (code >= 128 + SIGHUP && code <= 128 + SIGUSR2) {
+                        err = new Error(
+                            `${description} exited due to signal ${code - 128}`
+                        );
+                    } else {
+                        err = new Error(
+                            `${description} exited with status ${code}`
+                        );
+                    }
+                    reject(err);
+                } else {
+                    resolve();
+                }
+            }
+        });
     });
-  });
 }
 
 async function getHelper(command) {
-  if (command.match(/^\w+$/)) {
-    const dir = path.join(__dirname, 'portool-helpers');
-    const helper = path.join(dir, command);
-
-    try {
-      fs.accessSync(helper, fs.constants.X_OK);
-
-      if (fs.statSync(helper).isFile()) {
-        return helper;
-      }
-    } catch (error) {
-      if (error.code === 'ENOENT') {
-        // Check for a possible match.
-        const candidates = fs
-          .readdirSync(dir)
-          .filter((entry) => /^[a-z]/.test(entry));
-
-        const prefixMatches = candidates.filter((entry) =>
-          entry.startsWith(command)
-        );
-
-        if (prefixMatches.length === 1) {
-          // Unambiguous prefix match.
-          return getHelper(prefixMatches[0]);
-        } else if (prefixMatches.length > 1) {
-          // Multiple possible prefix matches.
-          log(
-            `Ambiguous command ${JSON.stringify(
-              command
-            )} - did you mean one of?\n` +
-              '\n' +
-              prefixMatches.map((prefix) => `  ${prefix}`).join('\n') +
-              '\n'
-          );
-        } else {
-          // Prompt for confirmation of best fuzzy match guess.
-          const fuzzyMatches = candidates
-            .map((candidate) => ({
-              candidate,
-              score: jaroWinkler(candidate, command),
-            }))
-            .filter(({score}) => score)
-            .sort((a, b) => {
-              if (a.score < b.score) {
-                return -1;
-              } else if (a.score > b.score) {
-                return 1;
-              } else {
-                return 0;
-              }
-            })
-            .reverse();
-
-          const proposed = fuzzyMatches[0];
-
-          if (proposed) {
-            log(
-              `Command ${JSON.stringify(
-                command
-              )} invoked, which does not exist.\n`
-            );
-
-            const proceed = await confirm(
-              `Do you want to run ${JSON.stringify(
-                proposed.candidate
-              )} instead?`
-            );
-
-            if (proceed) {
-              return getHelper(proposed.candidate);
+    if (command.match(/^\w+$/)) {
+        const dir = path.join(__dirname, 'portool-helpers');
+        const helper = path.join(dir, command);
+
+        try {
+            fs.accessSync(helper, fs.constants.X_OK);
+
+            if (fs.statSync(helper).isFile()) {
+                return helper;
+            }
+        } catch (error) {
+            if (error.code === 'ENOENT') {
+                // Check for a possible match.
+                const candidates = fs
+                    .readdirSync(dir)
+                    .filter((entry) => /^[a-z]/.test(entry));
+
+                const prefixMatches = candidates.filter((entry) =>
+                    entry.startsWith(command)
+                );
+
+                if (prefixMatches.length === 1) {
+                    // Unambiguous prefix match.
+                    return getHelper(prefixMatches[0]);
+                } else if (prefixMatches.length > 1) {
+                    // Multiple possible prefix matches.
+                    log(
+                        `Ambiguous command ${JSON.stringify(
+                            command
+                        )} - did you mean one of?\n` +
+                            '\n' +
+                            prefixMatches
+                                .map((prefix) => `  ${prefix}`)
+                                .join('\n') +
+                            '\n'
+                    );
+                } else {
+                    // Prompt for confirmation of best fuzzy match guess.
+                    const fuzzyMatches = candidates
+                        .map((candidate) => ({
+                            candidate,
+                            score: jaroWinkler(candidate, command),
+                        }))
+                        .filter(({score}) => score)
+                        .sort((a, b) => {
+                            if (a.score < b.score) {
+                                return -1;
+                            } else if (a.score > b.score) {
+                                return 1;
+                            } else {
+                                return 0;
+                            }
+                        })
+                        .reverse();
+
+                    const proposed = fuzzyMatches[0];
+
+                    if (proposed) {
+                        log(
+                            `Command ${JSON.stringify(
+                                command
+                            )} invoked, which does not exist.\n`
+                        );
+
+                        const proceed = await confirm(
+                            `Do you want to run ${JSON.stringify(
+                                proposed.candidate
+                            )} instead?`
+                        );
+
+                        if (proceed) {
+                            return getHelper(proposed.candidate);
+                        }
+                    }
+                }
             }
-          }
         }
-      }
     }
-  }
 }
 
 /**
@@ -194,106 +198,111 @@ async function getHelper(command) {
  *
  */
 function jaroWinkler(a, b) {
-  if (!a.length || !b.length) {
-    // Empty strings match nothing, not even each other.
-    return 0;
-  }
+    if (!a.length || !b.length) {
+        // Empty strings match nothing, not even each other.
+        return 0;
+    }
 
-  const s1 = a.toLowerCase();
-  const s2 = b.toLowerCase();
+    const s1 = a.toLowerCase();
+    const s2 = b.toLowerCase();
 
-  if (s1 === s2) {
-    // Exact match.
-    return 1;
-  }
+    if (s1 === s2) {
+        // Exact match.
+        return 1;
+    }
 
-  let m = 0;
+    let m = 0;
 
-  const range = Math.floor(Math.max(s1.length, s2.length) / 2) - 1;
-  const s1Matches = new Array(s1.length);
-  const s2Matches = new Array(s2.length);
+    const range = Math.floor(Math.max(s1.length, s2.length) / 2) - 1;
+    const s1Matches = new Array(s1.length);
+    const s2Matches = new Array(s2.length);
 
-  for (let i = 0; i < s1.length; i++) {
-    const low = i >= range ? i - range : 0;
-    const high = i + range <= s2.length - 1 ? i + range : s2.length - 1;
+    for (let i = 0; i < s1.length; i++) {
+        const low = i >= range ? i - range : 0;
+        const high = i + range <= s2.length - 1 ? i + range : s2.length - 1;
 
-    for (let j = low; j <= high; j++) {
-      if (s1Matches[i] !== true && s2Matches[j] !== true && s1[i] === s2[j]) {
-        ++m;
+        for (let j = low; j <= high; j++) {
+            if (
+                s1Matches[i] !== true &&
+                s2Matches[j] !== true &&
+                s1[i] === s2[j]
+            ) {
+                ++m;
 
-        s1Matches[i] = s2Matches[j] = true;
+                s1Matches[i] = s2Matches[j] = true;
 
-        break;
-      }
+                break;
+            }
+        }
     }
-  }
 
-  if (m === 0) {
-    // No matching letters.
-    return 0;
-  }
+    if (m === 0) {
+        // No matching letters.
+        return 0;
+    }
 
-  // Count transpositions.
-  let k = 0;
-  let transpositions = 0;
+    // Count transpositions.
+    let k = 0;
+    let transpositions = 0;
 
-  for (let i = 0; i < s1.length; i++) {
-    if (s1Matches[i] === true) {
-      let j;
+    for (let i = 0; i < s1.length; i++) {
+        if (s1Matches[i] === true) {
+            let j;
 
-      for (j = k; j < s2.length; j++) {
-        if (s2Matches[j] === true) {
-          k = j + 1;
-          break;
-        }
-      }
+            for (j = k; j < s2.length; j++) {
+                if (s2Matches[j] === true) {
+                    k = j + 1;
+                    break;
+                }
+            }
 
-      if (s1[i] !== s2[j]) {
-        ++transpositions;
-      }
+            if (s1[i] !== s2[j]) {
+                ++transpositions;
+            }
+        }
     }
-  }
 
-  // "sim" is the Jaro similarity ("sim[j]").
-  let sim = (m / s1.length + m / s2.length + (m - transpositions / 2) / m) / 3;
+    // "sim" is the Jaro similarity ("sim[j]").
+    let sim =
+        (m / s1.length + m / s2.length + (m - transpositions / 2) / m) / 3;
 
-  // Apply prefix scaling to obtain Jaro-Winkler similarity ("sim[w]").
+    // Apply prefix scaling to obtain Jaro-Winkler similarity ("sim[w]").
 
-  // Length of common prefix (up to a maximum of 4 characters).
-  let l = 0;
+    // Length of common prefix (up to a maximum of 4 characters).
+    let l = 0;
 
-  // Scaling factor. 0.1 is standard, 0.25 is upper limit to prevent similarity
-  // from exceeding 1.
-  const p = 0.1;
+    // Scaling factor. 0.1 is standard, 0.25 is upper limit to prevent similarity
+    // from exceeding 1.
+    const p = 0.1;
 
-  if (sim > 0.7) {
-    while (s1[l] === s2[l] && l < 4) {
-      ++l;
-    }
+    if (sim > 0.7) {
+        while (s1[l] === s2[l] && l < 4) {
+            ++l;
+        }
 
-    sim = sim + l * p * (1 - sim);
-  }
+        sim = sim + l * p * (1 - sim);
+    }
 
-  return sim;
+    return sim;
 }
 
 const HELP = 'run `portool help` to see available commands';
 
 async function main(_node, _script, command, ...args) {
-  if (!command) {
-    die(`must supply command: ${HELP}`);
-  }
-  const helper = await getHelper(command);
-  if (!helper) {
-    die(`no such command: ${command} - ${HELP}`);
-  }
-
-  await run(helper, ...args);
+    if (!command) {
+        die(`must supply command: ${HELP}`);
+    }
+    const helper = await getHelper(command);
+    if (!helper) {
+        die(`no such command: ${command} - ${HELP}`);
+    }
+
+    await run(helper, ...args);
 }
 
 main(...process.argv).catch((err) => {
-  error(err);
-  process.exit(1);
+    error(err);
+    process.exit(1);
 });
 
 // vim: ft=javascript
index 9e88fc83650bb555bd826893e2a512673cd9b322..68764be23637d95d9d58daa375dd873ee5ff16da 100755 (executable)
@@ -11,125 +11,126 @@ const assert = require('assert');
 const {bundleIdentifier, deepCopy, isObject, visit} = require('./karabiner');
 
 (function test_bundleIdentifier() {
-  (function $() {
-    assert(
-      bundleIdentifier('com.apple.TextEdit') === '^com\\.apple\\.TextEdit$',
-      $
-    );
-  })();
+    (function $() {
+        assert(
+            bundleIdentifier('com.apple.TextEdit') ===
+                '^com\\.apple\\.TextEdit$',
+            $
+        );
+    })();
 })();
 
 (function test_deepCopy() {
-  const source = {
-    object: {
-      isInner: true,
-    },
-    array: [1, 2, [3, 4]],
-  };
-  const copy = deepCopy(source);
-
-  // Copies look the same.
-  (function $() {
-    assert(
-      JSON.stringify(copy) ===
-        '{"object":{"isInner":true},"array":[1,2,[3,4]]}',
-      $
-    );
-  })();
-
-  // Objects are cloned.
-  (function $() {
-    assert(source !== copy, $);
-  })();
-
-  // Nested objects are cloned.
-  (function $() {
-    assert(source.object !== copy.object, $);
-  })();
-
-  // Arrays are cloned.
-  (function $() {
-    assert(source.array !== copy.array, $);
-  })();
-
-  // Nested arrays are cloned.
-  (function $() {
-    assert(source.array[2] !== copy.array[2], $);
-  })();
-
-  // Primitives are identical.
-  (function $() {
-    assert(source.array[0] === copy.array[0], $);
-  })();
+    const source = {
+        object: {
+            isInner: true,
+        },
+        array: [1, 2, [3, 4]],
+    };
+    const copy = deepCopy(source);
+
+    // Copies look the same.
+    (function $() {
+        assert(
+            JSON.stringify(copy) ===
+                '{"object":{"isInner":true},"array":[1,2,[3,4]]}',
+            $
+        );
+    })();
+
+    // Objects are cloned.
+    (function $() {
+        assert(source !== copy, $);
+    })();
+
+    // Nested objects are cloned.
+    (function $() {
+        assert(source.object !== copy.object, $);
+    })();
+
+    // Arrays are cloned.
+    (function $() {
+        assert(source.array !== copy.array, $);
+    })();
+
+    // Nested arrays are cloned.
+    (function $() {
+        assert(source.array[2] !== copy.array[2], $);
+    })();
+
+    // Primitives are identical.
+    (function $() {
+        assert(source.array[0] === copy.array[0], $);
+    })();
 })();
 
 (function test_isObject() {
-  // Arrays are not objects.
-  (function $() {
-    assert(!isObject([1]), $);
-  })();
-
-  // Booleans are not objects.
-  (function $() {
-    assert(!isObject(true), $);
-  })();
-
-  // `null` is not an object.
-  (function $() {
-    assert(!isObject(null), $);
-  })();
-
-  // Numbers are not objects.
-  (function $() {
-    assert(!isObject(1), $);
-  })();
-
-  // Strings are not objects.
-  (function $() {
-    assert(!isObject('this'), $);
-  })();
-
-  // `undefined` is not an object.
-  (function $() {
-    assert(!isObject(undefined), $);
-  })();
-
-  // Objects are objects.
-  (function $() {
-    assert(isObject({}), $);
-  })();
+    // Arrays are not objects.
+    (function $() {
+        assert(!isObject([1]), $);
+    })();
+
+    // Booleans are not objects.
+    (function $() {
+        assert(!isObject(true), $);
+    })();
+
+    // `null` is not an object.
+    (function $() {
+        assert(!isObject(null), $);
+    })();
+
+    // Numbers are not objects.
+    (function $() {
+        assert(!isObject(1), $);
+    })();
+
+    // Strings are not objects.
+    (function $() {
+        assert(!isObject('this'), $);
+    })();
+
+    // `undefined` is not an object.
+    (function $() {
+        assert(!isObject(undefined), $);
+    })();
+
+    // Objects are objects.
+    (function $() {
+        assert(isObject({}), $);
+    })();
 })();
 
 (function test_visit() {
-  const subject = () => ({
-    foo: 1,
-    bar: [
-      {
-        a: [{}, {deep: {prop: 3}}],
-      },
-      {
-        a: [],
-        b: [{deep: {prop: 10}}],
-      },
-    ],
-  });
-
-  // Helpers for readability.
-  const string = JSON.stringify;
-  const squish = (s) => s.replace(/\s+/g, '');
-
-  // Replacing the entire document.
-  (function $() {
-    const updated = visit(subject(), '$', (root) => 'replacement');
-    assert(updated === 'replacement', $);
-  })();
-
-  // Setting a property on an object.
-  (function $() {
-    const updated = visit(subject(), '$.foo', (value) => value + 5);
-    assert(
-      string(updated) ===
-        squish(`{
+    const subject = () => ({
+        foo: 1,
+        bar: [
+            {
+                a: [{}, {deep: {prop: 3}}],
+            },
+            {
+                a: [],
+                b: [{deep: {prop: 10}}],
+            },
+        ],
+    });
+
+    // Helpers for readability.
+    const string = JSON.stringify;
+    const squish = (s) => s.replace(/\s+/g, '');
+
+    // Replacing the entire document.
+    (function $() {
+        const updated = visit(subject(), '$', (root) => 'replacement');
+        assert(updated === 'replacement', $);
+    })();
+
+    // Setting a property on an object.
+    (function $() {
+        const updated = visit(subject(), '$.foo', (value) => value + 5);
+        assert(
+            string(updated) ===
+                squish(`{
           "foo": 6,
           "bar": [
             {
@@ -141,38 +142,42 @@ const {bundleIdentifier, deepCopy, isObject, visit} = require('./karabiner');
             }
           ]
         }`),
-      $
-    );
-  })();
-
-  // Modifying a list.
-  (function $() {
-    let counter = 10;
-    const updated = visit(subject(), '$.bar[0:]', (_) => counter++);
-    assert(
-      string(updated) ===
-        squish(`{
+            $
+        );
+    })();
+
+    // Modifying a list.
+    (function $() {
+        let counter = 10;
+        const updated = visit(subject(), '$.bar[0:]', (_) => counter++);
+        assert(
+            string(updated) ===
+                squish(`{
           "foo": 1,
           "bar": [10, 11]
         }`),
-      $
-    );
-  })();
-
-  // Re-cycling subtrees.
-  (function $() {
-    const original = subject();
-    const updated = visit(original, '$.bar[0:].a[0:].deep', (value) => 'xxx');
-
-    assert(
-      updated !== original &&
-        updated.foo === original.foo &&
-        updated.bar !== original.bar &&
-        updated.bar[0] !== original.bar[0] &&
-        updated.bar[0].a !== original.bar[0].a &&
-        updated.bar[0].a[0] !== original.bar[0].a[0] &&
-        updated.bar[1] === original.bar[1],
-      $
-    );
-  })();
+            $
+        );
+    })();
+
+    // Re-cycling subtrees.
+    (function $() {
+        const original = subject();
+        const updated = visit(
+            original,
+            '$.bar[0:].a[0:].deep',
+            (value) => 'xxx'
+        );
+
+        assert(
+            updated !== original &&
+                updated.foo === original.foo &&
+                updated.bar !== original.bar &&
+                updated.bar[0] !== original.bar[0] &&
+                updated.bar[0].a !== original.bar[0].a &&
+                updated.bar[0].a[0] !== original.bar[0].a[0] &&
+                updated.bar[1] === original.bar[1],
+            $
+        );
+    })();
 })();
index 34d028218658ff680a7a3836c9d5bef1e23b3e5a..efe74ef7ee24c1ce1e5bb92ea6e66fc184966c8e 100755 (executable)
 #!/usr/bin/env node
 
 function fromTo(from, to) {
-  return [
-    {
-      from: {
-        key_code: from,
-      },
-      to: {
-        key_code: to,
-      },
-    },
-  ];
+    return [
+        {
+            from: {
+                key_code: from,
+            },
+            to: {
+                key_code: to,
+            },
+        },
+    ];
 }
 
 function bundleIdentifier(identifier) {
-  return '^' + identifier.replace(/\./g, '\\.') + '$';
+    return '^' + identifier.replace(/\./g, '\\.') + '$';
 }
 
 function spaceFN(from, to) {
-  return [
-    {
-      from: {
-        modifiers: {
-          optional: ['any'],
-        },
-        simultaneous: [
-          {
-            key_code: 'spacebar',
-          },
-          {
-            key_code: from,
-          },
-        ],
-        simultaneous_options: {
-          key_down_order: 'strict',
-          key_up_order: 'strict_inverse',
-          to_after_key_up: [
-            {
-              set_variable: {
-                name: 'SpaceFN',
-                value: 0,
-              },
-            },
-          ],
-        },
-      },
-      parameters: {
-        'basic.simultaneous_threshold_milliseconds': 500 /* Default: 1000 */,
-      },
-      to: [
-        {
-          set_variable: {
-            name: 'SpaceFN',
-            value: 1,
-          },
-        },
-        {
-          key_code: to,
-        },
-      ],
-      type: 'basic',
-    },
-    {
-      conditions: [
+    return [
         {
-          name: 'SpaceFN',
-          type: 'variable_if',
-          value: 1,
-        },
-      ],
-      from: {
-        key_code: from,
-        modifiers: {
-          optional: ['any'],
+            from: {
+                modifiers: {
+                    optional: ['any'],
+                },
+                simultaneous: [
+                    {
+                        key_code: 'spacebar',
+                    },
+                    {
+                        key_code: from,
+                    },
+                ],
+                simultaneous_options: {
+                    key_down_order: 'strict',
+                    key_up_order: 'strict_inverse',
+                    to_after_key_up: [
+                        {
+                            set_variable: {
+                                name: 'SpaceFN',
+                                value: 0,
+                            },
+                        },
+                    ],
+                },
+            },
+            parameters: {
+                'basic.simultaneous_threshold_milliseconds': 500 /* Default: 1000 */,
+            },
+            to: [
+                {
+                    set_variable: {
+                        name: 'SpaceFN',
+                        value: 1,
+                    },
+                },
+                {
+                    key_code: to,
+                },
+            ],
+            type: 'basic',
         },
-      },
-      to: [
         {
-          key_code: to,
+            conditions: [
+                {
+                    name: 'SpaceFN',
+                    type: 'variable_if',
+                    value: 1,
+                },
+            ],
+            from: {
+                key_code: from,
+                modifiers: {
+                    optional: ['any'],
+                },
+            },
+            to: [
+                {
+                    key_code: to,
+                },
+            ],
+            type: 'basic',
         },
-      ],
-      type: 'basic',
-    },
-  ];
+    ];
 }
 
 function swap(a, b) {
-  return [...fromTo(a, b), ...fromTo(b, a)];
+    return [...fromTo(a, b), ...fromTo(b, a)];
 }
 
 const DEVICE_DEFAULTS = {
-  disable_built_in_keyboard_if_exists: false,
-  fn_function_keys: [],
-  ignore: false,
-  manipulate_caps_lock_led: true,
-  simple_modifications: [],
+    disable_built_in_keyboard_if_exists: false,
+    fn_function_keys: [],
+    ignore: false,
+    manipulate_caps_lock_led: true,
+    simple_modifications: [],
 };
 
 const IDENTIFIER_DEFAULTS = {
-  is_keyboard: true,
-  is_pointing_device: false,
+    is_keyboard: true,
+    is_pointing_device: false,
 };
 
 const APPLE_INTERNAL_US = {
-  ...DEVICE_DEFAULTS,
-  identifiers: {
-    ...IDENTIFIER_DEFAULTS,
-    product_id: 628,
-    vendor_id: 1452,
-  },
+    ...DEVICE_DEFAULTS,
+    identifiers: {
+        ...IDENTIFIER_DEFAULTS,
+        product_id: 628,
+        vendor_id: 1452,
+    },
 };
 
 const APPLE_INTERNAL_ES = {
-  ...DEVICE_DEFAULTS,
-  identifiers: {
-    ...IDENTIFIER_DEFAULTS,
-    product_id: 636,
-    vendor_id: 1452,
-  },
-  simple_modifications: [
-    ...fromTo('non_us_backslash', 'grave_accent_and_tilde'),
-    ...fromTo('grave_accent_and_tilde', 'left_shift'),
-    ...fromTo('backslash', 'return_or_enter'),
-  ],
+    ...DEVICE_DEFAULTS,
+    identifiers: {
+        ...IDENTIFIER_DEFAULTS,
+        product_id: 636,
+        vendor_id: 1452,
+    },
+    simple_modifications: [
+        ...fromTo('non_us_backslash', 'grave_accent_and_tilde'),
+        ...fromTo('grave_accent_and_tilde', 'left_shift'),
+        ...fromTo('backslash', 'return_or_enter'),
+    ],
 };
 
 const REALFORCE = {
-  ...DEVICE_DEFAULTS,
-  identifiers: {
-    ...IDENTIFIER_DEFAULTS,
-    product_id: 273,
-    vendor_id: 2131,
-  },
-  simple_modifications: [
-    ...swap('left_command', 'left_option'),
-    ...swap('right_command', 'right_option'),
-    ...fromTo('application', 'fn'),
-    ...fromTo('pause', 'power'),
-  ],
+    ...DEVICE_DEFAULTS,
+    identifiers: {
+        ...IDENTIFIER_DEFAULTS,
+        product_id: 273,
+        vendor_id: 2131,
+    },
+    simple_modifications: [
+        ...swap('left_command', 'left_option'),
+        ...swap('right_command', 'right_option'),
+        ...fromTo('application', 'fn'),
+        ...fromTo('pause', 'power'),
+    ],
 };
 
 const PARAMETER_DEFAULTS = {
-  'basic.simultaneous_threshold_milliseconds': 50,
-  'basic.to_delayed_action_delay_milliseconds': 500,
-  'basic.to_if_alone_timeout_milliseconds': 1000,
-  'basic.to_if_held_down_threshold_milliseconds': 500,
+    'basic.simultaneous_threshold_milliseconds': 50,
+    'basic.to_delayed_action_delay_milliseconds': 500,
+    'basic.to_if_alone_timeout_milliseconds': 1000,
+    'basic.to_if_held_down_threshold_milliseconds': 500,
 };
 
 const VANILLA_PROFILE = {
-  complex_modifications: {
-    parameters: PARAMETER_DEFAULTS,
-    rules: [],
-  },
-  devices: [],
-  fn_function_keys: [
-    ...fromTo('f1', 'display_brightness_decrement'),
-    ...fromTo('f2', 'display_brightness_increment'),
-    ...fromTo('f3', 'mission_control'),
-    ...fromTo('f4', 'launchpad'),
-    ...fromTo('f5', 'illumination_decrement'),
-    ...fromTo('f6', 'illumination_increment'),
-    ...fromTo('f7', 'rewind'),
-    ...fromTo('f8', 'play_or_pause'),
-    ...fromTo('f9', 'fastforward'),
-    ...fromTo('f10', 'mute'),
-    ...fromTo('f11', 'volume_decrement'),
-    ...fromTo('f12', 'volume_increment'),
-  ],
-  name: 'Vanilla',
-  selected: false,
-  simple_modifications: [],
-  virtual_hid_keyboard: {
-    caps_lock_delay_milliseconds: 0,
-    keyboard_type: 'ansi',
-  },
+    complex_modifications: {
+        parameters: PARAMETER_DEFAULTS,
+        rules: [],
+    },
+    devices: [],
+    fn_function_keys: [
+        ...fromTo('f1', 'display_brightness_decrement'),
+        ...fromTo('f2', 'display_brightness_increment'),
+        ...fromTo('f3', 'mission_control'),
+        ...fromTo('f4', 'launchpad'),
+        ...fromTo('f5', 'illumination_decrement'),
+        ...fromTo('f6', 'illumination_increment'),
+        ...fromTo('f7', 'rewind'),
+        ...fromTo('f8', 'play_or_pause'),
+        ...fromTo('f9', 'fastforward'),
+        ...fromTo('f10', 'mute'),
+        ...fromTo('f11', 'volume_decrement'),
+        ...fromTo('f12', 'volume_increment'),
+    ],
+    name: 'Vanilla',
+    selected: false,
+    simple_modifications: [],
+    virtual_hid_keyboard: {
+        caps_lock_delay_milliseconds: 0,
+        keyboard_type: 'ansi',
+    },
 };
 
 function isObject(item) {
-  return (
-    item !== null && Object.prototype.toString.call(item) === '[object Object]'
-  );
+    return (
+        item !== null &&
+        Object.prototype.toString.call(item) === '[object Object]'
+    );
 }
 
 function deepCopy(item) {
-  if (Array.isArray(item)) {
-    return item.map(deepCopy);
-  } else if (isObject(item)) {
-    const copy = {};
-    Object.entries(item).forEach(([k, v]) => {
-      copy[k] = deepCopy(v);
-    });
-    return copy;
-  }
-  return item;
+    if (Array.isArray(item)) {
+        return item.map(deepCopy);
+    } else if (isObject(item)) {
+        const copy = {};
+        Object.entries(item).forEach(([k, v]) => {
+            copy[k] = deepCopy(v);
+        });
+        return copy;
+    }
+    return item;
 }
 
 /**
@@ -208,305 +209,311 @@ function deepCopy(item) {
  * - `[start:end]`: selects an array slice; `end` is optional.
  */
 function visit(item, path, updater) {
-  const match = path.match(
-    /^(?<root>\$)|\.(?<child>\w+)|\[(?<slice>.+?)\]|(?<done>$)/
-  );
-  const {
-    groups: {root, child, slice},
-  } = match;
-  const subpath = path.slice(match[0].length);
-  if (root) {
-    return visit(item, subpath, updater);
-  } else if (child) {
-    const next = visit(item[child], subpath, updater);
-    if (next !== undefined) {
-      return {
-        ...item,
-        [child]: next,
-      };
-    }
-  } else if (slice) {
+    const match = path.match(
+        /^(?<root>\$)|\.(?<child>\w+)|\[(?<slice>.+?)\]|(?<done>$)/
+    );
     const {
-      groups: {start, end},
-    } = slice.match(/^(?<start>\d+):(?<end>\d+)?$/);
-    let array;
-    for (let i = start, max = end == null ? item.length : end; i < max; i++) {
-      const next = visit(item[i], subpath, updater);
-      if (next !== undefined) {
-        if (!array) {
-          array = item.slice(0, i);
+        groups: {root, child, slice},
+    } = match;
+    const subpath = path.slice(match[0].length);
+    if (root) {
+        return visit(item, subpath, updater);
+    } else if (child) {
+        const next = visit(item[child], subpath, updater);
+        if (next !== undefined) {
+            return {
+                ...item,
+                [child]: next,
+            };
         }
-        array[i] = next;
-      } else if (array) {
-        array[i] = item[i];
-      }
+    } else if (slice) {
+        const {
+            groups: {start, end},
+        } = slice.match(/^(?<start>\d+):(?<end>\d+)?$/);
+        let array;
+        for (
+            let i = start, max = end == null ? item.length : end;
+            i < max;
+            i++
+        ) {
+            const next = visit(item[i], subpath, updater);
+            if (next !== undefined) {
+                if (!array) {
+                    array = item.slice(0, i);
+                }
+                array[i] = next;
+            } else if (array) {
+                array[i] = item[i];
+            }
+        }
+        return array;
+    } else {
+        const next = updater(item);
+        return next === item ? undefined : next;
     }
-    return array;
-  } else {
-    const next = updater(item);
-    return next === item ? undefined : next;
-  }
 }
 
 const EXEMPTIONS = ['com.factorio', 'com.feralinteractive.dirtrally'];
 
 function applyExemptions(profile) {
-  const exemptions = {
-    type: 'frontmost_application_unless',
-    bundle_identifiers: EXEMPTIONS.map(bundleIdentifier),
-  };
+    const exemptions = {
+        type: 'frontmost_application_unless',
+        bundle_identifiers: EXEMPTIONS.map(bundleIdentifier),
+    };
 
-  return visit(
-    profile,
-    '$.complex_modifications.rules[0:].manipulators[0:].conditions',
-    (conditions) => {
-      if (conditions) {
-        if (
-          conditions.some(
-            (condition) => condition.type === 'frontmost_application_if'
-          )
-        ) {
-          return conditions;
+    return visit(
+        profile,
+        '$.complex_modifications.rules[0:].manipulators[0:].conditions',
+        (conditions) => {
+            if (conditions) {
+                if (
+                    conditions.some(
+                        (condition) =>
+                            condition.type === 'frontmost_application_if'
+                    )
+                ) {
+                    return conditions;
+                }
+                return [...deepCopy(conditions), exemptions];
+            } else {
+                return [exemptions];
+            }
         }
-        return [...deepCopy(conditions), exemptions];
-      } else {
-        return [exemptions];
-      }
-    }
-  );
+    );
 }
 
 const DEFAULT_PROFILE = applyExemptions({
-  ...VANILLA_PROFILE,
-  complex_modifications: {
-    parameters: {
-      ...PARAMETER_DEFAULTS,
-      'basic.to_if_alone_timeout_milliseconds': 500 /* Default: 1000 */,
-    },
-    rules: [
-      {
-        description: 'SpaceFN layer',
-        manipulators: [
-          ...spaceFN('b', 'spacebar'),
-          ...spaceFN('u', 'right_arrow'),
-          ...spaceFN('y', 'down_arrow'),
-          ...spaceFN('h', 'left_arrow'),
-          ...spaceFN('n', 'up_arrow'),
-          ...spaceFN('l', 'right_arrow'),
-          ...spaceFN('k', 'down_arrow'),
-          ...spaceFN('j', 'left_arrow'),
-          ...spaceFN('i', 'up_arrow'),
-        ],
-      },
-      {
-        description: 'Tab + Return to Backslash',
-        manipulators: [
-          {
-            from: {
-              modifiers: {
-                optional: ['any'],
-              },
-              simultaneous: [
-                {
-                  key_code: 'tab',
-                },
-                {
-                  key_code: 'return_or_enter',
-                },
-              ],
-              simultaneous_options: {
-                key_down_order: 'insensitive',
-                key_up_order: 'insensitive',
-              },
+    ...VANILLA_PROFILE,
+    complex_modifications: {
+        parameters: {
+            ...PARAMETER_DEFAULTS,
+            'basic.to_if_alone_timeout_milliseconds': 500 /* Default: 1000 */,
+        },
+        rules: [
+            {
+                description: 'SpaceFN layer',
+                manipulators: [
+                    ...spaceFN('b', 'spacebar'),
+                    ...spaceFN('u', 'right_arrow'),
+                    ...spaceFN('y', 'down_arrow'),
+                    ...spaceFN('h', 'left_arrow'),
+                    ...spaceFN('n', 'up_arrow'),
+                    ...spaceFN('l', 'right_arrow'),
+                    ...spaceFN('k', 'down_arrow'),
+                    ...spaceFN('j', 'left_arrow'),
+                    ...spaceFN('i', 'up_arrow'),
+                ],
             },
-            to: [
-              {
-                key_code: 'backslash',
-              },
-            ],
-            conditions: [
-              {
-                type: 'device_if',
-                identifiers: [APPLE_INTERNAL_ES.identifiers],
-              },
-            ],
-            type: 'basic',
-          },
-        ],
-      },
-      {
-        description:
-          'Disable Karabiner-Elements with Fn+Control+Option+Command+Z',
-        manipulators: [
-          {
-            type: 'basic',
-            from: {
-              key_code: 'z',
-              modifiers: {
-                mandatory: [
-                  'fn',
-                  'left_control',
-                  'left_command',
-                  'left_option',
+            {
+                description: 'Tab + Return to Backslash',
+                manipulators: [
+                    {
+                        from: {
+                            modifiers: {
+                                optional: ['any'],
+                            },
+                            simultaneous: [
+                                {
+                                    key_code: 'tab',
+                                },
+                                {
+                                    key_code: 'return_or_enter',
+                                },
+                            ],
+                            simultaneous_options: {
+                                key_down_order: 'insensitive',
+                                key_up_order: 'insensitive',
+                            },
+                        },
+                        to: [
+                            {
+                                key_code: 'backslash',
+                            },
+                        ],
+                        conditions: [
+                            {
+                                type: 'device_if',
+                                identifiers: [APPLE_INTERNAL_ES.identifiers],
+                            },
+                        ],
+                        type: 'basic',
+                    },
                 ],
-              },
             },
-            to: [
-              {
-                shell_command: 'osascript ~/.zsh/bin/karabiner-kill.scpt',
-              },
-            ],
-          },
-        ],
-      },
-      {
-        description:
-          'Change Caps Lock to Control when used as modifier, Backspace when used alone',
-        manipulators: [
-          {
-            from: {
-              key_code: 'caps_lock',
-              modifiers: {
-                optional: ['any'],
-              },
+            {
+                description:
+                    'Disable Karabiner-Elements with Fn+Control+Option+Command+Z',
+                manipulators: [
+                    {
+                        type: 'basic',
+                        from: {
+                            key_code: 'z',
+                            modifiers: {
+                                mandatory: [
+                                    'fn',
+                                    'left_control',
+                                    'left_command',
+                                    'left_option',
+                                ],
+                            },
+                        },
+                        to: [
+                            {
+                                shell_command:
+                                    'osascript ~/.zsh/bin/karabiner-kill.scpt',
+                            },
+                        ],
+                    },
+                ],
             },
-            to: [
-              {
-                key_code: 'left_control',
-                lazy: true,
-              },
-            ],
-            to_if_alone: [
-              {
-                key_code: 'delete_or_backspace',
-              },
-            ],
-            to_if_held_down: [
-              {
-                key_code: 'delete_or_backspace',
-              },
-            ],
-            type: 'basic',
-          },
-        ],
-      },
-      {
-        description:
-          'Change Return to Control when used as modifier, Return when used alone',
-        manipulators: [
-          {
-            from: {
-              key_code: 'return_or_enter',
-              modifiers: {
-                optional: ['any'],
-              },
+            {
+                description:
+                    'Change Caps Lock to Control when used as modifier, Backspace when used alone',
+                manipulators: [
+                    {
+                        from: {
+                            key_code: 'caps_lock',
+                            modifiers: {
+                                optional: ['any'],
+                            },
+                        },
+                        to: [
+                            {
+                                key_code: 'left_control',
+                                lazy: true,
+                            },
+                        ],
+                        to_if_alone: [
+                            {
+                                key_code: 'delete_or_backspace',
+                            },
+                        ],
+                        to_if_held_down: [
+                            {
+                                key_code: 'delete_or_backspace',
+                            },
+                        ],
+                        type: 'basic',
+                    },
+                ],
             },
-            to: [
-              {
-                key_code: 'right_control',
-                lazy: true,
-              },
-            ],
-            to_if_alone: [
-              {
-                key_code: 'return_or_enter',
-              },
-            ],
-            to_if_held_down: [
-              {
-                key_code: 'return_or_enter',
-              },
-            ],
-            type: 'basic',
-          },
-        ],
-      },
-      {
-        description: 'Change Control+I to F6 in Vim',
-        manipulators: [
-          {
-            conditions: [
-              {
-                bundle_identifiers: [
-                  bundleIdentifier('com.apple.Terminal'),
-                  bundleIdentifier('com.googlecode.iterm2'),
-                  bundleIdentifier('org.vim.MacVim.plist'),
+            {
+                description:
+                    'Change Return to Control when used as modifier, Return when used alone',
+                manipulators: [
+                    {
+                        from: {
+                            key_code: 'return_or_enter',
+                            modifiers: {
+                                optional: ['any'],
+                            },
+                        },
+                        to: [
+                            {
+                                key_code: 'right_control',
+                                lazy: true,
+                            },
+                        ],
+                        to_if_alone: [
+                            {
+                                key_code: 'return_or_enter',
+                            },
+                        ],
+                        to_if_held_down: [
+                            {
+                                key_code: 'return_or_enter',
+                            },
+                        ],
+                        type: 'basic',
+                    },
                 ],
-                type: 'frontmost_application_if',
-              },
-            ],
-            from: {
-              key_code: 'l',
-              modifiers: {
-                mandatory: ['control'],
-                optional: ['any'],
-              },
             },
-            to: [
-              {
-                key_code: 'f6',
-                modifiers: ['fn'],
-              },
-            ],
-            type: 'basic',
-          },
-        ],
-      },
-      {
-        description: 'Left and Right Shift together toggle Caps Lock',
-        manipulators: [
-          {
-            from: {
-              modifiers: {
-                optional: ['any'],
-              },
-              simultaneous: [
-                {
-                  key_code: 'left_shift',
-                },
-                {
-                  key_code: 'right_shift',
-                },
-              ],
-              simultaneous_options: {
-                key_down_order: 'insensitive',
-                key_up_order: 'insensitive',
-              },
+            {
+                description: 'Change Control+I to F6 in Vim',
+                manipulators: [
+                    {
+                        conditions: [
+                            {
+                                bundle_identifiers: [
+                                    bundleIdentifier('com.apple.Terminal'),
+                                    bundleIdentifier('com.googlecode.iterm2'),
+                                    bundleIdentifier('org.vim.MacVim.plist'),
+                                ],
+                                type: 'frontmost_application_if',
+                            },
+                        ],
+                        from: {
+                            key_code: 'l',
+                            modifiers: {
+                                mandatory: ['control'],
+                                optional: ['any'],
+                            },
+                        },
+                        to: [
+                            {
+                                key_code: 'f6',
+                                modifiers: ['fn'],
+                            },
+                        ],
+                        type: 'basic',
+                    },
+                ],
+            },
+            {
+                description: 'Left and Right Shift together toggle Caps Lock',
+                manipulators: [
+                    {
+                        from: {
+                            modifiers: {
+                                optional: ['any'],
+                            },
+                            simultaneous: [
+                                {
+                                    key_code: 'left_shift',
+                                },
+                                {
+                                    key_code: 'right_shift',
+                                },
+                            ],
+                            simultaneous_options: {
+                                key_down_order: 'insensitive',
+                                key_up_order: 'insensitive',
+                            },
+                        },
+                        to: [
+                            {
+                                key_code: 'caps_lock',
+                            },
+                        ],
+                        type: 'basic',
+                    },
+                ],
             },
-            to: [
-              {
-                key_code: 'caps_lock',
-              },
-            ],
-            type: 'basic',
-          },
         ],
-      },
-    ],
-  },
-  devices: [REALFORCE, APPLE_INTERNAL_US, APPLE_INTERNAL_ES],
-  name: 'Default',
-  selected: true,
+    },
+    devices: [REALFORCE, APPLE_INTERNAL_US, APPLE_INTERNAL_ES],
+    name: 'Default',
+    selected: true,
 });
 
 const CONFIG = {
-  global: {
-    check_for_updates_on_startup: true,
-    show_in_menu_bar: true,
-    show_profile_name_in_menu_bar: false,
-  },
-  profiles: [DEFAULT_PROFILE, VANILLA_PROFILE],
+    global: {
+        check_for_updates_on_startup: true,
+        show_in_menu_bar: true,
+        show_profile_name_in_menu_bar: false,
+    },
+    profiles: [DEFAULT_PROFILE, VANILLA_PROFILE],
 };
 
 if (require.main === module) {
-  // Script is being executed directly.
-  process.stdout.write(JSON.stringify(CONFIG, null, 2) + '\n');
+    // Script is being executed directly.
+    process.stdout.write(JSON.stringify(CONFIG, null, 2) + '\n');
 } else {
-  // File is being `require`-ed as a module.
-  module.exports = {
-    bundleIdentifier,
-    deepCopy,
-    isObject,
-    visit,
-  };
+    // File is being `require`-ed as a module.
+    module.exports = {
+        bundleIdentifier,
+        deepCopy,
+        isObject,
+        visit,
+    };
 }
index 37e592af95cab6bdfc5574777e47870ebe4ec330..eeae76ee18d7f5d2778ec81954fef7009a60e25f 100644 (file)
 {
-  "Profiles": [
-    {
-      "Name": "Base",
-      "Guid": "D70AD504-EFA6-41E2-AF8C-7EC47B16C6B8",
-      "Custom Directory": "No",
-      "Set Local Environment Vars": true,
-      "Working Directory": "/Users/glh",
-      "Prompt Before Closing 2": 0,
-      "Rows": 25,
-      "Use Italic Font": true,
-      "Use Custom Window Title": false,
-      "Right Option Key Sends": 0,
-      "Character Encoding": 4,
-      "Triggers": [],
-      "Blend": 0.300000011920929,
-      "Mouse Reporting": true,
-      "Cursor Boost": 0,
-      "Non-ASCII Anti Aliased": true,
-      "Sync Title": false,
-      "Disable Window Resizing": true,
-      "Close Sessions On End": true,
-      "Jobs to Ignore": ["rlogin", "ssh", "slogin", "telnet"],
-      "Scrollback With Status Bar": false,
-      "Scrollback Lines": 0,
-      "Scrollback in Alternate Screen": false,
-      "Hide After Opening": false,
-      "Flashing Bell": false,
-      "BM Growl": false,
-      "AWDS Window Directory": "",
-      "Use Non-ASCII Font": false,
-      "Shortcut": "",
-      "Background Image Location": "",
-      "Unlimited Scrollback": false,
-      "Custom Command": "No",
-      "AWDS Tab Option": "No",
-      "Smart Selection Rules": [
+    "Profiles": [
         {
-          "notes": "Word bounded by whitespace",
-          "regex": "\\S+",
-          "precision": "low"
-        },
-        {
-          "notes": "C++ namespace::identifier",
-          "regex": "([a-zA-Z0-9_]+::)+[a-zA-Z0-9_]+",
-          "precision": "normal"
-        },
-        {
-          "notes": "Paths",
-          "regex": "\\~?/?([[:letter:][:number:]._-]+/+)+[[:letter:][:number:]._-]+/?",
-          "precision": "normal"
-        },
-        {
-          "notes": "Quoted string",
-          "regex": "@?\"(?:[^\"\\\\]|\\\\.)*\"",
-          "precision": "normal"
-        },
-        {
-          "notes": "Java/Python include paths",
-          "regex": "([[:letter:][:number:]._]+\\.)+[[:letter:][:number:]._]+",
-          "precision": "normal"
-        },
-        {
-          "notes": "mailto URL",
-          "regex": "\\bmailto:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
-          "precision": "normal"
-        },
-        {
-          "notes": "Obj-C selector",
-          "regex": "@selector\\([^)]+\\)",
-          "precision": "high"
-        },
-        {
-          "notes": "email address",
-          "regex": "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}\\b",
-          "precision": "high"
-        },
-        {
-          "notes": "HTTP URL",
-          "regex": "https?://([a-z0-9A-Z]+(:[a-zA-Z0-9]+)?@)?[a-z0-9A-Z]+(\\.[a-z0-9A-Z]+)*((:[0-9]+)?)(/[a-zA-Z0-9;/\\.\\-_+%~?&@=#\\(\\)]*)?",
-          "precision": "very_high"
-        },
-        {
-          "notes": "SSH URL",
-          "regex": "\\bssh:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
-          "precision": "very_high"
-        },
-        {
-          "notes": "Telnet URL",
-          "regex": "\\btelnet:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
-          "precision": "very_high"
-        }
-      ],
-      "Keyboard Map": {
-        "0x9-0x40000": {
-          "Text": "9",
-          "Action": 11
-        },
-        "0xf700-0x260000": {
-          "Text": "[1;6A",
-          "Action": 10
-        },
-        "0x37-0x40000": {
-          "Text": "0x1f",
-          "Action": 11
-        },
-        "0x32-0x40000": {
-          "Text": "0x00",
-          "Action": 11
-        },
-        "0xf709-0x20000": {
-          "Text": "[17;2~",
-          "Action": 10
-        },
-        "0xf70c-0x20000": {
-          "Text": "[20;2~",
-          "Action": 10
-        },
-        "0xf729-0x20000": {
-          "Text": "[1;2H",
-          "Action": 10
-        },
-        "0xf72b-0x40000": {
-          "Text": "[1;5F",
-          "Action": 10
-        },
-        "0xf705-0x20000": {
-          "Text": "[1;2Q",
-          "Action": 10
-        },
-        "0xf703-0x260000": {
-          "Text": "[1;6C",
-          "Action": 10
-        },
-        "0xf700-0x220000": {
-          "Text": "[1;2A",
-          "Action": 10
-        },
-        "0xf701-0x280000": {
-          "Text": "0x1b 0x1b 0x5b 0x42",
-          "Action": 11
-        },
-        "0x38-0x40000": {
-          "Text": "0x7f",
-          "Action": 11
-        },
-        "0x33-0x40000": {
-          "Text": "0x1b",
-          "Action": 11
-        },
-        "0xf703-0x220000": {
-          "Text": "[1;2C",
-          "Action": 10
-        },
-        "0xf701-0x240000": {
-          "Text": "[1;5B",
-          "Action": 10
-        },
-        "0xf70d-0x20000": {
-          "Text": "[21;2~",
-          "Action": 10
-        },
-        "0xf702-0x260000": {
-          "Text": "[1;6D",
-          "Action": 10
-        },
-        "0xf729-0x40000": {
-          "Text": "[1;5H",
-          "Action": 10
-        },
-        "0xf706-0x20000": {
-          "Text": "[1;2R",
-          "Action": 10
-        },
-        "0x34-0x40000": {
-          "Text": "0x1c",
-          "Action": 11
-        },
-        "0xf700-0x280000": {
-          "Text": "0x1b 0x1b 0x5b 0x41",
-          "Action": 11
-        },
-        "0x2d-0x40000": {
-          "Text": "0x1f",
-          "Action": 11
-        },
-        "0xf70e-0x20000": {
-          "Text": "[23;2~",
-          "Action": 10
-        },
-        "0xf702-0x220000": {
-          "Text": "[1;2D",
-          "Action": 10
-        },
-        "0xf703-0x280000": {
-          "Text": "f",
-          "Action": 10
-        },
-        "0xf700-0x240000": {
-          "Text": "[1;5A",
-          "Action": 10
-        },
-        "0xf707-0x20000": {
-          "Text": "[1;2S",
-          "Action": 10
-        },
-        "0xf70a-0x20000": {
-          "Text": "[18;2~",
-          "Action": 10
-        },
-        "0x35-0x40000": {
-          "Text": "0x1d",
-          "Action": 11
-        },
-        "0xf70f-0x20000": {
-          "Text": "[24;2~",
-          "Action": 10
-        },
-        "0xf703-0x240000": {
-          "Text": "[1;5C",
-          "Action": 10
-        },
-        "0xf701-0x260000": {
-          "Text": "[1;6B",
-          "Action": 10
-        },
-        "0xf702-0x280000": {
-          "Text": "b",
-          "Action": 10
-        },
-        "0xf72b-0x20000": {
-          "Text": "[1;2F",
-          "Action": 10
-        },
-        "0x36-0x40000": {
-          "Text": "0x1e",
-          "Action": 11
-        },
-        "0xf708-0x20000": {
-          "Text": "[15;2~",
-          "Action": 10
-        },
-        "0xf701-0x220000": {
-          "Text": "[1;2B",
-          "Action": 10
-        },
-        "0xf70b-0x20000": {
-          "Text": "[19;2~",
-          "Action": 10
-        },
-        "0xf702-0x240000": {
-          "Text": "[1;5D",
-          "Action": 10
-        },
-        "0xf704-0x20000": {
-          "Text": "[1;2P",
-          "Action": 10
+            "Name": "Base",
+            "Guid": "D70AD504-EFA6-41E2-AF8C-7EC47B16C6B8",
+            "Custom Directory": "No",
+            "Set Local Environment Vars": true,
+            "Working Directory": "/Users/glh",
+            "Prompt Before Closing 2": 0,
+            "Rows": 25,
+            "Use Italic Font": true,
+            "Use Custom Window Title": false,
+            "Right Option Key Sends": 0,
+            "Character Encoding": 4,
+            "Triggers": [],
+            "Blend": 0.300000011920929,
+            "Mouse Reporting": true,
+            "Cursor Boost": 0,
+            "Non-ASCII Anti Aliased": true,
+            "Sync Title": false,
+            "Disable Window Resizing": true,
+            "Close Sessions On End": true,
+            "Jobs to Ignore": ["rlogin", "ssh", "slogin", "telnet"],
+            "Scrollback With Status Bar": false,
+            "Scrollback Lines": 0,
+            "Scrollback in Alternate Screen": false,
+            "Hide After Opening": false,
+            "Flashing Bell": false,
+            "BM Growl": false,
+            "AWDS Window Directory": "",
+            "Use Non-ASCII Font": false,
+            "Shortcut": "",
+            "Background Image Location": "",
+            "Unlimited Scrollback": false,
+            "Custom Command": "No",
+            "AWDS Tab Option": "No",
+            "Smart Selection Rules": [
+                {
+                    "notes": "Word bounded by whitespace",
+                    "regex": "\\S+",
+                    "precision": "low"
+                },
+                {
+                    "notes": "C++ namespace::identifier",
+                    "regex": "([a-zA-Z0-9_]+::)+[a-zA-Z0-9_]+",
+                    "precision": "normal"
+                },
+                {
+                    "notes": "Paths",
+                    "regex": "\\~?/?([[:letter:][:number:]._-]+/+)+[[:letter:][:number:]._-]+/?",
+                    "precision": "normal"
+                },
+                {
+                    "notes": "Quoted string",
+                    "regex": "@?\"(?:[^\"\\\\]|\\\\.)*\"",
+                    "precision": "normal"
+                },
+                {
+                    "notes": "Java/Python include paths",
+                    "regex": "([[:letter:][:number:]._]+\\.)+[[:letter:][:number:]._]+",
+                    "precision": "normal"
+                },
+                {
+                    "notes": "mailto URL",
+                    "regex": "\\bmailto:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
+                    "precision": "normal"
+                },
+                {
+                    "notes": "Obj-C selector",
+                    "regex": "@selector\\([^)]+\\)",
+                    "precision": "high"
+                },
+                {
+                    "notes": "email address",
+                    "regex": "\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}\\b",
+                    "precision": "high"
+                },
+                {
+                    "notes": "HTTP URL",
+                    "regex": "https?://([a-z0-9A-Z]+(:[a-zA-Z0-9]+)?@)?[a-z0-9A-Z]+(\\.[a-z0-9A-Z]+)*((:[0-9]+)?)(/[a-zA-Z0-9;/\\.\\-_+%~?&@=#\\(\\)]*)?",
+                    "precision": "very_high"
+                },
+                {
+                    "notes": "SSH URL",
+                    "regex": "\\bssh:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
+                    "precision": "very_high"
+                },
+                {
+                    "notes": "Telnet URL",
+                    "regex": "\\btelnet:([a-z0-9A-Z_]+@)?([a-zA-Z0-9\\-]+\\.)*[a-zA-Z0-9\\-]+\\b",
+                    "precision": "very_high"
+                }
+            ],
+            "Keyboard Map": {
+                "0x9-0x40000": {
+                    "Text": "9",
+                    "Action": 11
+                },
+                "0xf700-0x260000": {
+                    "Text": "[1;6A",
+                    "Action": 10
+                },
+                "0x37-0x40000": {
+                    "Text": "0x1f",
+                    "Action": 11
+                },
+                "0x32-0x40000": {
+                    "Text": "0x00",
+                    "Action": 11
+                },
+                "0xf709-0x20000": {
+                    "Text": "[17;2~",
+                    "Action": 10
+                },
+                "0xf70c-0x20000": {
+                    "Text": "[20;2~",
+                    "Action": 10
+                },
+                "0xf729-0x20000": {
+                    "Text": "[1;2H",
+                    "Action": 10
+                },
+                "0xf72b-0x40000": {
+                    "Text": "[1;5F",
+                    "Action": 10
+                },
+                "0xf705-0x20000": {
+                    "Text": "[1;2Q",
+                    "Action": 10
+                },
+                "0xf703-0x260000": {
+                    "Text": "[1;6C",
+                    "Action": 10
+                },
+                "0xf700-0x220000": {
+                    "Text": "[1;2A",
+                    "Action": 10
+                },
+                "0xf701-0x280000": {
+                    "Text": "0x1b 0x1b 0x5b 0x42",
+                    "Action": 11
+                },
+                "0x38-0x40000": {
+                    "Text": "0x7f",
+                    "Action": 11
+                },
+                "0x33-0x40000": {
+                    "Text": "0x1b",
+                    "Action": 11
+                },
+                "0xf703-0x220000": {
+                    "Text": "[1;2C",
+                    "Action": 10
+                },
+                "0xf701-0x240000": {
+                    "Text": "[1;5B",
+                    "Action": 10
+                },
+                "0xf70d-0x20000": {
+                    "Text": "[21;2~",
+                    "Action": 10
+                },
+                "0xf702-0x260000": {
+                    "Text": "[1;6D",
+                    "Action": 10
+                },
+                "0xf729-0x40000": {
+                    "Text": "[1;5H",
+                    "Action": 10
+                },
+                "0xf706-0x20000": {
+                    "Text": "[1;2R",
+                    "Action": 10
+                },
+                "0x34-0x40000": {
+                    "Text": "0x1c",
+                    "Action": 11
+                },
+                "0xf700-0x280000": {
+                    "Text": "0x1b 0x1b 0x5b 0x41",
+                    "Action": 11
+                },
+                "0x2d-0x40000": {
+                    "Text": "0x1f",
+                    "Action": 11
+                },
+                "0xf70e-0x20000": {
+                    "Text": "[23;2~",
+                    "Action": 10
+                },
+                "0xf702-0x220000": {
+                    "Text": "[1;2D",
+                    "Action": 10
+                },
+                "0xf703-0x280000": {
+                    "Text": "f",
+                    "Action": 10
+                },
+                "0xf700-0x240000": {
+                    "Text": "[1;5A",
+                    "Action": 10
+                },
+                "0xf707-0x20000": {
+                    "Text": "[1;2S",
+                    "Action": 10
+                },
+                "0xf70a-0x20000": {
+                    "Text": "[18;2~",
+                    "Action": 10
+                },
+                "0x35-0x40000": {
+                    "Text": "0x1d",
+                    "Action": 11
+                },
+                "0xf70f-0x20000": {
+                    "Text": "[24;2~",
+                    "Action": 10
+                },
+                "0xf703-0x240000": {
+                    "Text": "[1;5C",
+                    "Action": 10
+                },
+                "0xf701-0x260000": {
+                    "Text": "[1;6B",
+                    "Action": 10
+                },
+                "0xf702-0x280000": {
+                    "Text": "b",
+                    "Action": 10
+                },
+                "0xf72b-0x20000": {
+                    "Text": "[1;2F",
+                    "Action": 10
+                },
+                "0x36-0x40000": {
+                    "Text": "0x1e",
+                    "Action": 11
+                },
+                "0xf708-0x20000": {
+                    "Text": "[15;2~",
+                    "Action": 10
+                },
+                "0xf701-0x220000": {
+                    "Text": "[1;2B",
+                    "Action": 10
+                },
+                "0xf70b-0x20000": {
+                    "Text": "[19;2~",
+                    "Action": 10
+                },
+                "0xf702-0x240000": {
+                    "Text": "[1;5D",
+                    "Action": 10
+                },
+                "0xf704-0x20000": {
+                    "Text": "[1;2P",
+                    "Action": 10
+                }
+            },
+            "Log Directory": "",
+            "Use Canonical Parser": false,
+            "Background Image Is Tiled": false,
+            "Send Code When Idle": false,
+            "ASCII Anti Aliased": true,
+            "Tags": [],
+            "Use Bold Font": true,
+            "Silence Bell": true,
+            "Window Type": 12,
+            "Allow Title Reporting": false,
+            "Use Bright Bold": true,
+            "Default Bookmark": "No",
+            "Disable Smcup Rmcup": false,
+            "Blinking Cursor": false,
+            "Idle Code": 0,
+            "Automatically Log": false,
+            "Smart Cursor Color": true,
+            "Semantic History": {
+                "text": "vim \\1",
+                "action": "best editor",
+                "editor": "com.sublimetext.3"
+            },
+            "Ambiguous Double Width": false,
+            "Blur Radius": 15.08386418269231,
+            "Cursor Type": 2,
+            "AWDS Pane Directory": "",
+            "Blur": false,
+            "Normal Font": "SourceCodePro-Light 13",
+            "Vertical Spacing": 1,
+            "Disable Printing": false,
+            "AWDS Tab Directory": "",
+            "AWDS Pane Option": "No",
+            "Command": "",
+            "Terminal Type": "xterm-256color",
+            "Horizontal Spacing": 1,
+            "Option Key Sends": 0,
+            "Only The Default BG Color Uses Transparency": false,
+            "Blink Allowed": false,
+            "Minimum Contrast": 0.0,
+            "Transparency": 0,
+            "Initial Text": "",
+            "Screen": -1,
+            "AWDS Window Option": "No",
+            "Non Ascii Font": "Consolas 13",
+            "Columns": 80,
+            "Visual Bell": true,
+            "ASCII Ligatures": false,
+            "Thin Strokes": 1
         }
-      },
-      "Log Directory": "",
-      "Use Canonical Parser": false,
-      "Background Image Is Tiled": false,
-      "Send Code When Idle": false,
-      "ASCII Anti Aliased": true,
-      "Tags": [],
-      "Use Bold Font": true,
-      "Silence Bell": true,
-      "Window Type": 12,
-      "Allow Title Reporting": false,
-      "Use Bright Bold": true,
-      "Default Bookmark": "No",
-      "Disable Smcup Rmcup": false,
-      "Blinking Cursor": false,
-      "Idle Code": 0,
-      "Automatically Log": false,
-      "Smart Cursor Color": true,
-      "Semantic History": {
-        "text": "vim \\1",
-        "action": "best editor",
-        "editor": "com.sublimetext.3"
-      },
-      "Ambiguous Double Width": false,
-      "Blur Radius": 15.08386418269231,
-      "Cursor Type": 2,
-      "AWDS Pane Directory": "",
-      "Blur": false,
-      "Normal Font": "SourceCodePro-Light 13",
-      "Vertical Spacing": 1,
-      "Disable Printing": false,
-      "AWDS Tab Directory": "",
-      "AWDS Pane Option": "No",
-      "Command": "",
-      "Terminal Type": "xterm-256color",
-      "Horizontal Spacing": 1,
-      "Option Key Sends": 0,
-      "Only The Default BG Color Uses Transparency": false,
-      "Blink Allowed": false,
-      "Minimum Contrast": 0.0,
-      "Transparency": 0,
-      "Initial Text": "",
-      "Screen": -1,
-      "AWDS Window Option": "No",
-      "Non Ascii Font": "Consolas 13",
-      "Columns": 80,
-      "Visual Bell": true,
-      "ASCII Ligatures": false,
-      "Thin Strokes": 1
-    }
-  ]
+    ]
 }
index 715de36c289cdbf271c3301a264887c8aa7d2496..77cc8a0e9ad4cac4b26e87f9c4e04be31ee5360d 100644 (file)
@@ -1,11 +1,11 @@
 {
-  "Profiles": [
-    {
-      "Name": "Mutt (Base)",
-      "Guid": "2AAB5F82-9007-41DA-B772-5E776FD41383",
-      "Dynamic Profile Parent Name": "Base",
-      "Custom Command": "Yes",
-      "Command": "/usr/local/bin/zsh -ic \"/usr/local/bin/mutt -- $$URL$$\""
-    }
-  ]
+    "Profiles": [
+        {
+            "Name": "Mutt (Base)",
+            "Guid": "2AAB5F82-9007-41DA-B772-5E776FD41383",
+            "Dynamic Profile Parent Name": "Base",
+            "Custom Command": "Yes",
+            "Command": "/usr/local/bin/zsh -ic \"/usr/local/bin/mutt -- $$URL$$\""
+        }
+    ]
 }
index b8bc7c4b3f7e93949d1ee43edb187c9a54c5ef33..bb3fc6f0ec370ad849d8652bd6c803bc5556c184 100644 (file)
@@ -1,11 +1,11 @@
 {
-  "Profiles": [
-    {
-      "Name": "Vim (Base)",
-      "Guid": "8C012B27-65E8-470D-83FA-B5485479863D",
-      "Dynamic Profile Parent Name": "Base",
-      "Custom Command": "Yes",
-      "Command": "/usr/local/bin/zsh -ic \"/usr/local/bin/nvim $$RES$$\""
-    }
-  ]
+    "Profiles": [
+        {
+            "Name": "Vim (Base)",
+            "Guid": "8C012B27-65E8-470D-83FA-B5485479863D",
+            "Dynamic Profile Parent Name": "Base",
+            "Custom Command": "Yes",
+            "Command": "/usr/local/bin/zsh -ic \"/usr/local/bin/nvim $$RES$$\""
+        }
+    ]
 }
index 35c88eb793ca3986137518a9032076d119d78d22..27935108d8b46ea1015bec72884bfc02a95f119e 100644 (file)
@@ -1,10 +1,10 @@
 {
-  "Profiles": [
-    {
-      "Name": "Wincent",
-      "Guid": "C9389479-10A0-4D67-BCDF-80BA2CA3A3D6",
-      "Dynamic Profile Parent Name": "Base",
-      "Normal Font": "SourceCodePro-Light 16"
-    }
-  ]
+    "Profiles": [
+        {
+            "Name": "Wincent",
+            "Guid": "C9389479-10A0-4D67-BCDF-80BA2CA3A3D6",
+            "Dynamic Profile Parent Name": "Base",
+            "Normal Font": "SourceCodePro-Light 16"
+        }
+    ]
 }
index 7174403e846c94730288d88619b62da93e7012c3..092046d88c3441dcef6c2268fe17caea5a2dd1dc 100644 (file)
@@ -1,10 +1,10 @@
 {
-  "Profiles": [
-    {
-      "Name": "Wincent",
-      "Guid": "C9389479-10A0-4D67-BCDF-80BA2CA3A3D6",
-      "Dynamic Profile Parent Name": "Base",
-      "Normal Font": "SourceCodePro-Light 13"
-    }
-  ]
+    "Profiles": [
+        {
+            "Name": "Wincent",
+            "Guid": "C9389479-10A0-4D67-BCDF-80BA2CA3A3D6",
+            "Dynamic Profile Parent Name": "Base",
+            "Normal Font": "SourceCodePro-Light 13"
+        }
+    ]
 }
index 5eb42d9e5c546a3e069f7baac0d34da0c8439902..60bd2ff9021ffe62e6c1bfcd6fc73d1c081ae853 100644 (file)
@@ -1,10 +1,10 @@
 {
-  "Profiles": [
-    {
-      "Name": "Mutt",
-      "Guid": "98A7362E-A930-4331-A7DD-E4FC57EFBEEB",
-      "Dynamic Profile Parent Name": "Mutt (Base)",
-      "Normal Font": "SourceCodePro-Light 16"
-    }
-  ]
+    "Profiles": [
+        {
+            "Name": "Mutt",
+            "Guid": "98A7362E-A930-4331-A7DD-E4FC57EFBEEB",
+            "Dynamic Profile Parent Name": "Mutt (Base)",
+            "Normal Font": "SourceCodePro-Light 16"
+        }
+    ]
 }
index 6d736596ebf7ed520d22a88a387fb9b47c8d84cb..d0850d654df035287ea3f8d6d2a36aac1b29f06d 100644 (file)
@@ -1,10 +1,10 @@
 {
-  "Profiles": [
-    {
-      "Name": "Mutt",
-      "Guid": "98A7362E-A930-4331-A7DD-E4FC57EFBEEB",
-      "Dynamic Profile Parent Name": "Mutt (Base)",
-      "Normal Font": "SourceCodePro-Light 13"
-    }
-  ]
+    "Profiles": [
+        {
+            "Name": "Mutt",
+            "Guid": "98A7362E-A930-4331-A7DD-E4FC57EFBEEB",
+            "Dynamic Profile Parent Name": "Mutt (Base)",
+            "Normal Font": "SourceCodePro-Light 13"
+        }
+    ]
 }
index 3dc34851ee23b935c95ed6c81c239351bc14076d..62610d15a7f29192c5731187b3aeb8753f09cd87 100644 (file)
@@ -1,10 +1,10 @@
 {
-  "Profiles": [
-    {
-      "Name": "Vim",
-      "Guid": "1770FCDD-BCA2-403B-BABF-A6B2D994D725",
-      "Dynamic Profile Parent Name": "Vim (Base)",
-      "Normal Font": "SourceCodePro-Light 16"
-    }
-  ]
+    "Profiles": [
+        {
+            "Name": "Vim",
+            "Guid": "1770FCDD-BCA2-403B-BABF-A6B2D994D725",
+            "Dynamic Profile Parent Name": "Vim (Base)",
+            "Normal Font": "SourceCodePro-Light 16"
+        }
+    ]
 }
index c5dae685e662f956aa7f6640a264cd0c4a4cc8ce..c7c4e2db92307f83cca9ad728c626aed4bab3b7c 100644 (file)
@@ -1,10 +1,10 @@
 {
-  "Profiles": [
-    {
-      "Name": "Vim",
-      "Guid": "18A475CE-8744-4C20-AFAE-AAEBF4F20F40",
-      "Dynamic Profile Parent Name": "Vim (Base)",
-      "Normal Font": "SourceCodePro-Light 13"
-    }
-  ]
+    "Profiles": [
+        {
+            "Name": "Vim",
+            "Guid": "18A475CE-8744-4C20-AFAE-AAEBF4F20F40",
+            "Dynamic Profile Parent Name": "Vim (Base)",
+            "Normal Font": "SourceCodePro-Light 13"
+        }
+    ]
 }
index 913f7d737df4e84d14588a28e3a339f95fd24bf9..cd86b9c393116a87138654a3ee1ae1f9f5622469 100644 (file)
@@ -4,48 +4,51 @@ import * as os from 'os';
  * Immutable system attributes (read-only).
  */
 export default class Attributes {
-  #homedir?: string;
-  #platform?: 'darwin' | 'linux';
-  #uid?: number;
-  #username?: string;
-
-  get homedir(): string {
-    if (!this.#homedir) {
-      this.#homedir = os.homedir();
-    }
+    #homedir?: string;
+    #platform?: 'darwin' | 'linux';
+    #uid?: number;
+    #username?: string;
 
-    return this.#homedir;
-  }
+    get homedir(): string {
+        if (!this.#homedir) {
+            this.#homedir = os.homedir();
+        }
 
-  get platform(): 'darwin' | 'linux' {
-    if (!this.#platform) {
-      const uname = os.type();
+        return this.#homedir;
+    }
 
-      if (uname === 'Darwin') {
-        this.#platform = 'darwin';
-      } else if (uname === 'Linux') {
-        this.#platform = 'linux';
-      } else {
-        throw new Error(`Unsupported platform ${JSON.stringify(uname)}`);
-      }
+    get platform(): 'darwin' | 'linux' {
+        if (!this.#platform) {
+            const uname = os.type();
+
+            if (uname === 'Darwin') {
+                this.#platform = 'darwin';
+            } else if (uname === 'Linux') {
+                this.#platform = 'linux';
+            } else {
+                throw new Error(
+                    `Unsupported platform ${JSON.stringify(uname)}`
+                );
+            }
+        }
+
+        return this.#platform;
     }
 
-    return this.#platform;
-  }
+    get uid(): number {
+        if (typeof this.#uid !== 'number') {
+            this.#uid =
+                typeof process.getuid === 'function' ? process.getuid() : -1;
+        }
 
-  get uid(): number {
-    if (typeof this.#uid !== 'number') {
-      this.#uid = typeof process.getuid === 'function' ? process.getuid() : -1;
+        return this.#uid;
     }
 
-    return this.#uid;
-  }
+    get username(): string {
+        if (!this.#username) {
+            this.#username = os.userInfo().username;
+        }
 
-  get username(): string {
-    if (!this.#username) {
-      this.#username = os.userInfo().username;
+        return this.#username;
     }
-
-    return this.#username;
-  }
 }
index ea8c44d52320bc592c5f1102ebf91065515d0a13..8c9d3e501a6b4d65d432c943729fa500e92d450d 100644 (file)
@@ -11,27 +11,27 @@ const readFile = promisify(readFileAsync);
  * Template compiler that manages a cache of compiled templates.
  */
 export default class Compiler {
-  #compiled: Map<string, {fill: (scope: Scope) => string}>;
+    #compiled: Map<string, {fill: (scope: Scope) => string}>;
 
-  constructor() {
-    this.#compiled = new Map();
-  }
+    constructor() {
+        this.#compiled = new Map();
+    }
 
-  async compile(path: string): Promise<{fill: (scope: Scope) => string}> {
-    const map = this.#compiled;
+    async compile(path: string): Promise<{fill: (scope: Scope) => string}> {
+        const map = this.#compiled;
 
-    if (!map.has(path)) {
-      const source = await readFile(path, 'utf8');
+        if (!map.has(path)) {
+            const source = await readFile(path, 'utf8');
 
-      const compiled = compile(source);
+            const compiled = compile(source);
 
-      map.set(path, {
-        fill(scope) {
-          return fill(compiled, scope);
-        },
-      });
-    }
+            map.set(path, {
+                fill(scope) {
+                    return fill(compiled, scope);
+                },
+            });
+        }
 
-    return map.get(path)!;
-  }
+        return map.get(path)!;
+    }
 }
index df7fd1167a58992e1e08e1acb10a6727111d2ca8..a1c806a8527682f48ccc0c002cacb5734876064a 100644 (file)
@@ -1,11 +1,11 @@
 export type Metadata = {[key: string]: JSONValue};
 
 export default class ErrorWithMetadata extends Error {
-  metadata?: Metadata;
+    metadata?: Metadata;
 
-  constructor(message: string, metadata?: Metadata) {
-    super(message);
+    constructor(message: string, metadata?: Metadata) {
+        super(message);
 
-    this.metadata = metadata;
-  }
+        this.metadata = metadata;
+    }
 }
index c4db9ed81209f019e6ed6be0d0abd77cb9f1558a..d86cb0e7aaabd965df55b1ddf40cdd767c1d4b48 100644 (file)
@@ -11,10 +11,10 @@ import type {Metadata} from '../ErrorWithMetadata';
 import type {Aspect} from '../types/Project';
 
 type Counts = {
-  changed: number;
-  failed: number;
-  ok: number;
-  skipped: number;
+    changed: number;
+    failed: number;
+    ok: number;
+    skipped: number;
 };
 
 /**
@@ -24,138 +24,138 @@ type Counts = {
  * possible.
  */
 class Context {
-  #attributes: Attributes;
-  #compiler: Compiler;
-  #counts: Counts;
-  #currentAspect?: Aspect;
-  #currentVariables?: Variables;
-  #sudoPassphrase?: Promise<string>;
-  #tasks: TaskRegistry;
-
-  constructor() {
-    this.#attributes = new Attributes();
-    this.#compiler = new Compiler();
-
-    this.#counts = {
-      changed: 0,
-      failed: 0,
-      ok: 0,
-      skipped: 0,
-    };
-
-    this.#tasks = new TaskRegistry();
-  }
-
-  compile(path: string) {
-    return this.#compiler.compile(path);
-  }
-
-  informChanged(message: string) {
-    this.#counts.changed++;
-
-    status.changed(message);
-  }
-
-  /**
-   * @overload
-   */
-  informFailed(error: ErrorWithMetadata): never;
-
-  /**
-   * @overload
-   */
-  informFailed(message: string, metadata?: Metadata): never;
-
-  informFailed(...args: Array<any>): never {
-    let error: ErrorWithMetadata;
-
-    if (typeof args[0] === 'string') {
-      error = new ErrorWithMetadata(args[0], args[1]);
-    } else {
-      error = args[0];
+    #attributes: Attributes;
+    #compiler: Compiler;
+    #counts: Counts;
+    #currentAspect?: Aspect;
+    #currentVariables?: Variables;
+    #sudoPassphrase?: Promise<string>;
+    #tasks: TaskRegistry;
+
+    constructor() {
+        this.#attributes = new Attributes();
+        this.#compiler = new Compiler();
+
+        this.#counts = {
+            changed: 0,
+            failed: 0,
+            ok: 0,
+            skipped: 0,
+        };
+
+        this.#tasks = new TaskRegistry();
     }
 
-    this.#counts.failed++;
+    compile(path: string) {
+        return this.#compiler.compile(path);
+    }
 
-    status.failed(error.message);
+    informChanged(message: string) {
+        this.#counts.changed++;
 
-    throw error;
-  }
+        status.changed(message);
+    }
 
-  informOk(message: string) {
-    this.#counts.ok++;
+    /**
+     * @overload
+     */
+    informFailed(error: ErrorWithMetadata): never;
 
-    status.ok(message);
-  }
+    /**
+     * @overload
+     */
+    informFailed(message: string, metadata?: Metadata): never;
 
-  informSkipped(message: string) {
-    this.#counts.skipped++;
+    informFailed(...args: Array<any>): never {
+        let error: ErrorWithMetadata;
 
-    status.skipped(message);
-  }
+        if (typeof args[0] === 'string') {
+            error = new ErrorWithMetadata(args[0], args[1]);
+        } else {
+            error = args[0];
+        }
 
-  async withContext(
-    {aspect, variables}: {aspect: Aspect; variables: Variables},
-    callback: () => Promise<void>
-  ) {
-    let previousAspect = this.#currentAspect;
-    let previousVariables = this.#currentVariables;
+        this.#counts.failed++;
 
-    try {
-      this.#currentAspect = aspect;
-      this.#currentVariables = variables;
+        status.failed(error.message);
 
-      await callback();
-    } finally {
-      this.#currentAspect = previousAspect;
-      this.#currentVariables = previousVariables;
+        throw error;
     }
-  }
 
-  get attributes(): Attributes {
-    return this.#attributes;
-  }
+    informOk(message: string) {
+        this.#counts.ok++;
 
-  get counts() {
-    return this.#counts;
-  }
+        status.ok(message);
+    }
 
-  get currentAspect(): Aspect {
-    assert(this.#currentAspect);
+    informSkipped(message: string) {
+        this.#counts.skipped++;
 
-    return this.#currentAspect!;
-  }
+        status.skipped(message);
+    }
 
-  set currentAspect(aspect: Aspect) {
-    this.#currentAspect = aspect;
-  }
+    async withContext(
+        {aspect, variables}: {aspect: Aspect; variables: Variables},
+        callback: () => Promise<void>
+    ) {
+        let previousAspect = this.#currentAspect;
+        let previousVariables = this.#currentVariables;
+
+        try {
+            this.#currentAspect = aspect;
+            this.#currentVariables = variables;
+
+            await callback();
+        } finally {
+            this.#currentAspect = previousAspect;
+            this.#currentVariables = previousVariables;
+        }
+    }
 
-  get currentVariables(): Variables {
-    assert(this.#currentVariables);
+    get attributes(): Attributes {
+        return this.#attributes;
+    }
+
+    get counts() {
+        return this.#counts;
+    }
 
-    return this.#currentVariables!;
-  }
+    get currentAspect(): Aspect {
+        assert(this.#currentAspect);
 
-  set currentVariables(variables: Variables) {
-    this.#currentVariables = variables;
-  }
+        return this.#currentAspect!;
+    }
 
-  get sudoPassphrase(): Promise<string> {
-    if (!this.#sudoPassphrase) {
-      this.#sudoPassphrase = prompt(`Password [will not be echoed]: `, {
-        private: true,
-      });
+    set currentAspect(aspect: Aspect) {
+        this.#currentAspect = aspect;
     }
 
-    return this.#sudoPassphrase;
-  }
+    get currentVariables(): Variables {
+        assert(this.#currentVariables);
 
-  // TODO: note that I might be going overboard here with private
-  // variables for stuff that I really don't have to worry about getting
-  // meddled with
-  get tasks(): TaskRegistry {
-    return this.#tasks;
-  }
+        return this.#currentVariables!;
+    }
+
+    set currentVariables(variables: Variables) {
+        this.#currentVariables = variables;
+    }
+
+    get sudoPassphrase(): Promise<string> {
+        if (!this.#sudoPassphrase) {
+            this.#sudoPassphrase = prompt(`Password [will not be echoed]: `, {
+                private: true,
+            });
+        }
+
+        return this.#sudoPassphrase;
+    }
+
+    // TODO: note that I might be going overboard here with private
+    // variables for stuff that I really don't have to worry about getting
+    // meddled with
+    get tasks(): TaskRegistry {
+        return this.#tasks;
+    }
 }
 
 export default new Context();
index 0812043ac66675d70f15ec4c5e624e0ebb964763..ec28c59b5192073bc02ea102a35ced5862411176 100644 (file)
@@ -3,21 +3,21 @@ import type {Aspect} from '../types/Project';
 type Callback = () => Promise<void>;
 
 export default class TaskRegistry {
-  #callbacks: Map<Aspect, Array<[Callback, string]>>;
+    #callbacks: Map<Aspect, Array<[Callback, string]>>;
 
-  constructor() {
-    this.#callbacks = new Map();
-  }
-
-  register(aspect: Aspect, callback: Callback, name: string) {
-    if (!this.#callbacks.has(aspect)) {
-      this.#callbacks.set(aspect, []);
+    constructor() {
+        this.#callbacks = new Map();
     }
 
-    this.#callbacks.get(aspect)!.push([callback, name]);
-  }
+    register(aspect: Aspect, callback: Callback, name: string) {
+        if (!this.#callbacks.has(aspect)) {
+            this.#callbacks.set(aspect, []);
+        }
+
+        this.#callbacks.get(aspect)!.push([callback, name]);
+    }
 
-  get(aspect: Aspect): Array<[Callback, string]> {
-    return this.#callbacks.get(aspect) || [];
-  }
+    get(aspect: Aspect): Array<[Callback, string]> {
+        return this.#callbacks.get(aspect) || [];
+    }
 }
index fc84d56e6953c49013a1e6b404253848905ec5b1..ffe9a7a2089c8755ef1182ff7d11c5fd840acc2d 100644 (file)
@@ -16,82 +16,82 @@ import root from '../root';
  * Helper to get fixtures (in "src/") irrespective of where we run from.
  */
 function fixture(...components: Array<string>): string {
-  return join(root, 'src', 'Fig', '__tests__', '__fixtures__', ...components);
+    return join(root, 'src', 'Fig', '__tests__', '__fixtures__', ...components);
 }
 
 describe('compare()', () => {
-  describe('with {state: file} (implied)', () => {
-    test('indicates when the file exists', async () => {
-      const path = fixture('sample');
+    describe('with {state: file} (implied)', () => {
+        test('indicates when the file exists', async () => {
+            const path = fixture('sample');
 
-      const diff = await compare({path});
+            const diff = await compare({path});
 
-      expect(diff).toEqual({path});
-    });
+            expect(diff).toEqual({path});
+        });
 
-    test('indicates when contents match', async () => {
-      const path = fixture('sample');
+        test('indicates when contents match', async () => {
+            const path = fixture('sample');
 
-      const diff = await compare({path, contents: 'sample contents\n'});
+            const diff = await compare({path, contents: 'sample contents\n'});
 
-      expect(diff).toEqual({
-        path,
-      });
-    });
+            expect(diff).toEqual({
+                path,
+            });
+        });
 
-    test('indicates when contents do not match', async () => {
-      const path = fixture('sample');
+        test('indicates when contents do not match', async () => {
+            const path = fixture('sample');
 
-      const diff = await compare({path, contents: 'something'});
+            const diff = await compare({path, contents: 'something'});
 
-      expect(diff).toEqual({
-        contents: 'something',
-        path,
-      });
+            expect(diff).toEqual({
+                contents: 'something',
+                path,
+            });
+        });
     });
-  });
 
-  describe('with {state: file} (explicit)', () => {
-    test('returns an "empty" object for files that match', async () => {
-      const path = fixture('sample');
+    describe('with {state: file} (explicit)', () => {
+        test('returns an "empty" object for files that match', async () => {
+            const path = fixture('sample');
 
-      const diff = await compare({path, state: 'file'});
+            const diff = await compare({path, state: 'file'});
 
-      expect(diff).toEqual({path});
-    });
+            expect(diff).toEqual({path});
+        });
 
-    test('returns {state: "file"} for non-existent files', async () => {
-      const path = fixture('non-existent');
+        test('returns {state: "file"} for non-existent files', async () => {
+            const path = fixture('non-existent');
 
-      const diff = await compare({path, state: 'file'});
+            const diff = await compare({path, state: 'file'});
 
-      expect(diff).toEqual({
-        path,
-        state: 'file',
-      });
-    });
+            expect(diff).toEqual({
+                path,
+                state: 'file',
+            });
+        });
 
-    test('complains if parent directory does not exist', async () => {
-      const path = fixture('does', 'not', 'exist');
+        test('complains if parent directory does not exist', async () => {
+            const path = fixture('does', 'not', 'exist');
 
-      const diff = await compare({path, state: 'file'});
+            const diff = await compare({path, state: 'file'});
 
-      expect(diff.path).toEqual(path);
-      expect(diff.error!.message).toMatch(
-        /Cannot stat ".+" because parent ".+" does not exist/
-      );
+            expect(diff.path).toEqual(path);
+            expect(diff.error!.message).toMatch(
+                /Cannot stat ".+" because parent ".+" does not exist/
+            );
+        });
     });
-  });
 
-  describe('with {state: "absent"}', () => {
-    test('returns an "empty" object for missing files', async () => {
-      const path = fixture('non-existent');
+    describe('with {state: "absent"}', () => {
+        test('returns an "empty" object for missing files', async () => {
+            const path = fixture('non-existent');
 
-      const diff = await compare({path, state: 'absent'});
+            const diff = await compare({path, state: 'absent'});
 
-      expect(diff).toEqual({
-        path,
-      });
+            expect(diff).toEqual({
+                path,
+            });
+        });
     });
-  });
 });
index 5e00c11a077b8d7e30bd611ea548c67c94ed29e9..f27bb6bbc236d29f6599c926d9426854e81b8bf5 100644 (file)
@@ -38,14 +38,14 @@ import stat from '../stat';
  * required with respect to that property.
  */
 type Diff = {
-  contents?: string;
-  error?: Error;
-  force?: boolean;
-  group?: string;
-  mode?: Mode;
-  owner?: string;
-  readonly path: string;
-  state?: 'absent' | 'directory' | 'file' | 'link' | 'touch';
+    contents?: string;
+    error?: Error;
+    force?: boolean;
+    group?: string;
+    mode?: Mode;
+    owner?: string;
+    readonly path: string;
+    state?: 'absent' | 'directory' | 'file' | 'link' | 'touch';
 };
 
 type Compare = Omit<Diff, 'error'>;
@@ -58,116 +58,118 @@ const {stringify} = JSON;
  * made to the current file system to produce that desired end-state.
  */
 export default async function compare({
-  contents,
-  force,
-  group,
-  mode,
-  owner,
-  path,
-  state = 'file',
+    contents,
+    force,
+    group,
+    mode,
+    owner,
+    path,
+    state = 'file',
 }: Compare) {
-  const diff: Diff = {path};
+    const diff: Diff = {path};
 
-  const stats = await stat(path);
+    const stats = await stat(path);
 
-  // BUG: if you specify "owner": "root", we should be able to manage files that
-  // only root can stat, but this code stats as an unprivileged user
-  // (to fix this, port to use stat.ts instead
-  if (stats instanceof Error) {
-    // Can't stat; bail.
-    diff.error = stats;
-    return diff;
-  } else if (stats === null) {
-    // Object does not exist.
-    if (state === 'absent') {
-      // Want "absent", have "absent": no state change required.
-    } else {
-      // Distinguish between `path` itself not existing (in which case
-      // it can be created), and one of its parents not existing (in which case
-      // we have to bail).
-      const parent = dirname(path);
-      const stats = await stat(parent);
-      if (stats instanceof Error) {
-        // Unlikely (we were able to stat object but not its parent).
+    // BUG: if you specify "owner": "root", we should be able to manage files that
+    // only root can stat, but this code stats as an unprivileged user
+    // (to fix this, port to use stat.ts instead
+    if (stats instanceof Error) {
+        // Can't stat; bail.
         diff.error = stats;
         return diff;
-      } else if (stats === null) {
-        diff.error = new ErrorWithMetadata(
-          `Cannot stat ${stringify(path)} because parent ${stringify(
-            parent
-          )} does not exist`
-        );
-      } else {
-        // Parent exists.
-        diff.state = state;
-      }
+    } else if (stats === null) {
+        // Object does not exist.
+        if (state === 'absent') {
+            // Want "absent", have "absent": no state change required.
+        } else {
+            // Distinguish between `path` itself not existing (in which case
+            // it can be created), and one of its parents not existing (in which case
+            // we have to bail).
+            const parent = dirname(path);
+            const stats = await stat(parent);
+            if (stats instanceof Error) {
+                // Unlikely (we were able to stat object but not its parent).
+                diff.error = stats;
+                return diff;
+            } else if (stats === null) {
+                diff.error = new ErrorWithMetadata(
+                    `Cannot stat ${stringify(path)} because parent ${stringify(
+                        parent
+                    )} does not exist`
+                );
+            } else {
+                // Parent exists.
+                diff.state = state;
+            }
+        }
+        // Nothing else we can check without the object existing.
+        return diff;
     }
-    // Nothing else we can check without the object existing.
-    return diff;
-  }
 
-  // Object exists.
-  if (state === 'file') {
-    if (stats.type === 'file') {
-      // Want "file", have "file": no state change required.
-    } else if (stats.type === 'link') {
-      // Going to have to overwrite symlink.
-      diff.force = true;
-      diff.state = 'file';
-    } else if (stats.type === 'directory') {
-      diff.error = new ErrorWithMetadata(
-        `Cannot replace directory ${stringify(path)} with file`
-      );
-    } else {
-      // We're not going to bother with "exotic" types such as sockets etc.
-      diff.error = new ErrorWithMetadata(
-        `Cannot replace object ${stringify(path)} of unknown type with file`
-      );
-    }
+    // Object exists.
+    if (state === 'file') {
+        if (stats.type === 'file') {
+            // Want "file", have "file": no state change required.
+        } else if (stats.type === 'link') {
+            // Going to have to overwrite symlink.
+            diff.force = true;
+            diff.state = 'file';
+        } else if (stats.type === 'directory') {
+            diff.error = new ErrorWithMetadata(
+                `Cannot replace directory ${stringify(path)} with file`
+            );
+        } else {
+            // We're not going to bother with "exotic" types such as sockets etc.
+            diff.error = new ErrorWithMetadata(
+                `Cannot replace object ${stringify(
+                    path
+                )} of unknown type with file`
+            );
+        }
 
-    if (typeof contents === 'string') {
-      try {
-        const actual = await fs.readFile(path, 'utf8');
-        if (actual !== contents) {
-          diff.contents = contents;
+        if (typeof contents === 'string') {
+            try {
+                const actual = await fs.readFile(path, 'utf8');
+                if (actual !== contents) {
+                    diff.contents = contents;
+                }
+            } catch (error) {
+                // TODO: if this is a perms issue, that might be ok as long as user has
+                // specified "user"
+            }
         }
-      } catch (error) {
-        // TODO: if this is a perms issue, that might be ok as long as user has
-        // specified "user"
-      }
-    }
-  } else if (state === 'directory') {
-    if (stats.type === 'directory') {
-      // Want "directory", have "directory": no state change required.
-    } else if (stats.type === 'file' || stats.type === 'link') {
-      if (force) {
-        // Will have to remove file/link before creating directory.
-        diff.force = true;
-        diff.state = state;
-      } else {
-        const entity = stats.type === 'file' ? 'file' : 'symbolic link';
+    } else if (state === 'directory') {
+        if (stats.type === 'directory') {
+            // Want "directory", have "directory": no state change required.
+        } else if (stats.type === 'file' || stats.type === 'link') {
+            if (force) {
+                // Will have to remove file/link before creating directory.
+                diff.force = true;
+                diff.state = state;
+            } else {
+                const entity = stats.type === 'file' ? 'file' : 'symbolic link';
 
-        diff.error = new ErrorWithMetadata(
-          `Cannot replace ${entity} ${stringify(
-            path
-          )} with directory without 'force'`
-        );
-      }
-    } else {
-      // We're not going to bother with "exotic" types such as sockets etc.
-      diff.error = new ErrorWithMetadata(
-        `Cannot replace object ${stringify(
-          path
-        )} of unknown type with directory`
-      );
+                diff.error = new ErrorWithMetadata(
+                    `Cannot replace ${entity} ${stringify(
+                        path
+                    )} with directory without 'force'`
+                );
+            }
+        } else {
+            // We're not going to bother with "exotic" types such as sockets etc.
+            diff.error = new ErrorWithMetadata(
+                `Cannot replace object ${stringify(
+                    path
+                )} of unknown type with directory`
+            );
+        }
+    } else if (state === 'link') {
+        // TODO
+    } else if (state === 'absent') {
+        // TODO
+    } else if (state === 'touch') {
+        // TODO
     }
-  } else if (state === 'link') {
-    // TODO
-  } else if (state === 'absent') {
-    // TODO
-  } else if (state === 'touch') {
-    // TODO
-  }
 
-  return diff;
+    return diff;
 }
index c38c871002c781ea7707e81e3ce77b6b295f9764..6b735096ca1aa8f0e30a86fa4caa884e8a403bfb 100644 (file)
@@ -3,7 +3,7 @@ import escapeRegExpPattern from '../escapeRegExpPattern';
  * Just supports simple globs ("*") for now.
  */
 export default function globToRegExp(glob: string): RegExp {
-  const pattern = escapeRegExpPattern(glob);
+    const pattern = escapeRegExpPattern(glob);
 
-  return new RegExp(pattern.replace(/\\\*/g, '[^/]+'));
+    return new RegExp(pattern.replace(/\\\*/g, '[^/]+'));
 }
index c9025db848ceb24961a055b1c771eff69c499e7a..33faff0ac1b132a0f4211ac16ffd8543373ec744 100644 (file)
@@ -15,11 +15,11 @@ export {template};
 export {variable};
 
 export interface Fig {
-  command: typeof command;
-  file: typeof file;
-  resource: typeof resource;
-  root: typeof root;
-  task: typeof task;
-  template: typeof template;
-  variable: typeof variable;
+    command: typeof command;
+    file: typeof file;
+    resource: typeof resource;
+    root: typeof root;
+    task: typeof task;
+    template: typeof template;
+    variable: typeof variable;
 }
index 883c5ba2f15cde5cf893e62f9e731cada3092d7d..b12569c3aeeb968da74e54b8d971f0e6501f9d1f 100644 (file)
@@ -7,20 +7,20 @@ import Context from '../Context';
  * Implements basic shell expansion (of ~).
  */
 export default async function command(
-  executable: string,
-  ...args: Array<string>
+    executable: string,
+    ...args: Array<string>
 ): Promise<void> {
-  const description = [executable, ...args].join(' ');
+    const description = [executable, ...args].join(' ');
 
-  try {
-    await spawn(expand(executable), ...args.map(expand));
-    // TODO: decide whether to log full command here
-    Context.informChanged(`command \`${description}\``);
-  } catch (error) {
-    if (error instanceof ErrorWithMetadata) {
-      Context.informFailed(error);
-    } else {
-      Context.informFailed(`command \`${description}\` failed`);
+    try {
+        await spawn(expand(executable), ...args.map(expand));
+        // TODO: decide whether to log full command here
+        Context.informChanged(`command \`${description}\``);
+    } catch (error) {
+        if (error instanceof ErrorWithMetadata) {
+            Context.informFailed(error);
+        } else {
+            Context.informFailed(`command \`${description}\` failed`);
+        }
     }
-  }
 }
index 6bd2ec1f363eab2bcac7eb9353c490d40e08210b..d9e3ef4510a66ea7814d61a72ece4c5b7dc5f7c5 100644 (file)
@@ -7,42 +7,42 @@ import Context from '../Context';
 // TODO decide whether we want a separate "directory" operation
 // TODO: implement auto-expand of ~
 export default async function file({
-  force,
-  mode,
-  path,
-  src,
-  state,
+    force,
+    mode,
+    path,
+    src,
+    state,
 }: {
-  path: string;
-  mode?: Mode;
-  src?: string;
-  state: 'directory' | 'file' | 'link' | 'touch';
-  force?: boolean;
+    path: string;
+    mode?: Mode;
+    src?: string;
+    state: 'directory' | 'file' | 'link' | 'touch';
+    force?: boolean;
 }): Promise<void> {
-  if (state === 'directory') {
-    directory(path);
-  }
+    if (state === 'directory') {
+        directory(path);
+    }
 }
 
 function directory(path: string) {
-  const target = expand(path);
+    const target = expand(path);
 
-  // TODO: find out if ansible replaces regular file with dir or just errors?
-  // TODO: actually throw for errors
-  if (fs.existsSync(target)) {
-    try {
-      const stat = fs.statSync(target);
+    // TODO: find out if ansible replaces regular file with dir or just errors?
+    // TODO: actually throw for errors
+    if (fs.existsSync(target)) {
+        try {
+            const stat = fs.statSync(target);
 
-      if (stat.isDirectory()) {
-        Context.informOk(`directory ${path}`);
-      } else {
-        log.error(`${path} already exists but is not a directory`);
-      }
-    } catch (error) {
-      log.error(`Failed to stat: ${path}`);
+            if (stat.isDirectory()) {
+                Context.informOk(`directory ${path}`);
+            } else {
+                log.error(`${path} already exists but is not a directory`);
+            }
+        } catch (error) {
+            log.error(`Failed to stat: ${path}`);
+        }
+    } else {
+        Context.informChanged(`directory ${path}`);
+        fs.mkdirSync(target, {recursive: true});
     }
-  } else {
-    Context.informChanged(`directory ${path}`);
-    fs.mkdirSync(target, {recursive: true});
-  }
 }
index 2f81d6ae246fa9d6941a4a5cc39ad43a029d4ba2..f2d20a741eaab18793493de2f9452ccd295a00a3 100644 (file)
@@ -10,56 +10,56 @@ import Context from '../Context';
 import compare from '../compare';
 
 export default async function template({
-  force,
-  group,
-  mode,
-  owner,
-  path,
-  src,
-  variables = {},
-}: {
-  force?: boolean;
-  group?: string;
-  path: string;
-  mode?: Mode;
-  owner?: string;
-  src: string;
-  variables: Variables;
-}): Promise<void> {
-  const target = expand(path);
-
-  const contents = (await Context.compile(src)).fill({variables});
-
-  const diff = await compare({
-    contents,
     force,
     group,
     mode,
     owner,
-    path: target,
-    state: 'file',
-  });
+    path,
+    src,
+    variables = {},
+}: {
+    force?: boolean;
+    group?: string;
+    path: string;
+    mode?: Mode;
+    owner?: string;
+    src: string;
+    variables: Variables;
+}): Promise<void> {
+    const target = expand(path);
 
-  if (owner && owner !== Context.attributes.username) {
-    log.debug(`Needs sudo: ${Context.attributes.username} -> ${owner}`);
-    const passphrase = await Context.sudoPassphrase;
-    const result = await run('ls', ['-l', '/var/audit'], {passphrase});
+    const contents = (await Context.compile(src)).fill({variables});
 
-    if (result.status !== 0) {
-      throw new ErrorWithMetadata(`Failed command`, {
-        ...result,
-        error: result.error?.toString() ?? null,
-      });
-    }
-  } else {
-    if (diff.contents) {
-      // log.info('change!');
-      const temp = await tempfile(contents);
+    const diff = await compare({
+        contents,
+        force,
+        group,
+        mode,
+        owner,
+        path: target,
+        state: 'file',
+    });
 
-      // TODO: cp from temp to target
-      // TODO: deal with group/owner/mode etc
+    if (owner && owner !== Context.attributes.username) {
+        log.debug(`Needs sudo: ${Context.attributes.username} -> ${owner}`);
+        const passphrase = await Context.sudoPassphrase;
+        const result = await run('ls', ['-l', '/var/audit'], {passphrase});
+
+        if (result.status !== 0) {
+            throw new ErrorWithMetadata(`Failed command`, {
+                ...result,
+                error: result.error?.toString() ?? null,
+            });
+        }
     } else {
-      Context.informOk(`template ${path}`);
+        if (diff.contents) {
+            // log.info('change!');
+            const temp = await tempfile(contents);
+
+            // TODO: cp from temp to target
+            // TODO: deal with group/owner/mode etc
+        } else {
+            Context.informOk(`template ${path}`);
+        }
     }
-  }
 }
index 1c7c74b1c6107362d3a1c0db2538474ead889aaf..b92c36f803b8b0738e6a009a33b704ebef88f66f 100644 (file)
@@ -11,25 +11,25 @@ import globToRegExp from './globToRegExp';
  * active aspect (eg. `aspects/${aspect}/files/${name}`).
  */
 export function file(...path: Array<string>): string {
-  return join('aspects', Context.currentAspect, 'files', ...path);
+    return join('aspects', Context.currentAspect, 'files', ...path);
 }
 
 /**
  * Very simple glob-based file search (doesn't supported nested directories).
  */
 export function files(glob: string): Array<string> {
-  const aspect = Context.currentAspect;
+    const aspect = Context.currentAspect;
 
-  const regExp = globToRegExp(glob);
+    const regExp = globToRegExp(glob);
 
-  return fs
-    .readdirSync(join('aspects', aspect, 'files'), {withFileTypes: true})
-    .filter((entry) => entry.isFile())
-    .map(({name}) => name)
-    .filter((name) => regExp.test(name))
-    .map((name) => join('aspects', aspect, 'files', name));
+    return fs
+        .readdirSync(join('aspects', aspect, 'files'), {withFileTypes: true})
+        .filter((entry) => entry.isFile())
+        .map(({name}) => name)
+        .filter((name) => regExp.test(name))
+        .map((name) => join('aspects', aspect, 'files', name));
 }
 
 export function template(...path: Array<string>): string {
-  return join('aspects', Context.currentAspect, 'templates', ...path);
+    return join('aspects', Context.currentAspect, 'templates', ...path);
 }
index 28f1aebc81bdd94fecee4406610a80ced8e00868..9595d28ff0fea89eb784af6ab2e5eb6c3020f1c9 100644 (file)
@@ -9,19 +9,19 @@ import {existsSync} from 'fs';
  * (compiled).
  */
 const root = (function find(path): string {
-  const target = 'yarn.lock';
+    const target = 'yarn.lock';
 
-  if (existsSync(join(path, target))) {
-    return path;
-  } else {
-    const next = dirname(path);
+    if (existsSync(join(path, target))) {
+        return path;
+    } else {
+        const next = dirname(path);
 
-    if (next === path) {
-      throw new Error(`Searched up to ${path} without finding ${target}`);
-    }
+        if (next === path) {
+            throw new Error(`Searched up to ${path} without finding ${target}`);
+        }
 
-    return find(dirname(path));
-  }
+        return find(dirname(path));
+    }
 })(__dirname);
 
 export default root;
index fbe728b597a81363bd0801ab1ec35be3024eaed6..61552299b6b239c65e24c9f83f5a39f2d44f7a27 100644 (file)
@@ -4,17 +4,17 @@ import {log} from '../console';
 // decide whether we need any of those.
 
 export function changed(message: string) {
-  log.notice(`Changed: ${message}`);
+    log.notice(`Changed: ${message}`);
 }
 
 export function failed(message: string) {
-  log.error(`Failed: ${message}`);
+    log.error(`Failed: ${message}`);
 }
 
 export function ok(message: string) {
-  log.info(`Ok: ${message}`);
+    log.info(`Ok: ${message}`);
 }
 
 export function skipped(message: string) {
-  log.info(`Skipped: ${message}`);
+    log.info(`Skipped: ${message}`);
 }
index 0d09a460334ddbcc8e386b48f2a3f6432f66bcfe..1a42cc474d83ca5c1ae81fb5ed95bebb53083c25 100644 (file)
@@ -6,21 +6,21 @@ import Context from './Context';
 import {default as root} from './root';
 
 export default function task(name: string, callback: () => Promise<void>) {
-  const caller = getCaller();
+    const caller = getCaller();
 
-  const ancestors = relative(root, caller).split(sep);
+    const ancestors = relative(root, caller).split(sep);
 
-  const aspect =
-    ancestors[0] === 'lib' && ancestors[1] === 'aspects' && ancestors[2];
+    const aspect =
+        ancestors[0] === 'lib' && ancestors[1] === 'aspects' && ancestors[2];
 
-  if (!aspect) {
-    throw new Error(`Unable to determine aspect for ${caller}`);
-  }
+    if (!aspect) {
+        throw new Error(`Unable to determine aspect for ${caller}`);
+    }
 
-  assertAspect(aspect);
+    assertAspect(aspect);
 
-  // TODO: we use `caller` to make namespaced task name.
-  // (will be useful for --start-at)
-  // also, we can make an interactive mode that lets us choose where to start
-  Context.tasks.register(aspect, callback, `${aspect} | ${name}`);
+    // TODO: we use `caller` to make namespaced task name.
+    // (will be useful for --start-at)
+    // also, we can make an interactive mode that lets us choose where to start
+    Context.tasks.register(aspect, callback, `${aspect} | ${name}`);
 }
index 582c221f57fc409d2e0ad637e8b764a81a5da5cf..3256d36c954c5b0683df3c6973e8ee05a3a4619b 100644 (file)
@@ -2,21 +2,21 @@ import assert from '../assert';
 import Context from './Context';
 
 export default function variable(
-  name: string,
-  fallback?: JSONValue
+    name: string,
+    fallback?: JSONValue
 ): JSONValue {
-  const variables = Context.currentVariables;
+    const variables = Context.currentVariables;
 
-  return variables.hasOwnProperty(name) ? variables[name] : fallback || null;
+    return variables.hasOwnProperty(name) ? variables[name] : fallback || null;
 }
 
 variable.string = (name: string, fallback?: JSONValue): string => {
-  const value = variable(name, fallback);
+    const value = variable(name, fallback);
 
-  assert(
-    typeof value === 'string',
-    `Expected variable ${name} to have type string but it was ${typeof value}`
-  );
+    assert(
+        typeof value === 'string',
+        `Expected variable ${name} to have type string but it was ${typeof value}`
+    );
 
-  return value;
+    return value;
 };
index 199d4721d42f25ce871ac8dec31c003df11b3a37..3e54b29eb16e87e136d458c2e94c8dbced23c12d 100644 (file)
@@ -2,39 +2,39 @@ import {expect, test} from '../test/harness';
 import merge from '../merge';
 
 test('merge() returns an single object', () => {
-  expect(merge({example: 'obj'})).toEqual({example: 'obj'});
+    expect(merge({example: 'obj'})).toEqual({example: 'obj'});
 });
 
 test('merge() merges two objects with non-overlapping keys', () => {
-  expect(merge({example: 'obj'}, {more: 'stuff'})).toEqual({
-    example: 'obj',
-    more: 'stuff',
-  });
+    expect(merge({example: 'obj'}, {more: 'stuff'})).toEqual({
+        example: 'obj',
+        more: 'stuff',
+    });
 });
 
 test('merge() merges two objects with overlapping keys', () => {
-  expect(
-    merge({example: 'obj', more: 'things'}, {more: 'stuff', and: true})
-  ).toEqual({example: 'obj', more: 'stuff', and: true});
+    expect(
+        merge({example: 'obj', more: 'things'}, {more: 'stuff', and: true})
+    ).toEqual({example: 'obj', more: 'stuff', and: true});
 });
 
 test('merge() overwrites arrays', () => {
-  expect(merge({list: [1, 2, 3]}, {list: ['a', 'b', 'c', 'd']})).toEqual({
-    list: ['a', 'b', 'c', 'd'],
-  });
+    expect(merge({list: [1, 2, 3]}, {list: ['a', 'b', 'c', 'd']})).toEqual({
+        list: ['a', 'b', 'c', 'd'],
+    });
 });
 
 test('merge() deep-merges objects', () => {
-  expect(
-    merge(
-      {thing: true, nested: {prop: 'value'}},
-      {thing: true, nested: {other: false}}
-    )
-  ).toEqual({
-    thing: true,
-    nested: {
-      prop: 'value',
-      other: false,
-    },
-  });
+    expect(
+        merge(
+            {thing: true, nested: {prop: 'value'}},
+            {thing: true, nested: {other: false}}
+        )
+    ).toEqual({
+        thing: true,
+        nested: {
+            prop: 'value',
+            other: false,
+        },
+    });
 });
index 127f6d2c90809b040e77813ac35d74cd7968967b..5fbf7428b047a9abb6508e098cdbbf0e28262119 100644 (file)
@@ -2,23 +2,23 @@ import {expect, test} from '../test/harness';
 import regExpFromString from '../regExpFromString';
 
 test('regExpFromString() returns a RegExp', () => {
-  const regExp = regExpFromString('/\\bword\\b/');
+    const regExp = regExpFromString('/\\bword\\b/');
 
-  expect(regExp instanceof RegExp).toBe(true);
-  expect(regExp.source).toBe('\\bword\\b');
-  expect(regExp.flags).toBe('');
+    expect(regExp instanceof RegExp).toBe(true);
+    expect(regExp.source).toBe('\\bword\\b');
+    expect(regExp.flags).toBe('');
 });
 
 test('regExpFromString() preserves flags', () => {
-  const regExp = regExpFromString('/^foo/mig');
+    const regExp = regExpFromString('/^foo/mig');
 
-  expect(regExp instanceof RegExp).toBe(true);
-  expect(regExp.source).toBe('^foo');
-  expect(regExp.flags).toBe('gim');
+    expect(regExp instanceof RegExp).toBe(true);
+    expect(regExp.source).toBe('^foo');
+    expect(regExp.flags).toBe('gim');
 });
 
 test('regExpFromString() rejects an invalid pattern', () => {
-  expect(() => regExpFromString('thing')).toThrow(
-    'Invalid pattern "thing" does not match /^\\/(.+)\\/([gimsuy]*)$/'
-  );
+    expect(() => regExpFromString('thing')).toThrow(
+        'Invalid pattern "thing" does not match /^\\/(.+)\\/([gimsuy]*)$/'
+    );
 });
index 8a26c521cac6768169ea285ec2838b336c10a68c..ec5e0d0c0b5bbc23e0ddb6b2f8af38faaf548dd5 100644 (file)
@@ -3,52 +3,52 @@ import dedent from '../dedent';
 import stringify from '../stringify';
 
 test('stringify() null', () => {
-  expect(stringify(null)).toBe('null');
+    expect(stringify(null)).toBe('null');
 });
 
 test('stringify() undefined', () => {
-  expect(stringify(undefined)).toBe('undefined');
+    expect(stringify(undefined)).toBe('undefined');
 });
 
 test('stringify() true', () => {
-  expect(stringify(true)).toBe('true');
+    expect(stringify(true)).toBe('true');
 });
 
 test('stringify() false', () => {
-  expect(stringify(false)).toBe('false');
+    expect(stringify(false)).toBe('false');
 });
 
 test('stringify() a number', () => {
-  expect(stringify(9000)).toBe('9000');
+    expect(stringify(9000)).toBe('9000');
 });
 
 test('stringify() a string', () => {
-  expect(stringify('thing')).toBe('"thing"');
+    expect(stringify('thing')).toBe('"thing"');
 });
 
 test('stringify() a Symbol', () => {
-  expect(stringify(Symbol.for('sample'))).toBe('Symbol(sample)');
+    expect(stringify(Symbol.for('sample'))).toBe('Symbol(sample)');
 });
 
 test('stringify() a RegExp', () => {
-  expect(stringify(/stuff \w+/i)).toBe('/stuff \\w+/i');
+    expect(stringify(/stuff \w+/i)).toBe('/stuff \\w+/i');
 });
 
 test('stringify() an array', () => {
-  expect(stringify([1, true, 'thing'])).toBe(
-    dedent`
+    expect(stringify([1, true, 'thing'])).toBe(
+        dedent`
       [
         1,
         true,
         "thing",
       ]
     `.trimEnd()
-  );
+    );
 });
 
 test('stringify() nested arrays', () => {
-  expect(stringify([1, true, 'thing', ['nested', null]])).toBe(
-    dedent`
+    expect(stringify([1, true, 'thing', ['nested', null]])).toBe(
+        dedent`
       [
         1,
         true,
@@ -59,16 +59,16 @@ test('stringify() nested arrays', () => {
         ],
       ]
     `.trimEnd()
-  );
+    );
 });
 
 test('stringify() an array with circular references', () => {
-  const array: Array<any> = [1, true, 'thing'];
+    const array: Array<any> = [1, true, 'thing'];
 
-  array.push(array);
+    array.push(array);
 
-  expect(stringify(array)).toBe(
-    dedent`
+    expect(stringify(array)).toBe(
+        dedent`
       [
         1,
         true,
@@ -76,23 +76,23 @@ test('stringify() an array with circular references', () => {
         «circular»,
       ]
     `.trimEnd()
-  );
+    );
 });
 
 test('stringify() an object', () => {
-  expect(stringify({a: 1, b: true})).toBe(
-    dedent`
+    expect(stringify({a: 1, b: true})).toBe(
+        dedent`
       {
         "a": 1,
         "b": true,
       }
     `.trimEnd()
-  );
+    );
 });
 
 test('stringify() a nested object', () => {
-  expect(stringify({a: 1, b: true, c: {d: null}})).toBe(
-    dedent`
+    expect(stringify({a: 1, b: true, c: {d: null}})).toBe(
+        dedent`
       {
         "a": 1,
         "b": true,
@@ -101,45 +101,45 @@ test('stringify() a nested object', () => {
         },
       }
     `.trimEnd()
-  );
+    );
 });
 
 test('stringify() an object with circular references', () => {
-  const object: {[key: string]: any} = {a: 1, b: true};
+    const object: {[key: string]: any} = {a: 1, b: true};
 
-  object.c = object;
+    object.c = object;
 
-  expect(stringify(object)).toBe(
-    dedent`
+    expect(stringify(object)).toBe(
+        dedent`
       {
         "a": 1,
         "b": true,
         "c": «circular»,
       }
     `.trimEnd()
-  );
+    );
 });
 
 test('stringify() a Date', () => {
-  expect(stringify(new Date())).toBe('[object Date]');
+    expect(stringify(new Date())).toBe('[object Date]');
 });
 
 test('stringify() a one-line Function', () => {
-  expect(stringify(() => 1)).toBe('() => 1');
+    expect(stringify(() => 1)).toBe('() => 1');
 });
 
 // @ts-ignore: suppress TS7006: Parameter 'a' implicitly has an 'any' type.
 function fn(a, b) {
-  if (a > 0) {
-    return a + b;
-  }
+    if (a > 0) {
+        return a + b;
+    }
 }
 
 test('stringify() a multi-line Function', () => {
-  // Obviously this test is pretty fragile; depends on TS continuing to
-  // use a 4-space indent in its build output.
-  expect(stringify({fn})).toBe(
-    dedent`
+    // Obviously this test is pretty fragile; depends on TS continuing to
+    // use a 4-space indent in its build output.
+    expect(stringify({fn})).toBe(
+        dedent`
       {
         "fn": function fn(a, b) {
             if (a > 0) {
@@ -148,5 +148,5 @@ test('stringify() a multi-line Function', () => {
         },
       }
     `.trimEnd()
-  );
+    );
 });
index 19eca4269295fe7d1ee2b5dbacd9edde754915db..d528ac6960063fd912108f633e3bf2ade9df2a52 100644 (file)
@@ -3,14 +3,14 @@ import {expect, test} from '../test/harness';
 import {compile, fill, tokenize} from '../template';
 
 test('compile() compiles an empty template', () => {
-  expect(compile('')).toBe(dedent`
+    expect(compile('')).toBe(dedent`
     let __buffer__ = "";
     return __buffer__;
   `);
 });
 
 test('compile() compiles a template containing only template text', () => {
-  expect(compile('my stuff')).toBe(dedent`
+    expect(compile('my stuff')).toBe(dedent`
     let __buffer__ = "";
     __buffer__ += "my stuff";
     return __buffer__;
@@ -18,7 +18,7 @@ test('compile() compiles a template containing only template text', () => {
 });
 
 test('compile() compiles a template containing an expression', () => {
-  expect(compile('his name was <%= "Robert Paulson" %>')).toBe(dedent`
+    expect(compile('his name was <%= "Robert Paulson" %>')).toBe(dedent`
     let __buffer__ = "";
     __buffer__ += "his name was ";
     __buffer__ += ("Robert Paulson");
@@ -27,15 +27,15 @@ test('compile() compiles a template containing an expression', () => {
 });
 
 test('compile() compiles a template containing statements', () => {
-  expect(
-    compile(dedent`
+    expect(
+        compile(dedent`
       first
       <% if (true) { %>
       second
       <% } %>
       third
     `)
-  ).toBe(dedent`
+    ).toBe(dedent`
     let __buffer__ = "";
     __buffer__ += "first\\n";
     if (true) {
@@ -45,16 +45,16 @@ test('compile() compiles a template containing statements', () => {
     return __buffer__;
   `);
 
-  // In practice, you'd probably use the slurping variants ("<%-", "-%>").
-  expect(
-    compile(dedent`
+    // In practice, you'd probably use the slurping variants ("<%-", "-%>").
+    expect(
+        compile(dedent`
       first
       <%- if (true) { -%>
       second
       <%- } -%>
       third
     `)
-  ).toBe(dedent`
+    ).toBe(dedent`
     let __buffer__ = "";
     __buffer__ += "first\\n";
     if (true) {
@@ -66,42 +66,42 @@ test('compile() compiles a template containing statements', () => {
 });
 
 test('fill() fills an empty template', () => {
-  expect(fill(compile(''))).toBe('');
+    expect(fill(compile(''))).toBe('');
 });
 
 test('fill() fills a template containing only template text', () => {
-  expect(fill(compile('stuff'))).toBe('stuff');
+    expect(fill(compile('stuff'))).toBe('stuff');
 });
 
 test('fill() fills a template containing an expression', () => {
-  expect(fill(compile('stuff <%= "here" %>'))).toBe('stuff here');
+    expect(fill(compile('stuff <%= "here" %>'))).toBe('stuff here');
 });
 
 test('fill() fills a template that relies on scope', () => {
-  expect(fill(compile('name: <%= name %>'), {name: 'Bob'})).toBe('name: Bob');
+    expect(fill(compile('name: <%= name %>'), {name: 'Bob'})).toBe('name: Bob');
 });
 
 test('fill() complains when required scope is missing', () => {
-  expect(() => fill(compile('name: <%= name %>'))).toThrow(
-    'name is not defined'
-  );
+    expect(() => fill(compile('name: <%= name %>'))).toThrow(
+        'name is not defined'
+    );
 });
 
 test('fill() fills a template containing statements', () => {
-  expect(
-    fill(
-      compile(
-        dedent`
+    expect(
+        fill(
+            compile(
+                dedent`
           first
           <% if (something === 'that') { %>
           second
           <% } %>
           third
         `
-      ),
-      {something: 'that'}
-    )
-  ).toBe(dedent`
+            ),
+            {something: 'that'}
+        )
+    ).toBe(dedent`
     first
 
     second
@@ -109,21 +109,21 @@ test('fill() fills a template containing statements', () => {
     third
   `);
 
-  // In practice, you'd use the slurping variants ("<%-", "-%>").
-  expect(
-    fill(
-      compile(
-        dedent`
+    // In practice, you'd use the slurping variants ("<%-", "-%>").
+    expect(
+        fill(
+            compile(
+                dedent`
           first
           <%- if (something === 'that') { -%>
           second
           <%- } -%>
           third
         `
-      ),
-      {something: 'that'}
-    )
-  ).toBe(dedent`
+            ),
+            {something: 'that'}
+        )
+    ).toBe(dedent`
     first
     second
     third
@@ -131,20 +131,20 @@ test('fill() fills a template containing statements', () => {
 });
 
 test('fill() correctly handles indented slurping delimiters', () => {
-  expect(
-    fill(
-      compile(
-        dedent`
+    expect(
+        fill(
+            compile(
+                dedent`
           #start
             <%- if (something === 'that') { -%>
             middle
             <%- } -%>
           #end
         `
-      ),
-      {something: 'that'}
-    )
-  ).toBe(dedent`
+            ),
+            {something: 'that'}
+        )
+    ).toBe(dedent`
     #start
       middle
     #end
@@ -152,103 +152,103 @@ test('fill() correctly handles indented slurping delimiters', () => {
 });
 
 test('fill() correctly handles slurping delimiters at edges of template', () => {
-  expect(
-    fill(
-      compile(
-        dedent`
+    expect(
+        fill(
+            compile(
+                dedent`
           <%- if (something === 'that') { -%>
           conditional
           <%- } -%>
         `
-      ),
-      {something: 'that'}
-    )
-  ).toBe(dedent`
+            ),
+            {something: 'that'}
+        )
+    ).toBe(dedent`
     conditional
   `);
 });
 
 test('tokenize() handles empty input', () => {
-  expect([...tokenize('')]).toEqual([]);
+    expect([...tokenize('')]).toEqual([]);
 });
 
 test('tokenize() handles a template containing only template text', () => {
-  expect([...tokenize('an example here')]).toEqual([
-    {
-      kind: 'TemplateText',
-      text: 'an example here',
-    },
-  ]);
+    expect([...tokenize('an example here')]).toEqual([
+        {
+            kind: 'TemplateText',
+            text: 'an example here',
+        },
+    ]);
 });
 
 test('tokenize() handles a template containing an expression', () => {
-  expect([...tokenize('this <%= "thing" %>')]).toEqual([
-    {kind: 'TemplateText', text: 'this '},
-    {kind: 'StartExpression'},
-    {kind: 'HostText', text: ' "thing" '},
-    {kind: 'EndDelimiter'},
-  ]);
+    expect([...tokenize('this <%= "thing" %>')]).toEqual([
+        {kind: 'TemplateText', text: 'this '},
+        {kind: 'StartExpression'},
+        {kind: 'HostText', text: ' "thing" '},
+        {kind: 'EndDelimiter'},
+    ]);
 });
 
 test('tokenize() handles a template containing a statement', () => {
-  expect([...tokenize('this <% call() %>')]).toEqual([
-    {kind: 'TemplateText', text: 'this '},
-    {kind: 'StartStatement'},
-    {kind: 'HostText', text: ' call() '},
-    {kind: 'EndDelimiter'},
-  ]);
+    expect([...tokenize('this <% call() %>')]).toEqual([
+        {kind: 'TemplateText', text: 'this '},
+        {kind: 'StartStatement'},
+        {kind: 'HostText', text: ' call() '},
+        {kind: 'EndDelimiter'},
+    ]);
 });
 
 test('tokenize() eats a newline after a "-%>" delimiter', () => {
-  expect([...tokenize('before\n<% something -%>\nafter')]).toEqual([
-    {kind: 'TemplateText', text: 'before\n'},
-    {kind: 'StartStatement'},
-    {kind: 'HostText', text: ' something '},
-    {kind: 'EndDelimiter'},
-    {kind: 'TemplateText', text: 'after'},
-  ]);
-
-  expect([...tokenize('before\n<%= something -%>\nafter')]).toEqual([
-    {kind: 'TemplateText', text: 'before\n'},
-    {kind: 'StartExpression'},
-    {kind: 'HostText', text: ' something '},
-    {kind: 'EndDelimiter'},
-    {kind: 'TemplateText', text: 'after'},
-  ]);
+    expect([...tokenize('before\n<% something -%>\nafter')]).toEqual([
+        {kind: 'TemplateText', text: 'before\n'},
+        {kind: 'StartStatement'},
+        {kind: 'HostText', text: ' something '},
+        {kind: 'EndDelimiter'},
+        {kind: 'TemplateText', text: 'after'},
+    ]);
+
+    expect([...tokenize('before\n<%= something -%>\nafter')]).toEqual([
+        {kind: 'TemplateText', text: 'before\n'},
+        {kind: 'StartExpression'},
+        {kind: 'HostText', text: ' something '},
+        {kind: 'EndDelimiter'},
+        {kind: 'TemplateText', text: 'after'},
+    ]);
 });
 
 test('tokenize() eats whitespace between previous newline and "<%-" delimiter', () => {
-  expect([...tokenize('before\n  <%- something %>\nafter')]).toEqual([
-    {kind: 'TemplateText', text: 'before\n'},
-    {kind: 'StartStatement'},
-    {kind: 'HostText', text: ' something '},
-    {kind: 'EndDelimiter'},
-    {kind: 'TemplateText', text: '\nafter'},
-  ]);
-
-  // But note that, more realistically, "<%-" and "-%>" are generally used
-  // together.
-  expect([...tokenize('before\n  <%- something -%>\nafter')]).toEqual([
-    {kind: 'TemplateText', text: 'before\n'},
-    {kind: 'StartStatement'},
-    {kind: 'HostText', text: ' something '},
-    {kind: 'EndDelimiter'},
-    {kind: 'TemplateText', text: 'after'},
-  ]);
+    expect([...tokenize('before\n  <%- something %>\nafter')]).toEqual([
+        {kind: 'TemplateText', text: 'before\n'},
+        {kind: 'StartStatement'},
+        {kind: 'HostText', text: ' something '},
+        {kind: 'EndDelimiter'},
+        {kind: 'TemplateText', text: '\nafter'},
+    ]);
+
+    // But note that, more realistically, "<%-" and "-%>" are generally used
+    // together.
+    expect([...tokenize('before\n  <%- something -%>\nafter')]).toEqual([
+        {kind: 'TemplateText', text: 'before\n'},
+        {kind: 'StartStatement'},
+        {kind: 'HostText', text: ' something '},
+        {kind: 'EndDelimiter'},
+        {kind: 'TemplateText', text: 'after'},
+    ]);
 });
 
 test('tokenize() complains about unexpected start delimiters', () => {
-  expect(() => [...tokenize('outer <% inner <%')]).toThrow(
-    'Unexpected start delimiter "<%" at index 15'
-  );
+    expect(() => [...tokenize('outer <% inner <%')]).toThrow(
+        'Unexpected start delimiter "<%" at index 15'
+    );
 
-  expect(() => [...tokenize('outer <%= inner <%')]).toThrow(
-    'Unexpected start delimiter "<%" at index 16'
-  );
+    expect(() => [...tokenize('outer <%= inner <%')]).toThrow(
+        'Unexpected start delimiter "<%" at index 16'
+    );
 });
 
 test('tokenize() complains about unexpected end delimiters', () => {
-  expect(() => [...tokenize('before %>')]).toThrow(
-    'Unexpected end delimiter "%>" at index 7'
-  );
+    expect(() => [...tokenize('before %>')]).toThrow(
+        'Unexpected end delimiter "%>" at index 7'
+    );
 });
index 8b90e7d05a9fcbef39d170216d0c9352651e202e..362acffe1605f244477d75fc11d99e4d26746448 100644 (file)
@@ -4,10 +4,10 @@
  * sense).
  */
 export default function assert(
-  condition: any,
-  message?: string
+    condition: any,
+    message?: string
 ): asserts condition {
-  if (!condition) {
-    throw new Error(`assert(): ${message || 'assertion failed'}`);
-  }
+    if (!condition) {
+        throw new Error(`assert(): ${message || 'assertion failed'}`);
+    }
 }
index 0c8f4fea304d9956b217fc8c2d5018978f1ecb8a..99ead6ae52c2f046f8608ff7973943cfa65c8986 100644 (file)
@@ -3,20 +3,20 @@ import Context from './Fig/Context';
 import run from './run';
 
 type Options = {
-  group?: string;
-  sudo?: boolean;
-  user?: string;
+    group?: string;
+    sudo?: boolean;
+    user?: string;
 };
 
 export default async function chown(
-  path: string,
-  options: Options = {}
+    path: string,
+    options: Options = {}
 ): Promise<Error | null> {
-  if (Context.attributes.platform === 'darwin') {
-    return null; // TODO finish
-  } else {
-    throw new Error('TODO: implement');
-  }
+    if (Context.attributes.platform === 'darwin') {
+        return null; // TODO finish
+    } else {
+        throw new Error('TODO: implement');
+    }
 }
 
 // TODO: decide whether to throw/catch or just return errors
index 1c5e778163fbf37fd970d287617a69f39866ebdf..9c104f92df2f7dd349fa051fff9d9e4de917ed40 100644 (file)
@@ -23,16 +23,16 @@ function bold(input: string): string;
  * @overload
  */
 function bold(
-  input: TemplateStringsArray,
-  ...interpolations: unknown[]
+    input: TemplateStringsArray,
+    ...interpolations: unknown[]
 ): string;
 
 function bold(input: any, ...interpolations: unknown[]) {
-  if (Array.isArray(input)) {
-    return style(interpolate(input as any, interpolations), BOLD);
-  } else {
-    return style(input, BOLD);
-  }
+    if (Array.isArray(input)) {
+        return style(interpolate(input as any, interpolations), BOLD);
+    } else {
+        return style(input, BOLD);
+    }
 }
 
 /**
@@ -48,16 +48,16 @@ function green(input: string): string;
  * @overload
  */
 function green(
-  input: TemplateStringsArray,
-  ...interpolations: unknown[]
+    input: TemplateStringsArray,
+    ...interpolations: unknown[]
 ): string;
 
 function green(input: any, ...interpolations: unknown[]) {
-  if (Array.isArray(input)) {
-    return style(interpolate(input as any, interpolations), GREEN);
-  } else {
-    return style(input, GREEN);
-  }
+    if (Array.isArray(input)) {
+        return style(interpolate(input as any, interpolations), GREEN);
+    } else {
+        return style(input, GREEN);
+    }
 }
 
 /**
@@ -73,16 +73,16 @@ function purple(input: string): string;
  * @overload
  */
 function purple(
-  input: TemplateStringsArray,
-  ...interpolations: unknown[]
+    input: TemplateStringsArray,
+    ...interpolations: unknown[]
 ): string;
 
 function purple(input: any, ...interpolations: unknown[]) {
-  if (Array.isArray(input)) {
-    return style(interpolate(input as any, interpolations), PURPLE);
-  } else {
-    return style(input, PURPLE);
-  }
+    if (Array.isArray(input)) {
+        return style(interpolate(input as any, interpolations), PURPLE);
+    } else {
+        return style(input, PURPLE);
+    }
 }
 
 /**
@@ -100,11 +100,11 @@ function red(input: string): string;
 function red(input: TemplateStringsArray, ...interpolations: unknown[]): string;
 
 function red(input: any, ...interpolations: unknown[]) {
-  if (Array.isArray(input)) {
-    return style(interpolate(input as any, interpolations), RED);
-  } else {
-    return style(input, RED);
-  }
+    if (Array.isArray(input)) {
+        return style(interpolate(input as any, interpolations), RED);
+    } else {
+        return style(input, RED);
+    }
 }
 
 /**
@@ -120,16 +120,16 @@ function reverse(input: string): string;
  * @overload
  */
 function reverse(
-  input: TemplateStringsArray,
-  ...interpolations: unknown[]
+    input: TemplateStringsArray,
+    ...interpolations: unknown[]
 ): string;
 
 function reverse(input: any, ...interpolations: unknown[]) {
-  if (Array.isArray(input)) {
-    return style(interpolate(input as any, interpolations), REVERSE);
-  } else {
-    return style(input, REVERSE);
-  }
+    if (Array.isArray(input)) {
+        return style(interpolate(input as any, interpolations), REVERSE);
+    } else {
+        return style(input, REVERSE);
+    }
 }
 
 /**
@@ -145,123 +145,123 @@ function yellow(input: string): string;
  * @overload
  */
 function yellow(
-  input: TemplateStringsArray,
-  ...interpolations: unknown[]
+    input: TemplateStringsArray,
+    ...interpolations: unknown[]
 ): string;
 
 function yellow(input: any, ...interpolations: unknown[]) {
-  if (Array.isArray(input)) {
-    return style(interpolate(input as any, interpolations), YELLOW);
-  } else {
-    return style(input, YELLOW);
-  }
+    if (Array.isArray(input)) {
+        return style(interpolate(input as any, interpolations), YELLOW);
+    } else {
+        return style(input, YELLOW);
+    }
 }
 
 function style(text: string, escape: string) {
-  return `${escape}${text}${RESET}`;
+    return `${escape}${text}${RESET}`;
 }
 
 function interpolate(strings: TemplateStringsArray, interpolations: unknown[]) {
-  return strings.reduce((acc, string, i) => {
-    if (i < interpolations.length) {
-      return acc + string + String(interpolations[i]);
-    } else {
-      return acc + string;
-    }
-  }, '');
+    return strings.reduce((acc, string, i) => {
+        if (i < interpolations.length) {
+            return acc + string + String(interpolations[i]);
+        } else {
+            return acc + string;
+        }
+    }, '');
 }
 
 const COLORS = {
-  bold(
-    this: unknown,
-    strings: TemplateStringsArray,
-    ...interpolations: unknown[]
-  ): string {
-    const result = bold(strings, ...interpolations);
-
-    if (typeof this === 'function') {
-      return this.call(null, result);
-    } else {
-      return result;
-    }
-  },
-
-  green(
-    this: unknown,
-    strings: TemplateStringsArray,
-    ...interpolations: unknown[]
-  ): string {
-    const result = green(strings, ...interpolations);
-
-    if (typeof this === 'function') {
-      return this.call(null, result);
-    } else {
-      return result;
-    }
-  },
-
-  purple(
-    this: unknown,
-    strings: TemplateStringsArray,
-    ...interpolations: unknown[]
-  ): string {
-    const result = purple(strings, ...interpolations);
-
-    if (typeof this === 'function') {
-      return this.call(null, result);
-    } else {
-      return result;
-    }
-  },
-
-  red(
-    this: unknown,
-    strings: TemplateStringsArray,
-    ...interpolations: unknown[]
-  ): string {
-    const result = red(strings, ...interpolations);
-
-    if (typeof this === 'function') {
-      return this.call(null, result);
-    } else {
-      return result;
-    }
-  },
-
-  reverse(
-    this: unknown,
-    strings: TemplateStringsArray,
-    ...interpolations: unknown[]
-  ): string {
-    const result = reverse(strings, ...interpolations);
-
-    if (typeof this === 'function') {
-      return this.call(null, result);
-    } else {
-      return result;
-    }
-  },
-
-  yellow(
-    this: unknown,
-    strings: TemplateStringsArray,
-    ...interpolations: unknown[]
-  ): string {
-    const result = yellow(strings, ...interpolations);
-
-    if (typeof this === 'function') {
-      return this.call(null, result);
-    } else {
-      return result;
-    }
-  },
+    bold(
+        this: unknown,
+        strings: TemplateStringsArray,
+        ...interpolations: unknown[]
+    ): string {
+        const result = bold(strings, ...interpolations);
+
+        if (typeof this === 'function') {
+            return this.call(null, result);
+        } else {
+            return result;
+        }
+    },
+
+    green(
+        this: unknown,
+        strings: TemplateStringsArray,
+        ...interpolations: unknown[]
+    ): string {
+        const result = green(strings, ...interpolations);
+
+        if (typeof this === 'function') {
+            return this.call(null, result);
+        } else {
+            return result;
+        }
+    },
+
+    purple(
+        this: unknown,
+        strings: TemplateStringsArray,
+        ...interpolations: unknown[]
+    ): string {
+        const result = purple(strings, ...interpolations);
+
+        if (typeof this === 'function') {
+            return this.call(null, result);
+        } else {
+            return result;
+        }
+    },
+
+    red(
+        this: unknown,
+        strings: TemplateStringsArray,
+        ...interpolations: unknown[]
+    ): string {
+        const result = red(strings, ...interpolations);
+
+        if (typeof this === 'function') {
+            return this.call(null, result);
+        } else {
+            return result;
+        }
+    },
+
+    reverse(
+        this: unknown,
+        strings: TemplateStringsArray,
+        ...interpolations: unknown[]
+    ): string {
+        const result = reverse(strings, ...interpolations);
+
+        if (typeof this === 'function') {
+            return this.call(null, result);
+        } else {
+            return result;
+        }
+    },
+
+    yellow(
+        this: unknown,
+        strings: TemplateStringsArray,
+        ...interpolations: unknown[]
+    ): string {
+        const result = yellow(strings, ...interpolations);
+
+        if (typeof this === 'function') {
+            return this.call(null, result);
+        } else {
+            return result;
+        }
+    },
 };
 
 export default {
-  bold: Object.assign(COLORS.bold, COLORS),
-  green: Object.assign(COLORS.green, COLORS),
-  purple: Object.assign(COLORS.purple, COLORS),
-  red: Object.assign(COLORS.red, COLORS),
-  reverse: Object.assign(COLORS.reverse, COLORS),
-  yellow: Object.assign(COLORS.yellow, COLORS),
+    bold: Object.assign(COLORS.bold, COLORS),
+    green: Object.assign(COLORS.green, COLORS),
+    purple: Object.assign(COLORS.purple, COLORS),
+    red: Object.assign(COLORS.red, COLORS),
+    reverse: Object.assign(COLORS.reverse, COLORS),
+    yellow: Object.assign(COLORS.yellow, COLORS),
 };
index 08a712a0f08bd4f08747c1578f47c7d2c3117cd4..18f618a244a06acc140588c8fc7725338620d7d2 100644 (file)
@@ -15,13 +15,13 @@ let logLevel: LogLevel = 6;
 const PREFIXES = ['debug', 'error', 'info', 'notice', 'warning'];
 
 const PREFIX_LENGTH = PREFIXES.reduce((acc, prefix) => {
-  return Math.max(`[${prefix}] `.length, acc);
+    return Math.max(`[${prefix}] `.length, acc);
 }, 0);
 
 const PREFIX_MAP = Object.fromEntries(
-  PREFIXES.map((prefix) => {
-    return [prefix, `[${prefix}] `.padEnd(PREFIX_LENGTH)];
-  })
+    PREFIXES.map((prefix) => {
+        return [prefix, `[${prefix}] `.padEnd(PREFIX_LENGTH)];
+    })
 );
 
 /**
@@ -39,22 +39,22 @@ const PREFIX_MAP = Object.fromEntries(
  * @see https://en.wikipedia.org/wiki/Syslog
  */
 export const LOG_LEVEL = {
-  EMERGENCY: 0,
-  ALERT: 1,
-  CRITICAL: 2,
-  ERROR: 3,
-  WARNING: 4,
-  NOTICE: 5,
-  INFO: 6,
-  DEBUG: 7,
+    EMERGENCY: 0,
+    ALERT: 1,
+    CRITICAL: 2,
+    ERROR: 3,
+    WARNING: 4,
+    NOTICE: 5,
+    INFO: 6,
+    DEBUG: 7,
 } as const;
 
 export function clear() {
-  return new Promise((resolve) => {
-    clearLine(process.stderr, 0, () => {
-      cursorTo(process.stderr, 0, undefined, resolve);
+    return new Promise((resolve) => {
+        clearLine(process.stderr, 0, () => {
+            cursorTo(process.stderr, 0, undefined, resolve);
+        });
     });
-  });
 }
 
 /**
@@ -64,70 +64,70 @@ export function clear() {
  * arguments to `log.debug()`.
  */
 export async function debug(callback: () => unknown): Promise<void> {
-  if (logLevel >= LOG_LEVEL.DEBUG) {
-    await callback();
-  }
+    if (logLevel >= LOG_LEVEL.DEBUG) {
+        await callback();
+    }
 }
 
 export function getLogLevel(): LogLevel {
-  return logLevel;
+    return logLevel;
 }
 
 export function log(...args: Array<any>) {
-  print(...args);
-  print('\n');
+    print(...args);
+    print('\n');
 }
 
 log.debug = function debug(message: string) {
-  if (logLevel >= LOG_LEVEL.DEBUG) {
-    log(purple.bold`${PREFIX_MAP.debug}` + message);
-  }
+    if (logLevel >= LOG_LEVEL.DEBUG) {
+        log(purple.bold`${PREFIX_MAP.debug}` + message);
+    }
 };
 
 log.error = function error(message: string) {
-  if (logLevel >= LOG_LEVEL.ERROR) {
-    log(red.bold`${PREFIX_MAP.error}` + message);
-  }
+    if (logLevel >= LOG_LEVEL.ERROR) {
+        log(red.bold`${PREFIX_MAP.error}` + message);
+    }
 };
 
 log.info = function info(message: string) {
-  if (logLevel >= LOG_LEVEL.INFO) {
-    log(bold`${PREFIX_MAP.info}` + message);
-  }
+    if (logLevel >= LOG_LEVEL.INFO) {
+        log(bold`${PREFIX_MAP.info}` + message);
+    }
 };
 
 log.notice = function notice(message: string) {
-  if (logLevel >= LOG_LEVEL.NOTICE) {
-    log(green.bold`${PREFIX_MAP.notice}` + message);
-  }
+    if (logLevel >= LOG_LEVEL.NOTICE) {
+        log(green.bold`${PREFIX_MAP.notice}` + message);
+    }
 };
 
 log.warn = function warn(message: string) {
-  if (logLevel >= LOG_LEVEL.WARNING) {
-    log(yellow.bold`${PREFIX_MAP.warning}` + message);
-  }
+    if (logLevel >= LOG_LEVEL.WARNING) {
+        log(yellow.bold`${PREFIX_MAP.warning}` + message);
+    }
 };
 
 export function print(...args: Array<any>) {
-  process.stderr.write(
-    args
-      .map((arg) => {
-        try {
-          if (typeof arg === 'object' && arg) {
-            return JSON.stringify(arg, null, 2);
-          } else {
-            return String(arg);
-          }
-        } catch {
-          return '???';
-        }
-      })
-      .join(' ')
-  );
+    process.stderr.write(
+        args
+            .map((arg) => {
+                try {
+                    if (typeof arg === 'object' && arg) {
+                        return JSON.stringify(arg, null, 2);
+                    } else {
+                        return String(arg);
+                    }
+                } catch {
+                    return '???';
+                }
+            })
+            .join(' ')
+    );
 }
 
 export function setLogLevel(level: LogLevel) {
-  logLevel = level;
+    logLevel = level;
 }
 
 log.clear = clear;
index 9e4a196a392a893606a65e8c35046e2bfb76def9..7e669f6cd38a076bde1b4b811332f66b191f37f0 100644 (file)
@@ -1,50 +1,50 @@
 export default function dedent(
-  strings: TemplateStringsArray,
-  ...interpolations: unknown[]
+    strings: TemplateStringsArray,
+    ...interpolations: unknown[]
 ): string {
-  // Insert interpolations in template.
-  const text: string = strings.reduce((acc, string, i) => {
-    if (i < interpolations.length) {
-      return acc + string + String(interpolations[i]);
-    } else {
-      return acc + string;
-    }
-  }, '');
+    // Insert interpolations in template.
+    const text: string = strings.reduce((acc, string, i) => {
+        if (i < interpolations.length) {
+            return acc + string + String(interpolations[i]);
+        } else {
+            return acc + string;
+        }
+    }, '');
 
-  // Collapse totally blank lines to empty strings.
-  const lines = text.split(/\r\n?|\n/).map((line: string) => {
-    if (line.match(/^\s+$/)) {
-      return '';
-    } else {
-      return line;
-    }
-  });
+    // Collapse totally blank lines to empty strings.
+    const lines = text.split(/\r\n?|\n/).map((line: string) => {
+        if (line.match(/^\s+$/)) {
+            return '';
+        } else {
+            return line;
+        }
+    });
 
-  // Find minimum indent (ignoring empty lines).
-  const minimum = lines.reduce((acc: number, line: string) => {
-    const indent = line.match(/^\s+/);
-    if (indent) {
-      const length = indent[0].length;
-      return Math.min(acc, length);
-    }
-    return acc;
-  }, Infinity);
+    // Find minimum indent (ignoring empty lines).
+    const minimum = lines.reduce((acc: number, line: string) => {
+        const indent = line.match(/^\s+/);
+        if (indent) {
+            const length = indent[0].length;
+            return Math.min(acc, length);
+        }
+        return acc;
+    }, Infinity);
 
-  // Strip out minimum indent from every line.
-  const dedented = isFinite(minimum)
-    ? lines.map((line: string) =>
-        line.replace(new RegExp(`^${' '.repeat(minimum)}`, 'g'), '')
-      )
-    : lines;
+    // Strip out minimum indent from every line.
+    const dedented = isFinite(minimum)
+        ? lines.map((line: string) =>
+              line.replace(new RegExp(`^${' '.repeat(minimum)}`, 'g'), '')
+          )
+        : lines;
 
-  // Trim first and last line if empty.
-  if (dedented[0] === '') {
-    dedented.shift();
-  }
+    // Trim first and last line if empty.
+    if (dedented[0] === '') {
+        dedented.shift();
+    }
 
-  if (dedented[dedented.length - 1] === '') {
-    dedented.pop();
-  }
+    if (dedented[dedented.length - 1] === '') {
+        dedented.pop();
+    }
 
-  return dedented.join('\n') + '\n';
+    return dedented.join('\n') + '\n';
 }
index 12519e8d09c33d93c052295d9ad21d4148387558..b6722debd813b8e540c50698752aaaffe5439648 100644 (file)
@@ -1,4 +1,4 @@
 export default function escapeRegExpPattern(pattern: string): string {
-  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
-  return pattern.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
+    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+    return pattern.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
 }
index 211b7d1feb75e07271bad642f205d52d036fd95d..050494c8fdfe19c8104fbbd45a7d882a2506e3b7 100644 (file)
@@ -2,9 +2,9 @@ import {homedir} from 'os';
 import {join} from 'path';
 
 export default function expand(path: string) {
-  if (path.startsWith('~/')) {
-    return join(homedir(), path.slice(2));
-  } else {
-    return path;
-  }
+    if (path.startsWith('~/')) {
+        return join(homedir(), path.slice(2));
+    } else {
+        return path;
+    }
 }
index e3e1a573d4a3df146a871b9d1a14d98491d9848c..92e171d3ed983a83b443e24dae96b57cb433a031 100644 (file)
@@ -2,26 +2,26 @@
  * @see https://v8.dev/docs/stack-trace-api
  */
 export default function getCaller(): string {
-  let name;
+    let name;
 
-  const prepareStackTrace = Error.prepareStackTrace;
+    const prepareStackTrace = Error.prepareStackTrace;
 
-  try {
-    Error.prepareStackTrace = (
-      _error: Error,
-      callsites: Array<NodeJS.CallSite>
-    ): Array<NodeJS.CallSite> => {
-      return callsites;
-    };
+    try {
+        Error.prepareStackTrace = (
+            _error: Error,
+            callsites: Array<NodeJS.CallSite>
+        ): Array<NodeJS.CallSite> => {
+            return callsites;
+        };
 
-    const stack: Array<NodeJS.CallSite> = new Error().stack as any;
+        const stack: Array<NodeJS.CallSite> = new Error().stack as any;
 
-    // Skip two stack frames (this function, and caller of this
-    // function), to get caller of our caller.
-    name = stack.length > 2 ? stack[2].getFileName() : '';
-  } finally {
-    Error.prepareStackTrace = prepareStackTrace;
-  }
+        // Skip two stack frames (this function, and caller of this
+        // function), to get caller of our caller.
+        name = stack.length > 2 ? stack[2].getFileName() : '';
+    } finally {
+        Error.prepareStackTrace = prepareStackTrace;
+    }
 
-  return name || '[unknown]';
+    return name || '[unknown]';
 }
index 4ba1a1e27e23d998942f50034139f57dc7b80059..745ce62214213aa4fefd01775cef3197dd87e2b7 100644 (file)
@@ -13,98 +13,100 @@ import type {LogLevel} from './console';
 import type {Aspect} from './types/Project';
 
 type Options = {
-  focused: Set<Aspect>;
-  logLevel: LogLevel;
-  startAt: {
-    found: boolean;
-    literal: string;
-    fuzzy?: RegExp;
-  };
-  testsOnly: boolean;
+    focused: Set<Aspect>;
+    logLevel: LogLevel;
+    startAt: {
+        found: boolean;
+        literal: string;
+        fuzzy?: RegExp;
+    };
+    testsOnly: boolean;
 };
 
 const {bold} = COLORS;
 
 export default async function getOptions(
-  args: Array<string>
+    args: Array<string>
 ): Promise<Options> {
-  const options: Options = {
-    focused: new Set(),
-    logLevel: LOG_LEVEL.INFO,
-    startAt: {
-      found: false,
-      literal: '',
-    },
-    testsOnly: false,
-  };
+    const options: Options = {
+        focused: new Set(),
+        logLevel: LOG_LEVEL.INFO,
+        startAt: {
+            found: false,
+            literal: '',
+        },
+        testsOnly: false,
+    };
 
-  const directory = path.join(root, 'aspects');
+    const directory = path.join(root, 'aspects');
 
-  const entries = await fs.readdir(directory, {withFileTypes: true});
+    const entries = await fs.readdir(directory, {withFileTypes: true});
 
-  const aspects: Array<[string, string]> = [];
+    const aspects: Array<[string, string]> = [];
 
-  for (const entry of entries) {
-    if (entry.isDirectory()) {
-      const name = entry.name;
+    for (const entry of entries) {
+        if (entry.isDirectory()) {
+            const name = entry.name;
 
-      const {description} = await readAspect(
-        path.join(directory, name, 'aspect.json')
-      );
+            const {description} = await readAspect(
+                path.join(directory, name, 'aspect.json')
+            );
 
-      aspects.push([name, description]);
+            aspects.push([name, description]);
+        }
     }
-  }
-
-  for (const arg of args) {
-    if (arg === '--debug' || arg === '-d') {
-      options.logLevel = LOG_LEVEL.DEBUG;
-    } else if (arg === '--quiet' || arg === '-q') {
-      options.logLevel = LOG_LEVEL.NOTICE;
-    } else if (arg === '--test' || arg === '-t') {
-      options.testsOnly = true;
-    } else if (arg === '--help' || arg === '-h') {
-      await printUsage(aspects);
-      throw new ErrorWithMetadata('aborting');
-    } else if (arg.startsWith('--start-at-task=')) {
-      options.startAt.literal = (
-        arg.match(/^--start-at-task=(.*)/)?.[1] ?? ''
-      ).trim();
-      options.startAt.fuzzy = new RegExp(
-        [
-          '',
-          ...options.startAt.literal.split(/\s+/).map(escapeRegExpPattern),
-          '',
-        ].join('.*'),
-        'i'
-      );
-    } else if (arg.startsWith('-')) {
-      throw new ErrorWithMetadata(
-        `unrecognized argument ${JSON.stringify(
-          arg
-        )} - pass "--help" to see allowed options`
-      );
-    } else {
-      try {
-        assertAspect(arg);
-        options.focused.add(arg);
-      } catch {
-        throw new ErrorWithMetadata(
-          `unrecognized aspect ${JSON.stringify(
-            arg
-          )} - pass "--help" to see full list`
-        );
-      }
+
+    for (const arg of args) {
+        if (arg === '--debug' || arg === '-d') {
+            options.logLevel = LOG_LEVEL.DEBUG;
+        } else if (arg === '--quiet' || arg === '-q') {
+            options.logLevel = LOG_LEVEL.NOTICE;
+        } else if (arg === '--test' || arg === '-t') {
+            options.testsOnly = true;
+        } else if (arg === '--help' || arg === '-h') {
+            await printUsage(aspects);
+            throw new ErrorWithMetadata('aborting');
+        } else if (arg.startsWith('--start-at-task=')) {
+            options.startAt.literal = (
+                arg.match(/^--start-at-task=(.*)/)?.[1] ?? ''
+            ).trim();
+            options.startAt.fuzzy = new RegExp(
+                [
+                    '',
+                    ...options.startAt.literal
+                        .split(/\s+/)
+                        .map(escapeRegExpPattern),
+                    '',
+                ].join('.*'),
+                'i'
+            );
+        } else if (arg.startsWith('-')) {
+            throw new ErrorWithMetadata(
+                `unrecognized argument ${JSON.stringify(
+                    arg
+                )} - pass "--help" to see allowed options`
+            );
+        } else {
+            try {
+                assertAspect(arg);
+                options.focused.add(arg);
+            } catch {
+                throw new ErrorWithMetadata(
+                    `unrecognized aspect ${JSON.stringify(
+                        arg
+                    )} - pass "--help" to see full list`
+                );
+            }
+        }
     }
-  }
 
-  return options;
+    return options;
 }
 
 async function printUsage(aspects: Array<[string, string]>) {
-  // TODO: actually implement all the switches mentioned here
-  log(
-    dedent`
+    // TODO: actually implement all the switches mentioned here
+    log(
+        dedent`
 
       ./install [options] [aspects...]
 
@@ -122,12 +124,12 @@ async function printUsage(aspects: Array<[string, string]>) {
 
       ${bold`Aspects:`}
     `
-  );
+    );
 
-  for (const [aspect, description] of aspects) {
-    log(`  ${aspect}`);
-    log(`    ${description}`);
-  }
+    for (const [aspect, description] of aspects) {
+        log(`  ${aspect}`);
+        log(`    ${description}`);
+    }
 
-  log();
+    log();
 }
index be245771586dde4eff6e4e63d499a6969ec08fd3..2522e3bf4cf0f30c88d8a683f5d2ecef592325bf 100644 (file)
@@ -16,190 +16,197 @@ import stringify from './stringify';
 import test from './test';
 
 async function main() {
-  if (Context.attributes.uid === 0) {
-    throw new ErrorWithMetadata('Cannot run as root');
-  }
-
-  // Skip first two args (node executable and main.js script).
-  const options = await getOptions(process.argv.slice(2));
-
-  setLogLevel(options.logLevel);
-
-  debug(() => {
-    log.debug('process.argv:\n\n' + stringify(process.argv) + '\n');
-    log.debug('getOptions():\n\n' + stringify(options) + '\n');
-  });
-
-  if (process.cwd() === root) {
-    log.info(`Working from root: ${simplify(root)}`);
-  } else {
-    log.notice(`Changing to root: ${simplify(root)}`);
-    process.chdir(root);
-  }
-
-  log.info('Running tests');
-
-  await test();
-
-  if (options.testsOnly) {
-    return;
-  }
-
-  const project = await readProject(path.join(root, 'project.json'));
-
-  const hostname = os.hostname();
-
-  const profiles = project.profiles ?? {};
-
-  const [profile] =
-    Object.entries(profiles).find(([, {pattern}]) => {
-      if (regExpFromString(pattern).test(hostname)) {
-        return true;
-      }
-    }) || [];
+    if (Context.attributes.uid === 0) {
+        throw new ErrorWithMetadata('Cannot run as root');
+    }
 
-  log.info(`Profile: ${profile || 'n/a'}`);
+    // Skip first two args (node executable and main.js script).
+    const options = await getOptions(process.argv.slice(2));
 
-  const profileVariables: {[key: string]: JSONValue} = profile
-    ? profiles[profile]!.variables ?? {}
-    : {};
+    setLogLevel(options.logLevel);
 
-  const platform = Context.attributes.platform;
+    debug(() => {
+        log.debug('process.argv:\n\n' + stringify(process.argv) + '\n');
+        log.debug('getOptions():\n\n' + stringify(options) + '\n');
+    });
 
-  log.info(`Platform: ${platform}`);
+    if (process.cwd() === root) {
+        log.info(`Working from root: ${simplify(root)}`);
+    } else {
+        log.notice(`Changing to root: ${simplify(root)}`);
+        process.chdir(root);
+    }
 
-  const {aspects, variables: platformVariables = {}} = project.platforms[
-    platform
-  ];
+    log.info('Running tests');
 
-  // Register tasks.
-  const candidateTasks = [];
+    await test();
 
-  for (const aspect of aspects) {
-    switch (aspect) {
-      case 'launchd':
-        require('../aspects/launchd');
-        break;
-      case 'terminfo':
-        require('../aspects/terminfo');
-        break;
+    if (options.testsOnly) {
+        return;
     }
 
-    // Check for an exact match of the starting task if `--start-at-task=` was
-    // supplied.
-    for (const [, name] of Context.tasks.get(aspect)) {
-      if (name === options.startAt.literal) {
-        options.startAt.found = true;
-      } else if (
-        !options.startAt.found &&
-        options.startAt.fuzzy &&
-        options.startAt.fuzzy.test(name)
-      ) {
-        candidateTasks.push(name);
-      }
-    }
-  }
+    const project = await readProject(path.join(root, 'project.json'));
 
-  if (!options.startAt.found && candidateTasks.length === 1) {
-    log.notice(`Matching task found: ${candidateTasks[0]}`);
+    const hostname = os.hostname();
 
-    log();
+    const profiles = project.profiles ?? {};
 
-    const reply = await prompt('Start running at this task? [y/n]: ');
+    const [profile] =
+        Object.entries(profiles).find(([, {pattern}]) => {
+            if (regExpFromString(pattern).test(hostname)) {
+                return true;
+            }
+        }) || [];
 
-    if (/^\s*y(?:e(?:s)?)?\s*$/i.test(reply)) {
-      options.startAt.found = true;
-      options.startAt.literal = candidateTasks[0];
-    } else {
-      throw new ErrorWithMetadata('User aborted');
-    }
-  } else if (!options.startAt.found && candidateTasks.length > 1) {
-    log.notice(`${candidateTasks.length} tasks found:\n`);
+    log.info(`Profile: ${profile || 'n/a'}`);
 
-    const width = candidateTasks.length.toString().length;
+    const profileVariables: {[key: string]: JSONValue} = profile
+        ? profiles[profile]!.variables ?? {}
+        : {};
 
-    while (!options.startAt.found) {
-      candidateTasks.forEach((name, i) => {
-        log(`${(i + 1).toString().padStart(width)}: ${name}`);
-      });
+    const platform = Context.attributes.platform;
 
-      log();
+    log.info(`Platform: ${platform}`);
 
-      const reply = await prompt('Start at task number: ');
+    const {aspects, variables: platformVariables = {}} = project.platforms[
+        platform
+    ];
 
-      const choice = parseInt(reply.trim(), 10);
+    // Register tasks.
+    const candidateTasks = [];
 
-      if (
-        Number.isNaN(choice) ||
-        choice < 1 ||
-        choice > candidateTasks.length
-      ) {
-        log.warn(
-          `Invalid choice ${JSON.stringify(
-            reply
-          )}; try again or press CTRL-C to abort.`
-        );
+    for (const aspect of aspects) {
+        switch (aspect) {
+            case 'launchd':
+                require('../aspects/launchd');
+                break;
+            case 'terminfo':
+                require('../aspects/terminfo');
+                break;
+        }
 
-        log();
-      } else {
-        options.startAt.found = true;
-        options.startAt.literal = candidateTasks[choice - 1];
-      }
+        // Check for an exact match of the starting task if `--start-at-task=` was
+        // supplied.
+        for (const [, name] of Context.tasks.get(aspect)) {
+            if (name === options.startAt.literal) {
+                options.startAt.found = true;
+            } else if (
+                !options.startAt.found &&
+                options.startAt.fuzzy &&
+                options.startAt.fuzzy.test(name)
+            ) {
+                candidateTasks.push(name);
+            }
+        }
     }
-  }
-
-  const baseVariables = merge(profileVariables, platformVariables);
 
-  // Execute tasks.
-  try {
-    for (const aspect of aspects) {
-      const {variables: aspectVariables = {}} = await readAspect(
-        path.join(root, 'aspects', aspect, 'aspect.json')
-      );
-
-      if (options.focused.size && !options.focused.has(aspect)) {
-        log.info(`Skipping aspect: ${aspect}`);
-        continue;
-      }
+    if (!options.startAt.found && candidateTasks.length === 1) {
+        log.notice(`Matching task found: ${candidateTasks[0]}`);
 
-      const variables = merge(aspectVariables, baseVariables);
+        log();
 
-      // log.debug(`variables:\n\n${JSON.stringify(variables, null, 2)}\n`);
+        const reply = await prompt('Start running at this task? [y/n]: ');
 
-      for (const [callback, name] of Context.tasks.get(aspect)) {
-        if (!options.startAt.found || name === options.startAt.literal) {
-          options.startAt.found = false;
-          log.info(`Task: ${name}`);
+        if (/^\s*y(?:e(?:s)?)?\s*$/i.test(reply)) {
+            options.startAt.found = true;
+            options.startAt.literal = candidateTasks[0];
+        } else {
+            throw new ErrorWithMetadata('User aborted');
+        }
+    } else if (!options.startAt.found && candidateTasks.length > 1) {
+        log.notice(`${candidateTasks.length} tasks found:\n`);
+
+        const width = candidateTasks.length.toString().length;
+
+        while (!options.startAt.found) {
+            candidateTasks.forEach((name, i) => {
+                log(`${(i + 1).toString().padStart(width)}: ${name}`);
+            });
+
+            log();
+
+            const reply = await prompt('Start at task number: ');
+
+            const choice = parseInt(reply.trim(), 10);
+
+            if (
+                Number.isNaN(choice) ||
+                choice < 1 ||
+                choice > candidateTasks.length
+            ) {
+                log.warn(
+                    `Invalid choice ${JSON.stringify(
+                        reply
+                    )}; try again or press CTRL-C to abort.`
+                );
+
+                log();
+            } else {
+                options.startAt.found = true;
+                options.startAt.literal = candidateTasks[choice - 1];
+            }
+        }
+    }
 
-          await Context.withContext({aspect, variables}, async () => {
-            await callback();
-          });
+    const baseVariables = merge(profileVariables, platformVariables);
+
+    // Execute tasks.
+    try {
+        for (const aspect of aspects) {
+            const {variables: aspectVariables = {}} = await readAspect(
+                path.join(root, 'aspects', aspect, 'aspect.json')
+            );
+
+            if (options.focused.size && !options.focused.has(aspect)) {
+                log.info(`Skipping aspect: ${aspect}`);
+                continue;
+            }
+
+            const variables = merge(aspectVariables, baseVariables);
+
+            // log.debug(`variables:\n\n${JSON.stringify(variables, null, 2)}\n`);
+
+            for (const [callback, name] of Context.tasks.get(aspect)) {
+                if (
+                    !options.startAt.found ||
+                    name === options.startAt.literal
+                ) {
+                    options.startAt.found = false;
+                    log.info(`Task: ${name}`);
+
+                    await Context.withContext({aspect, variables}, async () => {
+                        await callback();
+                    });
+                }
+            }
         }
-      }
+    } finally {
+        const counts = Object.entries(Context.counts)
+            .map(([name, count]) => {
+                return `${name}=${count}`;
+            })
+            .join(' ');
+
+        log.info(`Summary: ${counts}`);
     }
-  } finally {
-    const counts = Object.entries(Context.counts)
-      .map(([name, count]) => {
-        return `${name}=${count}`;
-      })
-      .join(' ');
-
-    log.info(`Summary: ${counts}`);
-  }
 }
 
 main().catch((error) => {
-  if (error instanceof ErrorWithMetadata) {
-    if (error.metadata) {
-      log.error(
-        `${error.message}\n\n${JSON.stringify(error.metadata, null, 2)}\n`
-      );
+    if (error instanceof ErrorWithMetadata) {
+        if (error.metadata) {
+            log.error(
+                `${error.message}\n\n${JSON.stringify(
+                    error.metadata,
+                    null,
+                    2
+                )}\n`
+            );
+        } else {
+            log.error(error.message);
+        }
     } else {
-      log.error(error.message);
+        log.error(error.toString());
     }
-  } else {
-    log.error(error.toString());
-  }
 
-  process.exit(1);
+    process.exit(1);
 });
index 5f96583baf19d226eb3feb8da07580e40cba8e85..d6e1bd6fa7f42dd925dd924c22f737741aea7d16 100644 (file)
@@ -1,40 +1,43 @@
 type Variables = {
-  [key: string]: JSONValue;
+    [key: string]: JSONValue;
 };
 
 export default function merge(
-  variables: Variables,
-  ...rest: Array<Variables>
+    variables: Variables,
+    ...rest: Array<Variables>
 ): Variables {
-  if (!rest.length) {
-    return variables;
-  } else if (rest.length === 1) {
-    return mergeObjects(variables, rest[0]);
-  } else {
-    const last = rest.pop()!;
-    const penultimate = rest.pop()!;
+    if (!rest.length) {
+        return variables;
+    } else if (rest.length === 1) {
+        return mergeObjects(variables, rest[0]);
+    } else {
+        const last = rest.pop()!;
+        const penultimate = rest.pop()!;
 
-    return merge(variables, ...rest.concat(merge(penultimate, last)));
-  }
+        return merge(variables, ...rest.concat(merge(penultimate, last)));
+    }
 }
 
 function mergeObjects(target: Variables, source: Variables): Variables {
-  const output: Variables = {...target};
+    const output: Variables = {...target};
 
-  Object.entries(source).forEach(([key, value]) => {
-    if (
-      value &&
-      typeof value === 'object' &&
-      !Array.isArray(value) &&
-      target[key] &&
-      typeof target[key] === 'object' &&
-      !Array.isArray(target[key])
-    ) {
-      output[key] = mergeObjects(target[key] as Variables, value as Variables);
-    } else {
-      output[key] = value;
-    }
-  });
+    Object.entries(source).forEach(([key, value]) => {
+        if (
+            value &&
+            typeof value === 'object' &&
+            !Array.isArray(value) &&
+            target[key] &&
+            typeof target[key] === 'object' &&
+            !Array.isArray(target[key])
+        ) {
+            output[key] = mergeObjects(
+                target[key] as Variables,
+                value as Variables
+            );
+        } else {
+            output[key] = value;
+        }
+    });
 
-  return output;
+    return output;
 }
index 8b7834c97612740165527bfc4f3995cd83f5ae7a..2011420a10428dfc754f310e453e210fa7c4d03e 100644 (file)
@@ -2,44 +2,44 @@ import * as readline from 'readline';
 import {Writable} from 'stream';
 
 type Options = {
-  private?: boolean;
+    private?: boolean;
 };
 
 export default async function prompt(
-  text: string,
-  options: Options = {}
+    text: string,
+    options: Options = {}
 ): Promise<string> {
-  let muted = false;
+    let muted = false;
 
-  // https://stackoverflow.com/a/33500118/2103996
-  const stdout = new Writable({
-    write: (chunk, _encoding, callback) => {
-      if (!muted) {
-        process.stdout.write(chunk);
-      }
-      callback();
-    },
-  });
-
-  const rl = readline.createInterface({
-    historySize: 0,
-    input: process.stdin,
-    output: stdout,
-    terminal: true,
-  });
+    // https://stackoverflow.com/a/33500118/2103996
+    const stdout = new Writable({
+        write: (chunk, _encoding, callback) => {
+            if (!muted) {
+                process.stdout.write(chunk);
+            }
+            callback();
+        },
+    });
 
-  try {
-    const response = new Promise<string>((resolve) => {
-      rl.question(text, (response) => {
-        process.stdout.write('\n');
-        resolve(response);
-      });
+    const rl = readline.createInterface({
+        historySize: 0,
+        input: process.stdin,
+        output: stdout,
+        terminal: true,
     });
 
-    muted = !!options.private;
+    try {
+        const response = new Promise<string>((resolve) => {
+            rl.question(text, (response) => {
+                process.stdout.write('\n');
+                resolve(response);
+            });
+        });
+
+        muted = !!options.private;
 
-    return await response;
-  } finally {
-    rl.close();
-  }
+        return await response;
+    } finally {
+        rl.close();
+    }
 }
index 7895d7ba9353eb57fb356f6d55c1f96828bf3875..ae28e7cb465657970d8f73de14394f0a67d464eb 100644 (file)
@@ -7,17 +7,17 @@ import {Aspect, assertAspect} from './types/Aspect';
 const readFile = promisify(fs.readFile);
 
 export default async function readAspect(path: string): Promise<Aspect> {
-  log.debug(`Reading aspect configuration: ${path}`);
+    log.debug(`Reading aspect configuration: ${path}`);
 
-  const json = await readFile(path, 'utf8');
+    const json = await readFile(path, 'utf8');
 
-  const aspect = JSON.parse(json);
+    const aspect = JSON.parse(json);
 
-  try {
-    assertAspect(aspect);
-  } catch (error) {
-    throw new Error(`${error.message} in ${path}`);
-  }
+    try {
+        assertAspect(aspect);
+    } catch (error) {
+        throw new Error(`${error.message} in ${path}`);
+    }
 
-  return aspect;
+    return aspect;
 }
index e5a0946b95e9b2bbbd0c6164963f7d4c76da3dd1..ebc1f80bcf1e2e8a7ee74963d35797e926ea5141 100644 (file)
@@ -7,17 +7,17 @@ import {Project, assertProject} from './types/Project';
 const readFile = promisify(fs.readFile);
 
 export default async function readProject(path: string): Promise<Project> {
-  log.debug(`Reading project configuration: ${path}`);
+    log.debug(`Reading project configuration: ${path}`);
 
-  const json = await readFile(path, 'utf8');
+    const json = await readFile(path, 'utf8');
 
-  const project = JSON.parse(json);
+    const project = JSON.parse(json);
 
-  try {
-    assertProject(project);
-  } catch (error) {
-    throw new Error(`${error.message} in ${path}`);
-  }
+    try {
+        assertProject(project);
+    } catch (error) {
+        throw new Error(`${error.message} in ${path}`);
+    }
 
-  return project;
+    return project;
 }
index 9d7121c09476b1af987a5370dd4ea0ea638c6a57..536a59566d2334ba08c4252974ee8289c5d142bc 100644 (file)
@@ -4,15 +4,15 @@
 const VALID_REGEXP = /^\/(.+)\/([gimsuy]*)$/;
 
 export default function regExpFromString(pattern: string): RegExp {
-  const match = pattern.match(VALID_REGEXP);
+    const match = pattern.match(VALID_REGEXP);
 
-  if (!match) {
-    throw new Error(
-      `Invalid pattern ${JSON.stringify(
-        pattern
-      )} does not match ${VALID_REGEXP}`
-    );
-  }
+    if (!match) {
+        throw new Error(
+            `Invalid pattern ${JSON.stringify(
+                pattern
+            )} does not match ${VALID_REGEXP}`
+        );
+    }
 
-  return new RegExp(match[1], match[2] || '');
+    return new RegExp(match[1], match[2] || '');
 }
index 06816a0034ed9237c6588c489a88b96638450649..3cf4f0528c282d9596375be14d167766f5a3d270 100644 (file)
@@ -2,16 +2,16 @@ import * as child_process from 'child_process';
 import {randomBytes} from 'crypto';
 
 type Options = {
-  passphrase?: string;
+    passphrase?: string;
 };
 
 type Result = {
-  command: string;
-  error: Error | null;
-  signal: string | null;
-  status: number | null;
-  stderr: string;
-  stdout: string;
+    command: string;
+    error: Error | null;
+    signal: string | null;
+    status: number | null;
+    stderr: string;
+    stdout: string;
 };
 
 /**
@@ -19,67 +19,67 @@ type Result = {
  * is supplied via the `options` parameter.
  */
 export default async function run(
-  command: string,
-  args: Array<string>,
-  options: Options = {}
+    command: string,
+    args: Array<string>,
+    options: Options = {}
 ): Promise<Result> {
-  return new Promise((resolve, reject) => {
-    const prompt = `sudo[${randomBytes(16).toString('hex')}]:`;
+    return new Promise((resolve, reject) => {
+        const prompt = `sudo[${randomBytes(16).toString('hex')}]:`;
 
-    const final = options.passphrase
-      ? ['sudo', '-S', '-k', '-p', prompt, '--', command, ...args]
-      : [command, ...args];
+        const final = options.passphrase
+            ? ['sudo', '-S', '-k', '-p', prompt, '--', command, ...args]
+            : [command, ...args];
 
-    const result = {
-      command: final.join(' '),
-      error: null,
-      signal: null,
-      status: null,
-      stderr: '',
-      stdout: '',
-    };
+        const result = {
+            command: final.join(' '),
+            error: null,
+            signal: null,
+            status: null,
+            stderr: '',
+            stdout: '',
+        };
 
-    const child = child_process.spawn(final[0], final.slice(1));
+        const child = child_process.spawn(final[0], final.slice(1));
 
-    // Sadly, we may see "Sorry, try again" if the wrong password is
-    // supplied, because sudo may be configured to log it directly to
-    // /dev/tty, not to stderr (true on macOS, not on Amazon Linux).
-    //
-    // See: https://github.com/sudo-project/sudo/blob/972670bf/plugins/sudoers/sudo_printf.c#L47
-    child.stderr.on('data', (data) => {
-      if (data.toString() === prompt) {
-        child.stdin.write(`${options.passphrase}\n`);
+        // Sadly, we may see "Sorry, try again" if the wrong password is
+        // supplied, because sudo may be configured to log it directly to
+        // /dev/tty, not to stderr (true on macOS, not on Amazon Linux).
+        //
+        // See: https://github.com/sudo-project/sudo/blob/972670bf/plugins/sudoers/sudo_printf.c#L47
+        child.stderr.on('data', (data) => {
+            if (data.toString() === prompt) {
+                child.stdin.write(`${options.passphrase}\n`);
 
-        // No point in retrying; by calling `end()` here we'll get one shot.
-        child.stdin.end();
-      } else {
-        result.stderr += data.toString();
-      }
-    });
+                // No point in retrying; by calling `end()` here we'll get one shot.
+                child.stdin.end();
+            } else {
+                result.stderr += data.toString();
+            }
+        });
 
-    child.stdout.on('data', (data) => {
-      result.stdout += data.toString();
-    });
+        child.stdout.on('data', (data) => {
+            result.stdout += data.toString();
+        });
 
-    child.on('error', (error) =>
-      reject({
-        ...result,
-        error,
-      })
-    );
+        child.on('error', (error) =>
+            reject({
+                ...result,
+                error,
+            })
+        );
 
-    child.on('exit', (status, signal) => {
-      if (typeof status === 'number') {
-        resolve({
-          ...result,
-          status,
-        });
-      } else if (signal) {
-        resolve({
-          ...result,
-          status,
+        child.on('exit', (status, signal) => {
+            if (typeof status === 'number') {
+                resolve({
+                    ...result,
+                    status,
+                });
+            } else if (signal) {
+                resolve({
+                    ...result,
+                    status,
+                });
+            }
         });
-      }
     });
-  });
 }
index bd5e07ab41630de34a841fd6880d7f5295dc3320..2c968915133c284cc2b9e30d4c5d304c948d661e 100644 (file)
@@ -2,11 +2,11 @@ import {homedir} from 'os';
 import {join} from 'path';
 
 export default function simplify(path: string) {
-  const home = homedir();
+    const home = homedir();
 
-  if (path.startsWith(home)) {
-    return join('~', path.slice(home.length));
-  } else {
-    return path;
-  }
+    if (path.startsWith(home)) {
+        return join('~', path.slice(home.length));
+    } else {
+        return path;
+    }
 }
index c1b7ed2c7ac3006af1b8d81bc31ce185dcd29cdb..dd8f505f42edc71268c499aa2db12c38334a83b1 100644 (file)
@@ -8,41 +8,44 @@ import ErrorWithMetadata from './ErrorWithMetadata';
  * Doesn't return stderr or stdout; resolves on success and rejects on failure.
  */
 export default async function spawn(
-  command: string,
-  ...args: Array<string>
+    command: string,
+    ...args: Array<string>
 ): Promise<void> {
-  return new Promise((resolve, reject) => {
-    let stderr = '';
-    let stdout = '';
-
-    function fail(message: string) {
-      const description = [command, ...args].join(' ');
-      const metadata = {stderr, stdout};
-
-      reject(
-        new ErrorWithMetadata(`command ${description} ${message}`, metadata)
-      );
-    }
-
-    const child = child_process.spawn(command, args, {
-      stdio: ['inherit', 'pipe', 'pipe'],
-    });
-
-    child.stderr.on('data', (data) => (stderr += data.toString()));
-    child.stdout.on('data', (data) => (stdout += data.toString()));
-
-    child.on('error', (error) => {
-      fail(`encountered error: ${error}`);
-    });
-
-    child.on('exit', (code, signal) => {
-      if (code) {
-        fail(`exited with status ${code}`);
-      } else if (signal) {
-        fail(`exited due to signal ${signal}`);
-      } else {
-        resolve();
-      }
+    return new Promise((resolve, reject) => {
+        let stderr = '';
+        let stdout = '';
+
+        function fail(message: string) {
+            const description = [command, ...args].join(' ');
+            const metadata = {stderr, stdout};
+
+            reject(
+                new ErrorWithMetadata(
+                    `command ${description} ${message}`,
+                    metadata
+                )
+            );
+        }
+
+        const child = child_process.spawn(command, args, {
+            stdio: ['inherit', 'pipe', 'pipe'],
+        });
+
+        child.stderr.on('data', (data) => (stderr += data.toString()));
+        child.stdout.on('data', (data) => (stdout += data.toString()));
+
+        child.on('error', (error) => {
+            fail(`encountered error: ${error}`);
+        });
+
+        child.on('exit', (code, signal) => {
+            if (code) {
+                fail(`exited with status ${code}`);
+            } else if (signal) {
+                fail(`exited due to signal ${signal}`);
+            } else {
+                resolve();
+            }
+        });
     });
-  });
 }
index 87be50927f1c32719cde6363dcd5ec29c3d7f340..bf810b08156482d2f736da7be6ed694f9e6c7296 100644 (file)
@@ -3,20 +3,20 @@ import Context from './Fig/Context';
 import run from './run';
 
 type Stats = {
-  group: string;
-  mode: Mode;
-  target?: string;
-  type: 'directory' | 'file' | 'link' | 'socket' | 'special' | 'unknown';
-  user: string;
+    group: string;
+    mode: Mode;
+    target?: string;
+    type: 'directory' | 'file' | 'link' | 'socket' | 'special' | 'unknown';
+    user: string;
 };
 
 const TYPE_MAP = {
-  'character device': 'special',
-  'character special file': 'special',
-  directory: 'directory',
-  'regular file': 'file',
-  socket: 'socket',
-  'symbolic link': 'link',
+    'character device': 'special',
+    'character special file': 'special',
+    directory: 'directory',
+    'regular file': 'file',
+    socket: 'socket',
+    'symbolic link': 'link',
 } as const;
 
 /**
@@ -27,80 +27,80 @@ const TYPE_MAP = {
  * with `sudo` if necessary.
  */
 export default async function stat(
-  path: string
+    path: string
 ): Promise<Error | Stats | null> {
-  if (Context.attributes.platform === 'darwin') {
-    const formats = {
-      group: '%Sg',
-      mode: '%Lp',
-      newline: '%n',
-      target: '%Y',
-      type: '%HT',
-      user: '%Su',
-    };
+    if (Context.attributes.platform === 'darwin') {
+        const formats = {
+            group: '%Sg',
+            mode: '%Lp',
+            newline: '%n',
+            target: '%Y',
+            type: '%HT',
+            user: '%Su',
+        };
 
-    const formatString = [
-      formats.mode,
-      formats.type,
-      formats.user,
-      formats.group,
-      formats.target,
-    ].join(formats.newline);
+        const formatString = [
+            formats.mode,
+            formats.type,
+            formats.user,
+            formats.group,
+            formats.target,
+        ].join(formats.newline);
 
-    // Try without sudo, then with.
-    for (const sudo of [false, true]) {
-      const options = sudo
-        ? {passphrase: await Context.sudoPassphrase}
-        : undefined;
+        // Try without sudo, then with.
+        for (const sudo of [false, true]) {
+            const options = sudo
+                ? {passphrase: await Context.sudoPassphrase}
+                : undefined;
 
-      const {status, stderr, stdout} = await run(
-        'stat',
-        ['-f', formatString, path],
-        options
-      );
+            const {status, stderr, stdout} = await run(
+                'stat',
+                ['-f', formatString, path],
+                options
+            );
 
-      if (status === 0) {
-        const [mode, type, user, group, target] = stdout.split('\n');
+            if (status === 0) {
+                const [mode, type, user, group, target] = stdout.split('\n');
 
-        const paddedMode = mode.padStart(4, '0');
+                const paddedMode = mode.padStart(4, '0');
 
-        assertMode(paddedMode);
+                assertMode(paddedMode);
 
-        return {
-          group,
-          mode: paddedMode,
-          target: target || undefined,
-          type: (TYPE_MAP as any)[type.toLowerCase()] || 'unknown',
-          user,
-        };
-      }
+                return {
+                    group,
+                    mode: paddedMode,
+                    target: target || undefined,
+                    type: (TYPE_MAP as any)[type.toLowerCase()] || 'unknown',
+                    user,
+                };
+            }
 
-      if (/no such file/i.test(stderr)) {
-        return null;
-      } else if (!/permission denied/i.test(stderr)) {
-        // Give up...
-        break;
-      }
+            if (/no such file/i.test(stderr)) {
+                return null;
+            } else if (!/permission denied/i.test(stderr)) {
+                // Give up...
+                break;
+            }
+        }
+    } else {
+        throw new Error('TODO: implement');
+        // GNU: command stat --printf '%a\n%F\n%G\n%U\n'
+        // a = mode
+        // F = type
+        // G = group name
+        // U = user name
+        // maybe %N (link target): prints 'src' -> 'target'
+        // 644, 1777
+        // regular file, directory, symbolic link, character special file
     }
-  } else {
-    throw new Error('TODO: implement');
-    // GNU: command stat --printf '%a\n%F\n%G\n%U\n'
-    // a = mode
-    // F = type
-    // G = group name
-    // U = user name
-    // maybe %N (link target): prints 'src' -> 'target'
-    // 644, 1777
-    // regular file, directory, symbolic link, character special file
-  }
 
-  throw new ErrorWithMetadata(`Unable to stat ${path}`);
+    throw new ErrorWithMetadata(`Unable to stat ${path}`);
 }
 
 const MODE_REGEXP = /^0[0-7]{3}$/;
 
 function assertMode(mode: string): asserts mode is Mode {
-  if (!MODE_REGEXP.test(mode)) {
-    throw new Error(`Invalid mode ${mode}`);
-  }
+    if (!MODE_REGEXP.test(mode)) {
+        throw new Error(`Invalid mode ${mode}`);
+    }
 }
index f90edda6a6bc2feeefb52708282a5ebead34219f..2dd4289bb2f5166172d9ea1a117a4a55743cd782 100644 (file)
@@ -7,70 +7,72 @@ const CIRCULAR = `${LAQUO}circular${RAQUO}`;
  * types (eg. a `RegExp` is printed as "/pattern/" instead of "{}" etc).
  */
 export default function stringify(value: unknown) {
-  let indent = '';
+    let indent = '';
 
-  const seen = new Set<unknown>();
+    const seen = new Set<unknown>();
 
-  function traverse(value: unknown) {
-    if (
-      value == null ||
-      typeof value === 'boolean' ||
-      typeof value === 'number' ||
-      typeof value === 'symbol' ||
-      value instanceof RegExp
-    ) {
-      return String(value);
-    } else if (typeof value === 'string') {
-      return JSON.stringify(value);
-    } else if (Array.isArray(value)) {
-      if (seen.has(value)) {
-        return CIRCULAR;
-      } else {
-        seen.add(value);
-        indent += '  ';
-        let array = '[\n';
-        array += value
-          .map((v) => {
-            return `${indent}${traverse(v)},`;
-          })
-          .join('\n');
-        indent = indent.slice(0, -2);
-        array += `\n${indent}]`;
-        return array;
-      }
-    } else if (typeof value === 'object') {
-      const toString = Object.prototype.toString.call(value);
-      if (toString === '[object Object]') {
-        if (seen.has(value)) {
-          return CIRCULAR;
+    function traverse(value: unknown) {
+        if (
+            value == null ||
+            typeof value === 'boolean' ||
+            typeof value === 'number' ||
+            typeof value === 'symbol' ||
+            value instanceof RegExp
+        ) {
+            return String(value);
+        } else if (typeof value === 'string') {
+            return JSON.stringify(value);
+        } else if (Array.isArray(value)) {
+            if (seen.has(value)) {
+                return CIRCULAR;
+            } else {
+                seen.add(value);
+                indent += '  ';
+                let array = '[\n';
+                array += value
+                    .map((v) => {
+                        return `${indent}${traverse(v)},`;
+                    })
+                    .join('\n');
+                indent = indent.slice(0, -2);
+                array += `\n${indent}]`;
+                return array;
+            }
+        } else if (typeof value === 'object') {
+            const toString = Object.prototype.toString.call(value);
+            if (toString === '[object Object]') {
+                if (seen.has(value)) {
+                    return CIRCULAR;
+                } else {
+                    seen.add(value);
+                    indent += '  ';
+                    let object = '{\n';
+                    object += Object.entries(value!)
+                        .map(([key, value]) => {
+                            return `${indent}${JSON.stringify(key)}: ${traverse(
+                                value
+                            )},`;
+                        })
+                        .join('\n');
+                    indent = indent.slice(0, -2);
+                    object += `\n${indent}}`;
+                    return object;
+                }
+            } else {
+                return toString;
+            }
+        } else if (typeof value === 'function') {
+            return value
+                .toString()
+                .split('\n')
+                .map((line, i) => {
+                    return i ? `${indent}${line}` : line;
+                })
+                .join('\n');
         } else {
-          seen.add(value);
-          indent += '  ';
-          let object = '{\n';
-          object += Object.entries(value!)
-            .map(([key, value]) => {
-              return `${indent}${JSON.stringify(key)}: ${traverse(value)},`;
-            })
-            .join('\n');
-          indent = indent.slice(0, -2);
-          object += `\n${indent}}`;
-          return object;
+            return `${LAQUO}unknown${RAQUO}`;
         }
-      } else {
-        return toString;
-      }
-    } else if (typeof value === 'function') {
-      return value
-        .toString()
-        .split('\n')
-        .map((line, i) => {
-          return i ? `${indent}${line}` : line;
-        })
-        .join('\n');
-    } else {
-      return `${LAQUO}unknown${RAQUO}`;
     }
-  }
 
-  return traverse(value);
+    return traverse(value);
 }
index b122c3d3568c3c35cb0a094e92438370edd9cd19..e340be5d243d40e9639ce63bc9b503d2c185bd90 100644 (file)
@@ -13,13 +13,13 @@ const TMP_DIR = tmpdir();
  * leave them on the disk for debugging purposes.
  */
 export default async function tempfile(contents: string): Promise<string> {
-  const name = randomBytes(16).toString('hex');
+    const name = randomBytes(16).toString('hex');
 
-  const path = join(TMP_DIR, name);
+    const path = join(TMP_DIR, name);
 
-  await fs.writeFile(path, contents, 'utf8');
+    await fs.writeFile(path, contents, 'utf8');
 
-  log.debug(`Wrote ${contents.length} bytes to ${path}`);
+    log.debug(`Wrote ${contents.length} bytes to ${path}`);
 
-  return path;
+    return path;
 }
index 18fc417da543347c8b7c1f5ddc1d9f9f033499a3..e8d6cf57a5613386f866ce9a9a4ff4a132aabc63 100644 (file)
@@ -1,5 +1,5 @@
 export type Scope = {
-  [property: string]: JSONValue;
+    [property: string]: JSONValue;
 };
 
 /**
@@ -7,31 +7,31 @@ export type Scope = {
  * be evaluated to produce the template output).
  */
 export function compile(source: string) {
-  let output = 'let __buffer__ = "";\n';
-
-  let context = 'TemplateText';
-
-  for (const token of tokenize(source)) {
-    if (token.kind === 'TemplateText') {
-      output += `__buffer__ += ${JSON.stringify(token.text)};\n`;
-    } else if (token.kind === 'HostText') {
-      if (context === 'Expression') {
-        output += `__buffer__ += (${token.text.trim()});\n`;
-      } else if (context === 'Statement') {
-        output += `${token.text.trim()}\n`;
-      }
-    } else if (token.kind === 'StartExpression') {
-      context = 'Expression';
-    } else if (token.kind === 'StartStatement') {
-      context = 'Statement';
-    } else if (token.kind === 'EndDelimiter') {
-      context = 'TemplateText';
+    let output = 'let __buffer__ = "";\n';
+
+    let context = 'TemplateText';
+
+    for (const token of tokenize(source)) {
+        if (token.kind === 'TemplateText') {
+            output += `__buffer__ += ${JSON.stringify(token.text)};\n`;
+        } else if (token.kind === 'HostText') {
+            if (context === 'Expression') {
+                output += `__buffer__ += (${token.text.trim()});\n`;
+            } else if (context === 'Statement') {
+                output += `${token.text.trim()}\n`;
+            }
+        } else if (token.kind === 'StartExpression') {
+            context = 'Expression';
+        } else if (token.kind === 'StartStatement') {
+            context = 'Statement';
+        } else if (token.kind === 'EndDelimiter') {
+            context = 'TemplateText';
+        }
     }
-  }
 
-  output += 'return __buffer__;\n';
+    output += 'return __buffer__;\n';
 
-  return output;
+    return output;
 }
 
 /**
@@ -40,42 +40,42 @@ export function compile(source: string) {
  * producing the final string result.
  */
 export function fill(compiled: string, scope: Scope = {}) {
-  const context = Object.entries(scope).map(
-    ([key, value]) => `const ${key} = ${JSON.stringify(value)};\n`
-  );
+    const context = Object.entries(scope).map(
+        ([key, value]) => `const ${key} = ${JSON.stringify(value)};\n`
+    );
 
-  const sandbox = new Function(context + compiled);
+    const sandbox = new Function(context + compiled);
 
-  return sandbox();
+    return sandbox();
 }
 
 type Token =
-  | EndDelimiter
-  | HostText
-  | StartExpression
-  | StartStatement
-  | TemplateText;
+    | EndDelimiter
+    | HostText
+    | StartExpression
+    | StartStatement
+    | TemplateText;
 
 type EndDelimiter = {
-  kind: 'EndDelimiter';
+    kind: 'EndDelimiter';
 };
 
 type HostText = {
-  kind: 'HostText';
-  text: string;
+    kind: 'HostText';
+    text: string;
 };
 
 type StartExpression = {
-  kind: 'StartExpression';
+    kind: 'StartExpression';
 };
 
 type StartStatement = {
-  kind: 'StartStatement';
+    kind: 'StartStatement';
 };
 
 type TemplateText = {
-  kind: 'TemplateText';
-  text: string;
+    kind: 'TemplateText';
+    text: string;
 };
 
 /**
@@ -147,83 +147,85 @@ type TemplateText = {
  *
  */
 export function* tokenize(input: string): Generator<Token> {
-  const delimiter = /(<%=|<%-|<%|-%>|%>)/g;
-
-  let i = 0;
-
-  let inHost = false;
-
-  while (i < input.length) {
-    const match = delimiter.exec(input);
-
-    if (match) {
-      const text = match[0];
-
-      if (inHost) {
-        if (text.endsWith('%>')) {
-          yield {
-            kind: 'HostText',
-            text: input.slice(i, match.index),
-          };
-
-          yield {
-            kind: 'EndDelimiter',
-          };
-
-          inHost = false;
-
-          if (text === '-%>') {
-            // Remove next character if it is a newline.
-            if (input[delimiter.lastIndex] === '\n') {
-              delimiter.lastIndex++;
-              i = match.index! + text.length + 1;
-              continue;
+    const delimiter = /(<%=|<%-|<%|-%>|%>)/g;
+
+    let i = 0;
+
+    let inHost = false;
+
+    while (i < input.length) {
+        const match = delimiter.exec(input);
+
+        if (match) {
+            const text = match[0];
+
+            if (inHost) {
+                if (text.endsWith('%>')) {
+                    yield {
+                        kind: 'HostText',
+                        text: input.slice(i, match.index),
+                    };
+
+                    yield {
+                        kind: 'EndDelimiter',
+                    };
+
+                    inHost = false;
+
+                    if (text === '-%>') {
+                        // Remove next character if it is a newline.
+                        if (input[delimiter.lastIndex] === '\n') {
+                            delimiter.lastIndex++;
+                            i = match.index! + text.length + 1;
+                            continue;
+                        }
+                    }
+                } else {
+                    throw new Error(
+                        `Unexpected start delimiter "${text}" at index ${match.index}`
+                    );
+                }
+            } else {
+                if (text === '<%-') {
+                    // Eat whitspace between previous newline and delimiter.
+                    yield {
+                        kind: 'TemplateText',
+                        text: input
+                            .slice(i, match.index)
+                            .replace(/(^|\n)[ \t]+$/, '$1'),
+                    };
+                } else {
+                    yield {
+                        kind: 'TemplateText',
+                        text: input.slice(i, match.index),
+                    };
+                }
+
+                inHost = true;
+
+                if (text === '<%=') {
+                    yield {
+                        kind: 'StartExpression',
+                    };
+                } else if (text.startsWith('<%')) {
+                    yield {
+                        kind: 'StartStatement',
+                    };
+                } else if (text === '%>') {
+                    throw new Error(
+                        `Unexpected end delimiter "%>" at index ${match.index}`
+                    );
+                }
             }
-          }
-        } else {
-          throw new Error(
-            `Unexpected start delimiter "${text}" at index ${match.index}`
-          );
-        }
-      } else {
-        if (text === '<%-') {
-          // Eat whitspace between previous newline and delimiter.
-          yield {
-            kind: 'TemplateText',
-            text: input.slice(i, match.index).replace(/(^|\n)[ \t]+$/, '$1'),
-          };
+
+            i = match.index! + text.length;
         } else {
-          yield {
-            kind: 'TemplateText',
-            text: input.slice(i, match.index),
-          };
-        }
+            yield {
+                kind: 'TemplateText',
+                text: input.slice(i),
+            };
 
-        inHost = true;
-
-        if (text === '<%=') {
-          yield {
-            kind: 'StartExpression',
-          };
-        } else if (text.startsWith('<%')) {
-          yield {
-            kind: 'StartStatement',
-          };
-        } else if (text === '%>') {
-          throw new Error(
-            `Unexpected end delimiter "%>" at index ${match.index}`
-          );
+            break;
         }
-      }
-
-      i = match.index! + text.length;
-    } else {
-      yield {
-        kind: 'TemplateText',
-        text: input.slice(i),
-      };
-
-      break;
     }
-  }
 }
index 0ec27fca2c97178070324ef932984526812f286a..27c248ee882f62fe8f145787ee1565180f230b15 100644 (file)
@@ -12,144 +12,150 @@ const TESTS: Array<[string, () => void | Promise<void>]> = [];
 let context: Array<string> = [];
 
 export function describe(description: string, callback: () => void) {
-  context.push(description);
-  callback();
-  context.pop();
+    context.push(description);
+    callback();
+    context.pop();
 }
 
 export function expect(value: unknown) {
-  return {
-    toBe(expected: unknown) {
-      assert.strictEqual(
-        value,
-        expected,
-        `Expected ${stringify(value)} to be ${stringify(expected)}`
-      );
-    },
-
-    toEqual(expected: unknown) {
-      assert.deepStrictEqual(
-        value,
-        expected,
-        `Expected ${stringify(value)} to equal ${stringify(expected)}`
-      );
-    },
-
-    toMatch(expected: unknown) {
-      if (expected instanceof RegExp) {
-        assert(
-          expected.test(String(value)),
-          `Expected ${stringify(value)} to match ${stringify(expected)}`
-        );
-      } else {
-        throw new Error(`Expected RegExp but received ${typeof expected}`);
-      }
-    },
-
-    toThrow(expected: string | typeof Error | RegExp) {
-      let caught;
-
-      try {
-        if (typeof value === 'function') {
-          value();
-        } else {
-          throw new Error(`Expected function but received ${typeof value}`);
-        }
-      } catch (error) {
-        caught = error;
-      }
-
-      if (!caught) {
-        assert.fail('Expected error but none was thrown');
-      } else {
-        const message = caught.toString();
-
-        if (typeof expected === 'string') {
-          assert.ok(
-            message.includes(expected),
-            `Expected message ${stringify(message)} to contain ${stringify(
-              expected
-            )}`
-          );
-        } else if (expected instanceof RegExp) {
-          assert.ok(
-            expected.test(message),
-            `Expected message ${stringify(message)} to match ${stringify(
-              expected
-            )}`
-          );
-        } else {
-          assert.ok(
-            caught instanceof expected,
-            `Expected error to be instance of ${expected}`
-          );
-        }
-      }
-    },
-  };
+    return {
+        toBe(expected: unknown) {
+            assert.strictEqual(
+                value,
+                expected,
+                `Expected ${stringify(value)} to be ${stringify(expected)}`
+            );
+        },
+
+        toEqual(expected: unknown) {
+            assert.deepStrictEqual(
+                value,
+                expected,
+                `Expected ${stringify(value)} to equal ${stringify(expected)}`
+            );
+        },
+
+        toMatch(expected: unknown) {
+            if (expected instanceof RegExp) {
+                assert(
+                    expected.test(String(value)),
+                    `Expected ${stringify(value)} to match ${stringify(
+                        expected
+                    )}`
+                );
+            } else {
+                throw new Error(
+                    `Expected RegExp but received ${typeof expected}`
+                );
+            }
+        },
+
+        toThrow(expected: string | typeof Error | RegExp) {
+            let caught;
+
+            try {
+                if (typeof value === 'function') {
+                    value();
+                } else {
+                    throw new Error(
+                        `Expected function but received ${typeof value}`
+                    );
+                }
+            } catch (error) {
+                caught = error;
+            }
+
+            if (!caught) {
+                assert.fail('Expected error but none was thrown');
+            } else {
+                const message = caught.toString();
+
+                if (typeof expected === 'string') {
+                    assert.ok(
+                        message.includes(expected),
+                        `Expected message ${stringify(
+                            message
+                        )} to contain ${stringify(expected)}`
+                    );
+                } else if (expected instanceof RegExp) {
+                    assert.ok(
+                        expected.test(message),
+                        `Expected message ${stringify(
+                            message
+                        )} to match ${stringify(expected)}`
+                    );
+                } else {
+                    assert.ok(
+                        caught instanceof expected,
+                        `Expected error to be instance of ${expected}`
+                    );
+                }
+            }
+        },
+    };
 }
 
 export function test(description: string, callback: () => void) {
-  TESTS.push([[...context, description].join(` ${RAQUO} `), callback]);
+    TESTS.push([[...context, description].join(` ${RAQUO} `), callback]);
 }
 
 export async function run() {
-  const start = Date.now();
-
-  let failureCount = 0;
-  let successCount = 0;
-
-  debug(() => log());
-
-  for (const [description, callback] of TESTS) {
-    try {
-      // Need to stay within one line if `clear()` calls below are to work.
-      const trimmedDescription = description.slice(
-        0,
-        process.stderr.columns - ' TEST '.length - 1
-      );
-
-      debug(() => print(yellow.reverse` TEST `, trimmedDescription));
-      await callback();
-      successCount++;
-      await debug(async () => {
-        await print.clear();
-        log(green.reverse` PASS `, description);
-      });
-    } catch (error) {
-      failureCount++;
-      await print.clear();
-      log(red.reverse` FAIL `, description);
-      log(`\n${error.message}\n`);
-      log(error);
-      log();
+    const start = Date.now();
+
+    let failureCount = 0;
+    let successCount = 0;
+
+    debug(() => log());
+
+    for (const [description, callback] of TESTS) {
+        try {
+            // Need to stay within one line if `clear()` calls below are to work.
+            const trimmedDescription = description.slice(
+                0,
+                process.stderr.columns - ' TEST '.length - 1
+            );
+
+            debug(() => print(yellow.reverse` TEST `, trimmedDescription));
+            await callback();
+            successCount++;
+            await debug(async () => {
+                await print.clear();
+                log(green.reverse` PASS `, description);
+            });
+        } catch (error) {
+            failureCount++;
+            await print.clear();
+            log(red.reverse` FAIL `, description);
+            log(`\n${error.message}\n`);
+            log(error);
+            log();
+        }
     }
-  }
 
-  const elapsed = ((Date.now() - start) / 1000).toFixed(2);
+    const elapsed = ((Date.now() - start) / 1000).toFixed(2);
 
-  const successSummary = successCount
-    ? green.bold`${successCount} passed`
-    : '0 passed';
+    const successSummary = successCount
+        ? green.bold`${successCount} passed`
+        : '0 passed';
 
-  const failureSummary = failureCount
-    ? red.bold`${failureCount} failed`
-    : `0 failed`;
+    const failureSummary = failureCount
+        ? red.bold`${failureCount} failed`
+        : `0 failed`;
 
-  const totalSummary = `${successCount + failureCount} total in ${elapsed}s`;
+    const totalSummary = `${successCount + failureCount} total in ${elapsed}s`;
 
-  const logLevel = getLogLevel();
+    const logLevel = getLogLevel();
 
-  if (logLevel >= LOG_LEVEL.DEBUG || failureCount) {
-    log();
-    log(`${successSummary}, ${failureSummary}, ${totalSummary}`);
-    if (logLevel < LOG_LEVEL.DEBUG) {
-      log('Rerun with --debug to see full results');
+    if (logLevel >= LOG_LEVEL.DEBUG || failureCount) {
+        log();
+        log(`${successSummary}, ${failureSummary}, ${totalSummary}`);
+        if (logLevel < LOG_LEVEL.DEBUG) {
+            log('Rerun with --debug to see full results');
+        }
+        log();
     }
-    log();
-  }
 
-  if (failureCount) {
-    throw new ErrorWithMetadata('Test suite failed');
-  }
+    if (failureCount) {
+        throw new ErrorWithMetadata('Test suite failed');
+    }