balladeer 0.0.5 → 1.0.1
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 +200 -5
- package/README.md +167 -68
- package/dist/agent.d.ts +126 -0
- package/dist/agent.js +209 -0
- package/dist/cli.d.ts +48 -0
- package/dist/cli.js +531 -0
- package/dist/client.d.ts +66 -0
- package/dist/client.js +142 -0
- package/dist/commands/affected.d.ts +22 -0
- package/dist/commands/affected.js +123 -0
- package/dist/commands/check-seals.d.ts +37 -0
- package/dist/commands/check-seals.js +289 -0
- package/dist/commands/discover.d.ts +68 -0
- package/dist/commands/discover.js +403 -0
- package/dist/commands/explain.d.ts +35 -0
- package/dist/commands/explain.js +90 -0
- package/dist/commands/invite.d.ts +24 -0
- package/dist/commands/invite.js +198 -0
- package/dist/commands/mcp.d.ts +65 -0
- package/dist/commands/mcp.js +202 -0
- package/dist/commands/prepare.d.ts +74 -0
- package/dist/commands/prepare.js +217 -0
- package/dist/commands/propose.d.ts +69 -0
- package/dist/commands/propose.js +284 -0
- package/dist/commands/repositories.d.ts +18 -0
- package/dist/commands/repositories.js +185 -0
- package/dist/commands/session.d.ts +35 -0
- package/dist/commands/session.js +118 -0
- package/dist/commands/setup.d.ts +98 -0
- package/dist/commands/setup.js +1600 -0
- package/dist/commands/status.d.ts +51 -0
- package/dist/commands/status.js +542 -0
- package/dist/commands/touch-map.d.ts +42 -0
- package/dist/commands/touch-map.js +251 -0
- package/dist/commands/whoami.d.ts +8 -0
- package/dist/commands/whoami.js +80 -0
- package/dist/conventions.d.ts +77 -0
- package/dist/conventions.js +183 -0
- package/dist/copy.d.ts +224 -0
- package/dist/copy.js +641 -0
- package/dist/currency.d.ts +31 -0
- package/dist/currency.js +72 -0
- package/dist/desktop-config.d.ts +85 -0
- package/dist/desktop-config.js +217 -0
- package/dist/gh.d.ts +80 -0
- package/dist/gh.js +188 -0
- package/dist/git.d.ts +91 -0
- package/dist/git.js +226 -0
- package/dist/legacy.d.ts +41 -0
- package/dist/legacy.js +143 -0
- package/dist/local-time.d.ts +66 -0
- package/dist/local-time.js +84 -0
- package/dist/markers.d.ts +76 -0
- package/dist/markers.js +125 -0
- package/dist/mcp-config.d.ts +109 -0
- package/dist/mcp-config.js +234 -0
- package/dist/release.d.ts +55 -0
- package/dist/release.js +67 -0
- package/dist/repository.d.ts +8 -0
- package/dist/repository.js +32 -0
- package/dist/seals.d.ts +48 -0
- package/dist/seals.js +112 -0
- package/dist/session.d.ts +84 -0
- package/dist/session.js +135 -0
- package/dist/store.d.ts +108 -0
- package/dist/store.js +237 -0
- package/dist/touch-map.d.ts +241 -0
- package/dist/touch-map.js +487 -0
- package/dist/wire.d.ts +674 -0
- package/dist/wire.js +20 -0
- package/package.json +19 -10
- package/bin/balladeer.js +0 -161
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { ClientTooOldError, TransportError, request } from "../client.js";
|
|
2
|
+
import { ghAvailable, ghLogin, ghSignedIn, listVisibleRepositories } from "../gh.js";
|
|
3
|
+
import { commandLine } from "../release.js";
|
|
4
|
+
import { StoreError, findSession, readCredentials } from "../store.js";
|
|
5
|
+
import { repositoryHint } from "../repository.js";
|
|
6
|
+
import {} from "../wire.js";
|
|
7
|
+
/** How many to offer. A list nobody reads to the end is a list nobody chooses from. */
|
|
8
|
+
const LIMIT = 100;
|
|
9
|
+
function emit(options, step) {
|
|
10
|
+
if (options.json)
|
|
11
|
+
options.write(`${JSON.stringify(step)}\n`);
|
|
12
|
+
}
|
|
13
|
+
function say(options, text) {
|
|
14
|
+
if (!options.json)
|
|
15
|
+
options.write(`${text}\n`);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* What this machine could add to Balladeer, and which of them are already in.
|
|
19
|
+
*
|
|
20
|
+
* It exists so the agent asking "which repositories shall I set up" has
|
|
21
|
+
* something to show. Setting one up is still `setup`, run in a checkout of it or
|
|
22
|
+
* named with `--repository`; this command changes nothing and sends nothing
|
|
23
|
+
* anywhere. The one network call it makes is to Balladeer, to find out which of
|
|
24
|
+
* these are already enrolled, and it is skipped entirely when no session is
|
|
25
|
+
* stored.
|
|
26
|
+
*/
|
|
27
|
+
export async function runRepositories(options) {
|
|
28
|
+
const here = repositoryHint(options.cwd);
|
|
29
|
+
const known = here === "unknown/unknown" ? undefined : here;
|
|
30
|
+
if (!(await ghAvailable())) {
|
|
31
|
+
return withoutGh(options, known, "the GitHub CLI is not installed", "Install the GitHub CLI from https://cli.github.com, sign in with `gh auth login`, then run this command again to see the whole list.");
|
|
32
|
+
}
|
|
33
|
+
if (!(await ghSignedIn())) {
|
|
34
|
+
return withoutGh(options, known, "gh is installed but not signed in", "Run `gh auth login`, then run this command again to see the whole list.");
|
|
35
|
+
}
|
|
36
|
+
// The account's own repositories, and the organization this checkout belongs
|
|
37
|
+
// to. A Didero engineer sitting in one of their own repositories wants the
|
|
38
|
+
// other eleven, and those belong to the organization rather than to them.
|
|
39
|
+
const owners = new Set([undefined]);
|
|
40
|
+
const hereOwner = known?.split("/")[0];
|
|
41
|
+
if (hereOwner !== undefined)
|
|
42
|
+
owners.add(hereOwner);
|
|
43
|
+
const login = await ghLogin();
|
|
44
|
+
if (login !== undefined &&
|
|
45
|
+
hereOwner !== undefined &&
|
|
46
|
+
login.toLowerCase() === hereOwner.toLowerCase()) {
|
|
47
|
+
owners.delete(hereOwner);
|
|
48
|
+
}
|
|
49
|
+
const seen = new Map();
|
|
50
|
+
for (const owner of owners) {
|
|
51
|
+
for (const found of await listVisibleRepositories(owner, LIMIT)) {
|
|
52
|
+
if (!seen.has(found.repository.toLowerCase())) {
|
|
53
|
+
seen.set(found.repository.toLowerCase(), found.canWrite);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (known !== undefined && !seen.has(known.toLowerCase()))
|
|
58
|
+
seen.set(known.toLowerCase(), true);
|
|
59
|
+
const enrolled = await enrolledNames(options);
|
|
60
|
+
const rows = [...seen.keys()]
|
|
61
|
+
.map((name) => ({
|
|
62
|
+
repository: name,
|
|
63
|
+
here: known !== undefined && name === known.toLowerCase(),
|
|
64
|
+
...(enrolled === undefined ? {} : { enrolled: enrolled.has(name) }),
|
|
65
|
+
}))
|
|
66
|
+
// The one you are standing in first, then the rest alphabetically, so the
|
|
67
|
+
// default is the first thing read rather than something to hunt for.
|
|
68
|
+
.sort((left, right) => left.here === right.here
|
|
69
|
+
? left.repository.localeCompare(right.repository)
|
|
70
|
+
: left.here
|
|
71
|
+
? -1
|
|
72
|
+
: 1);
|
|
73
|
+
if (rows.length === 0) {
|
|
74
|
+
say(options, "Your GitHub account can see no repositories to add.");
|
|
75
|
+
emit(options, {
|
|
76
|
+
step: "repositories",
|
|
77
|
+
source: "none",
|
|
78
|
+
workspaceKnown: enrolled !== undefined,
|
|
79
|
+
repositories: [],
|
|
80
|
+
});
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
report(options, rows, enrolled !== undefined);
|
|
84
|
+
emit(options, {
|
|
85
|
+
step: "repositories",
|
|
86
|
+
source: "gh",
|
|
87
|
+
workspaceKnown: enrolled !== undefined,
|
|
88
|
+
repositories: rows,
|
|
89
|
+
});
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
/** The listing a person reads, and the one line that sets any of them up. */
|
|
93
|
+
function report(options, rows, workspaceKnown) {
|
|
94
|
+
say(options, workspaceKnown
|
|
95
|
+
? "Repositories your GitHub account can see. Ones already in Balladeer are marked."
|
|
96
|
+
: "Repositories your GitHub account can see. Nothing is stored for this Balladeer, so which of them are already added is unknown.");
|
|
97
|
+
say(options, "");
|
|
98
|
+
for (const row of rows) {
|
|
99
|
+
const marks = [
|
|
100
|
+
row.here ? "this directory" : undefined,
|
|
101
|
+
row.enrolled === true ? "already in Balladeer" : undefined,
|
|
102
|
+
].filter((mark) => mark !== undefined);
|
|
103
|
+
say(options, ` ${row.repository}${marks.length === 0 ? "" : ` (${marks.join(", ")})`}`);
|
|
104
|
+
}
|
|
105
|
+
const addable = rows.filter((row) => row.enrolled !== true);
|
|
106
|
+
say(options, "");
|
|
107
|
+
if (addable.length === 0) {
|
|
108
|
+
say(options, "Every repository this machine can see is already in Balladeer.");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
say(options, "Add one, or several, with:");
|
|
112
|
+
say(options, ` ${commandLine(null, `setup ${addable
|
|
113
|
+
.slice(0, 2)
|
|
114
|
+
.map((row) => `--repository ${row.repository}`)
|
|
115
|
+
.join(" ")}`)}`);
|
|
116
|
+
say(options, "Connecting a repository's coding agent and its CI happens in a checkout of that repository, so run setup there for each one. Naming a repository from somewhere else adds it to the workspace and stops there, which is said again when it happens.");
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Which repositories the workspace already holds, or nothing at all.
|
|
120
|
+
*
|
|
121
|
+
* Nothing rather than an empty set when there is no session or the read failed:
|
|
122
|
+
* an empty set would render every repository as addable, and an agent reading
|
|
123
|
+
* that would offer to add repositories the workspace already has.
|
|
124
|
+
*/
|
|
125
|
+
async function enrolledNames(options) {
|
|
126
|
+
let session;
|
|
127
|
+
try {
|
|
128
|
+
session = findSession(readCredentials(options.environment), options.controlPlane);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!(error instanceof StoreError))
|
|
132
|
+
throw error;
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
if (session === undefined)
|
|
136
|
+
return undefined;
|
|
137
|
+
try {
|
|
138
|
+
const state = await request(options.controlPlane, {
|
|
139
|
+
method: "GET",
|
|
140
|
+
path: "/api/setup/v1/state",
|
|
141
|
+
bearer: session.token,
|
|
142
|
+
});
|
|
143
|
+
return new Set(state.repositories
|
|
144
|
+
.filter((repository) => repository.status === "active")
|
|
145
|
+
.map((repository) => repository.displayName.toLowerCase()));
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (error instanceof ClientTooOldError || error instanceof TransportError)
|
|
149
|
+
return undefined;
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The answer when `gh` cannot list anything.
|
|
155
|
+
*
|
|
156
|
+
* Still an answer rather than a refusal: this directory's own repository is a
|
|
157
|
+
* real thing an agent can offer, and telling somebody with no `gh` installed
|
|
158
|
+
* that there is nothing to add would be false.
|
|
159
|
+
*/
|
|
160
|
+
function withoutGh(options, here, reason, next) {
|
|
161
|
+
if (here === undefined) {
|
|
162
|
+
say(options, `I could not list your repositories: ${reason}.`);
|
|
163
|
+
say(options, ` This directory has no GitHub origin remote either, so I have nothing to offer.`);
|
|
164
|
+
say(options, ` Next: ${next}`);
|
|
165
|
+
emit(options, {
|
|
166
|
+
step: "repositories",
|
|
167
|
+
source: "none",
|
|
168
|
+
workspaceKnown: false,
|
|
169
|
+
repositories: [],
|
|
170
|
+
reason,
|
|
171
|
+
});
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
say(options, `I could not list your repositories: ${reason}.`);
|
|
175
|
+
say(options, ` This directory's origin remote is ${here}, which is the one setup adds by default.`);
|
|
176
|
+
say(options, ` Next: ${next}`);
|
|
177
|
+
emit(options, {
|
|
178
|
+
step: "repositories",
|
|
179
|
+
source: "origin",
|
|
180
|
+
workspaceKnown: false,
|
|
181
|
+
repositories: [{ repository: here.toLowerCase(), here: true }],
|
|
182
|
+
reason,
|
|
183
|
+
});
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type SessionOptions = Readonly<{
|
|
2
|
+
controlPlane: string;
|
|
3
|
+
json: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Read the session out of the commit at HEAD and tell Balladeer which commit
|
|
6
|
+
* it wrote, instead of printing an id.
|
|
7
|
+
*/
|
|
8
|
+
record: boolean;
|
|
9
|
+
/** Start a new session even though one is still current. */
|
|
10
|
+
fresh: boolean;
|
|
11
|
+
/** The repository whose connection to use, named as `owner/name`. */
|
|
12
|
+
repo?: string;
|
|
13
|
+
/** The repository whose connection to use, named by id, as `mcp` takes it. */
|
|
14
|
+
repository?: string;
|
|
15
|
+
environment: NodeJS.ProcessEnv;
|
|
16
|
+
cwd: string;
|
|
17
|
+
now?: string;
|
|
18
|
+
write: (text: string) => void;
|
|
19
|
+
}>;
|
|
20
|
+
/**
|
|
21
|
+
* The id that joins what an agent was told to what it then wrote.
|
|
22
|
+
*
|
|
23
|
+
* Retrieval happens before the commit exists, so nothing ties the two together
|
|
24
|
+
* on its own, and without the tie a red check on Tuesday cannot be read back
|
|
25
|
+
* against what Balladeer had said on Monday. The agent's own working session
|
|
26
|
+
* spans both: it reads the index under this id and writes the same id into the
|
|
27
|
+
* commit it produces.
|
|
28
|
+
*
|
|
29
|
+
* `balladeer session` prints the id and the exact line to write.
|
|
30
|
+
* `balladeer session --record` reads that line back out of the commit at HEAD
|
|
31
|
+
* and tells Balladeer which commit the session wrote. The commit message itself
|
|
32
|
+
* never leaves this machine: only the pair of a session id and a forty-
|
|
33
|
+
* character SHA is sent, and the SHA is a fact CI already reports.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runSession(options: SessionOptions): Promise<number>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { callAgentTool, noAgentCredentialSentence, selectAgent } from "../agent.js";
|
|
2
|
+
import { headCommit } from "../git.js";
|
|
3
|
+
import { repositoryHint } from "../repository.js";
|
|
4
|
+
import { AGENT_SESSION_TRAILER, agentSessionTrailerLine, currentSession, readAgentSessionTrailer, } from "../session.js";
|
|
5
|
+
import { StoreError, readCredentials } from "../store.js";
|
|
6
|
+
import {} from "../wire.js";
|
|
7
|
+
/**
|
|
8
|
+
* The id that joins what an agent was told to what it then wrote.
|
|
9
|
+
*
|
|
10
|
+
* Retrieval happens before the commit exists, so nothing ties the two together
|
|
11
|
+
* on its own, and without the tie a red check on Tuesday cannot be read back
|
|
12
|
+
* against what Balladeer had said on Monday. The agent's own working session
|
|
13
|
+
* spans both: it reads the index under this id and writes the same id into the
|
|
14
|
+
* commit it produces.
|
|
15
|
+
*
|
|
16
|
+
* `balladeer session` prints the id and the exact line to write.
|
|
17
|
+
* `balladeer session --record` reads that line back out of the commit at HEAD
|
|
18
|
+
* and tells Balladeer which commit the session wrote. The commit message itself
|
|
19
|
+
* never leaves this machine: only the pair of a session id and a forty-
|
|
20
|
+
* character SHA is sent, and the SHA is a fact CI already reports.
|
|
21
|
+
*/
|
|
22
|
+
export async function runSession(options) {
|
|
23
|
+
const emit = (step) => {
|
|
24
|
+
if (options.json)
|
|
25
|
+
options.write(`${JSON.stringify(step)}\n`);
|
|
26
|
+
};
|
|
27
|
+
const say = (text) => {
|
|
28
|
+
if (!options.json)
|
|
29
|
+
options.write(`${text}\n`);
|
|
30
|
+
};
|
|
31
|
+
const fail = (reason, message, exitCode) => {
|
|
32
|
+
if (options.json)
|
|
33
|
+
emit({ step: "error", reason, message, changed: false, exitCode });
|
|
34
|
+
else
|
|
35
|
+
options.write(`${message}\n`);
|
|
36
|
+
return exitCode;
|
|
37
|
+
};
|
|
38
|
+
let credentials;
|
|
39
|
+
try {
|
|
40
|
+
credentials = readCredentials(options.environment);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return fail(error instanceof StoreError ? error.code : "credential_store_unusable", error instanceof StoreError ? error.message : String(error), 4);
|
|
44
|
+
}
|
|
45
|
+
const here = options.repo ?? repositoryHint(options.cwd);
|
|
46
|
+
const selection = selectAgent(credentials.agents, options.controlPlane, options.repository, here);
|
|
47
|
+
if (selection.kind === "refused") {
|
|
48
|
+
const message = selection.missingFor === undefined
|
|
49
|
+
? selection.reason
|
|
50
|
+
: noAgentCredentialSentence(selection.missingFor);
|
|
51
|
+
return fail("no_agent_credential_on_this_machine", message, 4);
|
|
52
|
+
}
|
|
53
|
+
const agent = selection.agent;
|
|
54
|
+
if (options.record)
|
|
55
|
+
return recordCommit(options, agent, emit, say, fail);
|
|
56
|
+
let session;
|
|
57
|
+
try {
|
|
58
|
+
session = currentSession({
|
|
59
|
+
repository: agent.repositoryId,
|
|
60
|
+
now: options.now ?? new Date().toISOString(),
|
|
61
|
+
...(options.fresh ? { forceNew: true } : {}),
|
|
62
|
+
environment: options.environment,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return fail(error instanceof StoreError ? error.code : "session_store_unwritable", error instanceof StoreError ? error.message : String(error), 4);
|
|
67
|
+
}
|
|
68
|
+
const trailer = agentSessionTrailerLine(session.sessionId);
|
|
69
|
+
emit({
|
|
70
|
+
step: "session",
|
|
71
|
+
sessionId: session.sessionId,
|
|
72
|
+
startedAt: session.startedAt,
|
|
73
|
+
minted: session.minted,
|
|
74
|
+
trailer,
|
|
75
|
+
changed: session.minted,
|
|
76
|
+
});
|
|
77
|
+
say(session.sessionId);
|
|
78
|
+
say("");
|
|
79
|
+
say(session.minted
|
|
80
|
+
? "This is a new session. Pass it to every promise read you make in this repository."
|
|
81
|
+
: "This session is already open. Pass it to every promise read you make in this repository, and use --new to start a different one.");
|
|
82
|
+
say(`Then write this line into the commit or pull-request body you produce:`);
|
|
83
|
+
say("");
|
|
84
|
+
say(` ${trailer}`);
|
|
85
|
+
say("");
|
|
86
|
+
say(`and run \`balladeer session --record\` once the commit exists, so a check that goes red on it can be read back against what you were told before you started. Balladeer is sent the session id and the commit SHA, and nothing else.`);
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
async function recordCommit(options, agent, emit, say, fail) {
|
|
90
|
+
const head = await headCommit(options.cwd);
|
|
91
|
+
if (head === undefined) {
|
|
92
|
+
return fail("no_commit_here", "There is no commit to record: this directory is not a git checkout, or it has no commits yet.", 4);
|
|
93
|
+
}
|
|
94
|
+
const sessionId = readAgentSessionTrailer(head.message);
|
|
95
|
+
if (sessionId === undefined) {
|
|
96
|
+
// Said in full rather than as a code. The likeliest reader of this line is
|
|
97
|
+
// an agent that forgot the trailer, and the remedy is one line it can add
|
|
98
|
+
// to the commit it just made.
|
|
99
|
+
return fail("no_session_trailer", `The commit at HEAD carries no ${AGENT_SESSION_TRAILER} line this release recognises, so there is nothing to record. Add \`${AGENT_SESSION_TRAILER}: <the id balladeer session prints>\` to the commit message and run this again. A commit carrying two different session ids is refused rather than guessed at.`, 4);
|
|
100
|
+
}
|
|
101
|
+
const call = await callAgentTool(agent, "record_session_commit", {
|
|
102
|
+
session: sessionId,
|
|
103
|
+
commitSha: head.sha,
|
|
104
|
+
});
|
|
105
|
+
if (call.kind !== "result") {
|
|
106
|
+
return fail(call.kind === "tool_refusal" ? "refused" : call.kind, call.kind === "tool_refusal"
|
|
107
|
+
? call.text
|
|
108
|
+
: `Balladeer could not record this commit (${call.kind}). Nothing was recorded, and running this again after the connection is back is safe: the same pair recorded twice is recorded once.`, 5);
|
|
109
|
+
}
|
|
110
|
+
emit({
|
|
111
|
+
step: "session_commit",
|
|
112
|
+
sessionId,
|
|
113
|
+
commitSha: head.sha,
|
|
114
|
+
changed: true,
|
|
115
|
+
});
|
|
116
|
+
say(`Recorded ${head.sha.slice(0, 12)} as written by this session.`);
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export type SetupOptions = Readonly<{
|
|
2
|
+
controlPlane: string;
|
|
3
|
+
json: boolean;
|
|
4
|
+
wait: boolean;
|
|
5
|
+
repo?: string;
|
|
6
|
+
createWorkspace?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Every repository this run was told to set up, in the order they were named.
|
|
9
|
+
*
|
|
10
|
+
* One of them is this run's own: the steps after the enrollment write files
|
|
11
|
+
* and push a branch, and they can only do that to the working tree this
|
|
12
|
+
* process was started in. The rest are added to the workspace and nothing
|
|
13
|
+
* else, which is exactly what an agent enrolling a team's repositories from
|
|
14
|
+
* one checkout can honestly do from here, and the run says so per repository.
|
|
15
|
+
*/
|
|
16
|
+
repositories?: readonly string[];
|
|
17
|
+
/**
|
|
18
|
+
* Repair the two files a previous setup wrote in this repository, and do
|
|
19
|
+
* nothing else. No pairing, no enrollment, no credential, no network: a
|
|
20
|
+
* stale install is repaired where it is, which may be a laptop that cannot
|
|
21
|
+
* reach the control plane at that moment.
|
|
22
|
+
*/
|
|
23
|
+
refresh?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Set up even though an earlier Balladeer is still installed on this machine.
|
|
26
|
+
*
|
|
27
|
+
* The check that stops this run is a warning about two programs claiming one
|
|
28
|
+
* name, not a safety property, so somebody who has decided to run both says so
|
|
29
|
+
* and this run carries on exactly as it would have.
|
|
30
|
+
*/
|
|
31
|
+
force?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Whether to also connect Claude desktop chat for this repository.
|
|
34
|
+
*
|
|
35
|
+
* Three states, because there are three answers and only two of them are a
|
|
36
|
+
* flag. `true` is `--claude-desktop`: connect it, and say out loud when this
|
|
37
|
+
* machine has no such app rather than skipping in silence. `false` is
|
|
38
|
+
* `--no-claude-desktop`: do not touch that file and do not mention it.
|
|
39
|
+
* Undefined is the ordinary run, which connects it where Claude desktop is
|
|
40
|
+
* installed and says nothing at all where it is not. Writing it by default is
|
|
41
|
+
* the same bargain `.mcp.json` already makes: the entry is additive, it
|
|
42
|
+
* carries no credential, it never replaces anybody else's server, and the
|
|
43
|
+
* people this is for write their plan in that chat before they open a coding
|
|
44
|
+
* agent at all.
|
|
45
|
+
*/
|
|
46
|
+
claudeDesktop?: boolean;
|
|
47
|
+
environment: NodeJS.ProcessEnv;
|
|
48
|
+
cwd: string;
|
|
49
|
+
write: (text: string) => void;
|
|
50
|
+
sleep?: (ms: number) => Promise<void>;
|
|
51
|
+
now?: () => Date;
|
|
52
|
+
}>;
|
|
53
|
+
/** The bound the control plane's own name check holds, refused here so a name
|
|
54
|
+
* nobody could create never costs a pairing code. */
|
|
55
|
+
export declare const CREATE_WORKSPACE_MAX_LENGTH = 120;
|
|
56
|
+
/**
|
|
57
|
+
* Which repository this run acts on, and which it only adds.
|
|
58
|
+
*
|
|
59
|
+
* Every step after the enrollment acts on the working tree this process was
|
|
60
|
+
* started in: it writes `.mcp.json` and the conventions block there, and it
|
|
61
|
+
* cuts, commits and pushes the CI branch to that tree's own remote. So exactly
|
|
62
|
+
* one named repository can be this run's own, and it has to be the one this
|
|
63
|
+
* directory is a checkout of. Naming one repository while sitting in another
|
|
64
|
+
* would enroll one and change the other, and push a branch to a repository
|
|
65
|
+
* nobody named.
|
|
66
|
+
*
|
|
67
|
+
* A directory with no readable GitHub origin is not a disagreement: naming a
|
|
68
|
+
* repository is exactly how a person adds one from somewhere that is not a
|
|
69
|
+
* checkout of it, and every step below says what it could not do there.
|
|
70
|
+
*/
|
|
71
|
+
type Chosen = Readonly<{
|
|
72
|
+
kind: "chosen";
|
|
73
|
+
primary: string;
|
|
74
|
+
also: readonly string[];
|
|
75
|
+
}> | Readonly<{
|
|
76
|
+
kind: "mismatch";
|
|
77
|
+
message: string;
|
|
78
|
+
}>;
|
|
79
|
+
export declare function chooseRepositories(named: readonly string[], cwd: string): Chosen;
|
|
80
|
+
/**
|
|
81
|
+
* One run. It prints what it found, does what it can, and exits within seconds.
|
|
82
|
+
* Waiting is the person's choice, behind `--wait`, because an agent shell kills
|
|
83
|
+
* a command that blocks for minutes and the pairing would be lost with it.
|
|
84
|
+
*/
|
|
85
|
+
export declare function runSetup(options: SetupOptions): Promise<number>;
|
|
86
|
+
/**
|
|
87
|
+
* Which invocation this repair writes, decided without asking anybody.
|
|
88
|
+
*
|
|
89
|
+
* A repair runs where the stale install is, and the server is not always
|
|
90
|
+
* reachable from there, so the two honest local sources are how this copy was
|
|
91
|
+
* itself obtained and what the file already says. A copy running out of a
|
|
92
|
+
* registry install writes the registry form. A copy running out of a checkout
|
|
93
|
+
* leaves a registry form alone rather than downgrading a published install to
|
|
94
|
+
* an absolute path on one person's laptop, and writes the checkout form only
|
|
95
|
+
* where the file was already a checkout form or had no entry at all.
|
|
96
|
+
*/
|
|
97
|
+
export declare function refreshPublishedForm(existing: unknown): string | null;
|
|
98
|
+
export {};
|