procoder-cli 0.1.0

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/AGENTS.md ADDED
@@ -0,0 +1,122 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for coding agents working in `procoder`.
4
+
5
+ ## Purpose
6
+
7
+ This repo builds `procoder`, a Go CLI for an offline Git exchange workflow between:
8
+
9
+ - a local developer repository
10
+ - a locked-down ChatGPT coding container
11
+
12
+ The main user flow is:
13
+
14
+ - `procoder prepare`
15
+ - remote work inside the exported repo
16
+ - `./procoder-return`
17
+ - `procoder apply <return-package.zip>`
18
+
19
+ ## Architecture
20
+
21
+ - `cmd/procoder/main.go`: process entrypoint, error handling, exits non-zero on failure.
22
+ - `cmd/procoder-return/main.go`: remote helper entrypoint used inside prepared task packages.
23
+ - `internal/app/app.go`: command parser + top-level CLI handlers.
24
+ - `internal/exchange/`: exchange IDs, task-ref helpers, and JSON models for `exchange.json` / `procoder-return.json`.
25
+ - `internal/gitx/`: Git command runner with structured output and typed failures.
26
+ - `internal/prepare/`: `procoder prepare` implementation.
27
+ - `internal/returnpkg/`: `./procoder-return` implementation.
28
+ - `internal/apply/`: `procoder apply` implementation.
29
+ - `bin/procoder.js`: npm shim that invokes packaged native binary.
30
+ - `scripts/postinstall.js`: downloads or builds both the host CLI binary and the packaged `linux/amd64` helper.
31
+ - `.github/workflows/release.yml`: tag-driven release pipeline.
32
+
33
+ ## Exchange Model
34
+
35
+ The current Git-valid task-family shape is:
36
+
37
+ - default prepared task branch: `refs/heads/procoder/<exchange-id>/task`
38
+ - writable task-family prefix: `refs/heads/procoder/<exchange-id>`
39
+ - allowed returned refs: `refs/heads/procoder/<exchange-id>/*`
40
+
41
+ Important:
42
+
43
+ - do not reintroduce the invalid older shape where both `refs/heads/procoder/<exchange-id>` and `refs/heads/procoder/<exchange-id>/*` exist at the same time
44
+ - Git cannot store both of those refs because one path would need to be both a ref and a directory
45
+
46
+ Machine-owned metadata lives under `.git/procoder/`.
47
+
48
+ - local exchange record: `.git/procoder/exchanges/<exchange-id>/exchange.json`
49
+ - exported repo exchange record: `.git/procoder/exchange.json`
50
+
51
+ User-facing artifacts live at repo root by default:
52
+
53
+ - task package: `./procoder-task-<exchange-id>.zip`
54
+ - return package: `./procoder-return-<exchange-id>.zip`
55
+
56
+ When changing exchange behavior, keep `gg/agent-outputs/procoder-handoff-v1-product-spec.md` and `gg/agent-outputs/procoder-exchange-v1-internal-spec.md` aligned with the code.
57
+
58
+ ## Local commands
59
+
60
+ Use `make` targets:
61
+
62
+ - `make fmt`
63
+ - `make test`
64
+ - `make vet`
65
+ - `make lint`
66
+ - `make check`
67
+ - `make build`
68
+ - `make build-helper`
69
+ - `make build-all`
70
+ - `make install-local`
71
+
72
+ Direct commands:
73
+
74
+ - `go test ./...`
75
+ - `go vet ./...`
76
+ - `npm run lint`
77
+
78
+ Phase-oriented validation:
79
+
80
+ - `procoder prepare` changes should be covered by integration tests under `internal/prepare/`
81
+ - `procoder-return` changes should be covered by integration tests under `internal/returnpkg/`
82
+ - `procoder apply` changes should be covered by integration tests under `internal/apply/`
83
+
84
+ ## How to customize safely
85
+
86
+ 1. Rename CLI command consistently in all places:
87
+ - directory `cmd/procoder`
88
+ - `package.json` values (`bin`, `config.cliBinaryName`)
89
+ - `bin/procoder.js`
90
+ - workflow env `CLI_BINARY`
91
+ - `Makefile` `BIN_NAME`
92
+
93
+ 2. Keep binary naming convention unchanged unless you also update postinstall/workflow:
94
+ - release assets: `<cli>_<goos>_<goarch>[.exe]`
95
+ - packaged helper asset: `procoder-return_linux_amd64`
96
+ - npm-installed binary path: `bin/<cli>-bin` (or `.exe` on Windows)
97
+
98
+ 3. If adding dependencies, commit `go.sum` and optionally enable Go cache in workflow.
99
+
100
+ 4. Keep help output expressive and command-local (`<command> --help` should explain examples).
101
+
102
+ 5. If you change exchange filenames, helper asset names, or task-family ref naming, update:
103
+ - code
104
+ - tests
105
+ - `README.md`
106
+ - `AGENTS.md`
107
+ - both spec docs under `gg/agent-outputs/`
108
+
109
+ ## Release contract
110
+
111
+ Release pipeline triggers on `v*` tags and expects:
112
+
113
+ - `NPM_TOKEN` GitHub secret present.
114
+ - npm package name in `package.json` is publishable under your account/org.
115
+ - repository URL matches the release origin used by `scripts/postinstall.js`.
116
+
117
+ ## Guardrails
118
+
119
+ - Prefer additive changes; do not break the release asset naming contract unintentionally.
120
+ - If you change release artifacts or CLI binary name, update both workflow and postinstall script in the same PR.
121
+ - Keep agent-facing failures specific and actionable, especially for `procoder-return`.
122
+ - Favor integration tests for real Git behavior over mocked unit-only coverage for exchange flows.
@@ -0,0 +1,79 @@
1
+ # CONTRIBUTORS.md
2
+
3
+ Maintainer notes for this template repository.
4
+
5
+ ## Prerequisites
6
+
7
+ - Go `1.26+`
8
+ - Node `18+`
9
+ - npm account with publish rights for the package name in `package.json`
10
+ - GitHub repo admin access
11
+
12
+ ## Local development
13
+
14
+ ```bash
15
+ make check
16
+ make build
17
+ make build-helper
18
+ ./dist/procoder --help
19
+ ```
20
+
21
+ Install command locally:
22
+
23
+ ```bash
24
+ make install-local
25
+ procoder --help
26
+ ```
27
+
28
+ `make install-local` installs both `procoder` and `procoder-return_linux_amd64` into `~/.local/bin`.
29
+
30
+ ## Release process
31
+
32
+ 1. Ensure `main` is green:
33
+
34
+ ```bash
35
+ make check
36
+ ```
37
+
38
+ 2. Prepare release tag:
39
+
40
+ ```bash
41
+ make release-tag VERSION=0.1.0
42
+ ```
43
+
44
+ 3. GitHub Actions `release` workflow runs automatically:
45
+ - quality checks
46
+ - cross-platform `procoder` binary build
47
+ - packaged `procoder-return_linux_amd64` helper build
48
+ - GitHub release publish
49
+ - npm publish
50
+
51
+ ## Required GitHub secret
52
+
53
+ - `NPM_TOKEN`: npm automation token with publish rights for your package.
54
+
55
+ Set via GitHub CLI:
56
+
57
+ ```bash
58
+ gh secret set NPM_TOKEN --repo amxv/procoder
59
+ ```
60
+
61
+ ## npm token setup
62
+
63
+ Create token at npm:
64
+
65
+ - Profile -> Access Tokens -> Create New Token
66
+ - Use an automation/granular token scoped to required package/org
67
+
68
+ Validate auth locally:
69
+
70
+ ```bash
71
+ npm whoami
72
+ ```
73
+
74
+ ## Notes on package naming
75
+
76
+ Before first publish, set a package name you control in `package.json`.
77
+
78
+ - Example unscoped: `"name": "your-cli-name"`
79
+ - Example scoped: `"name": "@your-scope/your-cli-name"`
package/Makefile ADDED
@@ -0,0 +1,82 @@
1
+ SHELL := /bin/bash
2
+
3
+ GO ?= go
4
+ GOFMT ?= gofmt
5
+ BIN_NAME ?= procoder
6
+ CMD_PATH ?= ./cmd/$(BIN_NAME)
7
+ HELPER_NAME ?= procoder-return
8
+ HELPER_GOOS ?= linux
9
+ HELPER_GOARCH ?= amd64
10
+ HELPER_ASSET ?= $(HELPER_NAME)_$(HELPER_GOOS)_$(HELPER_GOARCH)
11
+ HELPER_CMD_PATH ?= ./cmd/$(HELPER_NAME)
12
+ DIST_DIR ?= dist
13
+ BIN_PATH ?= $(DIST_DIR)/$(BIN_NAME)
14
+ HELPER_PATH ?= $(DIST_DIR)/$(HELPER_ASSET)
15
+ LDFLAGS ?= -s -w
16
+
17
+ .PHONY: help fmt test vet lint check build build-helper build-all install-local clean release-tag
18
+
19
+ help:
20
+ @echo "procoder command runner"
21
+ @echo ""
22
+ @echo "Targets:"
23
+ @echo " make fmt - format Go files"
24
+ @echo " make test - run go test ./..."
25
+ @echo " make vet - run go vet ./..."
26
+ @echo " make lint - run Node script checks"
27
+ @echo " make check - fmt + test + vet + lint"
28
+ @echo " make build - build local binary to dist/procoder"
29
+ @echo " make build-helper - build the linux/amd64 procoder-return helper asset"
30
+ @echo " make build-all - build release binaries for 5 target platforms plus helper asset"
31
+ @echo " make install-local - install CLI and helper to ~/.local/bin"
32
+ @echo " make clean - remove dist artifacts"
33
+ @echo " make release-tag - create and push git tag (requires VERSION=x.y.z)"
34
+
35
+ fmt:
36
+ @$(GOFMT) -w $$(find . -type f -name '*.go' -not -path './dist/*')
37
+
38
+ test:
39
+ @$(GO) test ./...
40
+
41
+ vet:
42
+ @$(GO) vet ./...
43
+
44
+ lint:
45
+ @npm run lint
46
+
47
+ check: fmt test vet lint
48
+
49
+ build:
50
+ @mkdir -p $(DIST_DIR)
51
+ @$(GO) build -trimpath -ldflags="$(LDFLAGS)" -o $(BIN_PATH) $(CMD_PATH)
52
+
53
+ build-helper:
54
+ @mkdir -p $(DIST_DIR)
55
+ @CGO_ENABLED=0 GOOS=$(HELPER_GOOS) GOARCH=$(HELPER_GOARCH) \
56
+ $(GO) build -trimpath -ldflags="$(LDFLAGS)" -o $(HELPER_PATH) $(HELPER_CMD_PATH)
57
+
58
+ build-all: build-helper
59
+ @mkdir -p $(DIST_DIR)
60
+ @for target in "darwin amd64" "darwin arm64" "linux amd64" "linux arm64" "windows amd64"; do \
61
+ set -- $$target; \
62
+ GOOS=$$1; GOARCH=$$2; \
63
+ EXT=""; \
64
+ if [ "$$GOOS" = "windows" ]; then EXT=".exe"; fi; \
65
+ echo "Building $(BIN_NAME) for $$GOOS/$$GOARCH"; \
66
+ CGO_ENABLED=0 GOOS=$$GOOS GOARCH=$$GOARCH $(GO) build -trimpath -ldflags="$(LDFLAGS)" -o "$(DIST_DIR)/$(BIN_NAME)_$$GOOS_$$GOARCH$$EXT" $(CMD_PATH); \
67
+ done
68
+
69
+ install-local: build build-helper
70
+ @mkdir -p $$HOME/.local/bin
71
+ @install -m 755 $(BIN_PATH) $$HOME/.local/bin/$(BIN_NAME)
72
+ @install -m 755 $(HELPER_PATH) $$HOME/.local/bin/$(HELPER_ASSET)
73
+ @echo "Installed $(BIN_NAME) to $$HOME/.local/bin/$(BIN_NAME)"
74
+ @echo "Installed $(HELPER_ASSET) to $$HOME/.local/bin/$(HELPER_ASSET)"
75
+
76
+ clean:
77
+ @rm -rf $(DIST_DIR)
78
+
79
+ release-tag:
80
+ @test -n "$(VERSION)" || (echo "Usage: make release-tag VERSION=x.y.z" && exit 1)
81
+ @git tag "v$(VERSION)"
82
+ @git push origin "v$(VERSION)"
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # procoder
2
+
3
+ `procoder` is a Go CLI for an offline Git exchange workflow between:
4
+
5
+ - a local developer repository
6
+ - a locked-down ChatGPT coding container
7
+
8
+ The default round trip is:
9
+
10
+ 1. Run `procoder prepare` in a clean local repository.
11
+ 2. Upload `./procoder-task-<exchange-id>.zip` to ChatGPT.
12
+ 3. Work inside the exported repo, commit on the prepared task branch, and run `./procoder-return`.
13
+ 4. Download `./procoder-return-<exchange-id>.zip`.
14
+ 5. Run `procoder apply <return-package.zip>` in the source repository.
15
+
16
+ ## Install
17
+
18
+ Global install via npm:
19
+
20
+ ```bash
21
+ npm i -g procoder-cli
22
+ procoder --help
23
+ ```
24
+
25
+ The installer fetches two release assets when available:
26
+
27
+ - the host `procoder` binary
28
+ - the packaged `linux/amd64` `procoder-return` helper used inside prepared task packages
29
+
30
+ If those release assets are unavailable, `scripts/postinstall.js` falls back to local Go builds for both binaries.
31
+
32
+ ## Workflow
33
+
34
+ ### `procoder prepare`
35
+
36
+ Run `procoder prepare` from the root of a clean Git worktree:
37
+
38
+ ```bash
39
+ procoder prepare
40
+ ```
41
+
42
+ `prepare` does the following:
43
+
44
+ - validates that the repo is clean and has no untracked files, submodules, or Git LFS usage
45
+ - creates a local task branch at `refs/heads/procoder/<exchange-id>/task`
46
+ - writes the local exchange record to `.git/procoder/exchanges/<exchange-id>/exchange.json`
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
50
+
51
+ The current checkout is left unchanged. On success the command prints the task branch ref and the absolute task package path.
52
+
53
+ ### `./procoder-return`
54
+
55
+ After ChatGPT has made one or more commits inside the exported repo, run the helper from that prepared task package:
56
+
57
+ ```bash
58
+ ./procoder-return
59
+ ```
60
+
61
+ The helper:
62
+
63
+ - verifies that it is running inside a prepared task package
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
67
+
68
+ On success it prints the absolute zip path and a `sandbox:` hint that can be pasted back to the local user.
69
+
70
+ ### `procoder apply`
71
+
72
+ Apply the returned work from the original source repo:
73
+
74
+ ```bash
75
+ procoder apply procoder-return-<exchange-id>.zip
76
+ ```
77
+
78
+ Supported flags:
79
+
80
+ - `--dry-run`: verify the return package and print the ref update plan without mutating refs
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
83
+
84
+ Examples:
85
+
86
+ ```bash
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
+ ```
92
+
93
+ Default apply behavior is "update when safe, otherwise fail." If the prepared task branch moved locally, `procoder apply` fails with a `BRANCH_MOVED` error and suggests retrying with `--namespace`.
94
+
95
+ ## Command Summary
96
+
97
+ ```bash
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
+ ```
106
+
107
+ ## Development
108
+
109
+ Common local commands:
110
+
111
+ ```bash
112
+ make fmt
113
+ make test
114
+ make vet
115
+ make lint
116
+ make check
117
+ make build
118
+ make build-helper
119
+ make build-all
120
+ make install-local
121
+ ```
122
+
123
+ `make build-all` produces the release binaries for the host CLI targets plus the packaged helper asset `procoder-return_linux_amd64`.
124
+
125
+ See [AGENTS.md](AGENTS.md) and [CONTRIBUTORS.md](CONTRIBUTORS.md) for implementation and release details.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { spawnSync } = require("node:child_process");
6
+
7
+ const pkg = require("../package.json");
8
+ const cliName = pkg.config?.cliBinaryName || "procoder";
9
+ const executableName = process.platform === "win32" ? `${cliName}.exe` : `${cliName}-bin`;
10
+ const executablePath = path.join(__dirname, executableName);
11
+
12
+ if (!fs.existsSync(executablePath)) {
13
+ console.error(`${cliName} binary is not installed. Re-run: npm rebuild -g ${pkg.name}`);
14
+ process.exit(1);
15
+ }
16
+
17
+ const child = spawnSync(executablePath, process.argv.slice(2), { stdio: "inherit" });
18
+
19
+ if (child.error) {
20
+ console.error(child.error.message);
21
+ process.exit(1);
22
+ }
23
+
24
+ if (child.signal) {
25
+ process.kill(process.pid, child.signal);
26
+ }
27
+
28
+ process.exit(child.status ?? 1);
@@ -0,0 +1,15 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+
6
+ "github.com/amxv/procoder/internal/app"
7
+ "github.com/amxv/procoder/internal/output"
8
+ )
9
+
10
+ func main() {
11
+ if err := app.Run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
12
+ output.WriteError(os.Stderr, err)
13
+ os.Exit(1)
14
+ }
15
+ }
@@ -0,0 +1,67 @@
1
+ package main
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+ "os"
7
+
8
+ "github.com/amxv/procoder/internal/errs"
9
+ "github.com/amxv/procoder/internal/output"
10
+ "github.com/amxv/procoder/internal/returnpkg"
11
+ )
12
+
13
+ var version = "dev"
14
+ var runReturn = returnpkg.Run
15
+
16
+ func main() {
17
+ if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil {
18
+ output.WriteError(os.Stderr, err)
19
+ os.Exit(1)
20
+ }
21
+ }
22
+
23
+ func run(args []string, stdout, stderr io.Writer) error {
24
+ _ = stderr
25
+
26
+ if len(args) > 0 && isHelpArg(args[0]) {
27
+ printHelp(stdout)
28
+ return nil
29
+ }
30
+ if len(args) > 0 {
31
+ return errs.New(
32
+ errs.CodeUnknownCommand,
33
+ fmt.Sprintf("unknown argument %q for `procoder-return`", args[0]),
34
+ errs.WithHint("run `procoder-return --help`"),
35
+ )
36
+ }
37
+
38
+ result, err := runReturn(returnpkg.Options{ToolVersion: version})
39
+ if err != nil {
40
+ return err
41
+ }
42
+ _, _ = fmt.Fprintln(stdout, returnpkg.FormatSuccess(result))
43
+ return nil
44
+ }
45
+
46
+ func printHelp(w io.Writer) {
47
+ lines := []string{
48
+ "procoder-return - create a return package",
49
+ "",
50
+ "Usage:",
51
+ " procoder-return",
52
+ "",
53
+ "This command creates a return package inside a prepared task package.",
54
+ }
55
+ for _, line := range lines {
56
+ _, _ = io.WriteString(w, line+"\n")
57
+ }
58
+ }
59
+
60
+ func isHelpArg(v string) bool {
61
+ switch v {
62
+ case "-h", "--help", "help":
63
+ return true
64
+ default:
65
+ return false
66
+ }
67
+ }
@@ -0,0 +1,77 @@
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "strings"
6
+ "testing"
7
+
8
+ "github.com/amxv/procoder/internal/errs"
9
+ "github.com/amxv/procoder/internal/returnpkg"
10
+ )
11
+
12
+ func TestPrintHelpTerminology(t *testing.T) {
13
+ var out bytes.Buffer
14
+ printHelp(&out)
15
+
16
+ got := out.String()
17
+ if !strings.Contains(got, "return package") {
18
+ t.Fatalf("expected return package terminology, got: %q", got)
19
+ }
20
+ if !strings.Contains(got, "prepared task package") {
21
+ t.Fatalf("expected prepared task package terminology, got: %q", got)
22
+ }
23
+ if strings.Contains(got, "remote handoff flow") {
24
+ t.Fatalf("unexpected stale wording in help output, got: %q", got)
25
+ }
26
+ }
27
+
28
+ func TestRunSuccessOutputIncludesSandboxHint(t *testing.T) {
29
+ originalRunReturn := runReturn
30
+ t.Cleanup(func() {
31
+ runReturn = originalRunReturn
32
+ })
33
+
34
+ runReturn = func(opts returnpkg.Options) (returnpkg.Result, error) {
35
+ if opts.ToolVersion == "" {
36
+ t.Fatal("expected tool version in returnpkg options")
37
+ }
38
+ return returnpkg.Result{
39
+ ExchangeID: "20260320-120000-a1b2c3",
40
+ ReturnPackagePath: "/tmp/procoder-return-20260320-120000-a1b2c3.zip",
41
+ }, nil
42
+ }
43
+
44
+ var out bytes.Buffer
45
+ var errBuf bytes.Buffer
46
+
47
+ if err := run(nil, &out, &errBuf); err != nil {
48
+ t.Fatalf("run returned error: %v", err)
49
+ }
50
+ got := out.String()
51
+ if !strings.Contains(got, "Created return package.") {
52
+ t.Fatalf("expected success header, got %q", got)
53
+ }
54
+ if !strings.Contains(got, "/tmp/procoder-return-20260320-120000-a1b2c3.zip") {
55
+ t.Fatalf("expected absolute return package path, got %q", got)
56
+ }
57
+ if !strings.Contains(got, "sandbox:/tmp/procoder-return-20260320-120000-a1b2c3.zip") {
58
+ t.Fatalf("expected sandbox hint output, got %q", got)
59
+ }
60
+ }
61
+
62
+ func TestRunUnknownArgument(t *testing.T) {
63
+ var out bytes.Buffer
64
+ var errBuf bytes.Buffer
65
+
66
+ err := run([]string{"unexpected"}, &out, &errBuf)
67
+ if err == nil {
68
+ t.Fatal("expected error")
69
+ }
70
+ typed, ok := errs.As(err)
71
+ if !ok {
72
+ t.Fatalf("expected typed error, got %T", err)
73
+ }
74
+ if typed.Code != errs.CodeUnknownCommand {
75
+ t.Fatalf("unexpected code: got %s want %s", typed.Code, errs.CodeUnknownCommand)
76
+ }
77
+ }
package/go.mod ADDED
@@ -0,0 +1,3 @@
1
+ module github.com/amxv/procoder
2
+
3
+ go 1.26