@allocator-one/harness-cli 1.16.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -0
- package/dist/{chunk-FQWV5MIC.js → chunk-5THYIKLW.js} +26 -4
- package/dist/chunk-67NX7MJ2.js +563 -0
- package/dist/chunk-BHBEIT5I.js +1348 -0
- package/dist/{chunk-GVTZKAXC.js → chunk-CEEQW6ZV.js} +299 -1592
- package/dist/chunk-WSDFLMO6.js +717 -0
- package/dist/chunk-XA5UDNJI.js +1448 -0
- package/dist/cli.js +1827 -1146
- package/dist/infraone-7TAHY4RN.js +593 -0
- package/dist/infraone-SLZXBJCV.js +22133 -0
- package/dist/infraone.js +372 -0
- package/dist/{login-page-KGEJFFXR.js → login-page-MFHNVTUV.js} +1 -1
- package/dist/{server-F6YRKNC2.js → server-TRXBTJSA.js} +11 -9
- package/package.json +4 -3
- package/skills/harness-code/SKILL.md +37 -0
- package/spec/openapi.json +3411 -0
package/dist/cli.js
CHANGED
|
@@ -1,57 +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-67NX7MJ2.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
|
-
LocalError,
|
|
17
18
|
PLAN_LIST_MAX_LIMIT,
|
|
18
19
|
PLAN_STATUSES,
|
|
19
20
|
RELATION_CREATE_TYPES,
|
|
20
21
|
RELATION_UPDATE_TYPES,
|
|
21
|
-
SESSION_PATH,
|
|
22
22
|
STEER_MAX_CHARACTERS,
|
|
23
|
-
TransportError,
|
|
24
|
-
UpgradeRequiredError,
|
|
25
|
-
UsageError,
|
|
26
23
|
activity,
|
|
27
24
|
archive,
|
|
28
25
|
assignments,
|
|
29
26
|
attachImage,
|
|
30
27
|
blocked,
|
|
31
|
-
clean,
|
|
32
28
|
close,
|
|
33
29
|
comment,
|
|
34
30
|
comments,
|
|
35
31
|
conditionList,
|
|
36
32
|
create,
|
|
37
|
-
defaultCredentialsPath,
|
|
38
33
|
deleteComment,
|
|
39
|
-
deleteCredentials,
|
|
40
34
|
dep,
|
|
41
35
|
docCreate,
|
|
42
36
|
docList,
|
|
43
37
|
docShow,
|
|
44
38
|
docUpdate,
|
|
45
39
|
editComment,
|
|
46
|
-
errorDetails,
|
|
47
|
-
expertSummary,
|
|
48
|
-
explainExpertRefusal,
|
|
49
|
-
findRepoConfig,
|
|
50
40
|
gateSet,
|
|
51
41
|
instructionFrom,
|
|
52
|
-
isExpertToken,
|
|
53
|
-
isHostTrusted,
|
|
54
|
-
isLoginCredential,
|
|
55
42
|
jobCancel,
|
|
56
43
|
jobContinue,
|
|
57
44
|
jobPause,
|
|
@@ -59,7 +46,6 @@ import {
|
|
|
59
46
|
jobSteer,
|
|
60
47
|
jobWatch,
|
|
61
48
|
jobsList,
|
|
62
|
-
json,
|
|
63
49
|
labelCreate,
|
|
64
50
|
labelDelete,
|
|
65
51
|
labelList,
|
|
@@ -69,13 +55,11 @@ import {
|
|
|
69
55
|
linkRm,
|
|
70
56
|
linkUpdate,
|
|
71
57
|
list,
|
|
72
|
-
loginHelp,
|
|
73
58
|
memberAdd,
|
|
74
59
|
memberList,
|
|
75
60
|
memberSetRole,
|
|
76
61
|
milestoneCreate,
|
|
77
62
|
milestoneList,
|
|
78
|
-
orgCredentialsPath,
|
|
79
63
|
planCreate,
|
|
80
64
|
planEdit,
|
|
81
65
|
planHold,
|
|
@@ -91,7 +75,6 @@ import {
|
|
|
91
75
|
projectList,
|
|
92
76
|
projectShow,
|
|
93
77
|
projectUpdate,
|
|
94
|
-
readCredentials,
|
|
95
78
|
readPlanFile,
|
|
96
79
|
ready,
|
|
97
80
|
relationAdd,
|
|
@@ -113,8 +96,6 @@ import {
|
|
|
113
96
|
requireSearchQuery,
|
|
114
97
|
requireToolsOpts,
|
|
115
98
|
requiredTeam,
|
|
116
|
-
resolveCacheHome,
|
|
117
|
-
resolveConfig,
|
|
118
99
|
resolveTextSources,
|
|
119
100
|
run,
|
|
120
101
|
search,
|
|
@@ -134,28 +115,50 @@ import {
|
|
|
134
115
|
toolsDomains,
|
|
135
116
|
toolsList,
|
|
136
117
|
toolsSearch,
|
|
137
|
-
trustHost,
|
|
138
118
|
update,
|
|
139
119
|
upload,
|
|
140
120
|
userList,
|
|
141
|
-
validOrgId,
|
|
142
|
-
validateBaseUrl,
|
|
143
121
|
validateIdempotencyKey,
|
|
144
122
|
version,
|
|
145
123
|
watchStopSignal,
|
|
146
|
-
workflowStates
|
|
147
|
-
|
|
148
|
-
|
|
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";
|
|
149
153
|
import {
|
|
150
154
|
CLI_VERSION,
|
|
151
|
-
OPERATOR_PACKAGE,
|
|
152
155
|
invokedName,
|
|
153
156
|
layoutOperatorHelp,
|
|
154
157
|
named
|
|
155
|
-
} from "./chunk-
|
|
158
|
+
} from "./chunk-5THYIKLW.js";
|
|
156
159
|
|
|
157
160
|
// src/cli.ts
|
|
158
|
-
import { createInterface
|
|
161
|
+
import { createInterface } from "readline";
|
|
159
162
|
import { Command, CommanderError, InvalidArgumentError } from "commander";
|
|
160
163
|
|
|
161
164
|
// src/commands/keys.ts
|
|
@@ -209,9 +212,9 @@ function selectSlugs(stored, only) {
|
|
|
209
212
|
function unknownSlugs(only) {
|
|
210
213
|
return only.map((slug) => slug.trim()).filter((slug) => slug !== "" && !KEY_ENV_VARS.has(slug.toLowerCase()));
|
|
211
214
|
}
|
|
212
|
-
function withholdOthers(
|
|
215
|
+
function withholdOthers(base2, requested) {
|
|
213
216
|
const keep = new Set(requested.map((slug) => envVarFor(slug)));
|
|
214
|
-
const env = { ...
|
|
217
|
+
const env = { ...base2 };
|
|
215
218
|
for (const name of KEY_ENV_VARS.values()) {
|
|
216
219
|
if (!keep.has(name)) delete env[name];
|
|
217
220
|
}
|
|
@@ -229,9 +232,9 @@ function toEnv(stored) {
|
|
|
229
232
|
}
|
|
230
233
|
return env;
|
|
231
234
|
}
|
|
232
|
-
function childEnv(
|
|
235
|
+
function childEnv(base2, fetched, override) {
|
|
233
236
|
const env = {};
|
|
234
|
-
for (const [name, value] of Object.entries(
|
|
237
|
+
for (const [name, value] of Object.entries(base2)) {
|
|
235
238
|
if (value !== void 0) env[name] = value;
|
|
236
239
|
}
|
|
237
240
|
for (const [name, value] of Object.entries(fetched)) {
|
|
@@ -240,8 +243,8 @@ function childEnv(base, fetched, override) {
|
|
|
240
243
|
}
|
|
241
244
|
return env;
|
|
242
245
|
}
|
|
243
|
-
function exitCodeFor(
|
|
244
|
-
if (
|
|
246
|
+
function exitCodeFor(code2, signal) {
|
|
247
|
+
if (code2 !== null) return code2;
|
|
245
248
|
if (signal !== null) {
|
|
246
249
|
const number = constants.signals[signal];
|
|
247
250
|
if (typeof number === "number") return 128 + number;
|
|
@@ -295,13 +298,13 @@ async function exec(ctx, opts) {
|
|
|
295
298
|
opts.override
|
|
296
299
|
);
|
|
297
300
|
const name = clean(program2);
|
|
298
|
-
const exitCode = await new Promise((
|
|
301
|
+
const exitCode = await new Promise((resolve3, reject) => {
|
|
299
302
|
let child;
|
|
300
303
|
try {
|
|
301
304
|
child = spawn(program2, args, { stdio: "inherit", env });
|
|
302
305
|
} catch (error) {
|
|
303
|
-
const
|
|
304
|
-
reject(new CommandError(`cannot start ${name}: ${
|
|
306
|
+
const code2 = error.code ?? "invalid arguments";
|
|
307
|
+
reject(new CommandError(`cannot start ${name}: ${code2}`));
|
|
305
308
|
return;
|
|
306
309
|
}
|
|
307
310
|
const handlers = [];
|
|
@@ -331,12 +334,12 @@ async function exec(ctx, opts) {
|
|
|
331
334
|
});
|
|
332
335
|
child.on("error", (error) => {
|
|
333
336
|
settle(() => {
|
|
334
|
-
const
|
|
335
|
-
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}`));
|
|
336
339
|
});
|
|
337
340
|
});
|
|
338
|
-
child.on("exit", (
|
|
339
|
-
settle(() =>
|
|
341
|
+
child.on("exit", (code2, signal) => {
|
|
342
|
+
settle(() => resolve3(exitCodeFor(code2, signal)));
|
|
340
343
|
});
|
|
341
344
|
});
|
|
342
345
|
return { json: { exit_code: exitCode }, human: "", exitCode };
|
|
@@ -490,8 +493,8 @@ async function retry(ctx, pr, reason) {
|
|
|
490
493
|
response = await ctx.client.post(`${gatePath(pr)}/retry`, body, { actor: false });
|
|
491
494
|
} catch (error) {
|
|
492
495
|
if (error instanceof HttpError && error.status === 409) {
|
|
493
|
-
const
|
|
494
|
-
throw new CommandError(`could not retry ${label(pr)}: ${REFUSALS[
|
|
496
|
+
const code2 = error.message.trim().split(/[\s:]+/).pop() ?? "";
|
|
497
|
+
throw new CommandError(`could not retry ${label(pr)}: ${REFUSALS[code2] ?? clean(error.message)}`);
|
|
495
498
|
}
|
|
496
499
|
if (error instanceof HttpError && error.status === 403) {
|
|
497
500
|
throw new CommandError(`${error.message} \u2014 a retry needs a login with reviews:write`);
|
|
@@ -508,1192 +511,1832 @@ async function retry(ctx, pr, reason) {
|
|
|
508
511
|
};
|
|
509
512
|
}
|
|
510
513
|
|
|
511
|
-
// src/
|
|
512
|
-
import {
|
|
513
|
-
import {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
import {
|
|
517
|
-
import {
|
|
518
|
-
var
|
|
519
|
-
var
|
|
520
|
-
var
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
514
|
+
// src/commands/code/index.ts
|
|
515
|
+
import { resolve as resolve2 } from "path";
|
|
516
|
+
import { statSync, realpathSync } 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;
|
|
601
627
|
try {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
openBrowser(url);
|
|
611
|
-
} catch (error) {
|
|
612
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
613
|
-
return { ok: false, message: `could not open a browser: ${detail}` };
|
|
614
|
-
}
|
|
615
|
-
if (options.org !== void 0) notify(`Choose organization ${options.org} on the login page; another organization's token will be refused.`);
|
|
616
|
-
notify(`Opening ${url}`);
|
|
617
|
-
notify("Waiting for the browser authorization (Ctrl-C to abort)\u2026");
|
|
618
|
-
const callback = await callbackPromise;
|
|
619
|
-
if (!callback.ok) return callback;
|
|
620
|
-
try {
|
|
621
|
-
const token = await exchangeCode(baseUrl, callback.code, verifier, options);
|
|
622
|
-
if (!token.ok) {
|
|
623
|
-
await callback.respond(
|
|
624
|
-
400,
|
|
625
|
-
"Harness could not exchange the browser authorization. Return to the terminal for details.",
|
|
626
|
-
"error"
|
|
627
|
-
);
|
|
628
|
-
return token;
|
|
629
|
-
}
|
|
630
|
-
const result = await verifyAndStore(baseUrl, token.token, options, { minted: true, kind: token.kind });
|
|
631
|
-
if (result.ok) {
|
|
632
|
-
await callback.respond(200, "You can close this browser and return to the terminal.", "success");
|
|
633
|
-
} else {
|
|
634
|
-
await callback.respond(
|
|
635
|
-
400,
|
|
636
|
-
"Harness could not verify and store this login. Return to the terminal for details.",
|
|
637
|
-
"error"
|
|
638
|
-
);
|
|
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.");
|
|
639
636
|
}
|
|
640
|
-
|
|
641
|
-
} catch (error) {
|
|
642
|
-
await callback.respond(500, "Harness could not finish the login. Return to the terminal for details.", "error");
|
|
643
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
644
|
-
return { ok: false, message: `could not finish the browser login: ${detail}` };
|
|
637
|
+
chunks.push(Buffer.from(part.value));
|
|
645
638
|
}
|
|
646
639
|
} finally {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const drainTrackedResponses = async () => {
|
|
669
|
-
await new Promise((settle) => setTimeout(settle, RESPONSE_DRAIN_GRACE_MS));
|
|
670
|
-
const deadline = Date.now() + RESPONSE_FLUSH_TIMEOUT_MS;
|
|
671
|
-
while (inFlightResponses.size > 0 && Date.now() < deadline) {
|
|
672
|
-
const remaining = deadline - Date.now();
|
|
673
|
-
await Promise.race([
|
|
674
|
-
Promise.allSettled([...inFlightResponses]),
|
|
675
|
-
new Promise((settle) => setTimeout(settle, remaining))
|
|
676
|
-
]);
|
|
677
|
-
}
|
|
678
|
-
};
|
|
679
|
-
const timer = setTimeout(() => {
|
|
680
|
-
if (phase !== "waiting") return;
|
|
681
|
-
const timeoutResponse = {
|
|
682
|
-
status: 410,
|
|
683
|
-
message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
|
|
684
|
-
pageState: "error"
|
|
685
|
-
};
|
|
686
|
-
phase = "terminal";
|
|
687
|
-
finalResponse = timeoutResponse;
|
|
688
|
-
resolve({
|
|
689
|
-
ok: false,
|
|
690
|
-
message: "timed out waiting for the browser authorization"
|
|
691
|
-
});
|
|
692
|
-
void drainTrackedResponses();
|
|
693
|
-
}, timeoutMs);
|
|
694
|
-
server.once("close", () => {
|
|
695
|
-
clearTimeout(timer);
|
|
696
|
-
if (phase !== "waiting") return;
|
|
697
|
-
phase = "terminal";
|
|
698
|
-
resolve({ ok: false, message: "the browser login listener closed before authorization completed" });
|
|
699
|
-
});
|
|
700
|
-
server.on("request", (request, response) => {
|
|
701
|
-
const respond = callbackResponder(response);
|
|
702
|
-
const respondWithPage = (status2, message, pageState) => {
|
|
703
|
-
if (callbackPageResponses >= MAX_PAGE_RESPONSES) {
|
|
704
|
-
respondEmpty(response, 429);
|
|
705
|
-
return Promise.resolve();
|
|
706
|
-
}
|
|
707
|
-
callbackPageResponses += 1;
|
|
708
|
-
return respond(status2, message, pageState);
|
|
709
|
-
};
|
|
710
|
-
const respondAtRoot = (status2, message, pageState) => {
|
|
711
|
-
const rootPhase = phase === "terminal" ? "terminal" : "waiting";
|
|
712
|
-
const responses = rootPageResponses.get(rootPhase) ?? 0;
|
|
713
|
-
if (responses >= MAX_PAGE_RESPONSES) {
|
|
714
|
-
respondEmpty(response, 429);
|
|
715
|
-
return Promise.resolve();
|
|
716
|
-
}
|
|
717
|
-
rootPageResponses.set(rootPhase, responses + 1);
|
|
718
|
-
return respond(status2, message, pageState);
|
|
719
|
-
};
|
|
720
|
-
let url;
|
|
640
|
+
reader.releaseLock();
|
|
641
|
+
}
|
|
642
|
+
try {
|
|
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++) {
|
|
721
661
|
try {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
727
|
-
const path = url.pathname.replace(/\/+$/, "") || "/";
|
|
728
|
-
if (path !== "/callback") {
|
|
729
|
-
if (path === "/") {
|
|
730
|
-
const rootResponse = phase === "terminal" && finalResponse ? finalResponse : {
|
|
731
|
-
status: 200,
|
|
732
|
-
message: "Waiting for the Harness browser authorization to finish.",
|
|
733
|
-
pageState: "waiting"
|
|
734
|
-
};
|
|
735
|
-
void trackResponse(
|
|
736
|
-
respondAtRoot(rootResponse.status, rootResponse.message, rootResponse.pageState)
|
|
737
|
-
);
|
|
738
|
-
} else {
|
|
739
|
-
respondEmpty(response, path === "/favicon.ico" ? 204 : 404);
|
|
740
|
-
}
|
|
741
|
-
return;
|
|
742
|
-
}
|
|
743
|
-
const matchingState = url.searchParams.get("state") === state2;
|
|
744
|
-
if (!matchingState) {
|
|
745
|
-
respondEmpty(response, 400);
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
if (phase === "terminal") {
|
|
749
|
-
if (!url.searchParams.has("code") && !url.searchParams.has("error")) {
|
|
750
|
-
respondEmpty(response, 400);
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
const result = finalResponse ?? {
|
|
754
|
-
status: 410,
|
|
755
|
-
message: `This login attempt has ended \u2014 run ${invokedName()} login again if needed.`,
|
|
756
|
-
pageState: "error"
|
|
662
|
+
const headers = {
|
|
663
|
+
authorization: `Bearer ${this.config.token}`,
|
|
664
|
+
"x-harness-cli-version": CLI_VERSION
|
|
757
665
|
};
|
|
758
|
-
void
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
await trackResponse(respondWithPage(denialResponse.status, denialResponse.message, denialResponse.pageState));
|
|
789
|
-
await drainTrackedResponses();
|
|
790
|
-
resolve({ ok: false, message: "browser authorization was denied" });
|
|
791
|
-
})();
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
const code = url.searchParams.get("code");
|
|
795
|
-
if (code !== null && code !== "") {
|
|
796
|
-
phase = "processing";
|
|
797
|
-
acceptedResponders.push(respondWithPage);
|
|
798
|
-
clearTimeout(timer);
|
|
799
|
-
resolve({
|
|
800
|
-
ok: true,
|
|
801
|
-
code,
|
|
802
|
-
respond: async (status2, message, pageState) => {
|
|
803
|
-
finalResponse = { status: status2, message, pageState };
|
|
804
|
-
phase = "terminal";
|
|
805
|
-
const responders = acceptedResponders.splice(0);
|
|
806
|
-
await Promise.all(
|
|
807
|
-
responders.map((acceptedRespond) => trackResponse(acceptedRespond(status2, message, pageState)))
|
|
808
|
-
);
|
|
809
|
-
await drainTrackedResponses();
|
|
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
|
+
);
|
|
810
696
|
}
|
|
811
|
-
});
|
|
812
|
-
} else {
|
|
813
|
-
respondEmpty(response, 400);
|
|
814
|
-
}
|
|
815
|
-
});
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
function callbackResponder(response) {
|
|
819
|
-
return async (status2, message, state2) => {
|
|
820
|
-
if (response.writableEnded || response.destroyed) return;
|
|
821
|
-
return new Promise((resolve) => {
|
|
822
|
-
let flushTimer;
|
|
823
|
-
const armFlushTimeout = () => {
|
|
824
|
-
if (flushTimer !== void 0) clearTimeout(flushTimer);
|
|
825
|
-
flushTimer = setTimeout(() => {
|
|
826
|
-
response.destroy();
|
|
827
|
-
settle();
|
|
828
|
-
}, RESPONSE_FLUSH_TIMEOUT_MS);
|
|
829
|
-
flushTimer.unref();
|
|
830
|
-
};
|
|
831
|
-
const settle = () => {
|
|
832
|
-
if (flushTimer !== void 0) clearTimeout(flushTimer);
|
|
833
|
-
response.off("finish", settle);
|
|
834
|
-
response.off("close", settle);
|
|
835
|
-
response.off("error", settle);
|
|
836
|
-
response.off("drain", armFlushTimeout);
|
|
837
|
-
resolve();
|
|
838
|
-
};
|
|
839
|
-
response.once("finish", settle);
|
|
840
|
-
response.once("close", settle);
|
|
841
|
-
response.once("error", settle);
|
|
842
|
-
response.on("drain", armFlushTimeout);
|
|
843
|
-
void loadLoginPage().then(({ loginPage }) => {
|
|
844
|
-
if (response.writableEnded || response.destroyed) {
|
|
845
|
-
settle();
|
|
846
|
-
return;
|
|
847
697
|
}
|
|
848
|
-
|
|
849
|
-
const
|
|
850
|
-
response.
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
response.end(html);
|
|
859
|
-
} catch {
|
|
860
|
-
response.destroy();
|
|
861
|
-
settle();
|
|
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
|
+
);
|
|
862
708
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
}
|
|
882
|
-
}
|
|
883
|
-
async function exchangeCode(baseUrl, code, verifier, options) {
|
|
884
|
-
try {
|
|
885
|
-
const body = await bareClient(baseUrl, "", options).post("/api/v1/cli/token-exchange", { code, code_verifier: verifier });
|
|
886
|
-
const token = body.data?.token;
|
|
887
|
-
const kind = typeof body.data?.kind === "string" ? body.data.kind : null;
|
|
888
|
-
if (typeof token === "string" && token !== "") return { ok: true, token, kind };
|
|
889
|
-
return {
|
|
890
|
-
ok: false,
|
|
891
|
-
message: "the login-code exchange returned no token \u2014 aborting"
|
|
892
|
-
};
|
|
893
|
-
} catch (error) {
|
|
894
|
-
if (error instanceof HttpError) {
|
|
895
|
-
return {
|
|
896
|
-
ok: false,
|
|
897
|
-
message: `the login-code exchange failed: ${error.message}`
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
if (error instanceof TransportError) {
|
|
901
|
-
return {
|
|
902
|
-
ok: false,
|
|
903
|
-
message: `the login-code exchange failed: cannot reach the API (${error.message})`
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
throw error;
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
async function manualLogin(baseUrl, options) {
|
|
910
|
-
const notify = options.notify ?? defaultNotify;
|
|
911
|
-
const prompt = options.prompt ?? defaultPrompt;
|
|
912
|
-
if (options.org !== void 0) notify(`Choose organization ${options.org} on the login page; another organization's token will be refused.`);
|
|
913
|
-
notify(
|
|
914
|
-
`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).`
|
|
915
|
-
);
|
|
916
|
-
const token = (await prompt()).trim();
|
|
917
|
-
if (token === "") return { ok: false, message: "no token entered" };
|
|
918
|
-
return verifyAndStore(baseUrl, token, options, { minted: false, kind: null });
|
|
919
|
-
}
|
|
920
|
-
async function verifyAndStore(baseUrl, token, options, received) {
|
|
921
|
-
const defaultPath = options.credentialsPath ?? defaultCredentialsPath(options.env ?? process.env);
|
|
922
|
-
const path = options.org === void 0 ? defaultPath : orgCredentialsPath(defaultPath, baseUrl, options.org);
|
|
923
|
-
const client = bareClient(baseUrl, token, options);
|
|
924
|
-
const expert = isExpertToken(token);
|
|
925
|
-
let kind = received.kind;
|
|
926
|
-
try {
|
|
927
|
-
if (expert) {
|
|
928
|
-
if (options.org !== void 0) return { ok: false, message: "cannot verify an outside expert credential against a repo org \u2014 nothing was stored" };
|
|
929
|
-
await client.get(EXPERT_ASSIGNMENTS_PATH);
|
|
930
|
-
} else {
|
|
931
|
-
const described = isLoginCredential(token) ? await describeSession(client) : null;
|
|
932
|
-
if (described !== null) kind = described.session.kind ?? kind;
|
|
933
|
-
if (options.org !== void 0 && (typeof described?.session.org?.["id"] !== "string" || described.session.org["id"].toLowerCase() !== options.org.toLowerCase())) {
|
|
934
|
-
const revoked = received.minted && await revokeSession(client, token);
|
|
935
|
-
return { ok: false, message: `the received token does not verify organization ${options.org} \u2014 nothing was stored` + (revoked ? ", and the token was revoked" : "") };
|
|
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);
|
|
936
727
|
}
|
|
937
|
-
if (described === null) await client.get("/api/v1/teams");
|
|
938
728
|
}
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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 };
|
|
947
742
|
}
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
};
|
|
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));
|
|
953
747
|
}
|
|
954
|
-
|
|
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 };
|
|
750
|
+
}
|
|
751
|
+
async getPolicy() {
|
|
752
|
+
const raw = (await this.request("GET", "/policy")).data;
|
|
753
|
+
if (raw && typeof raw === "object" && "deny_glob_case" in raw && raw.deny_glob_case !== void 0 && raw.deny_glob_case !== "ascii_insensitive")
|
|
754
|
+
throw new CommandError("The server returned an invalid code upload policy; upgrade the client before code sync.");
|
|
755
|
+
return decode(policy, raw);
|
|
756
|
+
}
|
|
757
|
+
async putRepository(repoKey) {
|
|
758
|
+
return decode(
|
|
759
|
+
z.object({ repository_id: UUID, repo_key: z.string() }),
|
|
760
|
+
(await this.request("PUT", "/repositories", { repo_key: repoKey })).data
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
async getBase(repositoryId, commit, skipped) {
|
|
764
|
+
const r = await this.request(
|
|
765
|
+
"GET",
|
|
766
|
+
`/repositories/${encodeURIComponent(repositoryId)}/bases/${encodeURIComponent(commit)}${skipped ? "?skipped=true" : ""}`
|
|
767
|
+
);
|
|
768
|
+
const data = decode(base, r.data);
|
|
769
|
+
const wait = Math.max(data.retry_after_ms ?? 0, r.retryAfter ?? 0);
|
|
770
|
+
return wait ? { ...data, retry_after_ms: wait } : data;
|
|
771
|
+
}
|
|
772
|
+
async putView(repositoryId, key) {
|
|
773
|
+
return decode(
|
|
774
|
+
view,
|
|
775
|
+
(await this.request(
|
|
776
|
+
"PUT",
|
|
777
|
+
`/repositories/${encodeURIComponent(repositoryId)}/views/${encodeURIComponent(key)}`,
|
|
778
|
+
{}
|
|
779
|
+
)).data
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
async getView(id) {
|
|
783
|
+
return decode(
|
|
784
|
+
view,
|
|
785
|
+
(await this.request("GET", `/views/${encodeURIComponent(id)}`)).data
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
async openSync(id, body) {
|
|
789
|
+
return decode(
|
|
790
|
+
opened,
|
|
791
|
+
(await this.request("POST", `/views/${encodeURIComponent(id)}/syncs`, {
|
|
792
|
+
...body
|
|
793
|
+
})).data
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
async putBlob(hash2, bytes) {
|
|
797
|
+
if (!HASH.safeParse(hash2).success) throw new CommandError("Invalid code blob SHA256; no source was uploaded.");
|
|
798
|
+
await this.request("PUT", `/blobs/${hash2}`, bytes);
|
|
799
|
+
}
|
|
800
|
+
async getSync(viewId, syncId) {
|
|
801
|
+
return decode(syncStatus, (await this.request(
|
|
802
|
+
"GET",
|
|
803
|
+
`/views/${encodeURIComponent(viewId)}/syncs/${encodeURIComponent(syncId)}`
|
|
804
|
+
)).data);
|
|
805
|
+
}
|
|
806
|
+
async activate(viewId, syncId) {
|
|
807
|
+
return decode(
|
|
808
|
+
activated,
|
|
809
|
+
(await this.request(
|
|
810
|
+
"POST",
|
|
811
|
+
`/views/${encodeURIComponent(viewId)}/syncs/${encodeURIComponent(syncId)}/activate`,
|
|
812
|
+
{}
|
|
813
|
+
)).data
|
|
814
|
+
);
|
|
955
815
|
}
|
|
956
|
-
|
|
957
|
-
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/core/code/sync.ts
|
|
819
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
820
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
821
|
+
|
|
822
|
+
// src/core/code/git.ts
|
|
823
|
+
import { execFile } from "child_process";
|
|
824
|
+
import { promisify } from "util";
|
|
825
|
+
import { realpath } from "fs/promises";
|
|
826
|
+
var run2 = promisify(execFile);
|
|
827
|
+
var MAX_GIT_BYTES = 16 * 1024 * 1024;
|
|
828
|
+
async function gitBytes(cwd, args, maxBuffer = MAX_GIT_BYTES, limitMessage = "Git returned more than 16 MiB of metadata; this exceeds the code-sync client limit.") {
|
|
958
829
|
try {
|
|
959
|
-
|
|
830
|
+
return (await run2(
|
|
831
|
+
"git",
|
|
832
|
+
[
|
|
833
|
+
"--no-optional-locks",
|
|
834
|
+
"--no-pager",
|
|
835
|
+
"-c",
|
|
836
|
+
"core.fsmonitor=false",
|
|
837
|
+
...args
|
|
838
|
+
],
|
|
839
|
+
{ cwd, maxBuffer, timeout: 3e4, encoding: "buffer" }
|
|
840
|
+
)).stdout;
|
|
960
841
|
} catch (error) {
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
842
|
+
const code2 = error.code;
|
|
843
|
+
if (code2 === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || error instanceof Error && /maxBuffer/i.test(error.message))
|
|
844
|
+
throw new LocalError(
|
|
845
|
+
limitMessage
|
|
846
|
+
);
|
|
847
|
+
throw new LocalError(
|
|
848
|
+
"Cannot read this Git working tree. Run code commands inside a committed repository with an origin remote."
|
|
849
|
+
);
|
|
967
850
|
}
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
851
|
+
}
|
|
852
|
+
function decodeUtf8(value, message) {
|
|
853
|
+
const decoded = value.toString("utf8");
|
|
854
|
+
if (!Buffer.from(decoded, "utf8").equals(value)) throw new LocalError(message);
|
|
855
|
+
return decoded;
|
|
856
|
+
}
|
|
857
|
+
function decodeGitPaths(outputs) {
|
|
858
|
+
const paths = [];
|
|
859
|
+
for (const output of outputs) {
|
|
860
|
+
if (output.length > 0 && output.at(-1) !== 0)
|
|
861
|
+
throw new LocalError("Code sync received an incomplete Git path record.");
|
|
862
|
+
let start = 0;
|
|
863
|
+
for (let end = output.indexOf(0, start); end !== -1; end = output.indexOf(0, start)) {
|
|
864
|
+
if (end === start)
|
|
865
|
+
throw new LocalError("Code sync received an empty Git path record.");
|
|
866
|
+
const path = decodeUtf8(
|
|
867
|
+
output.subarray(start, end),
|
|
868
|
+
"Code sync cannot safely index a Git path with invalid UTF-8."
|
|
869
|
+
);
|
|
870
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(path))
|
|
871
|
+
throw new LocalError(
|
|
872
|
+
"Code sync cannot safely index a Git path with control characters."
|
|
873
|
+
);
|
|
874
|
+
paths.push(path);
|
|
875
|
+
start = end + 1;
|
|
973
876
|
}
|
|
974
877
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
878
|
+
return paths;
|
|
879
|
+
}
|
|
880
|
+
function decodeGitLine(output, message) {
|
|
881
|
+
if (output.length === 0 || output.at(-1) !== 10)
|
|
882
|
+
throw new LocalError(message);
|
|
883
|
+
return decodeUtf8(output.subarray(0, -1), message);
|
|
884
|
+
}
|
|
885
|
+
function assertDistinctPathIdentities(paths) {
|
|
886
|
+
const known = /* @__PURE__ */ new Map();
|
|
887
|
+
for (const path of paths) {
|
|
888
|
+
let original = "";
|
|
889
|
+
let folded = "";
|
|
890
|
+
for (const part of path.split("/")) {
|
|
891
|
+
original += `/${part}`;
|
|
892
|
+
folded += `/${part.normalize("NFC").toLocaleLowerCase("en-US").toLocaleUpperCase("en-US").normalize("NFC")}`;
|
|
893
|
+
const previous = known.get(folded);
|
|
894
|
+
if (previous !== void 0 && previous !== original)
|
|
895
|
+
throw new LocalError(
|
|
896
|
+
"Code sync cannot safely distinguish colliding Git path identities."
|
|
897
|
+
);
|
|
898
|
+
known.set(folded, original);
|
|
899
|
+
}
|
|
987
900
|
}
|
|
988
|
-
if (kind === "harness-cli") return "This credential has full access (harness-cli).\n";
|
|
989
|
-
return "";
|
|
990
901
|
}
|
|
991
|
-
async function
|
|
902
|
+
async function identity(cwd) {
|
|
903
|
+
const rootRecord = decodeGitLine(
|
|
904
|
+
await gitBytes(cwd, ["rev-parse", "--show-toplevel"]),
|
|
905
|
+
"Cannot resolve the Git working tree root safely."
|
|
906
|
+
);
|
|
907
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(rootRecord))
|
|
908
|
+
throw new LocalError("Cannot resolve the Git working tree root safely.");
|
|
909
|
+
const root = await realpath(rootRecord);
|
|
910
|
+
const commit = decodeGitLine(
|
|
911
|
+
await gitBytes(root, ["rev-parse", "--verify", "HEAD^{commit}"]),
|
|
912
|
+
"Cannot resolve the repository HEAD commit."
|
|
913
|
+
);
|
|
914
|
+
const missingRemote = "repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection.";
|
|
915
|
+
let remote;
|
|
992
916
|
try {
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
if (error instanceof HttpError && (error.status === 404 || error.status === 405)) return null;
|
|
997
|
-
throw error;
|
|
917
|
+
remote = decodeGitLine(await gitBytes(root, ["remote", "get-url", "origin"]), missingRemote);
|
|
918
|
+
} catch {
|
|
919
|
+
throw new LocalError(missingRemote);
|
|
998
920
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
921
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(remote))
|
|
922
|
+
throw new LocalError(
|
|
923
|
+
"repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
|
|
924
|
+
);
|
|
925
|
+
const scp = /^(?:git@)?github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(remote);
|
|
926
|
+
let parts;
|
|
927
|
+
if (scp) parts = [scp[1], scp[2]];
|
|
928
|
+
else {
|
|
929
|
+
try {
|
|
930
|
+
const url = new URL(remote);
|
|
931
|
+
if (["https:", "ssh:", "git:"].includes(url.protocol) && url.hostname.toLowerCase() === "github.com" && !url.password && !url.search && !url.hash && (!url.port || url.protocol === "ssh:")) {
|
|
932
|
+
parts = url.pathname.replace(/^\//, "").replace(/\/$/, "").replace(/\.git$/, "").split("/");
|
|
933
|
+
}
|
|
934
|
+
} catch {
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (!parts || parts.length !== 2 || parts.some((p) => !p || !/^[\w.-]+$/.test(p) || p === "." || p === ".."))
|
|
938
|
+
throw new LocalError(
|
|
939
|
+
"repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
|
|
940
|
+
);
|
|
941
|
+
if (!/^[0-9a-f]{40,64}$/.test(commit))
|
|
942
|
+
throw new LocalError("Cannot resolve the repository HEAD commit.");
|
|
1003
943
|
return {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
user: isRecord3(data["user"]) ? data["user"] : null,
|
|
1008
|
-
org: isRecord3(data["org"]) ? data["org"] : null
|
|
944
|
+
root,
|
|
945
|
+
commit,
|
|
946
|
+
repoKey: `github.com/${parts.join("/").toLowerCase()}`
|
|
1009
947
|
};
|
|
1010
948
|
}
|
|
1011
|
-
async function
|
|
1012
|
-
|
|
1013
|
-
|
|
949
|
+
async function changedPaths(root, commit) {
|
|
950
|
+
const changed = await gitBytes(root, [
|
|
951
|
+
"diff",
|
|
952
|
+
"--name-only",
|
|
953
|
+
"-z",
|
|
954
|
+
"--no-renames",
|
|
955
|
+
"--no-ext-diff",
|
|
956
|
+
"--no-textconv",
|
|
957
|
+
commit,
|
|
958
|
+
"--"
|
|
959
|
+
]);
|
|
960
|
+
const untracked = await gitBytes(root, [
|
|
961
|
+
"ls-files",
|
|
962
|
+
"--others",
|
|
963
|
+
"--exclude-standard",
|
|
964
|
+
"-z"
|
|
965
|
+
]);
|
|
966
|
+
return [
|
|
967
|
+
...new Set(decodeGitPaths([changed, untracked]))
|
|
968
|
+
].sort(
|
|
969
|
+
(a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))
|
|
970
|
+
);
|
|
1014
971
|
}
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
};
|
|
972
|
+
function decodeGitTree(output) {
|
|
973
|
+
if (output.length && output.at(-1) !== 0)
|
|
974
|
+
throw new LocalError("Code sync received incomplete Git tree metadata.");
|
|
975
|
+
const entries = [];
|
|
976
|
+
for (let start = 0, end = output.indexOf(0); end !== -1; end = output.indexOf(0, start)) {
|
|
977
|
+
const record2 = output.subarray(start, end), tab = record2.indexOf(9);
|
|
978
|
+
if (tab < 0) throw new LocalError("Code sync received invalid Git tree metadata.");
|
|
979
|
+
const header = decodeUtf8(record2.subarray(0, tab), "Code sync received invalid Git tree metadata.");
|
|
980
|
+
const match = /^(100644|100755|120000|160000) (blob|commit) ([0-9a-f]{40}|[0-9a-f]{64})$/.exec(header);
|
|
981
|
+
if (!match || match[1] === "160000" !== (match[2] === "commit"))
|
|
982
|
+
throw new LocalError("Code sync received invalid Git tree metadata.");
|
|
983
|
+
const [path] = decodeGitPaths([Buffer.concat([record2.subarray(tab + 1), Buffer.from([0])])]);
|
|
984
|
+
entries.push({ path, regular: match[1] === "100644" || match[1] === "100755" });
|
|
985
|
+
start = end + 1;
|
|
986
|
+
}
|
|
987
|
+
return entries;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// src/core/code/scan.ts
|
|
991
|
+
import { constants as constants2 } from "fs";
|
|
992
|
+
import { open } from "fs/promises";
|
|
993
|
+
import { join } from "path";
|
|
994
|
+
import { createHash } from "crypto";
|
|
995
|
+
import { release } from "os";
|
|
996
|
+
|
|
997
|
+
// src/core/code/policy.ts
|
|
998
|
+
import { isUtf8 } from "buffer";
|
|
999
|
+
import { extname } from "path";
|
|
1000
|
+
|
|
1001
|
+
// src/core/code/glob.ts
|
|
1002
|
+
var MAX_PATTERN_BYTES = 256;
|
|
1003
|
+
var MAX_PATTERN_STARS = 8;
|
|
1004
|
+
var MAX_PATTERNS = 1e3;
|
|
1005
|
+
function trimIgnoreLine(line) {
|
|
1006
|
+
return line.replace(new RegExp("^\\p{White_Space}+|\\p{White_Space}+$", "gu"), "");
|
|
1007
|
+
}
|
|
1008
|
+
function compileIgnorePatterns(lines, warnings) {
|
|
1009
|
+
const patterns = [];
|
|
1010
|
+
for (const [line, raw] of lines.entries()) {
|
|
1011
|
+
const trimmed = trimIgnoreLine(raw);
|
|
1012
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1013
|
+
if (trimmed.startsWith("!")) {
|
|
1014
|
+
warnings.push(
|
|
1015
|
+
`Ignoring .harnesscodeignore negation on line ${line + 1}; code-index patterns do not support !.`
|
|
1016
|
+
);
|
|
1017
|
+
continue;
|
|
1018
|
+
}
|
|
1019
|
+
if (trimmed.includes("\\")) {
|
|
1020
|
+
warnings.push(
|
|
1021
|
+
`The .harnesscodeignore backslash on line ${line + 1} is literal and never matches a normalized source path.`
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
if (Buffer.byteLength(trimmed) > MAX_PATTERN_BYTES || (trimmed.match(/\*/g)?.length ?? 0) > MAX_PATTERN_STARS) {
|
|
1025
|
+
warnings.push(
|
|
1026
|
+
"Ignoring .harnesscodeignore pattern outside the code-index size limits."
|
|
1027
|
+
);
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (patterns.length === MAX_PATTERNS) {
|
|
1031
|
+
warnings.push(
|
|
1032
|
+
"Ignoring .harnesscodeignore patterns after the first 1,000 valid entries."
|
|
1033
|
+
);
|
|
1034
|
+
break;
|
|
1035
|
+
}
|
|
1036
|
+
const pattern = compilePattern(trimmed);
|
|
1037
|
+
if (pattern) patterns.push(pattern);
|
|
1038
|
+
}
|
|
1039
|
+
return Object.freeze(patterns);
|
|
1040
|
+
}
|
|
1041
|
+
function patternLimitError() {
|
|
1042
|
+
return new Error("Code upload policy contains an invalid path pattern.");
|
|
1043
|
+
}
|
|
1044
|
+
function tokens(glob) {
|
|
1045
|
+
const result = [];
|
|
1046
|
+
for (const [i, part] of glob.split("**/").entries()) {
|
|
1047
|
+
if (i) result.push("optDirs");
|
|
1048
|
+
for (const [j, piece] of part.split("/**").entries()) {
|
|
1049
|
+
if (j) result.push(47, "starAny");
|
|
1050
|
+
for (const byte of Buffer.from(piece)) {
|
|
1051
|
+
const token = byte === 42 ? "starSeg" : byte === 63 ? "anySeg" : byte;
|
|
1052
|
+
if (token !== "starSeg" || result.at(-1) !== token) result.push(token);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1024
1055
|
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1056
|
+
return result;
|
|
1057
|
+
}
|
|
1058
|
+
function mask(indices) {
|
|
1059
|
+
const top = indices.at(-1);
|
|
1060
|
+
const bytes = Buffer.alloc((top >> 3) + 1);
|
|
1061
|
+
for (const bit of indices) bytes[bytes.length - 1 - (bit >> 3)] |= 1 << (bit & 7);
|
|
1062
|
+
return BigInt("0x" + bytes.toString("hex"));
|
|
1063
|
+
}
|
|
1064
|
+
var ByteGlob = class {
|
|
1065
|
+
constructor(specs) {
|
|
1066
|
+
this.specs = specs;
|
|
1067
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1068
|
+
const add = (key, index) => {
|
|
1069
|
+
const values = positions.get(key);
|
|
1070
|
+
if (values) values.push(index);
|
|
1071
|
+
else positions.set(key, [index]);
|
|
1035
1072
|
};
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
let serverVersion;
|
|
1051
|
-
try {
|
|
1052
|
-
const described2 = isLoginCredential(config.token) ? await describeSession(client) : null;
|
|
1053
|
-
if (described2 !== null) {
|
|
1054
|
-
session = described2.session;
|
|
1055
|
-
serverVersion = described2.apiVersion;
|
|
1056
|
-
} else {
|
|
1057
|
-
const answered = await client.getVersioned(expert ? EXPERT_ASSIGNMENTS_PATH : "/api/v1/stats");
|
|
1058
|
-
probe = answered.envelope.data;
|
|
1059
|
-
serverVersion = answered.apiVersion;
|
|
1073
|
+
let base2 = 0;
|
|
1074
|
+
for (const [index, spec] of specs.entries()) {
|
|
1075
|
+
const sequence = [...spec.anchored ? [] : ["optPrefix"], ...tokens(spec.glob), ...spec.descendants ? ["optTail"] : []];
|
|
1076
|
+
add("start", base2);
|
|
1077
|
+
add("finals", base2 + sequence.length);
|
|
1078
|
+
this.finalIndices.set(base2 + sequence.length, index);
|
|
1079
|
+
for (const [offset, token] of sequence.entries()) {
|
|
1080
|
+
const bit = base2 + offset;
|
|
1081
|
+
add(token, bit);
|
|
1082
|
+
if (typeof token === "number") {
|
|
1083
|
+
if (spec.fold && (token >= 65 && token <= 90 || token >= 97 && token <= 122)) add(token ^ 32, bit);
|
|
1084
|
+
} else if (token !== "anySeg") add("eps", bit);
|
|
1085
|
+
}
|
|
1086
|
+
base2 += sequence.length + 1;
|
|
1060
1087
|
}
|
|
1061
|
-
|
|
1062
|
-
if (
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1088
|
+
const masks = new Map([...positions].map(([key, bits]) => [key, mask(bits)]));
|
|
1089
|
+
for (const [key, value] of masks) if (typeof key === "number") this.literals.set(key, value);
|
|
1090
|
+
const get = (key) => masks.get(key) ?? 0n;
|
|
1091
|
+
this.start = get("start");
|
|
1092
|
+
this.finals = get("finals");
|
|
1093
|
+
this.anySeg = get("anySeg");
|
|
1094
|
+
this.starSeg = get("starSeg");
|
|
1095
|
+
this.starAny = get("starAny");
|
|
1096
|
+
this.eps = get("eps");
|
|
1097
|
+
this.optDirs = get("optDirs");
|
|
1098
|
+
this.optPrefix = get("optPrefix");
|
|
1099
|
+
this.optTail = get("optTail");
|
|
1100
|
+
}
|
|
1101
|
+
specs;
|
|
1102
|
+
start;
|
|
1103
|
+
finals;
|
|
1104
|
+
literals = /* @__PURE__ */ new Map();
|
|
1105
|
+
anySeg;
|
|
1106
|
+
starSeg;
|
|
1107
|
+
starAny;
|
|
1108
|
+
eps;
|
|
1109
|
+
optDirs;
|
|
1110
|
+
optPrefix;
|
|
1111
|
+
optTail;
|
|
1112
|
+
finalIndices = /* @__PURE__ */ new Map();
|
|
1113
|
+
close(states, inner) {
|
|
1114
|
+
for (; ; ) {
|
|
1115
|
+
inner |= states & (this.optDirs | this.optPrefix);
|
|
1116
|
+
const closed = states | (states & this.eps) << 1n | (inner & this.optTail) << 1n;
|
|
1117
|
+
if (closed === states) return [states, inner];
|
|
1118
|
+
states = closed;
|
|
1067
1119
|
}
|
|
1068
|
-
throw error;
|
|
1069
1120
|
}
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1121
|
+
step(byte, states, inner) {
|
|
1122
|
+
const advance = (this.literals.get(byte) ?? 0n) | (byte === 47 ? 0n : this.anySeg);
|
|
1123
|
+
const stay = (byte === 47 ? 0n : this.starSeg) | (byte === 10 ? 0n : this.starAny);
|
|
1124
|
+
let next = (states & advance) << 1n | states & stay;
|
|
1125
|
+
let nextInner = byte === 10 ? inner & this.optPrefix : inner;
|
|
1126
|
+
if (byte === 47) {
|
|
1127
|
+
next |= (inner & (this.optDirs | this.optPrefix)) << 1n;
|
|
1128
|
+
nextInner |= states & this.optTail;
|
|
1129
|
+
}
|
|
1130
|
+
return this.close(next, nextInner);
|
|
1131
|
+
}
|
|
1132
|
+
/** Highest matching pattern preserves matchingPattern's last-match result. */
|
|
1133
|
+
matchedIndex(path) {
|
|
1134
|
+
if (!this.start) return null;
|
|
1135
|
+
let [states, inner] = this.close(this.start, 0n);
|
|
1136
|
+
const bytes = Buffer.from(path);
|
|
1137
|
+
let beforeFinalNewline = 0n;
|
|
1138
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1139
|
+
if (i === bytes.length - 1 && bytes[i] === 10) beforeFinalNewline = states;
|
|
1140
|
+
[states, inner] = this.step(bytes[i], states, inner);
|
|
1141
|
+
if (!states && !inner && !beforeFinalNewline) return null;
|
|
1078
1142
|
}
|
|
1143
|
+
const final = (states | beforeFinalNewline) & this.finals;
|
|
1144
|
+
if (!final) return null;
|
|
1145
|
+
const hex = final.toString(16);
|
|
1146
|
+
const highest = (hex.length - 1) * 4 + Math.floor(Math.log2(parseInt(hex[0], 16)));
|
|
1147
|
+
return this.finalIndices.get(highest) ?? null;
|
|
1079
1148
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
env: "HARNESS_API_URL",
|
|
1083
|
-
"repo-config": ".harness-cli/config.json",
|
|
1084
|
-
"stored-login": "stored login",
|
|
1085
|
-
default: "workspace default"
|
|
1086
|
-
};
|
|
1087
|
-
const via = config.hostSource !== void 0 ? ` (via ${hostSourceLabels[config.hostSource]})` : "";
|
|
1088
|
-
const expertLines = seat === null ? "" : `Expert: ${expertSummary(seat)}
|
|
1089
|
-
Reaches: its own assignments and the gates it owns on them (assignments, conditions, gate)
|
|
1090
|
-
`;
|
|
1091
|
-
const described = session ?? NO_SESSION;
|
|
1092
|
-
return {
|
|
1093
|
-
ok: true,
|
|
1094
|
-
json: {
|
|
1095
|
-
url: config.baseUrl,
|
|
1096
|
-
source: credentialSource,
|
|
1097
|
-
verified: true,
|
|
1098
|
-
server_version: serverVersion,
|
|
1099
|
-
host_source: config.hostSource,
|
|
1100
|
-
kind: described.kind,
|
|
1101
|
-
expires_at: described.expires_at,
|
|
1102
|
-
scopes: described.scopes,
|
|
1103
|
-
user: described.user,
|
|
1104
|
-
org: described.org,
|
|
1105
|
-
expert: seat
|
|
1106
|
-
},
|
|
1107
|
-
human: `Host: ${config.baseUrl}${via}
|
|
1108
|
-
Credential: ${credentialSource}${seat === null ? "" : ", an outside expert's pxc_ credential"}${kindLabel(described.kind)}
|
|
1109
|
-
` + sessionLines(described) + expertLines + `Verified: ok (server API ${serverVersion})
|
|
1110
|
-
`
|
|
1111
|
-
};
|
|
1112
|
-
}
|
|
1113
|
-
function kindLabel(kind) {
|
|
1114
|
-
if (kind === null) return "";
|
|
1115
|
-
if (kind === "harness-cli") return ", harness-cli (full access)";
|
|
1116
|
-
if (kind === "operator-cli") return ", operator-cli (Operator only)";
|
|
1117
|
-
return `, ${clean(kind)}`;
|
|
1118
|
-
}
|
|
1119
|
-
function sessionLines(session) {
|
|
1120
|
-
let lines = "";
|
|
1121
|
-
const user2 = session.user;
|
|
1122
|
-
if (user2 !== null) {
|
|
1123
|
-
const name = clean(user2["name"]);
|
|
1124
|
-
const email = clean(user2["email"]);
|
|
1125
|
-
lines += `User: ${name === "" ? email : email === "" ? name : `${name} <${email}>`}
|
|
1126
|
-
`;
|
|
1149
|
+
test(path) {
|
|
1150
|
+
return this.matchedIndex(path) !== null;
|
|
1127
1151
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1152
|
+
};
|
|
1153
|
+
function compilePattern(raw, {
|
|
1154
|
+
caseInsensitive = false,
|
|
1155
|
+
mode = "ignore",
|
|
1156
|
+
warnings,
|
|
1157
|
+
maxPatternBytes = MAX_PATTERN_BYTES,
|
|
1158
|
+
maxStars = MAX_PATTERN_STARS,
|
|
1159
|
+
rejectUnsupported = false
|
|
1160
|
+
} = {}) {
|
|
1161
|
+
let pattern = mode === "served" ? raw : trimIgnoreLine(raw);
|
|
1162
|
+
if (pattern === "") return null;
|
|
1163
|
+
if (pattern.startsWith("#")) {
|
|
1164
|
+
if (rejectUnsupported) throw patternLimitError();
|
|
1165
|
+
return null;
|
|
1134
1166
|
}
|
|
1135
|
-
if (
|
|
1136
|
-
|
|
1137
|
-
if (
|
|
1138
|
-
|
|
1167
|
+
if (Buffer.byteLength(pattern) > maxPatternBytes || (pattern.match(/\*/g)?.length ?? 0) > maxStars)
|
|
1168
|
+
throw patternLimitError();
|
|
1169
|
+
if (pattern.startsWith("!")) {
|
|
1170
|
+
if (rejectUnsupported) throw patternLimitError();
|
|
1171
|
+
warnings?.push(
|
|
1172
|
+
"Ignoring .harnesscodeignore negation; code-index patterns do not support !."
|
|
1173
|
+
);
|
|
1174
|
+
return null;
|
|
1139
1175
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
token,
|
|
1155
|
-
actor: null,
|
|
1156
|
-
team: null,
|
|
1157
|
-
cacheDir: ""
|
|
1158
|
-
};
|
|
1159
|
-
if (options.fetchImpl !== void 0) config.fetchImpl = options.fetchImpl;
|
|
1160
|
-
return new Client(config);
|
|
1161
|
-
}
|
|
1162
|
-
function defaultNotify(line) {
|
|
1163
|
-
process.stderr.write(named(line + "\n"));
|
|
1164
|
-
}
|
|
1165
|
-
function defaultPrompt() {
|
|
1166
|
-
const readline = createInterface({
|
|
1167
|
-
input: process.stdin,
|
|
1168
|
-
output: process.stderr
|
|
1169
|
-
});
|
|
1170
|
-
return new Promise((resolve) => {
|
|
1171
|
-
readline.question("Token: ", (answer) => {
|
|
1172
|
-
readline.close();
|
|
1173
|
-
resolve(answer);
|
|
1174
|
-
});
|
|
1176
|
+
if (pattern.includes("\\")) {
|
|
1177
|
+
if (rejectUnsupported) throw patternLimitError();
|
|
1178
|
+
warnings?.push(
|
|
1179
|
+
"A .harnesscodeignore backslash is literal, not an escape."
|
|
1180
|
+
);
|
|
1181
|
+
}
|
|
1182
|
+
if (rejectUnsupported && /[\[\]{}]/.test(pattern)) throw patternLimitError();
|
|
1183
|
+
const rooted = pattern.startsWith("/");
|
|
1184
|
+
if (mode === "ignore") pattern = pattern.replace(/^\/+|\/+$/g, "");
|
|
1185
|
+
const anchored = rooted || pattern.includes("/");
|
|
1186
|
+
return Object.freeze({
|
|
1187
|
+
source: mode === "served" ? raw : trimIgnoreLine(raw),
|
|
1188
|
+
negated: false,
|
|
1189
|
+
matcher: new ByteGlob([{ glob: pattern, anchored, descendants: mode === "ignore", fold: caseInsensitive }])
|
|
1175
1190
|
});
|
|
1176
1191
|
}
|
|
1177
|
-
function
|
|
1178
|
-
const
|
|
1179
|
-
if (
|
|
1180
|
-
|
|
1181
|
-
}
|
|
1192
|
+
function compilePatterns(lines, options = {}) {
|
|
1193
|
+
const maxPatterns = options.maxPatterns ?? MAX_PATTERNS;
|
|
1194
|
+
if (lines.length > maxPatterns) throw patternLimitError();
|
|
1195
|
+
return Object.freeze(lines.map((line) => compilePattern(line, options)).filter((pattern) => pattern !== null));
|
|
1182
1196
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
});
|
|
1197
|
+
var matcherCache = /* @__PURE__ */ new WeakMap();
|
|
1198
|
+
function matchingPattern(path, patterns) {
|
|
1199
|
+
if (!patterns.length) return null;
|
|
1200
|
+
let matcher = matcherCache.get(patterns);
|
|
1201
|
+
if (!matcher) {
|
|
1202
|
+
matcher = new ByteGlob(patterns.flatMap((pattern) => [...pattern.matcher.specs]));
|
|
1203
|
+
if (Object.isFrozen(patterns)) matcherCache.set(patterns, matcher);
|
|
1191
1204
|
}
|
|
1205
|
+
const index = matcher.matchedIndex(path);
|
|
1206
|
+
return index === null ? null : patterns[index];
|
|
1192
1207
|
}
|
|
1193
1208
|
|
|
1194
|
-
// src/core/
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1209
|
+
// src/core/code/policy.ts
|
|
1210
|
+
var MAX_FILE_BYTES = 1048576;
|
|
1211
|
+
var EXTENSIONS = [
|
|
1212
|
+
".ex",
|
|
1213
|
+
".exs",
|
|
1214
|
+
".js",
|
|
1215
|
+
".mjs",
|
|
1216
|
+
".cjs",
|
|
1217
|
+
".jsx",
|
|
1218
|
+
".ts",
|
|
1219
|
+
".tsx",
|
|
1220
|
+
".swift"
|
|
1221
|
+
];
|
|
1222
|
+
var DENY = [
|
|
1223
|
+
".env",
|
|
1224
|
+
".env.*",
|
|
1225
|
+
"secrets/**",
|
|
1226
|
+
"**/secrets/**",
|
|
1227
|
+
"*.pem",
|
|
1228
|
+
"*.key",
|
|
1229
|
+
"*.p12",
|
|
1230
|
+
"*.pfx",
|
|
1231
|
+
"*.jks",
|
|
1232
|
+
"*.keystore",
|
|
1233
|
+
"id_rsa*",
|
|
1234
|
+
"id_ecdsa*",
|
|
1235
|
+
"id_ed25519*",
|
|
1236
|
+
"*.kdbx",
|
|
1237
|
+
".npmrc",
|
|
1238
|
+
".netrc",
|
|
1239
|
+
"credentials.json",
|
|
1240
|
+
"**/.aws/**",
|
|
1241
|
+
".git/**",
|
|
1242
|
+
"node_modules/**",
|
|
1243
|
+
"_build/**",
|
|
1244
|
+
"deps/**",
|
|
1245
|
+
"dist/**",
|
|
1246
|
+
"*.lock",
|
|
1247
|
+
"package-lock.json",
|
|
1248
|
+
"*.min.js",
|
|
1249
|
+
"*.map",
|
|
1250
|
+
"priv/static/**",
|
|
1251
|
+
"src/generated/**",
|
|
1252
|
+
"DerivedData/**",
|
|
1253
|
+
"**/DerivedData/**",
|
|
1254
|
+
".build/**",
|
|
1255
|
+
"**/.build/**",
|
|
1256
|
+
"Pods/**",
|
|
1257
|
+
"Carthage/Build/**",
|
|
1258
|
+
"xcuserdata/**",
|
|
1259
|
+
"**/xcuserdata/**",
|
|
1260
|
+
"*.xcuserstate",
|
|
1261
|
+
"*.xcresult/**",
|
|
1262
|
+
"*.xcarchive/**",
|
|
1263
|
+
"*.dSYM/**",
|
|
1264
|
+
"*.ipa",
|
|
1265
|
+
"*.secret.exs"
|
|
1266
|
+
];
|
|
1267
|
+
var SECRET_PATTERN_SOURCES = [
|
|
1268
|
+
"-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----",
|
|
1269
|
+
"\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b",
|
|
1270
|
+
"\\baone_[A-Za-z0-9_-]{20,}",
|
|
1271
|
+
"\\bgh[opsur]_[A-Za-z0-9]{36}\\b",
|
|
1272
|
+
"\\b[rs]k_live_[A-Za-z0-9]{20,}",
|
|
1273
|
+
"\\bAIza[0-9A-Za-z_-]{35}\\b",
|
|
1274
|
+
"\\bxapp-\\d-[A-Za-z0-9-]{20,}",
|
|
1275
|
+
"hooks\\.slack\\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+",
|
|
1276
|
+
"\\bgithub_pat_[A-Za-z0-9_]{22,}",
|
|
1277
|
+
"\\bxox[baprs]-\\d{8,}-[A-Za-z0-9-]{10,}",
|
|
1278
|
+
"\\bsk-(?:proj-|ant-[a-z0-9]+-)?[A-Za-z0-9_-]{32,}"
|
|
1279
|
+
];
|
|
1280
|
+
var SECRETS = SECRET_PATTERN_SOURCES.map((source) => new RegExp(source));
|
|
1281
|
+
function contentMatchers(patterns) {
|
|
1282
|
+
const literalMarkers = [];
|
|
1283
|
+
for (const pattern of patterns) {
|
|
1284
|
+
if (SECRET_PATTERN_SOURCES.includes(pattern)) continue;
|
|
1285
|
+
if (!pattern || /[\\^$.*+?()[\]{}|]/.test(pattern))
|
|
1286
|
+
throw new Error("unknown regular expression");
|
|
1287
|
+
literalMarkers.push(pattern);
|
|
1288
|
+
}
|
|
1289
|
+
return literalMarkers;
|
|
1290
|
+
}
|
|
1291
|
+
function servedCase(served) {
|
|
1292
|
+
const value = served.deny_glob_case;
|
|
1293
|
+
if (value !== void 0 && value !== "ascii_insensitive")
|
|
1294
|
+
throw new Error("unknown deny glob case mode");
|
|
1295
|
+
return value;
|
|
1296
|
+
}
|
|
1297
|
+
function buildPolicy(served, ignore, includeFloor) {
|
|
1298
|
+
const warnings = [];
|
|
1299
|
+
const unsupported = [...new Set(
|
|
1300
|
+
served.include_extensions.filter((extension) => !EXTENSIONS.includes(extension))
|
|
1301
|
+
)].sort();
|
|
1302
|
+
if (unsupported.length)
|
|
1303
|
+
warnings.push(
|
|
1304
|
+
`This harness-cli does not send ${unsupported.join(", ")} files; upgrade the client.`
|
|
1305
|
+
);
|
|
1306
|
+
const insensitiveServed = servedCase(served) === "ascii_insensitive";
|
|
1307
|
+
const literalMarkers = contentMatchers(served.content_patterns);
|
|
1308
|
+
return {
|
|
1309
|
+
limit: Math.min(served.max_file_bytes, MAX_FILE_BYTES),
|
|
1310
|
+
deny: Object.freeze([
|
|
1311
|
+
...includeFloor ? compilePatterns(DENY, {
|
|
1312
|
+
caseInsensitive: true,
|
|
1313
|
+
maxPatterns: Infinity,
|
|
1314
|
+
maxPatternBytes: Infinity,
|
|
1315
|
+
maxStars: Infinity
|
|
1316
|
+
}) : [],
|
|
1317
|
+
...compilePatterns(served.deny_globs, {
|
|
1318
|
+
caseInsensitive: includeFloor || insensitiveServed,
|
|
1319
|
+
mode: "served",
|
|
1320
|
+
maxPatternBytes: 1024,
|
|
1321
|
+
maxStars: Infinity,
|
|
1322
|
+
rejectUnsupported: true
|
|
1323
|
+
})
|
|
1324
|
+
]),
|
|
1325
|
+
ignore: compileIgnorePatterns(ignore.split(/\r?\n/), warnings),
|
|
1326
|
+
patterns: SECRETS,
|
|
1327
|
+
literalMarkers,
|
|
1328
|
+
extensions: new Set(
|
|
1329
|
+
EXTENSIONS.filter((extension) => served.include_extensions.includes(extension))
|
|
1330
|
+
),
|
|
1331
|
+
warnings
|
|
1232
1332
|
};
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
if (found === null) return unknown;
|
|
1236
|
-
const { packageRoot, packageName } = found;
|
|
1237
|
-
if (existsSync(join2(packageRoot, ".git"))) {
|
|
1238
|
-
return { kind: "checkout", packageRoot, packageName, nodeModules: null };
|
|
1239
|
-
}
|
|
1240
|
-
const nodeModules = enclosingNodeModules(packageRoot);
|
|
1241
|
-
if (nodeModules === null) return { ...unknown, packageRoot, packageName };
|
|
1242
|
-
const kind = existsSync(join2(nodeModules, ".bin")) ? "local" : "global";
|
|
1243
|
-
return { kind, packageRoot, packageName, nodeModules };
|
|
1244
|
-
}
|
|
1245
|
-
function findPackageRoot(from) {
|
|
1246
|
-
let dir = dirname(from);
|
|
1247
|
-
const { root } = parse(dir);
|
|
1248
|
-
for (; ; ) {
|
|
1249
|
-
const name = packageNameAt(join2(dir, "package.json"));
|
|
1250
|
-
if (name !== null && PACKAGE_NAMES.includes(name))
|
|
1251
|
-
return { packageRoot: dir, packageName: name };
|
|
1252
|
-
if (dir === root) return null;
|
|
1253
|
-
dir = dirname(dir);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
function packageNameAt(path) {
|
|
1333
|
+
}
|
|
1334
|
+
function policyFor(served, ignore) {
|
|
1257
1335
|
try {
|
|
1258
|
-
|
|
1259
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1260
|
-
const name = parsed["name"];
|
|
1261
|
-
return typeof name === "string" ? name : null;
|
|
1336
|
+
return buildPolicy(served, ignore, true);
|
|
1262
1337
|
} catch {
|
|
1263
|
-
|
|
1338
|
+
throw new CommandError(
|
|
1339
|
+
"The server returned an invalid code upload policy; upgrade the client before code sync."
|
|
1340
|
+
);
|
|
1264
1341
|
}
|
|
1265
1342
|
}
|
|
1266
|
-
function
|
|
1343
|
+
function basePolicyFor(served, ignore) {
|
|
1267
1344
|
try {
|
|
1268
|
-
|
|
1269
|
-
readFileSync(join2(packageRoot, "package.json"), "utf8")
|
|
1270
|
-
);
|
|
1271
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
1272
|
-
const version2 = parsed["version"];
|
|
1273
|
-
return typeof version2 === "string" ? version2 : null;
|
|
1345
|
+
return buildPolicy(served, ignore, false);
|
|
1274
1346
|
} catch {
|
|
1275
|
-
|
|
1347
|
+
throw new CommandError(
|
|
1348
|
+
"The server returned an invalid code upload policy; upgrade the client before code sync."
|
|
1349
|
+
);
|
|
1276
1350
|
}
|
|
1277
1351
|
}
|
|
1278
|
-
function
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
dir = dirname(dir);
|
|
1285
|
-
}
|
|
1352
|
+
function pathReason(path, p) {
|
|
1353
|
+
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)))
|
|
1354
|
+
return "invalid_path";
|
|
1355
|
+
if (matchingPattern(path, p.deny)) return "excluded_path";
|
|
1356
|
+
if (matchingPattern(path, p.ignore)) return "ignored";
|
|
1357
|
+
return p.extensions.has(extname(path)) ? null : "unsupported_language";
|
|
1286
1358
|
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
if (match === null) return null;
|
|
1291
|
-
return {
|
|
1292
|
-
parts: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
1293
|
-
pre: match[4] ?? null
|
|
1294
|
-
};
|
|
1359
|
+
function extensionWarning(path, p) {
|
|
1360
|
+
const extension = extname(path);
|
|
1361
|
+
return !p.extensions.has(extension) && p.extensions.has(extension.toLowerCase()) ? `The server indexes ${extension.toLowerCase()} exactly; ${path} was skipped.` : null;
|
|
1295
1362
|
}
|
|
1296
|
-
function
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
if (a.parts[index] !== b.parts[index])
|
|
1302
|
-
return a.parts[index] > b.parts[index];
|
|
1303
|
-
}
|
|
1304
|
-
if (a.pre === b.pre) return false;
|
|
1305
|
-
if (a.pre === null) return true;
|
|
1306
|
-
if (b.pre === null) return false;
|
|
1307
|
-
return comparePrerelease(a.pre, b.pre) > 0;
|
|
1308
|
-
}
|
|
1309
|
-
function comparePrerelease(left, right) {
|
|
1310
|
-
const a = left.split(".");
|
|
1311
|
-
const b = right.split(".");
|
|
1312
|
-
for (let index = 0; index < Math.max(a.length, b.length); index++) {
|
|
1313
|
-
const x = a[index];
|
|
1314
|
-
const y = b[index];
|
|
1315
|
-
if (x === void 0) return -1;
|
|
1316
|
-
if (y === void 0) return 1;
|
|
1317
|
-
const xNumeric = /^\d+$/.test(x);
|
|
1318
|
-
const yNumeric = /^\d+$/.test(y);
|
|
1319
|
-
if (xNumeric && yNumeric) {
|
|
1320
|
-
if (Number(x) !== Number(y)) return Number(x) < Number(y) ? -1 : 1;
|
|
1321
|
-
} else if (xNumeric !== yNumeric) {
|
|
1322
|
-
return xNumeric ? -1 : 1;
|
|
1323
|
-
} else if (x !== y) {
|
|
1324
|
-
return x < y ? -1 : 1;
|
|
1325
|
-
}
|
|
1326
|
-
}
|
|
1327
|
-
return 0;
|
|
1363
|
+
function contentReason(bytes, p) {
|
|
1364
|
+
if (bytes.length > p.limit) return "too_large";
|
|
1365
|
+
if (bytes.includes(0) || !isUtf8(bytes)) return "binary";
|
|
1366
|
+
const text = bytes.toString("utf8");
|
|
1367
|
+
return p.patterns.some((pattern) => pattern.test(text)) || p.literalMarkers.some((marker) => text.includes(marker)) ? "secret_content" : null;
|
|
1328
1368
|
}
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1369
|
+
|
|
1370
|
+
// src/core/code/scan.ts
|
|
1371
|
+
var MAX_OVERLAY_BYTES = 64 * 1024 * 1024;
|
|
1372
|
+
var MAX_OVERLAY_CHANGES = 2e4;
|
|
1373
|
+
var O_NOFOLLOW_ANY = 536870912;
|
|
1374
|
+
function sha256(data) {
|
|
1375
|
+
return createHash("sha256").update(data).digest("hex");
|
|
1376
|
+
}
|
|
1377
|
+
function supportsDarwinNoFollowAny(osRelease = release()) {
|
|
1378
|
+
const major = Number(osRelease.split(".")[0]);
|
|
1379
|
+
return Number.isInteger(major) && major >= 20;
|
|
1380
|
+
}
|
|
1381
|
+
async function committedIgnore(root, commit) {
|
|
1382
|
+
const entry = decodeUtf8(
|
|
1383
|
+
await gitBytes(root, [
|
|
1384
|
+
"ls-tree",
|
|
1385
|
+
"-z",
|
|
1386
|
+
commit,
|
|
1387
|
+
"--",
|
|
1388
|
+
".harnesscodeignore"
|
|
1389
|
+
]),
|
|
1390
|
+
"Cannot read the committed .harnesscodeignore safely."
|
|
1391
|
+
);
|
|
1392
|
+
if (!entry) return "";
|
|
1393
|
+
const match = /^(\d+) blob ([0-9a-f]+)\t\.harnesscodeignore\0$/.exec(entry);
|
|
1394
|
+
if (!match || !["100644", "100755"].includes(match[1]))
|
|
1395
|
+
throw new LocalError(
|
|
1396
|
+
"The committed .harnesscodeignore must be a regular file before code sync."
|
|
1397
|
+
);
|
|
1398
|
+
return decodeUtf8(
|
|
1399
|
+
await gitBytes(root, ["cat-file", "blob", match[2]], 65537, "The committed .harnesscodeignore exceeds 64 KiB; reduce it before code sync."),
|
|
1400
|
+
"The committed .harnesscodeignore must be valid UTF-8 before code sync."
|
|
1401
|
+
);
|
|
1332
1402
|
}
|
|
1333
|
-
|
|
1403
|
+
var directoryFlags = constants2.O_RDONLY | constants2.O_DIRECTORY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK;
|
|
1404
|
+
async function openLinuxRoot(root) {
|
|
1405
|
+
let handle;
|
|
1334
1406
|
try {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
);
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1407
|
+
if (!root.startsWith("/"))
|
|
1408
|
+
throw new Error("repository root is not absolute");
|
|
1409
|
+
handle = await open("/", directoryFlags);
|
|
1410
|
+
for (const part of root.split("/").filter(Boolean)) {
|
|
1411
|
+
if (part === "." || part === "..")
|
|
1412
|
+
throw new Error("repository root is invalid");
|
|
1413
|
+
const next = await open(
|
|
1414
|
+
`/proc/self/fd/${handle.fd}/${part}`,
|
|
1415
|
+
directoryFlags
|
|
1416
|
+
);
|
|
1417
|
+
await handle.close();
|
|
1418
|
+
handle = next;
|
|
1419
|
+
}
|
|
1420
|
+
return handle;
|
|
1345
1421
|
} catch {
|
|
1346
|
-
|
|
1422
|
+
await handle?.close().catch(() => {
|
|
1423
|
+
});
|
|
1424
|
+
throw new LocalError(
|
|
1425
|
+
"Code sync cannot establish a safe source root on this platform."
|
|
1426
|
+
);
|
|
1347
1427
|
}
|
|
1348
1428
|
}
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1351
|
-
const temporary = `${path}.${process.pid}.tmp`;
|
|
1352
|
-
const latest = stamp.latest ?? readStamp(env, packageName)?.latest ?? null;
|
|
1429
|
+
async function openLinuxBelowRoot(root, path) {
|
|
1430
|
+
let handle = await openLinuxRoot(root);
|
|
1353
1431
|
try {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1432
|
+
const parts = path.split("/");
|
|
1433
|
+
for (const [index, part] of parts.entries()) {
|
|
1434
|
+
if (!part || part === "." || part === "..")
|
|
1435
|
+
throw new LocalError(
|
|
1436
|
+
"Cannot read an overlay file safely; invalid path."
|
|
1437
|
+
);
|
|
1438
|
+
const next = await open(
|
|
1439
|
+
`/proc/self/fd/${handle.fd}/${part}`,
|
|
1440
|
+
constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK | (index === parts.length - 1 ? 0 : constants2.O_DIRECTORY)
|
|
1441
|
+
);
|
|
1442
|
+
await handle.close();
|
|
1443
|
+
handle = next;
|
|
1361
1444
|
}
|
|
1445
|
+
return handle;
|
|
1446
|
+
} catch (error) {
|
|
1447
|
+
await handle.close().catch(() => {
|
|
1448
|
+
});
|
|
1449
|
+
throw error;
|
|
1362
1450
|
}
|
|
1363
1451
|
}
|
|
1364
|
-
function
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
(options.refresh ?? spawnRefresh)();
|
|
1375
|
-
}
|
|
1376
|
-
if (stamp !== null && stamp.latest !== null && isNewer(stamp.latest, CLI_VERSION)) {
|
|
1377
|
-
return `harness: ${stamp.latest} is available (you have ${CLI_VERSION}) \u2014 ${upgradeHint(install)}
|
|
1378
|
-
`;
|
|
1452
|
+
async function openSafe(root, path) {
|
|
1453
|
+
if (process.platform === "darwin") {
|
|
1454
|
+
if (!supportsDarwinNoFollowAny())
|
|
1455
|
+
throw new LocalError(
|
|
1456
|
+
"Code sync requires macOS 11 or later for safe source reads."
|
|
1457
|
+
);
|
|
1458
|
+
return open(
|
|
1459
|
+
join(root, path),
|
|
1460
|
+
constants2.O_RDONLY | O_NOFOLLOW_ANY | constants2.O_NONBLOCK
|
|
1461
|
+
);
|
|
1379
1462
|
}
|
|
1380
|
-
return
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1463
|
+
if (process.platform === "linux") return openLinuxBelowRoot(root, path);
|
|
1464
|
+
throw new LocalError(
|
|
1465
|
+
"Code sync cannot safely read source files on this platform."
|
|
1466
|
+
);
|
|
1384
1467
|
}
|
|
1385
|
-
function
|
|
1386
|
-
const self = process.argv[1];
|
|
1387
|
-
if (self === void 0 || self === "") return;
|
|
1468
|
+
async function readSafe(root, path, limit) {
|
|
1388
1469
|
try {
|
|
1389
|
-
const
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1470
|
+
const handle = await openSafe(root, path);
|
|
1471
|
+
try {
|
|
1472
|
+
const before = await handle.stat();
|
|
1473
|
+
if (!before.isFile()) return { reason: "not_regular_file" };
|
|
1474
|
+
if (before.size > limit) return { reason: "too_large" };
|
|
1475
|
+
const bytes = Buffer.allocUnsafe(Math.min(before.size, limit) + 1);
|
|
1476
|
+
let total = 0;
|
|
1477
|
+
while (total < bytes.length) {
|
|
1478
|
+
const { bytesRead } = await handle.read(
|
|
1479
|
+
bytes,
|
|
1480
|
+
total,
|
|
1481
|
+
bytes.length - total,
|
|
1482
|
+
null
|
|
1483
|
+
);
|
|
1484
|
+
if (bytesRead === 0) break;
|
|
1485
|
+
total += bytesRead;
|
|
1486
|
+
}
|
|
1487
|
+
const after = await handle.stat();
|
|
1488
|
+
if (total > limit) return { reason: "too_large" };
|
|
1489
|
+
if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || total !== after.size)
|
|
1490
|
+
throw new LocalError(
|
|
1491
|
+
"A source file changed during scanning; retry code sync."
|
|
1492
|
+
);
|
|
1493
|
+
return {
|
|
1494
|
+
bytes: bytes.subarray(0, total),
|
|
1495
|
+
mode: before.mode & 64 ? "100755" : "100644"
|
|
1496
|
+
};
|
|
1497
|
+
} finally {
|
|
1498
|
+
await handle.close();
|
|
1499
|
+
}
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
const code2 = error.code;
|
|
1502
|
+
if (code2 === "ENOENT") return { reason: "deleted" };
|
|
1503
|
+
if (code2 === "ELOOP") return { reason: "symlink" };
|
|
1504
|
+
if (code2 === "ENOTDIR") return { reason: "not_directory" };
|
|
1505
|
+
if (error instanceof LocalError) throw error;
|
|
1506
|
+
throw new LocalError(
|
|
1507
|
+
"Cannot read an overlay file safely; check permissions and retry code sync."
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
async function scanOverlay(id, served) {
|
|
1512
|
+
policyFor(served, "");
|
|
1513
|
+
const ignore = await readSafe(id.root, ".harnesscodeignore", 65536);
|
|
1514
|
+
if ("reason" in ignore && ignore.reason === "too_large")
|
|
1515
|
+
throw new LocalError("The local .harnesscodeignore exceeds 64 KiB; reduce it before code sync.");
|
|
1516
|
+
if ("reason" in ignore && ignore.reason !== "deleted")
|
|
1517
|
+
throw new LocalError(
|
|
1518
|
+
"Cannot read .harnesscodeignore safely; fix or remove it before code sync."
|
|
1519
|
+
);
|
|
1520
|
+
const localIgnore = "bytes" in ignore ? decodeUtf8(
|
|
1521
|
+
ignore.bytes,
|
|
1522
|
+
"The local .harnesscodeignore must be valid UTF-8 before code sync."
|
|
1523
|
+
) : "";
|
|
1524
|
+
const baseIgnore = await committedIgnore(id.root, id.commit);
|
|
1525
|
+
if (Buffer.byteLength(baseIgnore) > 65536)
|
|
1526
|
+
throw new LocalError(
|
|
1527
|
+
"The committed .harnesscodeignore exceeds 64 KiB; reduce it before code sync."
|
|
1528
|
+
);
|
|
1529
|
+
const policy2 = policyFor(served, localIgnore);
|
|
1530
|
+
const basePolicy = basePolicyFor(served, baseIgnore);
|
|
1531
|
+
const paths = await changedPaths(id.root, id.commit);
|
|
1532
|
+
const tree = decodeGitTree(await gitBytes(id.root, ["ls-tree", "-r", "-z", id.commit]));
|
|
1533
|
+
const basePaths = new Set(tree.filter((entry) => entry.regular).map((entry) => entry.path));
|
|
1534
|
+
assertDistinctPathIdentities([...tree.map((entry) => entry.path), ...paths]);
|
|
1535
|
+
const changes = [];
|
|
1536
|
+
const deleted = /* @__PURE__ */ new Set();
|
|
1537
|
+
const blobs = /* @__PURE__ */ new Map();
|
|
1538
|
+
const excluded = [];
|
|
1539
|
+
const baseWouldHold = (path) => basePaths.has(path) && pathReason(path, basePolicy) === null;
|
|
1540
|
+
const deleteBasePath = (path) => {
|
|
1541
|
+
if (baseWouldHold(path) && !deleted.has(path)) {
|
|
1542
|
+
if (changes.length >= MAX_OVERLAY_CHANGES)
|
|
1543
|
+
throw new LocalError(
|
|
1544
|
+
`overlay_too_large: the overlay exceeds ${MAX_OVERLAY_CHANGES} entries. Commit the ignore file so the base honors it.`
|
|
1545
|
+
);
|
|
1546
|
+
deleted.add(path);
|
|
1547
|
+
changes.push({ op: "delete", path });
|
|
1548
|
+
}
|
|
1549
|
+
};
|
|
1550
|
+
const candidates = new Set(paths);
|
|
1551
|
+
const warnings = /* @__PURE__ */ new Set([
|
|
1552
|
+
...policy2.warnings.map((warning) => warning.startsWith("This harness-cli") ? warning : `Local .harnesscodeignore: ${warning}`),
|
|
1553
|
+
...basePolicy.warnings.map((warning) => warning.startsWith("This harness-cli") ? warning : `Committed .harnesscodeignore: ${warning}`)
|
|
1554
|
+
]);
|
|
1555
|
+
for (const path of basePaths) {
|
|
1556
|
+
const baseReason = pathReason(path, basePolicy);
|
|
1557
|
+
const localReason = pathReason(path, policy2);
|
|
1558
|
+
if (baseReason === null && (localReason === "excluded_path" || localReason === "ignored"))
|
|
1559
|
+
deleteBasePath(path);
|
|
1560
|
+
if (baseReason === "ignored" && localReason === null) candidates.add(path);
|
|
1561
|
+
}
|
|
1562
|
+
let total = 0;
|
|
1563
|
+
for (const path of [...candidates].sort(
|
|
1564
|
+
(a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))
|
|
1565
|
+
)) {
|
|
1566
|
+
const reason = pathReason(path, policy2);
|
|
1567
|
+
if (reason) {
|
|
1568
|
+
excluded.push({ path, reason });
|
|
1569
|
+
if (reason === "ignored" || reason === "excluded_path") deleteBasePath(path);
|
|
1570
|
+
const warning = extensionWarning(path, policy2);
|
|
1571
|
+
if (warning) warnings.add(warning);
|
|
1572
|
+
continue;
|
|
1573
|
+
}
|
|
1574
|
+
const read = await readSafe(id.root, path, policy2.limit);
|
|
1575
|
+
if ("reason" in read) {
|
|
1576
|
+
if (read.reason !== "deleted")
|
|
1577
|
+
excluded.push({ path, reason: read.reason });
|
|
1578
|
+
deleteBasePath(path);
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
const content = contentReason(read.bytes, policy2);
|
|
1582
|
+
if (content) {
|
|
1583
|
+
excluded.push({ path, reason: content });
|
|
1584
|
+
deleteBasePath(path);
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
const hash2 = sha256(read.bytes);
|
|
1588
|
+
if (!blobs.has(hash2)) {
|
|
1589
|
+
total += read.bytes.length;
|
|
1590
|
+
if (total > MAX_OVERLAY_BYTES)
|
|
1591
|
+
throw new LocalError(
|
|
1592
|
+
"The overlay exceeds the 64 MiB client upload limit; split the change before syncing."
|
|
1593
|
+
);
|
|
1594
|
+
blobs.set(hash2, read.bytes);
|
|
1595
|
+
}
|
|
1596
|
+
if (changes.length >= MAX_OVERLAY_CHANGES)
|
|
1597
|
+
throw new LocalError(
|
|
1598
|
+
`overlay_too_large: the overlay exceeds ${MAX_OVERLAY_CHANGES} entries. Commit the ignore file so the base honors it.`
|
|
1599
|
+
);
|
|
1600
|
+
changes.push({
|
|
1601
|
+
op: "put",
|
|
1602
|
+
path,
|
|
1603
|
+
sha256: hash2,
|
|
1604
|
+
size: read.bytes.length,
|
|
1605
|
+
mode: read.mode
|
|
1394
1606
|
});
|
|
1395
|
-
child.unref();
|
|
1396
|
-
} catch {
|
|
1397
1607
|
}
|
|
1608
|
+
changes.sort((a, b) => Buffer.compare(Buffer.from(a.path), Buffer.from(b.path)));
|
|
1609
|
+
const digest = sha256(
|
|
1610
|
+
changes.map(
|
|
1611
|
+
(c) => c.op === "delete" ? `${c.path}\0-\0-
|
|
1612
|
+
` : `${c.path}\0${c.sha256}\0${c.mode}
|
|
1613
|
+
`
|
|
1614
|
+
).join("")
|
|
1615
|
+
);
|
|
1616
|
+
return {
|
|
1617
|
+
changes,
|
|
1618
|
+
blobs,
|
|
1619
|
+
excluded,
|
|
1620
|
+
warnings: [...warnings],
|
|
1621
|
+
digest
|
|
1622
|
+
};
|
|
1398
1623
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1624
|
+
|
|
1625
|
+
// src/core/code/state.ts
|
|
1626
|
+
import {
|
|
1627
|
+
readFile,
|
|
1628
|
+
mkdir,
|
|
1629
|
+
writeFile,
|
|
1630
|
+
rename,
|
|
1631
|
+
link,
|
|
1632
|
+
unlink,
|
|
1633
|
+
rmdir
|
|
1634
|
+
} from "fs/promises";
|
|
1635
|
+
import { join as join2, resolve, isAbsolute } from "path";
|
|
1636
|
+
import { randomUUID } from "crypto";
|
|
1637
|
+
function canonicalServer(server) {
|
|
1638
|
+
const url = new URL(server);
|
|
1639
|
+
return url.origin + url.pathname.replace(/\/+$/, "");
|
|
1640
|
+
}
|
|
1641
|
+
function validPending(pending) {
|
|
1642
|
+
if (pending === void 0) return true;
|
|
1643
|
+
if (!pending || typeof pending !== "object" || typeof pending.digest !== "string" || !/^[a-f0-9]{64}$/.test(pending.digest)) return false;
|
|
1644
|
+
const request = pending.request;
|
|
1645
|
+
return !!request && typeof request === "object" && request.mode === "full" && typeof request.base_commit === "string" && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(request.base_commit) && typeof request.policy_digest === "string" && request.policy_digest.length > 0 && request.policy_digest.length <= 256 && typeof request.client_sync_id === "string" && request.client_sync_id.length > 0 && Buffer.byteLength(request.client_sync_id) <= 255 && Array.isArray(request.changes) && request.changes.length <= 2e4 && request.changes.every((change) => change && typeof change === "object" && typeof change.path === "string" && (change.op === "delete" || change.op === "put" && typeof change.sha256 === "string" && /^[a-f0-9]{64}$/.test(change.sha256) && Number.isSafeInteger(change.size) && change.size >= 0 && ["100644", "100755"].includes(change.mode))) && (pending.syncId === void 0 || typeof pending.syncId === "string") && (pending.activationAttemptedAt === void 0 || Number.isSafeInteger(pending.activationAttemptedAt) && pending.activationAttemptedAt >= 0);
|
|
1646
|
+
}
|
|
1647
|
+
function statePath(server, org, root, repoKey, directory) {
|
|
1648
|
+
const canonical = canonicalServer(server);
|
|
1649
|
+
const base2 = directory ?? (process.env["HARNESS_CODE_STATE_DIR"] || void 0) ?? join2(resolveCacheHome(), "harness", "code");
|
|
1650
|
+
if (!isAbsolute(base2))
|
|
1651
|
+
throw new LocalError("HARNESS_CODE_STATE_DIR must be an absolute path.");
|
|
1652
|
+
return join2(
|
|
1653
|
+
resolve(base2),
|
|
1654
|
+
sha256(JSON.stringify([canonical, org.toLowerCase()])),
|
|
1655
|
+
sha256(JSON.stringify([root, repoKey])) + ".json"
|
|
1405
1656
|
);
|
|
1406
1657
|
}
|
|
1407
|
-
async function
|
|
1408
|
-
const configured = env["HARNESS_REGISTRY_URL"];
|
|
1409
|
-
const url = configured !== void 0 && configured.trim() !== "" ? configured.trim() : latestUrl(packageName);
|
|
1658
|
+
async function readState(path, server, org, root, repoKey) {
|
|
1410
1659
|
try {
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
})
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
} catch {
|
|
1423
|
-
return null;
|
|
1660
|
+
const s = JSON.parse(await readFile(path, "utf8"));
|
|
1661
|
+
if (typeof s !== "object" || s === null) throw new Error();
|
|
1662
|
+
const state2 = s;
|
|
1663
|
+
if (state2.org.toLowerCase() !== org.toLowerCase() || state2.root !== root || state2.repoKey !== repoKey || canonicalServer(state2.server) !== canonicalServer(server) || !validPending(state2.pending) || typeof state2.viewKey !== "string" || !/^[\da-f-]{36}$/.test(state2.viewKey))
|
|
1664
|
+
throw new Error();
|
|
1665
|
+
return { ...state2, server: canonicalServer(state2.server), org: state2.org.toLowerCase() };
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
if (error.code === "ENOENT") return null;
|
|
1668
|
+
throw new LocalError(
|
|
1669
|
+
"The code view state is invalid or belongs to another scope. Check HARNESS_CODE_STATE_DIR; no source was uploaded."
|
|
1670
|
+
);
|
|
1424
1671
|
}
|
|
1425
1672
|
}
|
|
1426
|
-
function
|
|
1427
|
-
const
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
reason: "already upgraded once for this command"
|
|
1433
|
-
};
|
|
1434
|
-
}
|
|
1435
|
-
if (!autoUpgradeEnabled(env))
|
|
1436
|
-
return { kind: "skipped", reason: "automatic upgrades are off" };
|
|
1437
|
-
if (argv2[2] === "mcp")
|
|
1438
|
-
return {
|
|
1439
|
-
kind: "skipped",
|
|
1440
|
-
reason: "an MCP session must not be replaced mid-stream"
|
|
1441
|
-
};
|
|
1442
|
-
const install = options.install ?? detectInstall();
|
|
1443
|
-
if (install.kind !== "global" || install.packageRoot === null) {
|
|
1444
|
-
return { kind: "skipped", reason: describeInstall(install.kind) };
|
|
1445
|
-
}
|
|
1446
|
-
const packageRoot = install.packageRoot;
|
|
1447
|
-
const installed = versionAt(packageRoot);
|
|
1448
|
-
if (installed !== null && isNewer(installed, CLI_VERSION)) {
|
|
1449
|
-
return { kind: "upgraded", code: reexec(options, env, argv2) };
|
|
1450
|
-
}
|
|
1451
|
-
const lock = takeLock(env);
|
|
1452
|
-
if (lock === null)
|
|
1453
|
-
return {
|
|
1454
|
-
kind: "skipped",
|
|
1455
|
-
reason: "another harness process is already upgrading"
|
|
1456
|
-
};
|
|
1673
|
+
async function initialState(path, server, org, root, repoKey) {
|
|
1674
|
+
const previous = await readState(path, server, org, root, repoKey);
|
|
1675
|
+
if (previous) return previous;
|
|
1676
|
+
const state2 = { server: canonicalServer(server), org: org.toLowerCase(), root, repoKey, viewKey: randomUUID() };
|
|
1677
|
+
await mkdir(join2(path, ".."), { recursive: true, mode: 448 });
|
|
1678
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
1457
1679
|
try {
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
if (after !== null && installed !== null && !isNewer(after, installed)) {
|
|
1469
|
-
return {
|
|
1470
|
-
kind: "failed",
|
|
1471
|
-
reason: `npm left ${after} installed, which is not newer than ${installed}`
|
|
1472
|
-
};
|
|
1680
|
+
await writeFile(tmp, JSON.stringify(state2) + "\n", { flag: "wx", mode: 384 });
|
|
1681
|
+
try {
|
|
1682
|
+
await link(tmp, path);
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
if (error.code !== "EEXIST") throw error;
|
|
1685
|
+
const raced = await readState(path, server, org, root, repoKey);
|
|
1686
|
+
if (!raced) throw new Error("initial state disappeared");
|
|
1687
|
+
return raced;
|
|
1473
1688
|
}
|
|
1689
|
+
return state2;
|
|
1690
|
+
} catch {
|
|
1691
|
+
throw new LocalError("Cannot save isolated code view state.");
|
|
1474
1692
|
} finally {
|
|
1475
|
-
|
|
1693
|
+
await unlink(tmp).catch(() => {
|
|
1694
|
+
});
|
|
1476
1695
|
}
|
|
1477
|
-
return { kind: "upgraded", code: reexec(options, env, argv2) };
|
|
1478
1696
|
}
|
|
1479
|
-
function
|
|
1480
|
-
|
|
1481
|
-
{ ...env, [REEXEC_SENTINEL]: "1" },
|
|
1482
|
-
argv2
|
|
1483
|
-
);
|
|
1484
|
-
}
|
|
1485
|
-
function describeInstall(kind) {
|
|
1486
|
-
switch (kind) {
|
|
1487
|
-
case "checkout":
|
|
1488
|
-
return "this is a git checkout, not an npm install";
|
|
1489
|
-
case "local":
|
|
1490
|
-
return "this copy is a project dependency, not a global install";
|
|
1491
|
-
case "npx":
|
|
1492
|
-
return "npx runs a throwaway copy";
|
|
1493
|
-
default:
|
|
1494
|
-
return "cannot tell how this CLI was installed";
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
|
-
function npmInstallGlobal(install, stillHeld) {
|
|
1498
|
-
const globalRoot = npmGlobalRoot();
|
|
1499
|
-
if (globalRoot === null)
|
|
1500
|
-
return { ok: false, detail: "npm did not say where its global root is" };
|
|
1501
|
-
if (globalRoot.includes("***")) {
|
|
1502
|
-
return {
|
|
1503
|
-
ok: false,
|
|
1504
|
-
detail: "npm redacted its own global root, so this copy's location cannot be confirmed"
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
if (install.nodeModules === null || !samePath(globalRoot, install.nodeModules)) {
|
|
1508
|
-
return {
|
|
1509
|
-
ok: false,
|
|
1510
|
-
detail: `this copy lives in ${install.nodeModules ?? "an unrecognized place"}, not npm's global root (${globalRoot})`
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1697
|
+
async function saveState(path, state2) {
|
|
1698
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
1513
1699
|
try {
|
|
1514
|
-
|
|
1700
|
+
await writeFile(tmp, JSON.stringify({ ...state2, server: canonicalServer(state2.server), org: state2.org.toLowerCase() }) + "\n", {
|
|
1701
|
+
flag: "wx",
|
|
1702
|
+
mode: 384
|
|
1703
|
+
});
|
|
1704
|
+
await rename(tmp, path);
|
|
1515
1705
|
} catch {
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
return {
|
|
1523
|
-
ok: false,
|
|
1524
|
-
detail: "lost the upgrade lock while preparing \u2014 another process took over"
|
|
1525
|
-
};
|
|
1526
|
-
}
|
|
1527
|
-
process.stderr.write(
|
|
1528
|
-
named(`harness: upgrading ${install.packageName ?? PACKAGE_NAME} \u2026
|
|
1529
|
-
`)
|
|
1530
|
-
);
|
|
1531
|
-
const args = [
|
|
1532
|
-
"install",
|
|
1533
|
-
"--global",
|
|
1534
|
-
"--ignore-scripts",
|
|
1535
|
-
"--no-audit",
|
|
1536
|
-
"--no-fund",
|
|
1537
|
-
"--no-update-notifier",
|
|
1538
|
-
"--loglevel=error",
|
|
1539
|
-
upgradeInstallSpec(install)
|
|
1540
|
-
];
|
|
1541
|
-
const result = spawnSync(npmCommand(), args, {
|
|
1542
|
-
// npm's diagnostics belong on stderr with ours; stdout stays the
|
|
1543
|
-
// command's own, so its summary is dropped rather than piped into jq.
|
|
1544
|
-
stdio: ["ignore", "ignore", "inherit"],
|
|
1545
|
-
timeout: INSTALL_TIMEOUT_MS,
|
|
1546
|
-
...npmSpawnOptions()
|
|
1547
|
-
});
|
|
1548
|
-
if (result.error !== void 0)
|
|
1549
|
-
return { ok: false, detail: result.error.message };
|
|
1550
|
-
if (result.status !== 0) {
|
|
1551
|
-
return {
|
|
1552
|
-
ok: false,
|
|
1553
|
-
detail: `npm install exited ${result.status ?? `on ${String(result.signal)}`}`
|
|
1554
|
-
};
|
|
1706
|
+
throw new LocalError(
|
|
1707
|
+
"Cannot save code sync progress; retry with the same worktree and state directory."
|
|
1708
|
+
);
|
|
1709
|
+
} finally {
|
|
1710
|
+
await unlink(tmp).catch(() => {
|
|
1711
|
+
});
|
|
1555
1712
|
}
|
|
1556
|
-
return { ok: true, detail: "" };
|
|
1557
1713
|
}
|
|
1558
|
-
function
|
|
1714
|
+
async function withSyncLock(path, action) {
|
|
1715
|
+
const lock = path + ".lock";
|
|
1716
|
+
await mkdir(join2(path, ".."), { recursive: true, mode: 448 });
|
|
1717
|
+
try {
|
|
1718
|
+
await mkdir(lock, { mode: 448 });
|
|
1719
|
+
} catch (error) {
|
|
1720
|
+
if (error.code === "EEXIST")
|
|
1721
|
+
throw new LocalError(
|
|
1722
|
+
`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}`
|
|
1723
|
+
);
|
|
1724
|
+
throw new LocalError(
|
|
1725
|
+
"Cannot lock code sync progress; check the state directory permissions."
|
|
1726
|
+
);
|
|
1727
|
+
}
|
|
1559
1728
|
try {
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1729
|
+
await writeFile(join2(lock, "owner"), `${process.pid}
|
|
1730
|
+
`, {
|
|
1731
|
+
flag: "wx",
|
|
1732
|
+
mode: 384
|
|
1564
1733
|
});
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1734
|
+
return await action();
|
|
1735
|
+
} finally {
|
|
1736
|
+
await unlink(join2(lock, "owner")).catch(() => {
|
|
1737
|
+
});
|
|
1738
|
+
try {
|
|
1739
|
+
await rmdir(lock);
|
|
1740
|
+
} catch {
|
|
1741
|
+
try {
|
|
1742
|
+
process.stderr.write(named(
|
|
1743
|
+
`harness: Warning: code sync lock cleanup failed at ${clean(lock)}. If the lock remains, confirm no sync is running before removing it.
|
|
1744
|
+
`
|
|
1745
|
+
));
|
|
1746
|
+
} catch {
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1570
1749
|
}
|
|
1571
1750
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1751
|
+
|
|
1752
|
+
// src/core/code/sync.ts
|
|
1753
|
+
async function baseReady(api, repo, commit, options) {
|
|
1754
|
+
const timeout = options.pollTimeoutMs ?? 6e5;
|
|
1755
|
+
const sleep = options.sleep ?? delay2;
|
|
1756
|
+
let waited = 0;
|
|
1757
|
+
let interval = 1e3;
|
|
1758
|
+
const started = Date.now();
|
|
1759
|
+
for (let attempts = 0; attempts < 600; attempts++) {
|
|
1760
|
+
const base2 = await api.getBase(repo, commit, options.skipped === true);
|
|
1761
|
+
if (base2.state === "complete" || base2.state === "ready") return base2;
|
|
1762
|
+
if (base2.state === "failed")
|
|
1763
|
+
throw new CommandError(
|
|
1764
|
+
"The server could not build this commit\u2019s base. Check code status --skipped, then retry after the server backoff."
|
|
1765
|
+
);
|
|
1766
|
+
const backoff = base2.retry_after_ms === 0 ? interval : base2.retry_after_ms ?? interval;
|
|
1767
|
+
if (!Number.isFinite(backoff) || backoff < 0 || Math.max(waited, Date.now() - started) + backoff > timeout)
|
|
1768
|
+
throw new CommandError(
|
|
1769
|
+
"Timed out waiting for the server-built base; retry code sync to resume."
|
|
1770
|
+
);
|
|
1771
|
+
await sleep(backoff);
|
|
1772
|
+
waited += backoff;
|
|
1773
|
+
interval = Math.min(interval * 2, 1e4);
|
|
1774
|
+
}
|
|
1775
|
+
throw new CommandError("The base is still fetching; retry code sync later.");
|
|
1776
|
+
}
|
|
1777
|
+
function stale(error) {
|
|
1778
|
+
return error instanceof HttpError && error.status === 409 && error.body["error"] === "stale_policy";
|
|
1574
1779
|
}
|
|
1575
|
-
function
|
|
1576
|
-
|
|
1780
|
+
function withinActivationRetention(attemptedAt) {
|
|
1781
|
+
const elapsed = typeof attemptedAt === "number" ? Date.now() - attemptedAt : NaN;
|
|
1782
|
+
return Number.isFinite(elapsed) && elapsed >= 0 && elapsed < 60 * 6e4;
|
|
1577
1783
|
}
|
|
1578
|
-
|
|
1579
|
-
|
|
1784
|
+
var ReplacedSyncError = class extends CommandError {
|
|
1785
|
+
constructor() {
|
|
1786
|
+
super(
|
|
1787
|
+
"The server replaced this sync before activation. Retry code sync with no other sync running for this view."
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
};
|
|
1791
|
+
function policyScope(policy2) {
|
|
1792
|
+
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)
|
|
1793
|
+
throw new CommandError(
|
|
1794
|
+
"The server did not provide a valid organization and upload policy; no source was uploaded."
|
|
1795
|
+
);
|
|
1796
|
+
policyFor(policy2, "");
|
|
1797
|
+
}
|
|
1798
|
+
async function syncCode(api, options) {
|
|
1799
|
+
const id = await identity(options.cwd);
|
|
1800
|
+
let policyRestarts = 0;
|
|
1801
|
+
let syncRestarts = 0;
|
|
1802
|
+
for (let restart = 0; restart < 3; restart++) {
|
|
1803
|
+
const policy2 = await api.getPolicy();
|
|
1804
|
+
policyScope(policy2);
|
|
1805
|
+
const path = statePath(
|
|
1806
|
+
api.origin,
|
|
1807
|
+
policy2.org_id,
|
|
1808
|
+
id.root,
|
|
1809
|
+
id.repoKey,
|
|
1810
|
+
options.stateDir
|
|
1811
|
+
);
|
|
1580
1812
|
try {
|
|
1581
|
-
return
|
|
1582
|
-
|
|
1583
|
-
|
|
1813
|
+
return await withSyncLock(path, async () => {
|
|
1814
|
+
const state2 = await initialState(
|
|
1815
|
+
path,
|
|
1816
|
+
api.origin,
|
|
1817
|
+
policy2.org_id,
|
|
1818
|
+
id.root,
|
|
1819
|
+
id.repoKey
|
|
1820
|
+
);
|
|
1821
|
+
const repo = await api.putRepository(id.repoKey);
|
|
1822
|
+
if (repo.repo_key !== id.repoKey)
|
|
1823
|
+
throw new CommandError(
|
|
1824
|
+
"The server resolved a different repository; no source was uploaded."
|
|
1825
|
+
);
|
|
1826
|
+
if (state2.repositoryId && state2.repositoryId !== repo.repository_id) {
|
|
1827
|
+
delete state2.viewId;
|
|
1828
|
+
delete state2.pending;
|
|
1829
|
+
delete state2.activated;
|
|
1830
|
+
}
|
|
1831
|
+
state2.repositoryId = repo.repository_id;
|
|
1832
|
+
state2.baseCommit = id.commit;
|
|
1833
|
+
await saveState(path, state2);
|
|
1834
|
+
const base2 = await baseReady(
|
|
1835
|
+
api,
|
|
1836
|
+
repo.repository_id,
|
|
1837
|
+
id.commit,
|
|
1838
|
+
options
|
|
1839
|
+
);
|
|
1840
|
+
if (base2.policy_digest && base2.policy_digest !== policy2.policy_digest)
|
|
1841
|
+
throw new HttpError(409, "The upload policy changed", {
|
|
1842
|
+
error: "stale_policy"
|
|
1843
|
+
});
|
|
1844
|
+
const overlay = await scanOverlay(id, policy2);
|
|
1845
|
+
const view2 = await api.putView(repo.repository_id, state2.viewKey);
|
|
1846
|
+
if (state2.viewId !== void 0 && state2.viewId !== view2.view_id && view2.repository_id === repo.repository_id && view2.view_key === state2.viewKey) {
|
|
1847
|
+
let removed = false;
|
|
1848
|
+
try {
|
|
1849
|
+
await api.getView(state2.viewId);
|
|
1850
|
+
} catch (error) {
|
|
1851
|
+
if (error instanceof HttpError && error.status === 404)
|
|
1852
|
+
removed = true;
|
|
1853
|
+
else throw error;
|
|
1854
|
+
}
|
|
1855
|
+
if (removed) {
|
|
1856
|
+
delete state2.viewId;
|
|
1857
|
+
delete state2.pending;
|
|
1858
|
+
delete state2.activated;
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
if (view2.repository_id !== repo.repository_id || view2.view_key !== state2.viewKey || state2.viewId !== void 0 && state2.viewId !== view2.view_id)
|
|
1862
|
+
throw new CommandError(
|
|
1863
|
+
"The server view does not match this repository/worktree; no source was uploaded."
|
|
1864
|
+
);
|
|
1865
|
+
state2.viewId = view2.view_id;
|
|
1866
|
+
const forgetReplaced = async () => {
|
|
1867
|
+
delete state2.pending;
|
|
1868
|
+
await saveState(path, state2);
|
|
1869
|
+
throw new ReplacedSyncError();
|
|
1870
|
+
};
|
|
1871
|
+
const receipt = async (syncId) => {
|
|
1872
|
+
try {
|
|
1873
|
+
const previous = await api.getSync(view2.view_id, syncId);
|
|
1874
|
+
if (previous.id !== syncId)
|
|
1875
|
+
throw new CommandError("The server returned a different sync; activation remains unverified.");
|
|
1876
|
+
if (previous.state === "receiving") return null;
|
|
1877
|
+
if (!Number.isSafeInteger(previous.version) || previous.version === null || previous.version < 1)
|
|
1878
|
+
throw new CommandError("The server did not return an activation version; retry code status.");
|
|
1879
|
+
return { pending_version: previous.version, state: previous.state };
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
if (error instanceof HttpError && error.status === 404) {
|
|
1882
|
+
if (withinActivationRetention(state2.pending?.activationAttemptedAt))
|
|
1883
|
+
return forgetReplaced();
|
|
1884
|
+
if (state2.pending?.activationAttemptedAt === void 0)
|
|
1885
|
+
throw new CommandError("The sync disappeared without a recorded activation attempt. Check code status before retrying code sync.");
|
|
1886
|
+
throw new CommandError("The previous activation can no longer be verified after retention. Check code status before syncing again.");
|
|
1887
|
+
}
|
|
1888
|
+
throw error;
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
const reconcileFailure = async (syncId, original) => {
|
|
1892
|
+
try {
|
|
1893
|
+
const recovered = await receipt(syncId);
|
|
1894
|
+
if (!recovered && original instanceof HttpError && original.status >= 400 && original.status < 500 && original.status !== 429 && state2.pending) {
|
|
1895
|
+
delete state2.pending.activationAttemptedAt;
|
|
1896
|
+
await saveState(path, state2);
|
|
1897
|
+
}
|
|
1898
|
+
return recovered;
|
|
1899
|
+
} catch (secondary) {
|
|
1900
|
+
if (secondary instanceof ReplacedSyncError) throw secondary;
|
|
1901
|
+
const primary = secondary instanceof CommandError ? secondary : original;
|
|
1902
|
+
const cause = primary === secondary ? original : secondary;
|
|
1903
|
+
try {
|
|
1904
|
+
if (primary instanceof Error && primary.cause === void 0 && cause !== primary) primary.cause = cause;
|
|
1905
|
+
} catch {
|
|
1906
|
+
}
|
|
1907
|
+
throw primary;
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1910
|
+
let current = state2.pending;
|
|
1911
|
+
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);
|
|
1912
|
+
if (current?.syncId && current.activationAttemptedAt !== void 0 && !matchesCurrent) {
|
|
1913
|
+
const prior = current;
|
|
1914
|
+
const originalSyncId = current.syncId;
|
|
1915
|
+
let recovered = await receipt(originalSyncId);
|
|
1916
|
+
if (!recovered) {
|
|
1917
|
+
prior.activationAttemptedAt = Date.now();
|
|
1918
|
+
await saveState(path, state2);
|
|
1919
|
+
try {
|
|
1920
|
+
recovered = await api.activate(view2.view_id, originalSyncId);
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
recovered = await reconcileFailure(originalSyncId, error);
|
|
1923
|
+
if (!recovered) throw error;
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
state2.activated = {
|
|
1927
|
+
baseCommit: prior.request.base_commit,
|
|
1928
|
+
overlayDigest: prior.digest,
|
|
1929
|
+
pendingVersion: recovered.pending_version,
|
|
1930
|
+
policyDigest: prior.request.policy_digest
|
|
1931
|
+
};
|
|
1932
|
+
delete state2.pending;
|
|
1933
|
+
await saveState(path, state2);
|
|
1934
|
+
current = void 0;
|
|
1935
|
+
}
|
|
1936
|
+
const request = matchesCurrent && current ? current.request : {
|
|
1937
|
+
client_sync_id: randomUUID2(),
|
|
1938
|
+
base_commit: id.commit,
|
|
1939
|
+
mode: "full",
|
|
1940
|
+
changes: overlay.changes,
|
|
1941
|
+
policy_digest: policy2.policy_digest
|
|
1942
|
+
};
|
|
1943
|
+
state2.pending = current && current.request === request ? current : { digest: overlay.digest, request };
|
|
1944
|
+
await saveState(path, state2);
|
|
1945
|
+
const finish = async (syncId, activated3, uploadedBlobs) => {
|
|
1946
|
+
state2.activated = {
|
|
1947
|
+
baseCommit: id.commit,
|
|
1948
|
+
overlayDigest: overlay.digest,
|
|
1949
|
+
pendingVersion: activated3.pending_version,
|
|
1950
|
+
policyDigest: policy2.policy_digest
|
|
1951
|
+
};
|
|
1952
|
+
delete state2.pending;
|
|
1953
|
+
await saveState(path, state2);
|
|
1954
|
+
return {
|
|
1955
|
+
repository_id: repo.repository_id,
|
|
1956
|
+
repo_key: id.repoKey,
|
|
1957
|
+
org_id: policy2.org_id,
|
|
1958
|
+
view_id: view2.view_id,
|
|
1959
|
+
view_key: state2.viewKey,
|
|
1960
|
+
sync_id: syncId,
|
|
1961
|
+
base_commit: id.commit,
|
|
1962
|
+
base: base2,
|
|
1963
|
+
overlay_digest: overlay.digest,
|
|
1964
|
+
changed_files: overlay.changes.length,
|
|
1965
|
+
uploaded_blobs: uploadedBlobs,
|
|
1966
|
+
excluded: overlay.excluded,
|
|
1967
|
+
warnings: overlay.warnings,
|
|
1968
|
+
policy_restarts: policyRestarts,
|
|
1969
|
+
sync_restarts: syncRestarts,
|
|
1970
|
+
...activated3
|
|
1971
|
+
};
|
|
1972
|
+
};
|
|
1973
|
+
if (state2.pending.syncId && state2.pending.activationAttemptedAt !== void 0) {
|
|
1974
|
+
const recovered = await receipt(state2.pending.syncId);
|
|
1975
|
+
if (recovered) return finish(state2.pending.syncId, recovered, 0);
|
|
1976
|
+
}
|
|
1977
|
+
const opened2 = await api.openSync(view2.view_id, request);
|
|
1978
|
+
if (opened2.rejected.length)
|
|
1979
|
+
throw new CommandError(
|
|
1980
|
+
`The server refused ${opened2.rejected.length} overlay entries; no sync was activated. Refetch the policy and check the skipped-files report.`
|
|
1981
|
+
);
|
|
1982
|
+
state2.pending.syncId = opened2.sync_id;
|
|
1983
|
+
await saveState(path, state2);
|
|
1984
|
+
if (opened2.state !== "receiving") {
|
|
1985
|
+
const recovered = await receipt(opened2.sync_id);
|
|
1986
|
+
if (recovered) return finish(opened2.sync_id, recovered, 0);
|
|
1987
|
+
throw new CommandError("The server returned an inconsistent sync state; no new version was requested.");
|
|
1988
|
+
}
|
|
1989
|
+
const missing = [...new Set(opened2.missing)];
|
|
1990
|
+
for (const hash2 of missing)
|
|
1991
|
+
if (!overlay.blobs.has(hash2))
|
|
1992
|
+
throw new CommandError(
|
|
1993
|
+
"The server requested an unexpected blob not present in this overlay; no blobs were uploaded."
|
|
1994
|
+
);
|
|
1995
|
+
let activating = false;
|
|
1996
|
+
let activated2;
|
|
1997
|
+
try {
|
|
1998
|
+
let next = 0;
|
|
1999
|
+
let failure;
|
|
2000
|
+
await Promise.all(Array.from({ length: Math.min(4, missing.length) }, async () => {
|
|
2001
|
+
while (!failure && next < missing.length) {
|
|
2002
|
+
const hash2 = missing[next++];
|
|
2003
|
+
try {
|
|
2004
|
+
await api.putBlob(hash2, overlay.blobs.get(hash2));
|
|
2005
|
+
} catch (error) {
|
|
2006
|
+
failure ??= { error };
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}));
|
|
2010
|
+
if (failure) throw failure.error;
|
|
2011
|
+
state2.pending.activationAttemptedAt = Date.now();
|
|
2012
|
+
await saveState(path, state2);
|
|
2013
|
+
activating = true;
|
|
2014
|
+
activated2 = await api.activate(view2.view_id, opened2.sync_id);
|
|
2015
|
+
} catch (error) {
|
|
2016
|
+
const replaced = error instanceof HttpError && (activating ? error.status === 404 : error.status === 409 && error.body["error"] === "unexpected_blob");
|
|
2017
|
+
if (replaced) {
|
|
2018
|
+
if (activating && !withinActivationRetention(state2.pending?.activationAttemptedAt))
|
|
2019
|
+
throw new CommandError("The activation retry exceeded its retention window. Check code status before syncing again.");
|
|
2020
|
+
return forgetReplaced();
|
|
2021
|
+
}
|
|
2022
|
+
if (activating) {
|
|
2023
|
+
const recovered = await reconcileFailure(opened2.sync_id, error);
|
|
2024
|
+
if (recovered) return finish(opened2.sync_id, recovered, missing.length);
|
|
2025
|
+
}
|
|
2026
|
+
throw error;
|
|
2027
|
+
}
|
|
2028
|
+
return finish(opened2.sync_id, activated2, missing.length);
|
|
2029
|
+
});
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
if (stale(error) && restart < 2) {
|
|
2032
|
+
policyRestarts++;
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
if (error instanceof ReplacedSyncError && restart < 2) {
|
|
2036
|
+
syncRestarts++;
|
|
2037
|
+
continue;
|
|
2038
|
+
}
|
|
2039
|
+
throw error;
|
|
1584
2040
|
}
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
2041
|
+
}
|
|
2042
|
+
throw new CommandError(
|
|
2043
|
+
"The upload policy kept changing. Retry code sync after it settles."
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
async function statusCode(api, options) {
|
|
2047
|
+
const id = await identity(options.cwd);
|
|
2048
|
+
const policy2 = await api.getPolicy();
|
|
2049
|
+
policyScope(policy2);
|
|
2050
|
+
const path = statePath(
|
|
2051
|
+
api.origin,
|
|
2052
|
+
policy2.org_id,
|
|
2053
|
+
id.root,
|
|
2054
|
+
id.repoKey,
|
|
2055
|
+
options.stateDir
|
|
2056
|
+
);
|
|
2057
|
+
const state2 = await readState(
|
|
2058
|
+
path,
|
|
2059
|
+
api.origin,
|
|
2060
|
+
policy2.org_id,
|
|
2061
|
+
id.root,
|
|
2062
|
+
id.repoKey
|
|
2063
|
+
);
|
|
2064
|
+
const repository = options.skipped ? await api.putRepository(id.repoKey) : void 0;
|
|
2065
|
+
if (repository && repository.repo_key !== id.repoKey)
|
|
2066
|
+
throw new CommandError(
|
|
2067
|
+
"The server resolved a different repository; its base status cannot be trusted."
|
|
1599
2068
|
);
|
|
1600
|
-
|
|
2069
|
+
const repositoryId = repository?.repository_id ?? state2?.repositoryId;
|
|
2070
|
+
const withoutView = async () => {
|
|
2071
|
+
const base3 = options.skipped && repositoryId ? await api.getBase(repositoryId, id.commit, true) : void 0;
|
|
2072
|
+
return {
|
|
2073
|
+
state: base3?.state === "failed" ? "failed" : base3?.state === "fetching" ? "fetching" : "not_synced",
|
|
2074
|
+
org_id: policy2.org_id,
|
|
2075
|
+
repo_key: id.repoKey,
|
|
2076
|
+
base_commit: id.commit,
|
|
2077
|
+
...base3 ? { base: base3 } : {}
|
|
2078
|
+
};
|
|
2079
|
+
};
|
|
2080
|
+
if (!state2?.viewId || repository && repositoryId !== state2.repositoryId) return withoutView();
|
|
2081
|
+
let view2;
|
|
2082
|
+
try {
|
|
2083
|
+
view2 = await api.getView(state2.viewId);
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
if (error instanceof HttpError && error.status === 404) return withoutView();
|
|
2086
|
+
throw error;
|
|
1601
2087
|
}
|
|
1602
|
-
|
|
2088
|
+
if (view2.repository_id !== state2.repositoryId || view2.view_id !== state2.viewId || view2.view_key !== state2.viewKey)
|
|
2089
|
+
throw new CommandError(
|
|
2090
|
+
"The server view does not match this repository/worktree; its status cannot be trusted."
|
|
2091
|
+
);
|
|
2092
|
+
const overlay = await scanOverlay(id, policy2);
|
|
2093
|
+
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;
|
|
2094
|
+
const base2 = options.skipped && state2.repositoryId ? await api.getBase(state2.repositoryId, id.commit, true) : void 0;
|
|
2095
|
+
return {
|
|
2096
|
+
state: base2?.state === "failed" || view2.pending?.state === "failed" ? "failed" : current ? "current" : view2.pending ? "pending" : view2.active ? "stale" : "not_synced",
|
|
2097
|
+
org_id: policy2.org_id,
|
|
2098
|
+
repo_key: id.repoKey,
|
|
2099
|
+
base_commit: id.commit,
|
|
2100
|
+
...view2,
|
|
2101
|
+
local_overlay_digest: overlay.digest,
|
|
2102
|
+
excluded: overlay.excluded,
|
|
2103
|
+
warnings: overlay.warnings,
|
|
2104
|
+
...base2 ? { base: base2 } : {}
|
|
2105
|
+
};
|
|
1603
2106
|
}
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
2107
|
+
|
|
2108
|
+
// src/core/code/query.ts
|
|
2109
|
+
import { z as z2 } from "zod";
|
|
2110
|
+
var hash = z2.string().regex(/^[0-9a-f]{64}$/);
|
|
2111
|
+
var queryView = z2.object({
|
|
2112
|
+
view_id: z2.string().uuid(),
|
|
2113
|
+
repository_id: z2.string().uuid(),
|
|
2114
|
+
view_key: z2.string().min(1),
|
|
2115
|
+
active: z2.object({ version: z2.number().int().positive(), base_commit: z2.string().regex(/^[0-9a-f]{40}$/), overlay_digest: hash }).passthrough(),
|
|
2116
|
+
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()
|
|
2117
|
+
}).passthrough();
|
|
2118
|
+
var envelopeSchema = z2.object({
|
|
2119
|
+
data: z2.unknown(),
|
|
2120
|
+
meta: z2.object({ contract: z2.literal("1.3"), view: queryView, truncation: z2.object({ truncated: z2.boolean() }).passthrough() }).passthrough()
|
|
2121
|
+
}).passthrough();
|
|
2122
|
+
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();
|
|
2123
|
+
var record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2124
|
+
var shell = (text) => "'" + text.replaceAll("'", "'\\''") + "'";
|
|
2125
|
+
function fail() {
|
|
2126
|
+
throw new CommandError("The code query API returned an invalid contract 1.3 response; no result was presented.");
|
|
2127
|
+
}
|
|
2128
|
+
function integer(value, name, min, max) {
|
|
2129
|
+
if (value !== void 0 && (!Number.isSafeInteger(value) || value < min || value > max)) throw new UsageError(`${name} must be an integer from ${min} to ${max}.`);
|
|
2130
|
+
}
|
|
2131
|
+
function validateQuery(operation, p) {
|
|
2132
|
+
const texts = [p.q, p.symbol, p.kind, p.pathPrefix, ...p.symbols ?? [], ...p.files ?? []];
|
|
2133
|
+
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).");
|
|
2134
|
+
if (operation === "search" && !p.q?.trim()) throw new UsageError("usage: harness code search <query>");
|
|
2135
|
+
if (operation === "explain" && !p.symbol) throw new UsageError("usage: harness code explain <symbol-id>");
|
|
2136
|
+
if (operation === "context" && Number(Boolean(p.symbol)) + Number(Boolean(p.q?.trim())) !== 1) throw new UsageError("code context requires exactly one query or --symbol.");
|
|
2137
|
+
if (operation === "affected") {
|
|
2138
|
+
const seeds = (p.symbols?.length ?? 0) + (p.files?.length ?? 0);
|
|
2139
|
+
if (seeds < 1 || seeds > 200) throw new UsageError("code affected needs 1 to 200 --symbol/--file seeds.");
|
|
2140
|
+
if (Buffer.byteLength(JSON.stringify(p)) > 24e4) throw new UsageError("Code affected seeds exceed the request size limit; narrow the selection.");
|
|
2141
|
+
}
|
|
2142
|
+
integer(p.limit, "limit", 1, 100);
|
|
2143
|
+
integer(p.depth, "depth", 1, 6);
|
|
2144
|
+
integer(p.budgetTokens, "budget-tokens", 100, operation === "context" ? 32e3 : 16e3);
|
|
2145
|
+
}
|
|
2146
|
+
function command(op, p) {
|
|
2147
|
+
const parts = ["harness", "code", op];
|
|
2148
|
+
if (p.q !== void 0) parts.push(shell(p.q));
|
|
2149
|
+
if (op === "explain" && p.symbol) parts.push(shell(p.symbol));
|
|
2150
|
+
else if (p.symbol) parts.push("--symbol", shell(p.symbol));
|
|
2151
|
+
for (const id of p.symbols ?? []) parts.push("--symbol", shell(id));
|
|
2152
|
+
for (const file of p.files ?? []) parts.push("--file", shell(file));
|
|
2153
|
+
for (const [key, value] of [["limit", p.limit], ["depth", p.depth], ["budget-tokens", p.budgetTokens], ["kind", p.kind], ["path-prefix", p.pathPrefix]])
|
|
2154
|
+
if (value !== void 0) parts.push(`--${key}`, typeof value === "number" ? String(value) : shell(value));
|
|
2155
|
+
if (p.includeAmbiguous) parts.push("--include-ambiguous");
|
|
2156
|
+
if (p.includeTypes) parts.push("--include-types");
|
|
2157
|
+
if (p.worktree) parts.push("--path", shell(p.worktree));
|
|
2158
|
+
return named(parts.join(" "));
|
|
2159
|
+
}
|
|
2160
|
+
function nextCommand(op, p, next) {
|
|
2161
|
+
if (!record(next)) return null;
|
|
2162
|
+
const updated = { ...p };
|
|
2163
|
+
let changed = false;
|
|
2164
|
+
for (const [key, field] of [["limit", "limit"], ["depth", "depth"], ["budget_tokens", "budgetTokens"]]) {
|
|
2165
|
+
if (key === "limit" && op !== "search" || key === "depth" && op !== "affected" || key === "budget_tokens" && op !== "explain" && op !== "context") continue;
|
|
2166
|
+
if (typeof next[key] === "number") {
|
|
2167
|
+
updated[field] = next[key];
|
|
2168
|
+
changed = true;
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
if (!changed) return null;
|
|
1607
2172
|
try {
|
|
1608
|
-
|
|
1609
|
-
writeFileSync(temporary, `${process.pid}
|
|
1610
|
-
`);
|
|
2173
|
+
validateQuery(op, updated);
|
|
1611
2174
|
} catch {
|
|
1612
2175
|
return null;
|
|
1613
2176
|
}
|
|
2177
|
+
return command(op, updated);
|
|
2178
|
+
}
|
|
2179
|
+
function renderQuery(operation, params, raw) {
|
|
2180
|
+
const parsed = envelopeSchema.safeParse(raw);
|
|
2181
|
+
if (!parsed.success) fail();
|
|
2182
|
+
const response = parsed.data;
|
|
2183
|
+
if (operation === "search" ? !Array.isArray(response.data) : !record(response.data)) fail();
|
|
2184
|
+
const { view: view2, truncation } = response.meta;
|
|
2185
|
+
const guidance = [];
|
|
2186
|
+
const lines = [`View ${view2.view_id}: version ${view2.active.version}; base ${view2.active.base_commit}; overlay ${view2.active.overlay_digest}`];
|
|
2187
|
+
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})` : ""}`);
|
|
2188
|
+
let visited = 0;
|
|
2189
|
+
const buckets = {};
|
|
2190
|
+
const walk = (value, label3, depth, enclosingSymbol, countBuckets = true) => {
|
|
2191
|
+
if (++visited > 1e5 || depth > 32) fail();
|
|
2192
|
+
if (record(value)) {
|
|
2193
|
+
const symbol = typeof value["symbol_id"] === "string" ? value["symbol_id"] : enclosingSymbol;
|
|
2194
|
+
if ("path" in value && "start_line" in value && "sha256" in value) {
|
|
2195
|
+
const citation = citationSchema.safeParse(value);
|
|
2196
|
+
if (!citation.success || citation.data.end_line < citation.data.start_line || citation.data.version !== view2.active.version) fail();
|
|
2197
|
+
const c = citation.data;
|
|
2198
|
+
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}]`);
|
|
2199
|
+
const context = command("context", { ...symbol || params.symbol ? { symbol: symbol ?? params.symbol } : { q: params.q ?? "symbol name" }, worktree: params.worktree });
|
|
2200
|
+
lines.push(c.snippet !== void 0 ? c.snippet.split("\n").map(clean).join("\n") : `Snippet not supplied; retrieve indexed text: ${context}`);
|
|
2201
|
+
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);
|
|
2202
|
+
} else for (const [key, child] of Object.entries(value)) walk(child, `${label3}.${clean(key)}`, depth + 1, symbol, countBuckets);
|
|
2203
|
+
} else if (Array.isArray(value)) {
|
|
2204
|
+
if (countBuckets && label3 !== "data") buckets[label3.slice(5)] = value.length;
|
|
2205
|
+
if (!value.length) lines.push(`${label3}: []`);
|
|
2206
|
+
value.forEach((v, i) => walk(v, `${label3}[${i}]`, depth + 1, enclosingSymbol, false));
|
|
2207
|
+
} else lines.push(`${label3}: ${clean(typeof value === "string" ? value : JSON.stringify(value))}`);
|
|
2208
|
+
};
|
|
2209
|
+
walk(response.data, "data", 0);
|
|
2210
|
+
const counts = Array.isArray(response.data) ? response.data.length : buckets;
|
|
2211
|
+
const total = typeof truncation["total"] === "number" ? truncation["total"] : response.meta["total"];
|
|
2212
|
+
lines.push(`Truncated: ${truncation.truncated ? "yes" : "no"}; returned: ${JSON.stringify(counts)}; total: ${typeof total === "number" ? total : "unknown (not supplied by server)"}`);
|
|
2213
|
+
if (truncation.truncated) {
|
|
2214
|
+
lines.push(`Truncation metadata: ${JSON.stringify(truncation)}`);
|
|
2215
|
+
const next = nextCommand(operation, params, truncation["next"]);
|
|
2216
|
+
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."}`);
|
|
2217
|
+
}
|
|
2218
|
+
const coverage = record(response.data) ? response.data["not_tracked"] : void 0;
|
|
2219
|
+
if (Array.isArray(coverage) && coverage.length) {
|
|
2220
|
+
for (const entry of coverage) if (record(entry)) guidance.push(`Coverage${entry["partial"] === true ? " (partial)" : ""}: ${clean(entry["reason"])}`);
|
|
2221
|
+
}
|
|
2222
|
+
if (Array.isArray(coverage) && coverage.length || Array.isArray(response.data) && response.data.length === 0) {
|
|
2223
|
+
const symbol = record(response.data) && record(response.data["symbol"]) ? response.data["symbol"]["name"] : void 0;
|
|
2224
|
+
guidance.push(`Search locally: rg -n --fixed-strings -- ${shell(params.q ?? (typeof symbol === "string" ? symbol : "symbol name"))} .`);
|
|
2225
|
+
}
|
|
2226
|
+
return { json: { ...response, guidance }, human: lines.concat(guidance).join("\n") + "\n" };
|
|
2227
|
+
}
|
|
2228
|
+
async function queryCode(api, operation, params, options) {
|
|
2229
|
+
validateQuery(operation, params);
|
|
2230
|
+
const id = await identity(options.cwd);
|
|
2231
|
+
const policy2 = await api.getPolicy();
|
|
2232
|
+
if (!z2.string().uuid().safeParse(policy2.org_id).success) fail();
|
|
2233
|
+
const path = statePath(api.origin, policy2.org_id, id.root, id.repoKey, options.stateDir);
|
|
2234
|
+
const state2 = await readState(path, api.origin, policy2.org_id, id.root, id.repoKey);
|
|
2235
|
+
if (!state2?.viewId || !state2.repositoryId) throw new CommandError(`This worktree has no code view. Run harness code sync --path ${shell(id.root)} first.`);
|
|
2236
|
+
const view2 = await api.getView(state2.viewId);
|
|
2237
|
+
const matches = (v) => v.view_id === state2.viewId && v.repository_id === state2.repositoryId && v.view_key === state2.viewKey;
|
|
2238
|
+
if (!matches(view2)) throw new CommandError("The code view does not match this repository/worktree; no query was sent.");
|
|
2239
|
+
if (!view2.active) throw new CommandError(`The view has no indexed version yet. Check harness code status --path ${shell(id.root)} until indexing finishes.`);
|
|
1614
2240
|
try {
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
const aged = Date.now() - holder.mtimeMs > LOCK_STALE_MS;
|
|
1623
|
-
const abandoned = holder.pid === null ? aged : !running(holder.pid) || aged;
|
|
1624
|
-
if (!abandoned) return null;
|
|
1625
|
-
try {
|
|
1626
|
-
rmSync(path, { force: true });
|
|
1627
|
-
} catch {
|
|
1628
|
-
return null;
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
}
|
|
1632
|
-
return null;
|
|
1633
|
-
} finally {
|
|
1634
|
-
try {
|
|
1635
|
-
rmSync(temporary, { force: true });
|
|
1636
|
-
} catch {
|
|
1637
|
-
}
|
|
2241
|
+
const raw = await api.query(state2.viewId, operation, params);
|
|
2242
|
+
const result = renderQuery(operation, { ...params, worktree: id.root }, raw);
|
|
2243
|
+
if (!matches(result.json.meta.view)) throw new CommandError("The query view does not match this repository/worktree; no result was presented.");
|
|
2244
|
+
return result;
|
|
2245
|
+
} catch (error) {
|
|
2246
|
+
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.`);
|
|
2247
|
+
throw error;
|
|
1638
2248
|
}
|
|
1639
2249
|
}
|
|
1640
|
-
|
|
2250
|
+
|
|
2251
|
+
// src/commands/code/index.ts
|
|
2252
|
+
function enterWorktree(options) {
|
|
2253
|
+
const refused = () => options.path === void 0 ? new LocalError("Cannot enter the code worktree safely; check the current directory.") : new UsageError("Cannot enter the code worktree safely; check --path.");
|
|
1641
2254
|
try {
|
|
1642
|
-
|
|
1643
|
-
|
|
2255
|
+
const cwd = process.cwd();
|
|
2256
|
+
const before = statSync(".");
|
|
2257
|
+
const declared = statSync(cwd);
|
|
2258
|
+
if (before.dev !== declared.dev || before.ino !== declared.ino) throw refused();
|
|
2259
|
+
const supplied = options.path ?? cwd;
|
|
2260
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(supplied) || Buffer.from(supplied, "utf8").toString("utf8") !== supplied || options.path !== void 0 && supplied.includes("\uFFFD")) throw refused();
|
|
2261
|
+
const path = resolve2(supplied), target = statSync(path);
|
|
2262
|
+
if (options.path === void 0 && (before.dev !== target.dev || before.ino !== target.ino)) throw refused();
|
|
2263
|
+
process.chdir(path);
|
|
2264
|
+
const entered = statSync(".");
|
|
2265
|
+
if (entered.dev !== target.dev || entered.ino !== target.ino) throw refused();
|
|
2266
|
+
const canonical = realpathSync(".");
|
|
2267
|
+
const pinned = statSync(canonical);
|
|
2268
|
+
if (pinned.dev !== entered.dev || pinned.ino !== entered.ino) throw refused();
|
|
2269
|
+
return { ...options, path: canonical };
|
|
1644
2270
|
} catch {
|
|
2271
|
+
throw refused();
|
|
1645
2272
|
}
|
|
1646
2273
|
}
|
|
1647
|
-
function
|
|
2274
|
+
async function sync(ctx, options = {}) {
|
|
1648
2275
|
try {
|
|
1649
|
-
const
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
return
|
|
2276
|
+
const result = await syncCode(new HttpCodeApi(ctx.config), {
|
|
2277
|
+
cwd: options.path ?? process.cwd(),
|
|
2278
|
+
skipped: options.skipped
|
|
2279
|
+
});
|
|
2280
|
+
return {
|
|
2281
|
+
json: result,
|
|
2282
|
+
exitCode: result.state === "failed" ? 1 : 0,
|
|
2283
|
+
human: `${clean(result.repo_key)}: ${clean(result.state)} version ${result.pending_version}; uploaded ${result.uploaded_blobs} overlay blobs.
|
|
2284
|
+
View: ${result.view_id}
|
|
2285
|
+
${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") : ""}
|
|
2286
|
+
${result.warnings.map((w) => `Warning: ${clean(w)}
|
|
2287
|
+
`).join("")}`
|
|
2288
|
+
};
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
throw error instanceof HttpError ? describeCodeError(error) : error;
|
|
1654
2291
|
}
|
|
1655
2292
|
}
|
|
1656
|
-
function
|
|
1657
|
-
if (pid === null) return false;
|
|
2293
|
+
async function status2(ctx, options = {}) {
|
|
1658
2294
|
try {
|
|
1659
|
-
|
|
1660
|
-
|
|
2295
|
+
const result = await statusCode(new HttpCodeApi(ctx.config), {
|
|
2296
|
+
cwd: options.path ?? process.cwd(),
|
|
2297
|
+
skipped: options.skipped
|
|
2298
|
+
});
|
|
2299
|
+
const report = "base" in result ? result.base : void 0;
|
|
2300
|
+
const pending = "pending" in result ? result.pending : null;
|
|
2301
|
+
const serverError = pending?.error ?? report?.error;
|
|
2302
|
+
const failure = serverError && typeof serverError === "object" && "message" in serverError && typeof serverError.message === "string" ? clean(serverError.message) : "";
|
|
2303
|
+
return {
|
|
2304
|
+
json: result,
|
|
2305
|
+
exitCode: result.state === "failed" ? 1 : 0,
|
|
2306
|
+
human: `${clean(result.repo_key)}: ${result.state.replace("_", " ")}${failure ? ": " + failure : ""}
|
|
2307
|
+
${report?.skipped?.map((s) => `${clean(s.path)}: ${clean(s.reason)}
|
|
2308
|
+
`).join("") ?? ""}${("warnings" in result ? result.warnings : [])?.map((w) => `Warning: ${clean(w)}
|
|
2309
|
+
`).join("") ?? ""}`
|
|
2310
|
+
};
|
|
1661
2311
|
} catch (error) {
|
|
1662
|
-
|
|
2312
|
+
throw error instanceof HttpError ? describeCodeError(error) : error;
|
|
1663
2313
|
}
|
|
1664
2314
|
}
|
|
1665
|
-
function
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
return !truthy(env["CI"]) && !truthy(env["HARNESS_NO_UPDATE_CHECK"]);
|
|
1672
|
-
}
|
|
1673
|
-
function autoUpgradeEnabled(env) {
|
|
1674
|
-
if (!checksEnabled(env)) return false;
|
|
1675
|
-
const explicit = env["HARNESS_AUTO_UPGRADE"];
|
|
1676
|
-
if (explicit !== void 0 && explicit.trim() !== "") return truthy(explicit);
|
|
1677
|
-
return true;
|
|
2315
|
+
async function query(ctx, operation, params, options = {}) {
|
|
2316
|
+
try {
|
|
2317
|
+
return await queryCode(new HttpCodeApi(ctx.config), operation, params, { cwd: options.path ?? process.cwd() });
|
|
2318
|
+
} catch (error) {
|
|
2319
|
+
throw error instanceof HttpError ? describeCodeError(error) : error;
|
|
2320
|
+
}
|
|
1678
2321
|
}
|
|
1679
2322
|
|
|
1680
2323
|
// src/cli.ts
|
|
1681
2324
|
function promptLine(question) {
|
|
1682
|
-
const readline =
|
|
2325
|
+
const readline = createInterface({
|
|
1683
2326
|
input: process.stdin,
|
|
1684
2327
|
output: process.stderr
|
|
1685
2328
|
});
|
|
1686
|
-
return new Promise((
|
|
2329
|
+
return new Promise((resolve3) => {
|
|
1687
2330
|
readline.question(question, (answer) => {
|
|
1688
2331
|
readline.close();
|
|
1689
|
-
|
|
2332
|
+
resolve3(answer);
|
|
1690
2333
|
});
|
|
1691
2334
|
});
|
|
1692
2335
|
}
|
|
1693
|
-
function exitWith(
|
|
1694
|
-
const drain = (stream) => new Promise((
|
|
2336
|
+
function exitWith(code2) {
|
|
2337
|
+
const drain = (stream) => new Promise((resolve3) => stream.write("", () => resolve3()));
|
|
1695
2338
|
return Promise.all([drain(process.stdout), drain(process.stderr)]).then(
|
|
1696
|
-
() => process.exit(
|
|
2339
|
+
() => process.exit(code2)
|
|
1697
2340
|
);
|
|
1698
2341
|
}
|
|
1699
2342
|
guardBrokenPipes();
|
|
@@ -1708,13 +2351,13 @@ program.name(invokedAs).description(
|
|
|
1708
2351
|
writeOut: (text) => process.stdout.write(named(text, invokedAs)),
|
|
1709
2352
|
writeErr: (text) => writeStderr(text)
|
|
1710
2353
|
});
|
|
1711
|
-
function
|
|
2354
|
+
function integer2(value) {
|
|
1712
2355
|
if (!/^\d+$/.test(value))
|
|
1713
2356
|
throw new InvalidArgumentError("expected a non-negative integer");
|
|
1714
2357
|
return Number.parseInt(value, 10);
|
|
1715
2358
|
}
|
|
1716
2359
|
function activityLimit(value) {
|
|
1717
|
-
const limit =
|
|
2360
|
+
const limit = integer2(value);
|
|
1718
2361
|
if (limit < 1 || limit > ACTIVITY_MAX_LIMIT) {
|
|
1719
2362
|
throw new InvalidArgumentError(
|
|
1720
2363
|
`expected an integer from 1 to ${ACTIVITY_MAX_LIMIT}`
|
|
@@ -1763,7 +2406,7 @@ async function confirmRepoLogin(baseUrl, source, path) {
|
|
|
1763
2406
|
trustHost(baseUrl);
|
|
1764
2407
|
return true;
|
|
1765
2408
|
}
|
|
1766
|
-
async function withConfig(jsonFlag, mutating,
|
|
2409
|
+
async function withConfig(jsonFlag, mutating, run3) {
|
|
1767
2410
|
const resolved = resolveConfig();
|
|
1768
2411
|
for (const warning of resolved.warnings) writeStderr(warning + "\n");
|
|
1769
2412
|
if (resolved.kind === "error") {
|
|
@@ -1796,25 +2439,25 @@ async function withConfig(jsonFlag, mutating, run2) {
|
|
|
1796
2439
|
);
|
|
1797
2440
|
return exitWith(1);
|
|
1798
2441
|
}
|
|
1799
|
-
return execute(again.config, jsonFlag, mutating,
|
|
2442
|
+
return execute(again.config, jsonFlag, mutating, run3);
|
|
1800
2443
|
}
|
|
1801
|
-
return execute(resolved.config, jsonFlag, mutating,
|
|
2444
|
+
return execute(resolved.config, jsonFlag, mutating, run3);
|
|
1802
2445
|
}
|
|
1803
|
-
async function execute(config, jsonFlag, mutating,
|
|
2446
|
+
async function execute(config, jsonFlag, mutating, run3) {
|
|
1804
2447
|
const client = new Client(config);
|
|
1805
2448
|
const hostNotice = mutating ? `\u2192 ${config.baseUrl}
|
|
1806
2449
|
` : "";
|
|
1807
2450
|
try {
|
|
1808
|
-
const output = await
|
|
2451
|
+
const output = await run3({ client, config });
|
|
1809
2452
|
writeStderr(hostNotice);
|
|
1810
2453
|
if (output.notice !== void 0) writeStderr(output.notice);
|
|
1811
2454
|
process.stdout.write(jsonFlag ? json(output.json) : output.human);
|
|
1812
2455
|
return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
|
|
1813
2456
|
} catch (error) {
|
|
1814
|
-
return
|
|
2457
|
+
return fail2(explainExpertRefusal(error, config.token), hostNotice);
|
|
1815
2458
|
}
|
|
1816
2459
|
}
|
|
1817
|
-
async function
|
|
2460
|
+
async function fail2(error, hostNotice) {
|
|
1818
2461
|
if (error instanceof UsageError) {
|
|
1819
2462
|
writeStderr(error.message + "\n");
|
|
1820
2463
|
return exitWith(2);
|
|
@@ -1858,7 +2501,7 @@ async function fail(error, hostNotice) {
|
|
|
1858
2501
|
program.command("ready").description("issues ready to work, highest priority first").option(
|
|
1859
2502
|
"--claim",
|
|
1860
2503
|
"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)"
|
|
1861
|
-
).option("--team <key>", "team filter; with --claim, the team to claim from").option("--limit <n>", "page size",
|
|
2504
|
+
).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) => {
|
|
1862
2505
|
if (opts.claim === true) requireReadyClaimArgs(opts);
|
|
1863
2506
|
await withConfig(
|
|
1864
2507
|
opts.json === true,
|
|
@@ -1866,7 +2509,7 @@ program.command("ready").description("issues ready to work, highest priority fir
|
|
|
1866
2509
|
(ctx) => ready(ctx, opts)
|
|
1867
2510
|
);
|
|
1868
2511
|
});
|
|
1869
|
-
program.command("blocked").description("issues waiting on a blocker").option("--team <key>", "team filter").option("--limit <n>", "page size",
|
|
2512
|
+
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) => {
|
|
1870
2513
|
await withConfig(
|
|
1871
2514
|
opts.json === true,
|
|
1872
2515
|
false,
|
|
@@ -1879,7 +2522,7 @@ program.command("list").description("list issues with filters").option("--status
|
|
|
1879
2522
|
).option("--created-by <who>", "me or a creator user id").option(
|
|
1880
2523
|
"--involved <who>",
|
|
1881
2524
|
"me for issues created by, assigned to, or mentioning you"
|
|
1882
|
-
).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",
|
|
2525
|
+
).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) => {
|
|
1883
2526
|
await withConfig(
|
|
1884
2527
|
opts.json === true,
|
|
1885
2528
|
false,
|
|
@@ -1888,16 +2531,16 @@ program.command("list").description("list issues with filters").option("--status
|
|
|
1888
2531
|
});
|
|
1889
2532
|
program.command("search").description(
|
|
1890
2533
|
"search by meaning \u2014 ranked, exact identifier first; use list --search for paginated filtering"
|
|
1891
|
-
).argument("[query...]").option("--limit <n>", "maximum results (default 10, capped at 20)",
|
|
2534
|
+
).argument("[query...]").option("--limit <n>", "maximum results (default 10, capped at 20)", integer2).option(
|
|
1892
2535
|
"--page <n>",
|
|
1893
2536
|
"(deprecated, ignored \u2014 results are ranked and capped)",
|
|
1894
|
-
|
|
2537
|
+
integer2
|
|
1895
2538
|
).option("--all", "(deprecated, ignored \u2014 results are ranked and capped)").option("--json", "emit JSON").action(
|
|
1896
|
-
async (
|
|
2539
|
+
async (query2, opts) => {
|
|
1897
2540
|
await withConfig(
|
|
1898
2541
|
opts.json === true,
|
|
1899
2542
|
false,
|
|
1900
|
-
(ctx) => search(ctx,
|
|
2543
|
+
(ctx) => search(ctx, query2, opts)
|
|
1901
2544
|
);
|
|
1902
2545
|
}
|
|
1903
2546
|
);
|
|
@@ -2117,7 +2760,7 @@ plan.command("create").description("create a plan from a JSON or YAML file (the
|
|
|
2117
2760
|
return planCreate(ctx, readPlanFile(opts.file), { ...opts, team: team2 });
|
|
2118
2761
|
});
|
|
2119
2762
|
});
|
|
2120
|
-
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)`,
|
|
2763
|
+
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) => {
|
|
2121
2764
|
requirePlanListArgs(opts);
|
|
2122
2765
|
await withConfig(opts.json === true, false, (ctx) => planList(ctx, opts));
|
|
2123
2766
|
});
|
|
@@ -2137,7 +2780,7 @@ plan.command("edit").description("change step executors and keys, and budgets, f
|
|
|
2137
2780
|
return planEdit(ctx, id, readPlanFile(opts.file));
|
|
2138
2781
|
});
|
|
2139
2782
|
});
|
|
2140
|
-
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",
|
|
2783
|
+
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(
|
|
2141
2784
|
async (id, opts) => {
|
|
2142
2785
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
2143
2786
|
if (id === void 0) throw new UsageError("usage: harness plan release <plan-id-or-root>");
|
|
@@ -2178,7 +2821,7 @@ var NO_OBJECTIVE_TEXT = "By reference only: never the objective's text, which th
|
|
|
2178
2821
|
program.command("jobs").description("Operator jobs you see in the workbench, newest first").option(
|
|
2179
2822
|
"--status <states>",
|
|
2180
2823
|
`comma-separated job states (${JOB_STATES.join(", ")}) or open | waiting | done`
|
|
2181
|
-
).option("--mine", "only jobs you own").option(`--limit <n>`, `jobs per page (default 20, max ${JOBS_MAX_LIMIT})`,
|
|
2824
|
+
).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", `
|
|
2182
2825
|
${NO_OBJECTIVE_TEXT}
|
|
2183
2826
|
`).action(async (opts) => {
|
|
2184
2827
|
requireJobsArgs(opts);
|
|
@@ -2195,7 +2838,7 @@ ${NO_OBJECTIVE_TEXT}
|
|
|
2195
2838
|
(ctx) => jobShow(ctx, id, { changes: opts.changes, people: opts.people })
|
|
2196
2839
|
);
|
|
2197
2840
|
});
|
|
2198
|
-
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)",
|
|
2841
|
+
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(
|
|
2199
2842
|
"after",
|
|
2200
2843
|
`
|
|
2201
2844
|
Polls the job's events every 2 s, and every 10 s while the job is paused or
|
|
@@ -2416,7 +3059,7 @@ program.command("attach-image").description("upload an image; prints markdown to
|
|
|
2416
3059
|
});
|
|
2417
3060
|
});
|
|
2418
3061
|
var project = program.command("project").description("projects");
|
|
2419
|
-
project.command("list").option("--search <query>", "full-text search").option("--state <state>", "project state filter").option("--limit <n>", "page size",
|
|
3062
|
+
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) => {
|
|
2420
3063
|
await withConfig(
|
|
2421
3064
|
opts.json === true,
|
|
2422
3065
|
false,
|
|
@@ -2464,7 +3107,7 @@ milestone.command("create").option("--project <id>", "project id (required)").op
|
|
|
2464
3107
|
);
|
|
2465
3108
|
});
|
|
2466
3109
|
var doc = program.command("doc").description("documents");
|
|
2467
|
-
doc.command("list").option("--search <query>", "full-text search").option("--limit <n>", "page size",
|
|
3110
|
+
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) => {
|
|
2468
3111
|
await withConfig(
|
|
2469
3112
|
opts.json === true,
|
|
2470
3113
|
false,
|
|
@@ -2496,8 +3139,8 @@ doc.command("update").argument("[id]").option("--title <title>").option("--conte
|
|
|
2496
3139
|
});
|
|
2497
3140
|
}
|
|
2498
3141
|
);
|
|
2499
|
-
var
|
|
2500
|
-
|
|
3142
|
+
var link2 = program.command("link").description("URLs attached to an issue (distinct from dep relations)");
|
|
3143
|
+
link2.command("list").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
|
|
2501
3144
|
await withConfig(opts.json === true, false, (ctx) => {
|
|
2502
3145
|
if (issue === void 0)
|
|
2503
3146
|
throw new UsageError("usage: harness link list <issue>");
|
|
@@ -2505,7 +3148,7 @@ link.command("list").argument("[issue]").option("--json", "emit JSON").action(as
|
|
|
2505
3148
|
return linkList(ctx, issue);
|
|
2506
3149
|
});
|
|
2507
3150
|
});
|
|
2508
|
-
|
|
3151
|
+
link2.command("add").argument("[issue]").argument("[url]").option("--title <title>", "link title").option("--json", "emit JSON").action(
|
|
2509
3152
|
async (issue, url, opts) => {
|
|
2510
3153
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
2511
3154
|
if (issue === void 0 || url === void 0) {
|
|
@@ -2518,7 +3161,7 @@ link.command("add").argument("[issue]").argument("[url]").option("--title <title
|
|
|
2518
3161
|
});
|
|
2519
3162
|
}
|
|
2520
3163
|
);
|
|
2521
|
-
|
|
3164
|
+
link2.command("update").argument("[issue]").argument("[id-or-url]").option("--url <url>", "replacement URL").option("--title <title>", "replacement title; pass an empty value to clear").option("--json", "emit JSON").action(
|
|
2522
3165
|
async (issue, idOrUrl, opts) => {
|
|
2523
3166
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
2524
3167
|
if (issue === void 0 || idOrUrl === void 0) {
|
|
@@ -2530,7 +3173,7 @@ link.command("update").argument("[issue]").argument("[id-or-url]").option("--url
|
|
|
2530
3173
|
});
|
|
2531
3174
|
}
|
|
2532
3175
|
);
|
|
2533
|
-
|
|
3176
|
+
link2.command("rm").argument("[issue]").argument("[id-or-url]").option("--json", "emit JSON").action(
|
|
2534
3177
|
async (issue, idOrUrl, opts) => {
|
|
2535
3178
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
2536
3179
|
if (issue === void 0 || idOrUrl === void 0) {
|
|
@@ -2648,14 +3291,14 @@ var tools = program.command("tools").description(
|
|
|
2648
3291
|
requireToolsOpts(opts);
|
|
2649
3292
|
await withConfig(opts.json === true, false, (ctx) => toolsList(ctx, opts));
|
|
2650
3293
|
});
|
|
2651
|
-
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 (
|
|
3294
|
+
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) => {
|
|
2652
3295
|
const merged = { ...tools.opts(), ...opts };
|
|
2653
|
-
if (
|
|
3296
|
+
if (query2.length === 0) {
|
|
2654
3297
|
throw new UsageError("usage: harness tools search <query> [--domain <domain>] [--all]");
|
|
2655
3298
|
}
|
|
2656
|
-
requireSearchQuery(
|
|
3299
|
+
requireSearchQuery(query2.join(" "));
|
|
2657
3300
|
requireToolsOpts(merged);
|
|
2658
|
-
await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx,
|
|
3301
|
+
await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx, query2.join(" "), merged));
|
|
2659
3302
|
});
|
|
2660
3303
|
tools.command("domains").description("every domain Operator can use capabilities in, with how many").option("--json", "emit JSON").action(async (opts) => {
|
|
2661
3304
|
const parent = tools.opts();
|
|
@@ -2674,7 +3317,7 @@ program.command("skills").description("the organization's skills assigned to Ope
|
|
|
2674
3317
|
await withConfig(opts.json === true, false, (ctx) => skillsList(ctx));
|
|
2675
3318
|
});
|
|
2676
3319
|
program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
|
|
2677
|
-
const { startMcpServer } = await import("./server-
|
|
3320
|
+
const { startMcpServer } = await import("./server-TRXBTJSA.js");
|
|
2678
3321
|
await startMcpServer(invokedAs);
|
|
2679
3322
|
});
|
|
2680
3323
|
var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
|
|
@@ -2688,14 +3331,48 @@ keys.command("exec").description(
|
|
|
2688
3331
|
"pass only these provider slugs (comma-separated, repeatable); default: everything stored",
|
|
2689
3332
|
collect,
|
|
2690
3333
|
[]
|
|
2691
|
-
).option("--json", "accepted for uniformity; stdout belongs to the command, so it changes nothing").passThroughOptions().action(async (
|
|
3334
|
+
).option("--json", "accepted for uniformity; stdout belongs to the command, so it changes nothing").passThroughOptions().action(async (command2, opts) => {
|
|
2692
3335
|
const only = opts.only.flatMap((value) => value.split(","));
|
|
2693
3336
|
await withConfig(
|
|
2694
3337
|
false,
|
|
2695
3338
|
false,
|
|
2696
|
-
(ctx) => exec(ctx, { command, override: opts.override === true, only })
|
|
3339
|
+
(ctx) => exec(ctx, { command: command2, override: opts.override === true, only })
|
|
2697
3340
|
);
|
|
2698
3341
|
});
|
|
3342
|
+
var code = program.command("code").description("sync and inspect a code-index worktree overlay");
|
|
3343
|
+
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) => {
|
|
3344
|
+
const target = enterWorktree(opts);
|
|
3345
|
+
await withConfig(opts.json === true, true, (ctx) => sync(ctx, target));
|
|
3346
|
+
});
|
|
3347
|
+
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) => {
|
|
3348
|
+
const target = enterWorktree(opts);
|
|
3349
|
+
await withConfig(opts.json === true, opts.skipped === true, (ctx) => status2(ctx, target));
|
|
3350
|
+
});
|
|
3351
|
+
for (const operation of ["search", "explain", "affected", "context"]) {
|
|
3352
|
+
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]);
|
|
3353
|
+
if (operation === "search" || operation === "explain" || operation === "context") query2.argument("[value]");
|
|
3354
|
+
if (operation === "search") query2.option("--kind <kind>").option("--path-prefix <prefix>").option("--limit <n>", "1 to 100", integer2);
|
|
3355
|
+
if (operation === "context") query2.option("--symbol <id>", "opaque ID from search");
|
|
3356
|
+
if (operation === "context" || operation === "explain") query2.option("--budget-tokens <n>", "indexed snippet token budget", integer2);
|
|
3357
|
+
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");
|
|
3358
|
+
query2.option("--path <directory>", "Git worktree").option("--json", "emit response data, meta and guidance");
|
|
3359
|
+
const run3 = async (value, opts) => {
|
|
3360
|
+
const target = enterWorktree({ path: opts["path"] });
|
|
3361
|
+
const params = {
|
|
3362
|
+
...operation === "search" || operation === "context" ? { q: value } : {},
|
|
3363
|
+
...operation === "explain" ? { symbol: value } : operation === "context" ? { symbol: opts["symbol"] } : {},
|
|
3364
|
+
...operation === "affected" ? { symbols: opts["symbol"], files: opts["file"], depth: opts["depth"], includeAmbiguous: opts["includeAmbiguous"], includeTypes: opts["includeTypes"] } : {},
|
|
3365
|
+
kind: opts["kind"],
|
|
3366
|
+
pathPrefix: opts["pathPrefix"],
|
|
3367
|
+
limit: opts["limit"],
|
|
3368
|
+
budgetTokens: opts["budgetTokens"]
|
|
3369
|
+
};
|
|
3370
|
+
validateQuery(operation, params);
|
|
3371
|
+
await withConfig(opts["json"] === true, false, (ctx) => query(ctx, operation, params, target));
|
|
3372
|
+
};
|
|
3373
|
+
if (operation === "affected") query2.action(async (opts) => run3(void 0, opts));
|
|
3374
|
+
else query2.action(run3);
|
|
3375
|
+
}
|
|
2699
3376
|
var review = program.command("review").description("a pull request's enforced Review Council gate, and a retry of its latest attempt");
|
|
2700
3377
|
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) => {
|
|
2701
3378
|
const ref = parsePullRequest(pr, opts.repo);
|
|
@@ -2710,7 +3387,7 @@ review.command("retry").description(
|
|
|
2710
3387
|
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(
|
|
2711
3388
|
"--timeout <ms>",
|
|
2712
3389
|
"how long to wait for the browser callback",
|
|
2713
|
-
|
|
3390
|
+
integer2
|
|
2714
3391
|
).option("--json", "emit JSON").action(
|
|
2715
3392
|
async (opts) => {
|
|
2716
3393
|
const repoLookup = findRepoConfig();
|
|
@@ -2801,6 +3478,10 @@ try {
|
|
|
2801
3478
|
if (notice !== null) writeStderr(notice);
|
|
2802
3479
|
} catch {
|
|
2803
3480
|
}
|
|
3481
|
+
if (invokedAs === "operator") {
|
|
3482
|
+
const deprecation = operatorDeprecationNotice();
|
|
3483
|
+
if (deprecation !== null) writeStderr(deprecation);
|
|
3484
|
+
}
|
|
2804
3485
|
if (invokedAs === "operator") layoutOperatorHelp(program);
|
|
2805
3486
|
if (argv.length <= 2) {
|
|
2806
3487
|
program.outputHelp({ error: true });
|