piagentsync 0.0.1rc0__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.
@@ -0,0 +1,8 @@
1
+ # piagentsync configuration
2
+ # Copy to .env and fill in values
3
+
4
+ # Path to your Obsidian vault (git repo)
5
+ PIAGENTSYNC_VAULT_PATH=~/vault
6
+
7
+ # Path to global OpenCode config
8
+ PIAGENTSYNC_GLOBAL_OPENCODE_PATH=~/.config/opencode
@@ -0,0 +1,203 @@
1
+ #!/usr/bin/env python3
2
+ """Generate changelog entry from conventional commits and compute next version.
3
+
4
+ This script:
5
+ 1. Finds the latest stable semantic version tag (vX.Y.Z)
6
+ 2. Computes the next version based on the BUMP input (MAJOR/MINOR/PATCH/RC)
7
+ 3. Gathers conventional commit messages since that tag
8
+ 4. Categorizes and formats them into a changelog entry
9
+ 5. Prepends the entry to CHANGELOG.md
10
+ 6. Outputs GITHUB_OUTPUT variables: new_version and tag_name
11
+ """
12
+
13
+ import os
14
+ import re
15
+ import subprocess
16
+ import sys
17
+ from datetime import datetime
18
+ from pathlib import Path
19
+
20
+
21
+ def run_git(*args):
22
+ """Run a git command and return stdout."""
23
+ result = subprocess.run(["git"] + list(args), capture_output=True, text=True, check=True)
24
+ return result.stdout.strip()
25
+
26
+
27
+ def get_latest_stable_tag():
28
+ """Get the latest vX.Y.Z tag (ignoring -rc tags)."""
29
+ try:
30
+ tags = run_git("tag", "-l", "v*", "--sort=-v:refname").splitlines()
31
+ except subprocess.CalledProcessError:
32
+ return None
33
+
34
+ for tag in tags:
35
+ # Match only pure semver without suffix
36
+ if re.fullmatch(r"v\d+\.\d+\.\d+", tag):
37
+ return tag
38
+ return None
39
+
40
+
41
+ def parse_version(tag):
42
+ """Parse vX.Y.Z into (major, minor, patch)."""
43
+ if tag is None:
44
+ return (0, 0, 0)
45
+ m = re.match(r"v(\d+)\.(\d+)\.(\d+)", tag)
46
+ if not m:
47
+ return (0, 0, 0)
48
+ return tuple(int(x) for x in m.groups())
49
+
50
+
51
+ def bump_version(major, minor, patch, bump_type):
52
+ """Return (new_major, new_minor, new_patch, suffix)."""
53
+ suffix = ""
54
+ if bump_type == "MAJOR":
55
+ major += 1
56
+ minor = 0
57
+ patch = 0
58
+ elif bump_type == "MINOR":
59
+ minor += 1
60
+ patch = 0
61
+ elif bump_type == "PATCH":
62
+ patch += 1
63
+ elif bump_type == "RC":
64
+ patch += 1
65
+ suffix = "-rc"
66
+ else:
67
+ raise ValueError(f"Unknown bump type: {bump_type}")
68
+ return major, minor, patch, suffix
69
+
70
+
71
+ def get_commits_since_tag(tag):
72
+ """Get commit messages from tag..HEAD (or all if tag is None)."""
73
+ if tag is None:
74
+ # All commits reachable from HEAD
75
+ rev_range = "HEAD"
76
+ else:
77
+ rev_range = f"{tag}..HEAD"
78
+
79
+ # Get conventional commits; format: <type>(<scope>): <subject>
80
+ # We'll grab the full subject line (skip body for changelog)
81
+ log = run_git("log", rev_range, "--pretty=format:%s", "--no-merges")
82
+ commits = [line.strip() for line in log.splitlines() if line.strip()]
83
+ return commits
84
+
85
+
86
+ def categorize_commits(commits):
87
+ """Map conventional commit types to changelog headings."""
88
+ mapping = {
89
+ "feat": "Added",
90
+ "fix": "Fixed",
91
+ "perf": "Performance",
92
+ "docs": "Documentation",
93
+ "style": "Style",
94
+ "refactor": "Refactored",
95
+ "test": "Tests",
96
+ "chore": "Chores",
97
+ "ci": "CI/CD",
98
+ "build": "Build",
99
+ "revert": "Reverts",
100
+ }
101
+ categories: dict[str, list[str]] = {v: [] for v in mapping.values()}
102
+ categories["Other"] = []
103
+
104
+ for msg in commits:
105
+ m = re.match(r"^([a-zA-Z]+)(?:\([^)]+\))?!?:\s*(.+)", msg)
106
+ if m:
107
+ commit_type = m.group(1).lower()
108
+ subject = m.group(2)
109
+ heading = mapping.get(commit_type, "Other")
110
+ categories[heading].append(f"- {msg}")
111
+ else:
112
+ # Non-conventional or cannot parse: include verbatim under Other
113
+ categories["Other"].append(f"- {msg}")
114
+
115
+ # Remove empty categories
116
+ return {k: v for k, v in categories.items() if v}
117
+
118
+
119
+ def format_changelog_entry(version, date, categories):
120
+ """Format a single changelog entry for the given version."""
121
+ lines = [f"\n## [{version}] - {date}\n"]
122
+ for heading in ["Added", "Fixed", "Performance", "Documentation", "Style",
123
+ "Refactored", "Tests", "Chores", "CI/CD", "Build", "Reverts", "Other"]:
124
+ items = categories.get(heading)
125
+ if items:
126
+ lines.append(f"### {heading}\n" + "\n".join(items) + "\n")
127
+ return "".join(lines).strip() + "\n"
128
+
129
+
130
+ def prepend_to_changelog(entry, changelog_path):
131
+ """Prepend entry to CHANGELOG.md, after any header."""
132
+ path = Path(changelog_path)
133
+ if not path.exists():
134
+ # Create minimal file with entry at top
135
+ header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n"
136
+ path.write_text(header + entry)
137
+ return
138
+
139
+ content = path.read_text(encoding="utf-8")
140
+ # Find the position of the first existing "## [" section
141
+ match = re.search(r"(?m)^## \[", content)
142
+ if match:
143
+ insert_pos = match.start()
144
+ new_content = content[:insert_pos] + entry + content[insert_pos:]
145
+ else:
146
+ # No existing entries, just append after header (or at end)
147
+ new_content = content.rstrip() + "\n\n" + entry
148
+
149
+ path.write_text(new_content, encoding="utf-8")
150
+
151
+
152
+ def tag_exists(tag):
153
+ """Check if a git tag exists."""
154
+ try:
155
+ existing = run_git("tag", "-l", tag)
156
+ return tag in existing.splitlines()
157
+ except subprocess.CalledProcessError:
158
+ return False
159
+
160
+
161
+ def main():
162
+ bump = os.getenv("BUMP")
163
+ if not bump:
164
+ print("Error: BUMP environment variable not set", file=sys.stderr)
165
+ sys.exit(1)
166
+
167
+ changelog_path = Path("CHANGELOG.md")
168
+
169
+ latest_tag = get_latest_stable_tag()
170
+ major, minor, patch = parse_version(latest_tag)
171
+ new_major, new_minor, new_patch, suffix = bump_version(major, minor, patch, bump)
172
+ new_version = f"{new_major}.{new_minor}.{new_patch}{suffix}"
173
+ tag_name = f"v{new_version}"
174
+
175
+ # Check if tag already exists
176
+ if tag_exists(tag_name):
177
+ print(f"Error: Tag '{tag_name}' already exists. Choose a different bump or delete the existing tag.", file=sys.stderr)
178
+ sys.exit(1)
179
+
180
+ commits = get_commits_since_tag(latest_tag)
181
+ categories = categorize_commits(commits)
182
+
183
+ date_str = datetime.now().strftime("%Y-%m-%d")
184
+ entry = format_changelog_entry(new_version, date_str, categories)
185
+
186
+ prepend_to_changelog(entry, changelog_path)
187
+
188
+ # Set GITHUB_OUTPUT
189
+ gh_output = os.getenv("GITHUB_OUTPUT")
190
+ if gh_output:
191
+ with open(gh_output, "a", encoding="utf-8") as f:
192
+ f.write(f"new_version={new_version}\n")
193
+ f.write(f"tag_name={tag_name}\n")
194
+ else:
195
+ # For local testing: print to stdout
196
+ print(f"::set-output name=new_version::{new_version}")
197
+ print(f"::set-output name=tag_name::{tag_name}")
198
+
199
+ print(f"Generated changelog for {tag_name}")
200
+
201
+
202
+ if __name__ == "__main__":
203
+ main()
@@ -0,0 +1,20 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ "**" ]
6
+ pull_request:
7
+ branches: [ "main" ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+ - uses: astral-sh/setup-uv@v4
15
+ with:
16
+ python-version: "3.13"
17
+ - run: uv sync --frozen
18
+ - run: uv run ruff check src/ tests/
19
+ - run: uv run ruff format --check src/ tests/
20
+ - run: uv run pytest tests/ --cov=piagentsync --cov-report=term-missing
@@ -0,0 +1,20 @@
1
+ name: Publish
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ permissions:
7
+ id-token: write
8
+ contents: read
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+ - uses: astral-sh/setup-uv@v4
16
+ with:
17
+ python-version: "3.13"
18
+ - run: uv sync --frozen
19
+ - run: uv build
20
+ - run: uv publish
@@ -0,0 +1,66 @@
1
+ name: Tag
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ inputs:
6
+ bump:
7
+ description: 'Version bump type'
8
+ required: true
9
+ type: choice
10
+ options:
11
+ - PATCH
12
+ - MINOR
13
+ - MAJOR
14
+ - RC
15
+
16
+ permissions:
17
+ contents: write
18
+
19
+ jobs:
20
+ test:
21
+ runs-on: ubuntu-latest
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ - uses: astral-sh/setup-uv@v4
25
+ with:
26
+ python-version: "3.13"
27
+ - run: uv sync --frozen
28
+ - run: uv run ruff check src/ tests/
29
+ - run: uv run ruff format --check src/ tests/
30
+ - run: uv run pytest tests/ --cov=piagentsync --cov-report=term-missing
31
+
32
+ tag:
33
+ needs: [test]
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ with:
38
+ fetch-depth: 0
39
+ token: ${{ secrets.GITHUB_TOKEN }}
40
+ - name: Generate changelog and compute version
41
+ id: changelog
42
+ env:
43
+ BUMP: ${{ inputs.bump }}
44
+ run: |
45
+ python .github/scripts/generate_changelog.py
46
+ - name: Bump version in pyproject.toml
47
+ run: |
48
+ sed -i "s/^version = .*/version = \"${{ steps.changelog.outputs.new_version }}\"/" pyproject.toml
49
+ - name: Bump version in src/piagentsync/__init__.py
50
+ run: |
51
+ sed -i "s/^__version__ = .*/__version__ = \"${{ steps.changelog.outputs.new_version }}\"/" src/piagentsync/__init__.py
52
+ - name: Configure git identity
53
+ run: |
54
+ git config user.name "github-actions[bot]"
55
+ git config user.email "github-actions[bot]@users.noreply.github.com"
56
+ - name: Commit version bump
57
+ run: |
58
+ git add pyproject.toml src/piagentsync/__init__.py CHANGELOG.md
59
+ git commit -m "chore(release): bump version to ${{ steps.changelog.outputs.new_version }}"
60
+ git push origin main
61
+ - name: Create and push tag
62
+ run: |
63
+ git tag ${{ steps.changelog.outputs.tag_name }}
64
+ git push origin ${{ steps.changelog.outputs.tag_name }}
65
+ - name: Print confirmation
66
+ run: echo "Created tag ${{ steps.changelog.outputs.tag_name }} for version ${{ steps.changelog.outputs.new_version }}"
@@ -0,0 +1,55 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ develop-eggs/
9
+ dist/
10
+ downloads/
11
+ eggs/
12
+ .eggs/
13
+ lib/
14
+ lib64/
15
+ parts/
16
+ sdist/
17
+ var/
18
+ wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+
23
+ # Virtual environments
24
+ venv/
25
+ env/
26
+ .venv
27
+ ENV/
28
+ env.bak/
29
+ venv.bak/
30
+
31
+ # IDEs
32
+ .idea/
33
+ .vscode/
34
+ *.swp
35
+ *.swo
36
+
37
+ # Testing
38
+ .pytest_cache/
39
+ .coverage
40
+ htmlcov/
41
+ .tox/
42
+
43
+ # UV
44
+ .uv/
45
+
46
+ # Local config
47
+ .env
48
+ .env.local
49
+
50
+ # Project specific
51
+ .vault/
52
+ .opencode/
53
+
54
+ # macOS
55
+ .DS_Store
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.0.1-rc] - 2026-03-27
9
+ ### Added
10
+ - feat(ci): automate changelog generation and remove test gate from publish
11
+ - feat(piagentsync): initial implementation with full test suite
12
+ ### Fixed
13
+ - fix(ci): prevent duplicate tags by checking existing tags
14
+ ### Tests
15
+ - test(cli): make version check dynamic using package __version__
16
+ ### Chores
17
+ - chore(release): bump version to 0.0.1-rc
18
+ - chore(release): bump version to 0.0.1-rc
19
+ ### CI/CD
20
+ - ci: add GitHub Actions workflows for CI, tagging, and publishing
21
+ ### Other
22
+ - Initial commit
23
+ ## [0.0.1-rc] - 2026-03-27
24
+ ### Added
25
+ - feat(ci): automate changelog generation and remove test gate from publish
26
+ - feat(piagentsync): initial implementation with full test suite
27
+ ### Tests
28
+ - test(cli): make version check dynamic using package __version__
29
+ ### Chores
30
+ - chore(release): bump version to 0.0.1-rc
31
+ ### CI/CD
32
+ - ci: add GitHub Actions workflows for CI, tagging, and publishing
33
+ ### Other
34
+ - Initial commit
35
+ ## [0.1.0] - 2026-03-27
36
+
37
+ ### Added
38
+ - Initial release of piagentsync
39
+ - Sync OpenCode agents and skills from Obsidian vault to workspace
40
+ - CLI commands: `pull`, `status`, `init`
41
+ - Support for global agents
42
+ - Dry-run mode
43
+ - Full TDD implementation with pytest
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Piwero
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.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: piagentsync
3
+ Version: 0.0.1rc0
4
+ Summary: Sync OpenCode agents and skills from an Obsidian vault to workspace directories
5
+ License: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: pydantic-settings>=2.3
9
+ Requires-Dist: pydantic>=2.7
10
+ Requires-Dist: python-frontmatter>=1.1
11
+ Requires-Dist: rich>=13
12
+ Requires-Dist: typer>=0.12
13
+ Description-Content-Type: text/markdown
14
+
15
+ # piagentsync
16
+
17
+ Sync OpenCode agents and skills from an Obsidian vault to workspace directories.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ uv add piagentsync
23
+ # or
24
+ pip install piagentsync
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ 1. Initialize a new project in your vault:
30
+
31
+ ```bash
32
+ piagentsync init myproject --workspace ~/workspace/myproject
33
+ ```
34
+
35
+ 2. Pull the synced files to your workspace:
36
+
37
+ ```bash
38
+ piagentsync pull myproject
39
+ ```
40
+
41
+ ## Configuration
42
+
43
+ Environment variables (also configurable via `.env` file):
44
+
45
+ | Variable | Default | Description |
46
+ |----------|---------|-------------|
47
+ | `PIAGENTSYNC_VAULT_PATH` | `~/vault` | Path to Obsidian vault |
48
+ | `PIAGENTSYNC_GLOBAL_OPENCODE_PATH` | `~/.config/opencode` | Path to global OpenCode config |
49
+
50
+ ## CLI reference
51
+
52
+ ### `piagentsync pull <project>`
53
+
54
+ Sync a single project from vault to workspace.
55
+
56
+ Options:
57
+ - `--dry-run` / `--no-dry-run` — preview changes without writing
58
+ - `--global` / `--no-global` — also sync global agents
59
+ - `--all` — sync all discovered projects
60
+
61
+ ### `piagentsync status [project]`
62
+
63
+ Show diff between vault and workspace.
64
+
65
+ Options:
66
+ - `--all` — show status for all projects (default when no project given)
67
+
68
+ ### `piagentsync init <project>`
69
+
70
+ Scaffold a new project in the vault.
71
+
72
+ Options:
73
+ - `--workspace PATH` — required, workspace directory
74
+ - `--notion-board-id TEXT` — optional Notion DB ID
75
+ - `--notion-project-filter TEXT` — optional Notion project filter (defaults to project slug)
76
+
77
+ ### `--version`
78
+
79
+ Print version and exit.
80
+
81
+ ## AGENTS.md manifest format
82
+
83
+ Each project must have an `AGENTS.md` file in its root with YAML frontmatter:
84
+
85
+ ```yaml
86
+ ---
87
+ project: myproject
88
+ workspace: ~/workspace/myproject
89
+ notion_board_id: 3305f9479a8d8055b3c3e86a9006cf91
90
+ notion_project_filter: myproject
91
+ ---
92
+ ```
93
+
94
+ The body below the frontmatter is the OpenCode routing table.
95
+
96
+ ## Expected vault structure
97
+
98
+ ```
99
+ vault/
100
+ ├── agents/
101
+ │ └── global/
102
+ │ └── *.md
103
+ └── projects/
104
+ └── {project}/
105
+ ├── AGENTS.md # manifest with frontmatter
106
+ ├── context.md # optional context file
107
+ ├── decisions.md # optional decisions log
108
+ ├── agents/
109
+ │ └── *.md
110
+ └── skills/
111
+ └── *.md
112
+ ```
113
+
114
+ ## Contributing
115
+
116
+ Development setup:
117
+
118
+ ```bash
119
+ uv sync
120
+ uv run pytest
121
+ ```
122
+
123
+ Lint and format:
124
+
125
+ ```bash
126
+ uv run ruff check src/ tests/
127
+ uv run ruff format src/ tests/
128
+ ```
129
+
130
+ All commits follow [Conventional Commits](https://www.conventionalcommits.org/).
131
+
132
+ ### Publishing (maintainers)
133
+
134
+ This repository uses GitHub Actions for CI and automated releases.
135
+
136
+ #### One-time PyPI setup (trusted publisher)
137
+
138
+ 1. Enable OIDC on PyPI for the repository:
139
+ - Publisher: GitHub Actions
140
+ - Repository owner: `Piwero`
141
+ - Repository name: `piagentsync`
142
+ - Workflow filename: `publish.yml`
143
+ - Environment name: (leave blank)
144
+
145
+ 2. Push a tag to trigger the `tag.yml` workflow to bump version and create a git tag.
146
+
147
+ 3. After the tag workflow completes, the `publish.yml` workflow will automatically build and publish the package to PyPI.
148
+
149
+ #### Release process
150
+
151
+ ```text
152
+ git push → ci.yml passes
153
+ → trigger tag.yml (choose MAJOR/MINOR/PATCH/RC)
154
+ → tests re-run → version bumped → tag pushed
155
+ → trigger publish.yml
156
+ → tests re-run → built → published to PyPI
157
+ ```
158
+
159
+ ## License
160
+
161
+ MIT