@hanzo/build 0.2.9 → 0.2.10
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/README.md +32 -1
- package/lib/api/changes.d.ts +56 -0
- package/lib/api/changes.js +63 -0
- package/lib/api/coding.d.ts +11 -3
- package/lib/api/coding.js +13 -3
- package/lib/api/environment.d.ts +57 -0
- package/lib/api/environment.js +85 -0
- package/lib/api/git.d.ts +6 -2
- package/lib/api/git.js +13 -5
- package/lib/api/sandbox.d.ts +30 -0
- package/lib/api/sandbox.js +49 -0
- package/lib/api/sessions.d.ts +7 -0
- package/lib/api/sessions.js +16 -0
- package/lib/api/turn.js +3 -0
- package/lib/desk.d.ts +5 -1
- package/lib/desk.js +30 -23
- package/lib/door.d.ts +7 -0
- package/lib/door.js +89 -0
- package/lib/environment.d.ts +15 -0
- package/lib/environment.js +132 -0
- package/lib/files.d.ts +14 -0
- package/lib/files.js +111 -0
- package/lib/forge.js +5 -1
- package/lib/git.d.ts +5 -0
- package/lib/git.js +70 -0
- package/lib/landing.js +18 -1
- package/lib/run.js +31 -4
- package/package.json +1 -1
- package/lib/e2e/site.spec.d.ts +0 -1
- package/lib/e2e/site.spec.js +0 -125
package/README.md
CHANGED
|
@@ -106,7 +106,29 @@ codebase or an issue from its own screen lands here with that choice already
|
|
|
106
106
|
made.
|
|
107
107
|
|
|
108
108
|
**A run.** The transcript as it streams, steering while it works, Stop, and
|
|
109
|
-
the pull request once it pushes one.
|
|
109
|
+
the pull request once it pushes one. Beside it:
|
|
110
|
+
|
|
111
|
+
- Environment: the codebase's environment, and the run's facts.
|
|
112
|
+
- Git: what it pushed, read from the forge — Diff, Review, Commits.
|
|
113
|
+
- Desktop: its sandbox's screen, framed from the sandbox's own noVNC page. A
|
|
114
|
+
sandbox run asks for a desktop.
|
|
115
|
+
- Terminal: a shell in the same working tree (a tmux session named for the run,
|
|
116
|
+
so it reattaches), and the agent's own log.
|
|
117
|
+
- Files: the working tree live from the sandbox, or the run's branch; and
|
|
118
|
+
Artifacts, what it produced.
|
|
119
|
+
- Subscriptions.
|
|
120
|
+
|
|
121
|
+
The desktop and the shell are the sandbox's, so they close when the run stops.
|
|
122
|
+
The setup bar says how long setup runs here have taken, once there are three.
|
|
123
|
+
|
|
124
|
+
**An environment.** What a sandbox run does to its codebase's checkout before
|
|
125
|
+
the agent starts: the install script, the start command, and the secrets it
|
|
126
|
+
exports. New offers to set one up when the chosen codebase has none. Start the
|
|
127
|
+
agent, and a run in mode `setup`, titled by the prompt's first line and working
|
|
128
|
+
through its steps, explores, installs and checks the codebase. Its answer is kept
|
|
129
|
+
as a proposal the Environment tab shows in its editors until an org admin saves
|
|
130
|
+
it. Or an org admin skips and saves it empty, and writes the scripts beside the
|
|
131
|
+
next run. Secret values are sealed in KMS and never shown again.
|
|
110
132
|
|
|
111
133
|
**A project.** The v2 workspace in the same window: its runs as a
|
|
112
134
|
conversation with suggestions and a Build/Plan composer on the left; Preview,
|
|
@@ -123,6 +145,10 @@ picker and open-in-tab; Share and Publish; the console dock under it.
|
|
|
123
145
|
| `POST /v1/agent/sessions/{id}/message` · `/stop` | steer, stop |
|
|
124
146
|
| `GET /v1/agent/targets` | the org's machines |
|
|
125
147
|
| `GET /v1/auto/flows` · `POST /v1/auto/flows` · `POST /v1/auto/flows/{id}/enable` | automations |
|
|
148
|
+
| `GET /v1/environment` · `GET`/`PUT`/`DELETE /v1/environment/{repo}` · `PUT`/`DELETE …/secrets/{name}` | a codebase's environment |
|
|
149
|
+
| `GET /v1/agent/coding/{session}/changes` · `/tree` · `/blob` | what a run pushed, and its branch's files, from the forge |
|
|
150
|
+
| `POST /v1/sandbox/{id}/screen/ticket` · `…/terminal/ticket` | the run's desktop and shell, framed with a single-use ticket |
|
|
151
|
+
| `POST /v1/sandbox/read` | the run's working tree, live |
|
|
126
152
|
| `GET /v1/provider/github/repos` · `POST /v1/provider/github/repos/import` | granted repositories, and bringing them onto the forge |
|
|
127
153
|
| `GET /v1/git/repos` · `GET /v1/git/repos/{name}` | the codebase chip, and its branches |
|
|
128
154
|
| `GET /v1/task/projects` · `GET /v1/task/board` · `GET /v1/task/projects/{key}/issues` | boards and issues, read from the forge |
|
|
@@ -146,6 +172,11 @@ src/
|
|
|
146
172
|
landing.tsx New
|
|
147
173
|
forge.tsx Codebase, Automations, Projects, Issues
|
|
148
174
|
run.tsx one run
|
|
175
|
+
desk.tsx the pane beside a run
|
|
176
|
+
door.tsx a run's desktop or shell, framed
|
|
177
|
+
git.tsx what a run pushed
|
|
178
|
+
files.tsx a run's files and artifacts
|
|
179
|
+
environment.tsx a codebase's environment
|
|
149
180
|
project.tsx a project's workspace
|
|
150
181
|
shelf.tsx Artifacts and Templates
|
|
151
182
|
publish.tsx Add to project / Publish
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run changed, read from the forge it pushed to, as the person asking.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/agent/coding/{session}/changes commits base..head, each file's patch, the pull request
|
|
5
|
+
* GET /v1/agent/coding/{session}/tree?path= one directory at the run's branch (its base until it pushes)
|
|
6
|
+
* GET /v1/agent/coding/{session}/blob?path= one file there, in /v1/git's blob shape
|
|
7
|
+
*
|
|
8
|
+
* Before the run pushes, changes answers empty, not 404.
|
|
9
|
+
*/
|
|
10
|
+
import { type Target } from './call.ts';
|
|
11
|
+
import { type Blob, type Entry } from './git.ts';
|
|
12
|
+
export interface Commit {
|
|
13
|
+
sha: string;
|
|
14
|
+
/** The first line. */
|
|
15
|
+
message: string;
|
|
16
|
+
author: string;
|
|
17
|
+
date: string;
|
|
18
|
+
}
|
|
19
|
+
export interface Change {
|
|
20
|
+
path: string;
|
|
21
|
+
/** The old path of a rename, or ''. */
|
|
22
|
+
from: string;
|
|
23
|
+
status: 'added' | 'modified' | 'deleted' | 'renamed';
|
|
24
|
+
additions: number;
|
|
25
|
+
deletions: number;
|
|
26
|
+
/** The file's unified diff hunks, or '' for a binary file. */
|
|
27
|
+
patch: string;
|
|
28
|
+
truncated: boolean;
|
|
29
|
+
}
|
|
30
|
+
export interface Review {
|
|
31
|
+
author: string;
|
|
32
|
+
state: string;
|
|
33
|
+
body: string;
|
|
34
|
+
at: string;
|
|
35
|
+
}
|
|
36
|
+
export interface Pull {
|
|
37
|
+
number: number;
|
|
38
|
+
url: string;
|
|
39
|
+
title: string;
|
|
40
|
+
state: 'open' | 'closed' | 'merged';
|
|
41
|
+
/** Null when the forge has not worked it out. */
|
|
42
|
+
mergeable: boolean | null;
|
|
43
|
+
reviews: Review[];
|
|
44
|
+
}
|
|
45
|
+
export interface Changes {
|
|
46
|
+
repo: string;
|
|
47
|
+
base: string;
|
|
48
|
+
head: string;
|
|
49
|
+
commits: Commit[];
|
|
50
|
+
files: Change[];
|
|
51
|
+
pull: Pull | null;
|
|
52
|
+
}
|
|
53
|
+
export declare function changes(raw: unknown): Changes;
|
|
54
|
+
export declare function read(t: Target, session: string): Promise<Changes>;
|
|
55
|
+
export declare function tree(t: Target, session: string, path?: string): Promise<Entry[]>;
|
|
56
|
+
export declare function blob(t: Target, session: string, path: string): Promise<Blob>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a run changed, read from the forge it pushed to, as the person asking.
|
|
3
|
+
*
|
|
4
|
+
* GET /v1/agent/coding/{session}/changes commits base..head, each file's patch, the pull request
|
|
5
|
+
* GET /v1/agent/coding/{session}/tree?path= one directory at the run's branch (its base until it pushes)
|
|
6
|
+
* GET /v1/agent/coding/{session}/blob?path= one file there, in /v1/git's blob shape
|
|
7
|
+
*
|
|
8
|
+
* Before the run pushes, changes answers empty, not 404.
|
|
9
|
+
*/
|
|
10
|
+
import { call, query, seg } from './call.js';
|
|
11
|
+
import { entries, file } from './git.js';
|
|
12
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
13
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : 0);
|
|
14
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
15
|
+
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
16
|
+
const STATUS = ['added', 'modified', 'deleted', 'renamed'];
|
|
17
|
+
const PULL = ['open', 'closed', 'merged'];
|
|
18
|
+
export function changes(raw) {
|
|
19
|
+
const o = obj(raw);
|
|
20
|
+
const p = o.pull ? obj(o.pull) : null;
|
|
21
|
+
return {
|
|
22
|
+
repo: str(o.repo),
|
|
23
|
+
base: str(o.base),
|
|
24
|
+
head: str(o.head),
|
|
25
|
+
commits: arr(o.commits)
|
|
26
|
+
.map(obj)
|
|
27
|
+
.map((c) => ({ sha: str(c.sha), message: str(c.message), author: str(c.author), date: str(c.date) }))
|
|
28
|
+
.filter((c) => c.sha),
|
|
29
|
+
files: arr(o.files)
|
|
30
|
+
.map(obj)
|
|
31
|
+
.map((f) => ({
|
|
32
|
+
path: str(f.path),
|
|
33
|
+
from: str(f.from),
|
|
34
|
+
status: STATUS.find((s) => s === f.status) ?? 'modified',
|
|
35
|
+
additions: num(f.additions),
|
|
36
|
+
deletions: num(f.deletions),
|
|
37
|
+
patch: str(f.patch),
|
|
38
|
+
truncated: f.truncated === true,
|
|
39
|
+
}))
|
|
40
|
+
.filter((f) => f.path),
|
|
41
|
+
pull: p && num(p.number)
|
|
42
|
+
? {
|
|
43
|
+
number: num(p.number),
|
|
44
|
+
url: str(p.url),
|
|
45
|
+
title: str(p.title),
|
|
46
|
+
state: PULL.find((s) => s === p.state) ?? 'open',
|
|
47
|
+
mergeable: typeof p.mergeable === 'boolean' ? p.mergeable : null,
|
|
48
|
+
reviews: arr(p.reviews)
|
|
49
|
+
.map(obj)
|
|
50
|
+
.map((r) => ({ author: str(r.author), state: str(r.state), body: str(r.body), at: str(r.at) })),
|
|
51
|
+
}
|
|
52
|
+
: null,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export async function read(t, session) {
|
|
56
|
+
return changes(await call(t, 'GET', `/v1/agent/coding/${seg(session)}/changes`));
|
|
57
|
+
}
|
|
58
|
+
export async function tree(t, session, path = '') {
|
|
59
|
+
return entries(await call(t, 'GET', `/v1/agent/coding/${seg(session)}/tree${query({ path })}`));
|
|
60
|
+
}
|
|
61
|
+
export async function blob(t, session, path) {
|
|
62
|
+
return file(await call(t, 'GET', `/v1/agent/coding/${seg(session)}/blob${query({ path })}`), path);
|
|
63
|
+
}
|
package/lib/api/coding.d.ts
CHANGED
|
@@ -18,9 +18,13 @@
|
|
|
18
18
|
*
|
|
19
19
|
* `model` and `effort` are taken by the dev harness the builder runs (and by
|
|
20
20
|
* codex); the builder names no other harness, so they are always honoured.
|
|
21
|
+
*
|
|
22
|
+
* `setup` is a plan's shape put to one question: how this codebase is installed,
|
|
23
|
+
* started and checked. Its agent may install and run things in its sandbox, and
|
|
24
|
+
* its answer is kept as the codebase's proposed environment (environment.ts).
|
|
21
25
|
*/
|
|
22
26
|
import { type Target } from './call.ts';
|
|
23
|
-
export type Mode = 'build' | 'plan';
|
|
27
|
+
export type Mode = 'build' | 'plan' | 'setup';
|
|
24
28
|
/** Why this mode cannot run where it is sent, or '' when it can. */
|
|
25
29
|
export declare const unhonoured: (mode: Mode | undefined, target?: string) => string;
|
|
26
30
|
export interface Ask {
|
|
@@ -48,6 +52,10 @@ export interface Run {
|
|
|
48
52
|
routed: boolean;
|
|
49
53
|
target: string;
|
|
50
54
|
}
|
|
51
|
-
/**
|
|
52
|
-
|
|
55
|
+
/**
|
|
56
|
+
* The request body: only what was asked, with no empty strings sent as values.
|
|
57
|
+
* A run in the sandbox asks for one with a desktop, so its Desktop tab has a
|
|
58
|
+
* screen to show; a machine is whatever it is.
|
|
59
|
+
*/
|
|
60
|
+
export declare function body(ask: Ask): Record<string, string | boolean>;
|
|
53
61
|
export declare function start(t: Target, ask: Ask): Promise<Run>;
|
package/lib/api/coding.js
CHANGED
|
@@ -18,13 +18,21 @@
|
|
|
18
18
|
*
|
|
19
19
|
* `model` and `effort` are taken by the dev harness the builder runs (and by
|
|
20
20
|
* codex); the builder names no other harness, so they are always honoured.
|
|
21
|
+
*
|
|
22
|
+
* `setup` is a plan's shape put to one question: how this codebase is installed,
|
|
23
|
+
* started and checked. Its agent may install and run things in its sandbox, and
|
|
24
|
+
* its answer is kept as the codebase's proposed environment (environment.ts).
|
|
21
25
|
*/
|
|
22
26
|
import { call, Refusal } from './call.js';
|
|
23
27
|
/** Why this mode cannot run where it is sent, or '' when it can. */
|
|
24
|
-
export const unhonoured = (mode, target) => mode === 'plan' && target?.trim()
|
|
25
|
-
?
|
|
28
|
+
export const unhonoured = (mode, target) => (mode === 'plan' || mode === 'setup') && target?.trim()
|
|
29
|
+
? `A ${mode} runs in the Hanzo sandbox: a machine clones and pushes with its own credential. Choose Default, or switch to Build.`
|
|
26
30
|
: '';
|
|
27
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* The request body: only what was asked, with no empty strings sent as values.
|
|
33
|
+
* A run in the sandbox asks for one with a desktop, so its Desktop tab has a
|
|
34
|
+
* screen to show; a machine is whatever it is.
|
|
35
|
+
*/
|
|
28
36
|
export function body(ask) {
|
|
29
37
|
const out = { prompt: ask.prompt.trim() };
|
|
30
38
|
for (const k of ['repo', 'base', 'targetId', 'project', 'after', 'mode', 'model', 'effort']) {
|
|
@@ -32,6 +40,8 @@ export function body(ask) {
|
|
|
32
40
|
if (v)
|
|
33
41
|
out[k] = v;
|
|
34
42
|
}
|
|
43
|
+
if (!out.targetId)
|
|
44
|
+
out.desktop = true;
|
|
35
45
|
return out;
|
|
36
46
|
}
|
|
37
47
|
export async function start(t, ask) {
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A codebase's environment: what a run does to its checkout before the agent
|
|
3
|
+
* starts, and which secrets it exports.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/environment {data: [environment]}
|
|
6
|
+
* GET /v1/environment/{repo} one; a codebase with none answers state "none"
|
|
7
|
+
* PUT /v1/environment/{repo} save {install, start} (org admin)
|
|
8
|
+
* DELETE /v1/environment/{repo} forget it and its secrets (org admin)
|
|
9
|
+
* PUT /v1/environment/{repo}/secrets/{name} set one value (org admin)
|
|
10
|
+
* DELETE /v1/environment/{repo}/secrets/{name} remove one (org admin)
|
|
11
|
+
*
|
|
12
|
+
* A setup run is a coding run in mode `setup` (coding.ts): its agent explores the
|
|
13
|
+
* checkout, installs and checks what it finds, and the platform keeps its answer
|
|
14
|
+
* here as `proposal` until someone saves it. Secrets are names only; their values
|
|
15
|
+
* are sealed in KMS and reach the run's environment, never this record.
|
|
16
|
+
*/
|
|
17
|
+
import { type Target } from './call.ts';
|
|
18
|
+
export type State = 'none' | 'proposed' | 'ready';
|
|
19
|
+
export interface Proposal {
|
|
20
|
+
install: string;
|
|
21
|
+
start: string;
|
|
22
|
+
/** Variables the agent found the codebase reads. Some may not be set yet. */
|
|
23
|
+
secrets: string[];
|
|
24
|
+
/** What the setup agent found and checked, in its own words. */
|
|
25
|
+
note: string;
|
|
26
|
+
}
|
|
27
|
+
export interface Environment {
|
|
28
|
+
repo: string;
|
|
29
|
+
install: string;
|
|
30
|
+
start: string;
|
|
31
|
+
/** The names set on this codebase. Values are never answered. */
|
|
32
|
+
secrets: string[];
|
|
33
|
+
state: State;
|
|
34
|
+
/** The setup run that produced the proposal, or ''. */
|
|
35
|
+
session: string;
|
|
36
|
+
proposal: Proposal | null;
|
|
37
|
+
updated: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function environment(raw: unknown, repo?: string): Environment;
|
|
40
|
+
export declare function environments(t: Target): Promise<Environment[]>;
|
|
41
|
+
export declare function read(t: Target, repo: string): Promise<Environment>;
|
|
42
|
+
export declare function save(t: Target, repo: string, scripts: {
|
|
43
|
+
install: string;
|
|
44
|
+
start: string;
|
|
45
|
+
}): Promise<Environment>;
|
|
46
|
+
export declare function forget(t: Target, repo: string): Promise<void>;
|
|
47
|
+
/** An environment variable name the run can export, and not one of its own. */
|
|
48
|
+
export declare const NAME: RegExp;
|
|
49
|
+
/** Why a secret name cannot be set, or '' when it can. */
|
|
50
|
+
export declare function refuse(name: string): string;
|
|
51
|
+
export declare function setSecret(t: Target, repo: string, name: string, value: string): Promise<Environment>;
|
|
52
|
+
export declare function removeSecret(t: Target, repo: string, name: string): Promise<Environment>;
|
|
53
|
+
/**
|
|
54
|
+
* What a setup run is asked. The first line is the run's title, the steps are
|
|
55
|
+
* its checklist, and the platform puts its own instructions ahead of both.
|
|
56
|
+
*/
|
|
57
|
+
export declare const SETUP: string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A codebase's environment: what a run does to its checkout before the agent
|
|
3
|
+
* starts, and which secrets it exports.
|
|
4
|
+
*
|
|
5
|
+
* GET /v1/environment {data: [environment]}
|
|
6
|
+
* GET /v1/environment/{repo} one; a codebase with none answers state "none"
|
|
7
|
+
* PUT /v1/environment/{repo} save {install, start} (org admin)
|
|
8
|
+
* DELETE /v1/environment/{repo} forget it and its secrets (org admin)
|
|
9
|
+
* PUT /v1/environment/{repo}/secrets/{name} set one value (org admin)
|
|
10
|
+
* DELETE /v1/environment/{repo}/secrets/{name} remove one (org admin)
|
|
11
|
+
*
|
|
12
|
+
* A setup run is a coding run in mode `setup` (coding.ts): its agent explores the
|
|
13
|
+
* checkout, installs and checks what it finds, and the platform keeps its answer
|
|
14
|
+
* here as `proposal` until someone saves it. Secrets are names only; their values
|
|
15
|
+
* are sealed in KMS and reach the run's environment, never this record.
|
|
16
|
+
*/
|
|
17
|
+
import { call, seg } from './call.js';
|
|
18
|
+
const str = (v) => (typeof v === 'string' ? v : '');
|
|
19
|
+
const obj = (v) => (v && typeof v === 'object' ? v : {});
|
|
20
|
+
const names = (v) => (Array.isArray(v) ? v.filter((n) => typeof n === 'string' && n !== '') : []);
|
|
21
|
+
const STATES = ['none', 'proposed', 'ready'];
|
|
22
|
+
export function environment(raw, repo = '') {
|
|
23
|
+
const o = obj(raw);
|
|
24
|
+
const p = o.proposal ? obj(o.proposal) : null;
|
|
25
|
+
const state = str(o.state);
|
|
26
|
+
return {
|
|
27
|
+
repo: str(o.repo) || repo,
|
|
28
|
+
install: str(o.install),
|
|
29
|
+
start: str(o.start),
|
|
30
|
+
secrets: names(o.secrets),
|
|
31
|
+
state: STATES.includes(state) ? state : 'none',
|
|
32
|
+
session: str(o.session),
|
|
33
|
+
proposal: p ? { install: str(p.install), start: str(p.start), secrets: names(p.secrets), note: str(p.note) } : null,
|
|
34
|
+
updated: str(o.updatedAt),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export async function environments(t) {
|
|
38
|
+
const r = await call(t, 'GET', '/v1/environment');
|
|
39
|
+
return (Array.isArray(r?.data) ? r.data : []).map((e) => environment(e)).filter((e) => e.repo);
|
|
40
|
+
}
|
|
41
|
+
export async function read(t, repo) {
|
|
42
|
+
return environment(await call(t, 'GET', `/v1/environment/${seg(repo)}`), repo);
|
|
43
|
+
}
|
|
44
|
+
export async function save(t, repo, scripts) {
|
|
45
|
+
return environment(await call(t, 'PUT', `/v1/environment/${seg(repo)}`, scripts), repo);
|
|
46
|
+
}
|
|
47
|
+
export async function forget(t, repo) {
|
|
48
|
+
await call(t, 'DELETE', `/v1/environment/${seg(repo)}`);
|
|
49
|
+
}
|
|
50
|
+
/** An environment variable name the run can export, and not one of its own. */
|
|
51
|
+
export const NAME = /^[A-Za-z_][A-Za-z0-9_]{0,127}$/;
|
|
52
|
+
const RESERVED = ['PATH', 'HOME', 'USER', 'SHELL', 'PWD', 'HOSTNAME', 'IFS', 'ENV', 'BASH_ENV'];
|
|
53
|
+
/** Why a secret name cannot be set, or '' when it can. */
|
|
54
|
+
export function refuse(name) {
|
|
55
|
+
if (!NAME.test(name))
|
|
56
|
+
return `${name || 'That'} is not an environment variable name`;
|
|
57
|
+
const up = name.toUpperCase();
|
|
58
|
+
if (up.startsWith('HANZO_') || RESERVED.includes(up))
|
|
59
|
+
return `${name} is reserved for the run itself`;
|
|
60
|
+
return '';
|
|
61
|
+
}
|
|
62
|
+
export async function setSecret(t, repo, name, value) {
|
|
63
|
+
const why = refuse(name);
|
|
64
|
+
if (why)
|
|
65
|
+
throw new Error(why);
|
|
66
|
+
if (!value)
|
|
67
|
+
throw new Error('A secret needs a value');
|
|
68
|
+
return environment(await call(t, 'PUT', `/v1/environment/${seg(repo)}/secrets/${seg(name)}`, { value }), repo);
|
|
69
|
+
}
|
|
70
|
+
export async function removeSecret(t, repo, name) {
|
|
71
|
+
return environment(await call(t, 'DELETE', `/v1/environment/${seg(repo)}/secrets/${seg(name)}`), repo);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* What a setup run is asked. The first line is the run's title, the steps are
|
|
75
|
+
* its checklist, and the platform puts its own instructions ahead of both.
|
|
76
|
+
*/
|
|
77
|
+
export const SETUP = [
|
|
78
|
+
'Set up the environment',
|
|
79
|
+
'',
|
|
80
|
+
'1. Understand the codebase: how it installs, builds, starts and tests.',
|
|
81
|
+
'2. Write the install script and the start command.',
|
|
82
|
+
'3. Run both in this fresh checkout.',
|
|
83
|
+
'4. Check that the build and the tests pass, and that the app answers if it is one.',
|
|
84
|
+
'5. Answer with what you checked and the environment.',
|
|
85
|
+
].join('\n');
|
package/lib/api/git.d.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* GET /v1/git/repos/{name}/blob?ref=&path= one file; binary is base64,
|
|
6
6
|
* past 1 MiB it is truncated with no content
|
|
7
7
|
*
|
|
8
|
-
* A run pushes its branch to
|
|
9
|
-
*
|
|
8
|
+
* A run pushes its branch to the forge, not here, so a run's files are read
|
|
9
|
+
* through the run (changes.ts); these read a codebase as the platform holds it.
|
|
10
10
|
*/
|
|
11
11
|
import { type Target } from './call.ts';
|
|
12
12
|
export interface Entry {
|
|
@@ -24,4 +24,8 @@ export interface Blob {
|
|
|
24
24
|
size: number;
|
|
25
25
|
}
|
|
26
26
|
export declare function tree(t: Target, repo: string, ref: string, path?: string): Promise<Entry[]>;
|
|
27
|
+
/** A tree answer's entries. A run's branch on the forge answers in the same shape. */
|
|
28
|
+
export declare function entries(raw: unknown): Entry[];
|
|
27
29
|
export declare function blob(t: Target, repo: string, ref: string, path: string): Promise<Blob>;
|
|
30
|
+
/** A blob answer. A run's branch on the forge answers in the same shape. */
|
|
31
|
+
export declare function file(raw: unknown, path: string): Blob;
|
package/lib/api/git.js
CHANGED
|
@@ -5,14 +5,18 @@
|
|
|
5
5
|
* GET /v1/git/repos/{name}/blob?ref=&path= one file; binary is base64,
|
|
6
6
|
* past 1 MiB it is truncated with no content
|
|
7
7
|
*
|
|
8
|
-
* A run pushes its branch to
|
|
9
|
-
*
|
|
8
|
+
* A run pushes its branch to the forge, not here, so a run's files are read
|
|
9
|
+
* through the run (changes.ts); these read a codebase as the platform holds it.
|
|
10
10
|
*/
|
|
11
11
|
import { call, query, seg } from './call.js';
|
|
12
12
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
13
13
|
export async function tree(t, repo, ref, path = '') {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
return entries(await call(t, 'GET', `/v1/git/repos/${seg(repo)}/tree${query({ ref, path })}`));
|
|
15
|
+
}
|
|
16
|
+
/** A tree answer's entries. A run's branch on the forge answers in the same shape. */
|
|
17
|
+
export function entries(raw) {
|
|
18
|
+
const o = (raw && typeof raw === 'object' ? raw : {});
|
|
19
|
+
const rows = Array.isArray(o.entries) ? o.entries : [];
|
|
16
20
|
return rows
|
|
17
21
|
.map((r) => {
|
|
18
22
|
const o = (r && typeof r === 'object' ? r : {});
|
|
@@ -26,7 +30,11 @@ export async function tree(t, repo, ref, path = '') {
|
|
|
26
30
|
.filter((e) => e.path);
|
|
27
31
|
}
|
|
28
32
|
export async function blob(t, repo, ref, path) {
|
|
29
|
-
|
|
33
|
+
return file(await call(t, 'GET', `/v1/git/repos/${seg(repo)}/blob${query({ ref, path })}`), path);
|
|
34
|
+
}
|
|
35
|
+
/** A blob answer. A run's branch on the forge answers in the same shape. */
|
|
36
|
+
export function file(raw, path) {
|
|
37
|
+
const o = (raw && typeof raw === 'object' ? raw : {});
|
|
30
38
|
const binary = o.binary === true;
|
|
31
39
|
const truncated = o.truncated === true;
|
|
32
40
|
return {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A run's sandbox while the run holds it: a door into its screen or its shell,
|
|
3
|
+
* and its working tree as it is right now.
|
|
4
|
+
*
|
|
5
|
+
* POST /v1/sandbox/{id}/{screen|terminal}/ticket one ticket, spent when the page's socket opens (30 s)
|
|
6
|
+
* POST /v1/sandbox/read {id, path} a directory's names, or a file's bytes
|
|
7
|
+
*
|
|
8
|
+
* The door is cloud's own page — noVNC for the screen, xterm for the shell — so
|
|
9
|
+
* the builder frames it and never speaks the socket itself. The sandbox is gone
|
|
10
|
+
* when the run ends, and every call here then answers 404.
|
|
11
|
+
*/
|
|
12
|
+
import { type Target } from './call.ts';
|
|
13
|
+
export type Door = 'screen' | 'terminal';
|
|
14
|
+
/** A shell's tmux session name: the sandbox reattaches to it when the door opens again. */
|
|
15
|
+
export declare const NAME: RegExp;
|
|
16
|
+
/** Where to frame one door, with a fresh ticket in its address. */
|
|
17
|
+
export declare function door(t: Target, sandbox: string, which: Door, session?: string): Promise<string>;
|
|
18
|
+
export interface Node {
|
|
19
|
+
/** The resolved path inside the sandbox. */
|
|
20
|
+
path: string;
|
|
21
|
+
dir: boolean;
|
|
22
|
+
/** A directory's entries, bare names, sorted. */
|
|
23
|
+
names: string[];
|
|
24
|
+
/** A file's text, or '' when it is binary or past the view cap. */
|
|
25
|
+
text: string;
|
|
26
|
+
binary: boolean;
|
|
27
|
+
truncated: boolean;
|
|
28
|
+
}
|
|
29
|
+
/** One path in the sandbox, relative to its working directory. */
|
|
30
|
+
export declare function read(t: Target, sandbox: string, path: string): Promise<Node>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A run's sandbox while the run holds it: a door into its screen or its shell,
|
|
3
|
+
* and its working tree as it is right now.
|
|
4
|
+
*
|
|
5
|
+
* POST /v1/sandbox/{id}/{screen|terminal}/ticket one ticket, spent when the page's socket opens (30 s)
|
|
6
|
+
* POST /v1/sandbox/read {id, path} a directory's names, or a file's bytes
|
|
7
|
+
*
|
|
8
|
+
* The door is cloud's own page — noVNC for the screen, xterm for the shell — so
|
|
9
|
+
* the builder frames it and never speaks the socket itself. The sandbox is gone
|
|
10
|
+
* when the run ends, and every call here then answers 404.
|
|
11
|
+
*/
|
|
12
|
+
import { call, Refusal, seg } from './call.js';
|
|
13
|
+
/** A shell's tmux session name: the sandbox reattaches to it when the door opens again. */
|
|
14
|
+
export const NAME = /^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$/;
|
|
15
|
+
/** Where to frame one door, with a fresh ticket in its address. */
|
|
16
|
+
export async function door(t, sandbox, which, session = '') {
|
|
17
|
+
const r = await call(t, 'POST', `/v1/sandbox/${seg(sandbox)}/${which}/ticket`);
|
|
18
|
+
const path = typeof r?.url === 'string' ? r.url : '';
|
|
19
|
+
if (!path.startsWith('/v1/'))
|
|
20
|
+
throw new Refusal(502, 'The sandbox answered with no address to open');
|
|
21
|
+
const arg = which === 'terminal' && NAME.test(session) ? `&arg=${session}` : '';
|
|
22
|
+
return `${t.api}${path}${arg}`;
|
|
23
|
+
}
|
|
24
|
+
/** What a file shows at most. */
|
|
25
|
+
const CAP = 1 << 20;
|
|
26
|
+
/** One path in the sandbox, relative to its working directory. */
|
|
27
|
+
export async function read(t, sandbox, path) {
|
|
28
|
+
const o = (await call(t, 'POST', '/v1/sandbox/read', { id: sandbox, path })) ?? {};
|
|
29
|
+
const dir = o.dir === true;
|
|
30
|
+
const names = Array.isArray(o.entries) ? o.entries.filter((n) => typeof n === 'string').sort() : [];
|
|
31
|
+
const bytes = !dir && typeof o.data === 'string' ? decode(o.data) : new Uint8Array();
|
|
32
|
+
const binary = bytes.includes(0);
|
|
33
|
+
const truncated = bytes.length > CAP;
|
|
34
|
+
return {
|
|
35
|
+
path: typeof o.path === 'string' ? o.path : path,
|
|
36
|
+
dir,
|
|
37
|
+
names,
|
|
38
|
+
text: binary || truncated ? '' : new TextDecoder().decode(bytes),
|
|
39
|
+
binary,
|
|
40
|
+
truncated,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function decode(b64) {
|
|
44
|
+
const bin = atob(b64);
|
|
45
|
+
const out = new Uint8Array(bin.length);
|
|
46
|
+
for (let i = 0; i < bin.length; i++)
|
|
47
|
+
out[i] = bin.charCodeAt(i);
|
|
48
|
+
return out;
|
|
49
|
+
}
|
package/lib/api/sessions.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface Session {
|
|
|
35
35
|
mode: string;
|
|
36
36
|
/** The pull request the run proposed, or ''. */
|
|
37
37
|
pr: string;
|
|
38
|
+
/** The sandbox the run leased, once it has one. It is gone when the run ends. */
|
|
39
|
+
sandbox: string;
|
|
38
40
|
events: number;
|
|
39
41
|
createdAt: string;
|
|
40
42
|
updatedAt: string;
|
|
@@ -54,6 +56,11 @@ export interface Detail extends Session {
|
|
|
54
56
|
}
|
|
55
57
|
export declare function session(raw: unknown): Session;
|
|
56
58
|
export declare function event(raw: unknown): Event;
|
|
59
|
+
/**
|
|
60
|
+
* How long runs in one mode took, in whole minutes: the median and the ninetieth
|
|
61
|
+
* percentile of the finished ones. Null under three, which is too few to say.
|
|
62
|
+
*/
|
|
63
|
+
export declare function took(runs: Session[], mode: string): [number, number] | null;
|
|
57
64
|
export interface ListQuery {
|
|
58
65
|
kind?: string;
|
|
59
66
|
project?: string;
|
package/lib/api/sessions.js
CHANGED
|
@@ -34,6 +34,7 @@ export function session(raw) {
|
|
|
34
34
|
environment: str(s.environment),
|
|
35
35
|
mode: str(s.mode),
|
|
36
36
|
pr: str(s.pr),
|
|
37
|
+
sandbox: str(s.sandbox),
|
|
37
38
|
events: num(s.events),
|
|
38
39
|
createdAt: str(s.createdAt),
|
|
39
40
|
updatedAt: str(s.updatedAt),
|
|
@@ -52,6 +53,21 @@ export function event(raw) {
|
|
|
52
53
|
createdAt: str(e.createdAt),
|
|
53
54
|
};
|
|
54
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* How long runs in one mode took, in whole minutes: the median and the ninetieth
|
|
58
|
+
* percentile of the finished ones. Null under three, which is too few to say.
|
|
59
|
+
*/
|
|
60
|
+
export function took(runs, mode) {
|
|
61
|
+
const spans = runs
|
|
62
|
+
.filter((r) => r.mode === mode && r.status === 'done' && r.endedAt)
|
|
63
|
+
.map((r) => Date.parse(r.endedAt) - Date.parse(r.createdAt))
|
|
64
|
+
.filter((ms) => Number.isFinite(ms) && ms > 0)
|
|
65
|
+
.sort((a, b) => a - b);
|
|
66
|
+
if (spans.length < 3)
|
|
67
|
+
return null;
|
|
68
|
+
const at = (q) => Math.max(1, Math.round(spans[Math.min(spans.length - 1, Math.floor(q * spans.length))] / 60_000));
|
|
69
|
+
return [at(0.5), at(0.9)];
|
|
70
|
+
}
|
|
55
71
|
export async function list(t, q = {}) {
|
|
56
72
|
const raw = obj(await call(t, 'GET', `/v1/agent/sessions${query({ ...q })}`));
|
|
57
73
|
return (Array.isArray(raw.sessions) ? raw.sessions : []).map(session).filter((s) => s.id);
|
package/lib/api/turn.js
CHANGED
|
@@ -34,8 +34,11 @@ function status(b, mode) {
|
|
|
34
34
|
// A plan's answer IS its final status: the run read and wrote nothing.
|
|
35
35
|
// Whether the run planned is the RECORD's word — any member of the org
|
|
36
36
|
// can append an event saying `mode: plan`, and its text is not an answer.
|
|
37
|
+
// A setup is a plan put to one question, and answers the same way.
|
|
37
38
|
if (mode === 'plan')
|
|
38
39
|
return str(b.plan) || 'Planned — the run answered with no plan';
|
|
40
|
+
if (mode === 'setup')
|
|
41
|
+
return str(b.plan) || 'Set up — the run answered with no environment';
|
|
39
42
|
if (b.changed === false)
|
|
40
43
|
return 'Done — nothing to change';
|
|
41
44
|
const pr = str(b.pr);
|
package/lib/desk.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Event } from './api/sessions.ts';
|
|
2
|
-
export declare function Desk({ id, repo, branch, base, environment, mode, pr, events, live, refused, retry, onRetry, onHide, }: {
|
|
2
|
+
export declare function Desk({ id, repo, branch, base, environment, mode, pr, title, project, sandbox, events, live, refused, retry, onRetry, onHide, }: {
|
|
3
3
|
id: string;
|
|
4
4
|
repo: string;
|
|
5
5
|
branch: string;
|
|
@@ -10,6 +10,10 @@ export declare function Desk({ id, repo, branch, base, environment, mode, pr, ev
|
|
|
10
10
|
href: string;
|
|
11
11
|
label: string;
|
|
12
12
|
};
|
|
13
|
+
title: string;
|
|
14
|
+
project: string;
|
|
15
|
+
/** The sandbox the run holds, or '' before it leases one and on a machine. */
|
|
16
|
+
sandbox: string;
|
|
13
17
|
events: Event[];
|
|
14
18
|
live: boolean;
|
|
15
19
|
refused: string;
|