auto-harness-client 0.6.0 → 0.8.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/README.md +467 -0
- package/package.json +5 -1
- package/src/cli/admin-login.js +71 -0
- package/src/cli/allowlist.js +34 -0
- package/src/cli/args.js +57 -0
- package/src/cli/cli-errors.js +16 -0
- package/src/cli/commands/api.js +91 -0
- package/src/cli/commands/attach-repository.js +117 -0
- package/src/cli/commands/dependency-conflict.js +15 -0
- package/src/cli/commands/detach-repository.js +109 -0
- package/src/cli/commands/doctor.js +164 -0
- package/src/cli/commands/host-drain.js +22 -0
- package/src/cli/commands/host-inventory-get.js +40 -0
- package/src/cli/commands/host-inventory-set.js +66 -0
- package/src/cli/commands/host-inventory.js +15 -0
- package/src/cli/commands/host-list.js +87 -0
- package/src/cli/commands/host-post-action.js +38 -0
- package/src/cli/commands/host-repo-add.js +81 -0
- package/src/cli/commands/host-repo-rm.js +51 -0
- package/src/cli/commands/host-repo.js +16 -0
- package/src/cli/commands/host-resume.js +17 -0
- package/src/cli/commands/host-smoke-format.js +30 -0
- package/src/cli/commands/host-smoke-poll.js +77 -0
- package/src/cli/commands/host-smoke-provider.js +172 -0
- package/src/cli/commands/host-smoke-repository.js +49 -0
- package/src/cli/commands/host-smoke-session-attempt.js +104 -0
- package/src/cli/commands/host-smoke-teardown.js +164 -0
- package/src/cli/commands/host-smoke.js +151 -0
- package/src/cli/commands/host.js +31 -0
- package/src/cli/commands/parse-worktree-flag.js +30 -0
- package/src/cli/commands/repo-add.js +38 -0
- package/src/cli/commands/repo-list.js +84 -0
- package/src/cli/commands/repo-rm.js +83 -0
- package/src/cli/commands/repo.js +18 -0
- package/src/cli/commands/service-account-create.js +114 -0
- package/src/cli/commands/service-account-list.js +84 -0
- package/src/cli/commands/service-account-rm.js +48 -0
- package/src/cli/commands/service-account.js +19 -0
- package/src/cli/commands/session-cancel.js +28 -0
- package/src/cli/commands/session-create.js +117 -0
- package/src/cli/commands/session-get.js +26 -0
- package/src/cli/commands/session-logs.js +65 -0
- package/src/cli/commands/session-target.js +29 -0
- package/src/cli/commands/session.js +22 -0
- package/src/cli/commands/whoami.js +22 -0
- package/src/cli/config.js +96 -0
- package/src/cli/index.js +21 -0
- package/src/cli/main.js +71 -0
- package/src/cli/path-segment.js +20 -0
- package/src/cli/read-stdin.js +9 -0
- package/src/cli/report-error.js +34 -0
- package/src/cli/service-account-format.js +24 -0
- package/src/cli/session-format.js +15 -0
- package/src/cli/usage.js +83 -0
- package/src/cli/wait-for-session.js +46 -0
- package/src/errors.js +1 -0
- package/src/index.d.ts +64 -4
- package/src/index.js +76 -30
- package/src/resolve-target.js +29 -13
package/src/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- one small client class covering every REST method is the published surface. */
|
|
1
2
|
import {
|
|
2
3
|
AutoHarnessDrainWaitTimeoutError,
|
|
3
4
|
AutoHarnessError,
|
|
@@ -5,7 +6,7 @@ import {
|
|
|
5
6
|
} from "./errors.js";
|
|
6
7
|
import { assertSecureTransport } from "./loopback.js";
|
|
7
8
|
import { resolveRepositoryId } from "./resolve-repository.js";
|
|
8
|
-
import { resolveCreateSessionTargets } from "./resolve-target.js";
|
|
9
|
+
import { resolveCreateSessionTargets, resolveTargetSpecs } from "./resolve-target.js";
|
|
9
10
|
|
|
10
11
|
export { AutoHarnessDrainWaitTimeoutError, AutoHarnessError, AutoHarnessRequestTimeoutError };
|
|
11
12
|
|
|
@@ -64,6 +65,7 @@ export class AutoHarnessClient {
|
|
|
64
65
|
retryAfter: response.headers.get("retry-after") ?? undefined,
|
|
65
66
|
operationId: error?.operationId,
|
|
66
67
|
statusUrl: error?.statusUrl,
|
|
68
|
+
details: error,
|
|
67
69
|
},
|
|
68
70
|
);
|
|
69
71
|
}
|
|
@@ -85,15 +87,42 @@ export class AutoHarnessClient {
|
|
|
85
87
|
return this.request(`/sessions/${encodeURIComponent(id)}`);
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
createChildSession(parentId, input) {
|
|
91
|
+
return this.request(`/sessions/${encodeURIComponent(parentId)}/children`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
body: JSON.stringify(input),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
listChildSessions(parentId, options = {}) {
|
|
98
|
+
const query = new URLSearchParams();
|
|
99
|
+
if (options.limit !== undefined) query.set("limit", String(options.limit));
|
|
100
|
+
if (options.cursor !== undefined) query.set("cursor", options.cursor);
|
|
101
|
+
const suffix = query.toString();
|
|
102
|
+
return this.request(
|
|
103
|
+
suffix
|
|
104
|
+
? `/sessions/${encodeURIComponent(parentId)}/children?${suffix}`
|
|
105
|
+
: `/sessions/${encodeURIComponent(parentId)}/children`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
88
109
|
cancelSession(id) {
|
|
89
110
|
return this.request(`/sessions/${encodeURIComponent(id)}/cancel`, { method: "POST" });
|
|
90
111
|
}
|
|
91
112
|
|
|
92
|
-
/**
|
|
93
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Resume a previously assigned session on its pinned host, native CLI resume where supported.
|
|
115
|
+
* An optional `target`/`fallbacks` override rebinds the resume to a different Command/Provider
|
|
116
|
+
* — a repoint applying on the next resume, instead of only to new dispatches — and its
|
|
117
|
+
* `providerName`/`commandName` sugar is resolved the same way `createSession()` resolves it.
|
|
118
|
+
* Resolution only runs when `target` is present, so an id-only or bodyless resume makes no
|
|
119
|
+
* extra `listCommands()`/`listProviders()` requests.
|
|
120
|
+
*/
|
|
121
|
+
async resumeSession(id, input) {
|
|
122
|
+
const body = input?.target === undefined ? input : await resolveTargetSpecs(this, input);
|
|
94
123
|
return this.request(`/sessions/${encodeURIComponent(id)}/resume`, {
|
|
95
124
|
method: "POST",
|
|
96
|
-
...(
|
|
125
|
+
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
97
126
|
});
|
|
98
127
|
}
|
|
99
128
|
|
|
@@ -161,37 +190,40 @@ export class AutoHarnessClient {
|
|
|
161
190
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
162
191
|
throw new TypeError("timeoutMs must be a finite positive number");
|
|
163
192
|
const deadline = Date.now() + timeoutMs;
|
|
164
|
-
const clampedClient = () =>
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
193
|
+
const clampedClient = () => {
|
|
194
|
+
const remainingMs = deadline - Date.now();
|
|
195
|
+
const requestTimeoutMs = Math.max(1, Math.ceil(Math.min(this.requestTimeoutMs, remainingMs)));
|
|
196
|
+
return {
|
|
197
|
+
client: new AutoHarnessClient({
|
|
198
|
+
baseUrl: this.baseUrl,
|
|
199
|
+
apiKey: this.apiKey,
|
|
200
|
+
allowInsecureHttp: this.allowInsecureHttp,
|
|
201
|
+
fetch: this.fetch,
|
|
202
|
+
requestTimeoutMs,
|
|
203
|
+
}),
|
|
204
|
+
clampedToWaitBudget: remainingMs <= this.requestTimeoutMs,
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
const rejectAtDeadline = (error, idForError, clampedToWaitBudget) =>
|
|
208
|
+
error instanceof AutoHarnessRequestTimeoutError &&
|
|
209
|
+
(clampedToWaitBudget || Date.now() >= deadline)
|
|
177
210
|
? new AutoHarnessDrainWaitTimeoutError(idForError, operationId, timeoutMs)
|
|
178
211
|
: error;
|
|
179
212
|
|
|
213
|
+
const repositoryLookup = clampedClient();
|
|
180
214
|
const id = await resolveRepositoryId(
|
|
181
|
-
{ listRepositories: (
|
|
215
|
+
{ listRepositories: (opts) => repositoryLookup.client.listRepositories(opts) },
|
|
182
216
|
repositoryId,
|
|
183
217
|
).catch((error) => {
|
|
184
|
-
throw rejectAtDeadline(error, repositoryId);
|
|
218
|
+
throw rejectAtDeadline(error, repositoryId, repositoryLookup.clampedToWaitBudget);
|
|
185
219
|
});
|
|
186
220
|
for (;;) {
|
|
187
|
-
if (deadline - Date.now() <= 0)
|
|
221
|
+
if (deadline - Date.now() <= 0)
|
|
188
222
|
throw new AutoHarnessDrainWaitTimeoutError(id, operationId, timeoutMs);
|
|
189
|
-
}
|
|
190
|
-
const sessionDrain = await
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
throw rejectAtDeadline(error, id);
|
|
194
|
-
});
|
|
223
|
+
const { client, clampedToWaitBudget } = clampedClient();
|
|
224
|
+
const sessionDrain = await client.getSessionDrain(id, operationId).catch((error) => {
|
|
225
|
+
throw rejectAtDeadline(error, id, clampedToWaitBudget);
|
|
226
|
+
});
|
|
195
227
|
if (sessionDrain.status !== "draining") return sessionDrain;
|
|
196
228
|
const delayMs = Math.min(pollIntervalMs, deadline - Date.now());
|
|
197
229
|
if (delayMs <= 0) throw new AutoHarnessDrainWaitTimeoutError(id, operationId, timeoutMs);
|
|
@@ -224,12 +256,26 @@ export class AutoHarnessClient {
|
|
|
224
256
|
}
|
|
225
257
|
|
|
226
258
|
async listProviders() {
|
|
227
|
-
|
|
228
|
-
return items;
|
|
259
|
+
return this.listCatalog("/providers");
|
|
229
260
|
}
|
|
230
261
|
|
|
231
262
|
async listCommands() {
|
|
232
|
-
|
|
233
|
-
|
|
263
|
+
return this.listCatalog("/commands");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async listCatalog(path) {
|
|
267
|
+
const items = [];
|
|
268
|
+
const seen = new Set();
|
|
269
|
+
let suffix = "?limit=100";
|
|
270
|
+
for (let page = 0; page < 20; page += 1) {
|
|
271
|
+
const body = await this.request(`${path}${suffix}`);
|
|
272
|
+
items.push(...(body.items ?? []));
|
|
273
|
+
const cursor = body.nextCursor ?? null;
|
|
274
|
+
if (!cursor) return items;
|
|
275
|
+
if (seen.has(cursor)) throw new Error(`repeated pagination cursor for ${path}`);
|
|
276
|
+
seen.add(cursor);
|
|
277
|
+
suffix = `?limit=100&cursor=${encodeURIComponent(cursor)}`;
|
|
278
|
+
}
|
|
279
|
+
throw new Error(`pagination exceeded 20 pages for ${path}`);
|
|
234
280
|
}
|
|
235
281
|
}
|
package/src/resolve-target.js
CHANGED
|
@@ -16,31 +16,47 @@ async function resolveRef(ref, providers, commands) {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Resolves `providerName`/`commandName` entries in `input.target`/`input.fallbacks
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* Resolves `providerName`/`commandName` entries in `input.target`/`input.fallbacks` to their ids
|
|
20
|
+
* via `client.listProviders()`/`listCommands()`, each called at most once regardless of how many
|
|
21
|
+
* refs need it. Id-shaped refs pass through untouched, so an all-id call makes no extra requests.
|
|
22
|
+
* Names are resolved defensively against more than one match: create/update checks are
|
|
23
|
+
* read-then-write races rather than atomic constraints, and legacy catalog rows are not rewritten.
|
|
24
|
+
* Shared by session create and resume — resume has no repository to resolve, so it calls this
|
|
25
|
+
* directly instead of `resolveCreateSessionTargets`.
|
|
25
26
|
*/
|
|
26
|
-
export async function
|
|
27
|
+
export async function resolveTargetSpecs(client, input) {
|
|
27
28
|
let providersPromise;
|
|
28
29
|
let commandsPromise;
|
|
29
30
|
const providers = () => (providersPromise ??= client.listProviders());
|
|
30
31
|
const commands = () => (commandsPromise ??= client.listCommands());
|
|
31
32
|
|
|
32
33
|
const target = await resolveRef(input.target, providers, commands);
|
|
34
|
+
const resolved = { ...input, target };
|
|
35
|
+
if (input.fallbacks === undefined) return resolved;
|
|
36
|
+
const fallbacks = await Promise.all(
|
|
37
|
+
input.fallbacks.map((fallback) => resolveRef(fallback, providers, commands)),
|
|
38
|
+
);
|
|
39
|
+
return { ...resolved, fallbacks };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolves `providerName`/`commandName` entries in `input.target`/`input.fallbacks`, and
|
|
44
|
+
* `input.repositoryName`, to their ids via `client.listProviders()`/`listCommands()`/
|
|
45
|
+
* `listRepositories()`.
|
|
46
|
+
*/
|
|
47
|
+
export async function resolveCreateSessionTargets(client, input) {
|
|
48
|
+
if (Object.hasOwn(input, "setupScript")) {
|
|
49
|
+
throw new TypeError("setupScript is not accepted; use setupProfileId");
|
|
50
|
+
}
|
|
51
|
+
const withTargets = await resolveTargetSpecs(client, input);
|
|
52
|
+
if (input.repositoryId === null) return withTargets;
|
|
33
53
|
const repositoryId = await resolveRepositoryId(
|
|
34
54
|
client,
|
|
35
55
|
input.repositoryId !== undefined
|
|
36
56
|
? input.repositoryId
|
|
37
57
|
: { repositoryName: input.repositoryName },
|
|
38
58
|
);
|
|
39
|
-
const resolved = { ...
|
|
59
|
+
const resolved = { ...withTargets, repositoryId };
|
|
40
60
|
delete resolved.repositoryName;
|
|
41
|
-
|
|
42
|
-
const fallbacks = await Promise.all(
|
|
43
|
-
input.fallbacks.map((fallback) => resolveRef(fallback, providers, commands)),
|
|
44
|
-
);
|
|
45
|
-
return { ...resolved, fallbacks };
|
|
61
|
+
return resolved;
|
|
46
62
|
}
|