node-tmux-cli 0.2.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.
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/dist/config/schema/schema.json +82 -0
- package/dist/config/template/node-tmux-config.json +22 -0
- package/dist/index.js +51 -0
- package/dist/util/commands/handler/handle-help.js +47 -0
- package/dist/util/commands/handler/handle-init.js +29 -0
- package/dist/util/commands/parser/is-initializer.js +4 -0
- package/dist/util/commands/parser/parse-cli-args.js +32 -0
- package/dist/util/commands/parser/parse-config.js +9 -0
- package/dist/util/commands/parser/parse-init.js +12 -0
- package/dist/util/config/config-paths.js +9 -0
- package/dist/util/config/get-config.js +32 -0
- package/dist/util/config/init.js +5 -0
- package/dist/util/config/list-configuration.js +21 -0
- package/dist/util/exit.js +4 -0
- package/dist/util/format-compound-path.js +12 -0
- package/dist/util/layout/line-separator.js +3 -0
- package/dist/util/layout/pad-text.js +3 -0
- package/dist/util/layout/print-padded-line.js +4 -0
- package/dist/util/state/fresh-state.js +4 -0
- package/dist/util/state/handler/handle-no-config-state.js +54 -0
- package/dist/util/state/render-state.js +24 -0
- package/dist/util/state/transition.js +30 -0
- package/dist/util/tmux/attach-session.js +9 -0
- package/dist/util/tmux/destroy-session.js +8 -0
- package/dist/util/tmux/has-session.js +13 -0
- package/dist/util/tmux/is-attached.js +19 -0
- package/dist/util/tmux/start/create-pane.js +22 -0
- package/dist/util/tmux/start/create-window.js +27 -0
- package/dist/util/tmux/start/start-session.js +16 -0
- package/dist/util/user-input/get-user-input.js +8 -0
- package/dist/util/user-input/user-choice/choices.js +37 -0
- package/dist/util/user-input/user-choice/get-parsed-available-options.js +17 -0
- package/dist/util/user-input/user-choice/handle-choice-with-confirmation.js +9 -0
- package/dist/util/user-input/user-choice/handle-user-choice.js +28 -0
- package/dist/util/user-input/user-choice/handler/attach-handler.js +8 -0
- package/dist/util/user-input/user-choice/handler/destroy-handler.js +9 -0
- package/dist/util/user-input/user-choice/handler/exit-handler.js +3 -0
- package/dist/util/user-input/user-choice/handler/other-handler.js +4 -0
- package/dist/util/user-input/user-choice/handler/restart-handler.js +13 -0
- package/dist/util/user-input/user-choice/list-choice-labels.js +12 -0
- package/dist/util/user-input/user-confirmation.js +11 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CassianKnoth
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# configurable `tmux` with `node.js` CLI
|
|
2
|
+
|
|
3
|
+
Create (or interact with) your `tmux` sessions quick and easy, again and again from one simple config file!
|
|
4
|
+
|
|
5
|
+
## 📚 Contents
|
|
6
|
+
|
|
7
|
+
- [Quick start](#quick-start)
|
|
8
|
+
- [How it works](#how-it-works)
|
|
9
|
+
- [Development](#️-development)
|
|
10
|
+
|
|
11
|
+
## 👉 Quick start
|
|
12
|
+
|
|
13
|
+
### Requirements
|
|
14
|
+
|
|
15
|
+
This project was developed with
|
|
16
|
+
|
|
17
|
+
- [node](https://nodejs.org/en) version 22.18.0
|
|
18
|
+
- [tmux](https://github.com/tmux/tmux/wiki) version 3.5a
|
|
19
|
+
|
|
20
|
+
### Getting started
|
|
21
|
+
|
|
22
|
+
Install the `node-tmux` CLI globally:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install -g node-tmux
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Create a config file:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
node-tmux init
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Edit the config file to your liking and run:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
node-tmux
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Default vs custom config file
|
|
41
|
+
|
|
42
|
+
`node-tmux init` will create a `.node-tmux` folder with a `node-tmux-config.json` file in your home directory. Running `node-tmux` will look for that as a default config file (with that name!). You can also run `node-tmux init --local` (with `-l`/`--local` flag) which creates a `node-tmux-config.json` file in your current directory. Whether you create such a config file that way or completely on your own, you can run any such config by passing its path with the `-c`/`--config` flag:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
node-tmux --config ./some/directory/node-tmux-config.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
These files you can also name whatever you desire:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
node-tmux --config ./some/directory/myConfig.json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 🔬 How it works
|
|
55
|
+
|
|
56
|
+
Look at this simple config template:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"$schema": "../schema/schema.json",
|
|
61
|
+
"shell": "bash", // choose bash or zsh for running commands
|
|
62
|
+
"sessions": {
|
|
63
|
+
"mySession": [
|
|
64
|
+
{
|
|
65
|
+
"name": "myWindow",
|
|
66
|
+
"workspacePath": "/absolute/path/to/my-awesome-project",
|
|
67
|
+
"additionalPanes": [
|
|
68
|
+
{
|
|
69
|
+
"name": "server"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"name": "watcher",
|
|
73
|
+
"subPath": "subfolder",
|
|
74
|
+
// results in /absolute/path/to/my-awesome-project/subfolder
|
|
75
|
+
"command": "echo HELLO FROM WATCHER" // runs in the shell you configured above
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
You can list any number of `tmux`-session configs under `sessions`. The porperty key (e. g. `mySession`) is the session name and holds a list of windows which can also hold a list of panes. Each window requires a `name` and a `workspacePath` (where the window's default pane should be initialized). Next to the default pane, the `additionalPanes` require a `name` and _optionally_ a `subPath` (if it should not be initalized at the window's `workspacePath`, which is the default) and also _optionally_ a `command` which should be run at creation (e. g. `echo hello world` or maybe `npm run build:watch`). This command will be run in `bash` or `zsh` depending what you configure as `shell`.
|
|
85
|
+
|
|
86
|
+
If you run the above default config file (e. g. as default config with just `node-tmux` after `node-tmux init`) you will see something like this:
|
|
87
|
+
|
|
88
|
+
<img src="./assets/config-selection.png">
|
|
89
|
+
|
|
90
|
+
If you then provide a valid key, three scenarios are possible:
|
|
91
|
+
|
|
92
|
+
- No session with that config is currently running
|
|
93
|
+
- A session with that config _is_ currently running in a _detached_ state
|
|
94
|
+
- A session with that config _is_ currently running in an _attached_ state
|
|
95
|
+
|
|
96
|
+
If no session is running, you could start it now:
|
|
97
|
+
|
|
98
|
+
<img src="./assets/start-session.png">
|
|
99
|
+
|
|
100
|
+
Every new session starts in _detached_ state.
|
|
101
|
+
|
|
102
|
+
Now you will have more options to deal with the session:
|
|
103
|
+
|
|
104
|
+
<img src="./assets/options.png">
|
|
105
|
+
|
|
106
|
+
> 💡 Notice how the states will be indicated in the session list already
|
|
107
|
+
|
|
108
|
+
- attach --> attaches the session to your current terminal
|
|
109
|
+
- restart --> restarts the session
|
|
110
|
+
- destroy --> kills the session
|
|
111
|
+
- other --> deal with another session
|
|
112
|
+
- exit --> exit CLI
|
|
113
|
+
|
|
114
|
+
> 💡 It is sufficient to type the first letter of each option. Also each option comes with confirmation.
|
|
115
|
+
|
|
116
|
+
Let's attach the session and look at the result in `tmux`:
|
|
117
|
+
|
|
118
|
+
<img src="./assets/tmux.png">
|
|
119
|
+
|
|
120
|
+
Compare this to the config: You can see that the session consists of one window (see `mySession-myWindow` at the bottom left. You can use or ignore the default window next to it). For `myWindow` two `additionalPanes` were configured, thus the window was split two times, resulting in three panes:
|
|
121
|
+
|
|
122
|
+
- A default pane
|
|
123
|
+
- The configured pane named "server"
|
|
124
|
+
- The configured pane named "watcher"
|
|
125
|
+
- The configured command was executed with the configured shell
|
|
126
|
+
|
|
127
|
+
You could actually run the CLI inside the `tmux`-session again:
|
|
128
|
+
|
|
129
|
+
<img src="./assets/attached.png">
|
|
130
|
+
|
|
131
|
+
The session is now correctly marked as _attached_ and the `[a]ttach` option is gone. This is to help keeping things organized – if the session is attached somewhere already, use that.
|
|
132
|
+
|
|
133
|
+
> 💡 You can technically attach a _detached_ session inside a running `tmux`-session, but that might take on confusing inception like nesting. Do as you please.
|
|
134
|
+
|
|
135
|
+
## 🛠️ Development
|
|
136
|
+
|
|
137
|
+
1. 📄 Clone the project
|
|
138
|
+
|
|
139
|
+
- HTTPS:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git clone https://github.com/CassianKnoth/configurable-tmux-node.git
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- SSH:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
git clone git@github.com:CassianKnoth/configurable-tmux-node.git
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
2. ➡️ Navigate into project
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
cd configurable-tmux-node
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
3. 🏗️ Build the project
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
npm run build
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
4. 🚀 Run CLI
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm run dev
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> 💡 Or directly `node dist/index.js` with or wothout arguments/flags
|
|
170
|
+
|
|
171
|
+
## 📈 Improvements?
|
|
172
|
+
|
|
173
|
+
- Reusable config snippets
|
|
174
|
+
- Reference windows or panes from a `snippets` list if you need the same ones in multiple session configs
|
|
175
|
+
|
|
176
|
+
- Configure shell per pane instead of globally
|
|
177
|
+
|
|
178
|
+
- Delete default window
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"additionalProperties": false,
|
|
4
|
+
"definitions": {
|
|
5
|
+
"SessionConfigurations": {
|
|
6
|
+
"$ref": "#/definitions/__type"
|
|
7
|
+
},
|
|
8
|
+
"Shell": {
|
|
9
|
+
"enum": [
|
|
10
|
+
"bash",
|
|
11
|
+
"zsh"
|
|
12
|
+
],
|
|
13
|
+
"type": "string"
|
|
14
|
+
},
|
|
15
|
+
"__type": {
|
|
16
|
+
"additionalProperties": {
|
|
17
|
+
"items": {
|
|
18
|
+
"additionalProperties": false,
|
|
19
|
+
"properties": {
|
|
20
|
+
"additionalPanes": {
|
|
21
|
+
"description": "Optional: Besides the given root, what other panes should be horizontally split next to it in the same window.",
|
|
22
|
+
"items": {
|
|
23
|
+
"additionalProperties": false,
|
|
24
|
+
"properties": {
|
|
25
|
+
"command": {
|
|
26
|
+
"description": "Optional: Command that should be run in ths pane",
|
|
27
|
+
"type": "string"
|
|
28
|
+
},
|
|
29
|
+
"name": {
|
|
30
|
+
"description": "This will be used together with the window name",
|
|
31
|
+
"type": "string"
|
|
32
|
+
},
|
|
33
|
+
"subPath": {
|
|
34
|
+
"description": "Optional: Opens the pane at the given relative path which will be appended to the `workSpacePath`",
|
|
35
|
+
"type": "string"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"required": [
|
|
39
|
+
"name"
|
|
40
|
+
],
|
|
41
|
+
"type": "object"
|
|
42
|
+
},
|
|
43
|
+
"type": "array"
|
|
44
|
+
},
|
|
45
|
+
"name": {
|
|
46
|
+
"description": "Name of the tmux window. This will be used as the prefix for window and pane names.",
|
|
47
|
+
"type": "string"
|
|
48
|
+
},
|
|
49
|
+
"workspacePath": {
|
|
50
|
+
"description": "Absolute path to workspace root.",
|
|
51
|
+
"type": "string"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"required": [
|
|
55
|
+
"name",
|
|
56
|
+
"workspacePath"
|
|
57
|
+
],
|
|
58
|
+
"type": "object"
|
|
59
|
+
},
|
|
60
|
+
"type": "array"
|
|
61
|
+
},
|
|
62
|
+
"type": "object"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"properties": {
|
|
66
|
+
"$schema": {
|
|
67
|
+
"type": "string"
|
|
68
|
+
},
|
|
69
|
+
"sessions": {
|
|
70
|
+
"$ref": "#/definitions/SessionConfigurations"
|
|
71
|
+
},
|
|
72
|
+
"shell": {
|
|
73
|
+
"$ref": "#/definitions/Shell"
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"required": [
|
|
77
|
+
"sessions",
|
|
78
|
+
"shell"
|
|
79
|
+
],
|
|
80
|
+
"type": "object"
|
|
81
|
+
}
|
|
82
|
+
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://unpkg.com/node-tmux@0.2.0/dist/config/schema/schema.json",
|
|
3
|
+
"shell": "bash",
|
|
4
|
+
"sessions": {
|
|
5
|
+
"mySession": [
|
|
6
|
+
{
|
|
7
|
+
"name": "myWindow",
|
|
8
|
+
"workspacePath": "/absolute/path/to/my-awesome-project",
|
|
9
|
+
"additionalPanes": [
|
|
10
|
+
{
|
|
11
|
+
"name": "server"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"name": "watcher",
|
|
15
|
+
"subPath": "src",
|
|
16
|
+
"command": "echo HELLO FROM WATCHER"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { styleText } from 'node:util';
|
|
3
|
+
import { renderState } from './util/state/render-state.js';
|
|
4
|
+
import { exitOut } from './util/exit.js';
|
|
5
|
+
import { printLineSeparator } from './util/layout/line-separator.js';
|
|
6
|
+
import { transition } from './util/state/transition.js';
|
|
7
|
+
import { getConfig } from './util/config/get-config.js';
|
|
8
|
+
import { parseCLiArgs } from './util/commands/parser/parse-cli-args.js';
|
|
9
|
+
import { handleInit } from './util/commands/handler/handle-init.js';
|
|
10
|
+
import { DEFAULT_CONFIG_FILE_PATH } from './util/config/config-paths.js';
|
|
11
|
+
import { handleHelp } from './util/commands/handler/handle-help.js';
|
|
12
|
+
printLineSeparator();
|
|
13
|
+
console.log('🚀 tmux ACTiON!');
|
|
14
|
+
const parsedArgs = parseCLiArgs();
|
|
15
|
+
// handle everything else than 'run'
|
|
16
|
+
switch (parsedArgs.command) {
|
|
17
|
+
case 'init':
|
|
18
|
+
handleInit(parsedArgs.local);
|
|
19
|
+
break;
|
|
20
|
+
case 'help':
|
|
21
|
+
handleHelp();
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
printLineSeparator();
|
|
25
|
+
console.log('⏳ Getting config file path...');
|
|
26
|
+
const configFilePath = parsedArgs.configPath ?? DEFAULT_CONFIG_FILE_PATH;
|
|
27
|
+
const config = getConfig(configFilePath);
|
|
28
|
+
// exit without configurations
|
|
29
|
+
const configuredSessionNames = Object.keys(config.sessions);
|
|
30
|
+
if (configuredSessionNames.length < 1) {
|
|
31
|
+
console.log(styleText('red', '❌ Found no configured sessions'));
|
|
32
|
+
console.log(styleText('cyan', '➡️ Please provide at least 1 configuration in config.ts'));
|
|
33
|
+
exitOut();
|
|
34
|
+
// EXiT
|
|
35
|
+
}
|
|
36
|
+
export const { shell: confiuredShell, sessions: configuredSessions } = config;
|
|
37
|
+
let context = {
|
|
38
|
+
sessionState: 'NO_CONFIG',
|
|
39
|
+
sessionName: '',
|
|
40
|
+
};
|
|
41
|
+
while (true) {
|
|
42
|
+
if (context.sessionState === 'EXIT') {
|
|
43
|
+
exitOut();
|
|
44
|
+
}
|
|
45
|
+
// render state
|
|
46
|
+
renderState(context);
|
|
47
|
+
// handle state transition
|
|
48
|
+
const newContext = await transition(context);
|
|
49
|
+
// set new state
|
|
50
|
+
context = newContext;
|
|
51
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { exitOut } from '../../exit.js';
|
|
2
|
+
import { printLineSeparator } from '../../layout/line-separator.js';
|
|
3
|
+
import { printPaddedLine } from '../../layout/print-padded-line.js';
|
|
4
|
+
const validCommands = [
|
|
5
|
+
{
|
|
6
|
+
name: 'node-tmux',
|
|
7
|
+
description: 'Runs the CLI with default config file',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
name: 'init',
|
|
11
|
+
description: 'Initializes a node-tmux-config.json file in the home directory under .node-tmux',
|
|
12
|
+
flags: [
|
|
13
|
+
{
|
|
14
|
+
name: '-l / --local',
|
|
15
|
+
description: 'Creates a node-tmux-config.json in the current directory instead of the home directory',
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: '-c / --config',
|
|
21
|
+
description: 'Allows you to pass any path to a config file, e. g. "node-tmux -c ./path/to/config.json"',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: '-h / --help',
|
|
25
|
+
description: 'Prints this reference',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
export const handleHelp = () => {
|
|
29
|
+
printLineSeparator();
|
|
30
|
+
console.log('📚 These references might help you:');
|
|
31
|
+
console.log('\n');
|
|
32
|
+
validCommands.forEach((command) => {
|
|
33
|
+
console.group();
|
|
34
|
+
printPaddedLine(command.name, command.description);
|
|
35
|
+
if (command.flags) {
|
|
36
|
+
console.log('🚩 (flags)');
|
|
37
|
+
command.flags.forEach((flag) => {
|
|
38
|
+
console.group();
|
|
39
|
+
printPaddedLine(flag.name, flag.description);
|
|
40
|
+
console.groupEnd();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
console.groupEnd();
|
|
44
|
+
console.log('\n');
|
|
45
|
+
});
|
|
46
|
+
exitOut();
|
|
47
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { printLineSeparator } from '../../layout/line-separator.js';
|
|
4
|
+
import { styleText } from 'node:util';
|
|
5
|
+
import { exitOut } from '../../exit.js';
|
|
6
|
+
import { DEFAULT_CONFIG_FILE_NAME, DEFAULT_CONFIG_FILE_PATH, DEFAULT_CONFIG_HOME_DIR, TEMPLATE_FILE, } from '../../config/config-paths.js';
|
|
7
|
+
export const handleInit = (local) => {
|
|
8
|
+
printLineSeparator();
|
|
9
|
+
// check local
|
|
10
|
+
const configFilePath = local
|
|
11
|
+
? path.join(process.cwd(), DEFAULT_CONFIG_FILE_NAME)
|
|
12
|
+
: DEFAULT_CONFIG_FILE_PATH;
|
|
13
|
+
// check if file exists
|
|
14
|
+
console.log(`👀 Looking for config file at ${configFilePath} ...`);
|
|
15
|
+
if (fs.existsSync(configFilePath)) {
|
|
16
|
+
console.log(styleText('yellow',
|
|
17
|
+
// double space due to emoji consisting of two characters
|
|
18
|
+
`⚠️ Config file already exists at ${configFilePath}.`));
|
|
19
|
+
exitOut();
|
|
20
|
+
}
|
|
21
|
+
// double space due to emoji consisting of two characters
|
|
22
|
+
console.log(`ℹ️ Found no config file at ${configFilePath}`);
|
|
23
|
+
console.log(`⏳ Initializing config file at ${configFilePath} ...`);
|
|
24
|
+
if (!local) {
|
|
25
|
+
fs.mkdirSync(DEFAULT_CONFIG_HOME_DIR, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
fs.copyFileSync(TEMPLATE_FILE, configFilePath);
|
|
28
|
+
exitOut();
|
|
29
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { argv } from 'node:process';
|
|
2
|
+
import { isInitializer } from './is-initializer.js';
|
|
3
|
+
import { parseInit } from './parse-init.js';
|
|
4
|
+
import { parseConfig } from './parse-config.js';
|
|
5
|
+
import { styleText } from 'node:util';
|
|
6
|
+
export const parseCLiArgs = () => {
|
|
7
|
+
const rawArgs = argv.slice(2);
|
|
8
|
+
const firstArg = rawArgs[0];
|
|
9
|
+
// ready to run immedeately
|
|
10
|
+
if (!firstArg) {
|
|
11
|
+
return { command: 'run' };
|
|
12
|
+
}
|
|
13
|
+
// catch unknown args
|
|
14
|
+
if (!isInitializer(firstArg)) {
|
|
15
|
+
console.log(styleText('red', `❌ Command not found: ${firstArg}`));
|
|
16
|
+
return { command: 'help' };
|
|
17
|
+
}
|
|
18
|
+
// or with run with (presumably) path argument
|
|
19
|
+
// if (!isInitializer(firstArg)) {
|
|
20
|
+
// return { command: 'run', configPath: firstArg };
|
|
21
|
+
// }
|
|
22
|
+
switch (firstArg) {
|
|
23
|
+
case 'init':
|
|
24
|
+
return parseInit(rawArgs[1]);
|
|
25
|
+
case '-c':
|
|
26
|
+
case '--config':
|
|
27
|
+
return parseConfig(rawArgs[1]);
|
|
28
|
+
default:
|
|
29
|
+
// help is the only option left
|
|
30
|
+
return { command: 'help' };
|
|
31
|
+
}
|
|
32
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
export const parseConfig = (input) => {
|
|
3
|
+
if (!input) {
|
|
4
|
+
console.log(styleText('red', `❌ Missing config file path`));
|
|
5
|
+
console.log(styleText('cyan', `➡️ Please provide a path to a config file or run node-tmux without any flags to use the default config file instead`));
|
|
6
|
+
return { command: 'help' };
|
|
7
|
+
}
|
|
8
|
+
return { command: 'run', configPath: input };
|
|
9
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
export const parseInit = (input) => {
|
|
3
|
+
if (!input) {
|
|
4
|
+
return { command: 'init' };
|
|
5
|
+
}
|
|
6
|
+
const isLocal = ['-l', '--local'].includes(input);
|
|
7
|
+
if (isLocal) {
|
|
8
|
+
return { command: 'init', local: true };
|
|
9
|
+
}
|
|
10
|
+
console.log(styleText('red', `❌ Unknown argument: ${input}`));
|
|
11
|
+
return { command: 'help' };
|
|
12
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
export const DEFAULT_CONFIG_HOME_DIR = path.join(os.homedir(), '.node-tmux');
|
|
7
|
+
export const DEFAULT_CONFIG_FILE_NAME = 'node-tmux-config.json';
|
|
8
|
+
export const DEFAULT_CONFIG_FILE_PATH = path.join(DEFAULT_CONFIG_HOME_DIR, DEFAULT_CONFIG_FILE_NAME);
|
|
9
|
+
export const TEMPLATE_FILE = path.join(__dirname, '../../config/template/', DEFAULT_CONFIG_FILE_NAME);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import schema from '../../config/schema/schema.json' with { type: 'json' };
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { printLineSeparator } from '../layout/line-separator.js';
|
|
5
|
+
import { styleText } from 'node:util';
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
const Ajv = require('ajv');
|
|
8
|
+
export const ajv = new Ajv({ allErrors: true });
|
|
9
|
+
const validate = ajv.compile(schema);
|
|
10
|
+
export const getConfig = (configFilePath) => {
|
|
11
|
+
printLineSeparator();
|
|
12
|
+
console.log(`👀 Looking for config file at ${configFilePath} ...`);
|
|
13
|
+
if (!fs.existsSync(configFilePath)) {
|
|
14
|
+
console.log(styleText('red', `❌ Config file not found: ${configFilePath}`));
|
|
15
|
+
console.log(styleText('cyan',
|
|
16
|
+
// double space due to emoji consisting of two characters
|
|
17
|
+
'➡️ Run `node-tmux init` to create one, or use -c or --config flag to specify a path'));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const raw = JSON.parse(fs.readFileSync(configFilePath, 'utf-8'));
|
|
21
|
+
if (!validate(raw)) {
|
|
22
|
+
console.error('Invalid node-tmux config:');
|
|
23
|
+
for (const err of validate.errors ?? []) {
|
|
24
|
+
console.error(`- ${err.instancePath} ${err.message}`);
|
|
25
|
+
}
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
console.log(
|
|
29
|
+
// double space due to emoji consisting of two characters
|
|
30
|
+
styleText('green', `⚙️ Found valid config file at ${configFilePath}`));
|
|
31
|
+
return raw;
|
|
32
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { configuredSessions } from '../../index.js';
|
|
3
|
+
import { hasTmuxSession } from '../tmux/has-session.js';
|
|
4
|
+
import { isTmuxAttached } from '../tmux/is-attached.js';
|
|
5
|
+
export const listConfigurations = () => {
|
|
6
|
+
const configuredSessionNames = Object.keys(configuredSessions);
|
|
7
|
+
// double space due to emoji consisting of two characters
|
|
8
|
+
console.log('ℹ️ Available configurations:');
|
|
9
|
+
configuredSessionNames.forEach((configuredSessionName) => {
|
|
10
|
+
const hasSession = hasTmuxSession(configuredSessionName);
|
|
11
|
+
if (hasSession) {
|
|
12
|
+
const isAttached = isTmuxAttached(configuredSessionName);
|
|
13
|
+
const color = isAttached ? 'green' : 'yellow';
|
|
14
|
+
const suffix = isAttached ? '(attached)' : '(detached)';
|
|
15
|
+
console.log(styleText(color, ` - ${configuredSessionName} ${suffix}`));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.log(` - ${configuredSessionName}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const parsePath = (path, pattern, replacement) => {
|
|
2
|
+
const parsedPath = path.replace(pattern, replacement);
|
|
3
|
+
return parsedPath;
|
|
4
|
+
};
|
|
5
|
+
export const formatCompoundPath = (basePath, subPath) => {
|
|
6
|
+
const leadingSlashRegex = /^\/+/;
|
|
7
|
+
const trailingSlashRegex = /\/+$/;
|
|
8
|
+
const parsedBasePath = parsePath(basePath, trailingSlashRegex, '');
|
|
9
|
+
const parsedSubPath = parsePath(subPath, leadingSlashRegex, '');
|
|
10
|
+
const compoundPath = `${parsedBasePath}/${parsedSubPath}`;
|
|
11
|
+
return compoundPath;
|
|
12
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { choices } from '../../user-input/user-choice/choices.js';
|
|
3
|
+
import { getUserInput } from '../../user-input/get-user-input.js';
|
|
4
|
+
import { hasTmuxSession } from '../../tmux/has-session.js';
|
|
5
|
+
import { isTmuxAttached } from '../../tmux/is-attached.js';
|
|
6
|
+
import { tmuxStartSession } from '../../tmux/start/start-session.js';
|
|
7
|
+
import { handleWithConfirmation } from '../../user-input/user-choice/handle-choice-with-confirmation.js';
|
|
8
|
+
import { configuredSessions } from '../../../index.js';
|
|
9
|
+
import { userConfirmation } from '../../user-input/user-confirmation.js';
|
|
10
|
+
export const handleNoConfigState = async (currentContext) => {
|
|
11
|
+
// get user input
|
|
12
|
+
const userInput = await getUserInput('🔑 Session-configuration key or [e]xit: ');
|
|
13
|
+
// handle exit
|
|
14
|
+
if (choices.exit.regex.test(userInput)) {
|
|
15
|
+
const newContext = await handleWithConfirmation(choices.exit, currentContext);
|
|
16
|
+
return newContext;
|
|
17
|
+
}
|
|
18
|
+
// handle other input
|
|
19
|
+
console.log(`👀 Looking for session "${userInput}"...`);
|
|
20
|
+
const configuration = configuredSessions[userInput];
|
|
21
|
+
// - invalid input
|
|
22
|
+
if (!configuration) {
|
|
23
|
+
console.log(styleText('red', `❌ "${userInput}" is not a valid configuration`));
|
|
24
|
+
console.log(styleText('cyan',
|
|
25
|
+
// double space due to emoji consisting of two characters
|
|
26
|
+
`➡️ Check for typos, create it and then try again, or choose another`));
|
|
27
|
+
return { ...currentContext, sessionState: 'NO_CONFIG' };
|
|
28
|
+
}
|
|
29
|
+
const sessionName = userInput;
|
|
30
|
+
console.log(
|
|
31
|
+
// double space due to emoji consisting of two characters
|
|
32
|
+
styleText('green', `⚙️ Found configuration for "${sessionName}"`));
|
|
33
|
+
console.log(`🔍 Checking status of "${sessionName}"...`);
|
|
34
|
+
const hasSession = hasTmuxSession(sessionName);
|
|
35
|
+
// - attached / detached
|
|
36
|
+
if (hasSession) {
|
|
37
|
+
console.log(styleText('yellow', `🏃 Session "${sessionName}" is already running`));
|
|
38
|
+
const isAttached = isTmuxAttached(sessionName);
|
|
39
|
+
if (isAttached) {
|
|
40
|
+
return { sessionState: 'ATTACHED_SESSION', sessionName: sessionName };
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
return { sessionState: 'DETACHED_SESSION', sessionName: sessionName };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// - start
|
|
47
|
+
console.log(`Status: Session "${sessionName}" is inactive`);
|
|
48
|
+
const confirmed = await userConfirmation(`Start session "${sessionName}?"`);
|
|
49
|
+
if (confirmed) {
|
|
50
|
+
tmuxStartSession(sessionName);
|
|
51
|
+
return { sessionState: 'DETACHED_SESSION', sessionName: sessionName };
|
|
52
|
+
}
|
|
53
|
+
return currentContext;
|
|
54
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { styleText } from 'node:util';
|
|
2
|
+
import { listConfigurations } from '../config/list-configuration.js';
|
|
3
|
+
import { printLineSeparator } from '../layout/line-separator.js';
|
|
4
|
+
export const renderState = async ({ sessionState, sessionName }) => {
|
|
5
|
+
printLineSeparator();
|
|
6
|
+
switch (sessionState) {
|
|
7
|
+
case 'NO_CONFIG':
|
|
8
|
+
console.log(styleText('yellow',
|
|
9
|
+
// double space due to emoji consisting of two characters
|
|
10
|
+
'⚠️ Please provide a valid session-configuration key to start or interact with a session'));
|
|
11
|
+
break;
|
|
12
|
+
case 'DETACHED_SESSION':
|
|
13
|
+
console.log(
|
|
14
|
+
// double space due to emoji consisting of two characters
|
|
15
|
+
styleText('yellow', `⚠️ Session "${sessionName}" is running detached`));
|
|
16
|
+
break;
|
|
17
|
+
case 'ATTACHED_SESSION':
|
|
18
|
+
console.log(
|
|
19
|
+
// double space due to emoji consisting of two characters
|
|
20
|
+
styleText('yellow', `⚠️ Session "${sessionName}" is running attached`));
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
listConfigurations();
|
|
24
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { printLineSeparator } from '../layout/line-separator.js';
|
|
2
|
+
import { handleUserChoice } from '../user-input/user-choice/handle-user-choice.js';
|
|
3
|
+
import { handleNoConfigState } from './handler/handle-no-config-state.js';
|
|
4
|
+
export const transition = async (currentContext) => {
|
|
5
|
+
printLineSeparator();
|
|
6
|
+
let newContext = { ...currentContext, sessionState: 'EXIT' };
|
|
7
|
+
switch (currentContext.sessionState) {
|
|
8
|
+
case 'NO_CONFIG':
|
|
9
|
+
newContext = await handleNoConfigState(currentContext);
|
|
10
|
+
break;
|
|
11
|
+
case 'DETACHED_SESSION':
|
|
12
|
+
newContext = await handleUserChoice({
|
|
13
|
+
attach: true,
|
|
14
|
+
restart: true,
|
|
15
|
+
destroy: true,
|
|
16
|
+
other: true,
|
|
17
|
+
exit: true,
|
|
18
|
+
}, currentContext);
|
|
19
|
+
break;
|
|
20
|
+
case 'ATTACHED_SESSION':
|
|
21
|
+
newContext = await handleUserChoice({
|
|
22
|
+
restart: true,
|
|
23
|
+
destroy: true,
|
|
24
|
+
other: true,
|
|
25
|
+
exit: true,
|
|
26
|
+
}, currentContext);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
return newContext;
|
|
30
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { printLineSeparator } from '../layout/line-separator.js';
|
|
3
|
+
export const tmuxAttach = (sessionName) => {
|
|
4
|
+
printLineSeparator();
|
|
5
|
+
console.log(`📌 Attaching session "${sessionName}"`);
|
|
6
|
+
execFileSync('tmux', ['attach', '-t', sessionName], {
|
|
7
|
+
stdio: 'inherit',
|
|
8
|
+
});
|
|
9
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { printLineSeparator } from '../layout/line-separator.js';
|
|
3
|
+
export const tmuxDestroySession = (sessionName) => {
|
|
4
|
+
printLineSeparator();
|
|
5
|
+
console.log(`💥 Destroying session "${sessionName}"...`);
|
|
6
|
+
execSync(`tmux kill-session -t ${sessionName}`);
|
|
7
|
+
console.log(`✅ Session "${sessionName}" was destroyed`);
|
|
8
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
/** Determines whether a given tmux session is running or not
|
|
3
|
+
* @param sessionName Name of the tmux session
|
|
4
|
+
*/
|
|
5
|
+
export const hasTmuxSession = (sessionName) => {
|
|
6
|
+
try {
|
|
7
|
+
execSync(`tmux has-session -t ${sessionName}`, { stdio: 'ignore' });
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
/** Determines whether a given tmux session is attached or not
|
|
3
|
+
* @param sessionName Name of the tmux session
|
|
4
|
+
*/
|
|
5
|
+
export const isTmuxAttached = (sessionName) => {
|
|
6
|
+
const attached = execSync(`tmux display-message -p -t ${sessionName} "#{session_attached}"`)
|
|
7
|
+
.toString()
|
|
8
|
+
.trim();
|
|
9
|
+
return attached === '1';
|
|
10
|
+
// try {
|
|
11
|
+
// execSync(
|
|
12
|
+
// `tmux list-sessions -F "#{session_name} #{session_attached}" | awk -v name="${sessionName}" '$1 == name {print $2}'`,
|
|
13
|
+
// { stdio: 'ignore' },
|
|
14
|
+
// );
|
|
15
|
+
// return true;
|
|
16
|
+
// } catch {
|
|
17
|
+
// return false;
|
|
18
|
+
// }
|
|
19
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { formatCompoundPath } from '../../format-compound-path.js';
|
|
3
|
+
import { confiuredShell } from '../../../index.js';
|
|
4
|
+
export const createPane = (pane, paneIndex, windowIdentifier, basePath) => {
|
|
5
|
+
const paneName = pane.name;
|
|
6
|
+
const splitTarget = `${windowIdentifier}.${paneIndex}`;
|
|
7
|
+
const newPaneIdentifier = `${windowIdentifier}.${paneIndex + 1}`;
|
|
8
|
+
const path = pane.subPath
|
|
9
|
+
? formatCompoundPath(basePath, pane.subPath)
|
|
10
|
+
: basePath;
|
|
11
|
+
console.log(`⏳ 🧩 Creating new Pane "${paneName}"`);
|
|
12
|
+
execSync(`tmux split-window -h -t ${splitTarget} -c ${path}`);
|
|
13
|
+
execSync(`tmux select-pane -t ${newPaneIdentifier} -T ${paneName}`);
|
|
14
|
+
console.log(`✅ Pane "${paneName}" was created`);
|
|
15
|
+
if (pane.command) {
|
|
16
|
+
console.log(`⏳ 🤖 Executing configured command in Pane "${paneName}"...`);
|
|
17
|
+
// replace double quotes because they can mess with shell execution
|
|
18
|
+
execSync(`tmux send-keys -t ${newPaneIdentifier} "${confiuredShell} -c '${pane.command.replace(/"/g, '\\"')}; exec ${confiuredShell}'" C-m`);
|
|
19
|
+
execSync(`tmux send-keys -t ${newPaneIdentifier} Enter`);
|
|
20
|
+
console.log(`✅ Command executed in Pane "${paneName}"`);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { printLineSeparator } from '../../layout/line-separator.js';
|
|
3
|
+
import { createPane } from './create-pane.js';
|
|
4
|
+
export const createWindow = (window, windowIndex, sessionName) => {
|
|
5
|
+
printLineSeparator();
|
|
6
|
+
const windowName = `${sessionName}-${window.name}`;
|
|
7
|
+
// double space due to emoji consisting of two characters
|
|
8
|
+
console.log(`🏗️ Building "${windowName}"...`);
|
|
9
|
+
// double space due to emoji consisting of two characters
|
|
10
|
+
console.log(`⏳ 🖼️ Creating new Window "${windowName}"`);
|
|
11
|
+
execSync(`tmux new-window -t ${sessionName}:${windowIndex + 1} -n ${windowName} -c ${window.workspacePath}`);
|
|
12
|
+
console.log(`✅ Window "${windowName}" was created`);
|
|
13
|
+
const windowIdentifier = `${sessionName}:${windowName}`;
|
|
14
|
+
const originPaneIdentifier = `${windowIdentifier}.0`;
|
|
15
|
+
const originPaneName = 'origin';
|
|
16
|
+
console.log(`⏳ 🧩 Creating new Pane "${originPaneName}"...`);
|
|
17
|
+
execSync(`tmux select-pane -t ${originPaneIdentifier} -T ${originPaneName}`);
|
|
18
|
+
console.log(`✅ Pane "${originPaneName}" was created`);
|
|
19
|
+
if (window.additionalPanes) {
|
|
20
|
+
printLineSeparator();
|
|
21
|
+
// double space due to emoji consisting of two characters
|
|
22
|
+
console.log(`🏗️ Building additional Panes...`);
|
|
23
|
+
window.additionalPanes.forEach((pane, paneIndex) => {
|
|
24
|
+
createPane(pane, paneIndex, windowIdentifier, window.workspacePath);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
import { printLineSeparator } from '../../layout/line-separator.js';
|
|
3
|
+
import { createWindow } from './create-window.js';
|
|
4
|
+
import { configuredSessions } from '../../../index.js';
|
|
5
|
+
export const tmuxStartSession = (sessionName) => {
|
|
6
|
+
const configuredWindows = configuredSessions[sessionName];
|
|
7
|
+
if (!configuredWindows) {
|
|
8
|
+
throw new Error(`Internal: No configuration found for: ${sessionName}`);
|
|
9
|
+
}
|
|
10
|
+
printLineSeparator();
|
|
11
|
+
const initialWindowName = 'default';
|
|
12
|
+
console.log(`⏳ Creating new detached session "${sessionName}"...`);
|
|
13
|
+
execSync(`tmux new -d -s ${sessionName} -n ${initialWindowName} -c "$(pwd)"`);
|
|
14
|
+
console.log(`✅ Session "${sessionName}" was created`);
|
|
15
|
+
configuredWindows.forEach((window, windowIndex) => createWindow(window, windowIndex, sessionName));
|
|
16
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as readline from 'node:readline/promises';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
export const getUserInput = async (prompt) => {
|
|
4
|
+
const rl = readline.createInterface({ input, output });
|
|
5
|
+
const userInput = await rl.question(prompt);
|
|
6
|
+
rl.close();
|
|
7
|
+
return userInput.trim();
|
|
8
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { attachHandler } from './handler/attach-handler.js';
|
|
2
|
+
import { destroyHandler } from './handler/destroy-handler.js';
|
|
3
|
+
import { exitHandler } from './handler/exit-handler.js';
|
|
4
|
+
import { otherHandler } from './handler/other-handler.js';
|
|
5
|
+
import { restartHandler } from './handler/restart-handler.js';
|
|
6
|
+
export const choices = {
|
|
7
|
+
attach: {
|
|
8
|
+
label: 'attach',
|
|
9
|
+
optionLabel: '[a]ttach',
|
|
10
|
+
regex: /a$|attach/i,
|
|
11
|
+
handler: attachHandler,
|
|
12
|
+
},
|
|
13
|
+
destroy: {
|
|
14
|
+
label: 'destroy',
|
|
15
|
+
optionLabel: '[d]estroy',
|
|
16
|
+
regex: /d$|destroy/i,
|
|
17
|
+
handler: destroyHandler,
|
|
18
|
+
},
|
|
19
|
+
restart: {
|
|
20
|
+
label: 'restart',
|
|
21
|
+
optionLabel: '[r]estart',
|
|
22
|
+
regex: /r$|restart/i,
|
|
23
|
+
handler: restartHandler,
|
|
24
|
+
},
|
|
25
|
+
other: {
|
|
26
|
+
label: 'other',
|
|
27
|
+
optionLabel: '[o]ther',
|
|
28
|
+
regex: /o$|other/i,
|
|
29
|
+
handler: otherHandler,
|
|
30
|
+
},
|
|
31
|
+
exit: {
|
|
32
|
+
label: 'exit',
|
|
33
|
+
optionLabel: '[e]xit',
|
|
34
|
+
regex: /e$|exit/i,
|
|
35
|
+
handler: exitHandler,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { choices } from './choices.js';
|
|
2
|
+
function isKeyOf(key, obj) {
|
|
3
|
+
return key in obj;
|
|
4
|
+
}
|
|
5
|
+
export const getParsedAvailableOptions = (availableChoices) => {
|
|
6
|
+
const parsedAvailableOptions = Object.keys(availableChoices)
|
|
7
|
+
.map((choiceKey) => {
|
|
8
|
+
if (isKeyOf(choiceKey, choices)) {
|
|
9
|
+
return choices[choiceKey];
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
})
|
|
15
|
+
.filter((choice) => choice !== undefined);
|
|
16
|
+
return parsedAvailableOptions;
|
|
17
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { getUserInput } from '../get-user-input.js';
|
|
2
|
+
export const handleWithConfirmation = async (choice, currentContext) => {
|
|
3
|
+
const userInput = await getUserInput(`👉 ${choice.label}? [Y/n]: `);
|
|
4
|
+
const confirmed = !/n$|no/i.test(userInput);
|
|
5
|
+
const newContext = confirmed
|
|
6
|
+
? choice.handler(currentContext)
|
|
7
|
+
: currentContext;
|
|
8
|
+
return newContext;
|
|
9
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { listChoiceLabels } from './list-choice-labels.js';
|
|
2
|
+
import { getUserInput } from '../get-user-input.js';
|
|
3
|
+
import { getParsedAvailableOptions } from './get-parsed-available-options.js';
|
|
4
|
+
import { styleText } from 'node:util';
|
|
5
|
+
import { handleWithConfirmation } from './handle-choice-with-confirmation.js';
|
|
6
|
+
/**
|
|
7
|
+
* Present the user with a set of choices and handle the input accordingly.
|
|
8
|
+
*
|
|
9
|
+
* @param availableChoices Opt in parameter for available choices.
|
|
10
|
+
* Value is always `true`, the property just needs to be set. All omitted properties will not be listed as choice.
|
|
11
|
+
*
|
|
12
|
+
* Choices: `attach`, `destroy`, `restart`, `other`, 'exit'
|
|
13
|
+
*/
|
|
14
|
+
export const handleUserChoice = async (availableChoices, currentContext) => {
|
|
15
|
+
const parsedAvailableOptions = getParsedAvailableOptions(availableChoices);
|
|
16
|
+
const question = `👉 The choice is yours: ${listChoiceLabels(parsedAvailableOptions)}`;
|
|
17
|
+
const userInput = await getUserInput(question);
|
|
18
|
+
const matchedOption = parsedAvailableOptions.find((option) => {
|
|
19
|
+
return option.regex.test(userInput);
|
|
20
|
+
});
|
|
21
|
+
// loop until valid input
|
|
22
|
+
if (!matchedOption) {
|
|
23
|
+
console.log(styleText('red', '❌ Invalid input, try again...'));
|
|
24
|
+
return currentContext;
|
|
25
|
+
}
|
|
26
|
+
const newContext = await handleWithConfirmation(matchedOption, currentContext);
|
|
27
|
+
return newContext;
|
|
28
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { tmuxAttach } from '../../../tmux/attach-session.js';
|
|
2
|
+
export const attachHandler = (currentContext) => {
|
|
3
|
+
if (!currentContext) {
|
|
4
|
+
throw new Error('Internal: No currentContext provided');
|
|
5
|
+
}
|
|
6
|
+
tmuxAttach(currentContext.sessionName);
|
|
7
|
+
return { ...currentContext, sessionState: 'EXIT' };
|
|
8
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { freshState } from '../../../state/fresh-state.js';
|
|
2
|
+
import { tmuxDestroySession } from '../../../tmux/destroy-session.js';
|
|
3
|
+
export const destroyHandler = (currentContext) => {
|
|
4
|
+
if (!currentContext) {
|
|
5
|
+
throw new Error('Internal: No currentContext provided');
|
|
6
|
+
}
|
|
7
|
+
tmuxDestroySession(currentContext.sessionName);
|
|
8
|
+
return freshState;
|
|
9
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { tmuxDestroySession } from '../../../tmux/destroy-session.js';
|
|
2
|
+
import { tmuxStartSession } from '../../../tmux/start/start-session.js';
|
|
3
|
+
export const restartHandler = (currentContext) => {
|
|
4
|
+
if (!currentContext) {
|
|
5
|
+
throw new Error('Internal: No currentContext provided');
|
|
6
|
+
}
|
|
7
|
+
const sessionName = currentContext.sessionName;
|
|
8
|
+
// double space due to emoji consisting of two characters
|
|
9
|
+
console.log(`🪃 Restarting session "${sessionName}"`);
|
|
10
|
+
tmuxDestroySession(sessionName);
|
|
11
|
+
tmuxStartSession(sessionName);
|
|
12
|
+
return { ...currentContext, sessionState: 'DETACHED_SESSION' };
|
|
13
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const listChoiceLabels = (choiceList) => {
|
|
2
|
+
if (!choiceList[0]) {
|
|
3
|
+
return '';
|
|
4
|
+
}
|
|
5
|
+
let choiceLabelList = choiceList[0].optionLabel;
|
|
6
|
+
const separator = ', ';
|
|
7
|
+
choiceList
|
|
8
|
+
.slice(1)
|
|
9
|
+
.forEach((choice) => (choiceLabelList = choiceLabelList + separator + choice.optionLabel));
|
|
10
|
+
choiceLabelList = choiceLabelList + ': ';
|
|
11
|
+
return choiceLabelList;
|
|
12
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { getUserInput } from './get-user-input.js';
|
|
2
|
+
/**
|
|
3
|
+
* Prompts the user for yes/no confirmation
|
|
4
|
+
* @param prompt Will be presented to the user as `👉 ${prompt} [Y/n]: `
|
|
5
|
+
* @example "Confirm?" --> `👉 Confirm? [Y/n]: `
|
|
6
|
+
*/
|
|
7
|
+
export const userConfirmation = async (prompt) => {
|
|
8
|
+
const userInput = await getUserInput(`👉 ${prompt} [Y/n]: `);
|
|
9
|
+
const confirmed = !/n$|no/i.test(userInput);
|
|
10
|
+
return confirmed;
|
|
11
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-tmux-cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"bin": {
|
|
11
|
+
"node-tmux-cli": "dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"schema:generate": "rm -rf ./src/config/schema/schema.json && typescript-json-schema tsconfig.json NodeTmuxConfig --topRef --aliasRefs --noExtraProps --required > ./src/config/schema/schema.json",
|
|
15
|
+
"schema:validate": "node .github/scripts/validate-schema.js",
|
|
16
|
+
"prebuild": "npm run schema:generate",
|
|
17
|
+
"build": "rm -rf dist && tsc && cp -r ./src/config dist/ && npm run build:template",
|
|
18
|
+
"build:template": "node scripts/build-template.js",
|
|
19
|
+
"postbuild": "chmod +x dist/index.js",
|
|
20
|
+
"dev": "node dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [],
|
|
23
|
+
"author": "",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.0.8",
|
|
27
|
+
"typescript": "^5.9.3",
|
|
28
|
+
"typescript-json-schema": "^0.67.1"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"ajv": "^8.17.1"
|
|
32
|
+
}
|
|
33
|
+
}
|