mxup 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +168 -6
- data/examples/myapp-dev.yml +38 -7
- data/lib/mxup/cli.rb +76 -14
- data/lib/mxup/config.rb +197 -12
- data/lib/mxup/env_file.rb +82 -0
- data/lib/mxup/launcher.rb +85 -25
- data/lib/mxup/reconciler.rb +7 -0
- data/lib/mxup/runner.rb +133 -6
- data/lib/mxup/status_view.rb +49 -0
- data/lib/mxup/tmux.rb +29 -12
- data/lib/mxup/version.rb +1 -1
- data/lib/mxup/watcher.rb +168 -0
- data/lib/mxup.rb +8 -3
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b091e5654c4a748617259e0ea10d9e028d96febfe91d808da4fa4bbc8c2fbe3b
|
|
4
|
+
data.tar.gz: d63f016660507556cde2cc66137bab6eff7a3e02ab14a39b5ec9d1209db5a07a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48326d3972367778ad604835d4f8d5c9417256bbc7266e9f8906ccec1d55eca23ad6424515208dc2a4dd9573912201f0760a410323ca74fc55571161bc281dbc
|
|
7
|
+
data.tar.gz: 5dcabc72786c9e48882a944fe583325efbdfb3cfce23ebad09bac7b09e479797288a959dcf8f9434bbc619953352eb0688ebd86b3bb409ca666bf57b72c2870e
|
data/README.md
CHANGED
|
@@ -53,11 +53,35 @@ mxup down myapp-dev
|
|
|
53
53
|
|
|
54
54
|
## Config format
|
|
55
55
|
|
|
56
|
-
Configs live in `~/.config/mxup/<name>.yml
|
|
56
|
+
Configs live in `~/.config/mxup/<name>.yml`, in a project-local `./.mxup/<name>.yml`, or can be passed via `-f path`.
|
|
57
|
+
|
|
58
|
+
### Where mxup looks for a config
|
|
59
|
+
|
|
60
|
+
In order, `mxup` tries:
|
|
61
|
+
|
|
62
|
+
1. `-f <path>` if supplied.
|
|
63
|
+
2. `./.mxup/<name>.yml` — project-local, takes precedence over the global dir.
|
|
64
|
+
3. `~/.config/mxup/<name>.yml`.
|
|
65
|
+
4. `./mxup.yml` — bare config in the current directory.
|
|
66
|
+
5. The sole `*.yml` in `./.mxup/`, if exactly one is present.
|
|
67
|
+
6. The sole `*.yml` in `~/.config/mxup/`, if exactly one is present.
|
|
68
|
+
|
|
69
|
+
A project-local `.mxup/` directory is handy for committing per-repo dev sessions alongside the code:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
mkdir .mxup
|
|
73
|
+
cp examples/myapp-dev.yml .mxup/dev.yml
|
|
74
|
+
mxup up dev # picks up ./.mxup/dev.yml
|
|
75
|
+
```
|
|
57
76
|
|
|
58
77
|
```yaml
|
|
59
78
|
session: my-project
|
|
60
79
|
|
|
80
|
+
# Optional. Relative paths are resolved against the config file's directory
|
|
81
|
+
# (or, when the config lives in `.mxup/`, against the project root that
|
|
82
|
+
# contains it — so a checked-in `.mxup/dev.yml` can just say `root: .`).
|
|
83
|
+
root: ~/projects/my-app
|
|
84
|
+
|
|
61
85
|
# Shell snippet run in every window before the command
|
|
62
86
|
setup: |
|
|
63
87
|
direnv allow . 2>/dev/null
|
|
@@ -65,22 +89,22 @@ setup: |
|
|
|
65
89
|
|
|
66
90
|
windows:
|
|
67
91
|
database:
|
|
68
|
-
root: ~/projects/my-app
|
|
92
|
+
root: . # = ~/projects/my-app
|
|
69
93
|
command: docker compose up postgres redis
|
|
70
94
|
|
|
71
95
|
backend:
|
|
72
|
-
root: ~/projects/my-app/backend
|
|
96
|
+
root: backend # = ~/projects/my-app/backend
|
|
73
97
|
wait_for: localhost:5432
|
|
74
98
|
env:
|
|
75
99
|
DATABASE_URL: postgres://localhost/myapp_dev
|
|
76
100
|
command: ./start-server.sh
|
|
77
101
|
|
|
78
102
|
frontend:
|
|
79
|
-
root:
|
|
103
|
+
root: frontend
|
|
80
104
|
command: npm run dev
|
|
81
105
|
|
|
82
106
|
shell:
|
|
83
|
-
root:
|
|
107
|
+
root: .
|
|
84
108
|
```
|
|
85
109
|
|
|
86
110
|
### Fields
|
|
@@ -89,16 +113,19 @@ windows:
|
|
|
89
113
|
|-------|----------|-------------|
|
|
90
114
|
| `session` | yes | tmux session name |
|
|
91
115
|
| `setup` | no | Shell snippet prepended to every window's command |
|
|
116
|
+
| `root` | no | Base directory for relative window `root` values. Relative paths resolve against the config file's directory — or, if the config lives in a project-local `.mxup/` directory, against `.mxup/`'s parent (the project root). `~` and absolute paths are kept as-is. Supports `$VAR` / `${VAR}` env-var expansion (an unset variable is an error). |
|
|
117
|
+
| `required_env` | no | Env vars that must be filled in before `mxup up` will start (see [Required env vars](#required-env-vars)) |
|
|
92
118
|
| `windows` | yes | Ordered map of window definitions |
|
|
93
119
|
|
|
94
120
|
Per window:
|
|
95
121
|
|
|
96
122
|
| Field | Required | Description |
|
|
97
123
|
|-------|----------|-------------|
|
|
98
|
-
| `root` | yes | Working directory (
|
|
124
|
+
| `root` | yes | Working directory. `~` and absolute paths are kept as-is; relative paths resolve against the top-level `root` (or CWD if unset). Supports `$VAR` / `${VAR}` env-var expansion (an unset variable is an error). |
|
|
99
125
|
| `command` | no | Command to run. Omit for an interactive shell. |
|
|
100
126
|
| `env` | no | Map of environment variables to export |
|
|
101
127
|
| `wait_for` | no | Readiness check to pass before running command (see below) |
|
|
128
|
+
| `commands` | no | Map of named one-off commands runnable via `mxup <config> <window>:<name>` (see [Named commands](#named-commands-mxup-configwindowname)) |
|
|
102
129
|
|
|
103
130
|
### Wait-for checks
|
|
104
131
|
|
|
@@ -160,6 +187,107 @@ Then override at invocation:
|
|
|
160
187
|
APP_ENV=production mxup up my-project
|
|
161
188
|
```
|
|
162
189
|
|
|
190
|
+
### Live env (command-backed env vars with auto-restart)
|
|
191
|
+
|
|
192
|
+
For env vars whose value comes from a command you re-run periodically (auth
|
|
193
|
+
tokens, short-lived credentials), declare them under `live_env`. Each value
|
|
194
|
+
is a shell command whose stdout becomes the variable's value:
|
|
195
|
+
|
|
196
|
+
```yaml
|
|
197
|
+
live_env:
|
|
198
|
+
GRAZIE_TOKEN: cat ~/.grazie-token
|
|
199
|
+
AWS_SESSION_TOKEN: aws configure export-credentials --format env | sed -n 's/.*AWS_SESSION_TOKEN=//p'
|
|
200
|
+
|
|
201
|
+
windows:
|
|
202
|
+
api:
|
|
203
|
+
root: api
|
|
204
|
+
live_env: [GRAZIE_TOKEN]
|
|
205
|
+
command: ./gradlew run
|
|
206
|
+
|
|
207
|
+
worker:
|
|
208
|
+
root: worker
|
|
209
|
+
live_env: [GRAZIE_TOKEN, AWS_SESSION_TOKEN]
|
|
210
|
+
command: ./gradlew run
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The window's launcher exports each variable from its command's output
|
|
214
|
+
(`export GRAZIE_TOKEN="$(cat ~/.grazie-token)"`) before running its own
|
|
215
|
+
command, so the value is always read fresh on (re)start. Commands run in
|
|
216
|
+
the config's base directory, so relative paths inside them resolve the same
|
|
217
|
+
way as `root` — regardless of an individual window's `root`.
|
|
218
|
+
|
|
219
|
+
When `mxup up` finds a `live_env` block in the config, it spawns a
|
|
220
|
+
background watcher process for the session. The watcher re-evaluates each
|
|
221
|
+
command on an interval; whenever a command's output changes, it restarts
|
|
222
|
+
every window whose `live_env` list references it. So `echo new >
|
|
223
|
+
~/.grazie-token` is enough to bounce `api` and `worker` with the new value
|
|
224
|
+
— no manual restart. A command that exits non-zero is treated as "no value
|
|
225
|
+
yet" and never triggers a restart on its own.
|
|
226
|
+
|
|
227
|
+
| Field | Required | Description |
|
|
228
|
+
|-------|----------|-------------|
|
|
229
|
+
| `live_env` (top-level) | no | Map of env var name → shell command. The command's stdout is the value; it runs in the config's base directory. |
|
|
230
|
+
| `live_env` (per-window) | no | List of names from the top-level pool that this window depends on. Unknown names are an error. |
|
|
231
|
+
|
|
232
|
+
The watcher is stopped automatically by `mxup down`. Its PID, log, and
|
|
233
|
+
last-restart state live under `~/.local/share/mxup/<session>/`. `mxup
|
|
234
|
+
status` shows the watcher's PID and the most recent restart it issued.
|
|
235
|
+
|
|
236
|
+
### Required env vars
|
|
237
|
+
|
|
238
|
+
Some values can't live in the config because they're secret or
|
|
239
|
+
machine-specific (API keys, DB passwords). Declare them under `required_env`
|
|
240
|
+
and `mxup up` will refuse to start until they're supplied:
|
|
241
|
+
|
|
242
|
+
```yaml
|
|
243
|
+
required_env:
|
|
244
|
+
DATABASE_PASSWORD: # list/name-only form
|
|
245
|
+
STRIPE_SECRET_KEY: Test key from the dashboard # map form: name => description
|
|
246
|
+
|
|
247
|
+
windows:
|
|
248
|
+
api:
|
|
249
|
+
root: api
|
|
250
|
+
command: ./gradlew run
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Values are read from a dotenv-style file named after the config, sitting
|
|
254
|
+
right next to it — `~/.config/mxup/myapp-dev.yml` → `~/.config/mxup/myapp-dev.env`
|
|
255
|
+
(a project-local `.mxup/dev.yml` → `.mxup/dev.env`).
|
|
256
|
+
|
|
257
|
+
On `mxup up`, mxup checks that each required var has a non-empty value in
|
|
258
|
+
that file. If any are missing, it **creates or extends** the file with a
|
|
259
|
+
blank `NAME=` line for each (descriptions become `#` comments), tells you
|
|
260
|
+
what's missing, and aborts:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
$ mxup up myapp-dev
|
|
264
|
+
Missing required environment variable(s):
|
|
265
|
+
DATABASE_PASSWORD
|
|
266
|
+
STRIPE_SECRET_KEY — Test key from the dashboard
|
|
267
|
+
Fill in the values in /Users/me/.config/mxup/myapp-dev.env, then re-run `mxup up`.
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
```ini
|
|
271
|
+
# myapp-dev.env (generated)
|
|
272
|
+
DATABASE_PASSWORD=
|
|
273
|
+
|
|
274
|
+
# Test key from the dashboard
|
|
275
|
+
STRIPE_SECRET_KEY=
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Fill in the blanks and re-run. Existing values are never overwritten, so the
|
|
279
|
+
file is safe to keep around. Each window's launcher sources it (`set -a; . file;
|
|
280
|
+
set +a`) before `setup` and `command`, so every window inherits the values.
|
|
281
|
+
|
|
282
|
+
`--dry-run` reports what's missing but writes nothing and doesn't abort.
|
|
283
|
+
|
|
284
|
+
> **Keep it out of git.** The `.env` file holds secrets — add `*.env` (or
|
|
285
|
+
> `.mxup/*.env` for project-local configs) to your `.gitignore`.
|
|
286
|
+
|
|
287
|
+
| Field | Required | Description |
|
|
288
|
+
|-------|----------|-------------|
|
|
289
|
+
| `required_env` | no | List of names (`[A, B]`) or a map of `name => description`. Gates `mxup up`; values come from the adjacent `<config-name>.env` file. |
|
|
290
|
+
|
|
163
291
|
### Layouts
|
|
164
292
|
|
|
165
293
|
Define multiple named layouts to control how windows are grouped as tmux panes:
|
|
@@ -293,6 +421,7 @@ graceful-stop dance), then brings the new profile up from a clean slate.
|
|
|
293
421
|
| `mxup target [name:]<window>` | Print the tmux target (`session:window.pane`) for a logical window |
|
|
294
422
|
| `mxup target [name]` | Print targets for every declared window (tab-separated) |
|
|
295
423
|
| `mxup exec -t [name:]<window> "<cmd>"` | Run `<cmd>` in a pane, wait for completion, print output, exit with its status |
|
|
424
|
+
| `mxup [name] <window>:<command>` | Run a window's named `command` in a copy of its environment, streaming output to your shell |
|
|
296
425
|
|
|
297
426
|
### Flags
|
|
298
427
|
|
|
@@ -345,6 +474,39 @@ send to a pane that's currently running a non-shell process — pass `--force`
|
|
|
345
474
|
to override. Use `--timeout N` to avoid hanging indefinitely on a runaway
|
|
346
475
|
command (exits 124 on timeout).
|
|
347
476
|
|
|
477
|
+
### Named commands (`mxup <config> <window>:<name>`)
|
|
478
|
+
|
|
479
|
+
A window can declare named one-off commands under `commands`. These are handy
|
|
480
|
+
for build/test/lint chores that share a window's working directory and
|
|
481
|
+
environment but shouldn't be baked into its long-running `command`:
|
|
482
|
+
|
|
483
|
+
```yaml
|
|
484
|
+
windows:
|
|
485
|
+
air-backend:
|
|
486
|
+
root: backend
|
|
487
|
+
command: ./start-server.sh
|
|
488
|
+
env:
|
|
489
|
+
APP_ENV: dev
|
|
490
|
+
commands:
|
|
491
|
+
rebuild: ./gradlew build
|
|
492
|
+
test: ./gradlew test
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Run one with:
|
|
496
|
+
|
|
497
|
+
```bash
|
|
498
|
+
mxup air-dev air-backend:rebuild # config name is optional if it resolves implicitly
|
|
499
|
+
mxup air-backend:test
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
Unlike `mxup exec`, a named command does **not** run inside the live tmux
|
|
503
|
+
pane. It executes in a *copy* of that window's environment — the same `root`,
|
|
504
|
+
`setup` snippet, `live_env`, and `env` — as a local subprocess, streaming its
|
|
505
|
+
output straight to the calling shell and exiting with the command's own return
|
|
506
|
+
code. Because it never touches the running pane, it works whether or not the
|
|
507
|
+
session is up, and won't disturb the long-running process. (The window's
|
|
508
|
+
`wait_for` readiness gate is skipped; it guards the main command, not chores.)
|
|
509
|
+
|
|
348
510
|
## Reconciliation
|
|
349
511
|
|
|
350
512
|
`mxup up` compares the declared config against the running tmux session:
|
data/examples/myapp-dev.yml
CHANGED
|
@@ -24,28 +24,59 @@
|
|
|
24
24
|
|
|
25
25
|
session: myapp-dev
|
|
26
26
|
|
|
27
|
+
# Base directory for the windows below. Relative values are resolved against
|
|
28
|
+
# the directory containing this file, so dropping this config into
|
|
29
|
+
# `.mxup/dev.yml` inside the repo would let you write `root: .` instead.
|
|
30
|
+
root: ~/code/myapp
|
|
31
|
+
|
|
27
32
|
setup: |
|
|
28
33
|
# Runs in every window before its command. Replace with whatever your
|
|
29
34
|
# project needs (direnv, nvm, asdf, sourcing a .env, etc.).
|
|
30
35
|
eval "$(direnv export zsh)"
|
|
31
36
|
|
|
37
|
+
# Env vars whose values come from a command and get rotated periodically.
|
|
38
|
+
# Each value is a shell command (run in this file's directory) whose stdout
|
|
39
|
+
# becomes the variable. mxup launches a background watcher; when a command's
|
|
40
|
+
# output changes, every window that lists the var in its `live_env:` block is
|
|
41
|
+
# auto-restarted.
|
|
42
|
+
live_env:
|
|
43
|
+
GRAZIE_TOKEN: cat ~/.grazie-token
|
|
44
|
+
|
|
45
|
+
# Env vars that must be filled in before `mxup up` will start anything.
|
|
46
|
+
# On `up`, mxup checks `myapp-dev.env` (next to this file); if any of these
|
|
47
|
+
# are unset it scaffolds that file with a blank line per var (the descriptions
|
|
48
|
+
# below become comments), then aborts so you can fill them in. Every window
|
|
49
|
+
# sources the file, so the values are available everywhere. Git-ignore *.env.
|
|
50
|
+
required_env:
|
|
51
|
+
STRIPE_SECRET_KEY: Test-mode secret key from the Stripe dashboard
|
|
52
|
+
DATABASE_PASSWORD: Password for the local postgres role
|
|
53
|
+
|
|
32
54
|
windows:
|
|
33
55
|
docker:
|
|
34
|
-
root: ~/code/myapp-infra
|
|
56
|
+
root: ../myapp-infra # = ~/code/myapp-infra
|
|
35
57
|
command: docker compose --profile ${DOCKER_PROFILES:-core} up
|
|
36
58
|
|
|
37
59
|
api:
|
|
38
|
-
root: ~/code/myapp/api
|
|
60
|
+
root: api # = ~/code/myapp/api
|
|
39
61
|
wait_for:
|
|
40
62
|
tcp: localhost:5432
|
|
41
63
|
timeout: 120
|
|
64
|
+
live_env: [GRAZIE_TOKEN] # re-exported via `cat ~/.grazie-token`; changed output restarts this window
|
|
42
65
|
env:
|
|
43
66
|
APP_ENV: development
|
|
44
67
|
DATABASE_URL: postgres://localhost/myapp_dev
|
|
45
68
|
command: ./gradlew run
|
|
69
|
+
# Named one-off chores that share this window's dir + env. Run them without
|
|
70
|
+
# disturbing the server: `mxup myapp-dev api:rebuild` (streams to your
|
|
71
|
+
# shell, exits with the command's own status; works even if the session is
|
|
72
|
+
# down). The window's wait_for gate is skipped for these.
|
|
73
|
+
commands:
|
|
74
|
+
rebuild: ./gradlew build
|
|
75
|
+
test: ./gradlew test
|
|
76
|
+
migrate: ./gradlew flywayMigrate
|
|
46
77
|
|
|
47
78
|
worker:
|
|
48
|
-
root:
|
|
79
|
+
root: worker
|
|
49
80
|
wait_for: localhost:6379 # shorthand for a tcp check (redis)
|
|
50
81
|
env:
|
|
51
82
|
APP_ENV: development
|
|
@@ -54,7 +85,7 @@ windows:
|
|
|
54
85
|
command: ./gradlew run
|
|
55
86
|
|
|
56
87
|
scheduler:
|
|
57
|
-
root:
|
|
88
|
+
root: scheduler
|
|
58
89
|
wait_for: localhost:5432
|
|
59
90
|
env:
|
|
60
91
|
APP_ENV: development
|
|
@@ -62,7 +93,7 @@ windows:
|
|
|
62
93
|
command: ./gradlew run
|
|
63
94
|
|
|
64
95
|
frontend:
|
|
65
|
-
root:
|
|
96
|
+
root: frontend
|
|
66
97
|
wait_for:
|
|
67
98
|
http: http://localhost:8080/health
|
|
68
99
|
timeout: 60
|
|
@@ -71,13 +102,13 @@ windows:
|
|
|
71
102
|
command: pnpm run dev
|
|
72
103
|
|
|
73
104
|
storybook:
|
|
74
|
-
root:
|
|
105
|
+
root: frontend
|
|
75
106
|
command: pnpm run storybook
|
|
76
107
|
|
|
77
108
|
scratch:
|
|
78
109
|
# No `command` -> plain interactive shell, handy for ad-hoc work
|
|
79
110
|
# (e.g. `mxup exec -t myapp-dev:scratch "./gradlew test"`).
|
|
80
|
-
root: ~/code/myapp
|
|
111
|
+
root: . # = ~/code/myapp
|
|
81
112
|
|
|
82
113
|
layouts:
|
|
83
114
|
full:
|
data/lib/mxup/cli.rb
CHANGED
|
@@ -6,11 +6,19 @@ module Mxup
|
|
|
6
6
|
# Argv parser + dispatch. Keeps parsing rules in one place; the actual
|
|
7
7
|
# behaviour lives in Runner and the focused modules it drives.
|
|
8
8
|
class CLI
|
|
9
|
-
COMMANDS = %w[up status down restart layout target exec].freeze
|
|
9
|
+
COMMANDS = %w[up status down restart layout target exec watch].freeze
|
|
10
10
|
|
|
11
11
|
def run(argv)
|
|
12
12
|
args, options = parse(argv.dup)
|
|
13
13
|
command = extract_command(args)
|
|
14
|
+
|
|
15
|
+
# `mxup [config] <window>:<command>` — run a window's named command.
|
|
16
|
+
# Only in the default (no explicit command keyword) path; `restart`/
|
|
17
|
+
# `target` etc. keep their own `session:windows` colon syntax.
|
|
18
|
+
if command == 'up' && (spec = window_command_spec(args))
|
|
19
|
+
return run_window_command(spec, options)
|
|
20
|
+
end
|
|
21
|
+
|
|
14
22
|
dispatch(command, args, options)
|
|
15
23
|
end
|
|
16
24
|
|
|
@@ -60,11 +68,31 @@ module Mxup
|
|
|
60
68
|
when 'restart', 'target' then run_restart_or_target(command, args, options)
|
|
61
69
|
when 'layout' then run_layout(args, options)
|
|
62
70
|
when 'exec' then run_exec(args, options)
|
|
71
|
+
when 'watch' then run_watch(args, options)
|
|
63
72
|
else
|
|
64
73
|
abort "Unknown command: #{command}. Use: #{COMMANDS.join(', ')}"
|
|
65
74
|
end
|
|
66
75
|
end
|
|
67
76
|
|
|
77
|
+
# Detect the `mxup [config] WINDOW:COMMAND` form. Returns
|
|
78
|
+
# [config_name, window, command_name] or nil. The colon-bearing positional
|
|
79
|
+
# is the window:command; any other positional is the config name.
|
|
80
|
+
def window_command_spec(args)
|
|
81
|
+
idx = args.index { |a| a.include?(':') }
|
|
82
|
+
return nil unless idx
|
|
83
|
+
|
|
84
|
+
window, command_name = args[idx].split(':', 2)
|
|
85
|
+
return nil if window.empty? || command_name.nil? || command_name.empty?
|
|
86
|
+
|
|
87
|
+
config_name = args.each_index.reject { |i| i == idx }.map { |i| args[i] }.first
|
|
88
|
+
[config_name, window, command_name]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def run_window_command(spec, options)
|
|
92
|
+
config_name, window_name, command_name = spec
|
|
93
|
+
build_runner(options, config_name).run_command(window_name, command_name)
|
|
94
|
+
end
|
|
95
|
+
|
|
68
96
|
def run_basic(command, args, options)
|
|
69
97
|
name = args.shift
|
|
70
98
|
runner = build_runner(options, name)
|
|
@@ -93,6 +121,18 @@ module Mxup
|
|
|
93
121
|
target_layout ? runner.switch_layout(target_layout) : runner.show_layouts
|
|
94
122
|
end
|
|
95
123
|
|
|
124
|
+
# `mxup watch [name]` — runs the live_env command watcher in the foreground.
|
|
125
|
+
# Mostly used internally: `Runner#up` spawns this detached when the config
|
|
126
|
+
# declares any `live_env`. Useful in the foreground for debugging.
|
|
127
|
+
def run_watch(args, options)
|
|
128
|
+
name = args.shift
|
|
129
|
+
config = load_config(options[:config], name, options[:profile])
|
|
130
|
+
if config.live_env.empty?
|
|
131
|
+
abort "Config has no live_env declared; nothing to watch."
|
|
132
|
+
end
|
|
133
|
+
exit Watcher.new(config).run
|
|
134
|
+
end
|
|
135
|
+
|
|
96
136
|
def run_exec(args, options)
|
|
97
137
|
target = options[:target] || args.shift
|
|
98
138
|
abort 'Usage: mxup exec -t [name:]WINDOW "command"' if target.nil? || target.empty?
|
|
@@ -124,12 +164,17 @@ module Mxup
|
|
|
124
164
|
end
|
|
125
165
|
|
|
126
166
|
# Ambiguous: is `spec` a config name or a window name?
|
|
127
|
-
looks_like_config =
|
|
167
|
+
looks_like_config = config_exists?(spec) ||
|
|
128
168
|
(options[:config] && spec !~ /,/)
|
|
129
169
|
return [spec, args] if looks_like_config
|
|
130
170
|
[nil, [spec] + args]
|
|
131
171
|
end
|
|
132
172
|
|
|
173
|
+
def config_exists?(name)
|
|
174
|
+
File.exist?(File.join(local_config_dir, "#{name}.yml")) ||
|
|
175
|
+
File.exist?(File.join(CONFIG_DIR, "#{name}.yml"))
|
|
176
|
+
end
|
|
177
|
+
|
|
133
178
|
def build_runner(options, name)
|
|
134
179
|
config = load_config(options[:config], name, options[:profile])
|
|
135
180
|
Runner.new(config, dry_run: options[:dry_run], layout: options[:layout])
|
|
@@ -137,7 +182,10 @@ module Mxup
|
|
|
137
182
|
|
|
138
183
|
def load_config(explicit_path, name, profile = nil)
|
|
139
184
|
path = resolve_config(explicit_path, name)
|
|
140
|
-
|
|
185
|
+
unless path
|
|
186
|
+
abort "Config not found. Provide -f path, place config in #{CONFIG_DIR}/, " \
|
|
187
|
+
"or in a local ./#{LOCAL_CONFIG_DIR}/ directory."
|
|
188
|
+
end
|
|
141
189
|
Config.new(path, profile: profile)
|
|
142
190
|
rescue ArgumentError => e
|
|
143
191
|
abort e.message
|
|
@@ -145,26 +193,40 @@ module Mxup
|
|
|
145
193
|
|
|
146
194
|
# Config resolution order:
|
|
147
195
|
# 1. explicit -f path
|
|
148
|
-
# 2.
|
|
149
|
-
# 3.
|
|
150
|
-
# 4.
|
|
196
|
+
# 2. ./.mxup/<name>.yml (project-local, takes precedence)
|
|
197
|
+
# 3. ~/.config/mxup/<name>.yml
|
|
198
|
+
# 4. ./mxup.yml
|
|
199
|
+
# 5. sole *.yml in ./.mxup/
|
|
200
|
+
# 6. sole *.yml in ~/.config/mxup/
|
|
151
201
|
def resolve_config(explicit, name)
|
|
152
202
|
return explicit if explicit && File.exist?(explicit)
|
|
153
203
|
|
|
154
204
|
if name
|
|
155
|
-
|
|
156
|
-
return
|
|
205
|
+
local_named = File.join(local_config_dir, "#{name}.yml")
|
|
206
|
+
return local_named if File.exist?(local_named)
|
|
207
|
+
|
|
208
|
+
global_named = File.join(CONFIG_DIR, "#{name}.yml")
|
|
209
|
+
return global_named if File.exist?(global_named)
|
|
157
210
|
end
|
|
158
211
|
|
|
159
|
-
|
|
160
|
-
return
|
|
212
|
+
bare = File.join(Dir.pwd, 'mxup.yml')
|
|
213
|
+
return bare if File.exist?(bare)
|
|
161
214
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return configs.first if configs.size == 1
|
|
165
|
-
end
|
|
215
|
+
sole = sole_config_in(local_config_dir) || sole_config_in(CONFIG_DIR)
|
|
216
|
+
return sole if sole
|
|
166
217
|
|
|
167
218
|
nil
|
|
168
219
|
end
|
|
220
|
+
|
|
221
|
+
def local_config_dir
|
|
222
|
+
File.join(Dir.pwd, LOCAL_CONFIG_DIR)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def sole_config_in(dir)
|
|
226
|
+
return nil unless Dir.exist?(dir)
|
|
227
|
+
|
|
228
|
+
configs = Dir.glob(File.join(dir, '*.yml'))
|
|
229
|
+
configs.size == 1 ? configs.first : nil
|
|
230
|
+
end
|
|
169
231
|
end
|
|
170
232
|
end
|