procoder-cli 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTORS.md +18 -0
- package/Makefile +2 -1
- package/README.md +97 -77
- package/cmd/procoder-return/main.go +2 -1
- package/internal/app/app.go +12 -6
- package/internal/app/app_test.go +22 -1
- package/internal/buildinfo/buildinfo.go +16 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +9 -2
package/CONTRIBUTORS.md
CHANGED
|
@@ -11,6 +11,24 @@ Maintainer notes for this template repository.
|
|
|
11
11
|
|
|
12
12
|
## Local development
|
|
13
13
|
|
|
14
|
+
Common local commands:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
make fmt
|
|
18
|
+
make test
|
|
19
|
+
make vet
|
|
20
|
+
make lint
|
|
21
|
+
make check
|
|
22
|
+
make build
|
|
23
|
+
make build-helper
|
|
24
|
+
make build-all
|
|
25
|
+
make install-local
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`make build-all` produces the release binaries for the host CLI targets plus the packaged helper asset `procoder-return_linux_amd64`.
|
|
29
|
+
|
|
30
|
+
Quick local smoke path:
|
|
31
|
+
|
|
14
32
|
```bash
|
|
15
33
|
make check
|
|
16
34
|
make build
|
package/Makefile
CHANGED
|
@@ -12,7 +12,8 @@ HELPER_CMD_PATH ?= ./cmd/$(HELPER_NAME)
|
|
|
12
12
|
DIST_DIR ?= dist
|
|
13
13
|
BIN_PATH ?= $(DIST_DIR)/$(BIN_NAME)
|
|
14
14
|
HELPER_PATH ?= $(DIST_DIR)/$(HELPER_ASSET)
|
|
15
|
-
|
|
15
|
+
VERSION ?= $(shell node -p "require('./package.json').version" 2>/dev/null)
|
|
16
|
+
LDFLAGS ?= -s -w -X github.com/amxv/procoder/internal/buildinfo.Version=$(if $(VERSION),$(VERSION),dev)
|
|
16
17
|
|
|
17
18
|
.PHONY: help fmt test vet lint check build build-helper build-all install-local clean release-tag
|
|
18
19
|
|
package/README.md
CHANGED
|
@@ -1,125 +1,145 @@
|
|
|
1
1
|
# procoder
|
|
2
2
|
|
|
3
|
-
`procoder` is a
|
|
3
|
+
`procoder` is a CLI for getting real Git commits back from ChatGPT's coding sandbox.
|
|
4
4
|
|
|
5
|
-
- a
|
|
6
|
-
- a locked-down ChatGPT coding container
|
|
5
|
+
ChatGPT 5.4 Pro is the best coding model, but it is not available in Codex. `procoder` solves that specific problem: your repository stays local, ChatGPT works inside its locked-down sandbox, and you still get real commits back through a clean upload/download workflow powered by `git bundle`.
|
|
7
6
|
|
|
8
|
-
The
|
|
7
|
+
The workflow is simple:
|
|
9
8
|
|
|
10
|
-
1.
|
|
11
|
-
2.
|
|
12
|
-
3.
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
1. You run `procoder prepare` and upload the generated zip.
|
|
10
|
+
2. ChatGPT works inside that package and gives you an optimized incremental Git bundle as a zip file.
|
|
11
|
+
3. You run `procoder apply`, which applies the commit ChatGPT made.
|
|
12
|
+
|
|
13
|
+
It is Git-native, not a patch-copying workaround.
|
|
15
14
|
|
|
16
15
|
## Install
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
Install globally with npm:
|
|
19
18
|
|
|
20
19
|
```bash
|
|
21
20
|
npm i -g procoder-cli
|
|
22
21
|
procoder --help
|
|
23
22
|
```
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
procoder prepare
|
|
28
|
+
```
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
- the packaged `linux/amd64` `procoder-return` helper used inside prepared task packages
|
|
30
|
+
Upload the generated task package to ChatGPT and give it a prompt like:
|
|
29
31
|
|
|
30
|
-
|
|
32
|
+
```text
|
|
33
|
+
Make the requested changes in this repository.
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
Commit them on the prepared branch.
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
Then run ./procoder-return and give me the sandbox path to the generated zip.
|
|
38
|
+
```
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
After downloading the return package locally:
|
|
37
41
|
|
|
38
42
|
```bash
|
|
39
|
-
procoder
|
|
43
|
+
procoder apply procoder-return-<exchange-id>.zip
|
|
40
44
|
```
|
|
41
45
|
|
|
42
|
-
|
|
46
|
+
If you want to inspect the import first:
|
|
43
47
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- builds a sanitized export repo that includes local heads and tags for read-only context
|
|
48
|
-
- injects the `procoder-return` helper at the exported repo root
|
|
49
|
-
- writes `./procoder-task-<exchange-id>.zip` in the source repo root
|
|
48
|
+
```bash
|
|
49
|
+
procoder apply procoder-return-<exchange-id>.zip --dry-run
|
|
50
|
+
```
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
## Why `git bundle` is the right primitive
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
`git bundle` is what makes this workflow feel native instead of fragile.
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
It lets `procoder` move Git commits, trees, blobs, and refs as a portable file, without requiring a network connection or a Git server. That matters because the ChatGPT sandbox can work with Git locally, but it cannot push to your remote.
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
Using an incremental bundle also keeps the return package small. The sandbox sends back only the objects created after `prepare`, not the whole repository again.
|
|
59
|
+
|
|
60
|
+
## Workflow Diagram
|
|
61
|
+
|
|
62
|
+
```mermaid
|
|
63
|
+
flowchart TD
|
|
64
|
+
A[Local Git repo] -->|procoder prepare| B[Task zip]
|
|
65
|
+
B -->|Upload to ChatGPT| C[ChatGPT codes, commits on the prepared branch,<br/>and runs the included ./procoder-return binary]
|
|
66
|
+
C -->|Download return zip and run procoder apply| D[Local task branch updated]
|
|
59
67
|
```
|
|
60
68
|
|
|
61
|
-
The
|
|
69
|
+
## The Three Tools
|
|
70
|
+
|
|
71
|
+
### `procoder prepare`
|
|
62
72
|
|
|
63
|
-
|
|
64
|
-
- rejects dirty worktrees, tag changes, and branch changes outside `refs/heads/procoder/<exchange-id>/*`
|
|
65
|
-
- bundles only task-family refs that descend from the prepared base commit
|
|
66
|
-
- writes `./procoder-return-<exchange-id>.zip` at the repo root
|
|
73
|
+
Runs locally in your clean repository.
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
It creates a dedicated task branch, builds a sanitized export of your repo, injects the `procoder-return` helper, and writes a task zip you can upload to ChatGPT.
|
|
76
|
+
|
|
77
|
+
### `./procoder-return`
|
|
78
|
+
|
|
79
|
+
Runs inside the exported repository in the ChatGPT sandbox.
|
|
80
|
+
|
|
81
|
+
It is a simple agent-friendly binary with no arguments or subcommands. After making one or more commits, the agent runs `./procoder-return`, and then sends back the path to the generated zip file.
|
|
69
82
|
|
|
70
83
|
### `procoder apply`
|
|
71
84
|
|
|
72
|
-
|
|
85
|
+
Runs locally in your original repository.
|
|
73
86
|
|
|
74
|
-
|
|
75
|
-
procoder apply procoder-return-<exchange-id>.zip
|
|
76
|
-
```
|
|
87
|
+
It verifies the returned bundle, checks whether the target refs are still safe to update, and then imports the returned commits into your repo.
|
|
77
88
|
|
|
78
|
-
|
|
89
|
+
## How it works under the hood
|
|
79
90
|
|
|
80
|
-
|
|
81
|
-
- `--namespace <prefix>`: import returned refs under `refs/heads/<prefix>/<exchange-id>/...`
|
|
82
|
-
- `--checkout`: check out the updated default task branch after a successful apply
|
|
91
|
+
At a high level, `procoder` works because it ships Git history into the sandbox and ships only new Git history back out.
|
|
83
92
|
|
|
84
|
-
|
|
93
|
+
### 1. `prepare` makes an offline-capable repo
|
|
85
94
|
|
|
86
|
-
|
|
87
|
-
procoder apply procoder-return-<exchange-id>.zip
|
|
88
|
-
procoder apply procoder-return-<exchange-id>.zip --dry-run
|
|
89
|
-
procoder apply procoder-return-<exchange-id>.zip --namespace procoder-import
|
|
90
|
-
procoder apply procoder-return-<exchange-id>.zip --checkout
|
|
91
|
-
```
|
|
95
|
+
The task package is a sanitized Git repository, not just a folder of files.
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
That export:
|
|
94
98
|
|
|
95
|
-
|
|
99
|
+
- includes your tracked files
|
|
100
|
+
- includes local branches and tags for read-only context
|
|
101
|
+
- creates a dedicated task branch for the exchange
|
|
102
|
+
- strips remotes, credentials, hooks, reflogs, and other local-only Git state
|
|
103
|
+
- includes a Linux `procoder-return` helper binary at the repo root
|
|
96
104
|
|
|
97
|
-
|
|
98
|
-
procoder --help
|
|
99
|
-
procoder prepare
|
|
100
|
-
procoder apply <return-package.zip>
|
|
101
|
-
procoder apply <return-package.zip> --dry-run
|
|
102
|
-
procoder apply <return-package.zip> --namespace <prefix>
|
|
103
|
-
procoder apply <return-package.zip> --checkout
|
|
104
|
-
procoder version
|
|
105
|
-
```
|
|
105
|
+
So when ChatGPT opens the zip, it already has a working repo with real history and a branch ready to commit on. No internet connection is required for that part because the relevant Git state is already inside the upload.
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
### 2. ChatGPT returns only the incremental update
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
After ChatGPT commits its changes, `./procoder-return` does not re-export the entire repository.
|
|
110
110
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
111
|
+
Instead, it creates:
|
|
112
|
+
|
|
113
|
+
- `procoder-return.json`
|
|
114
|
+
a small manifest describing the returned refs and commit IDs
|
|
115
|
+
- `procoder-return.bundle`
|
|
116
|
+
an incremental Git bundle containing only the new objects needed to move those refs forward
|
|
117
|
+
|
|
118
|
+
That is the key idea. The return package is a portable Git layer that can be applied on top of the base commit prepared earlier.
|
|
119
|
+
|
|
120
|
+
### 3. `apply` verifies before it updates anything
|
|
121
|
+
|
|
122
|
+
When you run `procoder apply`, the CLI verifies the bundle, imports it into a temporary namespace, checks that the imported refs match the manifest, and only then updates your local task branch when it is safe.
|
|
123
|
+
|
|
124
|
+
If the branch moved locally, or the return package tries to update something outside the allowed exchange branch family, `procoder` fails clearly instead of guessing.
|
|
125
|
+
|
|
126
|
+
That gives you the practical feeling of "ChatGPT committed to my repo", while still keeping the operation local and controlled.
|
|
127
|
+
|
|
128
|
+
## Current V1 Constraints
|
|
129
|
+
|
|
130
|
+
The happy path is intentionally narrow:
|
|
131
|
+
|
|
132
|
+
- run `procoder prepare` from a clean repo
|
|
133
|
+
- no Git LFS support
|
|
134
|
+
- no submodule support
|
|
135
|
+
- returned work must stay inside the prepared exchange branch family
|
|
136
|
+
- tag changes are not part of the V1 return flow
|
|
137
|
+
|
|
138
|
+
Those constraints keep the round trip predictable and let `apply` behave like "update when safe, otherwise fail."
|
|
122
139
|
|
|
123
|
-
|
|
140
|
+
## Docs
|
|
124
141
|
|
|
125
|
-
|
|
142
|
+
- [How It Works](docs/how-it-works.md)
|
|
143
|
+
- [Command Reference](docs/commands.md)
|
|
144
|
+
- [Agent Guidance](AGENTS.md)
|
|
145
|
+
- [Maintainer Notes](CONTRIBUTORS.md)
|
|
@@ -5,12 +5,13 @@ import (
|
|
|
5
5
|
"io"
|
|
6
6
|
"os"
|
|
7
7
|
|
|
8
|
+
"github.com/amxv/procoder/internal/buildinfo"
|
|
8
9
|
"github.com/amxv/procoder/internal/errs"
|
|
9
10
|
"github.com/amxv/procoder/internal/output"
|
|
10
11
|
"github.com/amxv/procoder/internal/returnpkg"
|
|
11
12
|
)
|
|
12
13
|
|
|
13
|
-
var version =
|
|
14
|
+
var version = buildinfo.CurrentVersion()
|
|
14
15
|
var runReturn = returnpkg.Run
|
|
15
16
|
|
|
16
17
|
func main() {
|
package/internal/app/app.go
CHANGED
|
@@ -6,13 +6,14 @@ import (
|
|
|
6
6
|
"strings"
|
|
7
7
|
|
|
8
8
|
"github.com/amxv/procoder/internal/apply"
|
|
9
|
+
"github.com/amxv/procoder/internal/buildinfo"
|
|
9
10
|
"github.com/amxv/procoder/internal/errs"
|
|
10
11
|
"github.com/amxv/procoder/internal/prepare"
|
|
11
12
|
)
|
|
12
13
|
|
|
13
14
|
const commandName = "procoder"
|
|
14
15
|
|
|
15
|
-
var version =
|
|
16
|
+
var version = buildinfo.CurrentVersion()
|
|
16
17
|
var runPrepare = prepare.Run
|
|
17
18
|
var runApplyDryRun = apply.RunDryRun
|
|
18
19
|
var runApply = apply.Run
|
|
@@ -24,11 +25,12 @@ func Run(args []string, stdout, stderr io.Writer) error {
|
|
|
24
25
|
printRootHelp(stdout)
|
|
25
26
|
return nil
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
switch args[0] {
|
|
29
|
-
case "version":
|
|
28
|
+
if len(args) == 1 && isVersionArg(args[0]) {
|
|
30
29
|
_, _ = fmt.Fprintf(stdout, "%s %s\n", commandName, version)
|
|
31
30
|
return nil
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
switch args[0] {
|
|
32
34
|
case "prepare":
|
|
33
35
|
if len(args) > 1 && isHelpArg(args[1]) {
|
|
34
36
|
printPrepareHelp(stdout)
|
|
@@ -172,22 +174,26 @@ func isHelpArg(v string) bool {
|
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
func isVersionArg(v string) bool {
|
|
178
|
+
return v == "--version"
|
|
179
|
+
}
|
|
180
|
+
|
|
175
181
|
func printRootHelp(w io.Writer) {
|
|
176
182
|
writeLines(w,
|
|
177
183
|
"procoder",
|
|
178
184
|
"",
|
|
179
185
|
"Usage:",
|
|
186
|
+
" procoder [--version]",
|
|
180
187
|
" procoder <command> [arguments]",
|
|
181
188
|
"",
|
|
182
189
|
"Commands:",
|
|
183
190
|
" prepare create a task package",
|
|
184
191
|
" apply <return-package.zip> apply a return package",
|
|
185
|
-
" version print CLI version",
|
|
186
192
|
"",
|
|
187
193
|
"Examples:",
|
|
194
|
+
" procoder --version",
|
|
188
195
|
" procoder prepare",
|
|
189
196
|
" procoder apply procoder-return-<exchange-id>.zip --dry-run",
|
|
190
|
-
" procoder version",
|
|
191
197
|
)
|
|
192
198
|
}
|
|
193
199
|
|
package/internal/app/app_test.go
CHANGED
|
@@ -31,7 +31,7 @@ func TestRunVersion(t *testing.T) {
|
|
|
31
31
|
var out bytes.Buffer
|
|
32
32
|
var errBuf bytes.Buffer
|
|
33
33
|
|
|
34
|
-
err := Run([]string{"version"}, &out, &errBuf)
|
|
34
|
+
err := Run([]string{"--version"}, &out, &errBuf)
|
|
35
35
|
if err != nil {
|
|
36
36
|
t.Fatalf("Run returned error: %v", err)
|
|
37
37
|
}
|
|
@@ -41,6 +41,27 @@ func TestRunVersion(t *testing.T) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
func TestRunVersionSubcommandUnknown(t *testing.T) {
|
|
45
|
+
var out bytes.Buffer
|
|
46
|
+
var errBuf bytes.Buffer
|
|
47
|
+
|
|
48
|
+
err := Run([]string{"version"}, &out, &errBuf)
|
|
49
|
+
if err == nil {
|
|
50
|
+
t.Fatal("expected error for removed version subcommand")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
typed, ok := errs.As(err)
|
|
54
|
+
if !ok {
|
|
55
|
+
t.Fatalf("expected typed error, got %T", err)
|
|
56
|
+
}
|
|
57
|
+
if typed.Code != errs.CodeUnknownCommand {
|
|
58
|
+
t.Fatalf("unexpected error code: %s", typed.Code)
|
|
59
|
+
}
|
|
60
|
+
if !strings.Contains(typed.Message, "unknown command") {
|
|
61
|
+
t.Fatalf("expected unknown command message, got %q", typed.Message)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
44
65
|
func TestRunApplyHelpTerminology(t *testing.T) {
|
|
45
66
|
var out bytes.Buffer
|
|
46
67
|
var errBuf bytes.Buffer
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package buildinfo
|
|
2
|
+
|
|
3
|
+
import "strings"
|
|
4
|
+
|
|
5
|
+
const defaultVersion = "dev"
|
|
6
|
+
|
|
7
|
+
// Version is overridden at build time via linker flags.
|
|
8
|
+
var Version = defaultVersion
|
|
9
|
+
|
|
10
|
+
func CurrentVersion() string {
|
|
11
|
+
trimmed := strings.TrimSpace(Version)
|
|
12
|
+
if trimmed == "" {
|
|
13
|
+
return defaultVersion
|
|
14
|
+
}
|
|
15
|
+
return trimmed
|
|
16
|
+
}
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -60,6 +60,11 @@ function buildReleaseAssetURL({ repoOwner, repoName, version, assetName }) {
|
|
|
60
60
|
return `https://github.com/${repoOwner}/${repoName}/releases/download/v${version}/${assetName}`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function buildLdflags(version) {
|
|
64
|
+
const resolvedVersion = `${version || ""}`.trim() || "dev";
|
|
65
|
+
return `-s -w -X github.com/amxv/procoder/internal/buildinfo.Version=${resolvedVersion}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
63
68
|
function buildInstallPlan({
|
|
64
69
|
pkg,
|
|
65
70
|
cliName,
|
|
@@ -167,6 +172,7 @@ function fallbackBuildOrExit(plan) {
|
|
|
167
172
|
}
|
|
168
173
|
|
|
169
174
|
function buildFallbackBuilds(plan) {
|
|
175
|
+
const ldflags = buildLdflags(plan.version);
|
|
170
176
|
return [
|
|
171
177
|
{
|
|
172
178
|
destination: plan.cli.destination,
|
|
@@ -174,7 +180,7 @@ function buildFallbackBuilds(plan) {
|
|
|
174
180
|
args: [
|
|
175
181
|
"build",
|
|
176
182
|
"-trimpath",
|
|
177
|
-
|
|
183
|
+
`-ldflags=${ldflags}`,
|
|
178
184
|
"-o",
|
|
179
185
|
plan.cli.destination,
|
|
180
186
|
`./cmd/${plan.cli.name}`
|
|
@@ -190,7 +196,7 @@ function buildFallbackBuilds(plan) {
|
|
|
190
196
|
args: [
|
|
191
197
|
"build",
|
|
192
198
|
"-trimpath",
|
|
193
|
-
|
|
199
|
+
`-ldflags=${ldflags}`,
|
|
194
200
|
"-o",
|
|
195
201
|
plan.helper.destination,
|
|
196
202
|
`./cmd/${plan.helper.name}`
|
|
@@ -245,6 +251,7 @@ function downloadToFile(url, destinationPath) {
|
|
|
245
251
|
|
|
246
252
|
module.exports = {
|
|
247
253
|
buildFallbackBuilds,
|
|
254
|
+
buildLdflags,
|
|
248
255
|
buildInstallPlan,
|
|
249
256
|
buildReleaseAssetName,
|
|
250
257
|
buildReleaseAssetURL,
|