do-again 0.1.0__tar.gz → 0.2.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.
- do_again-0.2.0/.github/dependabot.yml +11 -0
- do_again-0.2.0/.github/workflows/ci.yml +60 -0
- do_again-0.2.0/.github/workflows/npm-release.yml +50 -0
- do_again-0.2.0/.github/workflows/release.yml +55 -0
- do_again-0.2.0/.npmignore +6 -0
- {do_again-0.1.0 → do_again-0.2.0}/PKG-INFO +69 -14
- do_again-0.2.0/README.md +131 -0
- do_again-0.2.0/bin/do-again.js +94 -0
- do_again-0.2.0/npm-tests/launcher.test.js +30 -0
- do_again-0.2.0/package-lock.json +19 -0
- do_again-0.2.0/package.json +44 -0
- {do_again-0.1.0 → do_again-0.2.0}/pyproject.toml +1 -1
- do_again-0.2.0/src/do_again/cli.py +137 -0
- do_again-0.2.0/src/do_again/service/__init__.py +3 -0
- do_again-0.2.0/src/do_again/service/runtime.py +372 -0
- do_again-0.2.0/templates/policy.json +137 -0
- do_again-0.2.0/tests/test_service.py +62 -0
- do_again-0.2.0/tools/check_release_version.py +34 -0
- do_again-0.1.0/.github/workflows/ci.yml +0 -23
- do_again-0.1.0/.github/workflows/release.yml +0 -40
- do_again-0.1.0/README.md +0 -76
- do_again-0.1.0/src/do_again/cli.py +0 -63
- do_again-0.1.0/src/do_again/service/__init__.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/.gitignore +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/LICENSE +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/__init__.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/core/__init__.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/core/agent.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/core/executor.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/core/schema.py +0 -0
- /do_again-0.1.0/templates/policy.json → /do_again-0.2.0/src/do_again/default_policy.json +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/platforms/__init__.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/platforms/base.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/platforms/detect.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/platforms/linux.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/platforms/macos.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/src/do_again/platforms/windows.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/tests/test_executor.py +0 -0
- {do_again-0.1.0 → do_again-0.2.0}/tests/test_platforms.py +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
version-sync:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
21
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: "3.13"
|
|
24
|
+
- run: python tools/check_release_version.py
|
|
25
|
+
|
|
26
|
+
test:
|
|
27
|
+
strategy:
|
|
28
|
+
fail-fast: false
|
|
29
|
+
matrix:
|
|
30
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
31
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
32
|
+
runs-on: ${{ matrix.os }}
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
35
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
36
|
+
with:
|
|
37
|
+
python-version: ${{ matrix.python-version }}
|
|
38
|
+
- run: python -m pip install .
|
|
39
|
+
- run: python -m pip check
|
|
40
|
+
- run: python -m unittest discover -s tests -v
|
|
41
|
+
- run: do-again doctor
|
|
42
|
+
|
|
43
|
+
npm-package:
|
|
44
|
+
strategy:
|
|
45
|
+
fail-fast: false
|
|
46
|
+
matrix:
|
|
47
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
48
|
+
runs-on: ${{ matrix.os }}
|
|
49
|
+
steps:
|
|
50
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
51
|
+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
|
52
|
+
with:
|
|
53
|
+
node-version: "24"
|
|
54
|
+
package-manager-cache: false
|
|
55
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
56
|
+
with:
|
|
57
|
+
python-version: "3.13"
|
|
58
|
+
- run: npm ci
|
|
59
|
+
- run: npm test
|
|
60
|
+
- run: npm pack --dry-run --ignore-scripts
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
name: NPM Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
environment:
|
|
15
|
+
name: npm
|
|
16
|
+
url: https://www.npmjs.com/package/do-again
|
|
17
|
+
permissions:
|
|
18
|
+
contents: read
|
|
19
|
+
id-token: write
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
22
|
+
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
|
23
|
+
with:
|
|
24
|
+
node-version: "24"
|
|
25
|
+
registry-url: "https://registry.npmjs.org"
|
|
26
|
+
package-manager-cache: false
|
|
27
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: "3.13"
|
|
30
|
+
- name: Verify release versions
|
|
31
|
+
run: python tools/check_release_version.py --tag "$GITHUB_REF_NAME"
|
|
32
|
+
- run: npm ci
|
|
33
|
+
- run: npm test
|
|
34
|
+
- run: npm publish
|
|
35
|
+
- name: Wait for npm registry availability
|
|
36
|
+
shell: bash
|
|
37
|
+
run: |
|
|
38
|
+
version=$(node -p "require('./package.json').version")
|
|
39
|
+
for attempt in $(seq 1 120); do
|
|
40
|
+
published=$(npm view "do-again@$version" version 2>/dev/null || true)
|
|
41
|
+
latest=$(npm view do-again dist-tags.latest 2>/dev/null || true)
|
|
42
|
+
if [ "$published" = "$version" ] && [ "$latest" = "$version" ]; then
|
|
43
|
+
echo "do-again@$version is available from the public npm registry"
|
|
44
|
+
exit 0
|
|
45
|
+
fi
|
|
46
|
+
echo "Waiting for npm registry propagation ($attempt/120)..."
|
|
47
|
+
sleep 15
|
|
48
|
+
done
|
|
49
|
+
echo "Timed out waiting for do-again@$version to become publicly available" >&2
|
|
50
|
+
exit 1
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
16
|
+
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.13"
|
|
19
|
+
- name: Verify release versions
|
|
20
|
+
run: python tools/check_release_version.py --tag "$GITHUB_REF_NAME"
|
|
21
|
+
- run: python -m pip install --upgrade build twine
|
|
22
|
+
- run: python -m build
|
|
23
|
+
- run: python -m twine check dist/*
|
|
24
|
+
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
25
|
+
with:
|
|
26
|
+
name: python-package-distributions
|
|
27
|
+
path: dist/
|
|
28
|
+
|
|
29
|
+
publish:
|
|
30
|
+
needs: build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
environment:
|
|
33
|
+
name: pypi
|
|
34
|
+
url: https://pypi.org/p/do-again
|
|
35
|
+
permissions:
|
|
36
|
+
id-token: write
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
39
|
+
with:
|
|
40
|
+
name: python-package-distributions
|
|
41
|
+
path: dist/
|
|
42
|
+
- uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # release/v1
|
|
43
|
+
|
|
44
|
+
github-release:
|
|
45
|
+
needs: publish
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
permissions:
|
|
48
|
+
contents: write
|
|
49
|
+
steps:
|
|
50
|
+
- name: Create GitHub release
|
|
51
|
+
env:
|
|
52
|
+
GH_TOKEN: ${{ github.token }}
|
|
53
|
+
run: |
|
|
54
|
+
gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 ||
|
|
55
|
+
gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes --title "Do Again $GITHUB_REF_NAME"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: do-again
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Policy-controlled local execution for agent-driven development workflows
|
|
5
5
|
Project-URL: Homepage, https://github.com/Tran-Steven/do-again
|
|
6
6
|
Project-URL: Repository, https://github.com/Tran-Steven/do-again
|
|
@@ -35,69 +35,124 @@ Description-Content-Type: text/markdown
|
|
|
35
35
|
|
|
36
36
|
# Do Again
|
|
37
37
|
|
|
38
|
-
[](https://github.com/Tran-Steven/do-again/actions/workflows/ci.yml)
|
|
39
|
-
[](https://pypi.org/project/do-again/)
|
|
40
|
-
[](https://github.com/Tran-Steven/do-again/actions/workflows/ci.yml)
|
|
39
|
+
[](https://pypi.org/project/do-again/)
|
|
40
|
+
[](https://www.npmjs.com/package/do-again)
|
|
41
|
+
[](https://pypi.org/project/do-again/)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
Do Again gives AI coding agents a policy-controlled way to execute work on a local development machine, return auditable receipts and artifacts, and keep iterating when another pass is needed.
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
agent
|
|
50
|
+
|
|
|
51
|
+
| request
|
|
52
|
+
v
|
|
53
|
+
operator-control branch
|
|
54
|
+
|
|
|
55
|
+
v
|
|
56
|
+
Do Again -> policy validation -> local command / file / test / git action
|
|
57
|
+
^ |
|
|
58
|
+
| |
|
|
59
|
+
+----------- receipt / artifact -----------+
|
|
60
|
+
```
|
|
42
61
|
|
|
43
|
-
|
|
62
|
+
The agent can inspect the result, decide what still needs work, and submit another scoped action instead of stopping at the first attempt.
|
|
44
63
|
|
|
45
64
|
## Install
|
|
46
65
|
|
|
66
|
+
### Python / PyPI
|
|
67
|
+
|
|
47
68
|
```bash
|
|
48
69
|
pip install do-again
|
|
49
70
|
```
|
|
50
71
|
|
|
51
|
-
|
|
72
|
+
### Node / npm
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install -g do-again
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
or run it without a global install:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx do-again doctor
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Both distributions expose the same command:
|
|
52
85
|
|
|
53
86
|
```bash
|
|
54
87
|
do-again
|
|
55
88
|
```
|
|
56
89
|
|
|
90
|
+
The npm package ships the same Python runtime from this repository behind a small Node launcher. It requires Node.js 18+ and Python 3.11+.
|
|
91
|
+
|
|
57
92
|
## Quick start
|
|
58
93
|
|
|
59
94
|
```bash
|
|
60
95
|
do-again doctor
|
|
61
96
|
do-again init
|
|
97
|
+
do-again install
|
|
62
98
|
do-again status
|
|
63
99
|
```
|
|
64
100
|
|
|
101
|
+
## Service lifecycle
|
|
102
|
+
|
|
103
|
+
On macOS, Do Again can install a per-repository launchd agent:
|
|
104
|
+
|
|
105
|
+
do-again install
|
|
106
|
+
do-again status
|
|
107
|
+
do-again restart
|
|
108
|
+
do-again stop
|
|
109
|
+
do-again uninstall
|
|
110
|
+
|
|
111
|
+
For attended or cross-platform use, run the agent in the foreground:
|
|
112
|
+
|
|
113
|
+
do-again run
|
|
114
|
+
do-again run --once
|
|
115
|
+
|
|
116
|
+
Each repository gets an isolated runtime, control worktree, state directory, policy copy, and service label under ~/.do_again. The installed service runs from a copied runtime so installs made through either PyPI or npm remain stable after the invoking shell exits.
|
|
117
|
+
|
|
65
118
|
## Why Do Again
|
|
66
119
|
|
|
67
120
|
- Explicit policy-based command permissions
|
|
68
121
|
- GitHub-backed request and receipt transport
|
|
69
|
-
- Persistent local background execution architecture
|
|
70
122
|
- Project-scoped filesystem and command access
|
|
71
123
|
- Deterministic audit trail and recovery
|
|
72
124
|
- No agent API keys embedded in the project
|
|
73
125
|
- A shared platform interface for macOS, Linux, and Windows
|
|
126
|
+
- CI-tested on Python 3.11, 3.12, and 3.13 across all three platforms
|
|
74
127
|
|
|
75
128
|
## Platform status
|
|
76
129
|
|
|
77
130
|
| Platform | Status | Service backend |
|
|
78
131
|
| --- | --- | --- |
|
|
79
|
-
| macOS |
|
|
80
|
-
| Linux |
|
|
81
|
-
| Windows |
|
|
132
|
+
| macOS | Service lifecycle working | launchd |
|
|
133
|
+
| Linux | Foreground runner working; service adapter scaffolded | systemd user service |
|
|
134
|
+
| Windows | Foreground runner working; service adapter scaffolded | Windows service |
|
|
82
135
|
|
|
83
|
-
Do Again is currently alpha software. macOS is the first
|
|
136
|
+
Do Again is currently alpha software. The foreground runner works across the supported Python platforms; macOS is the first platform with an installable background service while Linux and Windows service integration are being completed behind the same interface.
|
|
84
137
|
|
|
85
138
|
## Development
|
|
86
139
|
|
|
87
140
|
```bash
|
|
88
141
|
git clone https://github.com/Tran-Steven/do-again.git
|
|
89
142
|
cd do-again
|
|
90
|
-
python -m pip install
|
|
143
|
+
python -m pip install .
|
|
91
144
|
python -m unittest discover -s tests -v
|
|
92
145
|
do-again doctor
|
|
93
146
|
```
|
|
94
147
|
|
|
95
148
|
## Release model
|
|
96
149
|
|
|
97
|
-
Releases use semantic versioning. Pushing a version tag such as `v0.
|
|
150
|
+
Releases use semantic versioning. Pushing a version tag such as `v0.2.0` validates the shared release version, publishes the Python distribution to PyPI and the Node launcher to npm through Trusted Publishing, and creates the matching GitHub Release.
|
|
98
151
|
|
|
99
152
|
PyPI distribution name: `do-again`
|
|
100
153
|
|
|
154
|
+
npm package name: `do-again`
|
|
155
|
+
|
|
101
156
|
CLI command: `do-again`
|
|
102
157
|
|
|
103
158
|
Python package: `do_again`
|
do_again-0.2.0/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Do Again
|
|
2
|
+
|
|
3
|
+
[](https://github.com/Tran-Steven/do-again/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/do-again/)
|
|
5
|
+
[](https://www.npmjs.com/package/do-again)
|
|
6
|
+
[](https://pypi.org/project/do-again/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Do Again gives AI coding agents a policy-controlled way to execute work on a local development machine, return auditable receipts and artifacts, and keep iterating when another pass is needed.
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
agent
|
|
15
|
+
|
|
|
16
|
+
| request
|
|
17
|
+
v
|
|
18
|
+
operator-control branch
|
|
19
|
+
|
|
|
20
|
+
v
|
|
21
|
+
Do Again -> policy validation -> local command / file / test / git action
|
|
22
|
+
^ |
|
|
23
|
+
| |
|
|
24
|
+
+----------- receipt / artifact -----------+
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The agent can inspect the result, decide what still needs work, and submit another scoped action instead of stopping at the first attempt.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
### Python / PyPI
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install do-again
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Node / npm
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g do-again
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
or run it without a global install:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx do-again doctor
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Both distributions expose the same command:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
do-again
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The npm package ships the same Python runtime from this repository behind a small Node launcher. It requires Node.js 18+ and Python 3.11+.
|
|
56
|
+
|
|
57
|
+
## Quick start
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
do-again doctor
|
|
61
|
+
do-again init
|
|
62
|
+
do-again install
|
|
63
|
+
do-again status
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Service lifecycle
|
|
67
|
+
|
|
68
|
+
On macOS, Do Again can install a per-repository launchd agent:
|
|
69
|
+
|
|
70
|
+
do-again install
|
|
71
|
+
do-again status
|
|
72
|
+
do-again restart
|
|
73
|
+
do-again stop
|
|
74
|
+
do-again uninstall
|
|
75
|
+
|
|
76
|
+
For attended or cross-platform use, run the agent in the foreground:
|
|
77
|
+
|
|
78
|
+
do-again run
|
|
79
|
+
do-again run --once
|
|
80
|
+
|
|
81
|
+
Each repository gets an isolated runtime, control worktree, state directory, policy copy, and service label under ~/.do_again. The installed service runs from a copied runtime so installs made through either PyPI or npm remain stable after the invoking shell exits.
|
|
82
|
+
|
|
83
|
+
## Why Do Again
|
|
84
|
+
|
|
85
|
+
- Explicit policy-based command permissions
|
|
86
|
+
- GitHub-backed request and receipt transport
|
|
87
|
+
- Project-scoped filesystem and command access
|
|
88
|
+
- Deterministic audit trail and recovery
|
|
89
|
+
- No agent API keys embedded in the project
|
|
90
|
+
- A shared platform interface for macOS, Linux, and Windows
|
|
91
|
+
- CI-tested on Python 3.11, 3.12, and 3.13 across all three platforms
|
|
92
|
+
|
|
93
|
+
## Platform status
|
|
94
|
+
|
|
95
|
+
| Platform | Status | Service backend |
|
|
96
|
+
| --- | --- | --- |
|
|
97
|
+
| macOS | Service lifecycle working | launchd |
|
|
98
|
+
| Linux | Foreground runner working; service adapter scaffolded | systemd user service |
|
|
99
|
+
| Windows | Foreground runner working; service adapter scaffolded | Windows service |
|
|
100
|
+
|
|
101
|
+
Do Again is currently alpha software. The foreground runner works across the supported Python platforms; macOS is the first platform with an installable background service while Linux and Windows service integration are being completed behind the same interface.
|
|
102
|
+
|
|
103
|
+
## Development
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
git clone https://github.com/Tran-Steven/do-again.git
|
|
107
|
+
cd do-again
|
|
108
|
+
python -m pip install .
|
|
109
|
+
python -m unittest discover -s tests -v
|
|
110
|
+
do-again doctor
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Release model
|
|
114
|
+
|
|
115
|
+
Releases use semantic versioning. Pushing a version tag such as `v0.2.0` validates the shared release version, publishes the Python distribution to PyPI and the Node launcher to npm through Trusted Publishing, and creates the matching GitHub Release.
|
|
116
|
+
|
|
117
|
+
PyPI distribution name: `do-again`
|
|
118
|
+
|
|
119
|
+
npm package name: `do-again`
|
|
120
|
+
|
|
121
|
+
CLI command: `do-again`
|
|
122
|
+
|
|
123
|
+
Python package: `do_again`
|
|
124
|
+
|
|
125
|
+
## Security
|
|
126
|
+
|
|
127
|
+
Do Again is designed around explicit allowlists, scoped filesystem roots, bounded execution, and auditable request/receipt records. It should not be configured as unrestricted shell access.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const { spawnSync } = require("node:child_process");
|
|
8
|
+
|
|
9
|
+
const MIN_PYTHON = [3, 11];
|
|
10
|
+
|
|
11
|
+
function parsePythonVersion(output) {
|
|
12
|
+
const match = String(output || "").match(/Python\s+(\d+)\.(\d+)(?:\.(\d+))?/i);
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
return [Number(match[1]), Number(match[2]), Number(match[3] || 0)];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function versionAtLeast(version, minimum = MIN_PYTHON) {
|
|
18
|
+
if (!version) return false;
|
|
19
|
+
if (version[0] !== minimum[0]) return version[0] > minimum[0];
|
|
20
|
+
return version[1] >= minimum[1];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function candidates() {
|
|
24
|
+
const configured = process.env.DO_AGAIN_PYTHON;
|
|
25
|
+
if (configured) return [{ command: configured, prefix: [] }];
|
|
26
|
+
|
|
27
|
+
if (process.platform === "win32") {
|
|
28
|
+
return [
|
|
29
|
+
{ command: "py", prefix: ["-3"] },
|
|
30
|
+
{ command: "python", prefix: [] },
|
|
31
|
+
{ command: "python3", prefix: [] }
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
return [
|
|
35
|
+
{ command: "python3", prefix: [] },
|
|
36
|
+
{ command: "python", prefix: [] }
|
|
37
|
+
];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function findPython() {
|
|
41
|
+
for (const candidate of candidates()) {
|
|
42
|
+
const probe = spawnSync(candidate.command, [...candidate.prefix, "--version"], {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
windowsHide: true
|
|
45
|
+
});
|
|
46
|
+
if (probe.error || probe.status !== 0) continue;
|
|
47
|
+
const version = parsePythonVersion(`${probe.stdout || ""}\n${probe.stderr || ""}`);
|
|
48
|
+
if (versionAtLeast(version)) return candidate;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function main() {
|
|
54
|
+
const python = findPython();
|
|
55
|
+
if (!python) {
|
|
56
|
+
console.error("Do Again requires Python 3.11 or newer.");
|
|
57
|
+
console.error("Install Python, or set DO_AGAIN_PYTHON to a Python 3.11+ executable.");
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const packageRoot = path.resolve(__dirname, "..");
|
|
62
|
+
const sourceRoot = path.join(packageRoot, "src");
|
|
63
|
+
const packageDir = path.join(sourceRoot, "do_again");
|
|
64
|
+
if (!fs.existsSync(packageDir)) {
|
|
65
|
+
console.error(`Do Again runtime is missing from the npm package: ${packageDir}`);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const env = { ...process.env };
|
|
70
|
+
env.PYTHONDONTWRITEBYTECODE = env.PYTHONDONTWRITEBYTECODE || "1";
|
|
71
|
+
env.PYTHONPATH = env.PYTHONPATH
|
|
72
|
+
? `${sourceRoot}${path.delimiter}${env.PYTHONPATH}`
|
|
73
|
+
: sourceRoot;
|
|
74
|
+
|
|
75
|
+
const child = spawnSync(
|
|
76
|
+
python.command,
|
|
77
|
+
[...python.prefix, "-m", "do_again.cli", ...process.argv.slice(2)],
|
|
78
|
+
{
|
|
79
|
+
cwd: process.cwd(),
|
|
80
|
+
env,
|
|
81
|
+
stdio: "inherit",
|
|
82
|
+
windowsHide: true
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (child.error) {
|
|
87
|
+
console.error(`Failed to launch Do Again: ${child.error.message}`);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
if (child.signal) return 1;
|
|
91
|
+
return child.status ?? 1;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
process.exitCode = main();
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const assert = require("node:assert/strict");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const { spawnSync } = require("node:child_process");
|
|
6
|
+
const test = require("node:test");
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(__dirname, "..");
|
|
9
|
+
const launcher = path.join(root, "bin", "do-again.js");
|
|
10
|
+
|
|
11
|
+
function run(args, extraEnv = {}) {
|
|
12
|
+
return spawnSync(process.execPath, [launcher, ...args], {
|
|
13
|
+
cwd: root,
|
|
14
|
+
encoding: "utf8",
|
|
15
|
+
env: { ...process.env, ...extraEnv }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
test("npm launcher runs the bundled Python CLI", () => {
|
|
20
|
+
const result = run(["status"]);
|
|
21
|
+
assert.equal(result.status, 0, result.stderr);
|
|
22
|
+
assert.match(result.stdout, /platform=/);
|
|
23
|
+
assert.match(result.stdout, /service_manager=/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("npm launcher reports a missing configured Python cleanly", () => {
|
|
27
|
+
const result = run(["status"], { DO_AGAIN_PYTHON: "definitely-not-a-python-executable" });
|
|
28
|
+
assert.equal(result.status, 1);
|
|
29
|
+
assert.match(result.stderr, /requires Python 3\.11 or newer/);
|
|
30
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "do-again",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "do-again",
|
|
9
|
+
"version": "0.2.0",
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"bin": {
|
|
12
|
+
"do-again": "bin/do-again.js"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "do-again",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Policy-controlled local execution for AI coding agents",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Steven Tran",
|
|
7
|
+
"type": "commonjs",
|
|
8
|
+
"bin": {
|
|
9
|
+
"do-again": "bin/do-again.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin/",
|
|
13
|
+
"src/do_again/**/*.py",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"src/do_again/default_policy.json"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/Tran-Steven/do-again.git"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/Tran-Steven/do-again/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/Tran-Steven/do-again#readme",
|
|
29
|
+
"keywords": [
|
|
30
|
+
"ai-agents",
|
|
31
|
+
"coding-agents",
|
|
32
|
+
"automation",
|
|
33
|
+
"developer-tools",
|
|
34
|
+
"python",
|
|
35
|
+
"local-execution"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "node --test npm-tests/*.test.js",
|
|
39
|
+
"prepack": "npm test"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|