monarbor 0.1.0__tar.gz
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.
- monarbor-0.1.0/.github/workflows/publish.yml +27 -0
- monarbor-0.1.0/.gitignore +13 -0
- monarbor-0.1.0/PKG-INFO +127 -0
- monarbor-0.1.0/README.md +117 -0
- monarbor-0.1.0/monarbor/__init__.py +3 -0
- monarbor-0.1.0/monarbor/cli.py +323 -0
- monarbor-0.1.0/monarbor/config.py +91 -0
- monarbor-0.1.0/monarbor/git_ops.py +98 -0
- monarbor-0.1.0/monarbor.egg-info/PKG-INFO +127 -0
- monarbor-0.1.0/monarbor.egg-info/SOURCES.txt +14 -0
- monarbor-0.1.0/monarbor.egg-info/dependency_links.txt +1 -0
- monarbor-0.1.0/monarbor.egg-info/entry_points.txt +2 -0
- monarbor-0.1.0/monarbor.egg-info/requires.txt +3 -0
- monarbor-0.1.0/monarbor.egg-info/top_level.txt +1 -0
- monarbor-0.1.0/pyproject.toml +21 -0
- monarbor-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.11"
|
|
19
|
+
|
|
20
|
+
- name: Install build tools
|
|
21
|
+
run: pip install build
|
|
22
|
+
|
|
23
|
+
- name: Build package
|
|
24
|
+
run: python -m build
|
|
25
|
+
|
|
26
|
+
- name: Publish to PyPI
|
|
27
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
monarbor-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: monarbor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Monarbor - AI 友好的逻辑大仓命令行工具
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: click>=8.0
|
|
8
|
+
Requires-Dist: pyyaml>=6.0
|
|
9
|
+
Requires-Dist: rich>=13.0
|
|
10
|
+
|
|
11
|
+
# Monarbor
|
|
12
|
+
|
|
13
|
+
AI 友好的逻辑大仓命令行工具。一个 `mona.yaml` 配置文件描述所有仓库,一套命令统一管理。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install monarbor
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
开发模式安装:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 快速开始
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 初始化一个逻辑大仓
|
|
31
|
+
monarbor init
|
|
32
|
+
|
|
33
|
+
# 添加仓库
|
|
34
|
+
monarbor add --path business-a/frontend --name "前端项目" --url "https://git.example.com/org/frontend.git"
|
|
35
|
+
|
|
36
|
+
# 拉取所有代码
|
|
37
|
+
monarbor clone
|
|
38
|
+
|
|
39
|
+
# 查看状态
|
|
40
|
+
monarbor status
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 命令一览
|
|
44
|
+
|
|
45
|
+
| 命令 | 说明 |
|
|
46
|
+
|------|------|
|
|
47
|
+
| `monarbor clone` | 拉取大仓下所有项目代码 |
|
|
48
|
+
| `monarbor pull` | 更新所有已 clone 仓库的代码 |
|
|
49
|
+
| `monarbor status` | 显示所有仓库的分支、改动、同步状态 |
|
|
50
|
+
| `monarbor list` | 以树形结构列出所有仓库 |
|
|
51
|
+
| `monarbor exec <cmd>` | 在所有仓库中执行命令 |
|
|
52
|
+
| `monarbor checkout <dev\|test\|prod>` | 批量切换到指定分支类型 |
|
|
53
|
+
| `monarbor init` | 初始化新的逻辑大仓 |
|
|
54
|
+
| `monarbor add` | 向当前大仓添加仓库 |
|
|
55
|
+
|
|
56
|
+
## 常用场景
|
|
57
|
+
|
|
58
|
+
### 拉取所有代码
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# 默认 clone dev 分支
|
|
62
|
+
monarbor clone
|
|
63
|
+
|
|
64
|
+
# clone 测试分支
|
|
65
|
+
monarbor clone -b test
|
|
66
|
+
|
|
67
|
+
# 递归 clone(包括嵌套的子逻辑大仓)
|
|
68
|
+
monarbor clone -r
|
|
69
|
+
|
|
70
|
+
# 只 clone 某个业务线
|
|
71
|
+
monarbor clone --filter business-a
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 批量切换分支
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 全部切到测试分支
|
|
78
|
+
monarbor checkout test
|
|
79
|
+
|
|
80
|
+
# 全部切到生产分支
|
|
81
|
+
monarbor checkout prod
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 批量执行命令
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# 查看每个仓库最近 5 条提交
|
|
88
|
+
monarbor exec "git log --oneline -5"
|
|
89
|
+
|
|
90
|
+
# 全部安装依赖
|
|
91
|
+
monarbor exec "npm install"
|
|
92
|
+
|
|
93
|
+
# 只在某个业务线执行
|
|
94
|
+
monarbor exec "pnpm build" --filter business-a
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 嵌套逻辑大仓
|
|
98
|
+
|
|
99
|
+
当子目录下存在自己的 `mona.yaml` 时,带 `-r` 参数即可递归处理:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
monarbor clone -r # 递归 clone
|
|
103
|
+
monarbor status -r # 递归查看状态
|
|
104
|
+
monarbor list -r # 递归列出树形结构
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## mona.yaml 格式
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
name: "我的大仓"
|
|
111
|
+
description: "大仓描述"
|
|
112
|
+
owner: your-name
|
|
113
|
+
|
|
114
|
+
repos:
|
|
115
|
+
- path: business-a/frontend
|
|
116
|
+
name: "前端项目"
|
|
117
|
+
repo_url: "https://git.example.com/org/frontend.git"
|
|
118
|
+
tech_stack: [typescript, react]
|
|
119
|
+
branches:
|
|
120
|
+
dev: develop
|
|
121
|
+
test: release/test
|
|
122
|
+
prod: main
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
monarbor-0.1.0/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Monarbor
|
|
2
|
+
|
|
3
|
+
AI 友好的逻辑大仓命令行工具。一个 `mona.yaml` 配置文件描述所有仓库,一套命令统一管理。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install monarbor
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
开发模式安装:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## 快速开始
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# 初始化一个逻辑大仓
|
|
21
|
+
monarbor init
|
|
22
|
+
|
|
23
|
+
# 添加仓库
|
|
24
|
+
monarbor add --path business-a/frontend --name "前端项目" --url "https://git.example.com/org/frontend.git"
|
|
25
|
+
|
|
26
|
+
# 拉取所有代码
|
|
27
|
+
monarbor clone
|
|
28
|
+
|
|
29
|
+
# 查看状态
|
|
30
|
+
monarbor status
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## 命令一览
|
|
34
|
+
|
|
35
|
+
| 命令 | 说明 |
|
|
36
|
+
|------|------|
|
|
37
|
+
| `monarbor clone` | 拉取大仓下所有项目代码 |
|
|
38
|
+
| `monarbor pull` | 更新所有已 clone 仓库的代码 |
|
|
39
|
+
| `monarbor status` | 显示所有仓库的分支、改动、同步状态 |
|
|
40
|
+
| `monarbor list` | 以树形结构列出所有仓库 |
|
|
41
|
+
| `monarbor exec <cmd>` | 在所有仓库中执行命令 |
|
|
42
|
+
| `monarbor checkout <dev\|test\|prod>` | 批量切换到指定分支类型 |
|
|
43
|
+
| `monarbor init` | 初始化新的逻辑大仓 |
|
|
44
|
+
| `monarbor add` | 向当前大仓添加仓库 |
|
|
45
|
+
|
|
46
|
+
## 常用场景
|
|
47
|
+
|
|
48
|
+
### 拉取所有代码
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# 默认 clone dev 分支
|
|
52
|
+
monarbor clone
|
|
53
|
+
|
|
54
|
+
# clone 测试分支
|
|
55
|
+
monarbor clone -b test
|
|
56
|
+
|
|
57
|
+
# 递归 clone(包括嵌套的子逻辑大仓)
|
|
58
|
+
monarbor clone -r
|
|
59
|
+
|
|
60
|
+
# 只 clone 某个业务线
|
|
61
|
+
monarbor clone --filter business-a
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### 批量切换分支
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# 全部切到测试分支
|
|
68
|
+
monarbor checkout test
|
|
69
|
+
|
|
70
|
+
# 全部切到生产分支
|
|
71
|
+
monarbor checkout prod
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 批量执行命令
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 查看每个仓库最近 5 条提交
|
|
78
|
+
monarbor exec "git log --oneline -5"
|
|
79
|
+
|
|
80
|
+
# 全部安装依赖
|
|
81
|
+
monarbor exec "npm install"
|
|
82
|
+
|
|
83
|
+
# 只在某个业务线执行
|
|
84
|
+
monarbor exec "pnpm build" --filter business-a
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 嵌套逻辑大仓
|
|
88
|
+
|
|
89
|
+
当子目录下存在自己的 `mona.yaml` 时,带 `-r` 参数即可递归处理:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
monarbor clone -r # 递归 clone
|
|
93
|
+
monarbor status -r # 递归查看状态
|
|
94
|
+
monarbor list -r # 递归列出树形结构
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## mona.yaml 格式
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
name: "我的大仓"
|
|
101
|
+
description: "大仓描述"
|
|
102
|
+
owner: your-name
|
|
103
|
+
|
|
104
|
+
repos:
|
|
105
|
+
- path: business-a/frontend
|
|
106
|
+
name: "前端项目"
|
|
107
|
+
repo_url: "https://git.example.com/org/frontend.git"
|
|
108
|
+
tech_stack: [typescript, react]
|
|
109
|
+
branches:
|
|
110
|
+
dev: develop
|
|
111
|
+
test: release/test
|
|
112
|
+
prod: main
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
MIT
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
"""Monarbor 命令行入口。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich.tree import Tree as RichTree
|
|
11
|
+
|
|
12
|
+
from . import __version__
|
|
13
|
+
from .config import CONFIG_FILENAME, MonorepoConfig, RepoDef, find_nested_monorepos, walk_monorepos
|
|
14
|
+
from .git_ops import (
|
|
15
|
+
ahead_behind,
|
|
16
|
+
checkout,
|
|
17
|
+
clone,
|
|
18
|
+
current_branch,
|
|
19
|
+
fetch,
|
|
20
|
+
is_dirty,
|
|
21
|
+
pull,
|
|
22
|
+
run_in_repo,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
console = Console()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_root(start: Path | None = None) -> Path:
|
|
29
|
+
"""向上查找最近的 mona.yaml 所在目录。"""
|
|
30
|
+
current = (start or Path.cwd()).resolve()
|
|
31
|
+
while True:
|
|
32
|
+
if (current / CONFIG_FILENAME).exists():
|
|
33
|
+
return current
|
|
34
|
+
parent = current.parent
|
|
35
|
+
if parent == current:
|
|
36
|
+
break
|
|
37
|
+
current = parent
|
|
38
|
+
raise click.ClickException(f"未找到 {CONFIG_FILENAME},请在逻辑大仓目录下运行,或使用 monarbor init 初始化")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@click.group()
|
|
42
|
+
@click.version_option(__version__, prog_name="monarbor")
|
|
43
|
+
def main():
|
|
44
|
+
"""Monarbor - AI 友好的逻辑大仓命令行工具"""
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── monarbor clone ───────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@main.command(name="clone")
|
|
52
|
+
@click.option("-r", "--recursive", is_flag=True, help="递归 clone 嵌套的子逻辑大仓")
|
|
53
|
+
@click.option("-b", "--branch-type", type=click.Choice(["dev", "test", "prod"]), default="dev", help="clone 哪个分支类型 (默认 dev)")
|
|
54
|
+
@click.option("--filter", "path_filter", default=None, help="只 clone 路径前缀匹配的仓库 (如 business-a)")
|
|
55
|
+
def clone_repos(recursive: bool, branch_type: str, path_filter: str | None):
|
|
56
|
+
"""拉取大仓下所有项目代码。"""
|
|
57
|
+
root = find_root()
|
|
58
|
+
configs = list(walk_monorepos(root, recursive=recursive))
|
|
59
|
+
total, success, skipped = 0, 0, 0
|
|
60
|
+
|
|
61
|
+
for config in configs:
|
|
62
|
+
if config.root != root:
|
|
63
|
+
console.rule(f"[bold]嵌套大仓: {config.name}[/bold] ({config.root.relative_to(root)})")
|
|
64
|
+
|
|
65
|
+
for repo in config.repos:
|
|
66
|
+
if path_filter and not repo.path.startswith(path_filter):
|
|
67
|
+
continue
|
|
68
|
+
total += 1
|
|
69
|
+
target = config.root / repo.path
|
|
70
|
+
if target.exists() and (target / ".git").exists():
|
|
71
|
+
console.print(f" [dim]跳过[/dim] {repo.path} (已存在)")
|
|
72
|
+
skipped += 1
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
branch = repo.branches.get(branch_type)
|
|
76
|
+
console.print(f" [cyan]克隆[/cyan] {repo.name} → {repo.path} [dim](branch: {branch})[/dim]")
|
|
77
|
+
result = clone(repo.repo_url, target, branch=branch)
|
|
78
|
+
if result.ok:
|
|
79
|
+
success += 1
|
|
80
|
+
console.print(f" [green]✓[/green]")
|
|
81
|
+
else:
|
|
82
|
+
console.print(f" [red]✗ {result.error}[/red]")
|
|
83
|
+
|
|
84
|
+
console.print(f"\n[bold]完成:[/bold] {success} 克隆, {skipped} 跳过, {total - success - skipped} 失败 (共 {total})")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ── monarbor pull ────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@main.command(name="pull")
|
|
91
|
+
@click.option("-r", "--recursive", is_flag=True, help="递归 pull 嵌套大仓")
|
|
92
|
+
def pull_repos(recursive: bool):
|
|
93
|
+
"""拉取所有已 clone 仓库的最新代码。"""
|
|
94
|
+
root = find_root()
|
|
95
|
+
configs = list(walk_monorepos(root, recursive=recursive))
|
|
96
|
+
total, success = 0, 0
|
|
97
|
+
|
|
98
|
+
for config in configs:
|
|
99
|
+
for repo in config.repos:
|
|
100
|
+
target = config.root / repo.path
|
|
101
|
+
if not (target / ".git").exists():
|
|
102
|
+
continue
|
|
103
|
+
total += 1
|
|
104
|
+
console.print(f" [cyan]拉取[/cyan] {repo.name} ({repo.path})")
|
|
105
|
+
result = pull(target)
|
|
106
|
+
if result.ok:
|
|
107
|
+
success += 1
|
|
108
|
+
console.print(f" [green]✓[/green] {result.output or 'Already up to date.'}")
|
|
109
|
+
else:
|
|
110
|
+
console.print(f" [red]✗ {result.error}[/red]")
|
|
111
|
+
|
|
112
|
+
console.print(f"\n[bold]完成:[/bold] {success}/{total} 成功")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── monarbor status ──────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@main.command()
|
|
119
|
+
@click.option("-r", "--recursive", is_flag=True, help="递归显示嵌套大仓")
|
|
120
|
+
@click.option("--fetch/--no-fetch", default=False, help="先 fetch 远端再显示状态")
|
|
121
|
+
def status(recursive: bool, fetch: bool):
|
|
122
|
+
"""显示所有仓库的当前状态。"""
|
|
123
|
+
root = find_root()
|
|
124
|
+
configs = list(walk_monorepos(root, recursive=recursive))
|
|
125
|
+
|
|
126
|
+
table = Table(title="仓库状态")
|
|
127
|
+
table.add_column("项目", style="bold")
|
|
128
|
+
table.add_column("路径", style="dim")
|
|
129
|
+
table.add_column("分支", style="cyan")
|
|
130
|
+
table.add_column("状态")
|
|
131
|
+
table.add_column("同步")
|
|
132
|
+
|
|
133
|
+
for config in configs:
|
|
134
|
+
for repo in config.repos:
|
|
135
|
+
target = config.root / repo.path
|
|
136
|
+
rel_path = str(target.relative_to(root))
|
|
137
|
+
if not (target / ".git").exists():
|
|
138
|
+
table.add_row(repo.name, rel_path, "-", "[dim]未 clone[/dim]", "-")
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
if fetch:
|
|
142
|
+
from .git_ops import fetch as git_fetch
|
|
143
|
+
git_fetch(target)
|
|
144
|
+
|
|
145
|
+
branch = current_branch(target)
|
|
146
|
+
dirty = is_dirty(target)
|
|
147
|
+
dirty_label = "[red]有改动[/red]" if dirty else "[green]干净[/green]"
|
|
148
|
+
a, b = ahead_behind(target)
|
|
149
|
+
sync_parts = []
|
|
150
|
+
if a:
|
|
151
|
+
sync_parts.append(f"[yellow]↑{a}[/yellow]")
|
|
152
|
+
if b:
|
|
153
|
+
sync_parts.append(f"[yellow]↓{b}[/yellow]")
|
|
154
|
+
sync_label = " ".join(sync_parts) if sync_parts else "[green]同步[/green]"
|
|
155
|
+
|
|
156
|
+
table.add_row(repo.name, rel_path, branch, dirty_label, sync_label)
|
|
157
|
+
|
|
158
|
+
console.print(table)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ── monarbor list ────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@main.command(name="list")
|
|
165
|
+
@click.option("-r", "--recursive", is_flag=True, help="递归显示嵌套大仓")
|
|
166
|
+
def list_repos(recursive: bool):
|
|
167
|
+
"""以树形结构列出所有仓库。"""
|
|
168
|
+
root = find_root()
|
|
169
|
+
configs = list(walk_monorepos(root, recursive=recursive))
|
|
170
|
+
|
|
171
|
+
for config in configs:
|
|
172
|
+
tree = RichTree(f"[bold]{config.name}[/bold] [dim]({config.root.relative_to(root) if config.root != root else '.'})[/dim]")
|
|
173
|
+
groups: dict[str, list[RepoDef]] = {}
|
|
174
|
+
for repo in config.repos:
|
|
175
|
+
parts = repo.path.split("/")
|
|
176
|
+
group = parts[0] if len(parts) > 1 else "."
|
|
177
|
+
groups.setdefault(group, []).append(repo)
|
|
178
|
+
|
|
179
|
+
for group_name, repos in sorted(groups.items()):
|
|
180
|
+
if group_name == ".":
|
|
181
|
+
for repo in repos:
|
|
182
|
+
tree.add(f"{repo.name} [dim]{repo.path}[/dim] [cyan]{'|'.join(repo.tech_stack)}[/cyan]")
|
|
183
|
+
else:
|
|
184
|
+
branch = tree.add(f"[bold]{group_name}/[/bold]")
|
|
185
|
+
for repo in repos:
|
|
186
|
+
sub_path = "/".join(repo.path.split("/")[1:])
|
|
187
|
+
branch.add(f"{repo.name} [dim]{sub_path}[/dim] [cyan]{'|'.join(repo.tech_stack)}[/cyan]")
|
|
188
|
+
|
|
189
|
+
nested = find_nested_monorepos(config.root)
|
|
190
|
+
for n in nested:
|
|
191
|
+
tree.add(f"[bold magenta]📦 {n.name}/[/bold magenta] [dim](嵌套大仓)[/dim]")
|
|
192
|
+
|
|
193
|
+
console.print(tree)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# ── monarbor exec ────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@main.command()
|
|
200
|
+
@click.argument("command")
|
|
201
|
+
@click.option("-r", "--recursive", is_flag=True, help="递归执行到嵌套大仓")
|
|
202
|
+
@click.option("--filter", "path_filter", default=None, help="只在路径前缀匹配的仓库中执行")
|
|
203
|
+
def exec_cmd(command: str, recursive: bool, path_filter: str | None):
|
|
204
|
+
"""在所有已 clone 的仓库中执行命令。
|
|
205
|
+
|
|
206
|
+
示例: monarbor exec "git log --oneline -5"
|
|
207
|
+
"""
|
|
208
|
+
root = find_root()
|
|
209
|
+
configs = list(walk_monorepos(root, recursive=recursive))
|
|
210
|
+
|
|
211
|
+
for config in configs:
|
|
212
|
+
for repo in config.repos:
|
|
213
|
+
if path_filter and not repo.path.startswith(path_filter):
|
|
214
|
+
continue
|
|
215
|
+
target = config.root / repo.path
|
|
216
|
+
if not (target / ".git").exists():
|
|
217
|
+
continue
|
|
218
|
+
console.rule(f"[bold]{repo.name}[/bold] ({repo.path})")
|
|
219
|
+
result = run_in_repo(target, command)
|
|
220
|
+
if result.output:
|
|
221
|
+
console.print(result.output)
|
|
222
|
+
if result.error:
|
|
223
|
+
console.print(f"[red]{result.error}[/red]")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# ── monarbor checkout ────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@main.command(name="checkout")
|
|
230
|
+
@click.argument("branch_type", type=click.Choice(["dev", "test", "prod"]))
|
|
231
|
+
@click.option("-r", "--recursive", is_flag=True, help="递归切换嵌套大仓")
|
|
232
|
+
@click.option("--filter", "path_filter", default=None, help="只切换路径前缀匹配的仓库")
|
|
233
|
+
def checkout_repos(branch_type: str, recursive: bool, path_filter: str | None):
|
|
234
|
+
"""将所有仓库切换到指定分支类型 (dev/test/prod)。"""
|
|
235
|
+
root = find_root()
|
|
236
|
+
configs = list(walk_monorepos(root, recursive=recursive))
|
|
237
|
+
|
|
238
|
+
for config in configs:
|
|
239
|
+
for repo in config.repos:
|
|
240
|
+
if path_filter and not repo.path.startswith(path_filter):
|
|
241
|
+
continue
|
|
242
|
+
target = config.root / repo.path
|
|
243
|
+
if not (target / ".git").exists():
|
|
244
|
+
continue
|
|
245
|
+
branch = repo.branches.get(branch_type)
|
|
246
|
+
if not branch:
|
|
247
|
+
console.print(f" [yellow]跳过[/yellow] {repo.name}: 未配置 {branch_type} 分支")
|
|
248
|
+
continue
|
|
249
|
+
console.print(f" [cyan]切换[/cyan] {repo.name} → {branch}")
|
|
250
|
+
result = checkout(target, branch)
|
|
251
|
+
if result.ok:
|
|
252
|
+
console.print(f" [green]✓[/green]")
|
|
253
|
+
else:
|
|
254
|
+
console.print(f" [red]✗ {result.error}[/red]")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
# ── monarbor init ────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@main.command()
|
|
261
|
+
@click.option("--name", prompt="大仓名称", help="逻辑大仓名称")
|
|
262
|
+
@click.option("--owner", prompt="负责人", help="负责人")
|
|
263
|
+
def init(name: str, owner: str):
|
|
264
|
+
"""在当前目录初始化一个新的逻辑大仓。"""
|
|
265
|
+
config_path = Path.cwd() / CONFIG_FILENAME
|
|
266
|
+
if config_path.exists():
|
|
267
|
+
raise click.ClickException(f"{CONFIG_FILENAME} 已存在")
|
|
268
|
+
|
|
269
|
+
content = f"""name: "{name}"
|
|
270
|
+
description: ""
|
|
271
|
+
owner: {owner}
|
|
272
|
+
|
|
273
|
+
repos: []
|
|
274
|
+
"""
|
|
275
|
+
config_path.write_text(content, encoding="utf-8")
|
|
276
|
+
console.print(f"[green]✓[/green] 已创建 {CONFIG_FILENAME}")
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
# ── monarbor add ─────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@main.command()
|
|
283
|
+
@click.option("--path", "repo_path", prompt="仓库路径 (如 business-a/frontend)", help="仓库在大仓中的相对路径")
|
|
284
|
+
@click.option("--name", "repo_name", prompt="项目名称", help="项目显示名称")
|
|
285
|
+
@click.option("--url", "repo_url", prompt="Git 仓库地址", help="Git 仓库 URL")
|
|
286
|
+
@click.option("--dev-branch", default="develop", help="开发分支 (默认 develop)")
|
|
287
|
+
@click.option("--test-branch", default="release/test", help="测试分支 (默认 release/test)")
|
|
288
|
+
@click.option("--prod-branch", default="main", help="生产分支 (默认 main)")
|
|
289
|
+
def add(repo_path: str, repo_name: str, repo_url: str, dev_branch: str, test_branch: str, prod_branch: str):
|
|
290
|
+
"""向当前大仓添加一个仓库。"""
|
|
291
|
+
import yaml
|
|
292
|
+
|
|
293
|
+
root = find_root()
|
|
294
|
+
config_path = root / CONFIG_FILENAME
|
|
295
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
296
|
+
data = yaml.safe_load(f)
|
|
297
|
+
|
|
298
|
+
repos = data.get("repos", [])
|
|
299
|
+
for r in repos:
|
|
300
|
+
if r.get("path") == repo_path:
|
|
301
|
+
raise click.ClickException(f"路径 {repo_path} 已存在于配置中")
|
|
302
|
+
|
|
303
|
+
new_repo = {
|
|
304
|
+
"path": repo_path,
|
|
305
|
+
"name": repo_name,
|
|
306
|
+
"repo_url": repo_url,
|
|
307
|
+
"branches": {
|
|
308
|
+
"dev": dev_branch,
|
|
309
|
+
"test": test_branch,
|
|
310
|
+
"prod": prod_branch,
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
repos.append(new_repo)
|
|
314
|
+
data["repos"] = repos
|
|
315
|
+
|
|
316
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
317
|
+
yaml.dump(data, f, allow_unicode=True, default_flow_style=False, sort_keys=False)
|
|
318
|
+
|
|
319
|
+
console.print(f"[green]✓[/green] 已添加 {repo_name} ({repo_path})")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
if __name__ == "__main__":
|
|
323
|
+
main()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""mona.yaml 配置的加载与解析。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Iterator
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
CONFIG_FILENAME = "mona.yaml"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RepoDef:
|
|
17
|
+
"""一个仓库的定义。"""
|
|
18
|
+
|
|
19
|
+
path: str
|
|
20
|
+
name: str
|
|
21
|
+
repo_url: str
|
|
22
|
+
description: str = ""
|
|
23
|
+
tech_stack: list[str] = field(default_factory=list)
|
|
24
|
+
branches: dict[str, str] = field(default_factory=dict)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def dev_branch(self) -> str:
|
|
28
|
+
return self.branches.get("dev", "develop")
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def test_branch(self) -> str:
|
|
32
|
+
return self.branches.get("test", "release/test")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def prod_branch(self) -> str:
|
|
36
|
+
return self.branches.get("prod", "main")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class MonorepoConfig:
|
|
41
|
+
"""一个逻辑大仓的配置。"""
|
|
42
|
+
|
|
43
|
+
name: str
|
|
44
|
+
owner: str
|
|
45
|
+
root: Path
|
|
46
|
+
description: str = ""
|
|
47
|
+
repos: list[RepoDef] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def load(cls, root: Path) -> MonorepoConfig:
|
|
51
|
+
config_path = root / CONFIG_FILENAME
|
|
52
|
+
if not config_path.exists():
|
|
53
|
+
raise FileNotFoundError(f"未找到配置文件: {config_path}")
|
|
54
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
55
|
+
data = yaml.safe_load(f)
|
|
56
|
+
repos = [RepoDef(**r) for r in data.get("repos", [])]
|
|
57
|
+
return cls(
|
|
58
|
+
name=data.get("name", ""),
|
|
59
|
+
owner=data.get("owner", ""),
|
|
60
|
+
description=data.get("description", ""),
|
|
61
|
+
root=root.resolve(),
|
|
62
|
+
repos=repos,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def find_nested_monorepos(root: Path, exclude_paths: set[str] | None = None) -> list[Path]:
|
|
67
|
+
"""扫描子目录,找到所有嵌套的逻辑大仓。"""
|
|
68
|
+
nested = []
|
|
69
|
+
exclude = exclude_paths or set()
|
|
70
|
+
for entry in sorted(root.iterdir()):
|
|
71
|
+
if not entry.is_dir() or entry.name.startswith("."):
|
|
72
|
+
continue
|
|
73
|
+
if entry.name in exclude:
|
|
74
|
+
continue
|
|
75
|
+
config = entry / CONFIG_FILENAME
|
|
76
|
+
if config.exists():
|
|
77
|
+
nested.append(entry)
|
|
78
|
+
else:
|
|
79
|
+
nested.extend(find_nested_monorepos(entry, exclude))
|
|
80
|
+
return nested
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def walk_monorepos(root: Path, recursive: bool = False) -> Iterator[MonorepoConfig]:
|
|
84
|
+
"""遍历当前大仓,可选递归加载嵌套大仓。"""
|
|
85
|
+
config = MonorepoConfig.load(root)
|
|
86
|
+
yield config
|
|
87
|
+
|
|
88
|
+
if recursive:
|
|
89
|
+
repo_paths = {r.path.split("/")[0] for r in config.repos}
|
|
90
|
+
for nested_root in find_nested_monorepos(root, exclude_paths=repo_paths):
|
|
91
|
+
yield from walk_monorepos(nested_root, recursive=True)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Git 操作封装。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class GitResult:
|
|
12
|
+
ok: bool
|
|
13
|
+
output: str
|
|
14
|
+
error: str = ""
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_git(args: list[str], cwd: Path | None = None) -> GitResult:
|
|
18
|
+
try:
|
|
19
|
+
result = subprocess.run(
|
|
20
|
+
["git", *args],
|
|
21
|
+
cwd=cwd,
|
|
22
|
+
capture_output=True,
|
|
23
|
+
text=True,
|
|
24
|
+
timeout=300,
|
|
25
|
+
)
|
|
26
|
+
return GitResult(
|
|
27
|
+
ok=result.returncode == 0,
|
|
28
|
+
output=result.stdout.strip(),
|
|
29
|
+
error=result.stderr.strip(),
|
|
30
|
+
)
|
|
31
|
+
except subprocess.TimeoutExpired:
|
|
32
|
+
return GitResult(ok=False, output="", error="操作超时 (300s)")
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
return GitResult(ok=False, output="", error="未找到 git 命令,请确认已安装 git")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def clone(repo_url: str, target: Path, branch: str | None = None) -> GitResult:
|
|
38
|
+
args = ["clone"]
|
|
39
|
+
if branch:
|
|
40
|
+
args.extend(["-b", branch])
|
|
41
|
+
args.extend([repo_url, str(target)])
|
|
42
|
+
return run_git(args)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def pull(repo_path: Path) -> GitResult:
|
|
46
|
+
return run_git(["pull"], cwd=repo_path)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def current_branch(repo_path: Path) -> str:
|
|
50
|
+
result = run_git(["rev-parse", "--abbrev-ref", "HEAD"], cwd=repo_path)
|
|
51
|
+
return result.output if result.ok else "(unknown)"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def is_dirty(repo_path: Path) -> bool:
|
|
55
|
+
result = run_git(["status", "--porcelain"], cwd=repo_path)
|
|
56
|
+
return bool(result.output)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def checkout(repo_path: Path, branch: str) -> GitResult:
|
|
60
|
+
return run_git(["checkout", branch], cwd=repo_path)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def fetch(repo_path: Path) -> GitResult:
|
|
64
|
+
return run_git(["fetch", "--all", "--prune"], cwd=repo_path)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def ahead_behind(repo_path: Path) -> tuple[int, int]:
|
|
68
|
+
"""返回 (ahead, behind) 相对于上游的提交数。"""
|
|
69
|
+
result = run_git(
|
|
70
|
+
["rev-list", "--left-right", "--count", "HEAD...@{upstream}"],
|
|
71
|
+
cwd=repo_path,
|
|
72
|
+
)
|
|
73
|
+
if not result.ok:
|
|
74
|
+
return (0, 0)
|
|
75
|
+
parts = result.output.split()
|
|
76
|
+
if len(parts) == 2:
|
|
77
|
+
return (int(parts[0]), int(parts[1]))
|
|
78
|
+
return (0, 0)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def run_in_repo(repo_path: Path, command: str) -> GitResult:
|
|
82
|
+
"""在仓库目录下执行任意 shell 命令。"""
|
|
83
|
+
try:
|
|
84
|
+
result = subprocess.run(
|
|
85
|
+
command,
|
|
86
|
+
cwd=repo_path,
|
|
87
|
+
capture_output=True,
|
|
88
|
+
text=True,
|
|
89
|
+
timeout=120,
|
|
90
|
+
shell=True,
|
|
91
|
+
)
|
|
92
|
+
return GitResult(
|
|
93
|
+
ok=result.returncode == 0,
|
|
94
|
+
output=result.stdout.strip(),
|
|
95
|
+
error=result.stderr.strip(),
|
|
96
|
+
)
|
|
97
|
+
except subprocess.TimeoutExpired:
|
|
98
|
+
return GitResult(ok=False, output="", error="命令超时 (120s)")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: monarbor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Monarbor - AI 友好的逻辑大仓命令行工具
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: click>=8.0
|
|
8
|
+
Requires-Dist: pyyaml>=6.0
|
|
9
|
+
Requires-Dist: rich>=13.0
|
|
10
|
+
|
|
11
|
+
# Monarbor
|
|
12
|
+
|
|
13
|
+
AI 友好的逻辑大仓命令行工具。一个 `mona.yaml` 配置文件描述所有仓库,一套命令统一管理。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install monarbor
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
开发模式安装:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 快速开始
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 初始化一个逻辑大仓
|
|
31
|
+
monarbor init
|
|
32
|
+
|
|
33
|
+
# 添加仓库
|
|
34
|
+
monarbor add --path business-a/frontend --name "前端项目" --url "https://git.example.com/org/frontend.git"
|
|
35
|
+
|
|
36
|
+
# 拉取所有代码
|
|
37
|
+
monarbor clone
|
|
38
|
+
|
|
39
|
+
# 查看状态
|
|
40
|
+
monarbor status
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## 命令一览
|
|
44
|
+
|
|
45
|
+
| 命令 | 说明 |
|
|
46
|
+
|------|------|
|
|
47
|
+
| `monarbor clone` | 拉取大仓下所有项目代码 |
|
|
48
|
+
| `monarbor pull` | 更新所有已 clone 仓库的代码 |
|
|
49
|
+
| `monarbor status` | 显示所有仓库的分支、改动、同步状态 |
|
|
50
|
+
| `monarbor list` | 以树形结构列出所有仓库 |
|
|
51
|
+
| `monarbor exec <cmd>` | 在所有仓库中执行命令 |
|
|
52
|
+
| `monarbor checkout <dev\|test\|prod>` | 批量切换到指定分支类型 |
|
|
53
|
+
| `monarbor init` | 初始化新的逻辑大仓 |
|
|
54
|
+
| `monarbor add` | 向当前大仓添加仓库 |
|
|
55
|
+
|
|
56
|
+
## 常用场景
|
|
57
|
+
|
|
58
|
+
### 拉取所有代码
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# 默认 clone dev 分支
|
|
62
|
+
monarbor clone
|
|
63
|
+
|
|
64
|
+
# clone 测试分支
|
|
65
|
+
monarbor clone -b test
|
|
66
|
+
|
|
67
|
+
# 递归 clone(包括嵌套的子逻辑大仓)
|
|
68
|
+
monarbor clone -r
|
|
69
|
+
|
|
70
|
+
# 只 clone 某个业务线
|
|
71
|
+
monarbor clone --filter business-a
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 批量切换分支
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
# 全部切到测试分支
|
|
78
|
+
monarbor checkout test
|
|
79
|
+
|
|
80
|
+
# 全部切到生产分支
|
|
81
|
+
monarbor checkout prod
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 批量执行命令
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# 查看每个仓库最近 5 条提交
|
|
88
|
+
monarbor exec "git log --oneline -5"
|
|
89
|
+
|
|
90
|
+
# 全部安装依赖
|
|
91
|
+
monarbor exec "npm install"
|
|
92
|
+
|
|
93
|
+
# 只在某个业务线执行
|
|
94
|
+
monarbor exec "pnpm build" --filter business-a
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 嵌套逻辑大仓
|
|
98
|
+
|
|
99
|
+
当子目录下存在自己的 `mona.yaml` 时,带 `-r` 参数即可递归处理:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
monarbor clone -r # 递归 clone
|
|
103
|
+
monarbor status -r # 递归查看状态
|
|
104
|
+
monarbor list -r # 递归列出树形结构
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## mona.yaml 格式
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
name: "我的大仓"
|
|
111
|
+
description: "大仓描述"
|
|
112
|
+
owner: your-name
|
|
113
|
+
|
|
114
|
+
repos:
|
|
115
|
+
- path: business-a/frontend
|
|
116
|
+
name: "前端项目"
|
|
117
|
+
repo_url: "https://git.example.com/org/frontend.git"
|
|
118
|
+
tech_stack: [typescript, react]
|
|
119
|
+
branches:
|
|
120
|
+
dev: develop
|
|
121
|
+
test: release/test
|
|
122
|
+
prod: main
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
.github/workflows/publish.yml
|
|
5
|
+
monarbor/__init__.py
|
|
6
|
+
monarbor/cli.py
|
|
7
|
+
monarbor/config.py
|
|
8
|
+
monarbor/git_ops.py
|
|
9
|
+
monarbor.egg-info/PKG-INFO
|
|
10
|
+
monarbor.egg-info/SOURCES.txt
|
|
11
|
+
monarbor.egg-info/dependency_links.txt
|
|
12
|
+
monarbor.egg-info/entry_points.txt
|
|
13
|
+
monarbor.egg-info/requires.txt
|
|
14
|
+
monarbor.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
monarbor
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "setuptools-scm"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "monarbor"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Monarbor - AI 友好的逻辑大仓命令行工具"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"click>=8.0",
|
|
13
|
+
"pyyaml>=6.0",
|
|
14
|
+
"rich>=13.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
monarbor = "monarbor.cli:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
include = ["monarbor*"]
|
monarbor-0.1.0/setup.cfg
ADDED