pjctx 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.
- pjctx-0.1.0/.gitignore +20 -0
- pjctx-0.1.0/PKG-INFO +136 -0
- pjctx-0.1.0/README.md +119 -0
- pjctx-0.1.0/pyproject.toml +34 -0
- pjctx-0.1.0/src/pjctx/__init__.py +3 -0
- pjctx-0.1.0/src/pjctx/cli.py +128 -0
- pjctx-0.1.0/src/pjctx/commands/__init__.py +0 -0
- pjctx-0.1.0/src/pjctx/commands/diff.py +48 -0
- pjctx-0.1.0/src/pjctx/commands/handoff.py +60 -0
- pjctx-0.1.0/src/pjctx/commands/hook.py +117 -0
- pjctx-0.1.0/src/pjctx/commands/init.py +53 -0
- pjctx-0.1.0/src/pjctx/commands/log.py +54 -0
- pjctx-0.1.0/src/pjctx/commands/resume.py +45 -0
- pjctx-0.1.0/src/pjctx/commands/save.py +144 -0
- pjctx-0.1.0/src/pjctx/commands/share.py +46 -0
- pjctx-0.1.0/src/pjctx/commands/watch.py +71 -0
- pjctx-0.1.0/src/pjctx/core/__init__.py +0 -0
- pjctx-0.1.0/src/pjctx/core/config.py +68 -0
- pjctx-0.1.0/src/pjctx/core/context.py +42 -0
- pjctx-0.1.0/src/pjctx/core/git_ops.py +102 -0
- pjctx-0.1.0/src/pjctx/core/storage.py +96 -0
- pjctx-0.1.0/src/pjctx/prompt.py +130 -0
- pjctx-0.1.0/src/pjctx/ui.py +76 -0
- pjctx-0.1.0/tests/__init__.py +0 -0
- pjctx-0.1.0/tests/conftest.py +39 -0
- pjctx-0.1.0/tests/test_cli.py +145 -0
- pjctx-0.1.0/tests/test_context.py +74 -0
- pjctx-0.1.0/tests/test_git_ops.py +41 -0
- pjctx-0.1.0/tests/test_prompt.py +77 -0
- pjctx-0.1.0/tests/test_storage.py +66 -0
pjctx-0.1.0/.gitignore
ADDED
pjctx-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pjctx
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Capture and restore AI coding context across sessions
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Keywords: ai,cli,context,developer-tools
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Requires-Dist: click>=8.0
|
|
9
|
+
Requires-Dist: gitpython>=3.1
|
|
10
|
+
Requires-Dist: pyperclip>=1.8
|
|
11
|
+
Requires-Dist: rich>=13.0
|
|
12
|
+
Requires-Dist: watchdog>=3.0
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# PJContext (pjctx)
|
|
19
|
+
|
|
20
|
+
Capture and restore AI coding context across sessions. Stop wasting 10-15 minutes re-explaining your project every time you start a new AI coding session.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install pjctx
|
|
26
|
+
# or
|
|
27
|
+
pipx install pjctx
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Via npm (for Node.js developers)
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g pjctx
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> Requires Python 3.9+ and pip. The npm package automatically installs the Python package during setup.
|
|
37
|
+
|
|
38
|
+
For development:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
git clone <repo-url>
|
|
42
|
+
cd pjctx
|
|
43
|
+
pip install -e ".[dev]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Initialize in your git repo
|
|
50
|
+
pjctx init
|
|
51
|
+
|
|
52
|
+
# Save context (interactive)
|
|
53
|
+
pjctx save
|
|
54
|
+
|
|
55
|
+
# Save context (quick, one-liner)
|
|
56
|
+
pjctx save "Refactoring payment service to event sourcing"
|
|
57
|
+
|
|
58
|
+
# Save context (auto-detect from git)
|
|
59
|
+
pjctx save --auto
|
|
60
|
+
|
|
61
|
+
# Resume — copies prompt to clipboard, paste into any AI tool
|
|
62
|
+
pjctx resume
|
|
63
|
+
|
|
64
|
+
# Resume with specific format
|
|
65
|
+
pjctx resume --format xml
|
|
66
|
+
pjctx resume --format compact
|
|
67
|
+
pjctx resume --no-copy # print to stdout
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
### `pjctx init`
|
|
73
|
+
Initialize `.pjctx/` in the current git repo. Adds `.pjctx/` to `.gitignore`.
|
|
74
|
+
|
|
75
|
+
### `pjctx save [message]`
|
|
76
|
+
Save current coding context. Three modes:
|
|
77
|
+
- **Interactive** (no args): Rich prompts walk through task, approaches, decisions, next steps
|
|
78
|
+
- **Quick** (`pjctx save "message"`): Auto-fill git data, message only
|
|
79
|
+
- **Auto** (`--auto`): No prompts, git-detected changes, carries forward previous context
|
|
80
|
+
|
|
81
|
+
Options: `--auto`, `-t/--tag TAG` (repeatable)
|
|
82
|
+
|
|
83
|
+
### `pjctx resume`
|
|
84
|
+
Generate a resume prompt from the latest context and copy it to clipboard. Paste into any AI coding tool (Cursor, Claude Code, Copilot, Windsurf) to instantly restore context.
|
|
85
|
+
|
|
86
|
+
Options: `-f/--format [default|xml|compact]`, `--no-copy`, `-b/--branch BRANCH`
|
|
87
|
+
|
|
88
|
+
### `pjctx log`
|
|
89
|
+
Show context history as a table.
|
|
90
|
+
|
|
91
|
+
Options: `-b/--branch BRANCH`, `-n/--limit N`, `--all`
|
|
92
|
+
|
|
93
|
+
### `pjctx diff`
|
|
94
|
+
Show changes since last context save.
|
|
95
|
+
|
|
96
|
+
Options: `--stat` (default), `--full`
|
|
97
|
+
|
|
98
|
+
### `pjctx handoff [@user]`
|
|
99
|
+
Create a handoff context for another developer. Carries forward all context fields and adds handoff note.
|
|
100
|
+
|
|
101
|
+
### `pjctx share`
|
|
102
|
+
Remove `.pjctx/` from `.gitignore` and commit it to git for team sharing.
|
|
103
|
+
|
|
104
|
+
### `pjctx watch`
|
|
105
|
+
Watch for file changes and auto-save context at intervals.
|
|
106
|
+
|
|
107
|
+
Options: `-i/--interval SECONDS` (default: 300)
|
|
108
|
+
|
|
109
|
+
### `pjctx hook install|uninstall|status`
|
|
110
|
+
Manage git post-commit hook for automatic context saving after each commit.
|
|
111
|
+
|
|
112
|
+
## Storage
|
|
113
|
+
|
|
114
|
+
Context is stored locally in `.pjctx/` within your repo, scoped by branch:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
.pjctx/
|
|
118
|
+
├── config.json
|
|
119
|
+
├── contexts/
|
|
120
|
+
│ ├── main/
|
|
121
|
+
│ │ ├── 2024-01-15T10-30-00.json
|
|
122
|
+
│ │ └── latest.json
|
|
123
|
+
│ └── feature/payment-refactor/
|
|
124
|
+
│ └── latest.json
|
|
125
|
+
└── hooks/
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Prompt Formats
|
|
129
|
+
|
|
130
|
+
- **default**: Markdown briefing document with headers
|
|
131
|
+
- **xml**: Structured XML for tools that parse it
|
|
132
|
+
- **compact**: Dense single paragraph for token-limited contexts
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT
|
pjctx-0.1.0/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# PJContext (pjctx)
|
|
2
|
+
|
|
3
|
+
Capture and restore AI coding context across sessions. Stop wasting 10-15 minutes re-explaining your project every time you start a new AI coding session.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install pjctx
|
|
9
|
+
# or
|
|
10
|
+
pipx install pjctx
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Via npm (for Node.js developers)
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g pjctx
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> Requires Python 3.9+ and pip. The npm package automatically installs the Python package during setup.
|
|
20
|
+
|
|
21
|
+
For development:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone <repo-url>
|
|
25
|
+
cd pjctx
|
|
26
|
+
pip install -e ".[dev]"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Initialize in your git repo
|
|
33
|
+
pjctx init
|
|
34
|
+
|
|
35
|
+
# Save context (interactive)
|
|
36
|
+
pjctx save
|
|
37
|
+
|
|
38
|
+
# Save context (quick, one-liner)
|
|
39
|
+
pjctx save "Refactoring payment service to event sourcing"
|
|
40
|
+
|
|
41
|
+
# Save context (auto-detect from git)
|
|
42
|
+
pjctx save --auto
|
|
43
|
+
|
|
44
|
+
# Resume — copies prompt to clipboard, paste into any AI tool
|
|
45
|
+
pjctx resume
|
|
46
|
+
|
|
47
|
+
# Resume with specific format
|
|
48
|
+
pjctx resume --format xml
|
|
49
|
+
pjctx resume --format compact
|
|
50
|
+
pjctx resume --no-copy # print to stdout
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Commands
|
|
54
|
+
|
|
55
|
+
### `pjctx init`
|
|
56
|
+
Initialize `.pjctx/` in the current git repo. Adds `.pjctx/` to `.gitignore`.
|
|
57
|
+
|
|
58
|
+
### `pjctx save [message]`
|
|
59
|
+
Save current coding context. Three modes:
|
|
60
|
+
- **Interactive** (no args): Rich prompts walk through task, approaches, decisions, next steps
|
|
61
|
+
- **Quick** (`pjctx save "message"`): Auto-fill git data, message only
|
|
62
|
+
- **Auto** (`--auto`): No prompts, git-detected changes, carries forward previous context
|
|
63
|
+
|
|
64
|
+
Options: `--auto`, `-t/--tag TAG` (repeatable)
|
|
65
|
+
|
|
66
|
+
### `pjctx resume`
|
|
67
|
+
Generate a resume prompt from the latest context and copy it to clipboard. Paste into any AI coding tool (Cursor, Claude Code, Copilot, Windsurf) to instantly restore context.
|
|
68
|
+
|
|
69
|
+
Options: `-f/--format [default|xml|compact]`, `--no-copy`, `-b/--branch BRANCH`
|
|
70
|
+
|
|
71
|
+
### `pjctx log`
|
|
72
|
+
Show context history as a table.
|
|
73
|
+
|
|
74
|
+
Options: `-b/--branch BRANCH`, `-n/--limit N`, `--all`
|
|
75
|
+
|
|
76
|
+
### `pjctx diff`
|
|
77
|
+
Show changes since last context save.
|
|
78
|
+
|
|
79
|
+
Options: `--stat` (default), `--full`
|
|
80
|
+
|
|
81
|
+
### `pjctx handoff [@user]`
|
|
82
|
+
Create a handoff context for another developer. Carries forward all context fields and adds handoff note.
|
|
83
|
+
|
|
84
|
+
### `pjctx share`
|
|
85
|
+
Remove `.pjctx/` from `.gitignore` and commit it to git for team sharing.
|
|
86
|
+
|
|
87
|
+
### `pjctx watch`
|
|
88
|
+
Watch for file changes and auto-save context at intervals.
|
|
89
|
+
|
|
90
|
+
Options: `-i/--interval SECONDS` (default: 300)
|
|
91
|
+
|
|
92
|
+
### `pjctx hook install|uninstall|status`
|
|
93
|
+
Manage git post-commit hook for automatic context saving after each commit.
|
|
94
|
+
|
|
95
|
+
## Storage
|
|
96
|
+
|
|
97
|
+
Context is stored locally in `.pjctx/` within your repo, scoped by branch:
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
.pjctx/
|
|
101
|
+
├── config.json
|
|
102
|
+
├── contexts/
|
|
103
|
+
│ ├── main/
|
|
104
|
+
│ │ ├── 2024-01-15T10-30-00.json
|
|
105
|
+
│ │ └── latest.json
|
|
106
|
+
│ └── feature/payment-refactor/
|
|
107
|
+
│ └── latest.json
|
|
108
|
+
└── hooks/
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Prompt Formats
|
|
112
|
+
|
|
113
|
+
- **default**: Markdown briefing document with headers
|
|
114
|
+
- **xml**: Structured XML for tools that parse it
|
|
115
|
+
- **compact**: Dense single paragraph for token-limited contexts
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pjctx"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Capture and restore AI coding context across sessions"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
keywords = ["ai", "context", "developer-tools", "cli"]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"click>=8.0",
|
|
15
|
+
"rich>=13.0",
|
|
16
|
+
"pyperclip>=1.8",
|
|
17
|
+
"gitpython>=3.1",
|
|
18
|
+
"watchdog>=3.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.optional-dependencies]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=7.0",
|
|
24
|
+
"pytest-cov>=4.0",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
pjctx = "pjctx.cli:cli"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/pjctx"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Click CLI group and command wiring."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from pjctx import __version__
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
@click.version_option(version=__version__, prog_name="pjctx")
|
|
12
|
+
@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output.")
|
|
13
|
+
@click.pass_context
|
|
14
|
+
def cli(ctx: click.Context, verbose: bool) -> None:
|
|
15
|
+
"""PJContext — Capture and restore AI coding context."""
|
|
16
|
+
ctx.ensure_object(dict)
|
|
17
|
+
ctx.obj["verbose"] = verbose
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# Lazy-load commands to keep --help/--version fast
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@cli.command()
|
|
24
|
+
@click.pass_context
|
|
25
|
+
def init(ctx: click.Context) -> None:
|
|
26
|
+
"""Initialize .pjctx/ in the current repo."""
|
|
27
|
+
from pjctx.commands.init import run_init
|
|
28
|
+
run_init(ctx.obj)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@cli.command()
|
|
32
|
+
@click.argument("message", required=False)
|
|
33
|
+
@click.option("--auto", "auto_mode", is_flag=True, help="Auto-detect from git, no prompts.")
|
|
34
|
+
@click.option("--tag", "-t", multiple=True, help="Add tags.")
|
|
35
|
+
@click.pass_context
|
|
36
|
+
def save(ctx: click.Context, message: str | None, auto_mode: bool, tag: tuple[str, ...]) -> None:
|
|
37
|
+
"""Save current context. Interactive if no message given."""
|
|
38
|
+
from pjctx.commands.save import run_save
|
|
39
|
+
run_save(ctx.obj, message=message, auto_mode=auto_mode, tags=list(tag))
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@cli.command()
|
|
43
|
+
@click.option("--format", "-f", "fmt", default="default",
|
|
44
|
+
type=click.Choice(["default", "xml", "compact"]),
|
|
45
|
+
help="Prompt format.")
|
|
46
|
+
@click.option("--no-copy", is_flag=True, help="Print to stdout instead of clipboard.")
|
|
47
|
+
@click.option("--branch", "-b", default=None, help="Resume from a specific branch.")
|
|
48
|
+
@click.pass_context
|
|
49
|
+
def resume(ctx: click.Context, fmt: str, no_copy: bool, branch: str | None) -> None:
|
|
50
|
+
"""Generate a resume prompt from the latest context."""
|
|
51
|
+
from pjctx.commands.resume import run_resume
|
|
52
|
+
run_resume(ctx.obj, fmt=fmt, no_copy=no_copy, branch=branch)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@cli.command()
|
|
56
|
+
@click.option("--branch", "-b", default=None, help="Show log for a specific branch.")
|
|
57
|
+
@click.option("--limit", "-n", default=10, help="Max entries to show.")
|
|
58
|
+
@click.option("--all", "all_branches", is_flag=True, help="Show logs across all branches.")
|
|
59
|
+
@click.pass_context
|
|
60
|
+
def log(ctx: click.Context, branch: str | None, limit: int, all_branches: bool) -> None:
|
|
61
|
+
"""Show context history."""
|
|
62
|
+
from pjctx.commands.log import run_log
|
|
63
|
+
run_log(ctx.obj, branch=branch, limit=limit, all_branches=all_branches)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@cli.command()
|
|
67
|
+
@click.option("--stat/--full", default=True, help="Show stat summary or full diff.")
|
|
68
|
+
@click.pass_context
|
|
69
|
+
def diff(ctx: click.Context, stat: bool) -> None:
|
|
70
|
+
"""Show changes since last context save."""
|
|
71
|
+
from pjctx.commands.diff import run_diff
|
|
72
|
+
run_diff(ctx.obj, stat_only=stat)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@cli.command()
|
|
76
|
+
@click.argument("user", required=False)
|
|
77
|
+
@click.pass_context
|
|
78
|
+
def handoff(ctx: click.Context, user: str | None) -> None:
|
|
79
|
+
"""Create a handoff context for another user."""
|
|
80
|
+
from pjctx.commands.handoff import run_handoff
|
|
81
|
+
run_handoff(ctx.obj, user=user)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@cli.command()
|
|
85
|
+
@click.pass_context
|
|
86
|
+
def share(ctx: click.Context) -> None:
|
|
87
|
+
"""Share .pjctx/ by committing it to git."""
|
|
88
|
+
from pjctx.commands.share import run_share
|
|
89
|
+
run_share(ctx.obj)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@cli.command()
|
|
93
|
+
@click.option("--interval", "-i", default=300, help="Auto-save interval in seconds.")
|
|
94
|
+
@click.pass_context
|
|
95
|
+
def watch(ctx: click.Context, interval: int) -> None:
|
|
96
|
+
"""Watch for file changes and auto-save context."""
|
|
97
|
+
from pjctx.commands.watch import run_watch
|
|
98
|
+
run_watch(ctx.obj, interval=interval)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@cli.group()
|
|
102
|
+
def hook() -> None:
|
|
103
|
+
"""Manage git hooks for auto-save."""
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@hook.command("install")
|
|
108
|
+
@click.pass_context
|
|
109
|
+
def hook_install(ctx: click.Context) -> None:
|
|
110
|
+
"""Install post-commit hook."""
|
|
111
|
+
from pjctx.commands.hook import run_install
|
|
112
|
+
run_install(ctx.obj)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@hook.command("uninstall")
|
|
116
|
+
@click.pass_context
|
|
117
|
+
def hook_uninstall(ctx: click.Context) -> None:
|
|
118
|
+
"""Uninstall post-commit hook."""
|
|
119
|
+
from pjctx.commands.hook import run_uninstall
|
|
120
|
+
run_uninstall(ctx.obj)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@hook.command("status")
|
|
124
|
+
@click.pass_context
|
|
125
|
+
def hook_status(ctx: click.Context) -> None:
|
|
126
|
+
"""Check hook installation status."""
|
|
127
|
+
from pjctx.commands.hook import run_status
|
|
128
|
+
run_status(ctx.obj)
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""pjctx diff — Show changes since last context save."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pjctx.core.config import find_repo_root, get_pjctx_dir
|
|
6
|
+
from pjctx.core.git_ops import get_current_branch, get_diff_summary, get_full_diff, get_files_changed
|
|
7
|
+
from pjctx.core.storage import load_latest
|
|
8
|
+
from pjctx import ui
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_diff(obj: dict, stat_only: bool = True) -> None:
|
|
12
|
+
repo_root = find_repo_root()
|
|
13
|
+
if repo_root is None:
|
|
14
|
+
ui.error("Not inside a git repository.")
|
|
15
|
+
raise SystemExit(1)
|
|
16
|
+
|
|
17
|
+
if not get_pjctx_dir(repo_root).exists():
|
|
18
|
+
ui.error("Not initialized. Run 'pjctx init' first.")
|
|
19
|
+
raise SystemExit(1)
|
|
20
|
+
|
|
21
|
+
branch = get_current_branch(repo_root)
|
|
22
|
+
last_ctx = load_latest(repo_root, branch)
|
|
23
|
+
|
|
24
|
+
if last_ctx:
|
|
25
|
+
ui.info(f"Last save: {last_ctx.timestamp[:19]} — {last_ctx.message}")
|
|
26
|
+
else:
|
|
27
|
+
ui.warning("No previous context. Showing all current changes.")
|
|
28
|
+
|
|
29
|
+
if stat_only:
|
|
30
|
+
summary = get_diff_summary(repo_root)
|
|
31
|
+
files = get_files_changed(repo_root)
|
|
32
|
+
if not summary and not files:
|
|
33
|
+
ui.info("No changes since last save.")
|
|
34
|
+
return
|
|
35
|
+
if summary:
|
|
36
|
+
ui._console().print(summary)
|
|
37
|
+
if files:
|
|
38
|
+
ui.info("Changed files:")
|
|
39
|
+
for f in files:
|
|
40
|
+
ui._console().print(f" {f}")
|
|
41
|
+
else:
|
|
42
|
+
full = get_full_diff(repo_root)
|
|
43
|
+
if not full:
|
|
44
|
+
ui.info("No changes since last save.")
|
|
45
|
+
return
|
|
46
|
+
from rich.syntax import Syntax
|
|
47
|
+
syntax = Syntax(full, "diff", theme="monokai")
|
|
48
|
+
ui._console().print(syntax)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""pjctx handoff — Create a handoff context for another user."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pjctx.core.config import find_repo_root, get_pjctx_dir
|
|
6
|
+
from pjctx.core.context import Context
|
|
7
|
+
from pjctx.core.git_ops import get_current_branch, get_files_changed, get_diff_summary
|
|
8
|
+
from pjctx.core.storage import save_context, load_latest
|
|
9
|
+
from pjctx import ui
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_handoff(obj: dict, user: str | None = None) -> None:
|
|
13
|
+
repo_root = find_repo_root()
|
|
14
|
+
if repo_root is None:
|
|
15
|
+
ui.error("Not inside a git repository.")
|
|
16
|
+
raise SystemExit(1)
|
|
17
|
+
|
|
18
|
+
if not get_pjctx_dir(repo_root).exists():
|
|
19
|
+
ui.error("Not initialized. Run 'pjctx init' first.")
|
|
20
|
+
raise SystemExit(1)
|
|
21
|
+
|
|
22
|
+
branch = get_current_branch(repo_root)
|
|
23
|
+
|
|
24
|
+
# Resolve target user
|
|
25
|
+
target = user
|
|
26
|
+
if target and target.startswith("@"):
|
|
27
|
+
target = target[1:]
|
|
28
|
+
if not target:
|
|
29
|
+
target = ui.prompt("Handoff to (username)")
|
|
30
|
+
if not target:
|
|
31
|
+
ui.error("Username required.")
|
|
32
|
+
raise SystemExit(1)
|
|
33
|
+
|
|
34
|
+
# Load previous context to carry forward
|
|
35
|
+
prev = load_latest(repo_root, branch)
|
|
36
|
+
|
|
37
|
+
# Gather handoff note
|
|
38
|
+
handoff_note = ui.prompt("Handoff note (what they need to know)")
|
|
39
|
+
|
|
40
|
+
files_changed = get_files_changed(repo_root)
|
|
41
|
+
diff_summary = get_diff_summary(repo_root)
|
|
42
|
+
|
|
43
|
+
ctx = Context(
|
|
44
|
+
message=f"Handoff to {target}",
|
|
45
|
+
task=prev.task if prev else "",
|
|
46
|
+
approaches_tried=prev.approaches_tried if prev else [],
|
|
47
|
+
current_approach=prev.current_approach if prev else "",
|
|
48
|
+
decisions=prev.decisions if prev else [],
|
|
49
|
+
next_steps=prev.next_steps if prev else [],
|
|
50
|
+
branch=branch,
|
|
51
|
+
files_changed=files_changed,
|
|
52
|
+
git_diff_summary=diff_summary,
|
|
53
|
+
handoff_to=target,
|
|
54
|
+
handoff_note=handoff_note,
|
|
55
|
+
tags=prev.tags if prev else [],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
path = save_context(repo_root, ctx)
|
|
59
|
+
ui.success(f"Handoff context saved for @{target} → {path.name}")
|
|
60
|
+
ui.info("They can run 'pjctx resume' to pick up the context.")
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""pjctx hook — Install/uninstall/status for git post-commit hook."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import stat
|
|
7
|
+
|
|
8
|
+
from pjctx.core.config import find_repo_root, get_pjctx_dir
|
|
9
|
+
from pjctx.core.git_ops import get_hooks_dir
|
|
10
|
+
from pjctx import ui
|
|
11
|
+
|
|
12
|
+
HOOK_NAME = "post-commit"
|
|
13
|
+
MARKER_START = "# --- pjctx auto-save start ---"
|
|
14
|
+
MARKER_END = "# --- pjctx auto-save end ---"
|
|
15
|
+
HOOK_BODY = """\
|
|
16
|
+
# --- pjctx auto-save start ---
|
|
17
|
+
if command -v pjctx >/dev/null 2>&1; then
|
|
18
|
+
pjctx save --auto 2>/dev/null || true
|
|
19
|
+
fi
|
|
20
|
+
# --- pjctx auto-save end ---
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_install(obj: dict) -> None:
|
|
25
|
+
repo_root = find_repo_root()
|
|
26
|
+
if repo_root is None:
|
|
27
|
+
ui.error("Not inside a git repository.")
|
|
28
|
+
raise SystemExit(1)
|
|
29
|
+
|
|
30
|
+
if not get_pjctx_dir(repo_root).exists():
|
|
31
|
+
ui.error("Not initialized. Run 'pjctx init' first.")
|
|
32
|
+
raise SystemExit(1)
|
|
33
|
+
|
|
34
|
+
hooks_dir = get_hooks_dir(repo_root)
|
|
35
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
hook_path = hooks_dir / HOOK_NAME
|
|
37
|
+
|
|
38
|
+
if hook_path.exists():
|
|
39
|
+
content = hook_path.read_text()
|
|
40
|
+
if MARKER_START in content:
|
|
41
|
+
ui.warning("Hook already installed.")
|
|
42
|
+
return
|
|
43
|
+
# Append to existing hook
|
|
44
|
+
if not content.endswith("\n"):
|
|
45
|
+
content += "\n"
|
|
46
|
+
content += "\n" + HOOK_BODY
|
|
47
|
+
else:
|
|
48
|
+
content = "#!/bin/sh\n\n" + HOOK_BODY
|
|
49
|
+
|
|
50
|
+
hook_path.write_text(content)
|
|
51
|
+
# Make executable
|
|
52
|
+
hook_path.chmod(hook_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
|
|
53
|
+
|
|
54
|
+
ui.success("Post-commit hook installed.")
|
|
55
|
+
ui.info("Context will auto-save after each commit.")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_uninstall(obj: dict) -> None:
|
|
59
|
+
repo_root = find_repo_root()
|
|
60
|
+
if repo_root is None:
|
|
61
|
+
ui.error("Not inside a git repository.")
|
|
62
|
+
raise SystemExit(1)
|
|
63
|
+
|
|
64
|
+
hooks_dir = get_hooks_dir(repo_root)
|
|
65
|
+
hook_path = hooks_dir / HOOK_NAME
|
|
66
|
+
|
|
67
|
+
if not hook_path.exists():
|
|
68
|
+
ui.warning("No post-commit hook found.")
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
content = hook_path.read_text()
|
|
72
|
+
if MARKER_START not in content:
|
|
73
|
+
ui.warning("pjctx hook not found in post-commit.")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
# Remove our section
|
|
77
|
+
lines = content.splitlines(keepends=True)
|
|
78
|
+
new_lines: list[str] = []
|
|
79
|
+
inside = False
|
|
80
|
+
for line in lines:
|
|
81
|
+
if MARKER_START in line:
|
|
82
|
+
inside = True
|
|
83
|
+
continue
|
|
84
|
+
if MARKER_END in line:
|
|
85
|
+
inside = False
|
|
86
|
+
continue
|
|
87
|
+
if not inside:
|
|
88
|
+
new_lines.append(line)
|
|
89
|
+
|
|
90
|
+
remaining = "".join(new_lines).strip()
|
|
91
|
+
if remaining == "#!/bin/sh" or not remaining:
|
|
92
|
+
# Remove the file entirely if only shebang left
|
|
93
|
+
hook_path.unlink()
|
|
94
|
+
ui.success("Post-commit hook removed.")
|
|
95
|
+
else:
|
|
96
|
+
hook_path.write_text(remaining + "\n")
|
|
97
|
+
ui.success("pjctx section removed from post-commit hook.")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run_status(obj: dict) -> None:
|
|
101
|
+
repo_root = find_repo_root()
|
|
102
|
+
if repo_root is None:
|
|
103
|
+
ui.error("Not inside a git repository.")
|
|
104
|
+
raise SystemExit(1)
|
|
105
|
+
|
|
106
|
+
hooks_dir = get_hooks_dir(repo_root)
|
|
107
|
+
hook_path = hooks_dir / HOOK_NAME
|
|
108
|
+
|
|
109
|
+
if not hook_path.exists():
|
|
110
|
+
ui.info("Post-commit hook: [bold red]not installed[/]")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
content = hook_path.read_text()
|
|
114
|
+
if MARKER_START in content:
|
|
115
|
+
ui.info("Post-commit hook: [bold green]installed[/]")
|
|
116
|
+
else:
|
|
117
|
+
ui.info("Post-commit hook: [bold yellow]exists but pjctx not found[/]")
|