@normful/picadillo 1.0.2
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +54 -0
- package/bun.lock +870 -0
- package/cliff.toml +94 -0
- package/extensions/.gitkeep +0 -0
- package/generate-changelog.sh +12 -0
- package/package.json +53 -0
- package/prompts/.gitkeep +0 -0
- package/skills/.gitkeep +0 -0
- package/skills/run-in-tmux/SKILL.md +81 -0
- package/skills/run-in-tmux/scripts/run-in-tmux +258 -0
package/cliff.toml
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# git-cliff ~ configuration file
|
|
2
|
+
# https://git-cliff.org/docs/configuration
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
[changelog]
|
|
6
|
+
# A Tera template to be rendered for each release in the changelog.
|
|
7
|
+
# See https://keats.github.io/tera/docs/#introduction
|
|
8
|
+
body = """
|
|
9
|
+
{% if version %}\
|
|
10
|
+
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
|
11
|
+
{% else %}\
|
|
12
|
+
## [unreleased]
|
|
13
|
+
{% endif %}\
|
|
14
|
+
{% for group, commits in commits | group_by(attribute="group") %}
|
|
15
|
+
### {{ group | striptags | trim | upper_first }}
|
|
16
|
+
{% for commit in commits %}
|
|
17
|
+
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
|
|
18
|
+
{% if commit.breaking %}[**breaking**] {% endif %}\
|
|
19
|
+
{{ commit.message | upper_first }}\
|
|
20
|
+
{% endfor %}
|
|
21
|
+
{% endfor %}
|
|
22
|
+
"""
|
|
23
|
+
# Remove leading and trailing whitespaces from the changelog's body.
|
|
24
|
+
trim = true
|
|
25
|
+
# Render body even when there are no releases to process.
|
|
26
|
+
render_always = true
|
|
27
|
+
# An array of regex based postprocessors to modify the changelog.
|
|
28
|
+
postprocessors = [
|
|
29
|
+
# Replace the placeholder <REPO> with a URL.
|
|
30
|
+
#{ pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" },
|
|
31
|
+
]
|
|
32
|
+
# render body even when there are no releases to process
|
|
33
|
+
# render_always = true
|
|
34
|
+
# output file path
|
|
35
|
+
# output = "test.md"
|
|
36
|
+
|
|
37
|
+
[git]
|
|
38
|
+
# Parse commits according to the conventional commits specification.
|
|
39
|
+
# See https://www.conventionalcommits.org
|
|
40
|
+
conventional_commits = true
|
|
41
|
+
# Exclude commits that do not match the conventional commits specification.
|
|
42
|
+
filter_unconventional = true
|
|
43
|
+
# Require all commits to be conventional.
|
|
44
|
+
# Takes precedence over filter_unconventional.
|
|
45
|
+
require_conventional = false
|
|
46
|
+
# Split commits on newlines, treating each line as an individual commit.
|
|
47
|
+
split_commits = false
|
|
48
|
+
# An array of regex based parsers to modify commit messages prior to further processing.
|
|
49
|
+
commit_preprocessors = [
|
|
50
|
+
# Replace issue numbers with link templates to be updated in `changelog.postprocessors`.
|
|
51
|
+
#{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"},
|
|
52
|
+
# Check spelling of the commit message using https://github.com/crate-ci/typos.
|
|
53
|
+
# If the spelling is incorrect, it will be fixed automatically.
|
|
54
|
+
#{ pattern = '.*', replace_command = 'typos --write-changes -' },
|
|
55
|
+
]
|
|
56
|
+
# Prevent commits that are breaking from being excluded by commit parsers.
|
|
57
|
+
protect_breaking_commits = false
|
|
58
|
+
# An array of regex based parsers for extracting data from the commit message.
|
|
59
|
+
# Assigns commits to groups.
|
|
60
|
+
# Optionally sets the commit's scope and can decide to exclude commits from further processing.
|
|
61
|
+
commit_parsers = [
|
|
62
|
+
{ message = "^feat", group = "<!-- 0 -->๐ Features" },
|
|
63
|
+
{ message = "^fix", group = "<!-- 1 -->๐ Bug Fixes" },
|
|
64
|
+
{ message = "^doc", group = "<!-- 3 -->๐ Documentation" },
|
|
65
|
+
{ message = "^perf", group = "<!-- 4 -->โก Performance" },
|
|
66
|
+
{ message = "^refactor", group = "<!-- 2 -->๐ Refactor" },
|
|
67
|
+
{ message = "^style", group = "<!-- 5 -->๐จ Styling" },
|
|
68
|
+
{ message = "^test", group = "<!-- 6 -->๐งช Testing" },
|
|
69
|
+
{ message = "^chore\\(release\\): prepare for", skip = true },
|
|
70
|
+
{ message = "^chore\\(deps.*\\)", skip = true },
|
|
71
|
+
{ message = "^chore\\(pr\\)", skip = true },
|
|
72
|
+
{ message = "^chore\\(pull\\)", skip = true },
|
|
73
|
+
{ message = "^chore|^ci", group = "<!-- 7 -->โ๏ธ Miscellaneous Tasks" },
|
|
74
|
+
{ body = ".*security", group = "<!-- 8 -->๐ก๏ธ Security" },
|
|
75
|
+
{ message = "^revert", group = "<!-- 9 -->โ๏ธ Revert" },
|
|
76
|
+
{ message = ".*", group = "<!-- 10 -->๐ผ Other" },
|
|
77
|
+
]
|
|
78
|
+
# Exclude commits that are not matched by any commit parser.
|
|
79
|
+
filter_commits = false
|
|
80
|
+
# Fail on a commit that is not matched by any commit parser.
|
|
81
|
+
fail_on_unmatched_commit = false
|
|
82
|
+
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
|
|
83
|
+
link_parsers = []
|
|
84
|
+
# Include only the tags that belong to the current branch.
|
|
85
|
+
use_branch_tags = false
|
|
86
|
+
# Order releases topologically instead of chronologically.
|
|
87
|
+
topo_order = false
|
|
88
|
+
# Order commits topologically instead of chronologically.
|
|
89
|
+
topo_order_commits = true
|
|
90
|
+
# Order of commits in each group/release within the changelog.
|
|
91
|
+
# Allowed values: newest, oldest
|
|
92
|
+
sort_commits = "oldest"
|
|
93
|
+
# Process submodules commits
|
|
94
|
+
recurse_submodules = false
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -o errexit
|
|
4
|
+
set -o pipefail
|
|
5
|
+
|
|
6
|
+
LATEST_VERSION=$(node -p -e "require('./package.json').version")
|
|
7
|
+
|
|
8
|
+
if [ "$1" = "stdout" ]; then
|
|
9
|
+
git-cliff --output - --unreleased --tag $LATEST_VERSION
|
|
10
|
+
else
|
|
11
|
+
git-cliff --output './CHANGELOG.md' --tag $LATEST_VERSION
|
|
12
|
+
fi
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@normful/picadillo",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Norman's pi coding agent commands, skills, extensions",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi",
|
|
8
|
+
"pi-coding-agent",
|
|
9
|
+
"coding-assistant",
|
|
10
|
+
"extensions",
|
|
11
|
+
"skills",
|
|
12
|
+
"ai",
|
|
13
|
+
"pi-package",
|
|
14
|
+
"pi-extension",
|
|
15
|
+
"pi-skill"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://github.com/normful/picadillo#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/normful/picadillo/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/normful/picadillo.git"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "normful",
|
|
27
|
+
"type": "commonjs",
|
|
28
|
+
"main": "none",
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "bun test",
|
|
31
|
+
"release": "release-it"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
35
|
+
"@mariozechner/pi-ai": "*",
|
|
36
|
+
"@mariozechner/pi-tui": "*",
|
|
37
|
+
"typescript": "^5"
|
|
38
|
+
},
|
|
39
|
+
"pi": {
|
|
40
|
+
"prompts": [],
|
|
41
|
+
"extensions": [],
|
|
42
|
+
"skills": [
|
|
43
|
+
"./run-in-tmux/SKILL.md"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@types/bun": "latest",
|
|
48
|
+
"release-it": "^19.2.4"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/prompts/.gitkeep
ADDED
|
File without changes
|
package/skills/.gitkeep
ADDED
|
File without changes
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: run-in-tmux
|
|
3
|
+
description: Use when the user wants to run commands in a new tmux session with split panes. Triggers on requests like "run X in tmux", "start a dev environment in tmux", or "open multiple terminals in tmux".
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# run-in-tmux
|
|
7
|
+
|
|
8
|
+
Creates a tmux session with one or more commands running in separate split panes. Automatically detects the git repository root and generates a unique session name.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- User wants to run development commands (e.g., `npm run dev`, `cargo watch`)
|
|
13
|
+
- User needs multiple terminal panes running different commands
|
|
14
|
+
- User wants to start a background process that persists after terminal closes
|
|
15
|
+
|
|
16
|
+
## How to run
|
|
17
|
+
|
|
18
|
+
The script is located at `scripts/run-in-tmux`. Execute it using `uv run`:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
uv run scripts/run-in-tmux -c "npm run dev"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# Single command (JSON output)
|
|
28
|
+
uv run scripts/run-in-tmux -c "npm run dev" --json
|
|
29
|
+
|
|
30
|
+
# Multiple commands (each gets its own pane)
|
|
31
|
+
uv run scripts/run-in-tmux -c "npm run dev" -c "npm test" --json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Options
|
|
35
|
+
|
|
36
|
+
| Flag | Description |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `-c, --command` | Command to run (required, repeatable) |
|
|
39
|
+
| `--json` | Output JSON to stdout |
|
|
40
|
+
| `--plain` | Line-based plain text output |
|
|
41
|
+
| `-q, --quiet` | Suppress non-essential output |
|
|
42
|
+
| `-v, --verbose` | Show debug details |
|
|
43
|
+
| `--no-color` | Disable colored output |
|
|
44
|
+
| `--version` | Show version and exit |
|
|
45
|
+
|
|
46
|
+
## Output
|
|
47
|
+
|
|
48
|
+
### JSON format (`--json`)
|
|
49
|
+
```bash
|
|
50
|
+
uv run scripts/run-in-tmux -c "echo hello" -c "echo world" --json
|
|
51
|
+
```
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"success": true,
|
|
55
|
+
"session_name": "picadillo-1f4f1afa",
|
|
56
|
+
"commands": ["echo hello", "echo world"],
|
|
57
|
+
"panes": 2,
|
|
58
|
+
"working_directory": "/Users/norman/code/picadillo",
|
|
59
|
+
"attach_command": "tmux attach -t picadillo-1f4f1afa",
|
|
60
|
+
"peek_commands": [
|
|
61
|
+
"tmux capture-pane -t picadillo-1f4f1afa.0 -p",
|
|
62
|
+
"tmux capture-pane -t picadillo-1f4f1afa.1 -p"
|
|
63
|
+
],
|
|
64
|
+
"kill_command": "tmux kill-session -t picadillo-1f4f1afa"
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Session naming
|
|
69
|
+
|
|
70
|
+
Session names follow the pattern `<repo-slug>-<hash>`:
|
|
71
|
+
- `repo-slug`: First 8 characters of the git repository basename
|
|
72
|
+
- `hash`: 8-character MD5 hash of the repo path + commands
|
|
73
|
+
|
|
74
|
+
This ensures unique sessions for different commands in the same repo.
|
|
75
|
+
|
|
76
|
+
## Requirements
|
|
77
|
+
|
|
78
|
+
- Python 3.10+
|
|
79
|
+
- tmux installed and running
|
|
80
|
+
- Must be in a git repository
|
|
81
|
+
- Dependencies: `typer`, `rich` (auto-installed via uv)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = ["typer", "rich"]
|
|
5
|
+
# ///
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
run-in-tmux: Create a new tmux session for a project and run a command inside it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.3.0"
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import typer
|
|
19
|
+
from rich.console import Console
|
|
20
|
+
|
|
21
|
+
app = typer.Typer(add_completion=False)
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_version() -> str:
|
|
26
|
+
return __version__
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_git_root() -> Path | None:
|
|
30
|
+
"""Get the root directory of the current Git repository."""
|
|
31
|
+
try:
|
|
32
|
+
result = subprocess.run(
|
|
33
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
34
|
+
capture_output=True,
|
|
35
|
+
text=True,
|
|
36
|
+
check=True,
|
|
37
|
+
)
|
|
38
|
+
return Path(result.stdout.strip())
|
|
39
|
+
except subprocess.CalledProcessError:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_repo_name(path: Path) -> str:
|
|
44
|
+
"""Get short slug from repo basename (first 16 chars, lowercase)."""
|
|
45
|
+
return path.name[:16].lower()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def generate_session_name(repo_path: Path, commands: list[str]) -> str:
|
|
49
|
+
"""Generate unique session name: <slug>-<hash> where hash = MD5(repo_path + commands)."""
|
|
50
|
+
# Combine repo absolute path with all commands concatenated
|
|
51
|
+
combined = str(repo_path.absolute()) + "".join(commands)
|
|
52
|
+
# MD5 hash
|
|
53
|
+
md5_hash = hashlib.md5(combined.encode()).hexdigest()
|
|
54
|
+
# Truncate to 8 chars
|
|
55
|
+
short_hash = md5_hash[:8]
|
|
56
|
+
# Get repo slug (first 8 chars of basename)
|
|
57
|
+
slug = get_repo_name(repo_path)
|
|
58
|
+
return f"{slug}-{short_hash}"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def session_exists(session_name: str) -> bool:
|
|
62
|
+
"""Check if a tmux session with the given name already exists."""
|
|
63
|
+
result = subprocess.run(
|
|
64
|
+
["tmux", "has-session", "-t", session_name],
|
|
65
|
+
capture_output=True,
|
|
66
|
+
)
|
|
67
|
+
return result.returncode == 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def create_tmux_session(
|
|
71
|
+
session_name: str, working_dir: Path, commands: list[str]
|
|
72
|
+
) -> bool:
|
|
73
|
+
"""Create a new tmux session with each command in its own split pane."""
|
|
74
|
+
try:
|
|
75
|
+
# Create session with first command in pane 0
|
|
76
|
+
# -s: session name
|
|
77
|
+
# -c: working directory
|
|
78
|
+
subprocess.run(
|
|
79
|
+
["tmux", "new-session", "-d", "-s", session_name, "-c", str(working_dir)],
|
|
80
|
+
check=True,
|
|
81
|
+
)
|
|
82
|
+
# Send first command to pane 0
|
|
83
|
+
subprocess.run(
|
|
84
|
+
["tmux", "send-keys", "-t", f"{session_name}.0", commands[0], "C-m"],
|
|
85
|
+
check=True,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# For each subsequent command, create a new pane and run the command
|
|
89
|
+
for i, cmd in enumerate(commands[1:], start=1):
|
|
90
|
+
# Split the window horizontally
|
|
91
|
+
subprocess.run(
|
|
92
|
+
["tmux", "split-window", "-h", "-t", session_name],
|
|
93
|
+
check=True,
|
|
94
|
+
)
|
|
95
|
+
# Select the new pane and send the command
|
|
96
|
+
subprocess.run(
|
|
97
|
+
["tmux", "send-keys", "-t", f"{session_name}.{i}", cmd, "C-m"],
|
|
98
|
+
check=True,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Arrange panes in a tiled layout
|
|
102
|
+
subprocess.run(
|
|
103
|
+
["tmux", "select-layout", "-t", session_name, "tiled"],
|
|
104
|
+
check=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return True
|
|
108
|
+
except subprocess.CalledProcessError as e:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def output_human(session_name: str, commands: list[str], working_dir: Path) -> None:
|
|
113
|
+
"""Output human-readable confirmation."""
|
|
114
|
+
console.print(f"[green]โ[/green] Session created: [bold]{session_name}[/bold]")
|
|
115
|
+
console.print(f" Commands ({len(commands)}):")
|
|
116
|
+
for i, cmd in enumerate(commands, 1):
|
|
117
|
+
console.print(f" {i}. {cmd}")
|
|
118
|
+
console.print(f" Working directory: {working_dir}")
|
|
119
|
+
console.print()
|
|
120
|
+
console.print("[bold]Attach to session:[/bold]")
|
|
121
|
+
console.print(f" tmux attach -t {session_name}")
|
|
122
|
+
console.print()
|
|
123
|
+
console.print("[bold]Peek pane output:[/bold]")
|
|
124
|
+
for i in range(len(commands)):
|
|
125
|
+
console.print(f" tmux capture-pane -t {session_name}.{i} -p")
|
|
126
|
+
console.print()
|
|
127
|
+
console.print("[bold]Kill session:[/bold]")
|
|
128
|
+
console.print(f" tmux kill-session -t {session_name}")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def output_json(session_name: str, commands: list[str], working_dir: Path) -> None:
|
|
132
|
+
"""Output JSON for scripting."""
|
|
133
|
+
import json
|
|
134
|
+
|
|
135
|
+
output = {
|
|
136
|
+
"success": True,
|
|
137
|
+
"session_name": session_name,
|
|
138
|
+
"commands": commands,
|
|
139
|
+
"panes": len(commands),
|
|
140
|
+
"working_directory": str(working_dir),
|
|
141
|
+
"attach_command": f"tmux attach -t {session_name}",
|
|
142
|
+
"peek_commands": [
|
|
143
|
+
f"tmux capture-pane -t {session_name}.{i} -p" for i in range(len(commands))
|
|
144
|
+
],
|
|
145
|
+
"kill_command": f"tmux kill-session -t {session_name}",
|
|
146
|
+
}
|
|
147
|
+
print(json.dumps(output))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def output_plain(session_name: str, commands: list[str], working_dir: Path) -> None:
|
|
151
|
+
"""Output line-based plain text."""
|
|
152
|
+
print(session_name)
|
|
153
|
+
for cmd in commands:
|
|
154
|
+
print(cmd)
|
|
155
|
+
print(str(working_dir))
|
|
156
|
+
print(f"tmux attach -t {session_name}")
|
|
157
|
+
for i in range(len(commands)):
|
|
158
|
+
print(f"tmux capture-pane -t {session_name}.{i} -p")
|
|
159
|
+
print(f"tmux kill-session -t {session_name}")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def error_exit(message: str, json_output: bool = False) -> None:
|
|
163
|
+
"""Print error to both stdout and stderr, exit with code 1."""
|
|
164
|
+
if json_output:
|
|
165
|
+
import json
|
|
166
|
+
|
|
167
|
+
print(json.dumps({"success": False, "error": message}), file=sys.stderr)
|
|
168
|
+
else:
|
|
169
|
+
# Use styled output for stderr - use Console with stderr, force color
|
|
170
|
+
err_console = Console(
|
|
171
|
+
stderr=True, force_terminal=True, no_color=console.no_color
|
|
172
|
+
)
|
|
173
|
+
err_console.print(f"[red]Error:[/red] {message}")
|
|
174
|
+
raise typer.Exit(code=1)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@app.command()
|
|
178
|
+
def main(
|
|
179
|
+
command: list[str] | None = typer.Option(
|
|
180
|
+
None,
|
|
181
|
+
"--command",
|
|
182
|
+
"-c",
|
|
183
|
+
help="Command to run inside tmux session. Repeat for multiple sequential commands.",
|
|
184
|
+
),
|
|
185
|
+
json: bool = typer.Option(False, "--json", help="Output JSON to stdout"),
|
|
186
|
+
plain: bool = typer.Option(False, "--plain", help="Line-based plain text output"),
|
|
187
|
+
quiet: bool = typer.Option(
|
|
188
|
+
False, "-q", "--quiet", help="Suppress non-essential output"
|
|
189
|
+
),
|
|
190
|
+
verbose: bool = typer.Option(False, "-v", "--verbose", help="Show debug details"),
|
|
191
|
+
no_color: bool = typer.Option(False, "--no-color", help="Disable colored output"),
|
|
192
|
+
version: bool = typer.Option(False, "--version", help="Show version and exit"),
|
|
193
|
+
):
|
|
194
|
+
"""
|
|
195
|
+
Create a new tmux session for a project and run commands in separate panes.
|
|
196
|
+
|
|
197
|
+
Each command runs in its own split pane of the first window.
|
|
198
|
+
|
|
199
|
+
Examples:
|
|
200
|
+
|
|
201
|
+
run-in-tmux -c "npm run dev"
|
|
202
|
+
run-in-tmux -c "npm run dev" -c "npm test"
|
|
203
|
+
run-in-tmux -c "echo one" -c "echo two" -c "echo three"
|
|
204
|
+
"""
|
|
205
|
+
# Handle version flag first
|
|
206
|
+
if version:
|
|
207
|
+
print(get_version())
|
|
208
|
+
raise typer.Exit(code=0)
|
|
209
|
+
|
|
210
|
+
# Handle no-color
|
|
211
|
+
if no_color:
|
|
212
|
+
console.no_color = True
|
|
213
|
+
|
|
214
|
+
# Validate required --command
|
|
215
|
+
if command is None or len(command) == 0:
|
|
216
|
+
error_exit('--command is required. Usage: run-in-tmux -c "npm test"', False)
|
|
217
|
+
|
|
218
|
+
# Check if in a git repo
|
|
219
|
+
git_root = get_git_root()
|
|
220
|
+
if git_root is None:
|
|
221
|
+
error_exit("not in a git repository. Run from a project root.", json)
|
|
222
|
+
|
|
223
|
+
if verbose:
|
|
224
|
+
console.print(f"[dim]Git root: {git_root}[/dim]")
|
|
225
|
+
|
|
226
|
+
# Generate session name
|
|
227
|
+
session_name = generate_session_name(git_root, command)
|
|
228
|
+
|
|
229
|
+
if verbose:
|
|
230
|
+
console.print(f"[dim]Session name: {session_name}[/dim]")
|
|
231
|
+
|
|
232
|
+
# Check if session already exists
|
|
233
|
+
if session_exists(session_name):
|
|
234
|
+
error_exit(
|
|
235
|
+
f"session {session_name} already exists.\nUse tmux attach -t {session_name} to reconnect.",
|
|
236
|
+
json,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Create the tmux session
|
|
240
|
+
success = create_tmux_session(session_name, git_root, command)
|
|
241
|
+
if not success:
|
|
242
|
+
error_exit("failed to create tmux session.", json)
|
|
243
|
+
|
|
244
|
+
# Output based on flags
|
|
245
|
+
if json:
|
|
246
|
+
output_json(session_name, command, git_root)
|
|
247
|
+
elif plain:
|
|
248
|
+
output_plain(session_name, command, git_root)
|
|
249
|
+
else:
|
|
250
|
+
output_human(session_name, command, git_root)
|
|
251
|
+
|
|
252
|
+
raise typer.Exit(code=0)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
if __name__ == "__main__":
|
|
256
|
+
app()
|
|
257
|
+
|
|
258
|
+
# vim: ft=python
|