@agentproto/storage-github 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/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/index.d.ts +232 -0
- package/dist/index.mjs +370 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 agentproto contributors
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @agentproto/storage-github
|
|
2
|
+
|
|
3
|
+
GitHub `WorkspaceSync` provider for `@agentproto/storage` (AIP-35). Clones a
|
|
4
|
+
repo on `pull`, commits with AIP-23 `identity` as git author on `push`, and
|
|
5
|
+
opens PRs per `sync.push.pr_policy`. PR creation is host-side via
|
|
6
|
+
`@octokit/rest` — no in-box `gh` CLI dependency, so the package works
|
|
7
|
+
identically whether the workspace runs locally or inside a sandbox.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { defineGithubStorage } from "@agentproto/storage-github"
|
|
13
|
+
import { hasWorkspaceSync } from "@agentproto/storage"
|
|
14
|
+
|
|
15
|
+
const handle = defineGithubStorage({
|
|
16
|
+
repoUrl: "https://github.com/owner/repo",
|
|
17
|
+
branchPolicy: "per-conversation",
|
|
18
|
+
prPolicy: "auto",
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// The factory slot is what the workspace/corpus layer calls with a runtime
|
|
22
|
+
// context (workspaceDir, token, identity).
|
|
23
|
+
const fs = handle.factory({
|
|
24
|
+
workspaceDir: "/tmp/work",
|
|
25
|
+
token: process.env.GITHUB_TOKEN!,
|
|
26
|
+
identity: { name: "Ops Bot", email: "ops@example.com" },
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
if (hasWorkspaceSync(fs)) {
|
|
30
|
+
await fs.pull(fs) // seed the tree
|
|
31
|
+
// … agent writes to fs …
|
|
32
|
+
await fs.push(fs, { label: "session-42", summary: "Apply operator edits" })
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The `GITHUB_TOKEN` is resolved through the existing `@agentproto/secrets`
|
|
37
|
+
broker (`SecretResolver` from `@agentproto/secrets/exposure`) by the caller
|
|
38
|
+
and passed in the factory context — never inlined in config, never logged.
|
|
39
|
+
|
|
40
|
+
## Executor choice: octokit (host-side) vs gh CLI (in-box)
|
|
41
|
+
|
|
42
|
+
This package uses `@octokit/rest` host-side for PR creation. Rationale:
|
|
43
|
+
|
|
44
|
+
- No coupling to the e2b sandbox template (which would need `gh` baked in).
|
|
45
|
+
- The token already reaches the host via the secrets broker; reusing it for
|
|
46
|
+
the octokit REST call is one fewer hop.
|
|
47
|
+
- Works the same whether the workspace is local or sandboxed.
|
|
48
|
+
|
|
49
|
+
If `gh` is later baked into the sandbox template, a `gh`-based `PrCreator`
|
|
50
|
+
can be slotted in via the factory context without touching this package's
|
|
51
|
+
public API.
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT — see [LICENSE](./LICENSE).
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import * as _agentproto_storage from '@agentproto/storage';
|
|
2
|
+
import { WorkspaceSync, SyncTree } from '@agentproto/storage';
|
|
3
|
+
export { PullResult, PushOptions, PushResult, SyncTree, WorkspaceSync } from '@agentproto/storage';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thin typed wrapper over `child_process.spawnSync` for git commands.
|
|
7
|
+
*
|
|
8
|
+
* The token is NEVER passed on the command line (git would log it in
|
|
9
|
+
* process listings and the auto-classifier blocks plaintext secrets). It
|
|
10
|
+
* reaches git through the environment as `GIT_HTTP_EXTRAHEADER` via a
|
|
11
|
+
* `.git/config` http.https.<host>.extraheader entry — the standard
|
|
12
|
+
* non-interactive credential pattern for `git clone`/`push` against
|
|
13
|
+
* GitHub HTTPS URLs.
|
|
14
|
+
*
|
|
15
|
+
* For tests, callers inject a `GitRunner` that returns canned stdout.
|
|
16
|
+
*/
|
|
17
|
+
/** Result of a single git invocation. */
|
|
18
|
+
interface GitResult {
|
|
19
|
+
ok: boolean;
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
status: number | null;
|
|
23
|
+
}
|
|
24
|
+
/** Injectable runner — production uses `realGitRunner`; tests stub it. */
|
|
25
|
+
type GitRunner = (args: readonly string[], opts: GitRunOpts) => GitResult;
|
|
26
|
+
interface GitRunOpts {
|
|
27
|
+
/** Working directory for `git -C`. */
|
|
28
|
+
cwd: string;
|
|
29
|
+
/** Extra env vars (token-bearing header injected here). */
|
|
30
|
+
env?: Record<string, string>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build the env block that injects the GitHub token into git's HTTPS auth.
|
|
34
|
+
* We set `GIT_HTTP_EXTRAHEADER` (a git-credential-env helper convention)
|
|
35
|
+
* AND `GIT_TERMINAL_PROMPT=0` so a missing token fails fast rather than
|
|
36
|
+
* hanging on a TTY prompt.
|
|
37
|
+
*/
|
|
38
|
+
declare function buildGitEnv(baseEnv: NodeJS.ProcessEnv, token: string): Record<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
* Configure a working tree's `.git/config` so subsequent `git` invocations
|
|
41
|
+
* in that tree authenticate against `github.com` with the token. We write
|
|
42
|
+
* the `http.https://github.com/.extraheader` entry via `git config` so the
|
|
43
|
+
* token never appears on the command line of `clone`/`push`/`fetch`.
|
|
44
|
+
*
|
|
45
|
+
* This is idempotent — `git config --replace-all` overwrites a prior entry.
|
|
46
|
+
*/
|
|
47
|
+
declare function writeAuthConfig(runner: GitRunner, cwd: string, token: string): void;
|
|
48
|
+
/** Production runner — spawns `git` with the given args + env. */
|
|
49
|
+
declare const realGitRunner: GitRunner;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @agentproto/storage-github — types.
|
|
53
|
+
*
|
|
54
|
+
* `GithubStorageConfig` is the AIP-35 `config` block for the `github`
|
|
55
|
+
* provider. `GithubFactoryContext` is the runtime bag the workspace/corpus
|
|
56
|
+
* layer passes to the `factory` slot — it carries the resolved `GITHUB_TOKEN`
|
|
57
|
+
* (via the `@agentproto/secrets` broker, never inlined in config) and the
|
|
58
|
+
* AIP-23 `identity` used as the git commit author.
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* AIP-23 identity-ref entry — the subset we use for git author attribution.
|
|
62
|
+
* Mirrors `IdentityRefEntry` from `@agentproto/storage`'s internal `types.ts`
|
|
63
|
+
* (which is not re-exported from the package barrel). We only need the
|
|
64
|
+
* named-author variant; `ref`/`file` entries are skipped by the sync impl
|
|
65
|
+
* (git needs a concrete name + email).
|
|
66
|
+
*/
|
|
67
|
+
interface GithubIdentityRef {
|
|
68
|
+
name: string;
|
|
69
|
+
email: string;
|
|
70
|
+
avatar?: string;
|
|
71
|
+
gpg_key?: string;
|
|
72
|
+
role?: string;
|
|
73
|
+
metadata?: Record<string, unknown>;
|
|
74
|
+
[k: string]: unknown;
|
|
75
|
+
}
|
|
76
|
+
/** Where commits land. Mirrors AIP-35 `sync.push.branch_policy`. */
|
|
77
|
+
type BranchPolicy = "main" | "per-conversation" | "per-turn";
|
|
78
|
+
/** Whether to open PRs automatically. Mirrors AIP-35 `sync.push.pr_policy`. */
|
|
79
|
+
type PrPolicy = "none" | "auto" | "manual";
|
|
80
|
+
/**
|
|
81
|
+
* AIP-35 `config` shape for the `github` provider. The host owns config
|
|
82
|
+
* validation; `@agentproto/storage` keeps `config` opaque (`config: {}`).
|
|
83
|
+
*/
|
|
84
|
+
interface GithubStorageConfig {
|
|
85
|
+
/** HTTPS clone URL, e.g. `https://github.com/owner/repo`. Also the URL
|
|
86
|
+
* used to parse `owner`/`repo` for PR creation unless `cloneUrl` is set. */
|
|
87
|
+
repoUrl: string;
|
|
88
|
+
/** Optional override for the actual git clone/fetch URL when it differs
|
|
89
|
+
* from `repoUrl` (e.g. a mirror or local file:// origin for tests).
|
|
90
|
+
* PR parsing always uses `repoUrl`. */
|
|
91
|
+
cloneUrl?: string;
|
|
92
|
+
/** Where commits land. Default `main`. */
|
|
93
|
+
branchPolicy?: BranchPolicy;
|
|
94
|
+
/** Whether to open PRs. Default `none`. */
|
|
95
|
+
prPolicy?: PrPolicy;
|
|
96
|
+
/** Base branch to PR against. Default `main`. */
|
|
97
|
+
baseBranch?: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Runtime context the workspace/corpus layer passes to the factory slot.
|
|
101
|
+
* The token is resolved by the caller through `@agentproto/secrets/exposure`
|
|
102
|
+
* — never stored in the config or the handle.
|
|
103
|
+
*/
|
|
104
|
+
interface GithubFactoryContext {
|
|
105
|
+
/** Absolute path to the local working tree the filesystem operates on. */
|
|
106
|
+
workspaceDir: string;
|
|
107
|
+
/** Resolved GitHub access token (fine-grained PAT with `contents:write`
|
|
108
|
+
* and, when `prPolicy: "auto"`, `pull-requests:write`). */
|
|
109
|
+
token: string;
|
|
110
|
+
/** AIP-23 identity — primary becomes the git author; additional entries
|
|
111
|
+
* become `Co-authored-by` trailers. Only named entries (name + email)
|
|
112
|
+
* are used; `ref`/`file`-style entries are skipped. */
|
|
113
|
+
identity?: GithubIdentityRef | GithubIdentityRef[];
|
|
114
|
+
/** Optional conversation/turn ids for `branchPolicy: per-conversation|per-turn`. */
|
|
115
|
+
conversationId?: string;
|
|
116
|
+
turnId?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* PR creation — host-side `@octokit/rest`.
|
|
121
|
+
*
|
|
122
|
+
* The `PrCreator` interface lets tests stub the octokit call without
|
|
123
|
+
* importing the SDK. Production uses `createOctokitPrCreator`, which
|
|
124
|
+
* lazily constructs an `Octokit` instance with the token from the
|
|
125
|
+
* factory context.
|
|
126
|
+
*/
|
|
127
|
+
|
|
128
|
+
/** Result of a PR-open attempt. */
|
|
129
|
+
interface PrResult {
|
|
130
|
+
prUrl: string;
|
|
131
|
+
prNumber: number;
|
|
132
|
+
}
|
|
133
|
+
/** Injectable PR-opener. Production uses octokit; tests stub this. */
|
|
134
|
+
interface PrCreator {
|
|
135
|
+
openPr(input: {
|
|
136
|
+
token: string;
|
|
137
|
+
owner: string;
|
|
138
|
+
repo: string;
|
|
139
|
+
head: string;
|
|
140
|
+
base: string;
|
|
141
|
+
title: string;
|
|
142
|
+
body: string;
|
|
143
|
+
}): Promise<PrResult>;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Parse `owner`/`repo` from a GitHub HTTPS clone URL.
|
|
147
|
+
* `https://github.com/owner/repo[.git]` → `{ owner: "owner", repo: "repo" }`.
|
|
148
|
+
*/
|
|
149
|
+
declare function parseGithubRepo(repoUrl: string): {
|
|
150
|
+
owner: string;
|
|
151
|
+
repo: string;
|
|
152
|
+
};
|
|
153
|
+
/** Production `PrCreator` backed by `@octokit/rest`. */
|
|
154
|
+
declare const createOctokitPrCreator: () => PrCreator;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* `WorkspaceSync` impl for the github provider — `pull`/`push` backed by
|
|
158
|
+
* real `git` invocations (host-side) + `@octokit/rest` for PR creation.
|
|
159
|
+
*
|
|
160
|
+
* # Token handling
|
|
161
|
+
* The token reaches git via the `GIT_HTTP_EXTRAHEADER` env var (see
|
|
162
|
+
* `git.ts:buildGitEnv`) — never on the command line, never in the repo
|
|
163
|
+
* URL, never logged. `GIT_TERMINAL_PROMPT=0` makes a missing token fail
|
|
164
|
+
* fast instead of hanging.
|
|
165
|
+
*
|
|
166
|
+
* # AIP-23 identity
|
|
167
|
+
* `identity` is one or more `IdentityRefEntry` records. The primary
|
|
168
|
+
* becomes the git author (`user.name` / `user.email`); the remaining
|
|
169
|
+
* entries become `Co-authored-by: Name <email>` trailers. `ref`-style
|
|
170
|
+
* entries (no name/email) are skipped — git needs a concrete author.
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
/** Extra (test-injected) deps for the sync impl. */
|
|
174
|
+
interface GithubWorkspaceSyncOpts {
|
|
175
|
+
gitRunner?: GitRunner;
|
|
176
|
+
prCreator?: PrCreator;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Create a `WorkspaceSync` bound to `config` + `ctx`. The `tree` argument
|
|
180
|
+
* to `pull`/`push` is the `SyncTree` the caller operates on — git itself
|
|
181
|
+
* runs against `ctx.workspaceDir` (the working tree on disk), so the tree
|
|
182
|
+
* is only used to detect "is the tree empty" on pull and to count files.
|
|
183
|
+
*/
|
|
184
|
+
declare function createGithubWorkspaceSync(config: GithubStorageConfig, ctx: GithubFactoryContext, extra?: GithubWorkspaceSyncOpts): WorkspaceSync;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* `GithubFilesystem` — implements `SyncTree` against a local working-tree
|
|
188
|
+
* directory AND `WorkspaceSync` for the pull/push lifecycle. The factory
|
|
189
|
+
* slot in `defineGithubStorage` returns one of these; callers feature-detect
|
|
190
|
+
* the sync surface with `hasWorkspaceSync(fs)` from `@agentproto/storage`.
|
|
191
|
+
*
|
|
192
|
+
* Paths are workspace-relative; the filesystem resolves them against
|
|
193
|
+
* `workspaceDir` from the factory context. `walk` skips `.git`.
|
|
194
|
+
*/
|
|
195
|
+
|
|
196
|
+
/** Filesystem + sync surface for a github-backed workspace. */
|
|
197
|
+
type GithubFilesystem = SyncTree & WorkspaceSync;
|
|
198
|
+
/**
|
|
199
|
+
* Create a `GithubFilesystem` bound to `workspaceDir`. The `WorkspaceSync`
|
|
200
|
+
* surface (`pull`/`push`) is wired in via `createGithubWorkspaceSync` so
|
|
201
|
+
* `hasWorkspaceSync(fs)` returns true.
|
|
202
|
+
*
|
|
203
|
+
* `extra` is for test injection (`gitRunner`, `prCreator`); production
|
|
204
|
+
* callers omit it.
|
|
205
|
+
*/
|
|
206
|
+
declare function createGithubFilesystem(config: GithubStorageConfig, ctx: GithubFactoryContext, extra?: Partial<Pick<GithubWorkspaceSyncOpts, "gitRunner" | "prCreator">>): GithubFilesystem;
|
|
207
|
+
|
|
208
|
+
/** Capabilities metadata the registry indexes (AIP-43). */
|
|
209
|
+
interface GithubCapabilities extends Record<string, unknown> {
|
|
210
|
+
readonly transport: "git";
|
|
211
|
+
readonly prPolicy: PrPolicy;
|
|
212
|
+
readonly pairsWith: "sandbox";
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Construct a `StorageHandle` for the github provider. The `factory` slot
|
|
216
|
+
* returns a `GithubFilesystem` (which is `SyncTree & WorkspaceSync`) when
|
|
217
|
+
* called with a `GithubFactoryContext` by the workspace/corpus layer.
|
|
218
|
+
*
|
|
219
|
+
* Example:
|
|
220
|
+
* ```ts
|
|
221
|
+
* const handle = defineGithubStorage({
|
|
222
|
+
* repoUrl: "https://github.com/owner/repo",
|
|
223
|
+
* branchPolicy: "per-conversation",
|
|
224
|
+
* prPolicy: "auto",
|
|
225
|
+
* })
|
|
226
|
+
* const fs = handle.factory({ workspaceDir, token, identity })
|
|
227
|
+
* if (hasWorkspaceSync(fs)) await fs.pull(fs)
|
|
228
|
+
* ```
|
|
229
|
+
*/
|
|
230
|
+
declare function defineGithubStorage(config: GithubStorageConfig): _agentproto_storage.StorageRuntimeHandle<(ctx: GithubFactoryContext) => ReturnType<typeof createGithubFilesystem>, GithubCapabilities>;
|
|
231
|
+
|
|
232
|
+
export { type BranchPolicy, type GitResult, type GitRunOpts, type GitRunner, type GithubCapabilities, type GithubFactoryContext, type GithubFilesystem, type GithubStorageConfig, type GithubWorkspaceSyncOpts, type PrCreator, type PrPolicy, type PrResult, buildGitEnv, createGithubFilesystem, createGithubWorkspaceSync, createOctokitPrCreator, defineGithubStorage, parseGithubRepo, realGitRunner, writeAuthConfig };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { defineStorage } from '@agentproto/storage';
|
|
2
|
+
import { mkdir, writeFile, readFile, stat, readdir } from 'fs/promises';
|
|
3
|
+
import { join, sep, relative } from 'path';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { Octokit } from '@octokit/rest';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @agentproto/storage-github v0.1.0-alpha
|
|
9
|
+
* AIP-35 github WorkspaceSync provider.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function buildGitEnv(baseEnv, token) {
|
|
13
|
+
if (!token) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"storage-github: buildGitEnv called with an empty token \u2014 refusing to spawn git (would hang on a TTY prompt or fail unauthenticated)."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
...baseEnv,
|
|
20
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
21
|
+
// `extraheader` is read by git's HTTP transport; the leading `AUTHORIZATION: basic <b64>`
|
|
22
|
+
// is what GitHub expects. base64(`${x-access-token}:${token}`) — the
|
|
23
|
+
// `x-access-token` user is GitHub's documented convention for PAT auth
|
|
24
|
+
// over HTTPS without a username.
|
|
25
|
+
GIT_HTTP_EXTRAHEADER: `Authorization: Basic ${btoa(`x-access-token:${token}`)}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function writeAuthConfig(runner, cwd, token) {
|
|
29
|
+
const header = `Authorization: Basic ${btoa(`x-access-token:${token}`)}`;
|
|
30
|
+
runner(
|
|
31
|
+
[
|
|
32
|
+
"config",
|
|
33
|
+
"--replace-all",
|
|
34
|
+
"http.https://github.com/.extraheader",
|
|
35
|
+
header
|
|
36
|
+
],
|
|
37
|
+
{ cwd }
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
var realGitRunner = (args, opts) => {
|
|
41
|
+
const child = spawnSync("git", [...args], {
|
|
42
|
+
cwd: opts.cwd,
|
|
43
|
+
env: { ...process.env, ...opts.env },
|
|
44
|
+
encoding: "utf8",
|
|
45
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
ok: child.status === 0,
|
|
49
|
+
stdout: child.stdout ?? "",
|
|
50
|
+
stderr: child.stderr ?? "",
|
|
51
|
+
status: child.status
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
function assertGitOk(result, label) {
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
const detail = result.stderr.trim() || result.stdout.trim() || "(no output)";
|
|
57
|
+
throw new Error(
|
|
58
|
+
`storage-github: git ${label} failed (status ${result.status ?? "?"}): ${detail}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function parseGithubRepo(repoUrl) {
|
|
63
|
+
const match = repoUrl.match(/^https?:\/\/github\.com\/([^/]+)\/([^/.?#]+?)(?:\.git)?(?:[/?#].*)?$/);
|
|
64
|
+
if (!match || !match[1] || !match[2]) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`storage-github: parseGithubRepo: could not parse owner/repo from '${repoUrl}'. Expected an HTTPS URL like 'https://github.com/owner/repo'.`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return { owner: match[1], repo: match[2] };
|
|
70
|
+
}
|
|
71
|
+
var createOctokitPrCreator = () => ({
|
|
72
|
+
async openPr(input) {
|
|
73
|
+
const octokit = new Octokit({ auth: input.token });
|
|
74
|
+
const res = await octokit.rest.pulls.create({
|
|
75
|
+
owner: input.owner,
|
|
76
|
+
repo: input.repo,
|
|
77
|
+
head: input.head,
|
|
78
|
+
base: input.base,
|
|
79
|
+
title: input.title,
|
|
80
|
+
body: input.body
|
|
81
|
+
});
|
|
82
|
+
if (!res.data.html_url) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`storage-github: octokit pulls.create returned no html_url for ${input.owner}/${input.repo}`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
return { prUrl: res.data.html_url, prNumber: res.data.number };
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
function buildPrText(opts) {
|
|
91
|
+
const title = opts.label ? `[agentproto] ${opts.label}` : "[agentproto] sync";
|
|
92
|
+
const body = opts.summary ?? "Automated sync push from agentproto workspace.";
|
|
93
|
+
return { title, body };
|
|
94
|
+
}
|
|
95
|
+
function isAutoPr(policy) {
|
|
96
|
+
return policy === "auto";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/github-sync.ts
|
|
100
|
+
var DEFAULT_BASE_BRANCH = "main";
|
|
101
|
+
function resolveConfig(config) {
|
|
102
|
+
return {
|
|
103
|
+
repoUrl: config.repoUrl,
|
|
104
|
+
cloneUrl: config.cloneUrl ?? config.repoUrl,
|
|
105
|
+
branchPolicy: config.branchPolicy ?? "main",
|
|
106
|
+
prPolicy: config.prPolicy ?? "none",
|
|
107
|
+
baseBranch: config.baseBranch ?? DEFAULT_BASE_BRANCH
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
function resolveBranch(policy, baseBranch, ctx) {
|
|
111
|
+
if (policy === "main") return baseBranch;
|
|
112
|
+
if (policy === "per-conversation") {
|
|
113
|
+
const id2 = ctx.conversationId;
|
|
114
|
+
if (!id2) {
|
|
115
|
+
const suffix = `agentproto-${Date.now()}`;
|
|
116
|
+
return `agentproto/${suffix}`;
|
|
117
|
+
}
|
|
118
|
+
return `agentproto/${id2}`;
|
|
119
|
+
}
|
|
120
|
+
const id = ctx.turnId ?? ctx.conversationId;
|
|
121
|
+
if (!id) {
|
|
122
|
+
return `agentproto/turn-${Date.now()}`;
|
|
123
|
+
}
|
|
124
|
+
return `agentproto/turn-${id}`;
|
|
125
|
+
}
|
|
126
|
+
function resolveAuthors(identity) {
|
|
127
|
+
if (!identity) {
|
|
128
|
+
return {
|
|
129
|
+
name: "agentproto",
|
|
130
|
+
email: "agentproto@users.noreply.github.com",
|
|
131
|
+
trailers: []
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const entries = Array.isArray(identity) ? identity : [identity];
|
|
135
|
+
const named = entries.filter(
|
|
136
|
+
(e) => "name" in e && "email" in e && typeof e.name === "string" && typeof e.email === "string"
|
|
137
|
+
);
|
|
138
|
+
const [primary, ...rest] = named;
|
|
139
|
+
if (!primary) {
|
|
140
|
+
return {
|
|
141
|
+
name: "agentproto",
|
|
142
|
+
email: "agentproto@users.noreply.github.com",
|
|
143
|
+
trailers: []
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const trailers = rest.map((e) => `Co-authored-by: ${e.name} <${e.email}>`);
|
|
147
|
+
return { name: primary.name, email: primary.email, trailers };
|
|
148
|
+
}
|
|
149
|
+
function createGithubWorkspaceSync(config, ctx, extra = {}) {
|
|
150
|
+
const runner = extra.gitRunner ?? realGitRunner;
|
|
151
|
+
const prCreator = extra.prCreator ?? createOctokitPrCreator();
|
|
152
|
+
const resolved = resolveConfig(config);
|
|
153
|
+
async function pull(tree, opts) {
|
|
154
|
+
const { workspaceDir, token } = ctx;
|
|
155
|
+
const existing = await tree.walk("");
|
|
156
|
+
const hasGit = existing.some((p) => p === ".git/HEAD" || p.startsWith(".git/"));
|
|
157
|
+
const populated = existing.length > 0 || hasGit;
|
|
158
|
+
if (populated && !opts?.force) {
|
|
159
|
+
const env2 = buildGitEnv(process.env, token);
|
|
160
|
+
const fetchRes = runner(["fetch", "origin", resolved.baseBranch], {
|
|
161
|
+
cwd: workspaceDir,
|
|
162
|
+
env: env2
|
|
163
|
+
});
|
|
164
|
+
assertGitOk(fetchRes, "fetch");
|
|
165
|
+
const mergeRes = runner(
|
|
166
|
+
["merge", `origin/${resolved.baseBranch}`, "--no-edit", "--ff-only"],
|
|
167
|
+
{ cwd: workspaceDir, env: env2 }
|
|
168
|
+
);
|
|
169
|
+
if (!mergeRes.ok) {
|
|
170
|
+
const detail = mergeRes.stderr.trim() || mergeRes.stdout.trim();
|
|
171
|
+
return {
|
|
172
|
+
seeded: false,
|
|
173
|
+
files: 0,
|
|
174
|
+
bytes: 0,
|
|
175
|
+
message: `pull: merge failed (local diverged from origin/${resolved.baseBranch}): ${detail}`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const files2 = await tree.walk("");
|
|
179
|
+
return {
|
|
180
|
+
seeded: false,
|
|
181
|
+
files: files2.length,
|
|
182
|
+
bytes: 0,
|
|
183
|
+
message: `pulled origin/${resolved.baseBranch}`
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const env = buildGitEnv(process.env, token);
|
|
187
|
+
const cloneRes = runner(
|
|
188
|
+
["clone", "--branch", resolved.baseBranch, resolved.cloneUrl, "."],
|
|
189
|
+
{ cwd: workspaceDir, env }
|
|
190
|
+
);
|
|
191
|
+
if (!cloneRes.ok) {
|
|
192
|
+
const detail = cloneRes.stderr.trim() || cloneRes.stdout.trim();
|
|
193
|
+
return {
|
|
194
|
+
seeded: false,
|
|
195
|
+
files: 0,
|
|
196
|
+
bytes: 0,
|
|
197
|
+
message: `pull: clone failed: ${detail}`
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
writeAuthConfig(runner, workspaceDir, token);
|
|
201
|
+
const files = await tree.walk("");
|
|
202
|
+
return {
|
|
203
|
+
seeded: true,
|
|
204
|
+
files: files.length,
|
|
205
|
+
bytes: 0,
|
|
206
|
+
message: `cloned ${resolved.repoUrl} @ ${resolved.baseBranch}`
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async function push(tree, opts) {
|
|
210
|
+
const { workspaceDir, token } = ctx;
|
|
211
|
+
const env = buildGitEnv(process.env, token);
|
|
212
|
+
const branch = resolveBranch(resolved.branchPolicy, resolved.baseBranch, ctx);
|
|
213
|
+
const addRes = runner(["add", "-A"], { cwd: workspaceDir, env });
|
|
214
|
+
assertGitOk(addRes, "add");
|
|
215
|
+
const statusRes = runner(["status", "--porcelain"], { cwd: workspaceDir, env });
|
|
216
|
+
assertGitOk(statusRes, "status");
|
|
217
|
+
if (statusRes.stdout.trim() === "") {
|
|
218
|
+
return { kind: "no_changes", message: "working tree clean \u2014 nothing to push" };
|
|
219
|
+
}
|
|
220
|
+
const { name, email, trailers } = resolveAuthors(ctx.identity);
|
|
221
|
+
runner(["config", "user.name", name], { cwd: workspaceDir, env });
|
|
222
|
+
runner(["config", "user.email", email], { cwd: workspaceDir, env });
|
|
223
|
+
const summary = opts?.summary ?? "agentproto sync";
|
|
224
|
+
const message = trailers.length > 0 ? `${summary}
|
|
225
|
+
|
|
226
|
+
${trailers.join("\n")}
|
|
227
|
+
` : `${summary}
|
|
228
|
+
`;
|
|
229
|
+
const commitRes = runner(["commit", "-m", message], {
|
|
230
|
+
cwd: workspaceDir,
|
|
231
|
+
env
|
|
232
|
+
});
|
|
233
|
+
assertGitOk(commitRes, "commit");
|
|
234
|
+
if (resolved.branchPolicy !== "main") {
|
|
235
|
+
const checkoutRes = runner(
|
|
236
|
+
["checkout", "-B", branch],
|
|
237
|
+
{ cwd: workspaceDir, env }
|
|
238
|
+
);
|
|
239
|
+
assertGitOk(checkoutRes, "checkout");
|
|
240
|
+
}
|
|
241
|
+
const pushRes = runner(["push", "-u", "origin", branch], {
|
|
242
|
+
cwd: workspaceDir,
|
|
243
|
+
env
|
|
244
|
+
});
|
|
245
|
+
if (!pushRes.ok) {
|
|
246
|
+
const detail = pushRes.stderr.trim() || pushRes.stdout.trim();
|
|
247
|
+
return {
|
|
248
|
+
kind: "failed",
|
|
249
|
+
message: `push to origin/${branch} failed: ${detail}`
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
const files = (await tree.walk("")).length;
|
|
253
|
+
if (resolved.branchPolicy === "main" || !isAutoPr(resolved.prPolicy)) {
|
|
254
|
+
if (resolved.prPolicy === "manual" && resolved.branchPolicy !== "main") {
|
|
255
|
+
const { title, body } = buildPrText({ label: opts?.label, summary: opts?.summary });
|
|
256
|
+
return {
|
|
257
|
+
kind: "pushed",
|
|
258
|
+
ref: branch,
|
|
259
|
+
files,
|
|
260
|
+
message: `pushed to ${branch} \u2014 open PR manually: ${title}`,
|
|
261
|
+
errors: [`pr_policy=manual: open PR from ${branch} \u2192 ${resolved.baseBranch} (body: ${body.slice(0, 80)}\u2026)`]
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return {
|
|
265
|
+
kind: "pushed",
|
|
266
|
+
ref: branch,
|
|
267
|
+
files,
|
|
268
|
+
message: `pushed to ${branch}`
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
try {
|
|
272
|
+
const { owner, repo } = parseGithubRepo(resolved.repoUrl);
|
|
273
|
+
const { title, body } = buildPrText({ label: opts?.label, summary: opts?.summary });
|
|
274
|
+
const pr = await prCreator.openPr({
|
|
275
|
+
token,
|
|
276
|
+
owner,
|
|
277
|
+
repo,
|
|
278
|
+
head: branch,
|
|
279
|
+
base: resolved.baseBranch,
|
|
280
|
+
title,
|
|
281
|
+
body
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
kind: "pushed",
|
|
285
|
+
ref: branch,
|
|
286
|
+
files,
|
|
287
|
+
prUrl: pr.prUrl,
|
|
288
|
+
prNumber: pr.prNumber,
|
|
289
|
+
message: `pushed to ${branch} and opened PR #${pr.prNumber}`
|
|
290
|
+
};
|
|
291
|
+
} catch (err) {
|
|
292
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
293
|
+
return {
|
|
294
|
+
kind: "pushed",
|
|
295
|
+
ref: branch,
|
|
296
|
+
files,
|
|
297
|
+
message: `pushed to ${branch} but PR open failed: ${msg}`,
|
|
298
|
+
errors: [`pr_policy=auto: PR open failed \u2014 ${msg}`]
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { pull, push };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/github-fs.ts
|
|
306
|
+
function createGithubFilesystem(config, ctx, extra = {}) {
|
|
307
|
+
const tree = {
|
|
308
|
+
async exists(path) {
|
|
309
|
+
try {
|
|
310
|
+
await stat(join(ctx.workspaceDir, path));
|
|
311
|
+
return true;
|
|
312
|
+
} catch {
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
async readFile(path) {
|
|
317
|
+
return readFile(join(ctx.workspaceDir, path), "utf8");
|
|
318
|
+
},
|
|
319
|
+
async writeFile(path, content) {
|
|
320
|
+
const full = join(ctx.workspaceDir, path);
|
|
321
|
+
const slash = full.lastIndexOf(sep);
|
|
322
|
+
if (slash > 0) await mkdir(full.slice(0, slash), { recursive: true });
|
|
323
|
+
await writeFile(full, content);
|
|
324
|
+
},
|
|
325
|
+
async walk(path) {
|
|
326
|
+
const root = join(ctx.workspaceDir, path);
|
|
327
|
+
const out = [];
|
|
328
|
+
async function recurse(dir) {
|
|
329
|
+
let entries;
|
|
330
|
+
try {
|
|
331
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
332
|
+
} catch {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
for (const e of entries) {
|
|
336
|
+
if (e.name === ".git") continue;
|
|
337
|
+
const full = join(dir, e.name);
|
|
338
|
+
if (e.isDirectory()) {
|
|
339
|
+
await recurse(full);
|
|
340
|
+
} else {
|
|
341
|
+
out.push(relative(ctx.workspaceDir, full).split(sep).join("/"));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
await recurse(root);
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
const sync = createGithubWorkspaceSync(config, ctx, extra);
|
|
350
|
+
return { ...tree, ...sync };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/index.ts
|
|
354
|
+
function defineGithubStorage(config) {
|
|
355
|
+
const capabilities = {
|
|
356
|
+
transport: "git",
|
|
357
|
+
prPolicy: config.prPolicy ?? "none",
|
|
358
|
+
pairsWith: "sandbox"
|
|
359
|
+
};
|
|
360
|
+
return defineStorage({
|
|
361
|
+
provider: "github",
|
|
362
|
+
config,
|
|
363
|
+
factory: (ctx) => createGithubFilesystem(config, ctx),
|
|
364
|
+
capabilities
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export { buildGitEnv, createGithubFilesystem, createGithubWorkspaceSync, createOctokitPrCreator, defineGithubStorage, parseGithubRepo, realGitRunner, writeAuthConfig };
|
|
369
|
+
//# sourceMappingURL=index.mjs.map
|
|
370
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/git.ts","../src/pr.ts","../src/github-sync.ts","../src/github-fs.ts","../src/index.ts"],"names":["id","env","files"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * Thin typed wrapper over `child_process.spawnSync` for git commands.\n *\n * The token is NEVER passed on the command line (git would log it in\n * process listings and the auto-classifier blocks plaintext secrets). It\n * reaches git through the environment as `GIT_HTTP_EXTRAHEADER` via a\n * `.git/config` http.https.<host>.extraheader entry — the standard\n * non-interactive credential pattern for `git clone`/`push` against\n * GitHub HTTPS URLs.\n *\n * For tests, callers inject a `GitRunner` that returns canned stdout.\n */\n\nimport { spawnSync, type SpawnSyncReturns } from \"node:child_process\"\n\n/** Result of a single git invocation. */\nexport interface GitResult {\n ok: boolean\n stdout: string\n stderr: string\n status: number | null\n}\n\n/** Injectable runner — production uses `realGitRunner`; tests stub it. */\nexport type GitRunner = (\n args: readonly string[],\n opts: GitRunOpts,\n) => GitResult\n\nexport interface GitRunOpts {\n /** Working directory for `git -C`. */\n cwd: string\n /** Extra env vars (token-bearing header injected here). */\n env?: Record<string, string>\n}\n\n/**\n * Build the env block that injects the GitHub token into git's HTTPS auth.\n * We set `GIT_HTTP_EXTRAHEADER` (a git-credential-env helper convention)\n * AND `GIT_TERMINAL_PROMPT=0` so a missing token fails fast rather than\n * hanging on a TTY prompt.\n */\nexport function buildGitEnv(\n baseEnv: NodeJS.ProcessEnv,\n token: string,\n): Record<string, string> {\n if (!token) {\n throw new Error(\n \"storage-github: buildGitEnv called with an empty token — refusing to \" +\n \"spawn git (would hang on a TTY prompt or fail unauthenticated).\",\n )\n }\n return {\n ...baseEnv,\n GIT_TERMINAL_PROMPT: \"0\",\n // `extraheader` is read by git's HTTP transport; the leading `AUTHORIZATION: basic <b64>`\n // is what GitHub expects. base64(`${x-access-token}:${token}`) — the\n // `x-access-token` user is GitHub's documented convention for PAT auth\n // over HTTPS without a username.\n GIT_HTTP_EXTRAHEADER: `Authorization: Basic ${btoa(`x-access-token:${token}`)}`,\n } satisfies Record<string, string>\n}\n\n/**\n * Configure a working tree's `.git/config` so subsequent `git` invocations\n * in that tree authenticate against `github.com` with the token. We write\n * the `http.https://github.com/.extraheader` entry via `git config` so the\n * token never appears on the command line of `clone`/`push`/`fetch`.\n *\n * This is idempotent — `git config --replace-all` overwrites a prior entry.\n */\nexport function writeAuthConfig(\n runner: GitRunner,\n cwd: string,\n token: string,\n): void {\n const header = `Authorization: Basic ${btoa(`x-access-token:${token}`)}`\n runner(\n [\n \"config\",\n \"--replace-all\",\n \"http.https://github.com/.extraheader\",\n header,\n ],\n { cwd },\n )\n}\n\n/** Production runner — spawns `git` with the given args + env. */\nexport const realGitRunner: GitRunner = (\n args: readonly string[],\n opts: GitRunOpts,\n): GitResult => {\n const child = spawnSync(\"git\", [...args], {\n cwd: opts.cwd,\n env: { ...process.env, ...opts.env },\n encoding: \"utf8\",\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n }) as SpawnSyncReturns<string>\n return {\n ok: child.status === 0,\n stdout: child.stdout ?? \"\",\n stderr: child.stderr ?? \"\",\n status: child.status,\n }\n}\n\n/** Throw a readable error when a git command fails. */\nexport function assertGitOk(\n result: GitResult,\n label: string,\n): void {\n if (!result.ok) {\n const detail = result.stderr.trim() || result.stdout.trim() || \"(no output)\"\n throw new Error(\n `storage-github: git ${label} failed (status ${result.status ?? \"?\"}): ${detail}`,\n )\n }\n}\n","/**\n * PR creation — host-side `@octokit/rest`.\n *\n * The `PrCreator` interface lets tests stub the octokit call without\n * importing the SDK. Production uses `createOctokitPrCreator`, which\n * lazily constructs an `Octokit` instance with the token from the\n * factory context.\n */\n\nimport { Octokit } from \"@octokit/rest\"\nimport type { PrPolicy } from \"./types.js\"\n\n/** Result of a PR-open attempt. */\nexport interface PrResult {\n prUrl: string\n prNumber: number\n}\n\n/** Injectable PR-opener. Production uses octokit; tests stub this. */\nexport interface PrCreator {\n openPr(input: {\n token: string\n owner: string\n repo: string\n head: string\n base: string\n title: string\n body: string\n }): Promise<PrResult>\n}\n\n/**\n * Parse `owner`/`repo` from a GitHub HTTPS clone URL.\n * `https://github.com/owner/repo[.git]` → `{ owner: \"owner\", repo: \"repo\" }`.\n */\nexport function parseGithubRepo(\n repoUrl: string,\n): { owner: string; repo: string } {\n const match = repoUrl.match(/^https?:\\/\\/github\\.com\\/([^/]+)\\/([^/.?#]+?)(?:\\.git)?(?:[/?#].*)?$/)\n if (!match || !match[1] || !match[2]) {\n throw new Error(\n `storage-github: parseGithubRepo: could not parse owner/repo from '${repoUrl}'. ` +\n \"Expected an HTTPS URL like 'https://github.com/owner/repo'.\",\n )\n }\n return { owner: match[1], repo: match[2] }\n}\n\n/** Production `PrCreator` backed by `@octokit/rest`. */\nexport const createOctokitPrCreator = (): PrCreator => ({\n async openPr(input): Promise<PrResult> {\n const octokit = new Octokit({ auth: input.token })\n const res = await octokit.rest.pulls.create({\n owner: input.owner,\n repo: input.repo,\n head: input.head,\n base: input.base,\n title: input.title,\n body: input.body,\n })\n if (!res.data.html_url) {\n throw new Error(\n `storage-github: octokit pulls.create returned no html_url for ${input.owner}/${input.repo}`,\n )\n }\n return { prUrl: res.data.html_url, prNumber: res.data.number }\n },\n})\n\n/**\n * Build the PR title/body from a `PushOptions`-shaped input.\n * Used by both `auto` (opens the PR) and `manual` (returns the title/body\n * as a hint in `PushResult.errors[]`).\n */\nexport function buildPrText(opts: {\n label?: string\n summary?: string\n}): { title: string; body: string } {\n const title = opts.label ? `[agentproto] ${opts.label}` : \"[agentproto] sync\"\n const body = opts.summary ?? \"Automated sync push from agentproto workspace.\"\n return { title, body }\n}\n\n/** Whether the policy opens a PR automatically. */\nexport function isAutoPr(policy: PrPolicy | undefined): boolean {\n return policy === \"auto\"\n}\n","/**\n * `WorkspaceSync` impl for the github provider — `pull`/`push` backed by\n * real `git` invocations (host-side) + `@octokit/rest` for PR creation.\n *\n * # Token handling\n * The token reaches git via the `GIT_HTTP_EXTRAHEADER` env var (see\n * `git.ts:buildGitEnv`) — never on the command line, never in the repo\n * URL, never logged. `GIT_TERMINAL_PROMPT=0` makes a missing token fail\n * fast instead of hanging.\n *\n * # AIP-23 identity\n * `identity` is one or more `IdentityRefEntry` records. The primary\n * becomes the git author (`user.name` / `user.email`); the remaining\n * entries become `Co-authored-by: Name <email>` trailers. `ref`-style\n * entries (no name/email) are skipped — git needs a concrete author.\n */\n\nimport {\n assertGitOk,\n buildGitEnv,\n realGitRunner,\n writeAuthConfig,\n type GitRunner,\n} from \"./git.js\"\nimport {\n buildPrText,\n createOctokitPrCreator,\n isAutoPr,\n parseGithubRepo,\n type PrCreator,\n} from \"./pr.js\"\nimport type {\n BranchPolicy,\n GithubFactoryContext,\n GithubIdentityRef,\n GithubStorageConfig,\n PrPolicy,\n} from \"./types.js\"\nimport type {\n PullResult,\n PushOptions,\n PushResult,\n SyncTree,\n WorkspaceSync,\n} from \"@agentproto/storage\"\n\n/** Extra (test-injected) deps for the sync impl. */\nexport interface GithubWorkspaceSyncOpts {\n gitRunner?: GitRunner\n prCreator?: PrCreator\n}\n\nconst DEFAULT_BASE_BRANCH = \"main\"\n\n/** Resolved config (defaults applied). */\ninterface ResolvedConfig {\n /** Canonical repo URL — used only to parse owner/repo for PR creation. */\n repoUrl: string\n /** URL git actually clones/pushes against. Defaults to `repoUrl`; set\n * separately when cloning from a mirror or a local origin (tests). */\n cloneUrl: string\n branchPolicy: BranchPolicy\n prPolicy: PrPolicy\n baseBranch: string\n}\n\nfunction resolveConfig(config: GithubStorageConfig): ResolvedConfig {\n return {\n repoUrl: config.repoUrl,\n cloneUrl: config.cloneUrl ?? config.repoUrl,\n branchPolicy: config.branchPolicy ?? \"main\",\n prPolicy: config.prPolicy ?? \"none\",\n baseBranch: config.baseBranch ?? DEFAULT_BASE_BRANCH,\n }\n}\n\n/** Build the branch name per `branchPolicy`. */\nfunction resolveBranch(\n policy: BranchPolicy,\n baseBranch: string,\n ctx: GithubFactoryContext,\n): string {\n if (policy === \"main\") return baseBranch\n if (policy === \"per-conversation\") {\n const id = ctx.conversationId\n if (!id) {\n const suffix = `agentproto-${Date.now()}`\n return `agentproto/${suffix}`\n }\n return `agentproto/${id}`\n }\n // per-turn\n const id = ctx.turnId ?? ctx.conversationId\n if (!id) {\n return `agentproto/turn-${Date.now()}`\n }\n return `agentproto/turn-${id}`\n}\n\n/** Resolve the primary author + co-author trailers from `identity`. */\nfunction resolveAuthors(\n identity: GithubFactoryContext[\"identity\"],\n): { name: string; email: string; trailers: readonly string[] } {\n if (!identity) {\n return {\n name: \"agentproto\",\n email: \"agentproto@users.noreply.github.com\",\n trailers: [],\n }\n }\n const entries: GithubIdentityRef[] = Array.isArray(identity) ? identity : [identity]\n const named = entries.filter(\n (e): e is GithubIdentityRef =>\n \"name\" in e && \"email\" in e &&\n typeof e.name === \"string\" && typeof e.email === \"string\",\n )\n // `primary` is `undefined` when `named` is empty (noUncheckedIndexedAccess);\n // this single guard covers the no-valid-identity case AND narrows the type.\n const [primary, ...rest] = named\n if (!primary) {\n return {\n name: \"agentproto\",\n email: \"agentproto@users.noreply.github.com\",\n trailers: [],\n }\n }\n const trailers = rest.map((e) => `Co-authored-by: ${e.name} <${e.email}>`)\n return { name: primary.name, email: primary.email, trailers }\n}\n\n/**\n * Create a `WorkspaceSync` bound to `config` + `ctx`. The `tree` argument\n * to `pull`/`push` is the `SyncTree` the caller operates on — git itself\n * runs against `ctx.workspaceDir` (the working tree on disk), so the tree\n * is only used to detect \"is the tree empty\" on pull and to count files.\n */\nexport function createGithubWorkspaceSync(\n config: GithubStorageConfig,\n ctx: GithubFactoryContext,\n extra: GithubWorkspaceSyncOpts = {},\n): WorkspaceSync {\n const runner = extra.gitRunner ?? realGitRunner\n const prCreator = extra.prCreator ?? createOctokitPrCreator()\n const resolved = resolveConfig(config)\n\n async function pull(\n tree: SyncTree,\n opts?: { force?: boolean },\n ): Promise<PullResult> {\n const { workspaceDir, token } = ctx\n // Detect an empty tree: if `.git` exists OR any file is present, skip clone.\n const existing = await tree.walk(\"\")\n const hasGit = existing.some((p: string) => p === \".git/HEAD\" || p.startsWith(\".git/\"))\n const populated = existing.length > 0 || hasGit\n\n if (populated && !opts?.force) {\n // Fetch + merge the base branch.\n const env = buildGitEnv(process.env, token)\n const fetchRes = runner([\"fetch\", \"origin\", resolved.baseBranch], {\n cwd: workspaceDir,\n env,\n })\n assertGitOk(fetchRes, \"fetch\")\n const mergeRes = runner(\n [\"merge\", `origin/${resolved.baseBranch}`, \"--no-edit\", \"--ff-only\"],\n { cwd: workspaceDir, env },\n )\n // ff-only may fail when the local branch diverged — surface as failed pull\n // per `sync.conflict.policy: abort` (the default, per the plan).\n if (!mergeRes.ok) {\n const detail = mergeRes.stderr.trim() || mergeRes.stdout.trim()\n return {\n seeded: false,\n files: 0,\n bytes: 0,\n message: `pull: merge failed (local diverged from origin/${resolved.baseBranch}): ${detail}`,\n }\n }\n const files = await tree.walk(\"\")\n return {\n seeded: false,\n files: files.length,\n bytes: 0,\n message: `pulled origin/${resolved.baseBranch}`,\n }\n }\n\n // Empty tree → clone.\n const env = buildGitEnv(process.env, token)\n // Clone into workspaceDir; the dir is empty so `.` works.\n const cloneRes = runner(\n [\"clone\", \"--branch\", resolved.baseBranch, resolved.cloneUrl, \".\"],\n { cwd: workspaceDir, env },\n )\n if (!cloneRes.ok) {\n const detail = cloneRes.stderr.trim() || cloneRes.stdout.trim()\n return {\n seeded: false,\n files: 0,\n bytes: 0,\n message: `pull: clone failed: ${detail}`,\n }\n }\n // Stamp the auth header into `.git/config` for future push/fetch.\n writeAuthConfig(runner, workspaceDir, token)\n const files = await tree.walk(\"\")\n return {\n seeded: true,\n files: files.length,\n bytes: 0,\n message: `cloned ${resolved.repoUrl} @ ${resolved.baseBranch}`,\n }\n }\n\n async function push(\n tree: SyncTree,\n opts?: PushOptions,\n ): Promise<PushResult> {\n const { workspaceDir, token } = ctx\n const env = buildGitEnv(process.env, token)\n const branch = resolveBranch(resolved.branchPolicy, resolved.baseBranch, ctx)\n\n // Stage all changes.\n const addRes = runner([\"add\", \"-A\"], { cwd: workspaceDir, env })\n assertGitOk(addRes, \"add\")\n\n // Check for changes.\n const statusRes = runner([\"status\", \"--porcelain\"], { cwd: workspaceDir, env })\n assertGitOk(statusRes, \"status\")\n if (statusRes.stdout.trim() === \"\") {\n return { kind: \"no_changes\", message: \"working tree clean — nothing to push\" }\n }\n\n // Resolve identity → author + trailers.\n const { name, email, trailers } = resolveAuthors(ctx.identity)\n // Set local user.name / user.email so the commit author is correct.\n runner([\"config\", \"user.name\", name], { cwd: workspaceDir, env })\n runner([\"config\", \"user.email\", email], { cwd: workspaceDir, env })\n\n // Build commit message with optional Co-authored-by trailers.\n const summary = opts?.summary ?? \"agentproto sync\"\n const message =\n trailers.length > 0\n ? `${summary}\\n\\n${trailers.join(\"\\n\")}\\n`\n : `${summary}\\n`\n\n const commitRes = runner([\"commit\", \"-m\", message], {\n cwd: workspaceDir,\n env,\n })\n assertGitOk(commitRes, \"commit\")\n\n // Branch handling — for per-conversation/per-turn, create/checkout the branch.\n if (resolved.branchPolicy !== \"main\") {\n // Create the branch at the current commit and check it out.\n const checkoutRes = runner(\n [\"checkout\", \"-B\", branch],\n { cwd: workspaceDir, env },\n )\n assertGitOk(checkoutRes, \"checkout\")\n }\n\n // Push.\n const pushRes = runner([\"push\", \"-u\", \"origin\", branch], {\n cwd: workspaceDir,\n env,\n })\n if (!pushRes.ok) {\n const detail = pushRes.stderr.trim() || pushRes.stdout.trim()\n return {\n kind: \"failed\",\n message: `push to origin/${branch} failed: ${detail}`,\n }\n }\n\n // Count files committed.\n const files = (await tree.walk(\"\")).length\n\n // PR policy.\n if (resolved.branchPolicy === \"main\" || !isAutoPr(resolved.prPolicy)) {\n // No PR (main lands directly; manual/none just pushes).\n if (resolved.prPolicy === \"manual\" && resolved.branchPolicy !== \"main\") {\n const { title, body } = buildPrText({ label: opts?.label, summary: opts?.summary })\n return {\n kind: \"pushed\",\n ref: branch,\n files,\n message: `pushed to ${branch} — open PR manually: ${title}`,\n errors: [`pr_policy=manual: open PR from ${branch} → ${resolved.baseBranch} (body: ${body.slice(0, 80)}…)`],\n }\n }\n return {\n kind: \"pushed\",\n ref: branch,\n files,\n message: `pushed to ${branch}`,\n }\n }\n\n // Auto-PR via octokit.\n try {\n const { owner, repo } = parseGithubRepo(resolved.repoUrl)\n const { title, body } = buildPrText({ label: opts?.label, summary: opts?.summary })\n const pr = await prCreator.openPr({\n token,\n owner,\n repo,\n head: branch,\n base: resolved.baseBranch,\n title,\n body,\n })\n return {\n kind: \"pushed\",\n ref: branch,\n files,\n prUrl: pr.prUrl,\n prNumber: pr.prNumber,\n message: `pushed to ${branch} and opened PR #${pr.prNumber}`,\n }\n } catch (err) {\n // Token lacks pull-requests:write — surface the error but keep the push.\n const msg = err instanceof Error ? err.message : String(err)\n return {\n kind: \"pushed\",\n ref: branch,\n files,\n message: `pushed to ${branch} but PR open failed: ${msg}`,\n errors: [`pr_policy=auto: PR open failed — ${msg}`],\n }\n }\n }\n\n return { pull, push }\n}\n","/**\n * `GithubFilesystem` — implements `SyncTree` against a local working-tree\n * directory AND `WorkspaceSync` for the pull/push lifecycle. The factory\n * slot in `defineGithubStorage` returns one of these; callers feature-detect\n * the sync surface with `hasWorkspaceSync(fs)` from `@agentproto/storage`.\n *\n * Paths are workspace-relative; the filesystem resolves them against\n * `workspaceDir` from the factory context. `walk` skips `.git`.\n */\n\nimport { readdir, readFile, stat, writeFile } from \"node:fs/promises\"\nimport type { Dirent } from \"node:fs\"\nimport { mkdir } from \"node:fs/promises\"\nimport { join, relative, sep } from \"node:path\"\n\nimport {\n createGithubWorkspaceSync,\n type GithubWorkspaceSyncOpts,\n} from \"./github-sync.js\"\nimport type { GithubFactoryContext, GithubStorageConfig } from \"./types.js\"\nimport type { SyncTree, WorkspaceSync } from \"@agentproto/storage\"\n\n/** Filesystem + sync surface for a github-backed workspace. */\nexport type GithubFilesystem = SyncTree & WorkspaceSync\n\n/**\n * Create a `GithubFilesystem` bound to `workspaceDir`. The `WorkspaceSync`\n * surface (`pull`/`push`) is wired in via `createGithubWorkspaceSync` so\n * `hasWorkspaceSync(fs)` returns true.\n *\n * `extra` is for test injection (`gitRunner`, `prCreator`); production\n * callers omit it.\n */\nexport function createGithubFilesystem(\n config: GithubStorageConfig,\n ctx: GithubFactoryContext,\n extra: Partial<Pick<GithubWorkspaceSyncOpts, \"gitRunner\" | \"prCreator\">> = {},\n): GithubFilesystem {\n const tree: SyncTree = {\n async exists(path: string): Promise<boolean> {\n try {\n await stat(join(ctx.workspaceDir, path))\n return true\n } catch {\n return false\n }\n },\n async readFile(path: string): Promise<string> {\n return readFile(join(ctx.workspaceDir, path), \"utf8\")\n },\n async writeFile(path: string, content: string): Promise<void> {\n const full = join(ctx.workspaceDir, path)\n const slash = full.lastIndexOf(sep)\n if (slash > 0) await mkdir(full.slice(0, slash), { recursive: true })\n await writeFile(full, content)\n },\n async walk(path: string): Promise<readonly string[]> {\n const root = join(ctx.workspaceDir, path)\n const out: string[] = []\n async function recurse(dir: string): Promise<void> {\n let entries: Dirent[]\n try {\n entries = await readdir(dir, { withFileTypes: true })\n } catch {\n return\n }\n for (const e of entries) {\n if (e.name === \".git\") continue\n const full = join(dir, e.name)\n if (e.isDirectory()) {\n await recurse(full)\n } else {\n out.push(relative(ctx.workspaceDir, full).split(sep).join(\"/\"))\n }\n }\n }\n await recurse(root)\n return out\n },\n }\n\n const sync = createGithubWorkspaceSync(config, ctx, extra)\n return { ...tree, ...sync }\n}\n","/**\n * @agentproto/storage-github — AIP-35 github `WorkspaceSync` provider.\n *\n * Plugs into `@agentproto/storage`'s `defineStorage()` the same way every\n * pull-push provider does: a `factory` returning a filesystem that also\n * implements `WorkspaceSync`, plus opaque `capabilities` metadata. Hosts\n * feature-detect the sync surface with `hasWorkspaceSync(fs)`.\n *\n * PR creation is host-side `@octokit/rest`; the `GITHUB_TOKEN` is resolved\n * by the caller through `@agentproto/secrets/exposure` and passed in the\n * factory context — never inlined in config.\n *\n * Spec: https://agentproto.sh/docs/aip-35\n */\n\nimport { defineStorage } from \"@agentproto/storage\"\n\nimport { createGithubFilesystem } from \"./github-fs.js\"\nimport type {\n GithubFactoryContext,\n GithubStorageConfig,\n PrPolicy,\n} from \"./types.js\"\n\nexport type {\n GithubStorageConfig,\n GithubFactoryContext,\n BranchPolicy,\n PrPolicy,\n} from \"./types.js\"\nexport type { GithubFilesystem } from \"./github-fs.js\"\nexport type { WorkspaceSync, SyncTree, PullResult, PushResult, PushOptions } from \"@agentproto/storage\"\nexport { createGithubFilesystem } from \"./github-fs.js\"\nexport { createGithubWorkspaceSync } from \"./github-sync.js\"\nexport type { GithubWorkspaceSyncOpts } from \"./github-sync.js\"\nexport { parseGithubRepo, createOctokitPrCreator } from \"./pr.js\"\nexport type { PrCreator, PrResult } from \"./pr.js\"\nexport { buildGitEnv, realGitRunner, writeAuthConfig } from \"./git.js\"\nexport type { GitRunner, GitResult, GitRunOpts } from \"./git.js\"\n\n/** Capabilities metadata the registry indexes (AIP-43). */\nexport interface GithubCapabilities extends Record<string, unknown> {\n readonly transport: \"git\"\n readonly prPolicy: PrPolicy\n readonly pairsWith: \"sandbox\"\n}\n\n/**\n * Construct a `StorageHandle` for the github provider. The `factory` slot\n * returns a `GithubFilesystem` (which is `SyncTree & WorkspaceSync`) when\n * called with a `GithubFactoryContext` by the workspace/corpus layer.\n *\n * Example:\n * ```ts\n * const handle = defineGithubStorage({\n * repoUrl: \"https://github.com/owner/repo\",\n * branchPolicy: \"per-conversation\",\n * prPolicy: \"auto\",\n * })\n * const fs = handle.factory({ workspaceDir, token, identity })\n * if (hasWorkspaceSync(fs)) await fs.pull(fs)\n * ```\n */\nexport function defineGithubStorage(config: GithubStorageConfig) {\n const capabilities: GithubCapabilities = {\n transport: \"git\",\n prPolicy: config.prPolicy ?? \"none\",\n pairsWith: \"sandbox\",\n }\n return defineStorage<\n (ctx: GithubFactoryContext) => ReturnType<typeof createGithubFilesystem>,\n GithubCapabilities\n >({\n provider: \"github\",\n config,\n factory: (ctx: GithubFactoryContext) => createGithubFilesystem(config, ctx),\n capabilities,\n })\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agentproto/storage-github",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "@agentproto/storage-github — github `WorkspaceSync` provider for @agentproto/storage. Clones a repo on pull, commits with AIP-23 identity on push, and opens PRs per the sync.push.pr_policy. Host-side @octokit/rest for PR creation — no in-box gh dependency.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"agentproto",
|
|
7
|
+
"aip-35",
|
|
8
|
+
"storage",
|
|
9
|
+
"github",
|
|
10
|
+
"workspace-sync",
|
|
11
|
+
"open-standard",
|
|
12
|
+
"agentic"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://agentproto.sh/docs/aip-35",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/agentproto/ts",
|
|
18
|
+
"directory": "packages/storage-github"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/agentproto/ts/issues"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "dist/index.mjs",
|
|
26
|
+
"module": "dist/index.mjs",
|
|
27
|
+
"types": "dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.mjs",
|
|
32
|
+
"default": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@octokit/rest": "^22.0.1",
|
|
46
|
+
"@agentproto/storage": "0.1.0-alpha.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/node": "^25.6.2",
|
|
50
|
+
"tsup": "^8.5.1",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
52
|
+
"vitest": "^3.2.4",
|
|
53
|
+
"@agentproto/tooling": "0.1.0-alpha.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"dev": "tsup --watch",
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"clean": "rm -rf dist",
|
|
59
|
+
"check-types": "tsc --noEmit",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"test:watch": "vitest"
|
|
62
|
+
}
|
|
63
|
+
}
|