mirror of
https://github.com/boxpositron/absolute-dotfiles.git
synced 2026-10-24 08:23:19 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fb7007ea9 | |||
| d1aeda397a | |||
| 66a3b9ee4d | |||
| 90ab5452b1 | |||
| 164dffc5eb | |||
| 81e5c6be67 | |||
| d0c2895797 |
@@ -1,5 +1,12 @@
|
||||
{
|
||||
"hooks": {
|
||||
"UserPromptSubmit": [
|
||||
{
|
||||
"hooks": [
|
||||
{ "type": "command", "command": "~/.local/bin/omo-notify", "timeout": 10 }
|
||||
]
|
||||
}
|
||||
],
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
|
||||
+31
-118
@@ -1,117 +1,14 @@
|
||||
// OMO routing for OpenCode and the native agent, using Claude, OpenAI and z.ai models.
|
||||
// OMO routing for the native agent, using Claude, OpenAI and z.ai models.
|
||||
{
|
||||
// Shared metadata.
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/omo.schema.json",
|
||||
"_migrations": [
|
||||
"2026-07-codex-config-jsonc",
|
||||
"2026-08-reasoning-unification",
|
||||
"2026-07-opencode-config-unification"
|
||||
"2026-07-opencode-config-unification",
|
||||
"2026-09-opencode-routing-notice"
|
||||
],
|
||||
|
||||
|
||||
// OpenCode agent and category routing.
|
||||
"[opencode]": {
|
||||
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/dev/assets/omo.schema.json",
|
||||
"claude_code": {
|
||||
"plugins_override": {
|
||||
"oh-my-claudecode@omc": false
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"sisyphus": {
|
||||
"model": "openai/gpt-6-astra"
|
||||
},
|
||||
"hephaestus": {
|
||||
"model": "openai/gpt-5.6-sol",
|
||||
"reasoning": "medium"
|
||||
},
|
||||
"oracle": {
|
||||
"model": "zai-coding-plan/glm-5.3",
|
||||
"reasoning": "high"
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai-coding-plan/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
},
|
||||
"explore": {
|
||||
"model": "zai-coding-plan/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
},
|
||||
"multimodal-looker": {
|
||||
"model": "zai-coding-plan/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
},
|
||||
"prometheus": {
|
||||
"model": "zai-coding-plan/glm-5.3",
|
||||
"reasoning": "high"
|
||||
},
|
||||
"metis": {
|
||||
"model": "zai-coding-plan/glm-5.3",
|
||||
"reasoning": "high"
|
||||
},
|
||||
"momus": {
|
||||
"model": "zai-coding-plan/glm-5.3",
|
||||
"reasoning": "high"
|
||||
},
|
||||
"atlas": {
|
||||
"model": "openai/gpt-6-astra"
|
||||
},
|
||||
"sisyphus-junior": {
|
||||
"model": "openai/gpt-6-astra",
|
||||
"reasoning": "high"
|
||||
},
|
||||
"document-writer": {
|
||||
"model": "zai-coding-plan/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
}
|
||||
},
|
||||
"categories": {
|
||||
"visual-engineering": {
|
||||
"model": "openai/gpt-6-astra",
|
||||
"reasoning": "high",
|
||||
"prompt_append": "For substantive UI work, load frontend-workflow on demand before editing. Use the existing GPT model for design, browser screenshot inspection, and a separate critique pass. Obtain the project design context, supplied references, primary user action, acceptance criteria, run command, assigned files and phase from the delegator. Own one sequential write lane; review-only passes do not edit. Load impeccable for direction and web-design-guidelines for accessibility/interaction review as needed. Preserve established conventions unless redesign is requested; the user brief overrides aesthetic bans. Read screenshot image content directly with the current GPT model, not only paths/DOM and not look_at as a substitute. Render desktop/mobile and intermediate width when warranted, rank up to three material visible issues, fix them, recapture, and retest interactions/states. Normally at most three passes; stop when acceptance criteria pass and report residual issues. Do not delegate a plan for a small implementation whose brief already resolves the decisions."
|
||||
},
|
||||
"ultrabrain": {
|
||||
"model": "openai/gpt-6-astra",
|
||||
"reasoning": "max"
|
||||
},
|
||||
"deep-low": {
|
||||
"model": "zai-coding-plan/glm-5.3",
|
||||
"reasoning": "high"
|
||||
},
|
||||
"artistry": {
|
||||
"model": "zai-coding-plan/glm-5.3",
|
||||
"reasoning": "high"
|
||||
},
|
||||
"quick": {
|
||||
"model": "zai-coding-plan/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
},
|
||||
"unspecified-low": {
|
||||
"model": "zai-coding-plan/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
},
|
||||
"unspecified-high": {
|
||||
"model": "zai-coding-plan/glm-5.3",
|
||||
"reasoning": "high"
|
||||
},
|
||||
"writing": {
|
||||
"model": "zai-coding-plan/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
}
|
||||
},
|
||||
"sisyphus_agent": {
|
||||
"planner_enabled": true,
|
||||
"replace_plan": true
|
||||
},
|
||||
"telemetry": false,
|
||||
"model_fallback": true,
|
||||
"disabled_hooks": [
|
||||
"auto-update-checker",
|
||||
"startup-toast"
|
||||
]
|
||||
},
|
||||
|
||||
// Native agent and category routing.
|
||||
"[native]": {
|
||||
// Claude leads implementation; GPT independently reviews technical work.
|
||||
@@ -122,11 +19,11 @@
|
||||
"models": [
|
||||
{
|
||||
"model": "anthropic-subscription/claude-opus-5-5",
|
||||
"reasoning": "medium"
|
||||
"reasoning": "xhigh"
|
||||
},
|
||||
{
|
||||
"model": "chatgpt-subscription/gpt-6.1-sol",
|
||||
"reasoning": "medium"
|
||||
"reasoning": "high"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -160,11 +57,11 @@
|
||||
"models": [
|
||||
{
|
||||
"model": "anthropic-subscription/claude-sonnet-5-5",
|
||||
"reasoning": "medium"
|
||||
"reasoning": "high"
|
||||
},
|
||||
{
|
||||
"model": "chatgpt-subscription/gpt-6.1-sol",
|
||||
"reasoning": "medium"
|
||||
"reasoning": "high"
|
||||
},
|
||||
{
|
||||
"model": "zai/glm-5.3",
|
||||
@@ -179,11 +76,11 @@
|
||||
"quick": {
|
||||
"models": [
|
||||
{
|
||||
"model": "anthropic-subscription/claude-sonnet-5-5",
|
||||
"model": "zai/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
},
|
||||
{
|
||||
"model": "zai/glm-5.3-flash",
|
||||
"model": "anthropic-subscription/claude-sonnet-5-5",
|
||||
"reasoning": "low"
|
||||
}
|
||||
]
|
||||
@@ -204,7 +101,7 @@
|
||||
},
|
||||
{
|
||||
"model": "zai/glm-5.3",
|
||||
"reasoning": "medium"
|
||||
"reasoning": "high"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -221,19 +118,35 @@
|
||||
},
|
||||
{
|
||||
"model": "zai/glm-5.3",
|
||||
"reasoning": "medium"
|
||||
"reasoning": "high"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"agents": {
|
||||
"explore": {
|
||||
"model": "zai/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
"models": [
|
||||
{
|
||||
"model": "zai/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
},
|
||||
{
|
||||
"model": "anthropic-subscription/claude-sonnet-5-5",
|
||||
"reasoning": "low"
|
||||
}
|
||||
]
|
||||
},
|
||||
"librarian": {
|
||||
"model": "zai/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
"models": [
|
||||
{
|
||||
"model": "zai/glm-5.3-flash",
|
||||
"reasoning": "low"
|
||||
},
|
||||
{
|
||||
"model": "anthropic-subscription/claude-sonnet-5-5",
|
||||
"reasoning": "low"
|
||||
}
|
||||
]
|
||||
},
|
||||
"plan-consultant": {
|
||||
"models": [
|
||||
|
||||
@@ -0,0 +1,452 @@
|
||||
keybinds clear-defaults=true {
|
||||
locked {
|
||||
bind "Ctrl g" { SwitchToMode "normal"; }
|
||||
}
|
||||
pane {
|
||||
bind "left" { MoveFocus "left"; }
|
||||
bind "down" { MoveFocus "down"; }
|
||||
bind "up" { MoveFocus "up"; }
|
||||
bind "right" { MoveFocus "right"; }
|
||||
bind "c" { SwitchToMode "renamepane"; PaneNameInput 0; }
|
||||
bind "d" { NewPane "down"; SwitchToMode "normal"; }
|
||||
bind "e" { TogglePaneEmbedOrFloating; SwitchToMode "normal"; }
|
||||
bind "f" { ToggleFocusFullscreen; SwitchToMode "normal"; }
|
||||
bind "h" { MoveFocus "left"; }
|
||||
bind "i" { TogglePanePinned; SwitchToMode "normal"; }
|
||||
bind "j" { MoveFocus "down"; }
|
||||
bind "k" { MoveFocus "up"; }
|
||||
bind "l" { MoveFocus "right"; }
|
||||
bind "n" { NewPane; SwitchToMode "normal"; }
|
||||
bind "p" { SwitchFocus; }
|
||||
bind "Ctrl p" { SwitchToMode "normal"; }
|
||||
bind "r" { NewPane "right"; SwitchToMode "normal"; }
|
||||
bind "w" { ToggleFloatingPanes; SwitchToMode "normal"; }
|
||||
bind "z" { TogglePaneFrames; SwitchToMode "normal"; }
|
||||
}
|
||||
tab {
|
||||
bind "left" { GoToPreviousTab; }
|
||||
bind "down" { GoToNextTab; }
|
||||
bind "up" { GoToPreviousTab; }
|
||||
bind "right" { GoToNextTab; }
|
||||
bind "1" { GoToTab 1; SwitchToMode "normal"; }
|
||||
bind "2" { GoToTab 2; SwitchToMode "normal"; }
|
||||
bind "3" { GoToTab 3; SwitchToMode "normal"; }
|
||||
bind "4" { GoToTab 4; SwitchToMode "normal"; }
|
||||
bind "5" { GoToTab 5; SwitchToMode "normal"; }
|
||||
bind "6" { GoToTab 6; SwitchToMode "normal"; }
|
||||
bind "7" { GoToTab 7; SwitchToMode "normal"; }
|
||||
bind "8" { GoToTab 8; SwitchToMode "normal"; }
|
||||
bind "9" { GoToTab 9; SwitchToMode "normal"; }
|
||||
bind "[" { BreakPaneLeft; SwitchToMode "normal"; }
|
||||
bind "]" { BreakPaneRight; SwitchToMode "normal"; }
|
||||
bind "b" { BreakPane; SwitchToMode "normal"; }
|
||||
bind "h" { GoToPreviousTab; }
|
||||
bind "j" { GoToNextTab; }
|
||||
bind "k" { GoToPreviousTab; }
|
||||
bind "l" { GoToNextTab; }
|
||||
bind "n" { NewTab; SwitchToMode "normal"; }
|
||||
bind "r" { SwitchToMode "renametab"; TabNameInput 0; }
|
||||
bind "s" { ToggleActiveSyncTab; SwitchToMode "normal"; }
|
||||
bind "Ctrl t" { SwitchToMode "normal"; }
|
||||
bind "x" { CloseTab; SwitchToMode "normal"; }
|
||||
bind "tab" { ToggleTab; }
|
||||
}
|
||||
resize {
|
||||
bind "left" { Resize "Increase left"; }
|
||||
bind "down" { Resize "Increase down"; }
|
||||
bind "up" { Resize "Increase up"; }
|
||||
bind "right" { Resize "Increase right"; }
|
||||
bind "+" { Resize "Increase"; }
|
||||
bind "-" { Resize "Decrease"; }
|
||||
bind "=" { Resize "Increase"; }
|
||||
bind "H" { Resize "Decrease left"; }
|
||||
bind "J" { Resize "Decrease down"; }
|
||||
bind "K" { Resize "Decrease up"; }
|
||||
bind "L" { Resize "Decrease right"; }
|
||||
bind "h" { Resize "Increase left"; }
|
||||
bind "j" { Resize "Increase down"; }
|
||||
bind "k" { Resize "Increase up"; }
|
||||
bind "l" { Resize "Increase right"; }
|
||||
bind "Ctrl n" { SwitchToMode "normal"; }
|
||||
}
|
||||
move {
|
||||
bind "left" { MovePane "left"; }
|
||||
bind "down" { MovePane "down"; }
|
||||
bind "up" { MovePane "up"; }
|
||||
bind "right" { MovePane "right"; }
|
||||
bind "h" { MovePane "left"; }
|
||||
bind "Ctrl h" { SwitchToMode "normal"; }
|
||||
bind "j" { MovePane "down"; }
|
||||
bind "k" { MovePane "up"; }
|
||||
bind "l" { MovePane "right"; }
|
||||
bind "n" { MovePane; }
|
||||
bind "p" { MovePaneBackwards; }
|
||||
bind "tab" { MovePane; }
|
||||
}
|
||||
scroll {
|
||||
bind "e" { EditScrollback; SwitchToMode "normal"; }
|
||||
bind "s" { SwitchToMode "entersearch"; SearchInput 0; }
|
||||
}
|
||||
search {
|
||||
bind "c" { SearchToggleOption "CaseSensitivity"; }
|
||||
bind "n" { Search "down"; }
|
||||
bind "o" { SearchToggleOption "WholeWord"; }
|
||||
bind "p" { Search "up"; }
|
||||
bind "w" { SearchToggleOption "Wrap"; }
|
||||
}
|
||||
session {
|
||||
bind "a" {
|
||||
LaunchOrFocusPlugin "zellij:about" {
|
||||
floating true
|
||||
move_to_focused_tab true
|
||||
}
|
||||
SwitchToMode "normal"
|
||||
}
|
||||
bind "c" {
|
||||
LaunchOrFocusPlugin "configuration" {
|
||||
floating true
|
||||
move_to_focused_tab true
|
||||
}
|
||||
SwitchToMode "normal"
|
||||
}
|
||||
bind "Ctrl o" { SwitchToMode "normal"; }
|
||||
bind "p" {
|
||||
LaunchOrFocusPlugin "plugin-manager" {
|
||||
floating true
|
||||
move_to_focused_tab true
|
||||
}
|
||||
SwitchToMode "normal"
|
||||
}
|
||||
bind "w" {
|
||||
LaunchOrFocusPlugin "session-manager" {
|
||||
floating true
|
||||
move_to_focused_tab true
|
||||
}
|
||||
SwitchToMode "normal"
|
||||
}
|
||||
}
|
||||
shared_except "locked" {
|
||||
bind "Alt left" { MoveFocusOrTab "left"; }
|
||||
bind "Alt down" { MoveFocus "down"; }
|
||||
bind "Alt up" { MoveFocus "up"; }
|
||||
bind "Alt right" { MoveFocusOrTab "right"; }
|
||||
bind "Alt +" { Resize "Increase"; }
|
||||
bind "Alt -" { Resize "Decrease"; }
|
||||
bind "Alt =" { Resize "Increase"; }
|
||||
bind "Alt [" { PreviousSwapLayout; }
|
||||
bind "Alt ]" { NextSwapLayout; }
|
||||
bind "Alt f" { ToggleFloatingPanes; }
|
||||
bind "Ctrl g" { SwitchToMode "locked"; }
|
||||
bind "Alt h" { MoveFocusOrTab "left"; }
|
||||
bind "Alt i" { MoveTab "left"; }
|
||||
bind "Alt j" { MoveFocus "down"; }
|
||||
bind "Alt k" { MoveFocus "up"; }
|
||||
bind "Alt l" { MoveFocusOrTab "right"; }
|
||||
bind "Alt n" { NewPane; }
|
||||
bind "Alt o" { MoveTab "right"; }
|
||||
bind "Ctrl q" { Quit; }
|
||||
}
|
||||
shared_except "locked" "move" {
|
||||
bind "Ctrl h" { SwitchToMode "move"; }
|
||||
}
|
||||
shared_except "locked" "session" {
|
||||
bind "Ctrl o" { SwitchToMode "session"; }
|
||||
}
|
||||
shared_except "locked" "scroll" "search" "tmux" {
|
||||
bind "Ctrl b" { SwitchToMode "tmux"; }
|
||||
}
|
||||
shared_except "locked" "scroll" "search" {
|
||||
bind "Ctrl s" { SwitchToMode "scroll"; }
|
||||
}
|
||||
shared_except "locked" "tab" {
|
||||
bind "Ctrl t" { SwitchToMode "tab"; }
|
||||
}
|
||||
shared_except "locked" "pane" {
|
||||
bind "Ctrl p" { SwitchToMode "pane"; }
|
||||
}
|
||||
shared_except "locked" "resize" {
|
||||
bind "Ctrl n" { SwitchToMode "resize"; }
|
||||
}
|
||||
shared_except "normal" "locked" "entersearch" {
|
||||
bind "enter" { SwitchToMode "normal"; }
|
||||
}
|
||||
shared_except "normal" "locked" "entersearch" "renametab" "renamepane" {
|
||||
bind "esc" { SwitchToMode "normal"; }
|
||||
}
|
||||
shared_among "pane" "tmux" {
|
||||
bind "x" { CloseFocus; SwitchToMode "normal"; }
|
||||
}
|
||||
shared_among "scroll" "search" {
|
||||
bind "PageDown" { PageScrollDown; }
|
||||
bind "PageUp" { PageScrollUp; }
|
||||
bind "left" { PageScrollUp; }
|
||||
bind "down" { ScrollDown; }
|
||||
bind "up" { ScrollUp; }
|
||||
bind "right" { PageScrollDown; }
|
||||
bind "Ctrl b" { PageScrollUp; }
|
||||
bind "Ctrl c" { ScrollToBottom; SwitchToMode "normal"; }
|
||||
bind "d" { HalfPageScrollDown; }
|
||||
bind "Ctrl f" { PageScrollDown; }
|
||||
bind "h" { PageScrollUp; }
|
||||
bind "j" { ScrollDown; }
|
||||
bind "k" { ScrollUp; }
|
||||
bind "l" { PageScrollDown; }
|
||||
bind "Ctrl s" { SwitchToMode "normal"; }
|
||||
bind "u" { HalfPageScrollUp; }
|
||||
}
|
||||
entersearch {
|
||||
bind "Ctrl c" { SwitchToMode "scroll"; }
|
||||
bind "esc" { SwitchToMode "scroll"; }
|
||||
bind "enter" { SwitchToMode "search"; }
|
||||
}
|
||||
renametab {
|
||||
bind "esc" { UndoRenameTab; SwitchToMode "tab"; }
|
||||
}
|
||||
shared_among "renametab" "renamepane" {
|
||||
bind "Ctrl c" { SwitchToMode "normal"; }
|
||||
}
|
||||
renamepane {
|
||||
bind "esc" { UndoRenamePane; SwitchToMode "pane"; }
|
||||
}
|
||||
shared_among "session" "tmux" {
|
||||
bind "d" { Detach; }
|
||||
}
|
||||
tmux {
|
||||
bind "left" { MoveFocus "left"; SwitchToMode "normal"; }
|
||||
bind "down" { MoveFocus "down"; SwitchToMode "normal"; }
|
||||
bind "up" { MoveFocus "up"; SwitchToMode "normal"; }
|
||||
bind "right" { MoveFocus "right"; SwitchToMode "normal"; }
|
||||
bind "space" { NextSwapLayout; }
|
||||
bind "\"" { NewPane "down"; SwitchToMode "normal"; }
|
||||
bind "%" { NewPane "right"; SwitchToMode "normal"; }
|
||||
bind "," { SwitchToMode "renametab"; }
|
||||
bind "[" { SwitchToMode "scroll"; }
|
||||
bind "Ctrl b" { Write 2; SwitchToMode "normal"; }
|
||||
bind "c" { NewTab; SwitchToMode "normal"; }
|
||||
bind "h" { MoveFocus "left"; SwitchToMode "normal"; }
|
||||
bind "j" { MoveFocus "down"; SwitchToMode "normal"; }
|
||||
bind "k" { MoveFocus "up"; SwitchToMode "normal"; }
|
||||
bind "l" { MoveFocus "right"; SwitchToMode "normal"; }
|
||||
bind "n" { GoToNextTab; SwitchToMode "normal"; }
|
||||
bind "o" { FocusNextPane; }
|
||||
bind "p" { GoToPreviousTab; SwitchToMode "normal"; }
|
||||
bind "z" { ToggleFocusFullscreen; SwitchToMode "normal"; }
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin aliases - can be used to change the implementation of Zellij
|
||||
// changing these requires a restart to take effect
|
||||
plugins {
|
||||
about location="zellij:about"
|
||||
compact-bar location="zellij:compact-bar"
|
||||
configuration location="zellij:configuration"
|
||||
filepicker location="zellij:strider" {
|
||||
cwd "/"
|
||||
}
|
||||
plugin-manager location="zellij:plugin-manager"
|
||||
session-manager location="zellij:session-manager"
|
||||
status-bar location="zellij:status-bar"
|
||||
strider location="zellij:strider"
|
||||
tab-bar location="zellij:tab-bar"
|
||||
welcome-screen location="zellij:session-manager" {
|
||||
welcome_screen true
|
||||
}
|
||||
}
|
||||
|
||||
// Plugins to load in the background when a new session starts
|
||||
// eg. "file:/path/to/my-plugin.wasm"
|
||||
// eg. "https://example.com/my-plugin.wasm"
|
||||
load_plugins {
|
||||
}
|
||||
|
||||
// Use a simplified UI without special fonts (arrow glyphs)
|
||||
// Options:
|
||||
// - true
|
||||
// - false (Default)
|
||||
//
|
||||
// simplified_ui true
|
||||
|
||||
// Choose the theme that is specified in the themes section.
|
||||
// Default: default
|
||||
//
|
||||
// theme "dracula"
|
||||
|
||||
// Choose the base input mode of zellij.
|
||||
// Default: normal
|
||||
//
|
||||
// default_mode "locked"
|
||||
|
||||
// Choose the path to the default shell that zellij will use for opening new panes
|
||||
// Default: $SHELL
|
||||
//
|
||||
// default_shell "fish"
|
||||
|
||||
// Choose the path to override cwd that zellij will use for opening new panes
|
||||
//
|
||||
// default_cwd "/tmp"
|
||||
|
||||
// The name of the default layout to load on startup
|
||||
// Default: "default"
|
||||
//
|
||||
// default_layout "compact"
|
||||
|
||||
// The folder in which Zellij will look for layouts
|
||||
// (Requires restart)
|
||||
//
|
||||
// layout_dir "/tmp"
|
||||
|
||||
// The folder in which Zellij will look for themes
|
||||
// (Requires restart)
|
||||
//
|
||||
// theme_dir "/tmp"
|
||||
|
||||
// Toggle enabling the mouse mode.
|
||||
// On certain configurations, or terminals this could
|
||||
// potentially interfere with copying text.
|
||||
// Options:
|
||||
// - true (default)
|
||||
// - false
|
||||
//
|
||||
// mouse_mode false
|
||||
|
||||
// Toggle having pane frames around the panes
|
||||
// Options:
|
||||
// - true (default, enabled)
|
||||
// - false
|
||||
//
|
||||
// pane_frames false
|
||||
|
||||
// When attaching to an existing session with other users,
|
||||
// should the session be mirrored (true)
|
||||
// or should each user have their own cursor (false)
|
||||
// (Requires restart)
|
||||
// Default: false
|
||||
//
|
||||
// mirror_session true
|
||||
|
||||
// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP
|
||||
// eg. when terminal window with an active zellij session is closed
|
||||
// (Requires restart)
|
||||
// Options:
|
||||
// - detach (Default)
|
||||
// - quit
|
||||
//
|
||||
// on_force_close "quit"
|
||||
|
||||
// Configure the scroll back buffer size
|
||||
// This is the number of lines zellij stores for each pane in the scroll back
|
||||
// buffer. Excess number of lines are discarded in a FIFO fashion.
|
||||
// (Requires restart)
|
||||
// Valid values: positive integers
|
||||
// Default value: 10000
|
||||
//
|
||||
// scroll_buffer_size 10000
|
||||
|
||||
// Provide a command to execute when copying text. The text will be piped to
|
||||
// the stdin of the program to perform the copy. This can be used with
|
||||
// terminal emulators which do not support the OSC 52 ANSI control sequence
|
||||
// that will be used by default if this option is not set.
|
||||
// Examples:
|
||||
//
|
||||
// copy_command "xclip -selection clipboard" // x11
|
||||
// copy_command "wl-copy" // wayland
|
||||
// copy_command "pbcopy" // osx
|
||||
//
|
||||
// copy_command "pbcopy"
|
||||
|
||||
// Choose the destination for copied text
|
||||
// Allows using the primary selection buffer (on x11/wayland) instead of the system clipboard.
|
||||
// Does not apply when using copy_command.
|
||||
// Options:
|
||||
// - system (default)
|
||||
// - primary
|
||||
//
|
||||
// copy_clipboard "primary"
|
||||
|
||||
// Enable automatic copying (and clearing) of selection when releasing mouse
|
||||
// Default: true
|
||||
//
|
||||
// copy_on_select true
|
||||
|
||||
// Path to the default editor to use to edit pane scrollbuffer
|
||||
// Default: $EDITOR or $VISUAL
|
||||
// scrollback_editor "/usr/bin/vim"
|
||||
|
||||
// A fixed name to always give the Zellij session.
|
||||
// Consider also setting `attach_to_session true,`
|
||||
// otherwise this will error if such a session exists.
|
||||
// Default: <RANDOM>
|
||||
//
|
||||
// session_name "My singleton session"
|
||||
|
||||
// When `session_name` is provided, attaches to that session
|
||||
// if it is already running or creates it otherwise.
|
||||
// Default: false
|
||||
//
|
||||
// attach_to_session true
|
||||
|
||||
// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible
|
||||
// Options:
|
||||
// - true (default)
|
||||
// - false
|
||||
//
|
||||
// auto_layout false
|
||||
|
||||
// Whether sessions should be serialized to the cache folder (including their tabs/panes, cwds and running commands) so that they can later be resurrected
|
||||
// Options:
|
||||
// - true (default)
|
||||
// - false
|
||||
//
|
||||
// session_serialization false
|
||||
|
||||
// Whether pane viewports are serialized along with the session, default is false
|
||||
// Options:
|
||||
// - true
|
||||
// - false (default)
|
||||
//
|
||||
// serialize_pane_viewport false
|
||||
|
||||
// Scrollback lines to serialize along with the pane viewport when serializing sessions, 0
|
||||
// defaults to the scrollback size. If this number is higher than the scrollback size, it will
|
||||
// also default to the scrollback size. This does nothing if `serialize_pane_viewport` is not true.
|
||||
//
|
||||
// scrollback_lines_to_serialize 10000
|
||||
|
||||
// Enable or disable the rendering of styled and colored underlines (undercurl).
|
||||
// May need to be disabled for certain unsupported terminals
|
||||
// (Requires restart)
|
||||
// Default: true
|
||||
//
|
||||
// styled_underlines false
|
||||
|
||||
// How often in seconds sessions are serialized
|
||||
//
|
||||
// serialization_interval 10000
|
||||
|
||||
// Enable or disable writing of session metadata to disk (if disabled, other sessions might not know
|
||||
// metadata info on this session)
|
||||
// (Requires restart)
|
||||
// Default: false
|
||||
//
|
||||
// disable_session_metadata false
|
||||
|
||||
// Enable or disable support for the enhanced Kitty Keyboard Protocol (the host terminal must also support it)
|
||||
// (Requires restart)
|
||||
// Default: true (if the host terminal supports it)
|
||||
//
|
||||
// support_kitty_keyboard_protocol false
|
||||
|
||||
// Whether to stack panes when resizing beyond a certain size
|
||||
// Default: true
|
||||
//
|
||||
// stacked_resize false
|
||||
|
||||
// Whether to show tips on startup
|
||||
// Default: true
|
||||
//
|
||||
// show_startup_tips false
|
||||
|
||||
// Whether to show release notes on first version run
|
||||
// Default: true
|
||||
//
|
||||
// show_release_notes false
|
||||
+68
-26
@@ -1,6 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# macOS notification for omo (senpi) Stop and Notification hooks.
|
||||
# Reads the hook JSON payload on stdin. Wired up in ~/.omo/agent/hooks.json.
|
||||
# Ding, tab marker and notification for omo (senpi) hooks: UserPromptSubmit,
|
||||
# Stop and Notification. Reads the hook JSON payload on stdin. Wired up in
|
||||
# ~/.omo/agent/hooks.json. The sound plays through afplay, so Focus modes and
|
||||
# notification permissions cannot mute it; the banner is a silent extra.
|
||||
# Ghostty posts the banner, so clicking it opens the tab it came from, and
|
||||
# omo-status then selects the session's pane. omo-status also colours that tab:
|
||||
# blue working, amber waiting for you, green finished, red error.
|
||||
|
||||
# Hooks run with a minimal environment, so make Homebrew tools reachable.
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
|
||||
@@ -11,42 +16,79 @@ field() { jq -r "$1 // empty" <<<"$payload" 2>/dev/null; }
|
||||
event=$(field '.hook_event_name')
|
||||
project=$(basename "$(field '.cwd')")
|
||||
|
||||
# Only top-level sessions live under .omo/agent/sessions/. Subagents, team members
|
||||
# and other background sessions keep transcripts elsewhere (e.g. senpi-task/children).
|
||||
transcript=$(field '.transcript_path')
|
||||
[[ -z "$transcript" || "$transcript" == */.omo/agent/sessions/* ]] || exit 0
|
||||
|
||||
message=""
|
||||
sound=""
|
||||
case "$event" in
|
||||
UserPromptSubmit)
|
||||
state=working
|
||||
;;
|
||||
Stop)
|
||||
# Esc/interrupts are your own doing; no need to be told about them.
|
||||
[[ "$(field '.stopReason')" == "aborted" ]] && exit 0
|
||||
# Only top-level sessions live under .omo/agent/sessions/. Subagents, team members
|
||||
# and other background sessions keep transcripts elsewhere (e.g. senpi-task/children).
|
||||
transcript=$(field '.transcript_path')
|
||||
[[ -z "$transcript" || "$transcript" == */.omo/agent/sessions/* ]] || exit 0
|
||||
if [[ "$(field '.stopReason')" == "error" ]]; then
|
||||
message="Stopped with an error"
|
||||
else
|
||||
message="Task finished"
|
||||
fi
|
||||
sound="Glass"
|
||||
case "$(field '.stopReason')" in
|
||||
# Esc/interrupts are your own doing; no need to be told about them.
|
||||
aborted) state="clear" ;;
|
||||
error) state="error" message="Stopped with an error" sound="Glass" ;;
|
||||
*) state="done" message="Task finished" sound="Glass" ;;
|
||||
esac
|
||||
;;
|
||||
Notification)
|
||||
# Only alert when a question appears, not when it is answered or times out.
|
||||
[[ "$(field '.kind')" == "ask-user-asked" ]] || exit 0
|
||||
message="Needs your input: $(field '.title')"
|
||||
sound="Ping"
|
||||
if [[ "$(field '.kind')" == "ask-user-asked" ]]; then
|
||||
state=input message="Needs your input: $(field '.title')" sound="Ping"
|
||||
else
|
||||
# Answered or timed out: the agent carries on.
|
||||
state=working
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
# Background the ding so the hook returns immediately; redirect output so the
|
||||
# hook runner is not left waiting on afplay's inherited pipes.
|
||||
[[ -n "$sound" ]] && afplay "/System/Library/Sounds/${sound}.aiff" >/dev/null 2>&1 &
|
||||
|
||||
# Find the tmux pane omo runs in: hooks run detached without TMUX_PANE, so walk
|
||||
# up the process tree to the first ancestor whose tty is a tmux pane.
|
||||
# Outside tmux, the first tty found is the Ghostty tab's own terminal.
|
||||
pane=""
|
||||
own_tty=""
|
||||
panes=$(tmux list-panes -a -F '#{pane_tty} #{pane_id}' 2>/dev/null)
|
||||
pid=$PPID
|
||||
while [[ -z "$pane" && "${pid:-1}" -gt 1 ]]; do
|
||||
tty=$(ps -o tty= -p "$pid" | tr -d ' ')
|
||||
if [[ -n "$tty" && "$tty" != "??" ]]; then
|
||||
[[ -n "$own_tty" ]] || own_tty="/dev/$tty"
|
||||
[[ -n "$panes" ]] && pane=$(awk -v t="/dev/$tty" '$1 == t { print $2 }' <<<"$panes")
|
||||
fi
|
||||
pid=$(ps -o ppid= -p "$pid" | tr -d ' ')
|
||||
done
|
||||
|
||||
[[ -n "$pane" ]] && "$HOME/.local/bin/omo-status" set "$pane" "$state"
|
||||
[[ -n "$message" ]] || exit 0
|
||||
|
||||
log="${OMO_NOTIFY_LOG:-$HOME/.local/state/omo-notify.log}"
|
||||
mkdir -p "$(dirname "$log")"
|
||||
printf '%s %s %s %s [%s %s]\n' "$(date '+%F %T')" "$event" "$project" "$message" \
|
||||
"$(field '.session_id')" "$(field '.transcript_path')" >>"$log"
|
||||
printf '%s %s %s %s [%s %s] pane=%s\n' "$(date '+%F %T')" "$event" "$project" "$message" \
|
||||
"$(field '.session_id')" "$(field '.transcript_path')" "${pane:-none}" >>"$log"
|
||||
|
||||
# Pass values as argv so quotes in titles cannot break the AppleScript.
|
||||
osascript - "omo: ${project:-session}" "$message" "$sound" <<'APPLESCRIPT' >/dev/null 2>&1
|
||||
on run argv
|
||||
display notification (item 2 of argv) with title (item 1 of argv) sound name (item 3 of argv)
|
||||
end run
|
||||
APPLESCRIPT
|
||||
# The banner is an OSC 777 notification for Ghostty. In tmux it goes to the tab's
|
||||
# tmux client terminal, because tmux does not pass it on from a pane, and
|
||||
# @omo_jump tells omo-status which pane to select when that tab gets focus.
|
||||
target=$own_tty
|
||||
if [[ -n "$pane" ]]; then
|
||||
session=$(tmux display-message -p -t "$pane" '#{session_id}')
|
||||
target=$(tmux list-clients -t "$session" -F '#{client_activity} #{client_tty}' | sort -rn | awk 'NR == 1 { print $2 }')
|
||||
tmux set-option -t "$session" @omo_jump "$pane"
|
||||
fi
|
||||
# Control characters would end the escape sequence early; ";" separates its fields.
|
||||
clean() { tr -d '\000-\037\177' <<<"$1" | tr ';' ','; }
|
||||
if [[ -n "$target" && -w "$target" ]]; then
|
||||
printf '\033]777;notify;%s;%s\a' "$(clean "omo: ${project:-session}")" "$(clean "$message")" >"$target"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
|
||||
Executable
+88
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
# Coloured attention markers on Ghostty tabs for omo sessions. tmux puts the
|
||||
# session's @omo_status in front of the tab title (set-titles-string in
|
||||
# ~/.tmux.conf), so each tab shows what its agents need:
|
||||
# 🔴 stopped with an error 🟡 waiting for your answer
|
||||
# 🟢 finished 🔵 working
|
||||
# Each pane running omo keeps its own state in the pane option @omo_state, and
|
||||
# the tab shows the most urgent one. Focusing the tab clears red, amber and green,
|
||||
# and selects the pane whose banner you were sent (@omo_jump, set by omo-notify).
|
||||
#
|
||||
# Usage: omo-status set <pane> <working|input|done|error|clear>
|
||||
# omo-status focus <client> (run by the tmux client-focus-in hook)
|
||||
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
|
||||
|
||||
icon() {
|
||||
case $1 in
|
||||
error) echo "🔴" ;;
|
||||
input) echo "🟡" ;;
|
||||
done) echo "🟢" ;;
|
||||
working) echo "🔵" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
rank() {
|
||||
case $1 in
|
||||
error) echo 4 ;;
|
||||
input) echo 3 ;;
|
||||
done) echo 2 ;;
|
||||
working) echo 1 ;;
|
||||
*) echo 0 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
show_most_urgent() {
|
||||
local session=$1 state best=""
|
||||
while read -r state; do
|
||||
if (($(rank "$state") > $(rank "$best"))); then best=$state; fi
|
||||
done < <(tmux list-panes -s -t "$session" -F '#{@omo_state}')
|
||||
if [[ -n $best ]]; then
|
||||
tmux set-option -t "$session" @omo_status "$(icon "$best")"
|
||||
else
|
||||
tmux set-option -u -t "$session" @omo_status
|
||||
fi
|
||||
}
|
||||
|
||||
# You are looking at a pane when its tab has focus and its window is showing.
|
||||
looking_at() {
|
||||
local pane=$1 session=$2
|
||||
[[ $(tmux display-message -p -t "$pane" '#{window_active}') == 1 ]] &&
|
||||
tmux list-clients -t "$session" -F '#{client_flags}' | grep -q focused
|
||||
}
|
||||
|
||||
case ${1:-} in
|
||||
set)
|
||||
pane=${2:?pane id required} state=${3:?state required}
|
||||
session=$(tmux display-message -p -t "$pane" '#{session_id}' 2>/dev/null) || exit 0
|
||||
if [[ $state != working && $state != clear ]] && looking_at "$pane" "$session"; then
|
||||
state=clear
|
||||
fi
|
||||
if [[ $state == clear ]]; then
|
||||
tmux set-option -p -u -t "$pane" @omo_state
|
||||
else
|
||||
tmux set-option -p -t "$pane" @omo_state "$state"
|
||||
fi
|
||||
show_most_urgent "$session"
|
||||
;;
|
||||
focus)
|
||||
client=${2:?client required}
|
||||
session=$(tmux list-clients -F '#{client_name} #{session_id}' | awk -v c="$client" '$1 == c { print $2 }')
|
||||
[[ -n $session ]] || exit 0
|
||||
jump=$(tmux show-option -qv -t "$session" @omo_jump)
|
||||
if [[ -n $jump ]]; then
|
||||
tmux set-option -u -t "$session" @omo_jump
|
||||
tmux select-window -t "$jump" \; select-pane -t "$jump" 2>/dev/null
|
||||
fi
|
||||
while read -r pane state; do
|
||||
case $state in
|
||||
input | done | error) tmux set-option -p -u -t "$pane" @omo_state ;;
|
||||
esac
|
||||
done < <(tmux list-panes -s -t "$session" -F '#{pane_id} #{@omo_state}')
|
||||
show_most_urgent "$session"
|
||||
;;
|
||||
*)
|
||||
sed -n '2,/^$/s/^# \{0,1\}//p' "$0" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -43,6 +43,12 @@ Usage (run from anywhere inside the repo):
|
||||
each issue to In Progress, and to In Review with the PR link.
|
||||
owt open <name> Open a tmux window running the agent in an existing workspace
|
||||
owt ls List this repo's workspaces and their branches
|
||||
owt all List the workspaces of every repo with their branches and
|
||||
source checkouts (works from any directory)
|
||||
owt cd [name] Jump into a workspace of any repo: <name> or <repo>/<name>,
|
||||
otherwise pick with fzf (filtered to this repo's workspaces
|
||||
when run inside one; jumps straight in if only one matches).
|
||||
Needs the owt shell function in .zshrc; without it, prints the path.
|
||||
owt path <name> Print a workspace path (cd "$(owt path lagos)")
|
||||
owt rm <name> [--force]
|
||||
Remove the workspace and delete its branch if merged.
|
||||
@@ -291,6 +297,73 @@ cmd_path() {
|
||||
printf '%s\n' "$dir"
|
||||
}
|
||||
|
||||
# One "<repo>/<name>\t<branch>\t<source checkout>" line per workspace under
|
||||
# $OWT_ROOT, across every repo. Works outside a repository.
|
||||
all_workspaces() {
|
||||
local dir common branch src
|
||||
for dir in "$OWT_ROOT"/*/*/; do
|
||||
dir="${dir%/}"
|
||||
[[ -f "$dir/.git" ]] || continue
|
||||
if common="$(git -C "$dir" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"; then
|
||||
branch="$(git -C "$dir" symbolic-ref --quiet --short HEAD || printf '(detached)')"
|
||||
src="$(dirname "$common")"
|
||||
if [[ "$src" == "$HOME"/* ]]; then src='~'/"${src#"$HOME"/}"; fi
|
||||
else
|
||||
branch="?"
|
||||
src="(source checkout missing)"
|
||||
fi
|
||||
printf '%s\t%s\t%s\n' "${dir#"$OWT_ROOT"/}" "$branch" "$src"
|
||||
done
|
||||
}
|
||||
|
||||
cmd_all() {
|
||||
local rows
|
||||
rows="$(all_workspaces)"
|
||||
if [[ -z "$rows" ]]; then
|
||||
say "no workspaces under $OWT_ROOT"
|
||||
return 0
|
||||
fi
|
||||
{ printf 'WORKSPACE\tBRANCH\tSOURCE\n'; printf '%s\n' "$rows"; } | column -t -s $'\t'
|
||||
}
|
||||
|
||||
# Print a workspace path for the owt shell function in .zshrc to cd into.
|
||||
cmd_cd() {
|
||||
[[ $# -le 1 ]] || die "usage: owt cd [name]"
|
||||
local query="${1:-}" rows repo="" common picked=""
|
||||
rows="$(all_workspaces)"
|
||||
[[ -n "$rows" ]] || die "no workspaces under $OWT_ROOT"
|
||||
if common="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)"; then
|
||||
repo="$(basename "$(dirname "$common")")"
|
||||
fi
|
||||
|
||||
# Exact <repo>/<name>, then <name> in this repo, then <name> in exactly one repo.
|
||||
if [[ -n "$query" ]]; then
|
||||
picked="$(printf '%s\n' "$rows" | awk -F'\t' -v q="$query" -v r="$repo" '
|
||||
{ ws = $1; name = substr(ws, index(ws, "/") + 1) }
|
||||
ws == q || ws == r "/" q { print ws; found = 1; exit }
|
||||
name == q { hits[++n] = ws }
|
||||
END { if (!found && n == 1) print hits[1] }')"
|
||||
fi
|
||||
|
||||
if [[ -z "$picked" ]]; then
|
||||
command -v fzf >/dev/null || die "no workspace named '$query'; install fzf to pick one, or see 'owt all'"
|
||||
if [[ -z "$query" && -n "$repo" && $'\n'"$rows" == *$'\n'"$repo/"* ]]; then query="$repo/"; fi
|
||||
picked="$(paste <(cut -f1 <<<"$rows") <(column -t -s $'\t' <<<"$rows") \
|
||||
| fzf --delimiter=$'\t' --with-nth=2.. --query="$query" --select-1 --exit-0 \
|
||||
--height=40% --reverse --prompt='owt cd> ' \
|
||||
| cut -f1)" || true
|
||||
if [[ -z "$picked" ]]; then
|
||||
[[ -z "$query" ]] || die "no workspace matches '$query'"
|
||||
die "no workspace selected"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
say "printing the path only; the owt shell function in .zshrc does the cd"
|
||||
fi
|
||||
printf '%s/%s\n' "$OWT_ROOT" "$picked"
|
||||
}
|
||||
|
||||
cmd_rm() {
|
||||
local name="" force=0
|
||||
while [[ $# -gt 0 ]]; do
|
||||
@@ -432,6 +505,8 @@ case "${1:-}" in
|
||||
done) shift; cmd_done "$@" ;;
|
||||
open) shift; cmd_open "$@" ;;
|
||||
ls) shift; cmd_ls ;;
|
||||
all) shift; cmd_all ;;
|
||||
cd) shift; cmd_cd "$@" ;;
|
||||
path) shift; cmd_path "$@" ;;
|
||||
rm) shift; cmd_rm "$@" ;;
|
||||
""|-h|--help|help) usage ;;
|
||||
|
||||
+8
-1
@@ -1,6 +1,10 @@
|
||||
set -g mouse on
|
||||
bind r source-file ~/.tmux.conf
|
||||
set-option -a terminal-features 'xterm*:RGB'
|
||||
# Extended keys so TUIs (omo, nvim) receive Shift+Enter and other modified keys
|
||||
set -g extended-keys on
|
||||
set -g extended-keys-format csi-u
|
||||
set-option -a terminal-features 'xterm*:extkeys'
|
||||
set-option -sg escape-time 10
|
||||
set-option -g focus-events on
|
||||
set-window-option -g mode-keys vi
|
||||
@@ -60,7 +64,10 @@ bind -r m resize-pane -Z
|
||||
|
||||
# Push tmux window/session info to the terminal title
|
||||
set -g set-titles on
|
||||
set -g set-titles-string "#{session_name}"
|
||||
# omo-status puts a coloured marker (@omo_status) in front of the session name;
|
||||
# focusing the tab clears the attention markers.
|
||||
set -g set-titles-string "#{?@omo_status,#{@omo_status} ,}#{session_name}"
|
||||
set-hook -g client-focus-in 'run-shell -b "~/.local/bin/omo-status focus #{q:hook_client}"'
|
||||
|
||||
# Set plugin manager path before loading plugins
|
||||
set-environment -g TMUX_PLUGIN_MANAGER_PATH "$HOME/.tmux/plugins"
|
||||
|
||||
@@ -322,7 +322,7 @@ if [ -d "$HOME/neovim/bin" ]; then
|
||||
export PATH="$HOME/neovim/bin:$PATH"
|
||||
fi
|
||||
|
||||
. "$HOME/.deno/env"
|
||||
[ -f "$HOME/.deno/env" ] && . "$HOME/.deno/env"
|
||||
|
||||
export NVM_DIR="$HOME/.config/nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
|
||||
@@ -357,6 +357,17 @@ sshm() { TERM=xterm-256color command sshm "$@"; }
|
||||
|
||||
source <(sshm completion zsh)
|
||||
|
||||
# `owt cd` jumps into an owt workspace. A script cannot change this shell's
|
||||
# directory, so owt prints the path and this wrapper does the cd.
|
||||
owt() {
|
||||
if [[ "$1" == cd ]]; then
|
||||
local dir
|
||||
dir="$(command owt "$@")" && cd "$dir"
|
||||
else
|
||||
command owt "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
export SSH_AUTH_SOCK=~/Library/Group\ Containers/2BUA8C4S2C.com.1password/t/agent.sock
|
||||
|
||||
# Connect to flowstate-remote over mosh instead of ssh. Mosh is UDP with no TCP
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
# Command-line tools, fonts and terminals. Installed by `./setup-mac.sh brew`.
|
||||
# GUI apps and App Store apps live in Brewfile.apps.
|
||||
# To see what is installed but missing here: brew bundle dump --file=- | diff Brewfile -
|
||||
|
||||
tap "bufbuild/buf"
|
||||
tap "gu1llaum-3/sshm"
|
||||
tap "hashicorp/tap"
|
||||
tap "leoafarias/fvm"
|
||||
tap "mongodb/brew"
|
||||
tap "oven-sh/bun"
|
||||
tap "pbkit/tap"
|
||||
tap "reoclo/tap"
|
||||
tap "teamookla/speedtest"
|
||||
|
||||
# Shell, prompt and terminal tools (.zshrc and .tmux.conf rely on these)
|
||||
brew "zsh-syntax-highlighting"
|
||||
brew "starship"
|
||||
brew "zoxide"
|
||||
brew "direnv"
|
||||
brew "fzf"
|
||||
brew "eza"
|
||||
brew "bat"
|
||||
brew "grc"
|
||||
brew "tmux"
|
||||
brew "zellij"
|
||||
brew "yazi"
|
||||
brew "mosh"
|
||||
brew "openssh"
|
||||
brew "gu1llaum-3/sshm/sshm", trusted: true
|
||||
|
||||
# Everyday CLI
|
||||
brew "coreutils"
|
||||
brew "curl"
|
||||
brew "wget"
|
||||
brew "jq"
|
||||
brew "ripgrep"
|
||||
brew "fd"
|
||||
brew "tree"
|
||||
brew "watch"
|
||||
brew "htop"
|
||||
brew "dust"
|
||||
brew "telnet"
|
||||
brew "stow"
|
||||
brew "sevenzip"
|
||||
brew "gnupg"
|
||||
brew "mas" # App Store installs in Brewfile.apps
|
||||
|
||||
# Git
|
||||
brew "gh"
|
||||
brew "tea"
|
||||
brew "lazygit"
|
||||
brew "git-filter-repo"
|
||||
brew "gitleaks"
|
||||
brew "colordiff"
|
||||
brew "diffstat"
|
||||
|
||||
# Editors and code tools
|
||||
brew "neovim"
|
||||
brew "luarocks"
|
||||
brew "ast-grep"
|
||||
brew "silicon"
|
||||
brew "just"
|
||||
brew "bufbuild/buf/buf", trusted: true
|
||||
brew "pbkit/tap/pbkit", trusted: true
|
||||
|
||||
# Languages and version managers
|
||||
brew "asdf"
|
||||
brew "pyenv"
|
||||
brew "pyenv-virtualenv"
|
||||
brew "python@3.11"
|
||||
brew "python@3.12"
|
||||
brew "ruby", link: false
|
||||
brew "go"
|
||||
brew "deno"
|
||||
brew "oven-sh/bun/bun", trusted: true
|
||||
brew "pnpm"
|
||||
brew "uv"
|
||||
brew "leoafarias/fvm/fvm", trusted: true
|
||||
|
||||
# Python tooling
|
||||
brew "black"
|
||||
brew "ruff"
|
||||
brew "mypy"
|
||||
brew "ipython"
|
||||
brew "python-lsp-server"
|
||||
brew "cookiecutter"
|
||||
|
||||
# Build toolchain and native libraries
|
||||
brew "cmake"
|
||||
brew "gcc"
|
||||
brew "lld"
|
||||
brew "libffi"
|
||||
brew "libraw"
|
||||
brew "librist"
|
||||
brew "libssh"
|
||||
brew "grpc"
|
||||
brew "portaudio"
|
||||
brew "tcl-tk"
|
||||
brew "cryptography"
|
||||
brew "python-setuptools"
|
||||
|
||||
# Containers, CI, cloud and deploy
|
||||
brew "lazydocker"
|
||||
brew "dive"
|
||||
brew "hadolint"
|
||||
brew "trivy"
|
||||
brew "nixpacks"
|
||||
brew "act"
|
||||
brew "actionlint"
|
||||
brew "woodpecker-cli"
|
||||
brew "awscli"
|
||||
brew "doctl"
|
||||
brew "cloudflared"
|
||||
brew "vercel"
|
||||
brew "pscale"
|
||||
brew "hashicorp/tap/terraform", trusted: true
|
||||
brew "bitwarden-cli"
|
||||
brew "reoclo/tap/reoclo", trusted: true
|
||||
|
||||
# Databases, servers and API clients
|
||||
brew "mongosh"
|
||||
brew "mongodb/brew/mongodb-community", trusted: true
|
||||
brew "mysql"
|
||||
brew "mysql-client@8.4"
|
||||
brew "redis"
|
||||
brew "httpd"
|
||||
brew "posting"
|
||||
|
||||
# Media, documents and images
|
||||
brew "ffmpeg"
|
||||
brew "mpv"
|
||||
brew "imagemagick"
|
||||
brew "graphicsmagick"
|
||||
brew "tesseract"
|
||||
brew "ghostscript"
|
||||
brew "poppler"
|
||||
brew "weasyprint"
|
||||
brew "pngquant"
|
||||
brew "potrace"
|
||||
brew "chafa"
|
||||
brew "folderify"
|
||||
|
||||
# Apple and mobile builds
|
||||
brew "xcodegen"
|
||||
brew "xcbeautify"
|
||||
brew "create-dmg"
|
||||
|
||||
# Hardware, installers and disk images
|
||||
brew "arduino-cli"
|
||||
brew "makensis"
|
||||
brew "wimlib"
|
||||
brew "cabextract"
|
||||
brew "cdrtools"
|
||||
|
||||
# Networking
|
||||
brew "tor"
|
||||
brew "teamookla/speedtest/speedtest", trusted: true
|
||||
|
||||
# Fonts, terminals and CLI apps
|
||||
cask "font-fira-code-nerd-font"
|
||||
cask "font-hack-nerd-font"
|
||||
cask "font-symbols-only-nerd-font"
|
||||
cask "ghostty"
|
||||
cask "wezterm"
|
||||
cask "1password-cli"
|
||||
cask "git-credential-manager"
|
||||
cask "ngrok"
|
||||
|
||||
# Go and uv tools
|
||||
go "golang.org/x/tools/gopls"
|
||||
uv "headroom-ai"
|
||||
uv "vectorcode"
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
# GUI apps and App Store apps. Installed by `./setup-mac.sh apps`.
|
||||
# Sign in to the App Store first, or the mas entries fail.
|
||||
# Apps already in /Applications that Homebrew did not install are skipped by
|
||||
# setup-mac.sh, so this is safe to run on a Mac that already has them.
|
||||
# Install by hand (no usable cask): Bartender 6 (the cask is Bartender 7),
|
||||
# FileZilla, VMware Fusion and YubiKey Manager.
|
||||
|
||||
tap "glinford/tap"
|
||||
tap "johannesnagl/tap"
|
||||
|
||||
# Development
|
||||
cask "docker-desktop"
|
||||
cask "zed"
|
||||
cask "android-studio"
|
||||
cask "bruno"
|
||||
cask "beekeeper-studio"
|
||||
cask "mongodb-compass"
|
||||
cask "redis-insight"
|
||||
cask "charles"
|
||||
cask "cyberduck"
|
||||
cask "chromium"
|
||||
cask "johannesnagl/tap/showmd", trusted: true
|
||||
|
||||
# AI and agents
|
||||
cask "conductor"
|
||||
cask "claude"
|
||||
cask "opencode-desktop"
|
||||
cask "lm-studio"
|
||||
cask "ollama-app"
|
||||
cask "emdash"
|
||||
cask "open-design"
|
||||
cask "block-buzz"
|
||||
cask "typewhisper"
|
||||
|
||||
# Browsers
|
||||
cask "brave-browser"
|
||||
cask "google-chrome"
|
||||
cask "firefox"
|
||||
cask "librewolf"
|
||||
cask "zen"
|
||||
|
||||
# Productivity
|
||||
cask "1password"
|
||||
cask "raycast"
|
||||
cask "linear"
|
||||
cask "obsidian"
|
||||
cask "ticktick"
|
||||
cask "granola"
|
||||
cask "mailspring"
|
||||
cask "libreoffice"
|
||||
cask "affinity"
|
||||
cask "numi"
|
||||
cask "mmex"
|
||||
|
||||
# Communication
|
||||
cask "discord"
|
||||
cask "element"
|
||||
cask "telegram-desktop"
|
||||
cask "whatsapp"
|
||||
cask "microsoft-teams"
|
||||
|
||||
# Menu bar and system utilities
|
||||
cask "hiddenbar"
|
||||
cask "meetingbar"
|
||||
cask "stats"
|
||||
cask "monitorcontrol"
|
||||
cask "rectangle-pro"
|
||||
cask "appcleaner"
|
||||
cask "latest"
|
||||
cask "glinford/tap/dns-easy-switcher", trusted: true
|
||||
cask "localsend"
|
||||
cask "logi-options+"
|
||||
cask "radio-silence"
|
||||
cask "reikey"
|
||||
cask "yubico-authenticator"
|
||||
cask "protonvpn"
|
||||
cask "teamviewer"
|
||||
cask "balenaetcher"
|
||||
|
||||
# Media and games
|
||||
cask "spotify"
|
||||
cask "vlc"
|
||||
cask "obs"
|
||||
cask "jellyfin-media-player"
|
||||
cask "transmission"
|
||||
cask "steam"
|
||||
cask "minecraft"
|
||||
|
||||
# App Store
|
||||
mas "1Password for Safari", id: 1569813296
|
||||
mas "AdBlock", id: 1402042596
|
||||
mas "Amphetamine", id: 937984704
|
||||
mas "Bitwarden", id: 1352778147
|
||||
mas "Boop", id: 1518425043
|
||||
mas "Color Picker", id: 1545870783
|
||||
mas "DeArrow", id: 6451469297
|
||||
mas "Disk Drill Media Recovery", id: 431224317
|
||||
mas "DuckDuckGo Privacy for Safari", id: 1482920575
|
||||
mas "Fresco", id: 1251572132
|
||||
mas "GarageBand", id: 682658836
|
||||
mas "Gestimer", id: 990588172
|
||||
mas "Keka", id: 470158793
|
||||
mas "Keynote", id: 409183694
|
||||
mas "Keynote Creator Studio", id: 361285480
|
||||
mas "Maccy", id: 1527619437
|
||||
mas "Microsoft Excel", id: 462058435
|
||||
mas "Microsoft PowerPoint", id: 462062816
|
||||
mas "Microsoft Word", id: 462054704
|
||||
mas "Numbers", id: 409203825
|
||||
mas "Numbers Creator Studio", id: 361304891
|
||||
mas "Pages", id: 409201541
|
||||
mas "Pages Creator Studio", id: 361309726
|
||||
mas "Pixelmator Pro", id: 1289583905
|
||||
mas "Slack", id: 803453959
|
||||
mas "SponsorBlock", id: 1573461917
|
||||
mas "Tailscale", id: 1475387142
|
||||
mas "Termius 2", id: 1176074088
|
||||
mas "TextSniper", id: 1528890965
|
||||
mas "Xcode", id: 497799835
|
||||
@@ -33,13 +33,10 @@ This repository contains personal dotfiles for configuring a complete developmen
|
||||
|
||||
Before installing these dotfiles, ensure you have the following:
|
||||
|
||||
- **Operating System**: macOS (Darwin platform)
|
||||
- **Package Manager**: [Homebrew](https://brew.sh/) installed
|
||||
- **Shell**: Zsh (default on modern macOS)
|
||||
- **Git**: Version control system
|
||||
- **Node.js**: Required for many Neovim plugins and LSP servers
|
||||
- **Python**: Required for certain development tools and Neovim plugins
|
||||
- **Rust**: (Optional) Required for some tools like ripgrep, fd, etc.
|
||||
- **Operating System**: macOS (Apple Silicon or Intel)
|
||||
- **Git**: To clone this repository. macOS offers to install it with the Command Line Tools the first time you run `git`
|
||||
|
||||
`setup-mac.sh` installs everything else: the Command Line Tools, Homebrew, Node.js, Python, Rust and the rest.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -53,17 +50,28 @@ Before installing these dotfiles, ensure you have the following:
|
||||
cd ~/dotfiles
|
||||
```
|
||||
|
||||
2. **Run the setup script**:
|
||||
2. **Preview, then run the setup script**:
|
||||
```bash
|
||||
./setup-mac.sh --dry-run
|
||||
./setup-mac.sh
|
||||
```
|
||||
|
||||
3. **Create symbolic links** (if not handled by setup script):
|
||||
```bash
|
||||
ln -s ~/dotfiles/.config ~/.config
|
||||
ln -s ~/dotfiles/.zshrc ~/.zshrc
|
||||
ln -s ~/dotfiles/.tmux.conf ~/.tmux.conf
|
||||
```
|
||||
The script is safe to rerun: each step checks what is already in place and only does the missing work. A failed step is reported at the end while the other steps still run. Name steps to run only those, for example `./setup-mac.sh links` after adding a config.
|
||||
|
||||
| Step | What it does |
|
||||
|------|--------------|
|
||||
| `xcode` | Installs the Xcode Command Line Tools |
|
||||
| `homebrew` | Installs Homebrew and loads it from `~/.zprofile` |
|
||||
| `brew` | Installs the CLI tools, fonts and terminals in `Brewfile` |
|
||||
| `links` | Fetches the Neovim submodule on a fresh clone and symlinks the configs and `.local/bin` scripts into `$HOME`, backing up anything it replaces to `~/.local/state/dotfiles/backups/` |
|
||||
| `shell` | Installs oh-my-zsh and the tmux plugins |
|
||||
| `runtimes` | Node with nvm and the global npm packages, Rust, Python with pyenv, poetry, and Flutter with fvm |
|
||||
| `ai` | Installs omo, Claude Code, OpenCode and Codex, then runs `link-ai-configs.sh` |
|
||||
| `apps` | Installs the GUI and App Store apps in `Brewfile.apps` |
|
||||
|
||||
`brew` and `apps` never upgrade packages that are already installed. `apps` skips apps already in `/Applications` that Homebrew did not install, and its App Store entries need you signed in to the App Store first. To list what is installed but missing from `Brewfile`, run `brew bundle dump --file=- | diff Brewfile -`.
|
||||
|
||||
A few things still need you afterwards, such as signing in to the App Store and the AI tools, and turning on the 1Password SSH agent. The script lists them when it finishes.
|
||||
|
||||
### Manual Installation
|
||||
|
||||
@@ -83,7 +91,7 @@ ln -s ~/dotfiles/.zshrc ~/.zshrc
|
||||
|
||||
### AI configuration restore
|
||||
|
||||
After installing Node.js, OpenCode, Claude Code, and OMC, restore the selected AI configuration files separately from the package installer:
|
||||
The `ai` step of `setup-mac.sh` runs this script. To run it on its own after installing Node.js, OpenCode, Claude Code, and OMC:
|
||||
|
||||
```bash
|
||||
bash ~/dotfiles/link-ai-configs.sh --dry-run
|
||||
@@ -99,14 +107,7 @@ The script creates individual relative symlinks, skips existing correct links, a
|
||||
|
||||
Credentials, `settings.local.json`, plugin installations, unrelated user skills, histories, caches, and runtime state are not copied into this repository or managed by this script. The selected Impeccable, Vercel web-design-guidelines and frontend-workflow skills are managed individually; see [the GPT visual workflow](.config/opencode/README.md) for provenance, verification and rollback. Install other desired plugins and portable skills separately. OpenCode's local MCP servers are referenced through `{env:HOME}`, so the paths are portable, but the servers themselves (headroom, agcanvas, uvx, ast-grep-mcp, portfolio-mcp) must be installed at those locations on a new machine.
|
||||
|
||||
The OmO hooks, slash commands and workspace scripts are not linked by the script yet. Link them by hand:
|
||||
|
||||
```bash
|
||||
ln -s ../../dotfiles/.config/omo/hooks.json ~/.omo/agent/hooks.json
|
||||
ln -s ../../dotfiles/.config/omo/prompts ~/.omo/agent/prompts
|
||||
ln -s ../../dotfiles/.local/bin/owt ~/.local/bin/owt
|
||||
ln -s ../../dotfiles/.local/bin/omo-notify ~/.local/bin/omo-notify
|
||||
```
|
||||
The OmO hooks and slash commands (`~/.omo/agent/hooks.json` and `~/.omo/agent/prompts`) and the workspace scripts in `~/.local/bin` are linked by the `links` step of `setup-mac.sh` instead.
|
||||
|
||||
Restart OpenCode and Claude Code after applying changes. On first use, Claude may ask to approve the shared preferences import. Updates can rewrite generated configuration or replace symlinks: review the live files and repository diff before rerunning the link script, which backs up divergent live files rather than merging them. No plugin versions or model choices are changed by this restore step.
|
||||
|
||||
@@ -156,7 +157,7 @@ Key features:
|
||||
|
||||
### Development Tools
|
||||
|
||||
- **tmux** (`.tmux.conf`): Terminal multiplexer with custom key bindings; sets the terminal title to the session name
|
||||
- **tmux** (`.tmux.conf`): Terminal multiplexer with custom key bindings; sets the terminal title to the session name, after the omo status marker when there is one
|
||||
- **Zed** (`.config/zed/keymap.json`): Editor key bindings. Settings stay local because they reference machine-specific agent paths
|
||||
- **tmux server** (`.tmux-server.conf`): Lightweight tmux config for remote servers
|
||||
- **Git**: Global gitignore patterns (`.rgignore`, `.gitignore`)
|
||||
@@ -195,7 +196,7 @@ MCP servers: Playwright, Chrome DevTools, Linear, reoclo, time, ast-grep, headro
|
||||
#### OmO (`.config/omo/`)
|
||||
|
||||
- `omo.jsonc` (linked as `~/.omo/omo.jsonc`): agent and category model routing for OpenCode and the native harness.
|
||||
- `hooks.json` (linked as `~/.omo/agent/hooks.json`): runs `omo-notify` for a macOS notification when a session stops or needs attention.
|
||||
- `hooks.json` (linked as `~/.omo/agent/hooks.json`): runs `omo-notify` when you send a prompt, when a session stops and when it asks you something, for the sound, the notification and the coloured tab marker.
|
||||
- `prompts/` (linked as `~/.omo/agent/prompts`): slash commands for `owt` workspaces.
|
||||
- `/linear-sync [notes]` posts a progress comment on the workspace's Linear issues and sets them to In Progress, or In Review once a PR is open.
|
||||
- `/owt-done [notes]` finishes a workspace after its PR merges: it comments on each issue and moves it to Done, then runs `owt done --detach`. It asks before discarding uncommitted or unmerged work.
|
||||
@@ -208,12 +209,16 @@ MCP servers: Playwright, Chrome DevTools, Linear, reoclo, time, ast-grep, headro
|
||||
owt new # random city name, e.g. ~/worktrees/<repo>/lagos on branch feature/lagos
|
||||
owt new PRA-107 PRA-108 # same, and tell the agent which Linear issues to start on
|
||||
owt ls # workspaces and their current branches
|
||||
owt all # workspaces of every repo, with branches and source checkouts (from anywhere)
|
||||
owt cd # jump into a workspace with fzf; owt cd lagos goes straight there
|
||||
owt open lagos # reopen the agent in a workspace
|
||||
owt done # remove the workspace you are in once its work is merged
|
||||
```
|
||||
|
||||
`owt new` starts from the latest `origin` default branch, copies local `.env` files, installs dependencies, and opens a tmux window running omo with the workspace conventions: rename the placeholder branch to `feature/<keys>-<slug>` before pushing, and keep the Linear issues' statuses in sync. Workspaces share the main checkout's omo memory through a link under `~/.omo/memory/agents/`.
|
||||
|
||||
`owt cd` works from any directory. Inside a repository the picker starts filtered to that repo's workspaces and jumps straight in when only one matches. The `owt` function in `.zshrc` performs the `cd`; without it, `owt cd` only prints the path.
|
||||
|
||||
`owt done` refuses to discard work unless you pass `--force`. Work counts as merged when the branch is part of the default branch or its Gitea PR is merged (checked with `tea`). It then deletes the remote and local branch, removes the worktree and its memory link, and closes tmux windows open in it. `OWT_ROOT` (default `~/worktrees`) and `OWT_AGENT` (default `omo`) override the defaults; `owt --help` lists every option.
|
||||
|
||||
#### Neovim AI Integration
|
||||
@@ -242,14 +247,11 @@ Custom scripts and binaries are stored in `.local/bin/`:
|
||||
- `claude-tmux`: Claude integration with tmux
|
||||
- `cldir`: Change directory with Claude context
|
||||
- `owt`: Git worktree workspaces for coding agents (see [Agent workspaces](#agent-workspaces-owt))
|
||||
- `omo-notify`: macOS notification for OmO's Stop and Notification hooks
|
||||
- `omo-notify`: Sound, tab marker and notification for OmO's hooks. Ghostty posts the notification, so clicking it opens the session's tab and pane
|
||||
- `omo-status`: Coloured marker in front of the Ghostty tab title for each tmux session running omo: 🔴 stopped with an error, 🟡 waiting for your answer, 🟢 finished, 🔵 working. A tab with several agents shows the most urgent one; focusing the tab clears red, amber and green
|
||||
- `docker-nuke`: Force-restart a wedged Docker Desktop, capturing diagnostics to `~/.local/state/docker-nuke/` first (`--dry-run` to preview, `--no-restart` to leave it down, `--deep` for root helpers)
|
||||
|
||||
Ensure this directory is in your PATH:
|
||||
|
||||
```bash
|
||||
export PATH="$HOME/dotfiles/.local/bin:$PATH"
|
||||
```
|
||||
The `links` step of `setup-mac.sh` links each script into `~/.local/bin`, which `.zshrc` puts on your PATH. `pbcopy` and `pbpaste` are OSC 52 clipboard shims for headless servers, so they are not linked on macOS.
|
||||
|
||||
### Local Servers
|
||||
|
||||
|
||||
+428
-209
@@ -1,239 +1,458 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
# Set up a Mac from this dotfiles repo. Safe to rerun: every step checks what
|
||||
# is already in place and only does the missing work.
|
||||
#
|
||||
# ./setup-mac.sh run every step in order
|
||||
# ./setup-mac.sh links shell run only the named steps
|
||||
# ./setup-mac.sh --dry-run show what would change without changing it
|
||||
#
|
||||
# Steps: xcode homebrew brew links shell runtimes ai apps
|
||||
# Packages live in Brewfile (CLI tools, fonts, terminals) and Brewfile.apps
|
||||
# (GUI and App Store apps). Written for the bash 3.2 that ships with macOS.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
echo "Setting up your Mac..."
|
||||
# Check if Homebrew is installed
|
||||
if command -v brew &> /dev/null; then
|
||||
echo "Homebrew is installed."
|
||||
else
|
||||
echo "Homebrew is not installed."
|
||||
echo "Installing Homebrew..."
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
fi
|
||||
# Check if Git is installed
|
||||
STEPS="xcode homebrew brew links shell runtimes ai apps"
|
||||
DOTFILES=$(dirname "$(realpath "${BASH_SOURCE[0]}")")
|
||||
NVM_DIR=$HOME/.config/nvm
|
||||
NODE_VERSION=22
|
||||
PYTHON_VERSION=3.11
|
||||
NPM_GLOBALS="oh-my-claude-sisyphus 9router better-sqlite3 sql.js systray2"
|
||||
DRY_RUN=false
|
||||
|
||||
if command -v git &> /dev/null; then
|
||||
echo "Git is installed."
|
||||
else
|
||||
echo "Git is not installed."
|
||||
echo "Installing Git..."
|
||||
brew install git
|
||||
fi
|
||||
# Repo paths linked into $HOME, as "source:target" when the target differs.
|
||||
# Paths missing from the repo (gitignored local files such as .env) are skipped.
|
||||
# The AI configs are linked separately by link-ai-configs.sh in the ai step.
|
||||
LINKS="
|
||||
.zshrc
|
||||
.tmux.conf
|
||||
.tmux-server.conf
|
||||
.rgignore
|
||||
.withcontextconfig.jsonc
|
||||
.withcontextignore
|
||||
.env
|
||||
.envrc
|
||||
.opencommit
|
||||
AGENTS.md
|
||||
docs
|
||||
local_servers
|
||||
.superpowers
|
||||
.config/nvim
|
||||
.config/ghostty
|
||||
.config/wezterm
|
||||
.config/direnv
|
||||
.config/starship.toml
|
||||
.config/ohmyposh/zen.toml
|
||||
.config/zellij/config.kdl
|
||||
.config/zed/keymap.json
|
||||
.config/zed/settings.json
|
||||
.config/opencode/agent
|
||||
.config/opencode/command
|
||||
.config/omo/hooks.json:.omo/agent/hooks.json
|
||||
.config/omo/prompts:.omo/agent/prompts
|
||||
"
|
||||
|
||||
# Check if coreutils is installed
|
||||
say() { printf '\n\033[1;34m==> %s\033[0m\n' "$*"; }
|
||||
info() { printf ' %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33m ! %s\033[0m\n' "$*" >&2; }
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
if command -v gdate &> /dev/null; then
|
||||
echo "coreutils is installed."
|
||||
else
|
||||
echo "coreutils is not installed."
|
||||
echo "Installing coreutils..."
|
||||
brew install coreutils
|
||||
fi
|
||||
run() {
|
||||
if $DRY_RUN; then info "would run: $*"; else "$@"; fi
|
||||
}
|
||||
|
||||
# Check if OhMyPosh is installed
|
||||
run_remote() {
|
||||
local url=$1 script
|
||||
shift
|
||||
if $DRY_RUN; then
|
||||
info "would run the installer from $url $*"
|
||||
return 0
|
||||
fi
|
||||
script=$(curl -fsSL "$url") || return 1
|
||||
/bin/bash -c "$script" "$url" "$@"
|
||||
}
|
||||
|
||||
if command -v oh-my-posh &> /dev/null; then
|
||||
echo "OhMyPosh is installed."
|
||||
else
|
||||
echo "OhMyPosh is not installed."
|
||||
echo "Installing OhMyPosh..."
|
||||
brew install jandedobbeleer/oh-my-posh/oh-my-posh
|
||||
fi
|
||||
load_brew_env() {
|
||||
local brew
|
||||
for brew in /opt/homebrew/bin/brew /usr/local/bin/brew; do
|
||||
if [[ -x $brew ]]; then
|
||||
eval "$("$brew" shellenv)"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if asdf is installed
|
||||
if command -v asdf &> /dev/null; then
|
||||
echo "asdf is installed."
|
||||
else
|
||||
echo "asdf is not installed."
|
||||
echo "Installing asdf..."
|
||||
brew install asdf
|
||||
describe() {
|
||||
case $1 in
|
||||
xcode) echo "Xcode Command Line Tools" ;;
|
||||
homebrew) echo "Homebrew" ;;
|
||||
brew) echo "CLI tools, fonts and terminals (Brewfile)" ;;
|
||||
links) echo "Dotfile symlinks" ;;
|
||||
shell) echo "oh-my-zsh and tmux plugins" ;;
|
||||
runtimes) echo "Node, Rust, Python and Flutter" ;;
|
||||
ai) echo "AI coding tools and their configs" ;;
|
||||
apps) echo "GUI and App Store apps (Brewfile.apps)" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo "Adding asdf plugins..."
|
||||
asdf plugin add nodejs https://github.com/asdf-vm/asdf-nodejs.git
|
||||
fi
|
||||
step_xcode() {
|
||||
if xcode-select -p >/dev/null 2>&1; then
|
||||
info "Command Line Tools already installed"
|
||||
return 0
|
||||
fi
|
||||
run xcode-select --install
|
||||
if ! $DRY_RUN; then
|
||||
info "Finish the Command Line Tools installer window; waiting for it"
|
||||
until xcode-select -p >/dev/null 2>&1; do sleep 5; done
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if NodeJS is installed
|
||||
if command -v node &> /dev/null; then
|
||||
echo "NodeJS is installed."
|
||||
else
|
||||
echo "NodeJS is not installed."
|
||||
echo "Installing NodeJS..."
|
||||
brew install node
|
||||
fi
|
||||
step_homebrew() {
|
||||
local prefix=/usr/local
|
||||
if [[ $(uname -m) == arm64 ]]; then prefix=/opt/homebrew; fi
|
||||
if [[ -x $prefix/bin/brew ]]; then
|
||||
info "Homebrew already installed"
|
||||
else
|
||||
run_remote https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh
|
||||
fi
|
||||
# Login shells get brew on PATH from .zprofile; .zshrc relies on it.
|
||||
if grep -qs 'brew shellenv' "$HOME/.zprofile"; then
|
||||
info "Homebrew already loaded by ~/.zprofile"
|
||||
elif $DRY_RUN; then
|
||||
info "would add Homebrew's shellenv to ~/.zprofile"
|
||||
else
|
||||
# shellcheck disable=SC2016 # the $(...) is meant for .zprofile, not here
|
||||
printf '\n# Homebrew\neval "$(%s/bin/brew shellenv)"\n' "$prefix" >>"$HOME/.zprofile"
|
||||
info "added Homebrew's shellenv to ~/.zprofile"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if Neovim is installed
|
||||
# Installs a Brewfile without upgrading what is already installed. In dry-run
|
||||
# mode it lists what is missing instead.
|
||||
bundle() {
|
||||
if ! have brew; then
|
||||
warn "Homebrew is missing; run the homebrew step first"
|
||||
return 1
|
||||
fi
|
||||
if $DRY_RUN; then
|
||||
info "missing from ${1##*/}:"
|
||||
HOMEBREW_NO_AUTO_UPDATE=1 HOMEBREW_BUNDLE_NO_UPGRADE=1 brew bundle check --file="$1" --verbose 2>&1 |
|
||||
grep -v '^The Brewfile' | sed 's/^/ /' || true
|
||||
return 0
|
||||
fi
|
||||
HOMEBREW_BUNDLE_NO_UPGRADE=1 brew bundle install --file="$1"
|
||||
}
|
||||
|
||||
if command -v nvim &> /dev/null; then
|
||||
echo "Neovim is installed."
|
||||
else
|
||||
echo "Neovim is not installed."
|
||||
echo "Installing Neovim..."
|
||||
brew install neovim
|
||||
fi
|
||||
step_brew() {
|
||||
bundle "$DOTFILES/Brewfile"
|
||||
}
|
||||
|
||||
# Check if WezTerm is installed
|
||||
BACKUP_DIR=""
|
||||
KEPT=0
|
||||
|
||||
# Links ~/<target> to the repo's <source>, backing up whatever was there.
|
||||
link() {
|
||||
local src=$DOTFILES/$1 dst=$HOME/$2 parent
|
||||
if [[ ! -e $src ]]; then
|
||||
info "skipped ~/$2 ($1 is not in the repo)"
|
||||
return 0
|
||||
fi
|
||||
if [[ $src -ef $dst ]]; then
|
||||
KEPT=$((KEPT + 1))
|
||||
return 0
|
||||
fi
|
||||
if $DRY_RUN; then
|
||||
if [[ -e $dst || -L $dst ]]; then
|
||||
info "would back up ~/$2 and link it to $1"
|
||||
else
|
||||
info "would link ~/$2 to $1"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
parent=$(dirname "$dst")
|
||||
mkdir -p "$parent"
|
||||
if [[ -e $dst || -L $dst ]]; then
|
||||
if [[ -z $BACKUP_DIR ]]; then
|
||||
BACKUP_DIR=$HOME/.local/state/dotfiles/backups/$(date +%Y-%m-%dT%H-%M-%S)
|
||||
fi
|
||||
mkdir -p "$(dirname "$BACKUP_DIR/$2")"
|
||||
mv "$dst" "$BACKUP_DIR/$2"
|
||||
info "backed up ~/$2 to $BACKUP_DIR/$2"
|
||||
fi
|
||||
ln -s "$(python3 -c 'import os, sys; print(os.path.relpath(sys.argv[1], os.path.realpath(sys.argv[2])))' "$src" "$parent")" "$dst"
|
||||
info "linked ~/$2"
|
||||
}
|
||||
|
||||
if command -v wezterm &> /dev/null; then
|
||||
echo "WezTerm is installed."
|
||||
else
|
||||
echo "WezTerm is not installed."
|
||||
echo "Installing WezTerm..."
|
||||
brew install wezterm
|
||||
fi
|
||||
step_links() {
|
||||
# Fetch the nvim config on a fresh clone only. An initialised submodule is
|
||||
# left alone so whatever branch it has checked out is never reset.
|
||||
if git -C "$DOTFILES" submodule status .config/nvim | grep -q '^-'; then
|
||||
run git -C "$DOTFILES" submodule update --init --recursive .config/nvim
|
||||
fi
|
||||
local entry script name
|
||||
for entry in $LINKS; do
|
||||
link "${entry%%:*}" "${entry#*:}"
|
||||
done
|
||||
for script in "$DOTFILES"/.local/bin/*; do
|
||||
name=${script##*/}
|
||||
case $name in
|
||||
pbcopy | pbpaste) continue ;; # OSC 52 clipboard shims for headless servers
|
||||
esac
|
||||
if [[ -x $script ]]; then link ".local/bin/$name" ".local/bin/$name"; fi
|
||||
done
|
||||
info "$KEPT links already in place"
|
||||
}
|
||||
|
||||
step_shell() {
|
||||
if [[ -d $HOME/.oh-my-zsh ]]; then
|
||||
info "oh-my-zsh already installed"
|
||||
else
|
||||
# --keep-zshrc leaves the linked .zshrc alone.
|
||||
run_remote https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh --unattended --keep-zshrc
|
||||
fi
|
||||
local tpm=$HOME/.tmux/plugins/tpm
|
||||
if [[ -d $tpm ]]; then
|
||||
info "tpm already installed"
|
||||
else
|
||||
run git clone --depth 1 https://github.com/tmux-plugins/tpm "$tpm"
|
||||
fi
|
||||
# Installs the .tmux.conf plugins that are not there yet.
|
||||
run "$tpm/bin/install_plugins"
|
||||
}
|
||||
|
||||
# Ensure fira font is installed
|
||||
install_node() {
|
||||
if [[ -s $NVM_DIR/nvm.sh ]]; then
|
||||
info "nvm already installed"
|
||||
else
|
||||
run git clone --quiet https://github.com/nvm-sh/nvm.git "$NVM_DIR"
|
||||
if $DRY_RUN; then
|
||||
info "would install Node $NODE_VERSION as the nvm default, plus npm globals: $NPM_GLOBALS"
|
||||
return 0
|
||||
fi
|
||||
git -C "$NVM_DIR" checkout --quiet "$(git -C "$NVM_DIR" describe --abbrev=0 --tags --match 'v[0-9]*' "$(git -C "$NVM_DIR" rev-list --tags --max-count=1)")"
|
||||
fi
|
||||
# nvm does not work under set -eu, so it gets a subshell of its own.
|
||||
(
|
||||
set +eu
|
||||
# shellcheck source=/dev/null
|
||||
. "$NVM_DIR/nvm.sh"
|
||||
if nvm which "$NODE_VERSION" >/dev/null 2>&1; then
|
||||
info "Node $NODE_VERSION already installed"
|
||||
else
|
||||
run nvm install "$NODE_VERSION" || exit 1
|
||||
fi
|
||||
if [[ $(nvm version default) != "$(nvm version "$NODE_VERSION")" ]]; then
|
||||
run nvm alias default "$NODE_VERSION" || exit 1
|
||||
fi
|
||||
nvm use --silent "$NODE_VERSION" >/dev/null 2>&1 || exit 0 # dry run before Node exists
|
||||
for pkg in $NPM_GLOBALS; do
|
||||
if npm ls -g --depth=0 "$pkg" >/dev/null 2>&1; then
|
||||
info "npm $pkg already installed"
|
||||
else
|
||||
run npm install -g "$pkg" || exit 1
|
||||
fi
|
||||
done
|
||||
)
|
||||
}
|
||||
|
||||
brew install font-fira-code-nerd-font
|
||||
install_rust() {
|
||||
if [[ -x $HOME/.cargo/bin/rustup ]]; then
|
||||
info "Rust already installed"
|
||||
else
|
||||
run_remote https://sh.rustup.rs -y --no-modify-path
|
||||
fi
|
||||
if [[ -x $HOME/.cargo/bin/cargo-xwin ]]; then
|
||||
info "cargo-xwin already installed"
|
||||
else
|
||||
run "$HOME/.cargo/bin/cargo" install --locked cargo-xwin
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if Zsh is installed
|
||||
install_python() {
|
||||
local current
|
||||
current=$(pyenv global 2>/dev/null || true)
|
||||
if [[ -n $current && $current != system ]]; then
|
||||
info "pyenv global Python already set ($current)"
|
||||
elif $DRY_RUN; then
|
||||
info "would install the latest Python $PYTHON_VERSION with pyenv and make it the global default"
|
||||
else
|
||||
pyenv install --skip-existing "$PYTHON_VERSION"
|
||||
pyenv global "$(pyenv latest "$PYTHON_VERSION")"
|
||||
fi
|
||||
if have poetry; then
|
||||
info "poetry already installed"
|
||||
else
|
||||
run uv tool install poetry
|
||||
fi
|
||||
# .zshrc adds ~/.zfunc to fpath for these completions.
|
||||
if [[ -f $HOME/.zfunc/_poetry ]]; then
|
||||
info "poetry completions already installed"
|
||||
elif $DRY_RUN; then
|
||||
info "would write poetry's zsh completions to ~/.zfunc/_poetry"
|
||||
else
|
||||
mkdir -p "$HOME/.zfunc"
|
||||
poetry completions zsh >"$HOME/.zfunc/_poetry"
|
||||
fi
|
||||
}
|
||||
|
||||
if command -v zsh &> /dev/null; then
|
||||
echo "Zsh is installed."
|
||||
else
|
||||
echo "Zsh is not installed."
|
||||
echo "Installing Zsh..."
|
||||
brew install zsh
|
||||
install_flutter() {
|
||||
if [[ -e $HOME/fvm/default ]]; then
|
||||
info "Flutter already installed (fvm global)"
|
||||
else
|
||||
run fvm install stable
|
||||
run fvm global stable
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Setting Zsh as default shell..."
|
||||
sudo sh -c "echo $(which zsh) >> /etc/shells"
|
||||
fi
|
||||
step_runtimes() {
|
||||
install_node
|
||||
install_rust
|
||||
install_python
|
||||
install_flutter
|
||||
}
|
||||
|
||||
# Check if Generic Colorizer is installed
|
||||
step_ai() {
|
||||
if have omo; then info "omo already installed"; else run bun add -g omo-ai; fi
|
||||
if have claude; then
|
||||
info "Claude Code already installed"
|
||||
else
|
||||
run_remote https://claude.ai/install.sh
|
||||
fi
|
||||
if have opencode; then
|
||||
info "OpenCode already installed"
|
||||
else
|
||||
run_remote https://opencode.ai/install --no-modify-path
|
||||
fi
|
||||
if have codex; then info "Codex already installed"; else run brew install --cask codex; fi
|
||||
# link-ai-configs.sh needs Node from the runtimes step.
|
||||
local args=""
|
||||
if $DRY_RUN; then args=--dry-run; fi
|
||||
(
|
||||
set +eu
|
||||
# shellcheck source=/dev/null
|
||||
. "$NVM_DIR/nvm.sh"
|
||||
bash "$DOTFILES/link-ai-configs.sh" $args
|
||||
)
|
||||
}
|
||||
|
||||
if command -v grc &> /dev/null; then
|
||||
echo "Generic Colorizer is installed."
|
||||
else
|
||||
echo "Generic Colorizer is not installed."
|
||||
echo "Installing Generic Colorizer..."
|
||||
brew install grc
|
||||
fi
|
||||
# Prints the casks in a Brewfile whose app is already installed, but not by
|
||||
# Homebrew. Installing those fails with "It seems there is already an App".
|
||||
unmanaged_casks() {
|
||||
local cask casks="" managed json name kind id
|
||||
if ! have jq; then
|
||||
warn "jq is missing, so apps installed outside Homebrew cannot be detected"
|
||||
return 0
|
||||
fi
|
||||
managed=$(brew list --cask)
|
||||
while read -r cask; do
|
||||
if ! grep -qx "${cask##*/}" <<<"$managed"; then casks="$casks $cask"; fi
|
||||
done < <(sed -nE 's/^cask "([^"]+)".*/\1/p' "$1")
|
||||
if [[ -z $casks ]]; then return 0; fi
|
||||
# Casks from taps that are not trusted yet cannot be looked up; on a fresh
|
||||
# Mac there is nothing to skip anyway.
|
||||
# shellcheck disable=SC2086 # one argument per cask
|
||||
if ! json=$(HOMEBREW_NO_AUTO_UPDATE=1 brew info --cask --json=v2 $casks 2>/dev/null); then
|
||||
warn "could not look up the casks, so none are skipped"
|
||||
return 0
|
||||
fi
|
||||
# Each cask yields the app names it installs (source and renamed target),
|
||||
# or, for pkg and installer casks, its display name plus its pkg receipts.
|
||||
jq -r '
|
||||
.casks[] | .token as $name |
|
||||
[.artifacts[] | select(has("app")) | .app | (.[0] | split("/") | last),
|
||||
(.[1]? | objects | .target // empty | split("/") | last)] as $apps |
|
||||
if ($apps | length) > 0 then $apps[] | "\($name)\tapp\t\(.)"
|
||||
else "\($name)\tapp\t\(.name[0]).app",
|
||||
(.artifacts[] | select(has("uninstall")) | .uninstall[] | .pkgutil? // empty
|
||||
| (if type == "array" then .[] else . end) | "\($name)\tpkg\t\(.)")
|
||||
end' <<<"$json" |
|
||||
while IFS=$'\t' read -r name kind id; do
|
||||
if [[ $kind == app && (-e /Applications/$id || -e $HOME/Applications/$id) ]] ||
|
||||
{ [[ $kind == pkg && -n $(pkgutil --pkgs="$id" 2>/dev/null) ]]; }; then
|
||||
echo "$name"
|
||||
fi
|
||||
done | sort -u | tr '\n' ' '
|
||||
}
|
||||
|
||||
# Check if bat is installed
|
||||
step_apps() {
|
||||
local skip
|
||||
if ! have brew; then
|
||||
warn "Homebrew is missing; run the homebrew step first"
|
||||
return 1
|
||||
fi
|
||||
skip=$(unmanaged_casks "$DOTFILES/Brewfile.apps")
|
||||
skip=${skip% }
|
||||
if [[ -n $skip ]]; then
|
||||
info "skipping casks for apps installed outside Homebrew: $skip"
|
||||
fi
|
||||
HOMEBREW_BUNDLE_CASK_SKIP=$skip bundle "$DOTFILES/Brewfile.apps"
|
||||
}
|
||||
|
||||
if command -v bat &> /dev/null; then
|
||||
echo "bat is installed."
|
||||
else
|
||||
echo "bat is not installed."
|
||||
echo "Installing bat..."
|
||||
brew install bat
|
||||
fi
|
||||
usage() {
|
||||
sed -n '2,/^$/s/^# \{0,1\}//p' "${BASH_SOURCE[0]}"
|
||||
}
|
||||
|
||||
# Check if eza is installed
|
||||
main() {
|
||||
local arg step status selected="" failed=""
|
||||
for arg in "$@"; do
|
||||
case $arg in
|
||||
-n | --dry-run) DRY_RUN=true ;;
|
||||
-h | --help)
|
||||
usage
|
||||
return 0
|
||||
;;
|
||||
-*)
|
||||
warn "unknown option: $arg"
|
||||
usage
|
||||
return 2
|
||||
;;
|
||||
*)
|
||||
if [[ " $STEPS " != *" $arg "* ]]; then
|
||||
warn "unknown step: $arg (steps: $STEPS)"
|
||||
return 2
|
||||
fi
|
||||
selected="$selected $arg"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
if [[ $(uname -s) != Darwin ]]; then
|
||||
warn "setup-mac.sh only runs on macOS"
|
||||
return 1
|
||||
fi
|
||||
if [[ -z $selected ]]; then selected=$STEPS; fi
|
||||
|
||||
if command -v eza &> /dev/null; then
|
||||
echo "eza is installed."
|
||||
else
|
||||
echo "eza is not installed."
|
||||
echo "Installing eza..."
|
||||
brew install eza
|
||||
fi
|
||||
# Tools from non-Homebrew installers live here; the steps look for them.
|
||||
export PATH="$HOME/.local/bin:$HOME/.bun/bin:$HOME/.opencode/bin:$HOME/.cargo/bin:$PATH"
|
||||
load_brew_env || true
|
||||
if $DRY_RUN; then say "Dry run: nothing will be changed"; fi
|
||||
|
||||
# Install global pnpm package
|
||||
if command -v pnpm &> /dev/null; then
|
||||
echo "pnpm is installed."
|
||||
else
|
||||
echo "pnpm is not installed."
|
||||
echo "Installing pnpm..."
|
||||
npm install -g pnpm
|
||||
fi
|
||||
for step in $selected; do
|
||||
say "$step: $(describe "$step")"
|
||||
# Each step runs in a subshell with errexit so it stops at its first
|
||||
# failure. Testing the subshell directly would switch errexit off.
|
||||
(
|
||||
set -e
|
||||
"step_$step"
|
||||
)
|
||||
status=$?
|
||||
if [[ $status -ne 0 ]]; then
|
||||
failed="$failed $step"
|
||||
warn "$step failed; fix the error above, then rerun ./setup-mac.sh $step"
|
||||
fi
|
||||
load_brew_env || true
|
||||
done
|
||||
|
||||
# Check if fzf is installed
|
||||
say "Done"
|
||||
if [[ -n $failed ]]; then warn "failed steps:$failed"; fi
|
||||
if ! $DRY_RUN && [[ $selected == "$STEPS" ]]; then
|
||||
info "Steps that need you:"
|
||||
info "- Sign in to the App Store, then rerun ./setup-mac.sh apps for the App Store apps"
|
||||
info "- 1Password: turn on the SSH agent (Settings > Developer); .zshrc uses it for SSH"
|
||||
info "- Sign in: gh auth login, then Claude Code, Codex and OpenCode"
|
||||
info "- Open a new terminal to load the new shell setup"
|
||||
fi
|
||||
[[ -z $failed ]]
|
||||
}
|
||||
|
||||
if command -v fzf &> /dev/null; then
|
||||
echo "fzf is installed."
|
||||
else
|
||||
echo "fzf is not installed."
|
||||
echo "Installing fzf..."
|
||||
brew install fzf
|
||||
fi
|
||||
|
||||
# Check if ripgrep is installed
|
||||
|
||||
if command -v rg &> /dev/null; then
|
||||
echo "ripgrep is installed."
|
||||
else
|
||||
echo "ripgrep is not installed."
|
||||
echo "Installing ripgrep..."
|
||||
brew install ripgrep
|
||||
fi
|
||||
|
||||
# Check if poetry is installed
|
||||
|
||||
if command -v poetry &> /dev/null; then
|
||||
echo "poetry is installed."
|
||||
else
|
||||
echo "poetry is not installed."
|
||||
echo "Installing poetry..."
|
||||
curl -sSL https://install.python-poetry.org | python3 -
|
||||
fi
|
||||
|
||||
# Check if gh is installed
|
||||
if command -v gh &> /dev/null; then
|
||||
echo "gh is installed"
|
||||
else
|
||||
echo "gh is not installed"
|
||||
echo "Installing gh with brew..."
|
||||
brew install gh
|
||||
|
||||
# Check if pyenv is installed
|
||||
|
||||
if command -v pyenv &> /dev/null; then
|
||||
echo "pyenv is installed."
|
||||
else
|
||||
echo "pyenv is not installed."
|
||||
echo "Installing pyenv with brew..."
|
||||
brew install pyenv
|
||||
|
||||
# Check if pyenv-virtualenv is installed
|
||||
if command -v pyenv &> /dev/null; then
|
||||
echo "pyenv-virtualenv is installed."
|
||||
else
|
||||
echo "pyenv-virtualenv is not installed."
|
||||
echo "Installing pyenv-virtualenv with brew..."
|
||||
brew install pyenv-virtualenv
|
||||
|
||||
# Check if anaconda is installed
|
||||
|
||||
if command -v conda &> /dev/null; then
|
||||
echo "anaconda is installed."
|
||||
else
|
||||
echo "anaconda is not installed."
|
||||
echo "Installing anaconda..."
|
||||
brew install --cask anaconda
|
||||
|
||||
fi
|
||||
|
||||
# Install Flutter version manager
|
||||
|
||||
if command -v fvm &> /dev/null; then
|
||||
echo "fvm is installed."
|
||||
else
|
||||
echo "fvm is not installed."
|
||||
echo "Installing fvm..."
|
||||
curl -fsSL https://fvm.app/install.sh | bash
|
||||
fi
|
||||
|
||||
|
||||
# Install SDK manager
|
||||
# Check if $HOME/.sdkman exists
|
||||
|
||||
if [ -d "$HOME/.sdkman" ]; then
|
||||
echo "sdkman is installed."
|
||||
else
|
||||
echo "sdkman is not installed."
|
||||
echo "Installing sdkman..."
|
||||
curl -s "https://get.sdkman.io" | bash
|
||||
fi
|
||||
|
||||
|
||||
# Install Aerospace
|
||||
|
||||
if command -v aerospace &> /dev/null; then
|
||||
echo "aerospace is installed."
|
||||
else
|
||||
echo "aerospace is not installed."
|
||||
echo "Installing aerospace..."
|
||||
brew install --cask nikitabobko/tap/aerospace
|
||||
fi
|
||||
main "$@"
|
||||
|
||||
Reference in New Issue
Block a user