@allocator-one/harness-cli 1.15.1 → 1.17.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 +256 -38
- package/dist/{chunk-FQWV5MIC.js → chunk-5THYIKLW.js} +26 -4
- package/dist/chunk-A4WZAC7L.js +563 -0
- package/dist/chunk-BHBEIT5I.js +1348 -0
- package/dist/{chunk-L2OWOZ4N.js → chunk-CEEQW6ZV.js} +411 -1556
- package/dist/chunk-W7BVOQQG.js +1448 -0
- package/dist/chunk-WSDFLMO6.js +717 -0
- package/dist/cli.js +1886 -1155
- package/dist/infraone-A3KDADRM.js +249 -0
- package/dist/infraone-V3IKKVBG.js +593 -0
- package/dist/infraone.js +372 -0
- package/dist/{login-page-KGEJFFXR.js → login-page-MFHNVTUV.js} +1 -1
- package/dist/{server-KVRQBVXI.js → server-TRXBTJSA.js} +37 -19
- package/package.json +4 -3
- package/skills/harness-code/SKILL.md +37 -0
- package/spec/openapi.json +4803 -25
package/dist/cli.js
CHANGED
|
@@ -1,55 +1,44 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
UPDATE_CHECK_COMMAND,
|
|
4
|
+
guardBrokenPipes,
|
|
5
|
+
operatorDeprecationNotice,
|
|
6
|
+
performUpdateCheck,
|
|
7
|
+
selfHeal,
|
|
8
|
+
updateCheck
|
|
9
|
+
} from "./chunk-A4WZAC7L.js";
|
|
2
10
|
import {
|
|
3
11
|
ACTIVITY_MAX_LIMIT,
|
|
4
|
-
Client,
|
|
5
|
-
CommandError,
|
|
6
|
-
DEFAULT_BASE_URL,
|
|
7
12
|
EXIT_INTERRUPTED,
|
|
8
13
|
EXIT_JOB_ENDED,
|
|
9
14
|
EXIT_WAITING_FOR_PERSON,
|
|
10
|
-
EXPERT_ASSIGNMENTS_PATH,
|
|
11
|
-
EXPERT_LAPSED,
|
|
12
|
-
HttpError,
|
|
13
15
|
JOBS_MAX_LIMIT,
|
|
14
16
|
JOBS_MAX_PAGES,
|
|
15
17
|
JOB_STATES,
|
|
16
|
-
|
|
18
|
+
PLAN_LIST_MAX_LIMIT,
|
|
19
|
+
PLAN_STATUSES,
|
|
17
20
|
RELATION_CREATE_TYPES,
|
|
18
21
|
RELATION_UPDATE_TYPES,
|
|
19
|
-
SESSION_PATH,
|
|
20
22
|
STEER_MAX_CHARACTERS,
|
|
21
|
-
TransportError,
|
|
22
|
-
UpgradeRequiredError,
|
|
23
|
-
UsageError,
|
|
24
23
|
activity,
|
|
25
24
|
archive,
|
|
26
25
|
assignments,
|
|
27
26
|
attachImage,
|
|
28
27
|
blocked,
|
|
29
|
-
clean,
|
|
30
28
|
close,
|
|
31
29
|
comment,
|
|
32
30
|
comments,
|
|
33
31
|
conditionList,
|
|
34
32
|
create,
|
|
35
|
-
defaultCredentialsPath,
|
|
36
33
|
deleteComment,
|
|
37
|
-
deleteCredentials,
|
|
38
34
|
dep,
|
|
39
35
|
docCreate,
|
|
40
36
|
docList,
|
|
41
37
|
docShow,
|
|
42
38
|
docUpdate,
|
|
43
39
|
editComment,
|
|
44
|
-
errorDetails,
|
|
45
|
-
expertSummary,
|
|
46
|
-
explainExpertRefusal,
|
|
47
|
-
findRepoConfig,
|
|
48
40
|
gateSet,
|
|
49
41
|
instructionFrom,
|
|
50
|
-
isExpertToken,
|
|
51
|
-
isHostTrusted,
|
|
52
|
-
isLoginCredential,
|
|
53
42
|
jobCancel,
|
|
54
43
|
jobContinue,
|
|
55
44
|
jobPause,
|
|
@@ -57,7 +46,6 @@ import {
|
|
|
57
46
|
jobSteer,
|
|
58
47
|
jobWatch,
|
|
59
48
|
jobsList,
|
|
60
|
-
json,
|
|
61
49
|
labelCreate,
|
|
62
50
|
labelDelete,
|
|
63
51
|
labelList,
|
|
@@ -67,7 +55,6 @@ import {
|
|
|
67
55
|
linkRm,
|
|
68
56
|
linkUpdate,
|
|
69
57
|
list,
|
|
70
|
-
loginHelp,
|
|
71
58
|
memberAdd,
|
|
72
59
|
memberList,
|
|
73
60
|
memberSetRole,
|
|
@@ -76,6 +63,7 @@ import {
|
|
|
76
63
|
planCreate,
|
|
77
64
|
planEdit,
|
|
78
65
|
planHold,
|
|
66
|
+
planList,
|
|
79
67
|
planRelease,
|
|
80
68
|
planShow,
|
|
81
69
|
prepareProcedureRun,
|
|
@@ -87,7 +75,6 @@ import {
|
|
|
87
75
|
projectList,
|
|
88
76
|
projectShow,
|
|
89
77
|
projectUpdate,
|
|
90
|
-
readCredentials,
|
|
91
78
|
readPlanFile,
|
|
92
79
|
ready,
|
|
93
80
|
relationAdd,
|
|
@@ -100,7 +87,8 @@ import {
|
|
|
100
87
|
requireIdentifiers,
|
|
101
88
|
requireJobId,
|
|
102
89
|
requireJobsArgs,
|
|
103
|
-
|
|
90
|
+
requirePlanListArgs,
|
|
91
|
+
requirePlanRef,
|
|
104
92
|
requireProcedureId,
|
|
105
93
|
requireProcedureName,
|
|
106
94
|
requireReadyClaimArgs,
|
|
@@ -108,8 +96,6 @@ import {
|
|
|
108
96
|
requireSearchQuery,
|
|
109
97
|
requireToolsOpts,
|
|
110
98
|
requiredTeam,
|
|
111
|
-
resolveCacheHome,
|
|
112
|
-
resolveConfig,
|
|
113
99
|
resolveTextSources,
|
|
114
100
|
run,
|
|
115
101
|
search,
|
|
@@ -129,27 +115,50 @@ import {
|
|
|
129
115
|
toolsDomains,
|
|
130
116
|
toolsList,
|
|
131
117
|
toolsSearch,
|
|
132
|
-
trustHost,
|
|
133
118
|
update,
|
|
134
119
|
upload,
|
|
135
120
|
userList,
|
|
136
|
-
validateBaseUrl,
|
|
137
121
|
validateIdempotencyKey,
|
|
138
122
|
version,
|
|
139
123
|
watchStopSignal,
|
|
140
|
-
workflowStates
|
|
141
|
-
|
|
142
|
-
|
|
124
|
+
workflowStates
|
|
125
|
+
} from "./chunk-CEEQW6ZV.js";
|
|
126
|
+
import {
|
|
127
|
+
login,
|
|
128
|
+
logout,
|
|
129
|
+
whoami
|
|
130
|
+
} from "./chunk-WSDFLMO6.js";
|
|
131
|
+
import {
|
|
132
|
+
Client,
|
|
133
|
+
CommandError,
|
|
134
|
+
DEFAULT_BASE_URL,
|
|
135
|
+
HttpError,
|
|
136
|
+
LocalError,
|
|
137
|
+
TransportError,
|
|
138
|
+
UpgradeRequiredError,
|
|
139
|
+
UsageError,
|
|
140
|
+
clean,
|
|
141
|
+
defaultCredentialsPath,
|
|
142
|
+
errorDetails,
|
|
143
|
+
explainExpertRefusal,
|
|
144
|
+
findRepoConfig,
|
|
145
|
+
isHostTrusted,
|
|
146
|
+
json,
|
|
147
|
+
loginHelp,
|
|
148
|
+
readCredentials,
|
|
149
|
+
resolveCacheHome,
|
|
150
|
+
resolveConfig,
|
|
151
|
+
trustHost
|
|
152
|
+
} from "./chunk-BHBEIT5I.js";
|
|
143
153
|
import {
|
|
144
154
|
CLI_VERSION,
|
|
145
|
-
OPERATOR_PACKAGE,
|
|
146
155
|
invokedName,
|
|
147
156
|
layoutOperatorHelp,
|
|
148
157
|
named
|
|
149
|
-
} from "./chunk-
|
|
158
|
+
} from "./chunk-5THYIKLW.js";
|
|
150
159
|
|
|
151
160
|
// src/cli.ts
|
|
152
|
-
import { createInterface
|
|
161
|
+
import { createInterface } from "readline";
|
|
153
162
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
154
163
|
|
|
155
164
|
// src/commands/keys.ts
|
|
@@ -203,9 +212,9 @@ function selectSlugs(stored, only) {
|
|
|
203
212
|
function unknownSlugs(only) {
|
|
204
213
|
return only.map((slug) => slug.trim()).filter((slug) => slug !== "" && !KEY_ENV_VARS.has(slug.toLowerCase()));
|
|
205
214
|
}
|
|
206
|
-
function withholdOthers(
|
|
215
|
+
function withholdOthers(base2, requested) {
|
|
207
216
|
const keep = new Set(requested.map((slug) => envVarFor(slug)));
|
|
208
|
-
const env = { ...
|
|
217
|
+
const env = { ...base2 };
|
|
209
218
|
for (const name of KEY_ENV_VARS.values()) {
|
|
210
219
|
if (!keep.has(name)) delete env[name];
|
|
211
220
|
}
|
|
@@ -223,9 +232,9 @@ function toEnv(stored) {
|
|
|
223
232
|
}
|
|
224
233
|
return env;
|
|
225
234
|
}
|
|
226
|
-
function childEnv(
|
|
235
|
+
function childEnv(base2, fetched, override) {
|
|
227
236
|
const env = {};
|
|
228
|
-
for (const [name, value] of Object.entries(
|
|
237
|
+
for (const [name, value] of Object.entries(base2)) {
|
|
229
238
|
if (value !== void 0) env[name] = value;
|
|
230
239
|
}
|
|
231
240
|
for (const [name, value] of Object.entries(fetched)) {
|
|
@@ -234,8 +243,8 @@ function childEnv(base, fetched, override) {
|
|
|
234
243
|
}
|
|
235
244
|
return env;
|
|
236
245
|
}
|
|
237
|
-
function exitCodeFor(
|
|
238
|
-
if (
|
|
246
|
+
function exitCodeFor(code2, signal) {
|
|
247
|
+
if (code2 !== null) return code2;
|
|
239
248
|
if (signal !== null) {
|
|
240
249
|
const number = constants.signals[signal];
|
|
241
250
|
if (typeof number === "number") return 128 + number;
|
|
@@ -289,13 +298,13 @@ async function exec(ctx, opts) {
|
|
|
289
298
|
opts.override
|
|
290
299
|
);
|
|
291
300
|
const name = clean(program2);
|
|
292
|
-
const exitCode = await new Promise((
|
|
301
|
+
const exitCode = await new Promise((resolve3, reject) => {
|
|
293
302
|
let child;
|
|
294
303
|
try {
|
|
295
304
|
child = spawn(program2, args, { stdio: "inherit", env });
|
|
296
305
|
} catch (error) {
|
|
297
|
-
const
|
|
298
|
-
reject(new CommandError(`cannot start ${name}: ${
|
|
306
|
+
const code2 = error.code ?? "invalid arguments";
|
|
307
|
+
reject(new CommandError(`cannot start ${name}: ${code2}`));
|
|
299
308
|
return;
|
|
300
309
|
}
|
|
301
310
|
const handlers = [];
|
|
@@ -325,1192 +334,1834 @@ async function exec(ctx, opts) {
|
|
|
325
334
|
});
|
|
326
335
|
child.on("error", (error) => {
|
|
327
336
|
settle(() => {
|
|
328
|
-
const
|
|
329
|
-
reject(new CommandError(spawned ? `${name} failed: ${
|
|
337
|
+
const code2 = error.code ?? error.message;
|
|
338
|
+
reject(new CommandError(spawned ? `${name} failed: ${code2}` : `cannot run ${name}: ${code2}`));
|
|
330
339
|
});
|
|
331
340
|
});
|
|
332
|
-
child.on("exit", (
|
|
333
|
-
settle(() =>
|
|
341
|
+
child.on("exit", (code2, signal) => {
|
|
342
|
+
settle(() => resolve3(exitCodeFor(code2, signal)));
|
|
334
343
|
});
|
|
335
344
|
});
|
|
336
345
|
return { json: { exit_code: exitCode }, human: "", exitCode };
|
|
337
346
|
}
|
|
338
347
|
|
|
339
|
-
// src/
|
|
340
|
-
import {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
import { createServer } from "http";
|
|
344
|
-
import { delimiter, join } from "path";
|
|
345
|
-
import { createInterface } from "readline";
|
|
346
|
-
var DEFAULT_TIMEOUT_MS = 18e4;
|
|
347
|
-
var RESPONSE_DRAIN_GRACE_MS = 100;
|
|
348
|
-
var RESPONSE_FLUSH_TIMEOUT_MS = 2e3;
|
|
349
|
-
var MAX_PAGE_RESPONSES = 4;
|
|
350
|
-
var loginPageImport;
|
|
351
|
-
function loadLoginPage() {
|
|
352
|
-
loginPageImport ??= import("./login-page-KGEJFFXR.js");
|
|
353
|
-
return loginPageImport;
|
|
354
|
-
}
|
|
355
|
-
var NO_SESSION = { kind: null, expires_at: null, scopes: null, user: null, org: null };
|
|
356
|
-
function generateState() {
|
|
357
|
-
return randomBytes(24).toString("base64url");
|
|
358
|
-
}
|
|
359
|
-
function generateVerifier() {
|
|
360
|
-
return randomBytes(32).toString("base64url");
|
|
361
|
-
}
|
|
362
|
-
function challengeFor(verifier) {
|
|
363
|
-
return createHash("sha256").update(verifier).digest("base64url");
|
|
364
|
-
}
|
|
365
|
-
async function login(baseUrl, options = {}) {
|
|
366
|
-
const validated = validateBaseUrl(baseUrl);
|
|
367
|
-
if (!validated.ok) return { ok: false, message: validated.message };
|
|
368
|
-
const base = baseUrl.replace(/\/+$/, "");
|
|
369
|
-
if (options.noBrowser === true || !browserAvailable(options)) {
|
|
370
|
-
return manualLogin(base, options);
|
|
371
|
-
}
|
|
372
|
-
return loopbackLogin(base, options);
|
|
373
|
-
}
|
|
374
|
-
function browserAvailable(options) {
|
|
375
|
-
if ("openBrowser" in options) return typeof options.openBrowser === "function";
|
|
376
|
-
return browserOpener() !== null && displayAvailable(options.env ?? process.env);
|
|
377
|
-
}
|
|
378
|
-
function findExecutable(name, pathVar = process.env["PATH"]) {
|
|
379
|
-
for (const dir of (pathVar ?? "").split(delimiter)) {
|
|
380
|
-
if (dir === "") continue;
|
|
381
|
-
const candidate = join(dir, name);
|
|
382
|
-
try {
|
|
383
|
-
accessSync(candidate, fsConstants.X_OK);
|
|
384
|
-
return candidate;
|
|
385
|
-
} catch {
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return null;
|
|
389
|
-
}
|
|
390
|
-
function browserOpener() {
|
|
391
|
-
const name = process.platform === "darwin" ? "open" : process.platform === "linux" ? "xdg-open" : null;
|
|
392
|
-
if (name === null) return null;
|
|
393
|
-
return findExecutable(name);
|
|
394
|
-
}
|
|
395
|
-
function displayAvailable(env) {
|
|
396
|
-
const display = env["DISPLAY"];
|
|
397
|
-
const remote = isSet(env["SSH_CONNECTION"]) || isSet(env["SSH_TTY"]);
|
|
398
|
-
const hasDisplay = isSet(display) || isSet(env["WAYLAND_DISPLAY"]);
|
|
399
|
-
const forwarded = typeof display === "string" && display.startsWith("localhost:");
|
|
400
|
-
if (process.platform === "darwin") return !remote;
|
|
401
|
-
if (process.platform === "linux") return hasDisplay && (!remote || forwarded);
|
|
402
|
-
return !remote;
|
|
403
|
-
}
|
|
404
|
-
function isSet(value) {
|
|
405
|
-
return value !== void 0 && value !== "";
|
|
406
|
-
}
|
|
407
|
-
async function loopbackLogin(baseUrl, options) {
|
|
408
|
-
const notify = options.notify ?? defaultNotify;
|
|
409
|
-
const state2 = generateState();
|
|
410
|
-
const verifier = generateVerifier();
|
|
411
|
-
const challenge = challengeFor(verifier);
|
|
412
|
-
const server = createServer();
|
|
413
|
-
const port = await new Promise((resolve) => {
|
|
414
|
-
server.once("error", () => resolve(null));
|
|
415
|
-
server.listen(0, "127.0.0.1", () => {
|
|
416
|
-
const address = server.address();
|
|
417
|
-
resolve(typeof address === "object" && address !== null ? address.port : null);
|
|
418
|
-
});
|
|
419
|
-
});
|
|
420
|
-
if (port === null) {
|
|
421
|
-
return {
|
|
422
|
-
ok: false,
|
|
423
|
-
message: "could not open a loopback port for the browser handoff"
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
try {
|
|
427
|
-
const query = new URLSearchParams({ port: String(port), state: state2, challenge });
|
|
428
|
-
const url = `${baseUrl}/cli/login?${query.toString()}`;
|
|
429
|
-
const callbackPromise = awaitCallback(server, state2, options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
430
|
-
void loadLoginPage().catch(() => {
|
|
431
|
-
loginPageImport = void 0;
|
|
432
|
-
});
|
|
433
|
-
const openBrowser = options.openBrowser ?? defaultOpenBrowser;
|
|
434
|
-
try {
|
|
435
|
-
openBrowser(url);
|
|
436
|
-
} catch (error) {
|
|
437
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
438
|
-
return { ok: false, message: `could not open a browser: ${detail}` };
|
|
439
|
-
}
|
|
440
|
-
notify(`Opening ${url}`);
|
|
441
|
-
notify("Waiting for the browser authorization (Ctrl-C to abort)\u2026");
|
|
442
|
-
const callback = await callbackPromise;
|
|
443
|
-
if (!callback.ok) return callback;
|
|
444
|
-
try {
|
|
445
|
-
const token = await exchangeCode(baseUrl, callback.code, verifier, options);
|
|
446
|
-
if (!token.ok) {
|
|
447
|
-
await callback.respond(
|
|
448
|
-
400,
|
|
449
|
-
"Harness could not exchange the browser authorization. Return to the terminal for details.",
|
|
450
|
-
"error"
|
|
451
|
-
);
|
|
452
|
-
return token;
|
|
453
|
-
}
|
|
454
|
-
const result = await verifyAndStore(baseUrl, token.token, options, { minted: true, kind: token.kind });
|
|
455
|
-
if (result.ok) {
|
|
456
|
-
await callback.respond(200, "You can close this browser and return to the terminal.", "success");
|
|
457
|
-
} else {
|
|
458
|
-
await callback.respond(
|
|
459
|
-
400,
|
|
460
|
-
"Harness could not verify and store this login. Return to the terminal for details.",
|
|
461
|
-
"error"
|
|
462
|
-
);
|
|
463
|
-
}
|
|
464
|
-
return result;
|
|
465
|
-
} catch (error) {
|
|
466
|
-
await callback.respond(500, "Harness could not finish the login. Return to the terminal for details.", "error");
|
|
467
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
468
|
-
return { ok: false, message: `could not finish the browser login: ${detail}` };
|
|
469
|
-
}
|
|
470
|
-
} finally {
|
|
471
|
-
server.close();
|
|
472
|
-
const forceClose = setTimeout(
|
|
473
|
-
() => server.closeAllConnections(),
|
|
474
|
-
RESPONSE_FLUSH_TIMEOUT_MS + RESPONSE_DRAIN_GRACE_MS
|
|
475
|
-
);
|
|
476
|
-
forceClose.unref();
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
function awaitCallback(server, state2, timeoutMs) {
|
|
480
|
-
return new Promise((resolve) => {
|
|
481
|
-
let phase = "waiting";
|
|
482
|
-
let finalResponse = null;
|
|
483
|
-
let callbackPageResponses = 0;
|
|
484
|
-
const rootPageResponses = /* @__PURE__ */ new Map();
|
|
485
|
-
const acceptedResponders = [];
|
|
486
|
-
const inFlightResponses = /* @__PURE__ */ new Set();
|
|
487
|
-
const trackResponse = (responsePromise) => {
|
|
488
|
-
inFlightResponses.add(responsePromise);
|
|
489
|
-
void responsePromise.finally(() => inFlightResponses.delete(responsePromise));
|
|
490
|
-
return responsePromise;
|
|
491
|
-
};
|
|
492
|
-
const drainTrackedResponses = async () => {
|
|
493
|
-
await new Promise((settle) => setTimeout(settle, RESPONSE_DRAIN_GRACE_MS));
|
|
494
|
-
const deadline = Date.now() + RESPONSE_FLUSH_TIMEOUT_MS;
|
|
495
|
-
while (inFlightResponses.size > 0 && Date.now() < deadline) {
|
|
496
|
-
const remaining = deadline - Date.now();
|
|
497
|
-
await Promise.race([
|
|
498
|
-
Promise.allSettled([...inFlightResponses]),
|
|
499
|
-
new Promise((settle) => setTimeout(settle, remaining))
|
|
500
|
-
]);
|
|
501
|
-
}
|
|
502
|
-
};
|
|
503
|
-
const timer = setTimeout(() => {
|
|
504
|
-
if (phase !== "waiting") return;
|
|
505
|
-
const timeoutResponse = {
|
|
506
|
-
status: 410,
|
|
507
|
-
message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
|
|
508
|
-
pageState: "error"
|
|
509
|
-
};
|
|
510
|
-
phase = "terminal";
|
|
511
|
-
finalResponse = timeoutResponse;
|
|
512
|
-
resolve({
|
|
513
|
-
ok: false,
|
|
514
|
-
message: "timed out waiting for the browser authorization"
|
|
515
|
-
});
|
|
516
|
-
void drainTrackedResponses();
|
|
517
|
-
}, timeoutMs);
|
|
518
|
-
server.once("close", () => {
|
|
519
|
-
clearTimeout(timer);
|
|
520
|
-
if (phase !== "waiting") return;
|
|
521
|
-
phase = "terminal";
|
|
522
|
-
resolve({ ok: false, message: "the browser login listener closed before authorization completed" });
|
|
523
|
-
});
|
|
524
|
-
server.on("request", (request, response) => {
|
|
525
|
-
const respond = callbackResponder(response);
|
|
526
|
-
const respondWithPage = (status, message, pageState) => {
|
|
527
|
-
if (callbackPageResponses >= MAX_PAGE_RESPONSES) {
|
|
528
|
-
respondEmpty(response, 429);
|
|
529
|
-
return Promise.resolve();
|
|
530
|
-
}
|
|
531
|
-
callbackPageResponses += 1;
|
|
532
|
-
return respond(status, message, pageState);
|
|
533
|
-
};
|
|
534
|
-
const respondAtRoot = (status, message, pageState) => {
|
|
535
|
-
const rootPhase = phase === "terminal" ? "terminal" : "waiting";
|
|
536
|
-
const responses = rootPageResponses.get(rootPhase) ?? 0;
|
|
537
|
-
if (responses >= MAX_PAGE_RESPONSES) {
|
|
538
|
-
respondEmpty(response, 429);
|
|
539
|
-
return Promise.resolve();
|
|
540
|
-
}
|
|
541
|
-
rootPageResponses.set(rootPhase, responses + 1);
|
|
542
|
-
return respond(status, message, pageState);
|
|
543
|
-
};
|
|
544
|
-
let url;
|
|
545
|
-
try {
|
|
546
|
-
url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
547
|
-
} catch {
|
|
548
|
-
respondEmpty(response, 404);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
const path = url.pathname.replace(/\/+$/, "") || "/";
|
|
552
|
-
if (path !== "/callback") {
|
|
553
|
-
if (path === "/") {
|
|
554
|
-
const rootResponse = phase === "terminal" && finalResponse ? finalResponse : {
|
|
555
|
-
status: 200,
|
|
556
|
-
message: "Waiting for the Harness browser authorization to finish.",
|
|
557
|
-
pageState: "waiting"
|
|
558
|
-
};
|
|
559
|
-
void trackResponse(
|
|
560
|
-
respondAtRoot(rootResponse.status, rootResponse.message, rootResponse.pageState)
|
|
561
|
-
);
|
|
562
|
-
} else {
|
|
563
|
-
respondEmpty(response, path === "/favicon.ico" ? 204 : 404);
|
|
564
|
-
}
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
const matchingState = url.searchParams.get("state") === state2;
|
|
568
|
-
if (!matchingState) {
|
|
569
|
-
respondEmpty(response, 400);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
if (phase === "terminal") {
|
|
573
|
-
if (!url.searchParams.has("code") && !url.searchParams.has("error")) {
|
|
574
|
-
respondEmpty(response, 400);
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
const result = finalResponse ?? {
|
|
578
|
-
status: 410,
|
|
579
|
-
message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
|
|
580
|
-
pageState: "error"
|
|
581
|
-
};
|
|
582
|
-
void trackResponse(respondWithPage(result.status, result.message, result.pageState));
|
|
583
|
-
return;
|
|
584
|
-
}
|
|
585
|
-
if (phase === "processing") {
|
|
586
|
-
if (url.searchParams.has("error")) {
|
|
587
|
-
respondEmpty(response, 409);
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
const duplicateCode = url.searchParams.get("code");
|
|
591
|
-
if (duplicateCode === null || duplicateCode === "") {
|
|
592
|
-
respondEmpty(response, 400);
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
if (acceptedResponders.length >= MAX_PAGE_RESPONSES) {
|
|
596
|
-
respondEmpty(response, 429);
|
|
597
|
-
return;
|
|
598
|
-
}
|
|
599
|
-
acceptedResponders.push(respondWithPage);
|
|
600
|
-
return;
|
|
601
|
-
}
|
|
602
|
-
if (url.searchParams.has("error")) {
|
|
603
|
-
const denialResponse = {
|
|
604
|
-
status: 400,
|
|
605
|
-
message: "Browser authorization was denied. Return to the terminal to try again.",
|
|
606
|
-
pageState: "error"
|
|
607
|
-
};
|
|
608
|
-
phase = "terminal";
|
|
609
|
-
finalResponse = denialResponse;
|
|
610
|
-
clearTimeout(timer);
|
|
611
|
-
void (async () => {
|
|
612
|
-
await trackResponse(respondWithPage(denialResponse.status, denialResponse.message, denialResponse.pageState));
|
|
613
|
-
await drainTrackedResponses();
|
|
614
|
-
resolve({ ok: false, message: "browser authorization was denied" });
|
|
615
|
-
})();
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
const code = url.searchParams.get("code");
|
|
619
|
-
if (code !== null && code !== "") {
|
|
620
|
-
phase = "processing";
|
|
621
|
-
acceptedResponders.push(respondWithPage);
|
|
622
|
-
clearTimeout(timer);
|
|
623
|
-
resolve({
|
|
624
|
-
ok: true,
|
|
625
|
-
code,
|
|
626
|
-
respond: async (status, message, pageState) => {
|
|
627
|
-
finalResponse = { status, message, pageState };
|
|
628
|
-
phase = "terminal";
|
|
629
|
-
const responders = acceptedResponders.splice(0);
|
|
630
|
-
await Promise.all(
|
|
631
|
-
responders.map((acceptedRespond) => trackResponse(acceptedRespond(status, message, pageState)))
|
|
632
|
-
);
|
|
633
|
-
await drainTrackedResponses();
|
|
634
|
-
}
|
|
635
|
-
});
|
|
636
|
-
} else {
|
|
637
|
-
respondEmpty(response, 400);
|
|
638
|
-
}
|
|
639
|
-
});
|
|
640
|
-
});
|
|
348
|
+
// src/commands/review.ts
|
|
349
|
+
import { execFileSync } from "child_process";
|
|
350
|
+
function isRecord2(value) {
|
|
351
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
641
352
|
}
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
let flushTimer;
|
|
647
|
-
const armFlushTimeout = () => {
|
|
648
|
-
if (flushTimer !== void 0) clearTimeout(flushTimer);
|
|
649
|
-
flushTimer = setTimeout(() => {
|
|
650
|
-
response.destroy();
|
|
651
|
-
settle();
|
|
652
|
-
}, RESPONSE_FLUSH_TIMEOUT_MS);
|
|
653
|
-
flushTimer.unref();
|
|
654
|
-
};
|
|
655
|
-
const settle = () => {
|
|
656
|
-
if (flushTimer !== void 0) clearTimeout(flushTimer);
|
|
657
|
-
response.off("finish", settle);
|
|
658
|
-
response.off("close", settle);
|
|
659
|
-
response.off("error", settle);
|
|
660
|
-
response.off("drain", armFlushTimeout);
|
|
661
|
-
resolve();
|
|
662
|
-
};
|
|
663
|
-
response.once("finish", settle);
|
|
664
|
-
response.once("close", settle);
|
|
665
|
-
response.once("error", settle);
|
|
666
|
-
response.on("drain", armFlushTimeout);
|
|
667
|
-
void loadLoginPage().then(({ loginPage }) => {
|
|
668
|
-
if (response.writableEnded || response.destroyed) {
|
|
669
|
-
settle();
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
try {
|
|
673
|
-
const html = loginPage(message, state2);
|
|
674
|
-
response.writeHead(status, {
|
|
675
|
-
"cache-control": "no-store",
|
|
676
|
-
connection: "close",
|
|
677
|
-
"content-length": String(Buffer.byteLength(html)),
|
|
678
|
-
"content-type": "text/html; charset=utf-8",
|
|
679
|
-
"x-content-type-options": "nosniff"
|
|
680
|
-
});
|
|
681
|
-
armFlushTimeout();
|
|
682
|
-
response.end(html);
|
|
683
|
-
} catch {
|
|
684
|
-
response.destroy();
|
|
685
|
-
settle();
|
|
686
|
-
}
|
|
687
|
-
}).catch(() => {
|
|
688
|
-
response.destroy();
|
|
689
|
-
settle();
|
|
690
|
-
});
|
|
691
|
-
});
|
|
692
|
-
};
|
|
353
|
+
var SLUG = /^([\w.-]+)\/([\w.-]+)$/;
|
|
354
|
+
function githubSlug(remote) {
|
|
355
|
+
const match = /^(?:(?:https?|ssh|git):\/\/(?:[^@/\s]+@)?|[\w.-]+@)github\.com[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/i.exec(remote.trim());
|
|
356
|
+
return match ? `${match[1]}/${match[2]}` : void 0;
|
|
693
357
|
}
|
|
694
|
-
function
|
|
695
|
-
response.once("error", () => void 0);
|
|
358
|
+
function originRemote() {
|
|
696
359
|
try {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
connection: "close",
|
|
700
|
-
"x-content-type-options": "nosniff"
|
|
701
|
-
});
|
|
702
|
-
response.end();
|
|
360
|
+
const url = execFileSync("git", ["remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
361
|
+
return githubSlug(url);
|
|
703
362
|
} catch {
|
|
704
|
-
|
|
363
|
+
return void 0;
|
|
705
364
|
}
|
|
706
365
|
}
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
const kind = typeof body.data?.kind === "string" ? body.data.kind : null;
|
|
712
|
-
if (typeof token === "string" && token !== "") return { ok: true, token, kind };
|
|
713
|
-
return {
|
|
714
|
-
ok: false,
|
|
715
|
-
message: "the login-code exchange returned no token \u2014 aborting"
|
|
716
|
-
};
|
|
717
|
-
} catch (error) {
|
|
718
|
-
if (error instanceof HttpError) {
|
|
719
|
-
return {
|
|
720
|
-
ok: false,
|
|
721
|
-
message: `the login-code exchange failed: ${error.message}`
|
|
722
|
-
};
|
|
723
|
-
}
|
|
724
|
-
if (error instanceof TransportError) {
|
|
725
|
-
return {
|
|
726
|
-
ok: false,
|
|
727
|
-
message: `the login-code exchange failed: cannot reach the API (${error.message})`
|
|
728
|
-
};
|
|
729
|
-
}
|
|
730
|
-
throw error;
|
|
731
|
-
}
|
|
366
|
+
function prNumber(digits, input) {
|
|
367
|
+
const number = Number(digits);
|
|
368
|
+
if (!Number.isSafeInteger(number) || number < 1) throw new UsageError(`not a pull request number: ${input}`);
|
|
369
|
+
return number;
|
|
732
370
|
}
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
notify(
|
|
737
|
-
`Open ${baseUrl}/cli/login in a browser, authorize, and paste the token below (an outside expert pastes the pxc_ credential minted in their expert view instead).`
|
|
738
|
-
);
|
|
739
|
-
const token = (await prompt()).trim();
|
|
740
|
-
if (token === "") return { ok: false, message: "no token entered" };
|
|
741
|
-
return verifyAndStore(baseUrl, token, options, { minted: false, kind: null });
|
|
742
|
-
}
|
|
743
|
-
async function verifyAndStore(baseUrl, token, options, received) {
|
|
744
|
-
const path = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
745
|
-
const client = bareClient(baseUrl, token, options);
|
|
746
|
-
const expert = isExpertToken(token);
|
|
747
|
-
let kind = received.kind;
|
|
371
|
+
var NAME = /^[\w.-]+$/;
|
|
372
|
+
function fromUrl(input) {
|
|
373
|
+
let url;
|
|
748
374
|
try {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
const described = isLoginCredential(token) ? await describeSession(client) : null;
|
|
753
|
-
if (described === null) await client.get("/api/v1/teams");
|
|
754
|
-
else kind = described.session.kind ?? kind;
|
|
755
|
-
}
|
|
756
|
-
} catch (error) {
|
|
757
|
-
if (error instanceof HttpError) {
|
|
758
|
-
const lapsed = expert && error.status === 401 ? ` \u2014 ${EXPERT_LAPSED}` : "";
|
|
759
|
-
const revoked = received.minted && await revokeSession(client, token);
|
|
760
|
-
return {
|
|
761
|
-
ok: false,
|
|
762
|
-
message: `the received token failed verification (${error.message}, HTTP ${error.status}) \u2014 nothing was stored${revoked ? ", and the token was revoked" : ""}${lapsed}`
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
if (error instanceof TransportError) {
|
|
766
|
-
return {
|
|
767
|
-
ok: false,
|
|
768
|
-
message: `could not verify the token: cannot reach the API (${error.message}) \u2014 nothing was stored`
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
throw error;
|
|
375
|
+
url = new URL(input);
|
|
376
|
+
} catch {
|
|
377
|
+
return null;
|
|
772
378
|
}
|
|
773
|
-
|
|
774
|
-
|
|
379
|
+
if (!["https:", "http:"].includes(url.protocol) || !/^(www\.)?github\.com$/i.test(url.hostname)) return null;
|
|
380
|
+
let segments;
|
|
775
381
|
try {
|
|
776
|
-
|
|
777
|
-
} catch
|
|
778
|
-
|
|
779
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
780
|
-
return {
|
|
781
|
-
ok: false,
|
|
782
|
-
message: `could not store credentials at ${path}: ${detail}` + (expert ? " (the pxc_ credential is still valid: revoke it in your expert view)" : received.minted ? "" : " (the pasted token was not revoked and stays valid)")
|
|
783
|
-
};
|
|
382
|
+
segments = url.pathname.split("/").filter((segment) => segment !== "").map(decodeURIComponent);
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
784
385
|
}
|
|
785
|
-
|
|
786
|
-
if (
|
|
787
|
-
|
|
788
|
-
if (isExpertToken(superseded.token)) {
|
|
789
|
-
supersededNote = "The pxc_ credential stored before stays valid until it expires: revoke it in your expert view.\n";
|
|
790
|
-
}
|
|
386
|
+
const [owner, repo, kind, number] = segments;
|
|
387
|
+
if (kind !== "pull" || !owner || !repo || !number || !NAME.test(owner) || !NAME.test(repo) || !/^\d+$/.test(number)) {
|
|
388
|
+
return null;
|
|
791
389
|
}
|
|
792
|
-
|
|
390
|
+
return [owner, repo, number];
|
|
391
|
+
}
|
|
392
|
+
function parsePullRequest(input, repo, origin = originRemote) {
|
|
393
|
+
const trimmed = input.trim();
|
|
394
|
+
const url = fromUrl(trimmed);
|
|
395
|
+
if (url) return { owner: url[0], repo: url[1], number: prNumber(url[2], input) };
|
|
396
|
+
const named2 = /^([\w.-]+)\/([\w.-]+)#(\d+)$/.exec(trimmed);
|
|
397
|
+
if (named2) return { owner: named2[1], repo: named2[2], number: prNumber(named2[3], input) };
|
|
398
|
+
const bare = /^#?(\d+)$/.exec(trimmed);
|
|
399
|
+
if (!bare) throw new UsageError(`not a pull request: ${input} \u2014 use owner/repo#<pr>, its URL, or <pr> with --repo`);
|
|
400
|
+
const slug = repo ?? origin();
|
|
401
|
+
const parts = slug === void 0 ? null : SLUG.exec(slug.trim());
|
|
402
|
+
if (!parts) throw new UsageError("name the repository: owner/repo#<pr>, the pull request URL, or --repo owner/repo");
|
|
403
|
+
return { owner: parts[1], repo: parts[2], number: prNumber(bare[1], input) };
|
|
404
|
+
}
|
|
405
|
+
var label = (pr) => `${pr.owner}/${pr.repo}#${pr.number}`;
|
|
406
|
+
var gatePath = (pr) => `/api/v1/reviews/prs/${encodeURIComponent(pr.owner)}/${encodeURIComponent(pr.repo)}/${pr.number}`;
|
|
407
|
+
async function fetchGate(ctx, pr) {
|
|
408
|
+
const body = await ctx.client.get(gatePath(pr));
|
|
409
|
+
if (!isRecord2(body.data)) throw new CommandError(`unexpected gate status for ${label(pr)}: no data`);
|
|
410
|
+
return body.data;
|
|
411
|
+
}
|
|
412
|
+
function latestAttempt(lifecycle) {
|
|
413
|
+
const attempts = Array.isArray(lifecycle["attempts"]) ? lifecycle["attempts"].filter(isRecord2) : [];
|
|
414
|
+
return attempts.reduce(
|
|
415
|
+
(latest, attempt) => latest === void 0 || Number(attempt["number"]) > Number(latest["number"]) ? attempt : latest,
|
|
416
|
+
void 0
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
function recovery(lifecycle) {
|
|
420
|
+
const latest = latestAttempt(lifecycle);
|
|
421
|
+
const sent = isRecord2(lifecycle["recovery"]) ? lifecycle["recovery"] : void 0;
|
|
422
|
+
const state2 = String(sent?.["state"] ?? latest?.["state"] ?? "");
|
|
423
|
+
if (state2 !== "failed" && state2 !== "timed_out") return void 0;
|
|
424
|
+
const reason = sent?.["terminal_reason"] ?? latest?.["terminal_reason"];
|
|
793
425
|
return {
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
426
|
+
number: latest?.["number"],
|
|
427
|
+
state: state2 === "timed_out" ? "timed out" : state2,
|
|
428
|
+
reason: typeof reason === "string" ? reason : null,
|
|
429
|
+
evidence: sent ? sent["evidence_recorded"] === true : latest?.["review_run_id"] != null,
|
|
430
|
+
next: sent?.["next"] === "automatic_retry" ? "automatic_retry" : "retry"
|
|
799
431
|
};
|
|
800
432
|
}
|
|
801
|
-
function
|
|
802
|
-
|
|
803
|
-
|
|
433
|
+
function describe(r) {
|
|
434
|
+
const why = [r.reason, r.evidence ? null : "no evidence recorded"].filter((part) => part !== null).join(", ");
|
|
435
|
+
return `attempt ${clean(String(r.number))} ${r.state}${why === "" ? "" : ` (${clean(why)})`}`;
|
|
436
|
+
}
|
|
437
|
+
function projection(gate2, key) {
|
|
438
|
+
const value = gate2[key];
|
|
439
|
+
return isRecord2(value) && typeof value["status"] === "string" ? clean(value["status"]) : "unknown";
|
|
440
|
+
}
|
|
441
|
+
async function status(ctx, pr) {
|
|
442
|
+
const gate2 = await fetchGate(ctx, pr);
|
|
443
|
+
const lifecycle = isRecord2(gate2["lifecycle"]) ? gate2["lifecycle"] : void 0;
|
|
444
|
+
const head = isRecord2(gate2["head"]) && typeof gate2["head"]["sha"] === "string" ? gate2["head"]["sha"].slice(0, 10) : "?";
|
|
445
|
+
const lines = [
|
|
446
|
+
`${label(pr)} at ${clean(head)}`,
|
|
447
|
+
`enforced: ${projection(gate2, "enforced")} advisory: ${projection(gate2, "advisory")}`
|
|
448
|
+
];
|
|
449
|
+
if (lifecycle) {
|
|
450
|
+
lines.push(`gate check: ${clean(String(lifecycle["state"] ?? "unknown")).replaceAll("_", " ")}`);
|
|
451
|
+
const r = recovery(lifecycle);
|
|
452
|
+
if (r) {
|
|
453
|
+
lines.push(`latest ${describe(r)}`);
|
|
454
|
+
lines.push(
|
|
455
|
+
r.next === "automatic_retry" ? "Harness retries it automatically; check again shortly." : named(`Retry it with: harness review retry ${label(pr)} --reason "\u2026"`)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
804
458
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
459
|
+
return { json: gate2, human: lines.join("\n") + "\n" };
|
|
460
|
+
}
|
|
461
|
+
var REFUSALS = {
|
|
462
|
+
execution_not_terminal: "the latest attempt is still running; wait for it to finish",
|
|
463
|
+
attempt_changed: "another attempt was created meanwhile; read the status and run this again",
|
|
464
|
+
check_changed: "the head's gate check changed meanwhile; run this again",
|
|
465
|
+
head_changed: "the pull request's head moved; the new head gets its own check",
|
|
466
|
+
advisory_not_converged: "the head's advisory review has not converged; converge it first",
|
|
467
|
+
retry_conflict: "this attempt was already retried with a different reason",
|
|
468
|
+
merged: "the pull request is merged"
|
|
469
|
+
};
|
|
470
|
+
async function retry(ctx, pr, reason) {
|
|
471
|
+
const gate2 = await fetchGate(ctx, pr);
|
|
472
|
+
const lifecycle = isRecord2(gate2["lifecycle"]) ? gate2["lifecycle"] : void 0;
|
|
473
|
+
if (!lifecycle || typeof lifecycle["check_id"] !== "string" || typeof lifecycle["head_sha"] !== "string") {
|
|
474
|
+
throw new CommandError(`${label(pr)} has no gate check at its current head: nothing to retry`);
|
|
475
|
+
}
|
|
476
|
+
const r = recovery(lifecycle);
|
|
477
|
+
if (r?.next === "automatic_retry") {
|
|
478
|
+
throw new CommandError(named(`Harness retries attempt ${clean(String(r.number))} itself; run harness review status ${label(pr)} shortly`));
|
|
479
|
+
}
|
|
480
|
+
const previous = latestAttempt(lifecycle);
|
|
481
|
+
const previousId = typeof previous?.["id"] === "string" ? previous["id"] : null;
|
|
482
|
+
const body = {
|
|
483
|
+
head_sha: lifecycle["head_sha"],
|
|
484
|
+
check_id: lifecycle["check_id"],
|
|
485
|
+
previous_attempt_id: previousId,
|
|
486
|
+
// One key per attempt retried: running this again for the same attempt returns the attempt already
|
|
487
|
+
// queued instead of paying for another review run.
|
|
488
|
+
request_key: `harness-cli:${previousId ?? lifecycle["check_id"]}`,
|
|
489
|
+
reason: reason?.trim() || "Retry requested with harness review retry"
|
|
490
|
+
};
|
|
491
|
+
let response;
|
|
809
492
|
try {
|
|
810
|
-
|
|
811
|
-
return { session: sessionOf(envelope.data), apiVersion };
|
|
493
|
+
response = await ctx.client.post(`${gatePath(pr)}/retry`, body, { actor: false });
|
|
812
494
|
} catch (error) {
|
|
813
|
-
if (error instanceof HttpError &&
|
|
495
|
+
if (error instanceof HttpError && error.status === 409) {
|
|
496
|
+
const code2 = error.message.trim().split(/[\s:]+/).pop() ?? "";
|
|
497
|
+
throw new CommandError(`could not retry ${label(pr)}: ${REFUSALS[code2] ?? clean(error.message)}`);
|
|
498
|
+
}
|
|
499
|
+
if (error instanceof HttpError && error.status === 403) {
|
|
500
|
+
throw new CommandError(`${error.message} \u2014 a retry needs a login with reviews:write`);
|
|
501
|
+
}
|
|
814
502
|
throw error;
|
|
815
503
|
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
const text = (value) => typeof value === "string" ? value : null;
|
|
504
|
+
const attempt = isRecord2(response.data) ? response.data : {};
|
|
505
|
+
const outcome = response["outcome"];
|
|
506
|
+
const verb = outcome === "existing" ? "Already queued" : "Queued";
|
|
820
507
|
return {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
user: isRecord2(data["user"]) ? data["user"] : null,
|
|
825
|
-
org: isRecord2(data["org"]) ? data["org"] : null
|
|
508
|
+
json: { attempt, outcome },
|
|
509
|
+
human: `${verb} attempt ${clean(String(attempt["number"]))} (${clean(String(attempt["id"]))}) for ${label(pr)}
|
|
510
|
+
`
|
|
826
511
|
};
|
|
827
512
|
}
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
513
|
+
|
|
514
|
+
// src/commands/code/index.ts
|
|
515
|
+
import { resolve as resolve2 } from "path";
|
|
516
|
+
import { statSync } from "fs";
|
|
517
|
+
|
|
518
|
+
// src/core/code/http.ts
|
|
519
|
+
import { setTimeout as delay } from "timers/promises";
|
|
520
|
+
import { z } from "zod";
|
|
521
|
+
var UUID = z.string().uuid();
|
|
522
|
+
var HASH = z.string().regex(/^[a-f0-9]{64}$/);
|
|
523
|
+
var view = z.object({
|
|
524
|
+
view_id: UUID,
|
|
525
|
+
view_key: z.string(),
|
|
526
|
+
repository_id: UUID,
|
|
527
|
+
active: z.object({
|
|
528
|
+
version: z.number().int(),
|
|
529
|
+
base_commit: z.string(),
|
|
530
|
+
overlay_digest: HASH,
|
|
531
|
+
indexed_at: z.string().nullable().optional()
|
|
532
|
+
}).nullable(),
|
|
533
|
+
pending: z.object({
|
|
534
|
+
version: z.number().int(),
|
|
535
|
+
state: z.string(),
|
|
536
|
+
progress: z.object({ done: z.number().int().nonnegative(), total: z.number().int().nonnegative() }).nullable().optional(),
|
|
537
|
+
error: z.unknown().optional()
|
|
538
|
+
}).nullable()
|
|
539
|
+
});
|
|
540
|
+
var policy = z.object({
|
|
541
|
+
org_id: UUID,
|
|
542
|
+
policy_digest: z.string().min(1).max(256),
|
|
543
|
+
max_file_bytes: z.number().int().positive(),
|
|
544
|
+
max_batch_bytes: z.number().int().positive(),
|
|
545
|
+
include_extensions: z.array(z.string().max(32)).max(100),
|
|
546
|
+
deny_globs: z.array(z.string().max(1024)).max(1e3),
|
|
547
|
+
deny_glob_case: z.literal("ascii_insensitive").optional(),
|
|
548
|
+
content_patterns: z.array(z.string().max(4096)).max(100),
|
|
549
|
+
org_name: z.string().optional()
|
|
550
|
+
});
|
|
551
|
+
var base = z.object({
|
|
552
|
+
state: z.enum(["fetching", "complete", "ready", "failed"]),
|
|
553
|
+
policy_digest: z.string().optional(),
|
|
554
|
+
file_count: z.number().optional(),
|
|
555
|
+
excluded_count: z.number().optional(),
|
|
556
|
+
skipped: z.array(z.object({ path: z.string(), reason: z.string() })).optional(),
|
|
557
|
+
error: z.unknown().optional(),
|
|
558
|
+
retry_after_ms: z.number().nonnegative().optional()
|
|
559
|
+
});
|
|
560
|
+
var opened = z.object({
|
|
561
|
+
sync_id: UUID,
|
|
562
|
+
state: z.string(),
|
|
563
|
+
missing: z.array(HASH),
|
|
564
|
+
rejected: z.array(z.object({ path: z.string(), error: z.string() }))
|
|
565
|
+
});
|
|
566
|
+
var activated = z.object({
|
|
567
|
+
pending_version: z.number().int().positive(),
|
|
568
|
+
state: z.string()
|
|
569
|
+
});
|
|
570
|
+
var syncStatus = z.object({
|
|
571
|
+
id: UUID,
|
|
572
|
+
state: z.enum(["receiving", "queued", "indexing", "active", "superseded", "failed"]),
|
|
573
|
+
version: z.number().int().nonnegative().nullable()
|
|
574
|
+
});
|
|
575
|
+
function decode(schema, data) {
|
|
576
|
+
const result = schema.safeParse(data);
|
|
577
|
+
if (!result.success)
|
|
578
|
+
throw new CommandError(
|
|
579
|
+
"The code index API returned an invalid response; no additional source will be uploaded."
|
|
580
|
+
);
|
|
581
|
+
return result.data;
|
|
582
|
+
}
|
|
583
|
+
function retryAfterMs(value, now = Date.now()) {
|
|
584
|
+
if (value === null) return void 0;
|
|
585
|
+
const seconds = Number(value);
|
|
586
|
+
if (/^\d+(?:\.\d+)?$/.test(value) && Number.isFinite(seconds))
|
|
587
|
+
return seconds * 1e3;
|
|
588
|
+
const date = Date.parse(value);
|
|
589
|
+
return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
|
|
590
|
+
}
|
|
591
|
+
function describeCodeError(error) {
|
|
592
|
+
const code2 = error.body["error"];
|
|
593
|
+
let message = error.message;
|
|
594
|
+
if (code2 === "repository_not_connected")
|
|
595
|
+
message = "repository_not_connected: share this repository through your organization\u2019s Harness GitHub connection before syncing.";
|
|
596
|
+
if (code2 === "code_index_disabled")
|
|
597
|
+
message = "code_index_disabled: code indexing is disabled for this organization; ask a Harness administrator to enable it.";
|
|
598
|
+
if (code2 === "quota_exceeded") {
|
|
599
|
+
const raw = error.body["details"];
|
|
600
|
+
const d = raw && typeof raw === "object" ? raw : {};
|
|
601
|
+
message = `quota_exceeded: your organization\u2019s code index limit ${typeof d["quota"] === "string" ? d["quota"] : "quota"} is reached (limit ${typeof d["limit"] === "number" ? d["limit"] : "unknown"}). Ask a Harness administrator.`;
|
|
602
|
+
}
|
|
603
|
+
if (code2 === "invalid_sync")
|
|
604
|
+
message = "invalid_sync: the client sync request does not match the server contract. Upgrade the client and retry code sync.";
|
|
605
|
+
if (code2 === "overlay_too_large") {
|
|
606
|
+
const raw = error.body["details"];
|
|
607
|
+
const details = raw && typeof raw === "object" ? raw : {};
|
|
608
|
+
const limit = typeof details["limit"] === "number" ? details["limit"] : 2e4;
|
|
609
|
+
message = `overlay_too_large: the overlay exceeds ${limit} paths. Sync against a newer base commit.`;
|
|
610
|
+
}
|
|
611
|
+
if (code2 === "unexpected_blob")
|
|
612
|
+
message = "unexpected_blob: no open sync names this upload. Retry code sync to reconcile the overlay; base blobs are never uploaded.";
|
|
613
|
+
if (code2 === "stale_policy")
|
|
614
|
+
message = "stale_policy: the upload policy kept changing; retry code sync to fetch the policy and rescan.";
|
|
615
|
+
if (code2 === "not_view_owner")
|
|
616
|
+
message = "not_view_owner: this code view belongs to another member; sync your own worktree.";
|
|
617
|
+
if (code2 === "insufficient_scope" || code2 === "missing_scope")
|
|
618
|
+
message = "Code commands need harness:read; sync and base status also need harness:write.";
|
|
619
|
+
return new HttpError(error.status, message, error.body, error.retryAfter);
|
|
620
|
+
}
|
|
621
|
+
async function responseText(response, maxBytes) {
|
|
622
|
+
if (maxBytes === void 0) return response.text();
|
|
623
|
+
if (!response.body) return "";
|
|
624
|
+
const reader = response.body.getReader();
|
|
625
|
+
const chunks = [];
|
|
626
|
+
let size = 0;
|
|
845
627
|
try {
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
628
|
+
while (true) {
|
|
629
|
+
const part = await reader.read();
|
|
630
|
+
if (part.done) break;
|
|
631
|
+
size += part.value.byteLength;
|
|
632
|
+
if (size > maxBytes) {
|
|
633
|
+
void reader.cancel().catch(() => {
|
|
634
|
+
});
|
|
635
|
+
throw new CommandError("The code query response exceeds the 16 MiB size limit; narrow the query.");
|
|
636
|
+
}
|
|
637
|
+
chunks.push(Buffer.from(part.value));
|
|
638
|
+
}
|
|
639
|
+
} finally {
|
|
640
|
+
reader.releaseLock();
|
|
853
641
|
}
|
|
854
|
-
return {
|
|
855
|
-
ok: true,
|
|
856
|
-
json: { logged_out: true, revoked },
|
|
857
|
-
human: expert ? `Forgot the outside expert's pxc_ credential for ${stored.url}. It stays valid until it expires: revoke it in your expert view to end it now.
|
|
858
|
-
` : `Logged out of ${stored.url}${note}.
|
|
859
|
-
`
|
|
860
|
-
};
|
|
861
|
-
}
|
|
862
|
-
async function whoami(config, credentialSource) {
|
|
863
|
-
const client = new Client(config);
|
|
864
|
-
const expert = isExpertToken(config.token);
|
|
865
|
-
let probe;
|
|
866
|
-
let session = null;
|
|
867
|
-
let serverVersion;
|
|
868
642
|
try {
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
643
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks));
|
|
644
|
+
} catch {
|
|
645
|
+
throw new CommandError("The code query response is not valid UTF-8.");
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
var HttpCodeApi = class {
|
|
649
|
+
constructor(config, options = {}) {
|
|
650
|
+
this.config = config;
|
|
651
|
+
this.origin = config.baseUrl;
|
|
652
|
+
this.fetchImpl = config.fetchImpl ?? globalThis.fetch;
|
|
653
|
+
this.sleep = options.sleep ?? delay;
|
|
654
|
+
}
|
|
655
|
+
config;
|
|
656
|
+
origin;
|
|
657
|
+
fetchImpl;
|
|
658
|
+
sleep;
|
|
659
|
+
async request(method, path, body, maxResponseBytes) {
|
|
660
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
661
|
+
try {
|
|
662
|
+
const headers = {
|
|
663
|
+
authorization: `Bearer ${this.config.token}`,
|
|
664
|
+
"x-harness-cli-version": CLI_VERSION
|
|
665
|
+
};
|
|
666
|
+
const encoded = body === void 0 ? void 0 : Buffer.isBuffer(body) ? body : JSON.stringify(body);
|
|
667
|
+
if (body !== void 0)
|
|
668
|
+
headers["content-type"] = Buffer.isBuffer(body) ? "application/octet-stream" : "application/json";
|
|
669
|
+
const response = await this.fetchImpl(
|
|
670
|
+
new URL(this.origin + "/api/v1/code" + path),
|
|
671
|
+
{
|
|
672
|
+
method,
|
|
673
|
+
headers,
|
|
674
|
+
body: encoded,
|
|
675
|
+
signal: AbortSignal.timeout(3e4),
|
|
676
|
+
redirect: "manual"
|
|
677
|
+
}
|
|
678
|
+
);
|
|
679
|
+
const retryAfter = retryAfterMs(response.headers.get("retry-after"));
|
|
680
|
+
const text = await responseText(response, maxResponseBytes);
|
|
681
|
+
let json2 = {};
|
|
682
|
+
if (text) {
|
|
683
|
+
try {
|
|
684
|
+
const parsed = JSON.parse(text);
|
|
685
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
|
|
686
|
+
json2 = parsed;
|
|
687
|
+
else if (response.ok)
|
|
688
|
+
throw new CommandError(
|
|
689
|
+
"The code index API returned an invalid JSON envelope."
|
|
690
|
+
);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
if (response.ok)
|
|
693
|
+
throw error instanceof CommandError ? error : new CommandError(
|
|
694
|
+
"The code index API returned a non-JSON body."
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
if (!response.ok) {
|
|
699
|
+
const message = typeof json2["message"] === "string" ? json2["message"] : `Code index request refused (HTTP ${response.status}).`;
|
|
700
|
+
if (response.status === 426)
|
|
701
|
+
throw new UpgradeRequiredError(message, json2);
|
|
702
|
+
throw new HttpError(
|
|
703
|
+
response.status,
|
|
704
|
+
message,
|
|
705
|
+
json2,
|
|
706
|
+
response.headers.get("retry-after")
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
return { data: json2["data"], meta: json2["meta"], retryAfter };
|
|
710
|
+
} catch (error) {
|
|
711
|
+
if (error instanceof UpgradeRequiredError || error instanceof CommandError)
|
|
712
|
+
throw error;
|
|
713
|
+
const transient = error instanceof HttpError ? [429, 500, 502, 503, 504].includes(error.status) : true;
|
|
714
|
+
if (!transient || attempt === 2) {
|
|
715
|
+
if (error instanceof HttpError) throw error;
|
|
716
|
+
throw new TransportError(
|
|
717
|
+
"Code index request did not complete. Retry this command with the same worktree to resume."
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
const backoff = error instanceof HttpError ? retryAfterMs(error.retryAfter) : void 0;
|
|
721
|
+
const wait = backoff ?? 250 * 2 ** attempt;
|
|
722
|
+
if (wait > 3e5)
|
|
723
|
+
throw error instanceof HttpError ? error : new TransportError(
|
|
724
|
+
"The code index retry backoff is too long; retry later."
|
|
725
|
+
);
|
|
726
|
+
await this.sleep(wait);
|
|
727
|
+
}
|
|
877
728
|
}
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
729
|
+
throw new CommandError("Code index retry limit exceeded.");
|
|
730
|
+
}
|
|
731
|
+
async query(id, operation, p) {
|
|
732
|
+
const root = `/views/${encodeURIComponent(id)}`;
|
|
733
|
+
if (operation === "affected") {
|
|
734
|
+
const result2 = await this.request("POST", root + "/affected", {
|
|
735
|
+
symbol_ids: p.symbols ?? [],
|
|
736
|
+
paths: p.files ?? [],
|
|
737
|
+
...p.depth !== void 0 ? { depth: p.depth } : {},
|
|
738
|
+
...p.includeAmbiguous !== void 0 ? { include_ambiguous: p.includeAmbiguous } : {},
|
|
739
|
+
...p.includeTypes !== void 0 ? { include_types: p.includeTypes } : {}
|
|
740
|
+
}, 16 * 1024 * 1024);
|
|
741
|
+
return { data: result2.data, meta: result2.meta };
|
|
884
742
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
seat = expertSeat(probe);
|
|
890
|
-
if (seat === null) {
|
|
891
|
-
return {
|
|
892
|
-
ok: false,
|
|
893
|
-
message: `signed in to ${config.baseUrl} with an outside expert's pxc_ credential, but the assignments response carried no expert`
|
|
894
|
-
};
|
|
743
|
+
const path = operation === "explain" ? root + `/symbols/${encodeURIComponent(p.symbol)}` : root + (operation === "search" ? "/symbols" : "/context");
|
|
744
|
+
const params = new URLSearchParams();
|
|
745
|
+
for (const [key, value] of Object.entries(operation === "search" ? { q: p.q, kind: p.kind, path_prefix: p.pathPrefix, limit: p.limit } : operation === "explain" ? { budget_tokens: p.budgetTokens } : { q: p.q, symbol_id: p.symbol, budget_tokens: p.budgetTokens })) {
|
|
746
|
+
if (value !== void 0) params.set(key, String(value));
|
|
895
747
|
}
|
|
748
|
+
const result = await this.request("GET", path + (params.size ? "?" + params.toString() : ""), void 0, 16 * 1024 * 1024);
|
|
749
|
+
return { data: result.data, meta: result.meta };
|
|
896
750
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
env: "HARNESS_API_URL",
|
|
900
|
-
"repo-config": ".harness-cli/config.json",
|
|
901
|
-
"stored-login": "stored login",
|
|
902
|
-
default: "workspace default"
|
|
903
|
-
};
|
|
904
|
-
const via = config.hostSource !== void 0 ? ` (via ${hostSourceLabels[config.hostSource]})` : "";
|
|
905
|
-
const expertLines = seat === null ? "" : `Expert: ${expertSummary(seat)}
|
|
906
|
-
Reaches: its own assignments and the gates it owns on them (assignments, conditions, gate)
|
|
907
|
-
`;
|
|
908
|
-
const described = session ?? NO_SESSION;
|
|
909
|
-
return {
|
|
910
|
-
ok: true,
|
|
911
|
-
json: {
|
|
912
|
-
url: config.baseUrl,
|
|
913
|
-
source: credentialSource,
|
|
914
|
-
verified: true,
|
|
915
|
-
server_version: serverVersion,
|
|
916
|
-
host_source: config.hostSource,
|
|
917
|
-
kind: described.kind,
|
|
918
|
-
expires_at: described.expires_at,
|
|
919
|
-
scopes: described.scopes,
|
|
920
|
-
user: described.user,
|
|
921
|
-
org: described.org,
|
|
922
|
-
expert: seat
|
|
923
|
-
},
|
|
924
|
-
human: `Host: ${config.baseUrl}${via}
|
|
925
|
-
Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}${kindLabel(described.kind)}
|
|
926
|
-
` + sessionLines(described) + expertLines + `Verified: ok (server API ${serverVersion})
|
|
927
|
-
`
|
|
928
|
-
};
|
|
929
|
-
}
|
|
930
|
-
function kindLabel(kind) {
|
|
931
|
-
if (kind === null) return "";
|
|
932
|
-
if (kind === "harness-cli") return ", harness-cli (full access)";
|
|
933
|
-
if (kind === "operator-cli") return ", operator-cli (Operator only)";
|
|
934
|
-
return `, ${clean(kind)}`;
|
|
935
|
-
}
|
|
936
|
-
function sessionLines(session) {
|
|
937
|
-
let lines = "";
|
|
938
|
-
const user2 = session.user;
|
|
939
|
-
if (user2 !== null) {
|
|
940
|
-
const name = clean(user2["name"]);
|
|
941
|
-
const email = clean(user2["email"]);
|
|
942
|
-
lines += `User: ${name === "" ? email : email === "" ? name : `${name} <${email}>`}
|
|
943
|
-
`;
|
|
751
|
+
async getPolicy() {
|
|
752
|
+
return decode(policy, (await this.request("GET", "/policy")).data);
|
|
944
753
|
}
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
`;
|
|
754
|
+
async putRepository(repoKey) {
|
|
755
|
+
return decode(
|
|
756
|
+
z.object({ repository_id: UUID, repo_key: z.string() }),
|
|
757
|
+
(await this.request("PUT", "/repositories", { repo_key: repoKey })).data
|
|
758
|
+
);
|
|
951
759
|
}
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
760
|
+
async getBase(repositoryId, commit, skipped) {
|
|
761
|
+
const r = await this.request(
|
|
762
|
+
"GET",
|
|
763
|
+
`/repositories/${encodeURIComponent(repositoryId)}/bases/${encodeURIComponent(commit)}${skipped ? "?skipped=true" : ""}`
|
|
764
|
+
);
|
|
765
|
+
const data = decode(base, r.data);
|
|
766
|
+
const wait = Math.max(data.retry_after_ms ?? 0, r.retryAfter ?? 0);
|
|
767
|
+
return wait ? { ...data, retry_after_ms: wait } : data;
|
|
768
|
+
}
|
|
769
|
+
async putView(repositoryId, key) {
|
|
770
|
+
return decode(
|
|
771
|
+
view,
|
|
772
|
+
(await this.request(
|
|
773
|
+
"PUT",
|
|
774
|
+
`/repositories/${encodeURIComponent(repositoryId)}/views/${encodeURIComponent(key)}`,
|
|
775
|
+
{}
|
|
776
|
+
)).data
|
|
777
|
+
);
|
|
956
778
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
const steps = Array.isArray(data["assignments"]) ? data["assignments"].filter(isRecord2).length : 0;
|
|
963
|
-
return { id: data["expert"]["id"], role: data["expert"]["role"] ?? null, plan: plan2, assigned_steps: steps };
|
|
964
|
-
}
|
|
965
|
-
function isRecord2(value) {
|
|
966
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
967
|
-
}
|
|
968
|
-
function bareClient(baseUrl, token, options) {
|
|
969
|
-
const config = {
|
|
970
|
-
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
971
|
-
token,
|
|
972
|
-
actor: null,
|
|
973
|
-
team: null,
|
|
974
|
-
cacheDir: ""
|
|
975
|
-
};
|
|
976
|
-
if (options.fetchImpl !== void 0) config.fetchImpl = options.fetchImpl;
|
|
977
|
-
return new Client(config);
|
|
978
|
-
}
|
|
979
|
-
function defaultNotify(line) {
|
|
980
|
-
process.stderr.write(named(line + "\n"));
|
|
981
|
-
}
|
|
982
|
-
function defaultPrompt() {
|
|
983
|
-
const readline = createInterface({
|
|
984
|
-
input: process.stdin,
|
|
985
|
-
output: process.stderr
|
|
986
|
-
});
|
|
987
|
-
return new Promise((resolve) => {
|
|
988
|
-
readline.question("Token: ", (answer) => {
|
|
989
|
-
readline.close();
|
|
990
|
-
resolve(answer);
|
|
991
|
-
});
|
|
992
|
-
});
|
|
993
|
-
}
|
|
994
|
-
function defaultOpenBrowser(url) {
|
|
995
|
-
const opener = browserOpener();
|
|
996
|
-
if (opener !== null) {
|
|
997
|
-
execFile(opener, [url], () => void 0);
|
|
779
|
+
async getView(id) {
|
|
780
|
+
return decode(
|
|
781
|
+
view,
|
|
782
|
+
(await this.request("GET", `/views/${encodeURIComponent(id)}`)).data
|
|
783
|
+
);
|
|
998
784
|
}
|
|
999
|
-
|
|
785
|
+
async openSync(id, body) {
|
|
786
|
+
return decode(
|
|
787
|
+
opened,
|
|
788
|
+
(await this.request("POST", `/views/${encodeURIComponent(id)}/syncs`, {
|
|
789
|
+
...body
|
|
790
|
+
})).data
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
async putBlob(hash2, bytes) {
|
|
794
|
+
HASH.parse(hash2);
|
|
795
|
+
await this.request("PUT", `/blobs/${hash2}`, bytes);
|
|
796
|
+
}
|
|
797
|
+
async getSync(viewId, syncId) {
|
|
798
|
+
return decode(syncStatus, (await this.request(
|
|
799
|
+
"GET",
|
|
800
|
+
`/views/${encodeURIComponent(viewId)}/syncs/${encodeURIComponent(syncId)}`
|
|
801
|
+
)).data);
|
|
802
|
+
}
|
|
803
|
+
async activate(viewId, syncId) {
|
|
804
|
+
return decode(
|
|
805
|
+
activated,
|
|
806
|
+
(await this.request(
|
|
807
|
+
"POST",
|
|
808
|
+
`/views/${encodeURIComponent(viewId)}/syncs/${encodeURIComponent(syncId)}/activate`,
|
|
809
|
+
{}
|
|
810
|
+
)).data
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
};
|
|
1000
814
|
|
|
1001
|
-
// src/core/
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
815
|
+
// src/core/code/sync.ts
|
|
816
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
817
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
818
|
+
|
|
819
|
+
// src/core/code/git.ts
|
|
820
|
+
import { execFile } from "child_process";
|
|
821
|
+
import { promisify } from "util";
|
|
822
|
+
import { realpath } from "fs/promises";
|
|
823
|
+
var run2 = promisify(execFile);
|
|
824
|
+
var MAX_GIT_BYTES = 16 * 1024 * 1024;
|
|
825
|
+
async function gitBytes(cwd, args, maxBuffer = MAX_GIT_BYTES) {
|
|
826
|
+
try {
|
|
827
|
+
return (await run2(
|
|
828
|
+
"git",
|
|
829
|
+
[
|
|
830
|
+
"--no-optional-locks",
|
|
831
|
+
"--no-pager",
|
|
832
|
+
"-c",
|
|
833
|
+
"core.fsmonitor=false",
|
|
834
|
+
...args
|
|
835
|
+
],
|
|
836
|
+
{ cwd, maxBuffer, timeout: 3e4, encoding: "buffer" }
|
|
837
|
+
)).stdout;
|
|
838
|
+
} catch (error) {
|
|
839
|
+
const code2 = error.code;
|
|
840
|
+
if (code2 === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || error instanceof Error && /maxBuffer/i.test(error.message))
|
|
841
|
+
throw new LocalError(
|
|
842
|
+
"Git returned more than 16 MiB of metadata; narrow the worktree before code sync."
|
|
843
|
+
);
|
|
844
|
+
throw new LocalError(
|
|
845
|
+
"Cannot read this Git working tree. Run code commands inside a committed repository with an origin remote."
|
|
846
|
+
);
|
|
1008
847
|
}
|
|
1009
848
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
const
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
849
|
+
function decodeUtf8(value, message) {
|
|
850
|
+
const decoded = value.toString("utf8");
|
|
851
|
+
if (!Buffer.from(decoded, "utf8").equals(value)) throw new LocalError(message);
|
|
852
|
+
return decoded;
|
|
853
|
+
}
|
|
854
|
+
function decodeGitPaths(outputs) {
|
|
855
|
+
const paths = [];
|
|
856
|
+
for (const output of outputs) {
|
|
857
|
+
if (output.length > 0 && output.at(-1) !== 0)
|
|
858
|
+
throw new LocalError("Code sync received an incomplete Git path record.");
|
|
859
|
+
let start = 0;
|
|
860
|
+
for (let end = output.indexOf(0, start); end !== -1; end = output.indexOf(0, start)) {
|
|
861
|
+
if (end === start)
|
|
862
|
+
throw new LocalError("Code sync received an empty Git path record.");
|
|
863
|
+
const path = decodeUtf8(
|
|
864
|
+
output.subarray(start, end),
|
|
865
|
+
"Code sync cannot safely index a Git path with invalid UTF-8."
|
|
866
|
+
);
|
|
867
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(path))
|
|
868
|
+
throw new LocalError(
|
|
869
|
+
"Code sync cannot safely index a Git path with control characters."
|
|
870
|
+
);
|
|
871
|
+
paths.push(path);
|
|
872
|
+
start = end + 1;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return paths;
|
|
876
|
+
}
|
|
877
|
+
function decodeGitLine(output, message) {
|
|
878
|
+
if (output.length === 0 || output.at(-1) !== 10)
|
|
879
|
+
throw new LocalError(message);
|
|
880
|
+
return decodeUtf8(output.subarray(0, -1), message);
|
|
881
|
+
}
|
|
882
|
+
function assertDistinctPathIdentities(paths) {
|
|
883
|
+
const known = /* @__PURE__ */ new Map();
|
|
884
|
+
for (const path of paths) {
|
|
885
|
+
let original = "";
|
|
886
|
+
let folded = "";
|
|
887
|
+
for (const part of path.split("/")) {
|
|
888
|
+
original += `/${part}`;
|
|
889
|
+
folded += `/${part.normalize("NFC").toLocaleLowerCase("en-US").toLocaleUpperCase("en-US").normalize("NFC")}`;
|
|
890
|
+
const previous = known.get(folded);
|
|
891
|
+
if (previous !== void 0 && previous !== original)
|
|
892
|
+
throw new LocalError(
|
|
893
|
+
"Code sync cannot safely distinguish colliding Git path identities."
|
|
894
|
+
);
|
|
895
|
+
known.set(folded, original);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function identity(cwd) {
|
|
900
|
+
const rootRecord = decodeGitLine(
|
|
901
|
+
await gitBytes(cwd, ["rev-parse", "--show-toplevel"]),
|
|
902
|
+
"Cannot resolve the Git working tree root safely."
|
|
903
|
+
);
|
|
904
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(rootRecord))
|
|
905
|
+
throw new LocalError("Cannot resolve the Git working tree root safely.");
|
|
906
|
+
const root = await realpath(rootRecord);
|
|
907
|
+
const commit = decodeGitLine(
|
|
908
|
+
await gitBytes(root, ["rev-parse", "--verify", "HEAD^{commit}"]),
|
|
909
|
+
"Cannot resolve the repository HEAD commit."
|
|
910
|
+
);
|
|
911
|
+
const remote = decodeGitLine(
|
|
912
|
+
await gitBytes(root, ["remote", "get-url", "origin"]),
|
|
913
|
+
"repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
|
|
914
|
+
);
|
|
915
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(remote))
|
|
916
|
+
throw new LocalError(
|
|
917
|
+
"repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
|
|
918
|
+
);
|
|
919
|
+
const scp = /^(?:git@)?github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(remote);
|
|
920
|
+
let parts;
|
|
921
|
+
if (scp) parts = [scp[1], scp[2]];
|
|
922
|
+
else {
|
|
923
|
+
try {
|
|
924
|
+
const url = new URL(remote);
|
|
925
|
+
if (["https:", "ssh:", "git:"].includes(url.protocol) && url.hostname.toLowerCase() === "github.com" && !url.password && !url.search && !url.hash && (!url.port || url.protocol === "ssh:")) {
|
|
926
|
+
parts = url.pathname.replace(/^\//, "").replace(/\/$/, "").replace(/\.git$/, "").split("/");
|
|
927
|
+
}
|
|
928
|
+
} catch {
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
if (!parts || parts.length !== 2 || parts.some((p) => !p || !/^[\w.-]+$/.test(p) || p === "." || p === ".."))
|
|
932
|
+
throw new LocalError(
|
|
933
|
+
"repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
|
|
934
|
+
);
|
|
935
|
+
if (!/^[0-9a-f]{40,64}$/.test(commit))
|
|
936
|
+
throw new LocalError("Cannot resolve the repository HEAD commit.");
|
|
937
|
+
return {
|
|
938
|
+
root,
|
|
939
|
+
commit,
|
|
940
|
+
repoKey: `github.com/${parts.join("/").toLowerCase()}`
|
|
1049
941
|
};
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
942
|
+
}
|
|
943
|
+
async function changedPaths(root, commit) {
|
|
944
|
+
const changed = await gitBytes(root, [
|
|
945
|
+
"diff",
|
|
946
|
+
"--name-only",
|
|
947
|
+
"-z",
|
|
948
|
+
"--no-renames",
|
|
949
|
+
"--no-ext-diff",
|
|
950
|
+
"--no-textconv",
|
|
951
|
+
commit,
|
|
952
|
+
"--"
|
|
953
|
+
]);
|
|
954
|
+
const untracked = await gitBytes(root, [
|
|
955
|
+
"ls-files",
|
|
956
|
+
"--others",
|
|
957
|
+
"--exclude-standard",
|
|
958
|
+
"-z"
|
|
959
|
+
]);
|
|
960
|
+
return [
|
|
961
|
+
...new Set(decodeGitPaths([changed, untracked]))
|
|
962
|
+
].sort(
|
|
963
|
+
(a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// src/core/code/scan.ts
|
|
968
|
+
import { constants as constants2 } from "fs";
|
|
969
|
+
import { open } from "fs/promises";
|
|
970
|
+
import { join } from "path";
|
|
971
|
+
import { createHash } from "crypto";
|
|
972
|
+
import { release } from "os";
|
|
973
|
+
|
|
974
|
+
// src/core/code/policy.ts
|
|
975
|
+
import { isUtf8 } from "buffer";
|
|
976
|
+
import { extname } from "path";
|
|
977
|
+
|
|
978
|
+
// src/core/code/glob.ts
|
|
979
|
+
var MAX_PATTERN_BYTES = 256;
|
|
980
|
+
var MAX_PATTERN_STARS = 8;
|
|
981
|
+
var MAX_PATTERNS = 1e3;
|
|
982
|
+
function compileIgnorePatterns(lines, warnings) {
|
|
983
|
+
const patterns = [];
|
|
984
|
+
for (const [line, raw] of lines.entries()) {
|
|
985
|
+
const trimmed = raw.trim();
|
|
986
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
987
|
+
if (trimmed.startsWith("!")) {
|
|
988
|
+
warnings.push(
|
|
989
|
+
`Ignoring .harnesscodeignore negation on line ${line + 1}; code-index patterns do not support !.`
|
|
990
|
+
);
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (trimmed.includes("\\")) {
|
|
994
|
+
warnings.push(
|
|
995
|
+
`The .harnesscodeignore backslash on line ${line + 1} is literal and never matches a normalized source path.`
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
if (Buffer.byteLength(trimmed) > MAX_PATTERN_BYTES || (trimmed.match(/\*/g)?.length ?? 0) > MAX_PATTERN_STARS) {
|
|
999
|
+
warnings.push(
|
|
1000
|
+
"Ignoring .harnesscodeignore pattern outside the code-index size limits."
|
|
1001
|
+
);
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (patterns.length === MAX_PATTERNS) {
|
|
1005
|
+
warnings.push(
|
|
1006
|
+
"Ignoring .harnesscodeignore patterns after the first 1,000 valid entries."
|
|
1007
|
+
);
|
|
1008
|
+
break;
|
|
1009
|
+
}
|
|
1010
|
+
const pattern = compilePattern(trimmed);
|
|
1011
|
+
if (pattern) patterns.push(pattern);
|
|
1012
|
+
}
|
|
1013
|
+
return patterns;
|
|
1014
|
+
}
|
|
1015
|
+
function patternLimitError() {
|
|
1016
|
+
return new Error("Code upload policy contains an invalid path pattern.");
|
|
1017
|
+
}
|
|
1018
|
+
function escapeRegex(pattern) {
|
|
1019
|
+
return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1020
|
+
}
|
|
1021
|
+
function globBody(pattern) {
|
|
1022
|
+
return escapeRegex(pattern).replaceAll("\\*\\*/", "(?:.*/)?").replaceAll("/\\*\\*", "/.*").replaceAll("\\*", "[^/]*").replaceAll("\\?", "[^/]");
|
|
1023
|
+
}
|
|
1024
|
+
function compilePattern(raw, {
|
|
1025
|
+
caseInsensitive = false,
|
|
1026
|
+
mode = "ignore",
|
|
1027
|
+
warnings,
|
|
1028
|
+
maxPatternBytes = MAX_PATTERN_BYTES,
|
|
1029
|
+
maxStars = MAX_PATTERN_STARS,
|
|
1030
|
+
rejectUnsupported = false
|
|
1031
|
+
} = {}) {
|
|
1032
|
+
let pattern = mode === "served" ? raw : raw.trim();
|
|
1033
|
+
if (pattern === "") return null;
|
|
1034
|
+
if (pattern.startsWith("#")) {
|
|
1035
|
+
if (rejectUnsupported) throw patternLimitError();
|
|
1080
1036
|
return null;
|
|
1081
1037
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1038
|
+
if (Buffer.byteLength(pattern) > maxPatternBytes || (pattern.match(/\*/g)?.length ?? 0) > maxStars)
|
|
1039
|
+
throw patternLimitError();
|
|
1040
|
+
if (pattern.startsWith("!")) {
|
|
1041
|
+
if (rejectUnsupported) throw patternLimitError();
|
|
1042
|
+
warnings?.push(
|
|
1043
|
+
"Ignoring .harnesscodeignore negation; code-index patterns do not support !."
|
|
1087
1044
|
);
|
|
1088
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1089
|
-
const version2 = parsed["version"];
|
|
1090
|
-
return typeof version2 === "string" ? version2 : null;
|
|
1091
|
-
} catch {
|
|
1092
1045
|
return null;
|
|
1093
1046
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if (basename(dir) === "node_modules") return dir;
|
|
1100
|
-
if (dir === root) return null;
|
|
1101
|
-
dir = dirname(dir);
|
|
1047
|
+
if (pattern.includes("\\")) {
|
|
1048
|
+
if (rejectUnsupported) throw patternLimitError();
|
|
1049
|
+
warnings?.push(
|
|
1050
|
+
"A .harnesscodeignore backslash is literal, not an escape."
|
|
1051
|
+
);
|
|
1102
1052
|
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
const
|
|
1107
|
-
|
|
1053
|
+
if (rejectUnsupported && /[\[\]{}]/.test(pattern)) throw patternLimitError();
|
|
1054
|
+
const anchored = pattern.includes("/");
|
|
1055
|
+
if (mode === "ignore") pattern = pattern.replace(/^\/+|\/+$/g, "");
|
|
1056
|
+
const prefix = anchored ? "^" : "^(?:.*/)?";
|
|
1057
|
+
const suffix = mode === "ignore" ? "(?:/.*)?$" : "$";
|
|
1108
1058
|
return {
|
|
1109
|
-
|
|
1110
|
-
|
|
1059
|
+
source: mode === "served" ? raw : raw.trim(),
|
|
1060
|
+
negated: false,
|
|
1061
|
+
regex: new RegExp(
|
|
1062
|
+
prefix + globBody(pattern) + suffix,
|
|
1063
|
+
caseInsensitive ? "i" : ""
|
|
1064
|
+
)
|
|
1111
1065
|
};
|
|
1112
1066
|
}
|
|
1113
|
-
function
|
|
1114
|
-
const
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
for (let index = 0; index < 3; index++) {
|
|
1118
|
-
if (a.parts[index] !== b.parts[index])
|
|
1119
|
-
return a.parts[index] > b.parts[index];
|
|
1120
|
-
}
|
|
1121
|
-
if (a.pre === b.pre) return false;
|
|
1122
|
-
if (a.pre === null) return true;
|
|
1123
|
-
if (b.pre === null) return false;
|
|
1124
|
-
return comparePrerelease(a.pre, b.pre) > 0;
|
|
1125
|
-
}
|
|
1126
|
-
function comparePrerelease(left, right) {
|
|
1127
|
-
const a = left.split(".");
|
|
1128
|
-
const b = right.split(".");
|
|
1129
|
-
for (let index = 0; index < Math.max(a.length, b.length); index++) {
|
|
1130
|
-
const x = a[index];
|
|
1131
|
-
const y = b[index];
|
|
1132
|
-
if (x === void 0) return -1;
|
|
1133
|
-
if (y === void 0) return 1;
|
|
1134
|
-
const xNumeric = /^\d+$/.test(x);
|
|
1135
|
-
const yNumeric = /^\d+$/.test(y);
|
|
1136
|
-
if (xNumeric && yNumeric) {
|
|
1137
|
-
if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1;
|
|
1138
|
-
} else if (xNumeric !== yNumeric) {
|
|
1139
|
-
return xNumeric ? -1 : 1;
|
|
1140
|
-
} else if (x !== y) {
|
|
1141
|
-
return x < y ? -1 : 1;
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
return 0;
|
|
1067
|
+
function compilePatterns(lines, options = {}) {
|
|
1068
|
+
const maxPatterns = options.maxPatterns ?? MAX_PATTERNS;
|
|
1069
|
+
if (lines.length > maxPatterns) throw patternLimitError();
|
|
1070
|
+
return lines.map((line) => compilePattern(line, options)).filter((pattern) => pattern !== null);
|
|
1145
1071
|
}
|
|
1146
|
-
function
|
|
1147
|
-
|
|
1148
|
-
|
|
1072
|
+
function matchingPattern(path, patterns) {
|
|
1073
|
+
let decided = null;
|
|
1074
|
+
for (const pattern of patterns) if (pattern.regex.test(path)) decided = pattern;
|
|
1075
|
+
return decided;
|
|
1149
1076
|
}
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1077
|
+
|
|
1078
|
+
// src/core/code/policy.ts
|
|
1079
|
+
var MAX_FILE_BYTES = 1048576;
|
|
1080
|
+
var EXTENSIONS = [
|
|
1081
|
+
".ex",
|
|
1082
|
+
".exs",
|
|
1083
|
+
".js",
|
|
1084
|
+
".mjs",
|
|
1085
|
+
".cjs",
|
|
1086
|
+
".jsx",
|
|
1087
|
+
".ts",
|
|
1088
|
+
".tsx",
|
|
1089
|
+
".swift"
|
|
1090
|
+
];
|
|
1091
|
+
var DENY = [
|
|
1092
|
+
".env",
|
|
1093
|
+
".env.*",
|
|
1094
|
+
"secrets/**",
|
|
1095
|
+
"**/secrets/**",
|
|
1096
|
+
"*.pem",
|
|
1097
|
+
"*.key",
|
|
1098
|
+
"*.p12",
|
|
1099
|
+
"*.pfx",
|
|
1100
|
+
"*.jks",
|
|
1101
|
+
"*.keystore",
|
|
1102
|
+
"id_rsa*",
|
|
1103
|
+
"id_ecdsa*",
|
|
1104
|
+
"id_ed25519*",
|
|
1105
|
+
"*.kdbx",
|
|
1106
|
+
".npmrc",
|
|
1107
|
+
".netrc",
|
|
1108
|
+
"credentials.json",
|
|
1109
|
+
"**/.aws/**",
|
|
1110
|
+
".git/**",
|
|
1111
|
+
"node_modules/**",
|
|
1112
|
+
"_build/**",
|
|
1113
|
+
"deps/**",
|
|
1114
|
+
"dist/**",
|
|
1115
|
+
"*.lock",
|
|
1116
|
+
"package-lock.json",
|
|
1117
|
+
"*.min.js",
|
|
1118
|
+
"*.map",
|
|
1119
|
+
"priv/static/**",
|
|
1120
|
+
"src/generated/**",
|
|
1121
|
+
"DerivedData/**",
|
|
1122
|
+
"**/DerivedData/**",
|
|
1123
|
+
".build/**",
|
|
1124
|
+
"**/.build/**",
|
|
1125
|
+
"Pods/**",
|
|
1126
|
+
"Carthage/Build/**",
|
|
1127
|
+
"xcuserdata/**",
|
|
1128
|
+
"**/xcuserdata/**",
|
|
1129
|
+
"*.xcuserstate",
|
|
1130
|
+
"*.xcresult/**",
|
|
1131
|
+
"*.xcarchive/**",
|
|
1132
|
+
"*.dSYM/**",
|
|
1133
|
+
"*.ipa",
|
|
1134
|
+
"*.secret.exs"
|
|
1135
|
+
];
|
|
1136
|
+
var SECRET_PATTERN_SOURCES = [
|
|
1137
|
+
"-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----",
|
|
1138
|
+
"\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b",
|
|
1139
|
+
"\\baone_[A-Za-z0-9_-]{20,}",
|
|
1140
|
+
"\\bgh[opsur]_[A-Za-z0-9]{36}\\b",
|
|
1141
|
+
"\\b[rs]k_live_[A-Za-z0-9]{20,}",
|
|
1142
|
+
"\\bAIza[0-9A-Za-z_-]{35}\\b",
|
|
1143
|
+
"\\bxapp-\\d-[A-Za-z0-9-]{20,}",
|
|
1144
|
+
"hooks\\.slack\\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+",
|
|
1145
|
+
"\\bgithub_pat_[A-Za-z0-9_]{22,}",
|
|
1146
|
+
"\\bxox[baprs]-\\d{8,}-[A-Za-z0-9-]{10,}",
|
|
1147
|
+
"\\bsk-(?:proj-|ant-[a-z0-9]+-)?[A-Za-z0-9_-]{32,}"
|
|
1148
|
+
];
|
|
1149
|
+
var SECRETS = SECRET_PATTERN_SOURCES.map((source) => new RegExp(source));
|
|
1150
|
+
function contentMatchers(patterns) {
|
|
1151
|
+
const literalMarkers = [];
|
|
1152
|
+
for (const pattern of patterns) {
|
|
1153
|
+
if (SECRET_PATTERN_SOURCES.includes(pattern)) continue;
|
|
1154
|
+
if (!pattern || /[\\^$.*+?()[\]{}|]/.test(pattern))
|
|
1155
|
+
throw new Error("unknown regular expression");
|
|
1156
|
+
literalMarkers.push(pattern);
|
|
1157
|
+
}
|
|
1158
|
+
return literalMarkers;
|
|
1159
|
+
}
|
|
1160
|
+
function servedCase(served) {
|
|
1161
|
+
const value = served.deny_glob_case;
|
|
1162
|
+
if (value !== void 0 && value !== "ascii_insensitive")
|
|
1163
|
+
throw new Error("unknown deny glob case mode");
|
|
1164
|
+
return value;
|
|
1165
|
+
}
|
|
1166
|
+
function buildPolicy(served, ignore, includeFloor) {
|
|
1167
|
+
const warnings = [];
|
|
1168
|
+
const unsupported = [...new Set(
|
|
1169
|
+
served.include_extensions.filter((extension) => !EXTENSIONS.includes(extension))
|
|
1170
|
+
)].sort();
|
|
1171
|
+
if (unsupported.length)
|
|
1172
|
+
warnings.push(
|
|
1173
|
+
`This harness-cli does not send ${unsupported.join(", ")} files; upgrade the client.`
|
|
1154
1174
|
);
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1175
|
+
const insensitiveServed = servedCase(served) === "ascii_insensitive";
|
|
1176
|
+
const literalMarkers = contentMatchers(served.content_patterns);
|
|
1177
|
+
return {
|
|
1178
|
+
limit: Math.min(served.max_file_bytes, MAX_FILE_BYTES),
|
|
1179
|
+
deny: [
|
|
1180
|
+
...includeFloor ? compilePatterns(DENY, {
|
|
1181
|
+
caseInsensitive: true,
|
|
1182
|
+
maxPatterns: Infinity,
|
|
1183
|
+
maxPatternBytes: Infinity,
|
|
1184
|
+
maxStars: Infinity
|
|
1185
|
+
}) : [],
|
|
1186
|
+
...compilePatterns(served.deny_globs, {
|
|
1187
|
+
caseInsensitive: includeFloor || insensitiveServed,
|
|
1188
|
+
mode: "served",
|
|
1189
|
+
maxPatternBytes: 1024,
|
|
1190
|
+
maxStars: Infinity,
|
|
1191
|
+
rejectUnsupported: true
|
|
1192
|
+
})
|
|
1193
|
+
],
|
|
1194
|
+
ignore: compileIgnorePatterns(ignore.split(/\r?\n/), warnings),
|
|
1195
|
+
patterns: SECRETS,
|
|
1196
|
+
literalMarkers,
|
|
1197
|
+
extensions: new Set(
|
|
1198
|
+
EXTENSIONS.filter((extension) => served.include_extensions.includes(extension))
|
|
1199
|
+
),
|
|
1200
|
+
warnings
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
function policyFor(served, ignore) {
|
|
1204
|
+
try {
|
|
1205
|
+
return buildPolicy(served, ignore, true);
|
|
1162
1206
|
} catch {
|
|
1163
|
-
|
|
1207
|
+
throw new CommandError(
|
|
1208
|
+
"The server returned an invalid code upload policy; upgrade the client before code sync."
|
|
1209
|
+
);
|
|
1164
1210
|
}
|
|
1165
1211
|
}
|
|
1166
|
-
function
|
|
1167
|
-
const path = stampPath(env, packageName);
|
|
1168
|
-
const temporary = `${path}.${process.pid}.tmp`;
|
|
1169
|
-
const latest = stamp.latest ?? readStamp(env, packageName)?.latest ?? null;
|
|
1212
|
+
function basePolicyFor(served, ignore) {
|
|
1170
1213
|
try {
|
|
1171
|
-
|
|
1172
|
-
writeFileSync(temporary, JSON.stringify({ ...stamp, latest }) + "\n");
|
|
1173
|
-
renameSync(temporary, path);
|
|
1214
|
+
return buildPolicy(served, ignore, false);
|
|
1174
1215
|
} catch {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
}
|
|
1216
|
+
throw new CommandError(
|
|
1217
|
+
"The server returned an invalid code upload policy; upgrade the client before code sync."
|
|
1218
|
+
);
|
|
1179
1219
|
}
|
|
1180
1220
|
}
|
|
1181
|
-
function
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
if (
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const packageName = install.packageName ?? PACKAGE_NAME;
|
|
1188
|
-
const stamp = readStamp(env, packageName);
|
|
1189
|
-
if (stamp === null || now - stamp.checkedAt >= CHECK_INTERVAL_MS) {
|
|
1190
|
-
writeStamp({ checkedAt: now, latest: stamp?.latest ?? null }, env, packageName);
|
|
1191
|
-
(options.refresh ?? spawnRefresh)();
|
|
1192
|
-
}
|
|
1193
|
-
if (stamp !== null && stamp.latest !== null && isNewer(stamp.latest, CLI_VERSION)) {
|
|
1194
|
-
return `harness: ${stamp.latest} is available (you have ${CLI_VERSION}) \u2014 ${upgradeHint(install)}
|
|
1195
|
-
`;
|
|
1196
|
-
}
|
|
1197
|
-
return null;
|
|
1221
|
+
function pathReason(path, p) {
|
|
1222
|
+
if (!path || Buffer.byteLength(path) > 1024 || /[\\\x00-\x1f\x7f-\x9f]/.test(path) || Buffer.from(path, "utf8").toString("utf8") !== path || path.split("/").some((segment) => ["", ".", ".."].includes(segment)))
|
|
1223
|
+
return "invalid_path";
|
|
1224
|
+
if (p.deny.some((pattern) => pattern.regex.test(path))) return "excluded_path";
|
|
1225
|
+
if (matchingPattern(path, p.ignore)) return "ignored";
|
|
1226
|
+
return p.extensions.has(extname(path)) ? null : "unsupported_language";
|
|
1198
1227
|
}
|
|
1199
|
-
function
|
|
1200
|
-
|
|
1228
|
+
function extensionWarning(path, p) {
|
|
1229
|
+
const extension = extname(path);
|
|
1230
|
+
return !p.extensions.has(extension) && p.extensions.has(extension.toLowerCase()) ? `The server indexes ${extension.toLowerCase()} exactly; ${path} was skipped.` : null;
|
|
1201
1231
|
}
|
|
1202
|
-
function
|
|
1203
|
-
|
|
1204
|
-
if (
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
detached: true,
|
|
1208
|
-
stdio: "ignore"
|
|
1209
|
-
});
|
|
1210
|
-
child.on("error", () => {
|
|
1211
|
-
});
|
|
1212
|
-
child.unref();
|
|
1213
|
-
} catch {
|
|
1214
|
-
}
|
|
1232
|
+
function contentReason(bytes, p) {
|
|
1233
|
+
if (bytes.length > p.limit) return "too_large";
|
|
1234
|
+
if (bytes.includes(0) || !isUtf8(bytes)) return "binary";
|
|
1235
|
+
const text = bytes.toString("utf8");
|
|
1236
|
+
return p.patterns.some((pattern) => pattern.test(text)) || p.literalMarkers.some((marker) => text.includes(marker)) ? "secret_content" : null;
|
|
1215
1237
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1238
|
+
|
|
1239
|
+
// src/core/code/scan.ts
|
|
1240
|
+
var MAX_OVERLAY_BYTES = 64 * 1024 * 1024;
|
|
1241
|
+
var MAX_OVERLAY_CHANGES = 2e4;
|
|
1242
|
+
var O_NOFOLLOW_ANY = 536870912;
|
|
1243
|
+
function sha256(data) {
|
|
1244
|
+
return createHash("sha256").update(data).digest("hex");
|
|
1245
|
+
}
|
|
1246
|
+
function supportsDarwinNoFollowAny(osRelease = release()) {
|
|
1247
|
+
const major = Number(osRelease.split(".")[0]);
|
|
1248
|
+
return Number.isInteger(major) && major >= 20;
|
|
1249
|
+
}
|
|
1250
|
+
async function committedIgnore(root, commit) {
|
|
1251
|
+
const entry = decodeUtf8(
|
|
1252
|
+
await gitBytes(root, [
|
|
1253
|
+
"ls-tree",
|
|
1254
|
+
"-z",
|
|
1255
|
+
commit,
|
|
1256
|
+
"--",
|
|
1257
|
+
".harnesscodeignore"
|
|
1258
|
+
]),
|
|
1259
|
+
"Cannot read the committed .harnesscodeignore safely."
|
|
1260
|
+
);
|
|
1261
|
+
if (!entry) return "";
|
|
1262
|
+
const match = /^(\d+) blob ([0-9a-f]+)\t\.harnesscodeignore\0$/.exec(entry);
|
|
1263
|
+
if (!match || !["100644", "100755"].includes(match[1]))
|
|
1264
|
+
throw new LocalError(
|
|
1265
|
+
"The committed .harnesscodeignore must be a regular file before code sync."
|
|
1266
|
+
);
|
|
1267
|
+
return decodeUtf8(
|
|
1268
|
+
await gitBytes(root, ["cat-file", "blob", match[2]], 65537),
|
|
1269
|
+
"The committed .harnesscodeignore must be valid UTF-8 before code sync."
|
|
1222
1270
|
);
|
|
1223
1271
|
}
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1272
|
+
var directoryFlags = constants2.O_RDONLY | constants2.O_DIRECTORY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK;
|
|
1273
|
+
async function openLinuxRoot(root) {
|
|
1274
|
+
let handle;
|
|
1227
1275
|
try {
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1276
|
+
if (!root.startsWith("/"))
|
|
1277
|
+
throw new Error("repository root is not absolute");
|
|
1278
|
+
handle = await open("/", directoryFlags);
|
|
1279
|
+
for (const part of root.split("/").filter(Boolean)) {
|
|
1280
|
+
if (part === "." || part === "..")
|
|
1281
|
+
throw new Error("repository root is invalid");
|
|
1282
|
+
const next = await open(
|
|
1283
|
+
`/proc/self/fd/${handle.fd}/${part}`,
|
|
1284
|
+
directoryFlags
|
|
1285
|
+
);
|
|
1286
|
+
await handle.close();
|
|
1287
|
+
handle = next;
|
|
1288
|
+
}
|
|
1289
|
+
return handle;
|
|
1239
1290
|
} catch {
|
|
1240
|
-
|
|
1291
|
+
await handle?.close().catch(() => {
|
|
1292
|
+
});
|
|
1293
|
+
throw new LocalError(
|
|
1294
|
+
"Code sync cannot establish a safe source root on this platform."
|
|
1295
|
+
);
|
|
1241
1296
|
}
|
|
1242
1297
|
}
|
|
1243
|
-
function
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1298
|
+
async function openLinuxBelowRoot(root, path) {
|
|
1299
|
+
let handle = await openLinuxRoot(root);
|
|
1300
|
+
try {
|
|
1301
|
+
const parts = path.split("/");
|
|
1302
|
+
for (const [index, part] of parts.entries()) {
|
|
1303
|
+
if (!part || part === "." || part === "..")
|
|
1304
|
+
throw new LocalError(
|
|
1305
|
+
"Cannot read an overlay file safely; invalid path."
|
|
1306
|
+
);
|
|
1307
|
+
const next = await open(
|
|
1308
|
+
`/proc/self/fd/${handle.fd}/${part}`,
|
|
1309
|
+
constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK | (index === parts.length - 1 ? 0 : constants2.O_DIRECTORY)
|
|
1310
|
+
);
|
|
1311
|
+
await handle.close();
|
|
1312
|
+
handle = next;
|
|
1313
|
+
}
|
|
1314
|
+
return handle;
|
|
1315
|
+
} catch (error) {
|
|
1316
|
+
await handle.close().catch(() => {
|
|
1317
|
+
});
|
|
1318
|
+
throw error;
|
|
1262
1319
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
if (
|
|
1266
|
-
|
|
1320
|
+
}
|
|
1321
|
+
async function openSafe(root, path) {
|
|
1322
|
+
if (process.platform === "darwin") {
|
|
1323
|
+
if (!supportsDarwinNoFollowAny())
|
|
1324
|
+
throw new LocalError(
|
|
1325
|
+
"Code sync requires macOS 11 or later for safe source reads."
|
|
1326
|
+
);
|
|
1327
|
+
return open(
|
|
1328
|
+
join(root, path),
|
|
1329
|
+
constants2.O_RDONLY | O_NOFOLLOW_ANY | constants2.O_NONBLOCK
|
|
1330
|
+
);
|
|
1267
1331
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1332
|
+
if (process.platform === "linux") return openLinuxBelowRoot(root, path);
|
|
1333
|
+
throw new LocalError(
|
|
1334
|
+
"Code sync cannot safely read source files on this platform."
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
async function readSafe(root, path, limit) {
|
|
1274
1338
|
try {
|
|
1275
|
-
const
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1339
|
+
const handle = await openSafe(root, path);
|
|
1340
|
+
try {
|
|
1341
|
+
const before = await handle.stat();
|
|
1342
|
+
if (!before.isFile()) return { reason: "not_regular_file" };
|
|
1343
|
+
if (before.size > limit) return { reason: "too_large" };
|
|
1344
|
+
const bytes = Buffer.allocUnsafe(Math.min(before.size, limit) + 1);
|
|
1345
|
+
let total = 0;
|
|
1346
|
+
while (total < bytes.length) {
|
|
1347
|
+
const { bytesRead } = await handle.read(
|
|
1348
|
+
bytes,
|
|
1349
|
+
total,
|
|
1350
|
+
bytes.length - total,
|
|
1351
|
+
null
|
|
1352
|
+
);
|
|
1353
|
+
if (bytesRead === 0) break;
|
|
1354
|
+
total += bytesRead;
|
|
1355
|
+
}
|
|
1356
|
+
const after = await handle.stat();
|
|
1357
|
+
if (total > limit) return { reason: "too_large" };
|
|
1358
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || total !== after.size)
|
|
1359
|
+
throw new LocalError(
|
|
1360
|
+
"A source file changed during scanning; retry code sync."
|
|
1361
|
+
);
|
|
1280
1362
|
return {
|
|
1281
|
-
|
|
1282
|
-
|
|
1363
|
+
bytes: bytes.subarray(0, total),
|
|
1364
|
+
mode: before.mode & 64 ? "100755" : "100644"
|
|
1283
1365
|
};
|
|
1366
|
+
} finally {
|
|
1367
|
+
await handle.close();
|
|
1284
1368
|
}
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1369
|
+
} catch (error) {
|
|
1370
|
+
const code2 = error.code;
|
|
1371
|
+
if (code2 === "ENOENT") return { reason: "deleted" };
|
|
1372
|
+
if (code2 === "ELOOP" || code2 === "ENOTDIR") return { reason: "symlink" };
|
|
1373
|
+
if (error instanceof LocalError) throw error;
|
|
1374
|
+
throw new LocalError(
|
|
1375
|
+
"Cannot read an overlay file safely; check permissions and retry code sync."
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
async function scanOverlay(id, served) {
|
|
1380
|
+
policyFor(served, "");
|
|
1381
|
+
const ignore = await readSafe(id.root, ".harnesscodeignore", 65536);
|
|
1382
|
+
if ("reason" in ignore && ignore.reason !== "deleted")
|
|
1383
|
+
throw new LocalError(
|
|
1384
|
+
"Cannot read .harnesscodeignore safely; fix or remove it before code sync."
|
|
1385
|
+
);
|
|
1386
|
+
const localIgnore = "bytes" in ignore ? decodeUtf8(
|
|
1387
|
+
ignore.bytes,
|
|
1388
|
+
"The local .harnesscodeignore must be valid UTF-8 before code sync."
|
|
1389
|
+
) : "";
|
|
1390
|
+
const baseIgnore = await committedIgnore(id.root, id.commit);
|
|
1391
|
+
if (Buffer.byteLength(baseIgnore) > 65536)
|
|
1392
|
+
throw new LocalError(
|
|
1393
|
+
"The committed .harnesscodeignore exceeds 64 KiB; reduce it before code sync."
|
|
1394
|
+
);
|
|
1395
|
+
const policy2 = policyFor(served, localIgnore);
|
|
1396
|
+
const basePolicy = basePolicyFor(served, baseIgnore);
|
|
1397
|
+
const paths = await changedPaths(id.root, id.commit);
|
|
1398
|
+
const basePaths = new Set(
|
|
1399
|
+
decodeGitPaths([
|
|
1400
|
+
await gitBytes(id.root, ["ls-tree", "-r", "--name-only", "-z", id.commit])
|
|
1401
|
+
])
|
|
1402
|
+
);
|
|
1403
|
+
assertDistinctPathIdentities([...basePaths, ...paths]);
|
|
1404
|
+
const changes = [];
|
|
1405
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
1406
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
1407
|
+
const excluded = [];
|
|
1408
|
+
const baseWouldHold = (path) => basePaths.has(path) && pathReason(path, basePolicy) === null;
|
|
1409
|
+
const deleteBasePath = (path) => {
|
|
1410
|
+
if (baseWouldHold(path) && !deleted.has(path)) {
|
|
1411
|
+
if (changes.length >= MAX_OVERLAY_CHANGES)
|
|
1412
|
+
throw new LocalError(
|
|
1413
|
+
`overlay_too_large: the overlay exceeds ${MAX_OVERLAY_CHANGES} entries. Commit the ignore file so the base honors it.`
|
|
1414
|
+
);
|
|
1415
|
+
deleted.add(path);
|
|
1416
|
+
changes.push({ op: "delete", path });
|
|
1290
1417
|
}
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1418
|
+
};
|
|
1419
|
+
const candidates = new Set(paths);
|
|
1420
|
+
const warnings = /* @__PURE__ */ new Set([...policy2.warnings, ...basePolicy.warnings]);
|
|
1421
|
+
for (const path of basePaths) {
|
|
1422
|
+
const baseReason = pathReason(path, basePolicy);
|
|
1423
|
+
const localReason = pathReason(path, policy2);
|
|
1424
|
+
if (baseReason === null && (localReason === "excluded_path" || localReason === "ignored"))
|
|
1425
|
+
deleteBasePath(path);
|
|
1426
|
+
if (baseReason === "ignored" && localReason === null) candidates.add(path);
|
|
1427
|
+
}
|
|
1428
|
+
let total = 0;
|
|
1429
|
+
for (const path of [...candidates].sort(
|
|
1430
|
+
(a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))
|
|
1431
|
+
)) {
|
|
1432
|
+
const reason = pathReason(path, policy2);
|
|
1433
|
+
if (reason) {
|
|
1434
|
+
excluded.push({ path, reason });
|
|
1435
|
+
if (reason === "ignored" || reason === "excluded_path") deleteBasePath(path);
|
|
1436
|
+
const warning = extensionWarning(path, policy2);
|
|
1437
|
+
if (warning) warnings.add(warning);
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
const read = await readSafe(id.root, path, policy2.limit);
|
|
1441
|
+
if ("reason" in read) {
|
|
1442
|
+
if (read.reason !== "deleted")
|
|
1443
|
+
excluded.push({ path, reason: read.reason });
|
|
1444
|
+
deleteBasePath(path);
|
|
1445
|
+
continue;
|
|
1446
|
+
}
|
|
1447
|
+
const content = contentReason(read.bytes, policy2);
|
|
1448
|
+
if (content) {
|
|
1449
|
+
excluded.push({ path, reason: content });
|
|
1450
|
+
deleteBasePath(path);
|
|
1451
|
+
continue;
|
|
1452
|
+
}
|
|
1453
|
+
const hash2 = sha256(read.bytes);
|
|
1454
|
+
if (!blobs.has(hash2)) {
|
|
1455
|
+
total += read.bytes.length;
|
|
1456
|
+
if (total > MAX_OVERLAY_BYTES)
|
|
1457
|
+
throw new LocalError(
|
|
1458
|
+
"The overlay exceeds the 64 MiB client upload limit; split the change before syncing."
|
|
1459
|
+
);
|
|
1460
|
+
blobs.set(hash2, read.bytes);
|
|
1461
|
+
}
|
|
1462
|
+
if (changes.length >= MAX_OVERLAY_CHANGES)
|
|
1463
|
+
throw new LocalError(
|
|
1464
|
+
`overlay_too_large: the overlay exceeds ${MAX_OVERLAY_CHANGES} entries. Commit the ignore file so the base honors it.`
|
|
1465
|
+
);
|
|
1466
|
+
changes.push({
|
|
1467
|
+
op: "put",
|
|
1468
|
+
path,
|
|
1469
|
+
sha256: hash2,
|
|
1470
|
+
size: read.bytes.length,
|
|
1471
|
+
mode: read.mode
|
|
1472
|
+
});
|
|
1293
1473
|
}
|
|
1294
|
-
|
|
1474
|
+
changes.sort((a, b) => Buffer.compare(Buffer.from(a.path), Buffer.from(b.path)));
|
|
1475
|
+
const digest = sha256(
|
|
1476
|
+
changes.map(
|
|
1477
|
+
(c) => c.op === "delete" ? `${c.path}\0-\0-
|
|
1478
|
+
` : `${c.path}\0${c.sha256}\0${c.mode}
|
|
1479
|
+
`
|
|
1480
|
+
).join("")
|
|
1481
|
+
);
|
|
1482
|
+
return {
|
|
1483
|
+
changes,
|
|
1484
|
+
blobs,
|
|
1485
|
+
excluded,
|
|
1486
|
+
warnings: [...warnings],
|
|
1487
|
+
digest
|
|
1488
|
+
};
|
|
1295
1489
|
}
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1490
|
+
|
|
1491
|
+
// src/core/code/state.ts
|
|
1492
|
+
import {
|
|
1493
|
+
readFile,
|
|
1494
|
+
mkdir,
|
|
1495
|
+
writeFile,
|
|
1496
|
+
rename,
|
|
1497
|
+
unlink,
|
|
1498
|
+
rmdir
|
|
1499
|
+
} from "fs/promises";
|
|
1500
|
+
import { join as join2, resolve, isAbsolute } from "path";
|
|
1501
|
+
import { randomUUID } from "crypto";
|
|
1502
|
+
function statePath(server, org, root, repoKey, directory) {
|
|
1503
|
+
const url = new URL(server);
|
|
1504
|
+
const canonical = url.origin + url.pathname.replace(/\/+$/, "");
|
|
1505
|
+
const base2 = directory ?? process.env["HARNESS_CODE_STATE_DIR"] ?? join2(resolveCacheHome(), "harness", "code");
|
|
1506
|
+
if (!isAbsolute(base2))
|
|
1507
|
+
throw new LocalError("HARNESS_CODE_STATE_DIR must be an absolute path.");
|
|
1508
|
+
return join2(
|
|
1509
|
+
resolve(base2),
|
|
1510
|
+
sha256(JSON.stringify([canonical, org.toLowerCase()])),
|
|
1511
|
+
sha256(JSON.stringify([root, repoKey])) + ".json"
|
|
1300
1512
|
);
|
|
1301
1513
|
}
|
|
1302
|
-
function
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
const globalRoot = npmGlobalRoot();
|
|
1316
|
-
if (globalRoot === null)
|
|
1317
|
-
return { ok: false, detail: "npm did not say where its global root is" };
|
|
1318
|
-
if (globalRoot.includes("***")) {
|
|
1319
|
-
return {
|
|
1320
|
-
ok: false,
|
|
1321
|
-
detail: "npm redacted its own global root, so this copy's location cannot be confirmed"
|
|
1322
|
-
};
|
|
1514
|
+
async function readState(path, server, org, root, repoKey) {
|
|
1515
|
+
try {
|
|
1516
|
+
const s = JSON.parse(await readFile(path, "utf8"));
|
|
1517
|
+
if (typeof s !== "object" || s === null) throw new Error();
|
|
1518
|
+
const state2 = s;
|
|
1519
|
+
if (state2.org.toLowerCase() !== org.toLowerCase() || state2.root !== root || state2.repoKey !== repoKey || new URL(state2.server).href !== new URL(server).href || typeof state2.viewKey !== "string" || !/^[\da-f-]{36}$/.test(state2.viewKey))
|
|
1520
|
+
throw new Error();
|
|
1521
|
+
return { ...state2, org: state2.org.toLowerCase() };
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
if (error.code === "ENOENT") return null;
|
|
1524
|
+
throw new LocalError(
|
|
1525
|
+
"The code view state is invalid or belongs to another scope. Check HARNESS_CODE_STATE_DIR; no source was uploaded."
|
|
1526
|
+
);
|
|
1323
1527
|
}
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1528
|
+
}
|
|
1529
|
+
async function initialState(path, server, org, root, repoKey) {
|
|
1530
|
+
const previous = await readState(path, server, org, root, repoKey);
|
|
1531
|
+
if (previous) return previous;
|
|
1532
|
+
const state2 = { server, org: org.toLowerCase(), root, repoKey, viewKey: randomUUID() };
|
|
1533
|
+
await mkdir(join2(path, ".."), { recursive: true, mode: 448 });
|
|
1534
|
+
try {
|
|
1535
|
+
await writeFile(path, JSON.stringify(state2) + "\n", {
|
|
1536
|
+
flag: "wx",
|
|
1537
|
+
mode: 384
|
|
1538
|
+
});
|
|
1539
|
+
return state2;
|
|
1540
|
+
} catch (error) {
|
|
1541
|
+
if (error.code === "EEXIST") {
|
|
1542
|
+
const raced = await readState(path, server, org, root, repoKey);
|
|
1543
|
+
if (raced) return raced;
|
|
1544
|
+
}
|
|
1545
|
+
throw new LocalError("Cannot save isolated code view state.");
|
|
1329
1546
|
}
|
|
1547
|
+
}
|
|
1548
|
+
async function saveState(path, state2) {
|
|
1549
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
1330
1550
|
try {
|
|
1331
|
-
|
|
1551
|
+
await writeFile(tmp, JSON.stringify({ ...state2, org: state2.org.toLowerCase() }) + "\n", {
|
|
1552
|
+
flag: "wx",
|
|
1553
|
+
mode: 384
|
|
1554
|
+
});
|
|
1555
|
+
await rename(tmp, path);
|
|
1332
1556
|
} catch {
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
return {
|
|
1340
|
-
ok: false,
|
|
1341
|
-
detail: "lost the upgrade lock while preparing \u2014 another process took over"
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
process.stderr.write(
|
|
1345
|
-
named(`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
|
|
1346
|
-
`)
|
|
1347
|
-
);
|
|
1348
|
-
const args = [
|
|
1349
|
-
"install",
|
|
1350
|
-
"--global",
|
|
1351
|
-
"--ignore-scripts",
|
|
1352
|
-
"--no-audit",
|
|
1353
|
-
"--no-fund",
|
|
1354
|
-
"--no-update-notifier",
|
|
1355
|
-
"--loglevel=error",
|
|
1356
|
-
upgradeInstallSpec(install)
|
|
1357
|
-
];
|
|
1358
|
-
const result = spawnSync(npmCommand(), args, {
|
|
1359
|
-
// npm's diagnostics belong on stderr with ours; stdout stays the
|
|
1360
|
-
// command's own, so its summary is dropped rather than piped into jq.
|
|
1361
|
-
stdio: ["ignore", "ignore", "inherit"],
|
|
1362
|
-
timeout: INSTALL_TIMEOUT_MS,
|
|
1363
|
-
...npmSpawnOptions()
|
|
1364
|
-
});
|
|
1365
|
-
if (result.error !== void 0)
|
|
1366
|
-
return { ok: false, detail: result.error.message };
|
|
1367
|
-
if (result.status !== 0) {
|
|
1368
|
-
return {
|
|
1369
|
-
ok: false,
|
|
1370
|
-
detail: `npm install exited ${result.status ?? `on ${String(result.signal)}`}`
|
|
1371
|
-
};
|
|
1557
|
+
throw new LocalError(
|
|
1558
|
+
"Cannot save code sync progress; retry with the same worktree and state directory."
|
|
1559
|
+
);
|
|
1560
|
+
} finally {
|
|
1561
|
+
await unlink(tmp).catch(() => {
|
|
1562
|
+
});
|
|
1372
1563
|
}
|
|
1373
|
-
return { ok: true, detail: "" };
|
|
1374
1564
|
}
|
|
1375
|
-
function
|
|
1565
|
+
async function withSyncLock(path, action) {
|
|
1566
|
+
const lock = path + ".lock";
|
|
1567
|
+
await mkdir(join2(path, ".."), { recursive: true, mode: 448 });
|
|
1568
|
+
try {
|
|
1569
|
+
await mkdir(lock, { mode: 448 });
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
if (error.code === "EEXIST")
|
|
1572
|
+
throw new LocalError(
|
|
1573
|
+
`A code sync is already running for this view. Wait for it to finish. If it was interrupted, confirm no sync is running before removing its lock directory: ${lock}`
|
|
1574
|
+
);
|
|
1575
|
+
throw new LocalError(
|
|
1576
|
+
"Cannot lock code sync progress; check the state directory permissions."
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1376
1579
|
try {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1580
|
+
await writeFile(join2(lock, "owner"), `${process.pid}
|
|
1581
|
+
`, {
|
|
1582
|
+
flag: "wx",
|
|
1583
|
+
mode: 384
|
|
1381
1584
|
});
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1585
|
+
return await action();
|
|
1586
|
+
} finally {
|
|
1587
|
+
await unlink(join2(lock, "owner")).catch(() => {
|
|
1588
|
+
});
|
|
1589
|
+
try {
|
|
1590
|
+
await rmdir(lock);
|
|
1591
|
+
} catch {
|
|
1592
|
+
try {
|
|
1593
|
+
process.stderr.write(named(
|
|
1594
|
+
`harness: Warning: code sync lock cleanup failed at ${clean(lock)}. If the lock remains, confirm no sync is running before removing it.
|
|
1595
|
+
`
|
|
1596
|
+
));
|
|
1597
|
+
} catch {
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// src/core/code/sync.ts
|
|
1604
|
+
async function baseReady(api, repo, commit, options) {
|
|
1605
|
+
const timeout = options.pollTimeoutMs ?? 6e5;
|
|
1606
|
+
const sleep = options.sleep ?? delay2;
|
|
1607
|
+
let waited = 0;
|
|
1608
|
+
let interval = 1e3;
|
|
1609
|
+
const started = Date.now();
|
|
1610
|
+
for (let attempts = 0; attempts < 600; attempts++) {
|
|
1611
|
+
const base2 = await api.getBase(repo, commit, options.skipped === true);
|
|
1612
|
+
if (base2.state === "complete" || base2.state === "ready") return base2;
|
|
1613
|
+
if (base2.state === "failed")
|
|
1614
|
+
throw new CommandError(
|
|
1615
|
+
"The server could not build this commit\u2019s base. Check code status --skipped, then retry after the server backoff."
|
|
1616
|
+
);
|
|
1617
|
+
const backoff = base2.retry_after_ms === 0 ? interval : base2.retry_after_ms ?? interval;
|
|
1618
|
+
if (!Number.isFinite(backoff) || backoff < 0 || Math.max(waited, Date.now() - started) + backoff > timeout)
|
|
1619
|
+
throw new CommandError(
|
|
1620
|
+
"Timed out waiting for the server-built base; retry code sync to resume."
|
|
1621
|
+
);
|
|
1622
|
+
await sleep(backoff);
|
|
1623
|
+
waited += backoff;
|
|
1624
|
+
interval = Math.min(interval * 2, 1e4);
|
|
1387
1625
|
}
|
|
1626
|
+
throw new CommandError("The base is still fetching; retry code sync later.");
|
|
1388
1627
|
}
|
|
1389
|
-
function
|
|
1390
|
-
return
|
|
1628
|
+
function stale(error) {
|
|
1629
|
+
return error instanceof HttpError && error.status === 409 && error.body["error"] === "stale_policy";
|
|
1391
1630
|
}
|
|
1392
|
-
function
|
|
1393
|
-
|
|
1631
|
+
function withinActivationRetention(attemptedAt) {
|
|
1632
|
+
const elapsed = typeof attemptedAt === "number" ? Date.now() - attemptedAt : NaN;
|
|
1633
|
+
return Number.isFinite(elapsed) && elapsed >= 0 && elapsed < 60 * 6e4;
|
|
1394
1634
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1635
|
+
var ReplacedSyncError = class extends CommandError {
|
|
1636
|
+
constructor() {
|
|
1637
|
+
super(
|
|
1638
|
+
"The server replaced this sync before activation. Retry code sync with no other sync running for this view."
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
function policyScope(policy2) {
|
|
1643
|
+
if (!/^[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12}$/i.test(policy2.org_id) || !policy2.policy_digest || !Number.isSafeInteger(policy2.max_file_bytes) || policy2.max_file_bytes < 1)
|
|
1644
|
+
throw new CommandError(
|
|
1645
|
+
"The server did not provide a valid organization and upload policy; no source was uploaded."
|
|
1646
|
+
);
|
|
1647
|
+
policyFor(policy2, "");
|
|
1648
|
+
}
|
|
1649
|
+
async function syncCode(api, options) {
|
|
1650
|
+
const id = await identity(options.cwd);
|
|
1651
|
+
let policyRestarts = 0;
|
|
1652
|
+
let syncRestarts = 0;
|
|
1653
|
+
for (let restart = 0; restart < 3; restart++) {
|
|
1654
|
+
const policy2 = await api.getPolicy();
|
|
1655
|
+
policyScope(policy2);
|
|
1656
|
+
const path = statePath(
|
|
1657
|
+
api.origin,
|
|
1658
|
+
policy2.org_id,
|
|
1659
|
+
id.root,
|
|
1660
|
+
id.repoKey,
|
|
1661
|
+
options.stateDir
|
|
1662
|
+
);
|
|
1397
1663
|
try {
|
|
1398
|
-
return
|
|
1399
|
-
|
|
1400
|
-
|
|
1664
|
+
return await withSyncLock(path, async () => {
|
|
1665
|
+
const state2 = await initialState(
|
|
1666
|
+
path,
|
|
1667
|
+
api.origin,
|
|
1668
|
+
policy2.org_id,
|
|
1669
|
+
id.root,
|
|
1670
|
+
id.repoKey
|
|
1671
|
+
);
|
|
1672
|
+
const repo = await api.putRepository(id.repoKey);
|
|
1673
|
+
if (repo.repo_key !== id.repoKey)
|
|
1674
|
+
throw new CommandError(
|
|
1675
|
+
"The server resolved a different repository; no source was uploaded."
|
|
1676
|
+
);
|
|
1677
|
+
if (state2.repositoryId && state2.repositoryId !== repo.repository_id) {
|
|
1678
|
+
delete state2.viewId;
|
|
1679
|
+
delete state2.pending;
|
|
1680
|
+
delete state2.activated;
|
|
1681
|
+
}
|
|
1682
|
+
state2.repositoryId = repo.repository_id;
|
|
1683
|
+
state2.baseCommit = id.commit;
|
|
1684
|
+
await saveState(path, state2);
|
|
1685
|
+
const base2 = await baseReady(
|
|
1686
|
+
api,
|
|
1687
|
+
repo.repository_id,
|
|
1688
|
+
id.commit,
|
|
1689
|
+
options
|
|
1690
|
+
);
|
|
1691
|
+
if (base2.policy_digest && base2.policy_digest !== policy2.policy_digest)
|
|
1692
|
+
throw new HttpError(409, "The upload policy changed", {
|
|
1693
|
+
error: "stale_policy"
|
|
1694
|
+
});
|
|
1695
|
+
const overlay = await scanOverlay(id, policy2);
|
|
1696
|
+
const view2 = await api.putView(repo.repository_id, state2.viewKey);
|
|
1697
|
+
if (state2.viewId !== void 0 && state2.viewId !== view2.view_id && view2.repository_id === repo.repository_id && view2.view_key === state2.viewKey) {
|
|
1698
|
+
let removed = false;
|
|
1699
|
+
try {
|
|
1700
|
+
await api.getView(state2.viewId);
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
if (error instanceof HttpError && error.status === 404)
|
|
1703
|
+
removed = true;
|
|
1704
|
+
else throw error;
|
|
1705
|
+
}
|
|
1706
|
+
if (removed) {
|
|
1707
|
+
delete state2.viewId;
|
|
1708
|
+
delete state2.pending;
|
|
1709
|
+
delete state2.activated;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
if (view2.repository_id !== repo.repository_id || view2.view_key !== state2.viewKey || state2.viewId !== void 0 && state2.viewId !== view2.view_id)
|
|
1713
|
+
throw new CommandError(
|
|
1714
|
+
"The server view does not match this repository/worktree; no source was uploaded."
|
|
1715
|
+
);
|
|
1716
|
+
state2.viewId = view2.view_id;
|
|
1717
|
+
const forgetReplaced = async () => {
|
|
1718
|
+
delete state2.pending;
|
|
1719
|
+
await saveState(path, state2);
|
|
1720
|
+
throw new ReplacedSyncError();
|
|
1721
|
+
};
|
|
1722
|
+
const receipt = async (syncId) => {
|
|
1723
|
+
try {
|
|
1724
|
+
const previous = await api.getSync(view2.view_id, syncId);
|
|
1725
|
+
if (previous.id !== syncId)
|
|
1726
|
+
throw new CommandError("The server returned a different sync; activation remains unverified.");
|
|
1727
|
+
if (previous.state === "receiving") return null;
|
|
1728
|
+
if (!Number.isSafeInteger(previous.version) || previous.version === null || previous.version < 1)
|
|
1729
|
+
throw new CommandError("The server did not return an activation version; retry code status.");
|
|
1730
|
+
return { pending_version: previous.version, state: previous.state };
|
|
1731
|
+
} catch (error) {
|
|
1732
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
1733
|
+
if (withinActivationRetention(state2.pending?.activationAttemptedAt))
|
|
1734
|
+
return forgetReplaced();
|
|
1735
|
+
throw new CommandError("The previous activation can no longer be verified after retention. Check code status before syncing again.");
|
|
1736
|
+
}
|
|
1737
|
+
throw error;
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
let current = state2.pending;
|
|
1741
|
+
const matchesCurrent = current && current.digest === overlay.digest && current.request?.mode === "full" && current.request.base_commit === id.commit && current.request.policy_digest === policy2.policy_digest && typeof current.request.client_sync_id === "string" && JSON.stringify(current.request.changes) === JSON.stringify(overlay.changes);
|
|
1742
|
+
if (current?.syncId && current.activationAttemptedAt !== void 0 && !matchesCurrent) {
|
|
1743
|
+
const prior = current;
|
|
1744
|
+
const originalSyncId = current.syncId;
|
|
1745
|
+
let recovered = await receipt(originalSyncId);
|
|
1746
|
+
if (!recovered) {
|
|
1747
|
+
prior.activationAttemptedAt = Date.now();
|
|
1748
|
+
await saveState(path, state2);
|
|
1749
|
+
try {
|
|
1750
|
+
recovered = await api.activate(view2.view_id, originalSyncId);
|
|
1751
|
+
} catch (error) {
|
|
1752
|
+
recovered = await receipt(originalSyncId);
|
|
1753
|
+
if (!recovered) throw error;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
state2.activated = {
|
|
1757
|
+
baseCommit: prior.request.base_commit,
|
|
1758
|
+
overlayDigest: prior.digest,
|
|
1759
|
+
pendingVersion: recovered.pending_version,
|
|
1760
|
+
policyDigest: prior.request.policy_digest
|
|
1761
|
+
};
|
|
1762
|
+
delete state2.pending;
|
|
1763
|
+
await saveState(path, state2);
|
|
1764
|
+
current = void 0;
|
|
1765
|
+
}
|
|
1766
|
+
const request = matchesCurrent && current ? current.request : {
|
|
1767
|
+
client_sync_id: randomUUID2(),
|
|
1768
|
+
base_commit: id.commit,
|
|
1769
|
+
mode: "full",
|
|
1770
|
+
changes: overlay.changes,
|
|
1771
|
+
policy_digest: policy2.policy_digest
|
|
1772
|
+
};
|
|
1773
|
+
state2.pending = current && current.request === request ? current : { digest: overlay.digest, request };
|
|
1774
|
+
await saveState(path, state2);
|
|
1775
|
+
const finish = async (syncId, activated3, uploadedBlobs) => {
|
|
1776
|
+
state2.activated = {
|
|
1777
|
+
baseCommit: id.commit,
|
|
1778
|
+
overlayDigest: overlay.digest,
|
|
1779
|
+
pendingVersion: activated3.pending_version,
|
|
1780
|
+
policyDigest: policy2.policy_digest
|
|
1781
|
+
};
|
|
1782
|
+
delete state2.pending;
|
|
1783
|
+
await saveState(path, state2);
|
|
1784
|
+
return {
|
|
1785
|
+
repository_id: repo.repository_id,
|
|
1786
|
+
repo_key: id.repoKey,
|
|
1787
|
+
org_id: policy2.org_id,
|
|
1788
|
+
view_id: view2.view_id,
|
|
1789
|
+
view_key: state2.viewKey,
|
|
1790
|
+
sync_id: syncId,
|
|
1791
|
+
base_commit: id.commit,
|
|
1792
|
+
base: base2,
|
|
1793
|
+
overlay_digest: overlay.digest,
|
|
1794
|
+
changed_files: overlay.changes.length,
|
|
1795
|
+
uploaded_blobs: uploadedBlobs,
|
|
1796
|
+
excluded: overlay.excluded,
|
|
1797
|
+
warnings: overlay.warnings,
|
|
1798
|
+
policy_restarts: policyRestarts,
|
|
1799
|
+
sync_restarts: syncRestarts,
|
|
1800
|
+
...activated3
|
|
1801
|
+
};
|
|
1802
|
+
};
|
|
1803
|
+
if (state2.pending.syncId && state2.pending.activationAttemptedAt !== void 0) {
|
|
1804
|
+
const recovered = await receipt(state2.pending.syncId);
|
|
1805
|
+
if (recovered) return finish(state2.pending.syncId, recovered, 0);
|
|
1806
|
+
}
|
|
1807
|
+
const opened2 = await api.openSync(view2.view_id, request);
|
|
1808
|
+
if (opened2.rejected.length)
|
|
1809
|
+
throw new CommandError(
|
|
1810
|
+
`The server refused ${opened2.rejected.length} overlay entries; no sync was activated. Refetch the policy and check the skipped-files report.`
|
|
1811
|
+
);
|
|
1812
|
+
state2.pending.syncId = opened2.sync_id;
|
|
1813
|
+
await saveState(path, state2);
|
|
1814
|
+
if (opened2.state !== "receiving") {
|
|
1815
|
+
const recovered = await receipt(opened2.sync_id);
|
|
1816
|
+
if (recovered) return finish(opened2.sync_id, recovered, 0);
|
|
1817
|
+
throw new CommandError("The server returned an inconsistent sync state; no new version was requested.");
|
|
1818
|
+
}
|
|
1819
|
+
const missing = [...new Set(opened2.missing)];
|
|
1820
|
+
for (const hash2 of missing)
|
|
1821
|
+
if (!overlay.blobs.has(hash2))
|
|
1822
|
+
throw new CommandError(
|
|
1823
|
+
"The server requested an unexpected blob not present in this overlay; no blobs were uploaded."
|
|
1824
|
+
);
|
|
1825
|
+
let activating = false;
|
|
1826
|
+
let activated2;
|
|
1827
|
+
try {
|
|
1828
|
+
let next = 0;
|
|
1829
|
+
let failure;
|
|
1830
|
+
await Promise.all(Array.from({ length: Math.min(4, missing.length) }, async () => {
|
|
1831
|
+
while (!failure && next < missing.length) {
|
|
1832
|
+
const hash2 = missing[next++];
|
|
1833
|
+
try {
|
|
1834
|
+
await api.putBlob(hash2, overlay.blobs.get(hash2));
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
failure ??= { error };
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}));
|
|
1840
|
+
if (failure) throw failure.error;
|
|
1841
|
+
state2.pending.activationAttemptedAt = Date.now();
|
|
1842
|
+
await saveState(path, state2);
|
|
1843
|
+
activating = true;
|
|
1844
|
+
activated2 = await api.activate(view2.view_id, opened2.sync_id);
|
|
1845
|
+
} catch (error) {
|
|
1846
|
+
const replaced = error instanceof HttpError && (activating ? error.status === 404 : error.status === 409 && error.body["error"] === "unexpected_blob");
|
|
1847
|
+
if (replaced) {
|
|
1848
|
+
if (activating && !withinActivationRetention(state2.pending?.activationAttemptedAt))
|
|
1849
|
+
throw new CommandError("The activation retry exceeded its retention window. Check code status before syncing again.");
|
|
1850
|
+
return forgetReplaced();
|
|
1851
|
+
}
|
|
1852
|
+
const ambiguous = activating && (!(error instanceof HttpError) || error.status >= 500 || error.status === 429);
|
|
1853
|
+
if (ambiguous) {
|
|
1854
|
+
const recovered = await receipt(opened2.sync_id);
|
|
1855
|
+
if (recovered) return finish(opened2.sync_id, recovered, missing.length);
|
|
1856
|
+
}
|
|
1857
|
+
throw error;
|
|
1858
|
+
}
|
|
1859
|
+
return finish(opened2.sync_id, activated2, missing.length);
|
|
1860
|
+
});
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
if (stale(error) && restart < 2) {
|
|
1863
|
+
policyRestarts++;
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
if (error instanceof ReplacedSyncError && restart < 2) {
|
|
1867
|
+
syncRestarts++;
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
throw error;
|
|
1401
1871
|
}
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
const
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1872
|
+
}
|
|
1873
|
+
throw new CommandError(
|
|
1874
|
+
"The upload policy kept changing. Retry code sync after it settles."
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
async function statusCode(api, options) {
|
|
1878
|
+
const id = await identity(options.cwd);
|
|
1879
|
+
const policy2 = await api.getPolicy();
|
|
1880
|
+
policyScope(policy2);
|
|
1881
|
+
const path = statePath(
|
|
1882
|
+
api.origin,
|
|
1883
|
+
policy2.org_id,
|
|
1884
|
+
id.root,
|
|
1885
|
+
id.repoKey,
|
|
1886
|
+
options.stateDir
|
|
1887
|
+
);
|
|
1888
|
+
const state2 = await readState(
|
|
1889
|
+
path,
|
|
1890
|
+
api.origin,
|
|
1891
|
+
policy2.org_id,
|
|
1892
|
+
id.root,
|
|
1893
|
+
id.repoKey
|
|
1894
|
+
);
|
|
1895
|
+
const repository = options.skipped ? await api.putRepository(id.repoKey) : void 0;
|
|
1896
|
+
if (repository && repository.repo_key !== id.repoKey)
|
|
1897
|
+
throw new CommandError(
|
|
1898
|
+
"The server resolved a different repository; its base status cannot be trusted."
|
|
1416
1899
|
);
|
|
1417
|
-
|
|
1900
|
+
const repositoryId = repository?.repository_id ?? state2?.repositoryId;
|
|
1901
|
+
if (!state2?.viewId || repository && repositoryId !== state2.repositoryId) {
|
|
1902
|
+
const base3 = options.skipped && repositoryId ? await api.getBase(repositoryId, id.commit, true) : void 0;
|
|
1903
|
+
return {
|
|
1904
|
+
state: base3?.state === "failed" ? "failed" : base3?.state === "fetching" ? "fetching" : "not_synced",
|
|
1905
|
+
org_id: policy2.org_id,
|
|
1906
|
+
repo_key: id.repoKey,
|
|
1907
|
+
base_commit: id.commit,
|
|
1908
|
+
...base3 ? { base: base3 } : {}
|
|
1909
|
+
};
|
|
1418
1910
|
}
|
|
1419
|
-
|
|
1911
|
+
const view2 = await api.getView(state2.viewId);
|
|
1912
|
+
if (view2.repository_id !== state2.repositoryId || view2.view_id !== state2.viewId || view2.view_key !== state2.viewKey)
|
|
1913
|
+
throw new CommandError(
|
|
1914
|
+
"The server view does not match this repository/worktree; its status cannot be trusted."
|
|
1915
|
+
);
|
|
1916
|
+
const overlay = await scanOverlay(id, policy2);
|
|
1917
|
+
const current = view2.active?.base_commit === id.commit && view2.active.overlay_digest === overlay.digest && state2.activated?.baseCommit === view2.active.base_commit && state2.activated.overlayDigest === view2.active.overlay_digest && state2.activated.pendingVersion === view2.active.version && state2.activated.policyDigest === policy2.policy_digest;
|
|
1918
|
+
const base2 = options.skipped && state2.repositoryId ? await api.getBase(state2.repositoryId, id.commit, true) : void 0;
|
|
1919
|
+
return {
|
|
1920
|
+
state: base2?.state === "failed" || view2.pending?.state === "failed" ? "failed" : current ? "current" : view2.pending ? "pending" : "stale",
|
|
1921
|
+
org_id: policy2.org_id,
|
|
1922
|
+
repo_key: id.repoKey,
|
|
1923
|
+
base_commit: id.commit,
|
|
1924
|
+
...view2,
|
|
1925
|
+
local_overlay_digest: overlay.digest,
|
|
1926
|
+
excluded: overlay.excluded,
|
|
1927
|
+
warnings: overlay.warnings,
|
|
1928
|
+
...base2 ? { base: base2 } : {}
|
|
1929
|
+
};
|
|
1420
1930
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1931
|
+
|
|
1932
|
+
// src/core/code/query.ts
|
|
1933
|
+
import { z as z2 } from "zod";
|
|
1934
|
+
var hash = z2.string().regex(/^[0-9a-f]{64}$/);
|
|
1935
|
+
var queryView = z2.object({
|
|
1936
|
+
view_id: z2.string().uuid(),
|
|
1937
|
+
repository_id: z2.string().uuid(),
|
|
1938
|
+
view_key: z2.string().min(1),
|
|
1939
|
+
active: z2.object({ version: z2.number().int().positive(), base_commit: z2.string().regex(/^[0-9a-f]{40}$/), overlay_digest: hash }).passthrough(),
|
|
1940
|
+
pending: z2.object({ version: z2.number().int(), state: z2.string(), progress: z2.object({ done: z2.number().int().nonnegative(), total: z2.number().int().nonnegative() }).nullable().optional() }).passthrough().nullable()
|
|
1941
|
+
}).passthrough();
|
|
1942
|
+
var envelopeSchema = z2.object({
|
|
1943
|
+
data: z2.unknown(),
|
|
1944
|
+
meta: z2.object({ contract: z2.literal("1.3"), view: queryView, truncation: z2.object({ truncated: z2.boolean() }).passthrough() }).passthrough()
|
|
1945
|
+
}).passthrough();
|
|
1946
|
+
var citationSchema = z2.object({ path: z2.string().min(1), start_line: z2.number().int().positive(), end_line: z2.number().int().positive(), sha256: hash, version: z2.number().int().positive(), from: z2.enum(["base", "overlay"]), snippet: z2.string().optional() }).passthrough();
|
|
1947
|
+
var record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1948
|
+
var shell = (text) => "'" + text.replaceAll("'", "'\\''") + "'";
|
|
1949
|
+
function fail() {
|
|
1950
|
+
throw new CommandError("The code query API returned an invalid contract 1.3 response; no result was presented.");
|
|
1951
|
+
}
|
|
1952
|
+
function integer(value, name, min, max) {
|
|
1953
|
+
if (value !== void 0 && (!Number.isSafeInteger(value) || value < min || value > max)) throw new UsageError(`${name} must be an integer from ${min} to ${max}.`);
|
|
1954
|
+
}
|
|
1955
|
+
function validateQuery(operation, p) {
|
|
1956
|
+
const texts = [p.q, p.symbol, p.kind, p.pathPrefix, ...p.symbols ?? [], ...p.files ?? []];
|
|
1957
|
+
if (texts.some((s) => s !== void 0 && (typeof s !== "string" || s.length === 0 || Buffer.byteLength(s) > 8192 || /[\x00-\x1f\x7f-\x9f]/.test(s) || Buffer.from(s).toString("utf8") !== s))) throw new UsageError("Code query arguments must be nonempty UTF-8 text without control characters (at most 8 KiB each).");
|
|
1958
|
+
if (operation === "search" && !p.q?.trim()) throw new UsageError("usage: harness code search <query>");
|
|
1959
|
+
if (operation === "explain" && !p.symbol) throw new UsageError("usage: harness code explain <symbol-id>");
|
|
1960
|
+
if (operation === "context" && Number(Boolean(p.symbol)) + Number(Boolean(p.q?.trim())) !== 1) throw new UsageError("code context requires exactly one query or --symbol.");
|
|
1961
|
+
if (operation === "affected") {
|
|
1962
|
+
const seeds = (p.symbols?.length ?? 0) + (p.files?.length ?? 0);
|
|
1963
|
+
if (seeds < 1 || seeds > 200) throw new UsageError("code affected needs 1 to 200 --symbol/--file seeds.");
|
|
1964
|
+
if (Buffer.byteLength(JSON.stringify(p)) > 24e4) throw new UsageError("Code affected seeds exceed the request size limit; narrow the selection.");
|
|
1965
|
+
}
|
|
1966
|
+
integer(p.limit, "limit", 1, 100);
|
|
1967
|
+
integer(p.depth, "depth", 1, 6);
|
|
1968
|
+
integer(p.budgetTokens, "budget-tokens", 100, operation === "context" ? 32e3 : 16e3);
|
|
1969
|
+
}
|
|
1970
|
+
function command(op, p) {
|
|
1971
|
+
const parts = ["harness", "code", op];
|
|
1972
|
+
if (p.q !== void 0) parts.push(shell(p.q));
|
|
1973
|
+
if (op === "explain" && p.symbol) parts.push(shell(p.symbol));
|
|
1974
|
+
else if (p.symbol) parts.push("--symbol", shell(p.symbol));
|
|
1975
|
+
for (const id of p.symbols ?? []) parts.push("--symbol", shell(id));
|
|
1976
|
+
for (const file of p.files ?? []) parts.push("--file", shell(file));
|
|
1977
|
+
for (const [key, value] of [["limit", p.limit], ["depth", p.depth], ["budget-tokens", p.budgetTokens], ["kind", p.kind], ["path-prefix", p.pathPrefix]])
|
|
1978
|
+
if (value !== void 0) parts.push(`--${key}`, typeof value === "number" ? String(value) : shell(value));
|
|
1979
|
+
if (p.includeAmbiguous) parts.push("--include-ambiguous");
|
|
1980
|
+
if (p.includeTypes) parts.push("--include-types");
|
|
1981
|
+
if (p.worktree) parts.push("--path", shell(p.worktree));
|
|
1982
|
+
return named(parts.join(" "));
|
|
1983
|
+
}
|
|
1984
|
+
function nextCommand(op, p, next) {
|
|
1985
|
+
if (!record(next)) return null;
|
|
1986
|
+
const updated = { ...p };
|
|
1987
|
+
let changed = false;
|
|
1988
|
+
for (const [key, field] of [["limit", "limit"], ["depth", "depth"], ["budget_tokens", "budgetTokens"]]) {
|
|
1989
|
+
if (key === "limit" && op !== "search" || key === "depth" && op !== "affected" || key === "budget_tokens" && op !== "explain" && op !== "context") continue;
|
|
1990
|
+
if (typeof next[key] === "number") {
|
|
1991
|
+
updated[field] = next[key];
|
|
1992
|
+
changed = true;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
if (!changed) return null;
|
|
1424
1996
|
try {
|
|
1425
|
-
|
|
1426
|
-
writeFileSync(temporary, `${process.pid}
|
|
1427
|
-
`);
|
|
1997
|
+
validateQuery(op, updated);
|
|
1428
1998
|
} catch {
|
|
1429
1999
|
return null;
|
|
1430
2000
|
}
|
|
2001
|
+
return command(op, updated);
|
|
2002
|
+
}
|
|
2003
|
+
function renderQuery(operation, params, raw) {
|
|
2004
|
+
const parsed = envelopeSchema.safeParse(raw);
|
|
2005
|
+
if (!parsed.success) fail();
|
|
2006
|
+
const response = parsed.data;
|
|
2007
|
+
if (operation === "search" ? !Array.isArray(response.data) : !record(response.data)) fail();
|
|
2008
|
+
const { view: view2, truncation } = response.meta;
|
|
2009
|
+
const guidance = [];
|
|
2010
|
+
const lines = [`View ${view2.view_id}: version ${view2.active.version}; base ${view2.active.base_commit}; overlay ${view2.active.overlay_digest}`];
|
|
2011
|
+
if (view2.pending) lines.push(`Pending version ${view2.pending.version}: ${clean(view2.pending.state)}${view2.pending.progress ? ` (${view2.pending.progress.done}/${view2.pending.progress.total})` : ""}`);
|
|
2012
|
+
let visited = 0;
|
|
2013
|
+
const buckets = {};
|
|
2014
|
+
const walk = (value, label3, depth, enclosingSymbol, countBuckets = true) => {
|
|
2015
|
+
if (++visited > 1e5 || depth > 32) fail();
|
|
2016
|
+
if (record(value)) {
|
|
2017
|
+
const symbol = typeof value["symbol_id"] === "string" ? value["symbol_id"] : enclosingSymbol;
|
|
2018
|
+
if ("path" in value && "start_line" in value && "sha256" in value) {
|
|
2019
|
+
const citation = citationSchema.safeParse(value);
|
|
2020
|
+
if (!citation.success || citation.data.end_line < citation.data.start_line || citation.data.version !== view2.active.version) fail();
|
|
2021
|
+
const c = citation.data;
|
|
2022
|
+
lines.push(`${label3}: ${clean(c.path)}:${c.start_line}${c.end_line === c.start_line ? "" : `-${c.end_line}`}@${c.sha256} [${c.from}, version ${c.version}]`);
|
|
2023
|
+
const context = command("context", { ...symbol || params.symbol ? { symbol: symbol ?? params.symbol } : { q: params.q ?? "symbol name" }, worktree: params.worktree });
|
|
2024
|
+
lines.push(c.snippet !== void 0 ? c.snippet.split("\n").map(clean).join("\n") : `Snippet not supplied; retrieve indexed text: ${context}`);
|
|
2025
|
+
for (const [key, extra] of Object.entries(value)) if (!["path", "start_line", "end_line", "sha256", "version", "from", "snippet"].includes(key)) walk(extra, `${label3}.${clean(key)}`, depth + 1, symbol, countBuckets);
|
|
2026
|
+
} else for (const [key, child] of Object.entries(value)) walk(child, `${label3}.${clean(key)}`, depth + 1, symbol, countBuckets);
|
|
2027
|
+
} else if (Array.isArray(value)) {
|
|
2028
|
+
if (countBuckets && label3 !== "data") buckets[label3.slice(5)] = value.length;
|
|
2029
|
+
if (!value.length) lines.push(`${label3}: []`);
|
|
2030
|
+
value.forEach((v, i) => walk(v, `${label3}[${i}]`, depth + 1, enclosingSymbol, false));
|
|
2031
|
+
} else lines.push(`${label3}: ${clean(typeof value === "string" ? value : JSON.stringify(value))}`);
|
|
2032
|
+
};
|
|
2033
|
+
walk(response.data, "data", 0);
|
|
2034
|
+
const counts = Array.isArray(response.data) ? response.data.length : buckets;
|
|
2035
|
+
const total = typeof truncation["total"] === "number" ? truncation["total"] : response.meta["total"];
|
|
2036
|
+
lines.push(`Truncated: ${truncation.truncated ? "yes" : "no"}; returned: ${JSON.stringify(counts)}; total: ${typeof total === "number" ? total : "unknown (not supplied by server)"}`);
|
|
2037
|
+
if (truncation.truncated) {
|
|
2038
|
+
lines.push(`Truncation metadata: ${JSON.stringify(truncation)}`);
|
|
2039
|
+
const next = nextCommand(operation, params, truncation["next"]);
|
|
2040
|
+
guidance.push(next ? `Next: ${next}` : `Next: narrow the query or use rg -n; ${typeof truncation["hint"] === "string" ? clean(truncation["hint"]) : "the server supplied no larger request."}`);
|
|
2041
|
+
}
|
|
2042
|
+
const coverage = record(response.data) ? response.data["not_tracked"] : void 0;
|
|
2043
|
+
if (Array.isArray(coverage) && coverage.length) {
|
|
2044
|
+
for (const entry of coverage) if (record(entry)) guidance.push(`Coverage${entry["partial"] === true ? " (partial)" : ""}: ${clean(entry["reason"])}`);
|
|
2045
|
+
}
|
|
2046
|
+
if (Array.isArray(coverage) && coverage.length || Array.isArray(response.data) && response.data.length === 0) {
|
|
2047
|
+
const symbol = record(response.data) && record(response.data["symbol"]) ? response.data["symbol"]["name"] : void 0;
|
|
2048
|
+
guidance.push(`Search locally: rg -n --fixed-strings -- ${shell(params.q ?? (typeof symbol === "string" ? symbol : "symbol name"))} .`);
|
|
2049
|
+
}
|
|
2050
|
+
return { json: { ...response, guidance }, human: lines.concat(guidance).join("\n") + "\n" };
|
|
2051
|
+
}
|
|
2052
|
+
async function queryCode(api, operation, params, options) {
|
|
2053
|
+
validateQuery(operation, params);
|
|
2054
|
+
const id = await identity(options.cwd);
|
|
2055
|
+
const policy2 = await api.getPolicy();
|
|
2056
|
+
if (!z2.string().uuid().safeParse(policy2.org_id).success) fail();
|
|
2057
|
+
const path = statePath(api.origin, policy2.org_id, id.root, id.repoKey, options.stateDir);
|
|
2058
|
+
const state2 = await readState(path, api.origin, policy2.org_id, id.root, id.repoKey);
|
|
2059
|
+
if (!state2?.viewId || !state2.repositoryId) throw new CommandError(`This worktree has no code view. Run harness code sync --path ${shell(id.root)} first.`);
|
|
2060
|
+
const view2 = await api.getView(state2.viewId);
|
|
2061
|
+
const matches = (v) => v.view_id === state2.viewId && v.repository_id === state2.repositoryId && v.view_key === state2.viewKey;
|
|
2062
|
+
if (!matches(view2)) throw new CommandError("The code view does not match this repository/worktree; no query was sent.");
|
|
2063
|
+
if (!view2.active) throw new CommandError(`The view has no indexed version yet. Check harness code status --path ${shell(id.root)} until indexing finishes.`);
|
|
1431
2064
|
try {
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
const aged = Date.now() - holder.mtimeMs > LOCK_STALE_MS;
|
|
1440
|
-
const abandoned = holder.pid === null ? aged : !running(holder.pid) || aged;
|
|
1441
|
-
if (!abandoned) return null;
|
|
1442
|
-
try {
|
|
1443
|
-
rmSync(path, { force: true });
|
|
1444
|
-
} catch {
|
|
1445
|
-
return null;
|
|
1446
|
-
}
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
return null;
|
|
1450
|
-
} finally {
|
|
1451
|
-
try {
|
|
1452
|
-
rmSync(temporary, { force: true });
|
|
1453
|
-
} catch {
|
|
1454
|
-
}
|
|
2065
|
+
const raw = await api.query(state2.viewId, operation, params);
|
|
2066
|
+
const result = renderQuery(operation, { ...params, worktree: id.root }, raw);
|
|
2067
|
+
if (!matches(result.json.meta.view)) throw new CommandError("The query view does not match this repository/worktree; no result was presented.");
|
|
2068
|
+
return result;
|
|
2069
|
+
} catch (error) {
|
|
2070
|
+
if (error instanceof HttpError && error.status === 404 && operation !== "search") throw new CommandError(`The code symbol or view was not found. Search again with ${command("search", { q: "symbol name", worktree: id.root })}; symbol IDs are opaque and can change after edits. Check code status for a removed view.`);
|
|
2071
|
+
throw error;
|
|
1455
2072
|
}
|
|
1456
2073
|
}
|
|
1457
|
-
|
|
2074
|
+
|
|
2075
|
+
// src/commands/code/index.ts
|
|
2076
|
+
function enterWorktree(options) {
|
|
2077
|
+
const cwd = process.cwd();
|
|
2078
|
+
const before = statSync(".");
|
|
2079
|
+
const declared = statSync(cwd);
|
|
2080
|
+
if (before.dev !== declared.dev || before.ino !== declared.ino)
|
|
2081
|
+
throw new LocalError("Cannot enter the code worktree safely.");
|
|
2082
|
+
const supplied = options.path ?? cwd;
|
|
2083
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(supplied) || Buffer.from(supplied, "utf8").toString("utf8") !== supplied || options.path !== void 0 && supplied.includes("\uFFFD"))
|
|
2084
|
+
throw new LocalError("Cannot enter the code worktree safely.");
|
|
2085
|
+
const path = resolve2(supplied);
|
|
1458
2086
|
try {
|
|
1459
|
-
|
|
1460
|
-
|
|
2087
|
+
const target = statSync(path);
|
|
2088
|
+
if (options.path === void 0 && (before.dev !== target.dev || before.ino !== target.ino))
|
|
2089
|
+
throw new Error("current directory identity changed");
|
|
2090
|
+
process.chdir(path);
|
|
2091
|
+
const entered = statSync(".");
|
|
2092
|
+
if (entered.dev !== target.dev || entered.ino !== target.ino)
|
|
2093
|
+
throw new Error("worktree identity changed");
|
|
1461
2094
|
} catch {
|
|
2095
|
+
throw new LocalError("Cannot enter the code worktree; check --path.");
|
|
1462
2096
|
}
|
|
2097
|
+
return { ...options, path };
|
|
1463
2098
|
}
|
|
1464
|
-
function
|
|
2099
|
+
async function sync(ctx, options = {}) {
|
|
1465
2100
|
try {
|
|
1466
|
-
const
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
return
|
|
2101
|
+
const result = await syncCode(new HttpCodeApi(ctx.config), {
|
|
2102
|
+
cwd: options.path ?? process.cwd(),
|
|
2103
|
+
skipped: options.skipped
|
|
2104
|
+
});
|
|
2105
|
+
return {
|
|
2106
|
+
json: result,
|
|
2107
|
+
exitCode: result.state === "failed" ? 1 : 0,
|
|
2108
|
+
human: `${clean(result.repo_key)}: ${clean(result.state)} version ${result.pending_version}; uploaded ${result.uploaded_blobs} overlay blobs.
|
|
2109
|
+
View: ${result.view_id}
|
|
2110
|
+
${result.excluded.length} overlay files skipped; ${result.base.excluded_count ?? 0} base files skipped.${options.skipped ? "\n" + (result.base.skipped ?? []).map((s) => `${clean(s.path)}: ${clean(s.reason)}`).join("\n") : ""}
|
|
2111
|
+
${result.warnings.map((w) => `Warning: ${clean(w)}
|
|
2112
|
+
`).join("")}`
|
|
2113
|
+
};
|
|
2114
|
+
} catch (error) {
|
|
2115
|
+
throw error instanceof HttpError ? describeCodeError(error) : error;
|
|
1471
2116
|
}
|
|
1472
2117
|
}
|
|
1473
|
-
function
|
|
1474
|
-
if (pid === null) return false;
|
|
2118
|
+
async function status2(ctx, options = {}) {
|
|
1475
2119
|
try {
|
|
1476
|
-
|
|
1477
|
-
|
|
2120
|
+
const result = await statusCode(new HttpCodeApi(ctx.config), {
|
|
2121
|
+
cwd: options.path ?? process.cwd(),
|
|
2122
|
+
skipped: options.skipped
|
|
2123
|
+
});
|
|
2124
|
+
const report = "base" in result ? result.base : void 0;
|
|
2125
|
+
const pending = "pending" in result ? result.pending : null;
|
|
2126
|
+
const serverError = pending?.error ?? report?.error;
|
|
2127
|
+
const failure = serverError && typeof serverError === "object" && "message" in serverError && typeof serverError.message === "string" ? clean(serverError.message) : "";
|
|
2128
|
+
return {
|
|
2129
|
+
json: result,
|
|
2130
|
+
exitCode: result.state === "failed" ? 1 : 0,
|
|
2131
|
+
human: `${clean(result.repo_key)}: ${result.state.replace("_", " ")}${failure ? ": " + failure : ""}
|
|
2132
|
+
${report?.skipped?.map((s) => `${clean(s.path)}: ${clean(s.reason)}
|
|
2133
|
+
`).join("") ?? ""}${("warnings" in result ? result.warnings : [])?.map((w) => `Warning: ${clean(w)}
|
|
2134
|
+
`).join("") ?? ""}`
|
|
2135
|
+
};
|
|
1478
2136
|
} catch (error) {
|
|
1479
|
-
|
|
2137
|
+
throw error instanceof HttpError ? describeCodeError(error) : error;
|
|
1480
2138
|
}
|
|
1481
2139
|
}
|
|
1482
|
-
function
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
return !truthy(env["CI"]) && !truthy(env["HARNESS_NO_UPDATE_CHECK"]);
|
|
1489
|
-
}
|
|
1490
|
-
function autoUpgradeEnabled(env) {
|
|
1491
|
-
if (!checksEnabled(env)) return false;
|
|
1492
|
-
const explicit = env["HARNESS_AUTO_UPGRADE"];
|
|
1493
|
-
if (explicit !== void 0 && explicit.trim() !== "") return truthy(explicit);
|
|
1494
|
-
return true;
|
|
2140
|
+
async function query(ctx, operation, params, options = {}) {
|
|
2141
|
+
try {
|
|
2142
|
+
return await queryCode(new HttpCodeApi(ctx.config), operation, params, { cwd: options.path ?? process.cwd() });
|
|
2143
|
+
} catch (error) {
|
|
2144
|
+
throw error instanceof HttpError ? describeCodeError(error) : error;
|
|
2145
|
+
}
|
|
1495
2146
|
}
|
|
1496
2147
|
|
|
1497
2148
|
// src/cli.ts
|
|
1498
2149
|
function promptLine(question) {
|
|
1499
|
-
const readline =
|
|
2150
|
+
const readline = createInterface({
|
|
1500
2151
|
input: process.stdin,
|
|
1501
2152
|
output: process.stderr
|
|
1502
2153
|
});
|
|
1503
|
-
return new Promise((
|
|
2154
|
+
return new Promise((resolve3) => {
|
|
1504
2155
|
readline.question(question, (answer) => {
|
|
1505
2156
|
readline.close();
|
|
1506
|
-
|
|
2157
|
+
resolve3(answer);
|
|
1507
2158
|
});
|
|
1508
2159
|
});
|
|
1509
2160
|
}
|
|
1510
|
-
function exitWith(
|
|
1511
|
-
const drain = (stream) => new Promise((
|
|
2161
|
+
function exitWith(code2) {
|
|
2162
|
+
const drain = (stream) => new Promise((resolve3) => stream.write("", () => resolve3()));
|
|
1512
2163
|
return Promise.all([drain(process.stdout), drain(process.stderr)]).then(
|
|
1513
|
-
() => process.exit(
|
|
2164
|
+
() => process.exit(code2)
|
|
1514
2165
|
);
|
|
1515
2166
|
}
|
|
1516
2167
|
guardBrokenPipes();
|
|
@@ -1525,13 +2176,13 @@ program.name(invokedAs).description(
|
|
|
1525
2176
|
writeOut: (text) => process.stdout.write(named(text, invokedAs)),
|
|
1526
2177
|
writeErr: (text) => writeStderr(text)
|
|
1527
2178
|
});
|
|
1528
|
-
function
|
|
2179
|
+
function integer2(value) {
|
|
1529
2180
|
if (!/^\d+$/.test(value))
|
|
1530
2181
|
throw new InvalidArgumentError("expected a non-negative integer");
|
|
1531
2182
|
return Number.parseInt(value, 10);
|
|
1532
2183
|
}
|
|
1533
2184
|
function activityLimit(value) {
|
|
1534
|
-
const limit =
|
|
2185
|
+
const limit = integer2(value);
|
|
1535
2186
|
if (limit < 1 || limit > ACTIVITY_MAX_LIMIT) {
|
|
1536
2187
|
throw new InvalidArgumentError(
|
|
1537
2188
|
`expected an integer from 1 to ${ACTIVITY_MAX_LIMIT}`
|
|
@@ -1564,7 +2215,23 @@ function interactive() {
|
|
|
1564
2215
|
const ci = process.env["CI"];
|
|
1565
2216
|
return process.stdout.isTTY === true && (ci === void 0 || ci === "");
|
|
1566
2217
|
}
|
|
1567
|
-
|
|
2218
|
+
function canConfirmRepoLogin() {
|
|
2219
|
+
return process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
2220
|
+
}
|
|
2221
|
+
async function confirmRepoLogin(baseUrl, source, path) {
|
|
2222
|
+
if (source !== "repo-config" || isHostTrusted(baseUrl)) return true;
|
|
2223
|
+
const answer = await promptLine(
|
|
2224
|
+
`This repo's config (${path ?? ".harness-cli/config.json"}) targets ${baseUrl} \u2014 log in there? [y/N] `
|
|
2225
|
+
);
|
|
2226
|
+
if (answer.trim().toLowerCase() !== "y") {
|
|
2227
|
+
writeStderr(`harness: not logging in to ${baseUrl} \u2014 set HARNESS_API_URL to override the repo config
|
|
2228
|
+
`);
|
|
2229
|
+
return false;
|
|
2230
|
+
}
|
|
2231
|
+
trustHost(baseUrl);
|
|
2232
|
+
return true;
|
|
2233
|
+
}
|
|
2234
|
+
async function withConfig(jsonFlag, mutating, run3) {
|
|
1568
2235
|
const resolved = resolveConfig();
|
|
1569
2236
|
for (const warning of resolved.warnings) writeStderr(warning + "\n");
|
|
1570
2237
|
if (resolved.kind === "error") {
|
|
@@ -1575,22 +2242,14 @@ async function withConfig(jsonFlag, mutating, run2) {
|
|
|
1575
2242
|
if (resolved.kind === "needs-login") {
|
|
1576
2243
|
if (!interactive()) {
|
|
1577
2244
|
writeStderr(loginHelp(resolved.baseUrl));
|
|
2245
|
+
if (resolved.org !== void 0) writeStderr(`Choose organization ${resolved.org} when logging in.
|
|
2246
|
+
`);
|
|
1578
2247
|
return exitWith(2);
|
|
1579
2248
|
}
|
|
1580
|
-
if (resolved.hostSource
|
|
1581
|
-
|
|
1582
|
-
`This repo's config (${resolved.repoConfigPath ?? ".harness-cli/config.json"}) targets ${resolved.baseUrl} \u2014 log in there? [y/N] `
|
|
1583
|
-
);
|
|
1584
|
-
if (answer.trim().toLowerCase() !== "y") {
|
|
1585
|
-
writeStderr(
|
|
1586
|
-
`harness: not logging in to ${resolved.baseUrl} \u2014 set HARNESS_API_URL to override the repo config
|
|
1587
|
-
`
|
|
1588
|
-
);
|
|
1589
|
-
return exitWith(2);
|
|
1590
|
-
}
|
|
1591
|
-
trustHost(resolved.baseUrl);
|
|
2249
|
+
if (!await confirmRepoLogin(resolved.baseUrl, resolved.hostSource, resolved.repoConfigPath)) {
|
|
2250
|
+
return exitWith(2);
|
|
1592
2251
|
}
|
|
1593
|
-
const loginResult = await login(resolved.baseUrl, {});
|
|
2252
|
+
const loginResult = await login(resolved.baseUrl, resolved.org === void 0 ? {} : { org: resolved.org });
|
|
1594
2253
|
if (!loginResult.ok) {
|
|
1595
2254
|
writeStderr(`harness: ${clean(loginResult.message)}
|
|
1596
2255
|
`);
|
|
@@ -1605,25 +2264,25 @@ async function withConfig(jsonFlag, mutating, run2) {
|
|
|
1605
2264
|
);
|
|
1606
2265
|
return exitWith(1);
|
|
1607
2266
|
}
|
|
1608
|
-
return execute(again.config, jsonFlag, mutating,
|
|
2267
|
+
return execute(again.config, jsonFlag, mutating, run3);
|
|
1609
2268
|
}
|
|
1610
|
-
return execute(resolved.config, jsonFlag, mutating,
|
|
2269
|
+
return execute(resolved.config, jsonFlag, mutating, run3);
|
|
1611
2270
|
}
|
|
1612
|
-
async function execute(config, jsonFlag, mutating,
|
|
2271
|
+
async function execute(config, jsonFlag, mutating, run3) {
|
|
1613
2272
|
const client = new Client(config);
|
|
1614
2273
|
const hostNotice = mutating ? `\u2192 ${config.baseUrl}
|
|
1615
2274
|
` : "";
|
|
1616
2275
|
try {
|
|
1617
|
-
const output = await
|
|
2276
|
+
const output = await run3({ client, config });
|
|
1618
2277
|
writeStderr(hostNotice);
|
|
1619
2278
|
if (output.notice !== void 0) writeStderr(output.notice);
|
|
1620
2279
|
process.stdout.write(jsonFlag ? json(output.json) : output.human);
|
|
1621
2280
|
return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
|
|
1622
2281
|
} catch (error) {
|
|
1623
|
-
return
|
|
2282
|
+
return fail2(explainExpertRefusal(error, config.token), hostNotice);
|
|
1624
2283
|
}
|
|
1625
2284
|
}
|
|
1626
|
-
async function
|
|
2285
|
+
async function fail2(error, hostNotice) {
|
|
1627
2286
|
if (error instanceof UsageError) {
|
|
1628
2287
|
writeStderr(error.message + "\n");
|
|
1629
2288
|
return exitWith(2);
|
|
@@ -1667,7 +2326,7 @@ async function fail(error, hostNotice) {
|
|
|
1667
2326
|
program.command("ready").description("issues ready to work, highest priority first").option(
|
|
1668
2327
|
"--claim",
|
|
1669
2328
|
"claim the team's next ready, unassigned issue in one server-side request (team from --team, HARNESS_TEAM or the repo config; no paging flags)"
|
|
1670
|
-
).option("--team <key>", "team filter; with --claim, the team to claim from").option("--limit <n>", "page size",
|
|
2329
|
+
).option("--team <key>", "team filter; with --claim, the team to claim from").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
1671
2330
|
if (opts.claim === true) requireReadyClaimArgs(opts);
|
|
1672
2331
|
await withConfig(
|
|
1673
2332
|
opts.json === true,
|
|
@@ -1675,7 +2334,7 @@ program.command("ready").description("issues ready to work, highest priority fir
|
|
|
1675
2334
|
(ctx) => ready(ctx, opts)
|
|
1676
2335
|
);
|
|
1677
2336
|
});
|
|
1678
|
-
program.command("blocked").description("issues waiting on a blocker").option("--team <key>", "team filter").option("--limit <n>", "page size",
|
|
2337
|
+
program.command("blocked").description("issues waiting on a blocker").option("--team <key>", "team filter").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
1679
2338
|
await withConfig(
|
|
1680
2339
|
opts.json === true,
|
|
1681
2340
|
false,
|
|
@@ -1688,7 +2347,7 @@ program.command("list").description("list issues with filters").option("--status
|
|
|
1688
2347
|
).option("--created-by <who>", "me or a creator user id").option(
|
|
1689
2348
|
"--involved <who>",
|
|
1690
2349
|
"me for issues created by, assigned to, or mentioning you"
|
|
1691
|
-
).option("--priority <name>", "urgent | high | medium | low | none").option("--label <name>", "label filter").option("--type <type>", "task | bug | feature | epic").option("--search <query>", "full-text search").option("--team <key>", "team filter").option("--parent <id>", "children of an epic").option("--scope <scope>", "all | archived").option("--limit <n>", "page size",
|
|
2350
|
+
).option("--priority <name>", "urgent | high | medium | low | none").option("--label <name>", "label filter").option("--type <type>", "task | bug | feature | epic").option("--search <query>", "full-text search").option("--team <key>", "team filter").option("--parent <id>", "children of an epic").option("--scope <scope>", "all | archived").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
1692
2351
|
await withConfig(
|
|
1693
2352
|
opts.json === true,
|
|
1694
2353
|
false,
|
|
@@ -1697,16 +2356,16 @@ program.command("list").description("list issues with filters").option("--status
|
|
|
1697
2356
|
});
|
|
1698
2357
|
program.command("search").description(
|
|
1699
2358
|
"search by meaning \u2014 ranked, exact identifier first; use list --search for paginated filtering"
|
|
1700
|
-
).argument("[query...]").option("--limit <n>", "maximum results (default 10, capped at 20)",
|
|
2359
|
+
).argument("[query...]").option("--limit <n>", "maximum results (default 10, capped at 20)", integer2).option(
|
|
1701
2360
|
"--page <n>",
|
|
1702
2361
|
"(deprecated, ignored \u2014 results are ranked and capped)",
|
|
1703
|
-
|
|
2362
|
+
integer2
|
|
1704
2363
|
).option("--all", "(deprecated, ignored \u2014 results are ranked and capped)").option("--json", "emit JSON").action(
|
|
1705
|
-
async (
|
|
2364
|
+
async (query2, opts) => {
|
|
1706
2365
|
await withConfig(
|
|
1707
2366
|
opts.json === true,
|
|
1708
2367
|
false,
|
|
1709
|
-
(ctx) => search(ctx,
|
|
2368
|
+
(ctx) => search(ctx, query2, opts)
|
|
1710
2369
|
);
|
|
1711
2370
|
}
|
|
1712
2371
|
);
|
|
@@ -1926,32 +2585,37 @@ plan.command("create").description("create a plan from a JSON or YAML file (the
|
|
|
1926
2585
|
return planCreate(ctx, readPlanFile(opts.file), { ...opts, team: team2 });
|
|
1927
2586
|
});
|
|
1928
2587
|
});
|
|
1929
|
-
plan.command("
|
|
2588
|
+
plan.command("list").description("a team's plans, newest first: root, status, held, revision and title").option("--team <key>", "the team whose issues root the plans (or HARNESS_TEAM)").option("--status <status>", PLAN_STATUSES.join(" | ")).option("--limit <n>", `maximum plans (default 50, at most ${PLAN_LIST_MAX_LIMIT}; there are no pages)`, integer2).option("--json", "emit JSON").action(async (opts) => {
|
|
2589
|
+
requirePlanListArgs(opts);
|
|
2590
|
+
await withConfig(opts.json === true, false, (ctx) => planList(ctx, opts));
|
|
2591
|
+
});
|
|
2592
|
+
var PLAN_ARGUMENT = "the plan's id, or its root issue's identifier (e.g. FR-1)";
|
|
2593
|
+
plan.command("show").description("a plan's status, budgets, revision, steps, edges and wait conditions").argument("[plan]", PLAN_ARGUMENT).option("--json", "emit JSON").action(async (id, opts) => {
|
|
1930
2594
|
await withConfig(opts.json === true, false, (ctx) => {
|
|
1931
|
-
if (id === void 0) throw new UsageError("usage: harness plan show <plan-id>");
|
|
2595
|
+
if (id === void 0) throw new UsageError("usage: harness plan show <plan-id-or-root>");
|
|
1932
2596
|
return planShow(ctx, id);
|
|
1933
2597
|
});
|
|
1934
2598
|
});
|
|
1935
|
-
plan.command("edit").description("change step executors and keys, and budgets, from a JSON or YAML file").argument("[
|
|
2599
|
+
plan.command("edit").description("change step executors and keys, and budgets, from a JSON or YAML file").argument("[plan]", PLAN_ARGUMENT).option("--file <path>", 'the edit: .json is JSON, anything else YAML ("-" for stdin)').option("--json", "emit JSON").action(async (id, opts) => {
|
|
1936
2600
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1937
2601
|
if (id === void 0 || opts.file === void 0) {
|
|
1938
|
-
throw new UsageError("usage: harness plan edit <plan-id> --file <edits.json|edits.yaml>");
|
|
2602
|
+
throw new UsageError("usage: harness plan edit <plan-id-or-root> --file <edits.json|edits.yaml>");
|
|
1939
2603
|
}
|
|
1940
|
-
|
|
2604
|
+
requirePlanRef(id);
|
|
1941
2605
|
return planEdit(ctx, id, readPlanFile(opts.file));
|
|
1942
2606
|
});
|
|
1943
2607
|
});
|
|
1944
|
-
plan.command("release").description("release a plan as yourself: freeze its next revision and let its ready steps run").argument("[
|
|
2608
|
+
plan.command("release").description("release a plan as yourself: freeze its next revision and let its ready steps run").argument("[plan]", PLAN_ARGUMENT).option("--collaborator <who>", "a person of this organization (id or email) for the plan room (repeatable)", collect, []).option("--retry-budget <n>", "retries per failed Operator step", integer2).option("--spend-budget-cents <n>", "the plan's spend budget; needed to release Operator steps", integer2).option("--json", "emit JSON").action(
|
|
1945
2609
|
async (id, opts) => {
|
|
1946
2610
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1947
|
-
if (id === void 0) throw new UsageError("usage: harness plan release <plan-id>");
|
|
2611
|
+
if (id === void 0) throw new UsageError("usage: harness plan release <plan-id-or-root>");
|
|
1948
2612
|
return planRelease(ctx, id, { ...opts, collaborators: opts.collaborator });
|
|
1949
2613
|
});
|
|
1950
2614
|
}
|
|
1951
2615
|
);
|
|
1952
|
-
plan.command("hold").description("pause every step of a plan until it is released again").argument("[
|
|
2616
|
+
plan.command("hold").description("pause every step of a plan until it is released again").argument("[plan]", PLAN_ARGUMENT).option("--json", "emit JSON").action(async (id, opts) => {
|
|
1953
2617
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
1954
|
-
if (id === void 0) throw new UsageError("usage: harness plan hold <plan-id>");
|
|
2618
|
+
if (id === void 0) throw new UsageError("usage: harness plan hold <plan-id-or-root>");
|
|
1955
2619
|
return planHold(ctx, id);
|
|
1956
2620
|
});
|
|
1957
2621
|
});
|
|
@@ -1982,7 +2646,7 @@ var NO_OBJECTIVE_TEXT = "By reference only: never the objective's text, which th
|
|
|
1982
2646
|
program.command("jobs").description("Operator jobs you see in the workbench, newest first").option(
|
|
1983
2647
|
"--status <states>",
|
|
1984
2648
|
`comma-separated job states (${JOB_STATES.join(", ")}) or open | waiting | done`
|
|
1985
|
-
).option("--mine", "only jobs you own").option(`--limit <n>`, `jobs per page (default 20, max ${JOBS_MAX_LIMIT})`,
|
|
2649
|
+
).option("--mine", "only jobs you own").option(`--limit <n>`, `jobs per page (default 20, max ${JOBS_MAX_LIMIT})`, integer2).option("--all", `follow every page (up to ${JOBS_MAX_PAGES})`).option("--json", "emit JSON").addHelpText("after", `
|
|
1986
2650
|
${NO_OBJECTIVE_TEXT}
|
|
1987
2651
|
`).action(async (opts) => {
|
|
1988
2652
|
requireJobsArgs(opts);
|
|
@@ -1999,7 +2663,7 @@ ${NO_OBJECTIVE_TEXT}
|
|
|
1999
2663
|
(ctx) => jobShow(ctx, id, { changes: opts.changes, people: opts.people })
|
|
2000
2664
|
);
|
|
2001
2665
|
});
|
|
2002
|
-
program.command("watch").description("follow an Operator job until it completes, fails, or waits for a person").argument("[id]").option("--after <seq>", "start after this event sequence (default 0: the whole history)",
|
|
2666
|
+
program.command("watch").description("follow an Operator job until it completes, fails, or waits for a person").argument("[id]").option("--after <seq>", "start after this event sequence (default 0: the whole history)", integer2).option("--through-waits", "keep following while a person has to act, instead of stopping").option("--json", "emit one JSON document when the watch ends").addHelpText(
|
|
2003
2667
|
"after",
|
|
2004
2668
|
`
|
|
2005
2669
|
Polls the job's events every 2 s, and every 10 s while the job is paused or
|
|
@@ -2220,7 +2884,7 @@ program.command("attach-image").description("upload an image; prints markdown to
|
|
|
2220
2884
|
});
|
|
2221
2885
|
});
|
|
2222
2886
|
var project = program.command("project").description("projects");
|
|
2223
|
-
project.command("list").option("--search <query>", "full-text search").option("--state <state>", "project state filter").option("--limit <n>", "page size",
|
|
2887
|
+
project.command("list").option("--search <query>", "full-text search").option("--state <state>", "project state filter").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
2224
2888
|
await withConfig(
|
|
2225
2889
|
opts.json === true,
|
|
2226
2890
|
false,
|
|
@@ -2268,7 +2932,7 @@ milestone.command("create").option("--project <id>", "project id (required)").op
|
|
|
2268
2932
|
);
|
|
2269
2933
|
});
|
|
2270
2934
|
var doc = program.command("doc").description("documents");
|
|
2271
|
-
doc.command("list").option("--search <query>", "full-text search").option("--limit <n>", "page size",
|
|
2935
|
+
doc.command("list").option("--search <query>", "full-text search").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
|
|
2272
2936
|
await withConfig(
|
|
2273
2937
|
opts.json === true,
|
|
2274
2938
|
false,
|
|
@@ -2347,15 +3011,15 @@ link.command("rm").argument("[issue]").argument("[id-or-url]").option("--json",
|
|
|
2347
3011
|
});
|
|
2348
3012
|
}
|
|
2349
3013
|
);
|
|
2350
|
-
var
|
|
2351
|
-
|
|
3014
|
+
var label2 = program.command("label").description("labels");
|
|
3015
|
+
label2.command("list").option("--json", "emit JSON").action(async (opts) => {
|
|
2352
3016
|
await withConfig(
|
|
2353
3017
|
opts.json === true,
|
|
2354
3018
|
false,
|
|
2355
3019
|
(ctx) => labelList(ctx)
|
|
2356
3020
|
);
|
|
2357
3021
|
});
|
|
2358
|
-
|
|
3022
|
+
label2.command("create").argument("[name]").option("--color <color>", "label color").option("--json", "emit JSON").action(
|
|
2359
3023
|
async (name, opts) => {
|
|
2360
3024
|
await withConfig(
|
|
2361
3025
|
opts.json === true,
|
|
@@ -2364,10 +3028,10 @@ label.command("create").argument("[name]").option("--color <color>", "label colo
|
|
|
2364
3028
|
);
|
|
2365
3029
|
}
|
|
2366
3030
|
);
|
|
2367
|
-
|
|
3031
|
+
label2.command("update").description("rename or recolor a label").argument("[id]").option("--name <name>", "new name").option("--color <color>", "swatch name (red, blue, \u2026) or a hex code").option("--json", "emit JSON").action(async (id, opts) => {
|
|
2368
3032
|
await withConfig(opts.json === true, true, (ctx) => labelUpdate(ctx, id, opts));
|
|
2369
3033
|
});
|
|
2370
|
-
|
|
3034
|
+
label2.command("delete").description("delete a label (refused while issues carry it)").argument("[id]").option("--yes", "confirm the deletion").option("--json", "emit JSON").action(async (id, opts) => {
|
|
2371
3035
|
await withConfig(opts.json === true, true, (ctx) => labelDelete(ctx, id, opts));
|
|
2372
3036
|
});
|
|
2373
3037
|
var team = program.command("team").description("teams");
|
|
@@ -2452,14 +3116,14 @@ var tools = program.command("tools").description(
|
|
|
2452
3116
|
requireToolsOpts(opts);
|
|
2453
3117
|
await withConfig(opts.json === true, false, (ctx) => toolsList(ctx, opts));
|
|
2454
3118
|
});
|
|
2455
|
-
tools.command("search").description("search what Operator can use now by keyword, best match first").argument("[query...]").option("--domain <domain>", "only this domain").option("--cursor <cursor>", "continue from a page's cursor (from the paging notice)").option("--all", "follow every page").option("--json", "emit JSON").action(async (
|
|
3119
|
+
tools.command("search").description("search what Operator can use now by keyword, best match first").argument("[query...]").option("--domain <domain>", "only this domain").option("--cursor <cursor>", "continue from a page's cursor (from the paging notice)").option("--all", "follow every page").option("--json", "emit JSON").action(async (query2, opts) => {
|
|
2456
3120
|
const merged = { ...tools.opts(), ...opts };
|
|
2457
|
-
if (
|
|
3121
|
+
if (query2.length === 0) {
|
|
2458
3122
|
throw new UsageError("usage: harness tools search <query> [--domain <domain>] [--all]");
|
|
2459
3123
|
}
|
|
2460
|
-
requireSearchQuery(
|
|
3124
|
+
requireSearchQuery(query2.join(" "));
|
|
2461
3125
|
requireToolsOpts(merged);
|
|
2462
|
-
await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx,
|
|
3126
|
+
await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx, query2.join(" "), merged));
|
|
2463
3127
|
});
|
|
2464
3128
|
tools.command("domains").description("every domain Operator can use capabilities in, with how many").option("--json", "emit JSON").action(async (opts) => {
|
|
2465
3129
|
const parent = tools.opts();
|
|
@@ -2478,7 +3142,7 @@ program.command("skills").description("the organization's skills assigned to Ope
|
|
|
2478
3142
|
await withConfig(opts.json === true, false, (ctx) => skillsList(ctx));
|
|
2479
3143
|
});
|
|
2480
3144
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
2481
|
-
const { startMcpServer } = await import("./server-
|
|
3145
|
+
const { startMcpServer } = await import("./server-TRXBTJSA.js");
|
|
2482
3146
|
await startMcpServer(invokedAs);
|
|
2483
3147
|
});
|
|
2484
3148
|
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
@@ -2492,38 +3156,99 @@ keys.command("exec").description(
|
|
|
2492
3156
|
"pass only these provider slugs (comma-separated, repeatable); default: everything stored",
|
|
2493
3157
|
collect,
|
|
2494
3158
|
[]
|
|
2495
|
-
).option("--json", "accepted for uniformity; stdout belongs to the command, so it changes nothing").passThroughOptions().action(async (
|
|
3159
|
+
).option("--json", "accepted for uniformity; stdout belongs to the command, so it changes nothing").passThroughOptions().action(async (command2, opts) => {
|
|
2496
3160
|
const only = opts.only.flatMap((value) => value.split(","));
|
|
2497
3161
|
await withConfig(
|
|
2498
3162
|
false,
|
|
2499
3163
|
false,
|
|
2500
|
-
(ctx) => exec(ctx, { command, override: opts.override === true, only })
|
|
3164
|
+
(ctx) => exec(ctx, { command: command2, override: opts.override === true, only })
|
|
2501
3165
|
);
|
|
2502
3166
|
});
|
|
3167
|
+
var code = program.command("code").description("sync and inspect a code-index worktree overlay");
|
|
3168
|
+
code.command("sync").description("sync an overlay on a server-built GitHub base (needs harness:read and harness:write)").option("--path <directory>", "Git worktree (defaults to the current directory)").option("--skipped", "include the server base\u2019s skipped-files report").option("--json", "emit JSON").action(async (opts) => {
|
|
3169
|
+
const target = enterWorktree(opts);
|
|
3170
|
+
await withConfig(opts.json === true, true, (ctx) => sync(ctx, target));
|
|
3171
|
+
});
|
|
3172
|
+
code.command("status").description("inspect this worktree view; --skipped also asks for base status (needs harness:write)").option("--path <directory>", "Git worktree (defaults to the current directory)").option("--skipped", "include the server base\u2019s skipped-files report").option("--json", "emit JSON").action(async (opts) => {
|
|
3173
|
+
const target = enterWorktree(opts);
|
|
3174
|
+
await withConfig(opts.json === true, opts.skipped === true, (ctx) => status2(ctx, target));
|
|
3175
|
+
});
|
|
3176
|
+
for (const operation of ["search", "explain", "affected", "context"]) {
|
|
3177
|
+
const query2 = code.command(operation).description({ search: "find indexed declarations", explain: "explain an opaque symbol ID and its callers", affected: "find impact from symbol IDs or file paths", context: "retrieve indexed source snippets within a token budget" }[operation]);
|
|
3178
|
+
if (operation === "search" || operation === "explain" || operation === "context") query2.argument("[value]");
|
|
3179
|
+
if (operation === "search") query2.option("--kind <kind>").option("--path-prefix <prefix>").option("--limit <n>", "1 to 100", integer2);
|
|
3180
|
+
if (operation === "context") query2.option("--symbol <id>", "opaque ID from search");
|
|
3181
|
+
if (operation === "context" || operation === "explain") query2.option("--budget-tokens <n>", "indexed snippet token budget", integer2);
|
|
3182
|
+
if (operation === "affected") query2.option("--symbol <id>", "repeat for multiple IDs", (value, all) => [...all, value], []).option("--file <path>", "repeat for multiple paths", (value, all) => [...all, value], []).option("--depth <n>", "1 to 6", integer2).option("--include-ambiguous", "follow possible bindings too").option("--include-types", "follow type dependents too");
|
|
3183
|
+
query2.option("--path <directory>", "Git worktree").option("--json", "emit response data, meta and guidance");
|
|
3184
|
+
const run3 = async (value, opts) => {
|
|
3185
|
+
const target = enterWorktree({ path: opts["path"] });
|
|
3186
|
+
const params = {
|
|
3187
|
+
...operation === "search" || operation === "context" ? { q: value } : {},
|
|
3188
|
+
...operation === "explain" ? { symbol: value } : operation === "context" ? { symbol: opts["symbol"] } : {},
|
|
3189
|
+
...operation === "affected" ? { symbols: opts["symbol"], files: opts["file"], depth: opts["depth"], includeAmbiguous: opts["includeAmbiguous"], includeTypes: opts["includeTypes"] } : {},
|
|
3190
|
+
kind: opts["kind"],
|
|
3191
|
+
pathPrefix: opts["pathPrefix"],
|
|
3192
|
+
limit: opts["limit"],
|
|
3193
|
+
budgetTokens: opts["budgetTokens"]
|
|
3194
|
+
};
|
|
3195
|
+
validateQuery(operation, params);
|
|
3196
|
+
await withConfig(opts["json"] === true, false, (ctx) => query(ctx, operation, params, target));
|
|
3197
|
+
};
|
|
3198
|
+
if (operation === "affected") query2.action(async (opts) => run3(void 0, opts));
|
|
3199
|
+
else query2.action(run3);
|
|
3200
|
+
}
|
|
3201
|
+
var review = program.command("review").description("a pull request's enforced Review Council gate, and a retry of its latest attempt");
|
|
3202
|
+
review.command("status").description("the gate's enforced and advisory status, and a failed latest attempt with what comes next").argument("<pr>", "owner/repo#<pr>, the pull request URL, or <pr> with --repo or this checkout's origin").option("--repo <owner/repo>", "the repository a bare pull request number belongs to").option("--json", "emit JSON").action(async (pr, opts) => {
|
|
3203
|
+
const ref = parsePullRequest(pr, opts.repo);
|
|
3204
|
+
await withConfig(opts.json === true, false, (ctx) => status(ctx, ref));
|
|
3205
|
+
});
|
|
3206
|
+
review.command("retry").description(
|
|
3207
|
+
"retry the latest gate attempt of the pull request's current head (needs reviews:write); the head, check and attempt are read from the gate status"
|
|
3208
|
+
).argument("<pr>", "owner/repo#<pr>, the pull request URL, or <pr> with --repo or this checkout's origin").option("--repo <owner/repo>", "the repository a bare pull request number belongs to").option("--reason <text>", "why the attempt is retried, recorded with it").option("--json", "emit JSON").action(async (pr, opts) => {
|
|
3209
|
+
const ref = parsePullRequest(pr, opts.repo);
|
|
3210
|
+
await withConfig(opts.json === true, true, (ctx) => retry(ctx, ref, opts.reason));
|
|
3211
|
+
});
|
|
2503
3212
|
program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option(
|
|
2504
3213
|
"--timeout <ms>",
|
|
2505
3214
|
"how long to wait for the browser callback",
|
|
2506
|
-
|
|
3215
|
+
integer2
|
|
2507
3216
|
).option("--json", "emit JSON").action(
|
|
2508
3217
|
async (opts) => {
|
|
2509
3218
|
const repoLookup = findRepoConfig();
|
|
2510
3219
|
for (const warning of repoLookup.warnings)
|
|
2511
3220
|
writeStderr(warning + "\n");
|
|
2512
3221
|
const repoUrl = repoLookup.config?.url;
|
|
2513
|
-
const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? storedLoginUrl() ?? DEFAULT_BASE_URL;
|
|
3222
|
+
const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? (repoLookup.config?.org === void 0 ? storedLoginUrl() : void 0) ?? DEFAULT_BASE_URL;
|
|
2514
3223
|
if (url === repoUrl && opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0) {
|
|
2515
3224
|
writeStderr(
|
|
2516
3225
|
`harness: host ${url} comes from ${repoLookup.config?.path}
|
|
2517
3226
|
`
|
|
2518
3227
|
);
|
|
2519
3228
|
}
|
|
3229
|
+
const repoSelected = opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0 && repoUrl !== void 0;
|
|
3230
|
+
if (repoSelected && !isHostTrusted(url)) {
|
|
3231
|
+
if (!canConfirmRepoLogin()) {
|
|
3232
|
+
writeStderr("harness: cannot confirm a repo-selected host without a terminal \u2014 pass --url explicitly\n");
|
|
3233
|
+
return exitWith(2);
|
|
3234
|
+
}
|
|
3235
|
+
if (!await confirmRepoLogin(url, "repo-config", repoLookup.config?.path)) return exitWith(2);
|
|
3236
|
+
}
|
|
2520
3237
|
const options = { noBrowser: !opts.browser };
|
|
3238
|
+
if (repoLookup.config?.org !== void 0) options.org = repoLookup.config.org;
|
|
2521
3239
|
if (opts.timeout !== void 0) options.timeoutMs = opts.timeout;
|
|
2522
3240
|
await finishAuth(await login(url, options), opts.json === true);
|
|
2523
3241
|
}
|
|
2524
3242
|
);
|
|
2525
3243
|
program.command("logout").description("revoke and forget the stored login").option("--json", "emit JSON").action(async (opts) => {
|
|
2526
|
-
|
|
3244
|
+
const selected = resolveConfig({ ...process.env, HARNESS_API_TOKEN: void 0 });
|
|
3245
|
+
if (selected.kind === "error") {
|
|
3246
|
+
writeStderr(`harness: ${selected.message}
|
|
3247
|
+
`);
|
|
3248
|
+
return exitWith(2);
|
|
3249
|
+
}
|
|
3250
|
+
const path = selected.kind === "ok" ? selected.config.credentialsPath : selected.credentialsPath;
|
|
3251
|
+
await finishAuth(await logout(path === void 0 ? {} : { credentialsPath: path }), opts.json === true);
|
|
2527
3252
|
});
|
|
2528
3253
|
program.command("whoami").description("show the resolved host and credential source").option("--json", "emit JSON").action(async (opts) => {
|
|
2529
3254
|
const resolved = resolveConfig();
|
|
@@ -2536,6 +3261,8 @@ program.command("whoami").description("show the resolved host and credential sou
|
|
|
2536
3261
|
}
|
|
2537
3262
|
if (resolved.kind === "needs-login") {
|
|
2538
3263
|
writeStderr(loginHelp(resolved.baseUrl));
|
|
3264
|
+
if (resolved.org !== void 0) writeStderr(`Choose organization ${resolved.org} when logging in.
|
|
3265
|
+
`);
|
|
2539
3266
|
return exitWith(2);
|
|
2540
3267
|
}
|
|
2541
3268
|
const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
|
|
@@ -2576,6 +3303,10 @@ try {
|
|
|
2576
3303
|
if (notice !== null) writeStderr(notice);
|
|
2577
3304
|
} catch {
|
|
2578
3305
|
}
|
|
3306
|
+
if (invokedAs === "operator") {
|
|
3307
|
+
const deprecation = operatorDeprecationNotice();
|
|
3308
|
+
if (deprecation !== null) writeStderr(deprecation);
|
|
3309
|
+
}
|
|
2579
3310
|
if (invokedAs === "operator") layoutOperatorHelp(program);
|
|
2580
3311
|
if (argv.length <= 2) {
|
|
2581
3312
|
program.outputHelp({ error: true });
|