@allocator-one/infraone 0.0.0-stage → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,3329 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ UPDATE_CHECK_COMMAND,
4
+ guardBrokenPipes,
5
+ operatorDeprecationNotice,
6
+ performUpdateCheck,
7
+ selfHeal,
8
+ updateCheck
9
+ } from "./chunk-A4WZAC7L.js";
10
+ import {
11
+ ACTIVITY_MAX_LIMIT,
12
+ EXIT_INTERRUPTED,
13
+ EXIT_JOB_ENDED,
14
+ EXIT_WAITING_FOR_PERSON,
15
+ JOBS_MAX_LIMIT,
16
+ JOBS_MAX_PAGES,
17
+ JOB_STATES,
18
+ PLAN_LIST_MAX_LIMIT,
19
+ PLAN_STATUSES,
20
+ RELATION_CREATE_TYPES,
21
+ RELATION_UPDATE_TYPES,
22
+ STEER_MAX_CHARACTERS,
23
+ activity,
24
+ archive,
25
+ assignments,
26
+ attachImage,
27
+ blocked,
28
+ close,
29
+ comment,
30
+ comments,
31
+ conditionList,
32
+ create,
33
+ deleteComment,
34
+ dep,
35
+ docCreate,
36
+ docList,
37
+ docShow,
38
+ docUpdate,
39
+ editComment,
40
+ gateSet,
41
+ instructionFrom,
42
+ jobCancel,
43
+ jobContinue,
44
+ jobPause,
45
+ jobShow,
46
+ jobSteer,
47
+ jobWatch,
48
+ jobsList,
49
+ labelCreate,
50
+ labelDelete,
51
+ labelList,
52
+ labelUpdate,
53
+ linkAdd,
54
+ linkList,
55
+ linkRm,
56
+ linkUpdate,
57
+ list,
58
+ memberAdd,
59
+ memberList,
60
+ memberSetRole,
61
+ milestoneCreate,
62
+ milestoneList,
63
+ planCreate,
64
+ planEdit,
65
+ planHold,
66
+ planList,
67
+ planRelease,
68
+ planShow,
69
+ prepareProcedureRun,
70
+ prepareRun,
71
+ prime,
72
+ procedureSave,
73
+ procedureShow,
74
+ projectCreate,
75
+ projectList,
76
+ projectShow,
77
+ projectUpdate,
78
+ readPlanFile,
79
+ ready,
80
+ relationAdd,
81
+ relationList,
82
+ relationRm,
83
+ relationUpdate,
84
+ requireCapabilityId,
85
+ requireCloseArgs,
86
+ requireCommentId,
87
+ requireIdentifiers,
88
+ requireJobId,
89
+ requireJobsArgs,
90
+ requirePlanListArgs,
91
+ requirePlanRef,
92
+ requireProcedureId,
93
+ requireProcedureName,
94
+ requireReadyClaimArgs,
95
+ requireSchemaView,
96
+ requireSearchQuery,
97
+ requireToolsOpts,
98
+ requiredTeam,
99
+ resolveTextSources,
100
+ run,
101
+ search,
102
+ show,
103
+ skillsList,
104
+ startProcedureRun,
105
+ stateAdd,
106
+ stateDelete,
107
+ stateOrder,
108
+ stateUpdate,
109
+ stats,
110
+ teamCreate,
111
+ teamList,
112
+ teamRename,
113
+ teamSetDefault,
114
+ toolShow,
115
+ toolsDomains,
116
+ toolsList,
117
+ toolsSearch,
118
+ update,
119
+ upload,
120
+ userList,
121
+ validateIdempotencyKey,
122
+ version,
123
+ watchStopSignal,
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";
153
+ import {
154
+ CLI_VERSION,
155
+ invokedName,
156
+ layoutOperatorHelp,
157
+ named
158
+ } from "./chunk-5THYIKLW.js";
159
+
160
+ // src/cli.ts
161
+ import { createInterface } from "readline";
162
+ import { Command, CommanderError, InvalidArgumentError } from "commander";
163
+
164
+ // src/commands/keys.ts
165
+ import { spawn } from "child_process";
166
+ import { constants } from "os";
167
+ var KEY_ENV_VARS = /* @__PURE__ */ new Map([
168
+ ["anthropic", "ANTHROPIC_API_KEY"],
169
+ ["openai", "OPENAI_API_KEY"],
170
+ ["google", "GEMINI_API_KEY"],
171
+ ["openrouter", "OPENROUTER_API_KEY"],
172
+ ["brave", "BRAVE_API_KEY"],
173
+ ["perplexity", "PERPLEXITY_API_KEY"]
174
+ ]);
175
+ function isRecord(value) {
176
+ return typeof value === "object" && value !== null && !Array.isArray(value);
177
+ }
178
+ async function fetchKeys(ctx) {
179
+ let body;
180
+ try {
181
+ body = await ctx.client.get("/api/v1/model-keys");
182
+ } catch (error) {
183
+ if (error instanceof HttpError && error.status === 403 && error.message.includes("model-keys:read")) {
184
+ throw new CommandError(
185
+ `${error.message} \u2014 login tokens carry model-keys:read; aone_ API tokens must be minted with it`
186
+ );
187
+ }
188
+ throw error;
189
+ }
190
+ const data = isRecord(body) ? body["data"] : void 0;
191
+ const keys2 = isRecord(data) ? data["keys"] : void 0;
192
+ if (!isRecord(keys2)) throw new CommandError("unexpected model-keys response: no keys map");
193
+ const stored = /* @__PURE__ */ new Map();
194
+ for (const [slug, value] of Object.entries(keys2)) {
195
+ const canonical = slug.trim().toLowerCase();
196
+ if (stored.has(canonical)) continue;
197
+ if (typeof value === "string" && value.trim() !== "" && !value.includes("\0")) stored.set(canonical, value);
198
+ }
199
+ return stored;
200
+ }
201
+ function envVarFor(slug) {
202
+ return KEY_ENV_VARS.get(slug.trim().toLowerCase());
203
+ }
204
+ function normalizeSlugs(only) {
205
+ return [...new Set(only.map((slug) => slug.trim().toLowerCase()).filter((slug) => slug !== ""))];
206
+ }
207
+ function selectSlugs(stored, only) {
208
+ if (only === void 0 || only.length === 0) return new Map(stored);
209
+ const wanted = new Set(only.map((slug) => slug.trim().toLowerCase()).filter((slug) => slug !== ""));
210
+ return new Map([...stored].filter(([slug]) => wanted.has(slug)));
211
+ }
212
+ function unknownSlugs(only) {
213
+ return only.map((slug) => slug.trim()).filter((slug) => slug !== "" && !KEY_ENV_VARS.has(slug.toLowerCase()));
214
+ }
215
+ function withholdOthers(base2, requested) {
216
+ const keep = new Set(requested.map((slug) => envVarFor(slug)));
217
+ const env = { ...base2 };
218
+ for (const name of KEY_ENV_VARS.values()) {
219
+ if (!keep.has(name)) delete env[name];
220
+ }
221
+ return env;
222
+ }
223
+ function unstoredSlugs(requested, stored) {
224
+ const canonical = [...new Set(requested.map((slug) => slug.trim().toLowerCase()).filter((slug) => slug !== ""))];
225
+ return canonical.filter((slug) => !stored.has(slug));
226
+ }
227
+ function toEnv(stored) {
228
+ const env = {};
229
+ for (const [slug, value] of stored) {
230
+ const name = envVarFor(slug);
231
+ if (name !== void 0) env[name] = value;
232
+ }
233
+ return env;
234
+ }
235
+ function childEnv(base2, fetched, override) {
236
+ const env = {};
237
+ for (const [name, value] of Object.entries(base2)) {
238
+ if (value !== void 0) env[name] = value;
239
+ }
240
+ for (const [name, value] of Object.entries(fetched)) {
241
+ const existing = env[name];
242
+ if (override || existing === void 0 || existing.trim() === "") env[name] = value;
243
+ }
244
+ return env;
245
+ }
246
+ function exitCodeFor(code2, signal) {
247
+ if (code2 !== null) return code2;
248
+ if (signal !== null) {
249
+ const number = constants.signals[signal];
250
+ if (typeof number === "number") return 128 + number;
251
+ }
252
+ return 1;
253
+ }
254
+ async function list2(ctx) {
255
+ const stored = await fetchKeys(ctx);
256
+ const providers = [...stored.keys()].sort().map((provider) => ({ provider, env: envVarFor(provider) ?? null }));
257
+ const human = providers.map(({ provider, env }) => `${clean(provider)} ${env ?? "(no env var)"}
258
+ `).join("");
259
+ return { json: { providers }, human };
260
+ }
261
+ var FORWARDED_SIGNALS = ["SIGTERM"];
262
+ var HELD_SIGNALS = ["SIGINT"];
263
+ async function exec(ctx, opts) {
264
+ const [program2, ...args] = opts.command;
265
+ if (program2 === void 0 || program2 === "") {
266
+ throw new UsageError("keys exec: name the command to run after `--`, e.g. `harness keys exec -- npm test`");
267
+ }
268
+ if (process.platform === "win32") {
269
+ throw new LocalError("keys exec is available on macOS and Linux only");
270
+ }
271
+ const onlyGiven = opts.only !== void 0 && opts.only.length > 0;
272
+ if (onlyGiven && normalizeSlugs(opts.only ?? []).length === 0) {
273
+ throw new UsageError(`keys exec: --only needs at least one provider slug \u2014 known: ${[...KEY_ENV_VARS.keys()].join(", ")}`);
274
+ }
275
+ const unknown = unknownSlugs(opts.only ?? []);
276
+ if (unknown.length > 0) {
277
+ throw new UsageError(
278
+ `keys exec: unknown provider slug ${unknown.map(clean).join(", ")} \u2014 known: ${[...KEY_ENV_VARS.keys()].join(", ")}`
279
+ );
280
+ }
281
+ const stored = await fetchKeys(ctx);
282
+ const requested = normalizeSlugs(opts.only ?? []);
283
+ const narrowed = requested.length > 0;
284
+ if (narrowed) {
285
+ const missing = unstoredSlugs(requested, stored);
286
+ if (missing.length > 0) {
287
+ process.stderr.write(
288
+ named(
289
+ `harness: no key stored for ${missing.join(", ")} \u2014 the command gets your shell's own variable for those, if any
290
+ `
291
+ )
292
+ );
293
+ }
294
+ }
295
+ const env = childEnv(
296
+ narrowed ? withholdOthers(process.env, requested) : process.env,
297
+ toEnv(selectSlugs(stored, requested)),
298
+ opts.override
299
+ );
300
+ const name = clean(program2);
301
+ const exitCode = await new Promise((resolve3, reject) => {
302
+ let child;
303
+ try {
304
+ child = spawn(program2, args, { stdio: "inherit", env });
305
+ } catch (error) {
306
+ const code2 = error.code ?? "invalid arguments";
307
+ reject(new CommandError(`cannot start ${name}: ${code2}`));
308
+ return;
309
+ }
310
+ const handlers = [];
311
+ for (const signal of FORWARDED_SIGNALS) {
312
+ const handler = () => {
313
+ child.kill(signal);
314
+ };
315
+ process.on(signal, handler);
316
+ handlers.push([signal, handler]);
317
+ }
318
+ for (const signal of HELD_SIGNALS) {
319
+ const handler = () => {
320
+ };
321
+ process.on(signal, handler);
322
+ handlers.push([signal, handler]);
323
+ }
324
+ let settled = false;
325
+ let spawned = false;
326
+ const settle = (outcome) => {
327
+ if (settled) return;
328
+ settled = true;
329
+ for (const [signal, handler] of handlers) process.off(signal, handler);
330
+ outcome();
331
+ };
332
+ child.once("spawn", () => {
333
+ spawned = true;
334
+ });
335
+ child.on("error", (error) => {
336
+ settle(() => {
337
+ const code2 = error.code ?? error.message;
338
+ reject(new CommandError(spawned ? `${name} failed: ${code2}` : `cannot run ${name}: ${code2}`));
339
+ });
340
+ });
341
+ child.on("exit", (code2, signal) => {
342
+ settle(() => resolve3(exitCodeFor(code2, signal)));
343
+ });
344
+ });
345
+ return { json: { exit_code: exitCode }, human: "", exitCode };
346
+ }
347
+
348
+ // src/commands/review.ts
349
+ import { execFileSync } from "child_process";
350
+ function isRecord2(value) {
351
+ return typeof value === "object" && value !== null && !Array.isArray(value);
352
+ }
353
+ var SLUG = /^([\w.-]+)\/([\w.-]+)$/;
354
+ function githubSlug(remote) {
355
+ const match = /^(?:(?:https?|ssh|git):\/\/(?:[^@/\s]+@)?|[\w.-]+@)github\.com[:/]([\w.-]+)\/([\w.-]+?)(?:\.git)?\/?$/i.exec(remote.trim());
356
+ return match ? `${match[1]}/${match[2]}` : void 0;
357
+ }
358
+ function originRemote() {
359
+ try {
360
+ const url = execFileSync("git", ["remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
361
+ return githubSlug(url);
362
+ } catch {
363
+ return void 0;
364
+ }
365
+ }
366
+ function prNumber(digits, input) {
367
+ const number = Number(digits);
368
+ if (!Number.isSafeInteger(number) || number < 1) throw new UsageError(`not a pull request number: ${input}`);
369
+ return number;
370
+ }
371
+ var NAME = /^[\w.-]+$/;
372
+ function fromUrl(input) {
373
+ let url;
374
+ try {
375
+ url = new URL(input);
376
+ } catch {
377
+ return null;
378
+ }
379
+ if (!["https:", "http:"].includes(url.protocol) || !/^(www\.)?github\.com$/i.test(url.hostname)) return null;
380
+ let segments;
381
+ try {
382
+ segments = url.pathname.split("/").filter((segment) => segment !== "").map(decodeURIComponent);
383
+ } catch {
384
+ return null;
385
+ }
386
+ const [owner, repo, kind, number] = segments;
387
+ if (kind !== "pull" || !owner || !repo || !number || !NAME.test(owner) || !NAME.test(repo) || !/^\d+$/.test(number)) {
388
+ return null;
389
+ }
390
+ return [owner, repo, number];
391
+ }
392
+ function parsePullRequest(input, repo, origin = originRemote) {
393
+ const trimmed = input.trim();
394
+ const url = fromUrl(trimmed);
395
+ if (url) return { owner: url[0], repo: url[1], number: prNumber(url[2], input) };
396
+ const named2 = /^([\w.-]+)\/([\w.-]+)#(\d+)$/.exec(trimmed);
397
+ if (named2) return { owner: named2[1], repo: named2[2], number: prNumber(named2[3], input) };
398
+ const bare = /^#?(\d+)$/.exec(trimmed);
399
+ if (!bare) throw new UsageError(`not a pull request: ${input} \u2014 use owner/repo#<pr>, its URL, or <pr> with --repo`);
400
+ const slug = repo ?? origin();
401
+ const parts = slug === void 0 ? null : SLUG.exec(slug.trim());
402
+ if (!parts) throw new UsageError("name the repository: owner/repo#<pr>, the pull request URL, or --repo owner/repo");
403
+ return { owner: parts[1], repo: parts[2], number: prNumber(bare[1], input) };
404
+ }
405
+ var label = (pr) => `${pr.owner}/${pr.repo}#${pr.number}`;
406
+ var gatePath = (pr) => `/api/v1/reviews/prs/${encodeURIComponent(pr.owner)}/${encodeURIComponent(pr.repo)}/${pr.number}`;
407
+ async function fetchGate(ctx, pr) {
408
+ const body = await ctx.client.get(gatePath(pr));
409
+ if (!isRecord2(body.data)) throw new CommandError(`unexpected gate status for ${label(pr)}: no data`);
410
+ return body.data;
411
+ }
412
+ function latestAttempt(lifecycle) {
413
+ const attempts = Array.isArray(lifecycle["attempts"]) ? lifecycle["attempts"].filter(isRecord2) : [];
414
+ return attempts.reduce(
415
+ (latest, attempt) => latest === void 0 || Number(attempt["number"]) > Number(latest["number"]) ? attempt : latest,
416
+ void 0
417
+ );
418
+ }
419
+ function recovery(lifecycle) {
420
+ const latest = latestAttempt(lifecycle);
421
+ const sent = isRecord2(lifecycle["recovery"]) ? lifecycle["recovery"] : void 0;
422
+ const state2 = String(sent?.["state"] ?? latest?.["state"] ?? "");
423
+ if (state2 !== "failed" && state2 !== "timed_out") return void 0;
424
+ const reason = sent?.["terminal_reason"] ?? latest?.["terminal_reason"];
425
+ return {
426
+ number: latest?.["number"],
427
+ state: state2 === "timed_out" ? "timed out" : state2,
428
+ reason: typeof reason === "string" ? reason : null,
429
+ evidence: sent ? sent["evidence_recorded"] === true : latest?.["review_run_id"] != null,
430
+ next: sent?.["next"] === "automatic_retry" ? "automatic_retry" : "retry"
431
+ };
432
+ }
433
+ function describe(r) {
434
+ const why = [r.reason, r.evidence ? null : "no evidence recorded"].filter((part) => part !== null).join(", ");
435
+ return `attempt ${clean(String(r.number))} ${r.state}${why === "" ? "" : ` (${clean(why)})`}`;
436
+ }
437
+ function projection(gate2, key) {
438
+ const value = gate2[key];
439
+ return isRecord2(value) && typeof value["status"] === "string" ? clean(value["status"]) : "unknown";
440
+ }
441
+ async function status(ctx, pr) {
442
+ const gate2 = await fetchGate(ctx, pr);
443
+ const lifecycle = isRecord2(gate2["lifecycle"]) ? gate2["lifecycle"] : void 0;
444
+ const head = isRecord2(gate2["head"]) && typeof gate2["head"]["sha"] === "string" ? gate2["head"]["sha"].slice(0, 10) : "?";
445
+ const lines = [
446
+ `${label(pr)} at ${clean(head)}`,
447
+ `enforced: ${projection(gate2, "enforced")} advisory: ${projection(gate2, "advisory")}`
448
+ ];
449
+ if (lifecycle) {
450
+ lines.push(`gate check: ${clean(String(lifecycle["state"] ?? "unknown")).replaceAll("_", " ")}`);
451
+ const r = recovery(lifecycle);
452
+ if (r) {
453
+ lines.push(`latest ${describe(r)}`);
454
+ lines.push(
455
+ r.next === "automatic_retry" ? "Harness retries it automatically; check again shortly." : named(`Retry it with: harness review retry ${label(pr)} --reason "\u2026"`)
456
+ );
457
+ }
458
+ }
459
+ return { json: gate2, human: lines.join("\n") + "\n" };
460
+ }
461
+ var REFUSALS = {
462
+ execution_not_terminal: "the latest attempt is still running; wait for it to finish",
463
+ attempt_changed: "another attempt was created meanwhile; read the status and run this again",
464
+ check_changed: "the head's gate check changed meanwhile; run this again",
465
+ head_changed: "the pull request's head moved; the new head gets its own check",
466
+ advisory_not_converged: "the head's advisory review has not converged; converge it first",
467
+ retry_conflict: "this attempt was already retried with a different reason",
468
+ merged: "the pull request is merged"
469
+ };
470
+ async function retry(ctx, pr, reason) {
471
+ const gate2 = await fetchGate(ctx, pr);
472
+ const lifecycle = isRecord2(gate2["lifecycle"]) ? gate2["lifecycle"] : void 0;
473
+ if (!lifecycle || typeof lifecycle["check_id"] !== "string" || typeof lifecycle["head_sha"] !== "string") {
474
+ throw new CommandError(`${label(pr)} has no gate check at its current head: nothing to retry`);
475
+ }
476
+ const r = recovery(lifecycle);
477
+ if (r?.next === "automatic_retry") {
478
+ throw new CommandError(named(`Harness retries attempt ${clean(String(r.number))} itself; run harness review status ${label(pr)} shortly`));
479
+ }
480
+ const previous = latestAttempt(lifecycle);
481
+ const previousId = typeof previous?.["id"] === "string" ? previous["id"] : null;
482
+ const body = {
483
+ head_sha: lifecycle["head_sha"],
484
+ check_id: lifecycle["check_id"],
485
+ previous_attempt_id: previousId,
486
+ // One key per attempt retried: running this again for the same attempt returns the attempt already
487
+ // queued instead of paying for another review run.
488
+ request_key: `harness-cli:${previousId ?? lifecycle["check_id"]}`,
489
+ reason: reason?.trim() || "Retry requested with harness review retry"
490
+ };
491
+ let response;
492
+ try {
493
+ response = await ctx.client.post(`${gatePath(pr)}/retry`, body, { actor: false });
494
+ } catch (error) {
495
+ if (error instanceof HttpError && error.status === 409) {
496
+ const code2 = error.message.trim().split(/[\s:]+/).pop() ?? "";
497
+ throw new CommandError(`could not retry ${label(pr)}: ${REFUSALS[code2] ?? clean(error.message)}`);
498
+ }
499
+ if (error instanceof HttpError && error.status === 403) {
500
+ throw new CommandError(`${error.message} \u2014 a retry needs a login with reviews:write`);
501
+ }
502
+ throw error;
503
+ }
504
+ const attempt = isRecord2(response.data) ? response.data : {};
505
+ const outcome = response["outcome"];
506
+ const verb = outcome === "existing" ? "Already queued" : "Queued";
507
+ return {
508
+ json: { attempt, outcome },
509
+ human: `${verb} attempt ${clean(String(attempt["number"]))} (${clean(String(attempt["id"]))}) for ${label(pr)}
510
+ `
511
+ };
512
+ }
513
+
514
+ // src/commands/code/index.ts
515
+ import { resolve as resolve2 } from "path";
516
+ import { statSync } from "fs";
517
+
518
+ // src/core/code/http.ts
519
+ import { setTimeout as delay } from "timers/promises";
520
+ import { z } from "zod";
521
+ var UUID = z.string().uuid();
522
+ var HASH = z.string().regex(/^[a-f0-9]{64}$/);
523
+ var view = z.object({
524
+ view_id: UUID,
525
+ view_key: z.string(),
526
+ repository_id: UUID,
527
+ active: z.object({
528
+ version: z.number().int(),
529
+ base_commit: z.string(),
530
+ overlay_digest: HASH,
531
+ indexed_at: z.string().nullable().optional()
532
+ }).nullable(),
533
+ pending: z.object({
534
+ version: z.number().int(),
535
+ state: z.string(),
536
+ progress: z.object({ done: z.number().int().nonnegative(), total: z.number().int().nonnegative() }).nullable().optional(),
537
+ error: z.unknown().optional()
538
+ }).nullable()
539
+ });
540
+ var policy = z.object({
541
+ org_id: UUID,
542
+ policy_digest: z.string().min(1).max(256),
543
+ max_file_bytes: z.number().int().positive(),
544
+ max_batch_bytes: z.number().int().positive(),
545
+ include_extensions: z.array(z.string().max(32)).max(100),
546
+ deny_globs: z.array(z.string().max(1024)).max(1e3),
547
+ deny_glob_case: z.literal("ascii_insensitive").optional(),
548
+ content_patterns: z.array(z.string().max(4096)).max(100),
549
+ org_name: z.string().optional()
550
+ });
551
+ var base = z.object({
552
+ state: z.enum(["fetching", "complete", "ready", "failed"]),
553
+ policy_digest: z.string().optional(),
554
+ file_count: z.number().optional(),
555
+ excluded_count: z.number().optional(),
556
+ skipped: z.array(z.object({ path: z.string(), reason: z.string() })).optional(),
557
+ error: z.unknown().optional(),
558
+ retry_after_ms: z.number().nonnegative().optional()
559
+ });
560
+ var opened = z.object({
561
+ sync_id: UUID,
562
+ state: z.string(),
563
+ missing: z.array(HASH),
564
+ rejected: z.array(z.object({ path: z.string(), error: z.string() }))
565
+ });
566
+ var activated = z.object({
567
+ pending_version: z.number().int().positive(),
568
+ state: z.string()
569
+ });
570
+ var syncStatus = z.object({
571
+ id: UUID,
572
+ state: z.enum(["receiving", "queued", "indexing", "active", "superseded", "failed"]),
573
+ version: z.number().int().nonnegative().nullable()
574
+ });
575
+ function decode(schema, data) {
576
+ const result = schema.safeParse(data);
577
+ if (!result.success)
578
+ throw new CommandError(
579
+ "The code index API returned an invalid response; no additional source will be uploaded."
580
+ );
581
+ return result.data;
582
+ }
583
+ function retryAfterMs(value, now = Date.now()) {
584
+ if (value === null) return void 0;
585
+ const seconds = Number(value);
586
+ if (/^\d+(?:\.\d+)?$/.test(value) && Number.isFinite(seconds))
587
+ return seconds * 1e3;
588
+ const date = Date.parse(value);
589
+ return Number.isNaN(date) ? void 0 : Math.max(0, date - now);
590
+ }
591
+ function describeCodeError(error) {
592
+ const code2 = error.body["error"];
593
+ let message = error.message;
594
+ if (code2 === "repository_not_connected")
595
+ message = "repository_not_connected: share this repository through your organization\u2019s Harness GitHub connection before syncing.";
596
+ if (code2 === "code_index_disabled")
597
+ message = "code_index_disabled: code indexing is disabled for this organization; ask a Harness administrator to enable it.";
598
+ if (code2 === "quota_exceeded") {
599
+ const raw = error.body["details"];
600
+ const d = raw && typeof raw === "object" ? raw : {};
601
+ message = `quota_exceeded: your organization\u2019s code index limit ${typeof d["quota"] === "string" ? d["quota"] : "quota"} is reached (limit ${typeof d["limit"] === "number" ? d["limit"] : "unknown"}). Ask a Harness administrator.`;
602
+ }
603
+ if (code2 === "invalid_sync")
604
+ message = "invalid_sync: the client sync request does not match the server contract. Upgrade the client and retry code sync.";
605
+ if (code2 === "overlay_too_large") {
606
+ const raw = error.body["details"];
607
+ const details = raw && typeof raw === "object" ? raw : {};
608
+ const limit = typeof details["limit"] === "number" ? details["limit"] : 2e4;
609
+ message = `overlay_too_large: the overlay exceeds ${limit} paths. Sync against a newer base commit.`;
610
+ }
611
+ if (code2 === "unexpected_blob")
612
+ message = "unexpected_blob: no open sync names this upload. Retry code sync to reconcile the overlay; base blobs are never uploaded.";
613
+ if (code2 === "stale_policy")
614
+ message = "stale_policy: the upload policy kept changing; retry code sync to fetch the policy and rescan.";
615
+ if (code2 === "not_view_owner")
616
+ message = "not_view_owner: this code view belongs to another member; sync your own worktree.";
617
+ if (code2 === "insufficient_scope" || code2 === "missing_scope")
618
+ message = "Code commands need harness:read; sync and base status also need harness:write.";
619
+ return new HttpError(error.status, message, error.body, error.retryAfter);
620
+ }
621
+ async function responseText(response, maxBytes) {
622
+ if (maxBytes === void 0) return response.text();
623
+ if (!response.body) return "";
624
+ const reader = response.body.getReader();
625
+ const chunks = [];
626
+ let size = 0;
627
+ try {
628
+ while (true) {
629
+ const part = await reader.read();
630
+ if (part.done) break;
631
+ size += part.value.byteLength;
632
+ if (size > maxBytes) {
633
+ void reader.cancel().catch(() => {
634
+ });
635
+ throw new CommandError("The code query response exceeds the 16 MiB size limit; narrow the query.");
636
+ }
637
+ chunks.push(Buffer.from(part.value));
638
+ }
639
+ } finally {
640
+ reader.releaseLock();
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++) {
661
+ try {
662
+ const headers = {
663
+ authorization: `Bearer ${this.config.token}`,
664
+ "x-harness-cli-version": CLI_VERSION
665
+ };
666
+ const encoded = body === void 0 ? void 0 : Buffer.isBuffer(body) ? body : JSON.stringify(body);
667
+ if (body !== void 0)
668
+ headers["content-type"] = Buffer.isBuffer(body) ? "application/octet-stream" : "application/json";
669
+ const response = await this.fetchImpl(
670
+ new URL(this.origin + "/api/v1/code" + path),
671
+ {
672
+ method,
673
+ headers,
674
+ body: encoded,
675
+ signal: AbortSignal.timeout(3e4),
676
+ redirect: "manual"
677
+ }
678
+ );
679
+ const retryAfter = retryAfterMs(response.headers.get("retry-after"));
680
+ const text = await responseText(response, maxResponseBytes);
681
+ let json2 = {};
682
+ if (text) {
683
+ try {
684
+ const parsed = JSON.parse(text);
685
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
686
+ json2 = parsed;
687
+ else if (response.ok)
688
+ throw new CommandError(
689
+ "The code index API returned an invalid JSON envelope."
690
+ );
691
+ } catch (error) {
692
+ if (response.ok)
693
+ throw error instanceof CommandError ? error : new CommandError(
694
+ "The code index API returned a non-JSON body."
695
+ );
696
+ }
697
+ }
698
+ if (!response.ok) {
699
+ const message = typeof json2["message"] === "string" ? json2["message"] : `Code index request refused (HTTP ${response.status}).`;
700
+ if (response.status === 426)
701
+ throw new UpgradeRequiredError(message, json2);
702
+ throw new HttpError(
703
+ response.status,
704
+ message,
705
+ json2,
706
+ response.headers.get("retry-after")
707
+ );
708
+ }
709
+ return { data: json2["data"], meta: json2["meta"], retryAfter };
710
+ } catch (error) {
711
+ if (error instanceof UpgradeRequiredError || error instanceof CommandError)
712
+ throw error;
713
+ const transient = error instanceof HttpError ? [429, 500, 502, 503, 504].includes(error.status) : true;
714
+ if (!transient || attempt === 2) {
715
+ if (error instanceof HttpError) throw error;
716
+ throw new TransportError(
717
+ "Code index request did not complete. Retry this command with the same worktree to resume."
718
+ );
719
+ }
720
+ const backoff = error instanceof HttpError ? retryAfterMs(error.retryAfter) : void 0;
721
+ const wait = backoff ?? 250 * 2 ** attempt;
722
+ if (wait > 3e5)
723
+ throw error instanceof HttpError ? error : new TransportError(
724
+ "The code index retry backoff is too long; retry later."
725
+ );
726
+ await this.sleep(wait);
727
+ }
728
+ }
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 };
742
+ }
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));
747
+ }
748
+ const result = await this.request("GET", path + (params.size ? "?" + params.toString() : ""), void 0, 16 * 1024 * 1024);
749
+ return { data: result.data, meta: result.meta };
750
+ }
751
+ async getPolicy() {
752
+ return decode(policy, (await this.request("GET", "/policy")).data);
753
+ }
754
+ async putRepository(repoKey) {
755
+ return decode(
756
+ z.object({ repository_id: UUID, repo_key: z.string() }),
757
+ (await this.request("PUT", "/repositories", { repo_key: repoKey })).data
758
+ );
759
+ }
760
+ async getBase(repositoryId, commit, skipped) {
761
+ const r = await this.request(
762
+ "GET",
763
+ `/repositories/${encodeURIComponent(repositoryId)}/bases/${encodeURIComponent(commit)}${skipped ? "?skipped=true" : ""}`
764
+ );
765
+ const data = decode(base, r.data);
766
+ const wait = Math.max(data.retry_after_ms ?? 0, r.retryAfter ?? 0);
767
+ return wait ? { ...data, retry_after_ms: wait } : data;
768
+ }
769
+ async putView(repositoryId, key) {
770
+ return decode(
771
+ view,
772
+ (await this.request(
773
+ "PUT",
774
+ `/repositories/${encodeURIComponent(repositoryId)}/views/${encodeURIComponent(key)}`,
775
+ {}
776
+ )).data
777
+ );
778
+ }
779
+ async getView(id) {
780
+ return decode(
781
+ view,
782
+ (await this.request("GET", `/views/${encodeURIComponent(id)}`)).data
783
+ );
784
+ }
785
+ async openSync(id, body) {
786
+ return decode(
787
+ opened,
788
+ (await this.request("POST", `/views/${encodeURIComponent(id)}/syncs`, {
789
+ ...body
790
+ })).data
791
+ );
792
+ }
793
+ async putBlob(hash2, bytes) {
794
+ HASH.parse(hash2);
795
+ await this.request("PUT", `/blobs/${hash2}`, bytes);
796
+ }
797
+ async getSync(viewId, syncId) {
798
+ return decode(syncStatus, (await this.request(
799
+ "GET",
800
+ `/views/${encodeURIComponent(viewId)}/syncs/${encodeURIComponent(syncId)}`
801
+ )).data);
802
+ }
803
+ async activate(viewId, syncId) {
804
+ return decode(
805
+ activated,
806
+ (await this.request(
807
+ "POST",
808
+ `/views/${encodeURIComponent(viewId)}/syncs/${encodeURIComponent(syncId)}/activate`,
809
+ {}
810
+ )).data
811
+ );
812
+ }
813
+ };
814
+
815
+ // src/core/code/sync.ts
816
+ import { randomUUID as randomUUID2 } from "crypto";
817
+ import { setTimeout as delay2 } from "timers/promises";
818
+
819
+ // src/core/code/git.ts
820
+ import { execFile } from "child_process";
821
+ import { promisify } from "util";
822
+ import { realpath } from "fs/promises";
823
+ var run2 = promisify(execFile);
824
+ var MAX_GIT_BYTES = 16 * 1024 * 1024;
825
+ async function gitBytes(cwd, args, maxBuffer = MAX_GIT_BYTES) {
826
+ try {
827
+ return (await run2(
828
+ "git",
829
+ [
830
+ "--no-optional-locks",
831
+ "--no-pager",
832
+ "-c",
833
+ "core.fsmonitor=false",
834
+ ...args
835
+ ],
836
+ { cwd, maxBuffer, timeout: 3e4, encoding: "buffer" }
837
+ )).stdout;
838
+ } catch (error) {
839
+ const code2 = error.code;
840
+ if (code2 === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || error instanceof Error && /maxBuffer/i.test(error.message))
841
+ throw new LocalError(
842
+ "Git returned more than 16 MiB of metadata; narrow the worktree before code sync."
843
+ );
844
+ throw new LocalError(
845
+ "Cannot read this Git working tree. Run code commands inside a committed repository with an origin remote."
846
+ );
847
+ }
848
+ }
849
+ function decodeUtf8(value, message) {
850
+ const decoded = value.toString("utf8");
851
+ if (!Buffer.from(decoded, "utf8").equals(value)) throw new LocalError(message);
852
+ return decoded;
853
+ }
854
+ function decodeGitPaths(outputs) {
855
+ const paths = [];
856
+ for (const output of outputs) {
857
+ if (output.length > 0 && output.at(-1) !== 0)
858
+ throw new LocalError("Code sync received an incomplete Git path record.");
859
+ let start = 0;
860
+ for (let end = output.indexOf(0, start); end !== -1; end = output.indexOf(0, start)) {
861
+ if (end === start)
862
+ throw new LocalError("Code sync received an empty Git path record.");
863
+ const path = decodeUtf8(
864
+ output.subarray(start, end),
865
+ "Code sync cannot safely index a Git path with invalid UTF-8."
866
+ );
867
+ if (/[\x00-\x1f\x7f-\x9f]/.test(path))
868
+ throw new LocalError(
869
+ "Code sync cannot safely index a Git path with control characters."
870
+ );
871
+ paths.push(path);
872
+ start = end + 1;
873
+ }
874
+ }
875
+ return paths;
876
+ }
877
+ function decodeGitLine(output, message) {
878
+ if (output.length === 0 || output.at(-1) !== 10)
879
+ throw new LocalError(message);
880
+ return decodeUtf8(output.subarray(0, -1), message);
881
+ }
882
+ function assertDistinctPathIdentities(paths) {
883
+ const known = /* @__PURE__ */ new Map();
884
+ for (const path of paths) {
885
+ let original = "";
886
+ let folded = "";
887
+ for (const part of path.split("/")) {
888
+ original += `/${part}`;
889
+ folded += `/${part.normalize("NFC").toLocaleLowerCase("en-US").toLocaleUpperCase("en-US").normalize("NFC")}`;
890
+ const previous = known.get(folded);
891
+ if (previous !== void 0 && previous !== original)
892
+ throw new LocalError(
893
+ "Code sync cannot safely distinguish colliding Git path identities."
894
+ );
895
+ known.set(folded, original);
896
+ }
897
+ }
898
+ }
899
+ async function identity(cwd) {
900
+ const rootRecord = decodeGitLine(
901
+ await gitBytes(cwd, ["rev-parse", "--show-toplevel"]),
902
+ "Cannot resolve the Git working tree root safely."
903
+ );
904
+ if (/[\x00-\x1f\x7f-\x9f]/.test(rootRecord))
905
+ throw new LocalError("Cannot resolve the Git working tree root safely.");
906
+ const root = await realpath(rootRecord);
907
+ const commit = decodeGitLine(
908
+ await gitBytes(root, ["rev-parse", "--verify", "HEAD^{commit}"]),
909
+ "Cannot resolve the repository HEAD commit."
910
+ );
911
+ const remote = decodeGitLine(
912
+ await gitBytes(root, ["remote", "get-url", "origin"]),
913
+ "repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
914
+ );
915
+ if (/[\x00-\x1f\x7f-\x9f]/.test(remote))
916
+ throw new LocalError(
917
+ "repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
918
+ );
919
+ const scp = /^(?:git@)?github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i.exec(remote);
920
+ let parts;
921
+ if (scp) parts = [scp[1], scp[2]];
922
+ else {
923
+ try {
924
+ const url = new URL(remote);
925
+ if (["https:", "ssh:", "git:"].includes(url.protocol) && url.hostname.toLowerCase() === "github.com" && !url.password && !url.search && !url.hash && (!url.port || url.protocol === "ssh:")) {
926
+ parts = url.pathname.replace(/^\//, "").replace(/\/$/, "").replace(/\.git$/, "").split("/");
927
+ }
928
+ } catch {
929
+ }
930
+ }
931
+ if (!parts || parts.length !== 2 || parts.some((p) => !p || !/^[\w.-]+$/.test(p) || p === "." || p === ".."))
932
+ throw new LocalError(
933
+ "repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
934
+ );
935
+ if (!/^[0-9a-f]{40,64}$/.test(commit))
936
+ throw new LocalError("Cannot resolve the repository HEAD commit.");
937
+ return {
938
+ root,
939
+ commit,
940
+ repoKey: `github.com/${parts.join("/").toLowerCase()}`
941
+ };
942
+ }
943
+ async function changedPaths(root, commit) {
944
+ const changed = await gitBytes(root, [
945
+ "diff",
946
+ "--name-only",
947
+ "-z",
948
+ "--no-renames",
949
+ "--no-ext-diff",
950
+ "--no-textconv",
951
+ commit,
952
+ "--"
953
+ ]);
954
+ const untracked = await gitBytes(root, [
955
+ "ls-files",
956
+ "--others",
957
+ "--exclude-standard",
958
+ "-z"
959
+ ]);
960
+ return [
961
+ ...new Set(decodeGitPaths([changed, untracked]))
962
+ ].sort(
963
+ (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))
964
+ );
965
+ }
966
+
967
+ // src/core/code/scan.ts
968
+ import { constants as constants2 } from "fs";
969
+ import { open } from "fs/promises";
970
+ import { join } from "path";
971
+ import { createHash } from "crypto";
972
+ import { release } from "os";
973
+
974
+ // src/core/code/policy.ts
975
+ import { isUtf8 } from "buffer";
976
+ import { extname } from "path";
977
+
978
+ // src/core/code/glob.ts
979
+ var MAX_PATTERN_BYTES = 256;
980
+ var MAX_PATTERN_STARS = 8;
981
+ var MAX_PATTERNS = 1e3;
982
+ function compileIgnorePatterns(lines, warnings) {
983
+ const patterns = [];
984
+ for (const [line, raw] of lines.entries()) {
985
+ const trimmed = raw.trim();
986
+ if (!trimmed || trimmed.startsWith("#")) continue;
987
+ if (trimmed.startsWith("!")) {
988
+ warnings.push(
989
+ `Ignoring .harnesscodeignore negation on line ${line + 1}; code-index patterns do not support !.`
990
+ );
991
+ continue;
992
+ }
993
+ if (trimmed.includes("\\")) {
994
+ warnings.push(
995
+ `The .harnesscodeignore backslash on line ${line + 1} is literal and never matches a normalized source path.`
996
+ );
997
+ }
998
+ if (Buffer.byteLength(trimmed) > MAX_PATTERN_BYTES || (trimmed.match(/\*/g)?.length ?? 0) > MAX_PATTERN_STARS) {
999
+ warnings.push(
1000
+ "Ignoring .harnesscodeignore pattern outside the code-index size limits."
1001
+ );
1002
+ continue;
1003
+ }
1004
+ if (patterns.length === MAX_PATTERNS) {
1005
+ warnings.push(
1006
+ "Ignoring .harnesscodeignore patterns after the first 1,000 valid entries."
1007
+ );
1008
+ break;
1009
+ }
1010
+ const pattern = compilePattern(trimmed);
1011
+ if (pattern) patterns.push(pattern);
1012
+ }
1013
+ return patterns;
1014
+ }
1015
+ function patternLimitError() {
1016
+ return new Error("Code upload policy contains an invalid path pattern.");
1017
+ }
1018
+ function escapeRegex(pattern) {
1019
+ return pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1020
+ }
1021
+ function globBody(pattern) {
1022
+ return escapeRegex(pattern).replaceAll("\\*\\*/", "(?:.*/)?").replaceAll("/\\*\\*", "/.*").replaceAll("\\*", "[^/]*").replaceAll("\\?", "[^/]");
1023
+ }
1024
+ function compilePattern(raw, {
1025
+ caseInsensitive = false,
1026
+ mode = "ignore",
1027
+ warnings,
1028
+ maxPatternBytes = MAX_PATTERN_BYTES,
1029
+ maxStars = MAX_PATTERN_STARS,
1030
+ rejectUnsupported = false
1031
+ } = {}) {
1032
+ let pattern = mode === "served" ? raw : raw.trim();
1033
+ if (pattern === "") return null;
1034
+ if (pattern.startsWith("#")) {
1035
+ if (rejectUnsupported) throw patternLimitError();
1036
+ return null;
1037
+ }
1038
+ if (Buffer.byteLength(pattern) > maxPatternBytes || (pattern.match(/\*/g)?.length ?? 0) > maxStars)
1039
+ throw patternLimitError();
1040
+ if (pattern.startsWith("!")) {
1041
+ if (rejectUnsupported) throw patternLimitError();
1042
+ warnings?.push(
1043
+ "Ignoring .harnesscodeignore negation; code-index patterns do not support !."
1044
+ );
1045
+ return null;
1046
+ }
1047
+ if (pattern.includes("\\")) {
1048
+ if (rejectUnsupported) throw patternLimitError();
1049
+ warnings?.push(
1050
+ "A .harnesscodeignore backslash is literal, not an escape."
1051
+ );
1052
+ }
1053
+ if (rejectUnsupported && /[\[\]{}]/.test(pattern)) throw patternLimitError();
1054
+ const anchored = pattern.includes("/");
1055
+ if (mode === "ignore") pattern = pattern.replace(/^\/+|\/+$/g, "");
1056
+ const prefix = anchored ? "^" : "^(?:.*/)?";
1057
+ const suffix = mode === "ignore" ? "(?:/.*)?$" : "$";
1058
+ return {
1059
+ source: mode === "served" ? raw : raw.trim(),
1060
+ negated: false,
1061
+ regex: new RegExp(
1062
+ prefix + globBody(pattern) + suffix,
1063
+ caseInsensitive ? "i" : ""
1064
+ )
1065
+ };
1066
+ }
1067
+ function compilePatterns(lines, options = {}) {
1068
+ const maxPatterns = options.maxPatterns ?? MAX_PATTERNS;
1069
+ if (lines.length > maxPatterns) throw patternLimitError();
1070
+ return lines.map((line) => compilePattern(line, options)).filter((pattern) => pattern !== null);
1071
+ }
1072
+ function matchingPattern(path, patterns) {
1073
+ let decided = null;
1074
+ for (const pattern of patterns) if (pattern.regex.test(path)) decided = pattern;
1075
+ return decided;
1076
+ }
1077
+
1078
+ // src/core/code/policy.ts
1079
+ var MAX_FILE_BYTES = 1048576;
1080
+ var EXTENSIONS = [
1081
+ ".ex",
1082
+ ".exs",
1083
+ ".js",
1084
+ ".mjs",
1085
+ ".cjs",
1086
+ ".jsx",
1087
+ ".ts",
1088
+ ".tsx",
1089
+ ".swift"
1090
+ ];
1091
+ var DENY = [
1092
+ ".env",
1093
+ ".env.*",
1094
+ "secrets/**",
1095
+ "**/secrets/**",
1096
+ "*.pem",
1097
+ "*.key",
1098
+ "*.p12",
1099
+ "*.pfx",
1100
+ "*.jks",
1101
+ "*.keystore",
1102
+ "id_rsa*",
1103
+ "id_ecdsa*",
1104
+ "id_ed25519*",
1105
+ "*.kdbx",
1106
+ ".npmrc",
1107
+ ".netrc",
1108
+ "credentials.json",
1109
+ "**/.aws/**",
1110
+ ".git/**",
1111
+ "node_modules/**",
1112
+ "_build/**",
1113
+ "deps/**",
1114
+ "dist/**",
1115
+ "*.lock",
1116
+ "package-lock.json",
1117
+ "*.min.js",
1118
+ "*.map",
1119
+ "priv/static/**",
1120
+ "src/generated/**",
1121
+ "DerivedData/**",
1122
+ "**/DerivedData/**",
1123
+ ".build/**",
1124
+ "**/.build/**",
1125
+ "Pods/**",
1126
+ "Carthage/Build/**",
1127
+ "xcuserdata/**",
1128
+ "**/xcuserdata/**",
1129
+ "*.xcuserstate",
1130
+ "*.xcresult/**",
1131
+ "*.xcarchive/**",
1132
+ "*.dSYM/**",
1133
+ "*.ipa",
1134
+ "*.secret.exs"
1135
+ ];
1136
+ var SECRET_PATTERN_SOURCES = [
1137
+ "-----BEGIN [A-Z ]*PRIVATE KEY(?: BLOCK)?-----",
1138
+ "\\b(?:AKIA|ASIA)[0-9A-Z]{16}\\b",
1139
+ "\\baone_[A-Za-z0-9_-]{20,}",
1140
+ "\\bgh[opsur]_[A-Za-z0-9]{36}\\b",
1141
+ "\\b[rs]k_live_[A-Za-z0-9]{20,}",
1142
+ "\\bAIza[0-9A-Za-z_-]{35}\\b",
1143
+ "\\bxapp-\\d-[A-Za-z0-9-]{20,}",
1144
+ "hooks\\.slack\\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+",
1145
+ "\\bgithub_pat_[A-Za-z0-9_]{22,}",
1146
+ "\\bxox[baprs]-\\d{8,}-[A-Za-z0-9-]{10,}",
1147
+ "\\bsk-(?:proj-|ant-[a-z0-9]+-)?[A-Za-z0-9_-]{32,}"
1148
+ ];
1149
+ var SECRETS = SECRET_PATTERN_SOURCES.map((source) => new RegExp(source));
1150
+ function contentMatchers(patterns) {
1151
+ const literalMarkers = [];
1152
+ for (const pattern of patterns) {
1153
+ if (SECRET_PATTERN_SOURCES.includes(pattern)) continue;
1154
+ if (!pattern || /[\\^$.*+?()[\]{}|]/.test(pattern))
1155
+ throw new Error("unknown regular expression");
1156
+ literalMarkers.push(pattern);
1157
+ }
1158
+ return literalMarkers;
1159
+ }
1160
+ function servedCase(served) {
1161
+ const value = served.deny_glob_case;
1162
+ if (value !== void 0 && value !== "ascii_insensitive")
1163
+ throw new Error("unknown deny glob case mode");
1164
+ return value;
1165
+ }
1166
+ function buildPolicy(served, ignore, includeFloor) {
1167
+ const warnings = [];
1168
+ const unsupported = [...new Set(
1169
+ served.include_extensions.filter((extension) => !EXTENSIONS.includes(extension))
1170
+ )].sort();
1171
+ if (unsupported.length)
1172
+ warnings.push(
1173
+ `This harness-cli does not send ${unsupported.join(", ")} files; upgrade the client.`
1174
+ );
1175
+ const insensitiveServed = servedCase(served) === "ascii_insensitive";
1176
+ const literalMarkers = contentMatchers(served.content_patterns);
1177
+ return {
1178
+ limit: Math.min(served.max_file_bytes, MAX_FILE_BYTES),
1179
+ deny: [
1180
+ ...includeFloor ? compilePatterns(DENY, {
1181
+ caseInsensitive: true,
1182
+ maxPatterns: Infinity,
1183
+ maxPatternBytes: Infinity,
1184
+ maxStars: Infinity
1185
+ }) : [],
1186
+ ...compilePatterns(served.deny_globs, {
1187
+ caseInsensitive: includeFloor || insensitiveServed,
1188
+ mode: "served",
1189
+ maxPatternBytes: 1024,
1190
+ maxStars: Infinity,
1191
+ rejectUnsupported: true
1192
+ })
1193
+ ],
1194
+ ignore: compileIgnorePatterns(ignore.split(/\r?\n/), warnings),
1195
+ patterns: SECRETS,
1196
+ literalMarkers,
1197
+ extensions: new Set(
1198
+ EXTENSIONS.filter((extension) => served.include_extensions.includes(extension))
1199
+ ),
1200
+ warnings
1201
+ };
1202
+ }
1203
+ function policyFor(served, ignore) {
1204
+ try {
1205
+ return buildPolicy(served, ignore, true);
1206
+ } catch {
1207
+ throw new CommandError(
1208
+ "The server returned an invalid code upload policy; upgrade the client before code sync."
1209
+ );
1210
+ }
1211
+ }
1212
+ function basePolicyFor(served, ignore) {
1213
+ try {
1214
+ return buildPolicy(served, ignore, false);
1215
+ } catch {
1216
+ throw new CommandError(
1217
+ "The server returned an invalid code upload policy; upgrade the client before code sync."
1218
+ );
1219
+ }
1220
+ }
1221
+ function pathReason(path, p) {
1222
+ if (!path || Buffer.byteLength(path) > 1024 || /[\\\x00-\x1f\x7f-\x9f]/.test(path) || Buffer.from(path, "utf8").toString("utf8") !== path || path.split("/").some((segment) => ["", ".", ".."].includes(segment)))
1223
+ return "invalid_path";
1224
+ if (p.deny.some((pattern) => pattern.regex.test(path))) return "excluded_path";
1225
+ if (matchingPattern(path, p.ignore)) return "ignored";
1226
+ return p.extensions.has(extname(path)) ? null : "unsupported_language";
1227
+ }
1228
+ function extensionWarning(path, p) {
1229
+ const extension = extname(path);
1230
+ return !p.extensions.has(extension) && p.extensions.has(extension.toLowerCase()) ? `The server indexes ${extension.toLowerCase()} exactly; ${path} was skipped.` : null;
1231
+ }
1232
+ function contentReason(bytes, p) {
1233
+ if (bytes.length > p.limit) return "too_large";
1234
+ if (bytes.includes(0) || !isUtf8(bytes)) return "binary";
1235
+ const text = bytes.toString("utf8");
1236
+ return p.patterns.some((pattern) => pattern.test(text)) || p.literalMarkers.some((marker) => text.includes(marker)) ? "secret_content" : null;
1237
+ }
1238
+
1239
+ // src/core/code/scan.ts
1240
+ var MAX_OVERLAY_BYTES = 64 * 1024 * 1024;
1241
+ var MAX_OVERLAY_CHANGES = 2e4;
1242
+ var O_NOFOLLOW_ANY = 536870912;
1243
+ function sha256(data) {
1244
+ return createHash("sha256").update(data).digest("hex");
1245
+ }
1246
+ function supportsDarwinNoFollowAny(osRelease = release()) {
1247
+ const major = Number(osRelease.split(".")[0]);
1248
+ return Number.isInteger(major) && major >= 20;
1249
+ }
1250
+ async function committedIgnore(root, commit) {
1251
+ const entry = decodeUtf8(
1252
+ await gitBytes(root, [
1253
+ "ls-tree",
1254
+ "-z",
1255
+ commit,
1256
+ "--",
1257
+ ".harnesscodeignore"
1258
+ ]),
1259
+ "Cannot read the committed .harnesscodeignore safely."
1260
+ );
1261
+ if (!entry) return "";
1262
+ const match = /^(\d+) blob ([0-9a-f]+)\t\.harnesscodeignore\0$/.exec(entry);
1263
+ if (!match || !["100644", "100755"].includes(match[1]))
1264
+ throw new LocalError(
1265
+ "The committed .harnesscodeignore must be a regular file before code sync."
1266
+ );
1267
+ return decodeUtf8(
1268
+ await gitBytes(root, ["cat-file", "blob", match[2]], 65537),
1269
+ "The committed .harnesscodeignore must be valid UTF-8 before code sync."
1270
+ );
1271
+ }
1272
+ var directoryFlags = constants2.O_RDONLY | constants2.O_DIRECTORY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK;
1273
+ async function openLinuxRoot(root) {
1274
+ let handle;
1275
+ try {
1276
+ if (!root.startsWith("/"))
1277
+ throw new Error("repository root is not absolute");
1278
+ handle = await open("/", directoryFlags);
1279
+ for (const part of root.split("/").filter(Boolean)) {
1280
+ if (part === "." || part === "..")
1281
+ throw new Error("repository root is invalid");
1282
+ const next = await open(
1283
+ `/proc/self/fd/${handle.fd}/${part}`,
1284
+ directoryFlags
1285
+ );
1286
+ await handle.close();
1287
+ handle = next;
1288
+ }
1289
+ return handle;
1290
+ } catch {
1291
+ await handle?.close().catch(() => {
1292
+ });
1293
+ throw new LocalError(
1294
+ "Code sync cannot establish a safe source root on this platform."
1295
+ );
1296
+ }
1297
+ }
1298
+ async function openLinuxBelowRoot(root, path) {
1299
+ let handle = await openLinuxRoot(root);
1300
+ try {
1301
+ const parts = path.split("/");
1302
+ for (const [index, part] of parts.entries()) {
1303
+ if (!part || part === "." || part === "..")
1304
+ throw new LocalError(
1305
+ "Cannot read an overlay file safely; invalid path."
1306
+ );
1307
+ const next = await open(
1308
+ `/proc/self/fd/${handle.fd}/${part}`,
1309
+ constants2.O_RDONLY | constants2.O_NOFOLLOW | constants2.O_NONBLOCK | (index === parts.length - 1 ? 0 : constants2.O_DIRECTORY)
1310
+ );
1311
+ await handle.close();
1312
+ handle = next;
1313
+ }
1314
+ return handle;
1315
+ } catch (error) {
1316
+ await handle.close().catch(() => {
1317
+ });
1318
+ throw error;
1319
+ }
1320
+ }
1321
+ async function openSafe(root, path) {
1322
+ if (process.platform === "darwin") {
1323
+ if (!supportsDarwinNoFollowAny())
1324
+ throw new LocalError(
1325
+ "Code sync requires macOS 11 or later for safe source reads."
1326
+ );
1327
+ return open(
1328
+ join(root, path),
1329
+ constants2.O_RDONLY | O_NOFOLLOW_ANY | constants2.O_NONBLOCK
1330
+ );
1331
+ }
1332
+ if (process.platform === "linux") return openLinuxBelowRoot(root, path);
1333
+ throw new LocalError(
1334
+ "Code sync cannot safely read source files on this platform."
1335
+ );
1336
+ }
1337
+ async function readSafe(root, path, limit) {
1338
+ try {
1339
+ const handle = await openSafe(root, path);
1340
+ try {
1341
+ const before = await handle.stat();
1342
+ if (!before.isFile()) return { reason: "not_regular_file" };
1343
+ if (before.size > limit) return { reason: "too_large" };
1344
+ const bytes = Buffer.allocUnsafe(Math.min(before.size, limit) + 1);
1345
+ let total = 0;
1346
+ while (total < bytes.length) {
1347
+ const { bytesRead } = await handle.read(
1348
+ bytes,
1349
+ total,
1350
+ bytes.length - total,
1351
+ null
1352
+ );
1353
+ if (bytesRead === 0) break;
1354
+ total += bytesRead;
1355
+ }
1356
+ const after = await handle.stat();
1357
+ if (total > limit) return { reason: "too_large" };
1358
+ if (before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || total !== after.size)
1359
+ throw new LocalError(
1360
+ "A source file changed during scanning; retry code sync."
1361
+ );
1362
+ return {
1363
+ bytes: bytes.subarray(0, total),
1364
+ mode: before.mode & 64 ? "100755" : "100644"
1365
+ };
1366
+ } finally {
1367
+ await handle.close();
1368
+ }
1369
+ } catch (error) {
1370
+ const code2 = error.code;
1371
+ if (code2 === "ENOENT") return { reason: "deleted" };
1372
+ if (code2 === "ELOOP" || code2 === "ENOTDIR") return { reason: "symlink" };
1373
+ if (error instanceof LocalError) throw error;
1374
+ throw new LocalError(
1375
+ "Cannot read an overlay file safely; check permissions and retry code sync."
1376
+ );
1377
+ }
1378
+ }
1379
+ async function scanOverlay(id, served) {
1380
+ policyFor(served, "");
1381
+ const ignore = await readSafe(id.root, ".harnesscodeignore", 65536);
1382
+ if ("reason" in ignore && ignore.reason !== "deleted")
1383
+ throw new LocalError(
1384
+ "Cannot read .harnesscodeignore safely; fix or remove it before code sync."
1385
+ );
1386
+ const localIgnore = "bytes" in ignore ? decodeUtf8(
1387
+ ignore.bytes,
1388
+ "The local .harnesscodeignore must be valid UTF-8 before code sync."
1389
+ ) : "";
1390
+ const baseIgnore = await committedIgnore(id.root, id.commit);
1391
+ if (Buffer.byteLength(baseIgnore) > 65536)
1392
+ throw new LocalError(
1393
+ "The committed .harnesscodeignore exceeds 64 KiB; reduce it before code sync."
1394
+ );
1395
+ const policy2 = policyFor(served, localIgnore);
1396
+ const basePolicy = basePolicyFor(served, baseIgnore);
1397
+ const paths = await changedPaths(id.root, id.commit);
1398
+ const basePaths = new Set(
1399
+ decodeGitPaths([
1400
+ await gitBytes(id.root, ["ls-tree", "-r", "--name-only", "-z", id.commit])
1401
+ ])
1402
+ );
1403
+ assertDistinctPathIdentities([...basePaths, ...paths]);
1404
+ const changes = [];
1405
+ const deleted = /* @__PURE__ */ new Set();
1406
+ const blobs = /* @__PURE__ */ new Map();
1407
+ const excluded = [];
1408
+ const baseWouldHold = (path) => basePaths.has(path) && pathReason(path, basePolicy) === null;
1409
+ const deleteBasePath = (path) => {
1410
+ if (baseWouldHold(path) && !deleted.has(path)) {
1411
+ if (changes.length >= MAX_OVERLAY_CHANGES)
1412
+ throw new LocalError(
1413
+ `overlay_too_large: the overlay exceeds ${MAX_OVERLAY_CHANGES} entries. Commit the ignore file so the base honors it.`
1414
+ );
1415
+ deleted.add(path);
1416
+ changes.push({ op: "delete", path });
1417
+ }
1418
+ };
1419
+ const candidates = new Set(paths);
1420
+ const warnings = /* @__PURE__ */ new Set([...policy2.warnings, ...basePolicy.warnings]);
1421
+ for (const path of basePaths) {
1422
+ const baseReason = pathReason(path, basePolicy);
1423
+ const localReason = pathReason(path, policy2);
1424
+ if (baseReason === null && (localReason === "excluded_path" || localReason === "ignored"))
1425
+ deleteBasePath(path);
1426
+ if (baseReason === "ignored" && localReason === null) candidates.add(path);
1427
+ }
1428
+ let total = 0;
1429
+ for (const path of [...candidates].sort(
1430
+ (a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))
1431
+ )) {
1432
+ const reason = pathReason(path, policy2);
1433
+ if (reason) {
1434
+ excluded.push({ path, reason });
1435
+ if (reason === "ignored" || reason === "excluded_path") deleteBasePath(path);
1436
+ const warning = extensionWarning(path, policy2);
1437
+ if (warning) warnings.add(warning);
1438
+ continue;
1439
+ }
1440
+ const read = await readSafe(id.root, path, policy2.limit);
1441
+ if ("reason" in read) {
1442
+ if (read.reason !== "deleted")
1443
+ excluded.push({ path, reason: read.reason });
1444
+ deleteBasePath(path);
1445
+ continue;
1446
+ }
1447
+ const content = contentReason(read.bytes, policy2);
1448
+ if (content) {
1449
+ excluded.push({ path, reason: content });
1450
+ deleteBasePath(path);
1451
+ continue;
1452
+ }
1453
+ const hash2 = sha256(read.bytes);
1454
+ if (!blobs.has(hash2)) {
1455
+ total += read.bytes.length;
1456
+ if (total > MAX_OVERLAY_BYTES)
1457
+ throw new LocalError(
1458
+ "The overlay exceeds the 64 MiB client upload limit; split the change before syncing."
1459
+ );
1460
+ blobs.set(hash2, read.bytes);
1461
+ }
1462
+ if (changes.length >= MAX_OVERLAY_CHANGES)
1463
+ throw new LocalError(
1464
+ `overlay_too_large: the overlay exceeds ${MAX_OVERLAY_CHANGES} entries. Commit the ignore file so the base honors it.`
1465
+ );
1466
+ changes.push({
1467
+ op: "put",
1468
+ path,
1469
+ sha256: hash2,
1470
+ size: read.bytes.length,
1471
+ mode: read.mode
1472
+ });
1473
+ }
1474
+ changes.sort((a, b) => Buffer.compare(Buffer.from(a.path), Buffer.from(b.path)));
1475
+ const digest = sha256(
1476
+ changes.map(
1477
+ (c) => c.op === "delete" ? `${c.path}\0-\0-
1478
+ ` : `${c.path}\0${c.sha256}\0${c.mode}
1479
+ `
1480
+ ).join("")
1481
+ );
1482
+ return {
1483
+ changes,
1484
+ blobs,
1485
+ excluded,
1486
+ warnings: [...warnings],
1487
+ digest
1488
+ };
1489
+ }
1490
+
1491
+ // src/core/code/state.ts
1492
+ import {
1493
+ readFile,
1494
+ mkdir,
1495
+ writeFile,
1496
+ rename,
1497
+ unlink,
1498
+ rmdir
1499
+ } from "fs/promises";
1500
+ import { join as join2, resolve, isAbsolute } from "path";
1501
+ import { randomUUID } from "crypto";
1502
+ function statePath(server, org, root, repoKey, directory) {
1503
+ const url = new URL(server);
1504
+ const canonical = url.origin + url.pathname.replace(/\/+$/, "");
1505
+ const base2 = directory ?? process.env["HARNESS_CODE_STATE_DIR"] ?? join2(resolveCacheHome(), "harness", "code");
1506
+ if (!isAbsolute(base2))
1507
+ throw new LocalError("HARNESS_CODE_STATE_DIR must be an absolute path.");
1508
+ return join2(
1509
+ resolve(base2),
1510
+ sha256(JSON.stringify([canonical, org.toLowerCase()])),
1511
+ sha256(JSON.stringify([root, repoKey])) + ".json"
1512
+ );
1513
+ }
1514
+ async function readState(path, server, org, root, repoKey) {
1515
+ try {
1516
+ const s = JSON.parse(await readFile(path, "utf8"));
1517
+ if (typeof s !== "object" || s === null) throw new Error();
1518
+ const state2 = s;
1519
+ if (state2.org.toLowerCase() !== org.toLowerCase() || state2.root !== root || state2.repoKey !== repoKey || new URL(state2.server).href !== new URL(server).href || typeof state2.viewKey !== "string" || !/^[\da-f-]{36}$/.test(state2.viewKey))
1520
+ throw new Error();
1521
+ return { ...state2, org: state2.org.toLowerCase() };
1522
+ } catch (error) {
1523
+ if (error.code === "ENOENT") return null;
1524
+ throw new LocalError(
1525
+ "The code view state is invalid or belongs to another scope. Check HARNESS_CODE_STATE_DIR; no source was uploaded."
1526
+ );
1527
+ }
1528
+ }
1529
+ async function initialState(path, server, org, root, repoKey) {
1530
+ const previous = await readState(path, server, org, root, repoKey);
1531
+ if (previous) return previous;
1532
+ const state2 = { server, org: org.toLowerCase(), root, repoKey, viewKey: randomUUID() };
1533
+ await mkdir(join2(path, ".."), { recursive: true, mode: 448 });
1534
+ try {
1535
+ await writeFile(path, JSON.stringify(state2) + "\n", {
1536
+ flag: "wx",
1537
+ mode: 384
1538
+ });
1539
+ return state2;
1540
+ } catch (error) {
1541
+ if (error.code === "EEXIST") {
1542
+ const raced = await readState(path, server, org, root, repoKey);
1543
+ if (raced) return raced;
1544
+ }
1545
+ throw new LocalError("Cannot save isolated code view state.");
1546
+ }
1547
+ }
1548
+ async function saveState(path, state2) {
1549
+ const tmp = `${path}.${randomUUID()}.tmp`;
1550
+ try {
1551
+ await writeFile(tmp, JSON.stringify({ ...state2, org: state2.org.toLowerCase() }) + "\n", {
1552
+ flag: "wx",
1553
+ mode: 384
1554
+ });
1555
+ await rename(tmp, path);
1556
+ } catch {
1557
+ throw new LocalError(
1558
+ "Cannot save code sync progress; retry with the same worktree and state directory."
1559
+ );
1560
+ } finally {
1561
+ await unlink(tmp).catch(() => {
1562
+ });
1563
+ }
1564
+ }
1565
+ async function withSyncLock(path, action) {
1566
+ const lock = path + ".lock";
1567
+ await mkdir(join2(path, ".."), { recursive: true, mode: 448 });
1568
+ try {
1569
+ await mkdir(lock, { mode: 448 });
1570
+ } catch (error) {
1571
+ if (error.code === "EEXIST")
1572
+ throw new LocalError(
1573
+ `A code sync is already running for this view. Wait for it to finish. If it was interrupted, confirm no sync is running before removing its lock directory: ${lock}`
1574
+ );
1575
+ throw new LocalError(
1576
+ "Cannot lock code sync progress; check the state directory permissions."
1577
+ );
1578
+ }
1579
+ try {
1580
+ await writeFile(join2(lock, "owner"), `${process.pid}
1581
+ `, {
1582
+ flag: "wx",
1583
+ mode: 384
1584
+ });
1585
+ return await action();
1586
+ } finally {
1587
+ await unlink(join2(lock, "owner")).catch(() => {
1588
+ });
1589
+ try {
1590
+ await rmdir(lock);
1591
+ } catch {
1592
+ try {
1593
+ process.stderr.write(named(
1594
+ `harness: Warning: code sync lock cleanup failed at ${clean(lock)}. If the lock remains, confirm no sync is running before removing it.
1595
+ `
1596
+ ));
1597
+ } catch {
1598
+ }
1599
+ }
1600
+ }
1601
+ }
1602
+
1603
+ // src/core/code/sync.ts
1604
+ async function baseReady(api, repo, commit, options) {
1605
+ const timeout = options.pollTimeoutMs ?? 6e5;
1606
+ const sleep = options.sleep ?? delay2;
1607
+ let waited = 0;
1608
+ let interval = 1e3;
1609
+ const started = Date.now();
1610
+ for (let attempts = 0; attempts < 600; attempts++) {
1611
+ const base2 = await api.getBase(repo, commit, options.skipped === true);
1612
+ if (base2.state === "complete" || base2.state === "ready") return base2;
1613
+ if (base2.state === "failed")
1614
+ throw new CommandError(
1615
+ "The server could not build this commit\u2019s base. Check code status --skipped, then retry after the server backoff."
1616
+ );
1617
+ const backoff = base2.retry_after_ms === 0 ? interval : base2.retry_after_ms ?? interval;
1618
+ if (!Number.isFinite(backoff) || backoff < 0 || Math.max(waited, Date.now() - started) + backoff > timeout)
1619
+ throw new CommandError(
1620
+ "Timed out waiting for the server-built base; retry code sync to resume."
1621
+ );
1622
+ await sleep(backoff);
1623
+ waited += backoff;
1624
+ interval = Math.min(interval * 2, 1e4);
1625
+ }
1626
+ throw new CommandError("The base is still fetching; retry code sync later.");
1627
+ }
1628
+ function stale(error) {
1629
+ return error instanceof HttpError && error.status === 409 && error.body["error"] === "stale_policy";
1630
+ }
1631
+ function withinActivationRetention(attemptedAt) {
1632
+ const elapsed = typeof attemptedAt === "number" ? Date.now() - attemptedAt : NaN;
1633
+ return Number.isFinite(elapsed) && elapsed >= 0 && elapsed < 60 * 6e4;
1634
+ }
1635
+ var ReplacedSyncError = class extends CommandError {
1636
+ constructor() {
1637
+ super(
1638
+ "The server replaced this sync before activation. Retry code sync with no other sync running for this view."
1639
+ );
1640
+ }
1641
+ };
1642
+ function policyScope(policy2) {
1643
+ if (!/^[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12}$/i.test(policy2.org_id) || !policy2.policy_digest || !Number.isSafeInteger(policy2.max_file_bytes) || policy2.max_file_bytes < 1)
1644
+ throw new CommandError(
1645
+ "The server did not provide a valid organization and upload policy; no source was uploaded."
1646
+ );
1647
+ policyFor(policy2, "");
1648
+ }
1649
+ async function syncCode(api, options) {
1650
+ const id = await identity(options.cwd);
1651
+ let policyRestarts = 0;
1652
+ let syncRestarts = 0;
1653
+ for (let restart = 0; restart < 3; restart++) {
1654
+ const policy2 = await api.getPolicy();
1655
+ policyScope(policy2);
1656
+ const path = statePath(
1657
+ api.origin,
1658
+ policy2.org_id,
1659
+ id.root,
1660
+ id.repoKey,
1661
+ options.stateDir
1662
+ );
1663
+ try {
1664
+ return await withSyncLock(path, async () => {
1665
+ const state2 = await initialState(
1666
+ path,
1667
+ api.origin,
1668
+ policy2.org_id,
1669
+ id.root,
1670
+ id.repoKey
1671
+ );
1672
+ const repo = await api.putRepository(id.repoKey);
1673
+ if (repo.repo_key !== id.repoKey)
1674
+ throw new CommandError(
1675
+ "The server resolved a different repository; no source was uploaded."
1676
+ );
1677
+ if (state2.repositoryId && state2.repositoryId !== repo.repository_id) {
1678
+ delete state2.viewId;
1679
+ delete state2.pending;
1680
+ delete state2.activated;
1681
+ }
1682
+ state2.repositoryId = repo.repository_id;
1683
+ state2.baseCommit = id.commit;
1684
+ await saveState(path, state2);
1685
+ const base2 = await baseReady(
1686
+ api,
1687
+ repo.repository_id,
1688
+ id.commit,
1689
+ options
1690
+ );
1691
+ if (base2.policy_digest && base2.policy_digest !== policy2.policy_digest)
1692
+ throw new HttpError(409, "The upload policy changed", {
1693
+ error: "stale_policy"
1694
+ });
1695
+ const overlay = await scanOverlay(id, policy2);
1696
+ const view2 = await api.putView(repo.repository_id, state2.viewKey);
1697
+ if (state2.viewId !== void 0 && state2.viewId !== view2.view_id && view2.repository_id === repo.repository_id && view2.view_key === state2.viewKey) {
1698
+ let removed = false;
1699
+ try {
1700
+ await api.getView(state2.viewId);
1701
+ } catch (error) {
1702
+ if (error instanceof HttpError && error.status === 404)
1703
+ removed = true;
1704
+ else throw error;
1705
+ }
1706
+ if (removed) {
1707
+ delete state2.viewId;
1708
+ delete state2.pending;
1709
+ delete state2.activated;
1710
+ }
1711
+ }
1712
+ if (view2.repository_id !== repo.repository_id || view2.view_key !== state2.viewKey || state2.viewId !== void 0 && state2.viewId !== view2.view_id)
1713
+ throw new CommandError(
1714
+ "The server view does not match this repository/worktree; no source was uploaded."
1715
+ );
1716
+ state2.viewId = view2.view_id;
1717
+ const forgetReplaced = async () => {
1718
+ delete state2.pending;
1719
+ await saveState(path, state2);
1720
+ throw new ReplacedSyncError();
1721
+ };
1722
+ const receipt = async (syncId) => {
1723
+ try {
1724
+ const previous = await api.getSync(view2.view_id, syncId);
1725
+ if (previous.id !== syncId)
1726
+ throw new CommandError("The server returned a different sync; activation remains unverified.");
1727
+ if (previous.state === "receiving") return null;
1728
+ if (!Number.isSafeInteger(previous.version) || previous.version === null || previous.version < 1)
1729
+ throw new CommandError("The server did not return an activation version; retry code status.");
1730
+ return { pending_version: previous.version, state: previous.state };
1731
+ } catch (error) {
1732
+ if (error instanceof HttpError && error.status === 404) {
1733
+ if (withinActivationRetention(state2.pending?.activationAttemptedAt))
1734
+ return forgetReplaced();
1735
+ throw new CommandError("The previous activation can no longer be verified after retention. Check code status before syncing again.");
1736
+ }
1737
+ throw error;
1738
+ }
1739
+ };
1740
+ let current = state2.pending;
1741
+ const matchesCurrent = current && current.digest === overlay.digest && current.request?.mode === "full" && current.request.base_commit === id.commit && current.request.policy_digest === policy2.policy_digest && typeof current.request.client_sync_id === "string" && JSON.stringify(current.request.changes) === JSON.stringify(overlay.changes);
1742
+ if (current?.syncId && current.activationAttemptedAt !== void 0 && !matchesCurrent) {
1743
+ const prior = current;
1744
+ const originalSyncId = current.syncId;
1745
+ let recovered = await receipt(originalSyncId);
1746
+ if (!recovered) {
1747
+ prior.activationAttemptedAt = Date.now();
1748
+ await saveState(path, state2);
1749
+ try {
1750
+ recovered = await api.activate(view2.view_id, originalSyncId);
1751
+ } catch (error) {
1752
+ recovered = await receipt(originalSyncId);
1753
+ if (!recovered) throw error;
1754
+ }
1755
+ }
1756
+ state2.activated = {
1757
+ baseCommit: prior.request.base_commit,
1758
+ overlayDigest: prior.digest,
1759
+ pendingVersion: recovered.pending_version,
1760
+ policyDigest: prior.request.policy_digest
1761
+ };
1762
+ delete state2.pending;
1763
+ await saveState(path, state2);
1764
+ current = void 0;
1765
+ }
1766
+ const request = matchesCurrent && current ? current.request : {
1767
+ client_sync_id: randomUUID2(),
1768
+ base_commit: id.commit,
1769
+ mode: "full",
1770
+ changes: overlay.changes,
1771
+ policy_digest: policy2.policy_digest
1772
+ };
1773
+ state2.pending = current && current.request === request ? current : { digest: overlay.digest, request };
1774
+ await saveState(path, state2);
1775
+ const finish = async (syncId, activated3, uploadedBlobs) => {
1776
+ state2.activated = {
1777
+ baseCommit: id.commit,
1778
+ overlayDigest: overlay.digest,
1779
+ pendingVersion: activated3.pending_version,
1780
+ policyDigest: policy2.policy_digest
1781
+ };
1782
+ delete state2.pending;
1783
+ await saveState(path, state2);
1784
+ return {
1785
+ repository_id: repo.repository_id,
1786
+ repo_key: id.repoKey,
1787
+ org_id: policy2.org_id,
1788
+ view_id: view2.view_id,
1789
+ view_key: state2.viewKey,
1790
+ sync_id: syncId,
1791
+ base_commit: id.commit,
1792
+ base: base2,
1793
+ overlay_digest: overlay.digest,
1794
+ changed_files: overlay.changes.length,
1795
+ uploaded_blobs: uploadedBlobs,
1796
+ excluded: overlay.excluded,
1797
+ warnings: overlay.warnings,
1798
+ policy_restarts: policyRestarts,
1799
+ sync_restarts: syncRestarts,
1800
+ ...activated3
1801
+ };
1802
+ };
1803
+ if (state2.pending.syncId && state2.pending.activationAttemptedAt !== void 0) {
1804
+ const recovered = await receipt(state2.pending.syncId);
1805
+ if (recovered) return finish(state2.pending.syncId, recovered, 0);
1806
+ }
1807
+ const opened2 = await api.openSync(view2.view_id, request);
1808
+ if (opened2.rejected.length)
1809
+ throw new CommandError(
1810
+ `The server refused ${opened2.rejected.length} overlay entries; no sync was activated. Refetch the policy and check the skipped-files report.`
1811
+ );
1812
+ state2.pending.syncId = opened2.sync_id;
1813
+ await saveState(path, state2);
1814
+ if (opened2.state !== "receiving") {
1815
+ const recovered = await receipt(opened2.sync_id);
1816
+ if (recovered) return finish(opened2.sync_id, recovered, 0);
1817
+ throw new CommandError("The server returned an inconsistent sync state; no new version was requested.");
1818
+ }
1819
+ const missing = [...new Set(opened2.missing)];
1820
+ for (const hash2 of missing)
1821
+ if (!overlay.blobs.has(hash2))
1822
+ throw new CommandError(
1823
+ "The server requested an unexpected blob not present in this overlay; no blobs were uploaded."
1824
+ );
1825
+ let activating = false;
1826
+ let activated2;
1827
+ try {
1828
+ let next = 0;
1829
+ let failure;
1830
+ await Promise.all(Array.from({ length: Math.min(4, missing.length) }, async () => {
1831
+ while (!failure && next < missing.length) {
1832
+ const hash2 = missing[next++];
1833
+ try {
1834
+ await api.putBlob(hash2, overlay.blobs.get(hash2));
1835
+ } catch (error) {
1836
+ failure ??= { error };
1837
+ }
1838
+ }
1839
+ }));
1840
+ if (failure) throw failure.error;
1841
+ state2.pending.activationAttemptedAt = Date.now();
1842
+ await saveState(path, state2);
1843
+ activating = true;
1844
+ activated2 = await api.activate(view2.view_id, opened2.sync_id);
1845
+ } catch (error) {
1846
+ const replaced = error instanceof HttpError && (activating ? error.status === 404 : error.status === 409 && error.body["error"] === "unexpected_blob");
1847
+ if (replaced) {
1848
+ if (activating && !withinActivationRetention(state2.pending?.activationAttemptedAt))
1849
+ throw new CommandError("The activation retry exceeded its retention window. Check code status before syncing again.");
1850
+ return forgetReplaced();
1851
+ }
1852
+ const ambiguous = activating && (!(error instanceof HttpError) || error.status >= 500 || error.status === 429);
1853
+ if (ambiguous) {
1854
+ const recovered = await receipt(opened2.sync_id);
1855
+ if (recovered) return finish(opened2.sync_id, recovered, missing.length);
1856
+ }
1857
+ throw error;
1858
+ }
1859
+ return finish(opened2.sync_id, activated2, missing.length);
1860
+ });
1861
+ } catch (error) {
1862
+ if (stale(error) && restart < 2) {
1863
+ policyRestarts++;
1864
+ continue;
1865
+ }
1866
+ if (error instanceof ReplacedSyncError && restart < 2) {
1867
+ syncRestarts++;
1868
+ continue;
1869
+ }
1870
+ throw error;
1871
+ }
1872
+ }
1873
+ throw new CommandError(
1874
+ "The upload policy kept changing. Retry code sync after it settles."
1875
+ );
1876
+ }
1877
+ async function statusCode(api, options) {
1878
+ const id = await identity(options.cwd);
1879
+ const policy2 = await api.getPolicy();
1880
+ policyScope(policy2);
1881
+ const path = statePath(
1882
+ api.origin,
1883
+ policy2.org_id,
1884
+ id.root,
1885
+ id.repoKey,
1886
+ options.stateDir
1887
+ );
1888
+ const state2 = await readState(
1889
+ path,
1890
+ api.origin,
1891
+ policy2.org_id,
1892
+ id.root,
1893
+ id.repoKey
1894
+ );
1895
+ const repository = options.skipped ? await api.putRepository(id.repoKey) : void 0;
1896
+ if (repository && repository.repo_key !== id.repoKey)
1897
+ throw new CommandError(
1898
+ "The server resolved a different repository; its base status cannot be trusted."
1899
+ );
1900
+ const repositoryId = repository?.repository_id ?? state2?.repositoryId;
1901
+ if (!state2?.viewId || repository && repositoryId !== state2.repositoryId) {
1902
+ const base3 = options.skipped && repositoryId ? await api.getBase(repositoryId, id.commit, true) : void 0;
1903
+ return {
1904
+ state: base3?.state === "failed" ? "failed" : base3?.state === "fetching" ? "fetching" : "not_synced",
1905
+ org_id: policy2.org_id,
1906
+ repo_key: id.repoKey,
1907
+ base_commit: id.commit,
1908
+ ...base3 ? { base: base3 } : {}
1909
+ };
1910
+ }
1911
+ const view2 = await api.getView(state2.viewId);
1912
+ if (view2.repository_id !== state2.repositoryId || view2.view_id !== state2.viewId || view2.view_key !== state2.viewKey)
1913
+ throw new CommandError(
1914
+ "The server view does not match this repository/worktree; its status cannot be trusted."
1915
+ );
1916
+ const overlay = await scanOverlay(id, policy2);
1917
+ const current = view2.active?.base_commit === id.commit && view2.active.overlay_digest === overlay.digest && state2.activated?.baseCommit === view2.active.base_commit && state2.activated.overlayDigest === view2.active.overlay_digest && state2.activated.pendingVersion === view2.active.version && state2.activated.policyDigest === policy2.policy_digest;
1918
+ const base2 = options.skipped && state2.repositoryId ? await api.getBase(state2.repositoryId, id.commit, true) : void 0;
1919
+ return {
1920
+ state: base2?.state === "failed" || view2.pending?.state === "failed" ? "failed" : current ? "current" : view2.pending ? "pending" : "stale",
1921
+ org_id: policy2.org_id,
1922
+ repo_key: id.repoKey,
1923
+ base_commit: id.commit,
1924
+ ...view2,
1925
+ local_overlay_digest: overlay.digest,
1926
+ excluded: overlay.excluded,
1927
+ warnings: overlay.warnings,
1928
+ ...base2 ? { base: base2 } : {}
1929
+ };
1930
+ }
1931
+
1932
+ // src/core/code/query.ts
1933
+ import { z as z2 } from "zod";
1934
+ var hash = z2.string().regex(/^[0-9a-f]{64}$/);
1935
+ var queryView = z2.object({
1936
+ view_id: z2.string().uuid(),
1937
+ repository_id: z2.string().uuid(),
1938
+ view_key: z2.string().min(1),
1939
+ active: z2.object({ version: z2.number().int().positive(), base_commit: z2.string().regex(/^[0-9a-f]{40}$/), overlay_digest: hash }).passthrough(),
1940
+ pending: z2.object({ version: z2.number().int(), state: z2.string(), progress: z2.object({ done: z2.number().int().nonnegative(), total: z2.number().int().nonnegative() }).nullable().optional() }).passthrough().nullable()
1941
+ }).passthrough();
1942
+ var envelopeSchema = z2.object({
1943
+ data: z2.unknown(),
1944
+ meta: z2.object({ contract: z2.literal("1.3"), view: queryView, truncation: z2.object({ truncated: z2.boolean() }).passthrough() }).passthrough()
1945
+ }).passthrough();
1946
+ var citationSchema = z2.object({ path: z2.string().min(1), start_line: z2.number().int().positive(), end_line: z2.number().int().positive(), sha256: hash, version: z2.number().int().positive(), from: z2.enum(["base", "overlay"]), snippet: z2.string().optional() }).passthrough();
1947
+ var record = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
1948
+ var shell = (text) => "'" + text.replaceAll("'", "'\\''") + "'";
1949
+ function fail() {
1950
+ throw new CommandError("The code query API returned an invalid contract 1.3 response; no result was presented.");
1951
+ }
1952
+ function integer(value, name, min, max) {
1953
+ if (value !== void 0 && (!Number.isSafeInteger(value) || value < min || value > max)) throw new UsageError(`${name} must be an integer from ${min} to ${max}.`);
1954
+ }
1955
+ function validateQuery(operation, p) {
1956
+ const texts = [p.q, p.symbol, p.kind, p.pathPrefix, ...p.symbols ?? [], ...p.files ?? []];
1957
+ if (texts.some((s) => s !== void 0 && (typeof s !== "string" || s.length === 0 || Buffer.byteLength(s) > 8192 || /[\x00-\x1f\x7f-\x9f]/.test(s) || Buffer.from(s).toString("utf8") !== s))) throw new UsageError("Code query arguments must be nonempty UTF-8 text without control characters (at most 8 KiB each).");
1958
+ if (operation === "search" && !p.q?.trim()) throw new UsageError("usage: harness code search <query>");
1959
+ if (operation === "explain" && !p.symbol) throw new UsageError("usage: harness code explain <symbol-id>");
1960
+ if (operation === "context" && Number(Boolean(p.symbol)) + Number(Boolean(p.q?.trim())) !== 1) throw new UsageError("code context requires exactly one query or --symbol.");
1961
+ if (operation === "affected") {
1962
+ const seeds = (p.symbols?.length ?? 0) + (p.files?.length ?? 0);
1963
+ if (seeds < 1 || seeds > 200) throw new UsageError("code affected needs 1 to 200 --symbol/--file seeds.");
1964
+ if (Buffer.byteLength(JSON.stringify(p)) > 24e4) throw new UsageError("Code affected seeds exceed the request size limit; narrow the selection.");
1965
+ }
1966
+ integer(p.limit, "limit", 1, 100);
1967
+ integer(p.depth, "depth", 1, 6);
1968
+ integer(p.budgetTokens, "budget-tokens", 100, operation === "context" ? 32e3 : 16e3);
1969
+ }
1970
+ function command(op, p) {
1971
+ const parts = ["harness", "code", op];
1972
+ if (p.q !== void 0) parts.push(shell(p.q));
1973
+ if (op === "explain" && p.symbol) parts.push(shell(p.symbol));
1974
+ else if (p.symbol) parts.push("--symbol", shell(p.symbol));
1975
+ for (const id of p.symbols ?? []) parts.push("--symbol", shell(id));
1976
+ for (const file of p.files ?? []) parts.push("--file", shell(file));
1977
+ for (const [key, value] of [["limit", p.limit], ["depth", p.depth], ["budget-tokens", p.budgetTokens], ["kind", p.kind], ["path-prefix", p.pathPrefix]])
1978
+ if (value !== void 0) parts.push(`--${key}`, typeof value === "number" ? String(value) : shell(value));
1979
+ if (p.includeAmbiguous) parts.push("--include-ambiguous");
1980
+ if (p.includeTypes) parts.push("--include-types");
1981
+ if (p.worktree) parts.push("--path", shell(p.worktree));
1982
+ return named(parts.join(" "));
1983
+ }
1984
+ function nextCommand(op, p, next) {
1985
+ if (!record(next)) return null;
1986
+ const updated = { ...p };
1987
+ let changed = false;
1988
+ for (const [key, field] of [["limit", "limit"], ["depth", "depth"], ["budget_tokens", "budgetTokens"]]) {
1989
+ if (key === "limit" && op !== "search" || key === "depth" && op !== "affected" || key === "budget_tokens" && op !== "explain" && op !== "context") continue;
1990
+ if (typeof next[key] === "number") {
1991
+ updated[field] = next[key];
1992
+ changed = true;
1993
+ }
1994
+ }
1995
+ if (!changed) return null;
1996
+ try {
1997
+ validateQuery(op, updated);
1998
+ } catch {
1999
+ return null;
2000
+ }
2001
+ return command(op, updated);
2002
+ }
2003
+ function renderQuery(operation, params, raw) {
2004
+ const parsed = envelopeSchema.safeParse(raw);
2005
+ if (!parsed.success) fail();
2006
+ const response = parsed.data;
2007
+ if (operation === "search" ? !Array.isArray(response.data) : !record(response.data)) fail();
2008
+ const { view: view2, truncation } = response.meta;
2009
+ const guidance = [];
2010
+ const lines = [`View ${view2.view_id}: version ${view2.active.version}; base ${view2.active.base_commit}; overlay ${view2.active.overlay_digest}`];
2011
+ if (view2.pending) lines.push(`Pending version ${view2.pending.version}: ${clean(view2.pending.state)}${view2.pending.progress ? ` (${view2.pending.progress.done}/${view2.pending.progress.total})` : ""}`);
2012
+ let visited = 0;
2013
+ const buckets = {};
2014
+ const walk = (value, label3, depth, enclosingSymbol, countBuckets = true) => {
2015
+ if (++visited > 1e5 || depth > 32) fail();
2016
+ if (record(value)) {
2017
+ const symbol = typeof value["symbol_id"] === "string" ? value["symbol_id"] : enclosingSymbol;
2018
+ if ("path" in value && "start_line" in value && "sha256" in value) {
2019
+ const citation = citationSchema.safeParse(value);
2020
+ if (!citation.success || citation.data.end_line < citation.data.start_line || citation.data.version !== view2.active.version) fail();
2021
+ const c = citation.data;
2022
+ lines.push(`${label3}: ${clean(c.path)}:${c.start_line}${c.end_line === c.start_line ? "" : `-${c.end_line}`}@${c.sha256} [${c.from}, version ${c.version}]`);
2023
+ const context = command("context", { ...symbol || params.symbol ? { symbol: symbol ?? params.symbol } : { q: params.q ?? "symbol name" }, worktree: params.worktree });
2024
+ lines.push(c.snippet !== void 0 ? c.snippet.split("\n").map(clean).join("\n") : `Snippet not supplied; retrieve indexed text: ${context}`);
2025
+ for (const [key, extra] of Object.entries(value)) if (!["path", "start_line", "end_line", "sha256", "version", "from", "snippet"].includes(key)) walk(extra, `${label3}.${clean(key)}`, depth + 1, symbol, countBuckets);
2026
+ } else for (const [key, child] of Object.entries(value)) walk(child, `${label3}.${clean(key)}`, depth + 1, symbol, countBuckets);
2027
+ } else if (Array.isArray(value)) {
2028
+ if (countBuckets && label3 !== "data") buckets[label3.slice(5)] = value.length;
2029
+ if (!value.length) lines.push(`${label3}: []`);
2030
+ value.forEach((v, i) => walk(v, `${label3}[${i}]`, depth + 1, enclosingSymbol, false));
2031
+ } else lines.push(`${label3}: ${clean(typeof value === "string" ? value : JSON.stringify(value))}`);
2032
+ };
2033
+ walk(response.data, "data", 0);
2034
+ const counts = Array.isArray(response.data) ? response.data.length : buckets;
2035
+ const total = typeof truncation["total"] === "number" ? truncation["total"] : response.meta["total"];
2036
+ lines.push(`Truncated: ${truncation.truncated ? "yes" : "no"}; returned: ${JSON.stringify(counts)}; total: ${typeof total === "number" ? total : "unknown (not supplied by server)"}`);
2037
+ if (truncation.truncated) {
2038
+ lines.push(`Truncation metadata: ${JSON.stringify(truncation)}`);
2039
+ const next = nextCommand(operation, params, truncation["next"]);
2040
+ guidance.push(next ? `Next: ${next}` : `Next: narrow the query or use rg -n; ${typeof truncation["hint"] === "string" ? clean(truncation["hint"]) : "the server supplied no larger request."}`);
2041
+ }
2042
+ const coverage = record(response.data) ? response.data["not_tracked"] : void 0;
2043
+ if (Array.isArray(coverage) && coverage.length) {
2044
+ for (const entry of coverage) if (record(entry)) guidance.push(`Coverage${entry["partial"] === true ? " (partial)" : ""}: ${clean(entry["reason"])}`);
2045
+ }
2046
+ if (Array.isArray(coverage) && coverage.length || Array.isArray(response.data) && response.data.length === 0) {
2047
+ const symbol = record(response.data) && record(response.data["symbol"]) ? response.data["symbol"]["name"] : void 0;
2048
+ guidance.push(`Search locally: rg -n --fixed-strings -- ${shell(params.q ?? (typeof symbol === "string" ? symbol : "symbol name"))} .`);
2049
+ }
2050
+ return { json: { ...response, guidance }, human: lines.concat(guidance).join("\n") + "\n" };
2051
+ }
2052
+ async function queryCode(api, operation, params, options) {
2053
+ validateQuery(operation, params);
2054
+ const id = await identity(options.cwd);
2055
+ const policy2 = await api.getPolicy();
2056
+ if (!z2.string().uuid().safeParse(policy2.org_id).success) fail();
2057
+ const path = statePath(api.origin, policy2.org_id, id.root, id.repoKey, options.stateDir);
2058
+ const state2 = await readState(path, api.origin, policy2.org_id, id.root, id.repoKey);
2059
+ if (!state2?.viewId || !state2.repositoryId) throw new CommandError(`This worktree has no code view. Run harness code sync --path ${shell(id.root)} first.`);
2060
+ const view2 = await api.getView(state2.viewId);
2061
+ const matches = (v) => v.view_id === state2.viewId && v.repository_id === state2.repositoryId && v.view_key === state2.viewKey;
2062
+ if (!matches(view2)) throw new CommandError("The code view does not match this repository/worktree; no query was sent.");
2063
+ if (!view2.active) throw new CommandError(`The view has no indexed version yet. Check harness code status --path ${shell(id.root)} until indexing finishes.`);
2064
+ try {
2065
+ const raw = await api.query(state2.viewId, operation, params);
2066
+ const result = renderQuery(operation, { ...params, worktree: id.root }, raw);
2067
+ if (!matches(result.json.meta.view)) throw new CommandError("The query view does not match this repository/worktree; no result was presented.");
2068
+ return result;
2069
+ } catch (error) {
2070
+ if (error instanceof HttpError && error.status === 404 && operation !== "search") throw new CommandError(`The code symbol or view was not found. Search again with ${command("search", { q: "symbol name", worktree: id.root })}; symbol IDs are opaque and can change after edits. Check code status for a removed view.`);
2071
+ throw error;
2072
+ }
2073
+ }
2074
+
2075
+ // src/commands/code/index.ts
2076
+ function enterWorktree(options) {
2077
+ const cwd = process.cwd();
2078
+ const before = statSync(".");
2079
+ const declared = statSync(cwd);
2080
+ if (before.dev !== declared.dev || before.ino !== declared.ino)
2081
+ throw new LocalError("Cannot enter the code worktree safely.");
2082
+ const supplied = options.path ?? cwd;
2083
+ if (/[\x00-\x1f\x7f-\x9f]/.test(supplied) || Buffer.from(supplied, "utf8").toString("utf8") !== supplied || options.path !== void 0 && supplied.includes("\uFFFD"))
2084
+ throw new LocalError("Cannot enter the code worktree safely.");
2085
+ const path = resolve2(supplied);
2086
+ try {
2087
+ const target = statSync(path);
2088
+ if (options.path === void 0 && (before.dev !== target.dev || before.ino !== target.ino))
2089
+ throw new Error("current directory identity changed");
2090
+ process.chdir(path);
2091
+ const entered = statSync(".");
2092
+ if (entered.dev !== target.dev || entered.ino !== target.ino)
2093
+ throw new Error("worktree identity changed");
2094
+ } catch {
2095
+ throw new LocalError("Cannot enter the code worktree; check --path.");
2096
+ }
2097
+ return { ...options, path };
2098
+ }
2099
+ async function sync(ctx, options = {}) {
2100
+ try {
2101
+ const result = await syncCode(new HttpCodeApi(ctx.config), {
2102
+ cwd: options.path ?? process.cwd(),
2103
+ skipped: options.skipped
2104
+ });
2105
+ return {
2106
+ json: result,
2107
+ exitCode: result.state === "failed" ? 1 : 0,
2108
+ human: `${clean(result.repo_key)}: ${clean(result.state)} version ${result.pending_version}; uploaded ${result.uploaded_blobs} overlay blobs.
2109
+ View: ${result.view_id}
2110
+ ${result.excluded.length} overlay files skipped; ${result.base.excluded_count ?? 0} base files skipped.${options.skipped ? "\n" + (result.base.skipped ?? []).map((s) => `${clean(s.path)}: ${clean(s.reason)}`).join("\n") : ""}
2111
+ ${result.warnings.map((w) => `Warning: ${clean(w)}
2112
+ `).join("")}`
2113
+ };
2114
+ } catch (error) {
2115
+ throw error instanceof HttpError ? describeCodeError(error) : error;
2116
+ }
2117
+ }
2118
+ async function status2(ctx, options = {}) {
2119
+ try {
2120
+ const result = await statusCode(new HttpCodeApi(ctx.config), {
2121
+ cwd: options.path ?? process.cwd(),
2122
+ skipped: options.skipped
2123
+ });
2124
+ const report = "base" in result ? result.base : void 0;
2125
+ const pending = "pending" in result ? result.pending : null;
2126
+ const serverError = pending?.error ?? report?.error;
2127
+ const failure = serverError && typeof serverError === "object" && "message" in serverError && typeof serverError.message === "string" ? clean(serverError.message) : "";
2128
+ return {
2129
+ json: result,
2130
+ exitCode: result.state === "failed" ? 1 : 0,
2131
+ human: `${clean(result.repo_key)}: ${result.state.replace("_", " ")}${failure ? ": " + failure : ""}
2132
+ ${report?.skipped?.map((s) => `${clean(s.path)}: ${clean(s.reason)}
2133
+ `).join("") ?? ""}${("warnings" in result ? result.warnings : [])?.map((w) => `Warning: ${clean(w)}
2134
+ `).join("") ?? ""}`
2135
+ };
2136
+ } catch (error) {
2137
+ throw error instanceof HttpError ? describeCodeError(error) : error;
2138
+ }
2139
+ }
2140
+ async function query(ctx, operation, params, options = {}) {
2141
+ try {
2142
+ return await queryCode(new HttpCodeApi(ctx.config), operation, params, { cwd: options.path ?? process.cwd() });
2143
+ } catch (error) {
2144
+ throw error instanceof HttpError ? describeCodeError(error) : error;
2145
+ }
2146
+ }
2147
+
2148
+ // src/cli.ts
2149
+ function promptLine(question) {
2150
+ const readline = createInterface({
2151
+ input: process.stdin,
2152
+ output: process.stderr
2153
+ });
2154
+ return new Promise((resolve3) => {
2155
+ readline.question(question, (answer) => {
2156
+ readline.close();
2157
+ resolve3(answer);
2158
+ });
2159
+ });
2160
+ }
2161
+ function exitWith(code2) {
2162
+ const drain = (stream) => new Promise((resolve3) => stream.write("", () => resolve3()));
2163
+ return Promise.all([drain(process.stdout), drain(process.stderr)]).then(
2164
+ () => process.exit(code2)
2165
+ );
2166
+ }
2167
+ guardBrokenPipes();
2168
+ var program = new Command();
2169
+ var invokedAs = invokedName();
2170
+ function writeStderr(text) {
2171
+ return process.stderr.write(named(text, invokedAs));
2172
+ }
2173
+ program.name(invokedAs).description(
2174
+ invokedAs === "operator" ? "drive Operator from the terminal and any agent, with Harness issue tracking alongside (harness.infra.one)" : "issue tracking for humans and agents (harness.infra.one)"
2175
+ ).exitOverride().configureOutput({
2176
+ writeOut: (text) => process.stdout.write(named(text, invokedAs)),
2177
+ writeErr: (text) => writeStderr(text)
2178
+ });
2179
+ function integer2(value) {
2180
+ if (!/^\d+$/.test(value))
2181
+ throw new InvalidArgumentError("expected a non-negative integer");
2182
+ return Number.parseInt(value, 10);
2183
+ }
2184
+ function activityLimit(value) {
2185
+ const limit = integer2(value);
2186
+ if (limit < 1 || limit > ACTIVITY_MAX_LIMIT) {
2187
+ throw new InvalidArgumentError(
2188
+ `expected an integer from 1 to ${ACTIVITY_MAX_LIMIT}`
2189
+ );
2190
+ }
2191
+ return limit;
2192
+ }
2193
+ function collect(value, previous) {
2194
+ return [...previous, value];
2195
+ }
2196
+ function resolvedCommentBody(text, bodyFile, action = "post") {
2197
+ if (bodyFile !== void 0 && text.length > 0) {
2198
+ throw new UsageError(
2199
+ "comment text and --body-file both given \u2014 pass one; there is no precedence between them"
2200
+ );
2201
+ }
2202
+ const fromFile = resolveTextSources([
2203
+ { name: "body", inline: void 0, file: bodyFile }
2204
+ ]).body;
2205
+ const body = fromFile === void 0 ? text : [fromFile];
2206
+ if (body.join(" ").trim() === "") {
2207
+ throw new UsageError(
2208
+ bodyFile === void 0 ? `the comment text is empty \u2014 nothing to ${action}` : `--body-file ${bodyFile} produced an empty comment \u2014 nothing to ${action}`
2209
+ );
2210
+ }
2211
+ return body;
2212
+ }
2213
+ var requireIdentifiers2 = requireIdentifiers;
2214
+ function interactive() {
2215
+ const ci = process.env["CI"];
2216
+ return process.stdout.isTTY === true && (ci === void 0 || ci === "");
2217
+ }
2218
+ function canConfirmRepoLogin() {
2219
+ return process.stdin.isTTY === true && process.stderr.isTTY === true;
2220
+ }
2221
+ async function confirmRepoLogin(baseUrl, source, path) {
2222
+ if (source !== "repo-config" || isHostTrusted(baseUrl)) return true;
2223
+ const answer = await promptLine(
2224
+ `This repo's config (${path ?? ".harness-cli/config.json"}) targets ${baseUrl} \u2014 log in there? [y/N] `
2225
+ );
2226
+ if (answer.trim().toLowerCase() !== "y") {
2227
+ writeStderr(`harness: not logging in to ${baseUrl} \u2014 set HARNESS_API_URL to override the repo config
2228
+ `);
2229
+ return false;
2230
+ }
2231
+ trustHost(baseUrl);
2232
+ return true;
2233
+ }
2234
+ async function withConfig(jsonFlag, mutating, run3) {
2235
+ const resolved = resolveConfig();
2236
+ for (const warning of resolved.warnings) writeStderr(warning + "\n");
2237
+ if (resolved.kind === "error") {
2238
+ writeStderr(`harness: ${resolved.message}
2239
+ `);
2240
+ return exitWith(2);
2241
+ }
2242
+ if (resolved.kind === "needs-login") {
2243
+ if (!interactive()) {
2244
+ writeStderr(loginHelp(resolved.baseUrl));
2245
+ if (resolved.org !== void 0) writeStderr(`Choose organization ${resolved.org} when logging in.
2246
+ `);
2247
+ return exitWith(2);
2248
+ }
2249
+ if (!await confirmRepoLogin(resolved.baseUrl, resolved.hostSource, resolved.repoConfigPath)) {
2250
+ return exitWith(2);
2251
+ }
2252
+ const loginResult = await login(resolved.baseUrl, resolved.org === void 0 ? {} : { org: resolved.org });
2253
+ if (!loginResult.ok) {
2254
+ writeStderr(`harness: ${clean(loginResult.message)}
2255
+ `);
2256
+ return exitWith(1);
2257
+ }
2258
+ writeStderr(loginResult.human);
2259
+ const again = resolveConfig();
2260
+ if (again.kind !== "ok") {
2261
+ writeStderr(
2262
+ `harness: still not signed in to ${resolved.baseUrl} after logging in
2263
+ `
2264
+ );
2265
+ return exitWith(1);
2266
+ }
2267
+ return execute(again.config, jsonFlag, mutating, run3);
2268
+ }
2269
+ return execute(resolved.config, jsonFlag, mutating, run3);
2270
+ }
2271
+ async function execute(config, jsonFlag, mutating, run3) {
2272
+ const client = new Client(config);
2273
+ const hostNotice = mutating ? `\u2192 ${config.baseUrl}
2274
+ ` : "";
2275
+ try {
2276
+ const output = await run3({ client, config });
2277
+ writeStderr(hostNotice);
2278
+ if (output.notice !== void 0) writeStderr(output.notice);
2279
+ process.stdout.write(jsonFlag ? json(output.json) : output.human);
2280
+ return exitWith(output.exitCode ?? (output.partial === true ? 1 : 0));
2281
+ } catch (error) {
2282
+ return fail2(explainExpertRefusal(error, config.token), hostNotice);
2283
+ }
2284
+ }
2285
+ async function fail2(error, hostNotice) {
2286
+ if (error instanceof UsageError) {
2287
+ writeStderr(error.message + "\n");
2288
+ return exitWith(2);
2289
+ }
2290
+ if (error instanceof LocalError) {
2291
+ writeStderr(`harness: ${clean(error.message)}
2292
+ `);
2293
+ return exitWith(1);
2294
+ }
2295
+ if (error instanceof TransportError) {
2296
+ writeStderr(
2297
+ hostNotice + `harness: cannot reach the API: ${clean(error.message)}
2298
+ `
2299
+ );
2300
+ return exitWith(3);
2301
+ }
2302
+ if (error instanceof UpgradeRequiredError) {
2303
+ const healed = selfHeal();
2304
+ if (healed.kind === "upgraded") return exitWith(healed.code);
2305
+ const why = healed.kind === "failed" ? ` (tried to upgrade: ${clean(healed.reason)})` : "";
2306
+ writeStderr(
2307
+ hostNotice + `harness: ${clean(error.message)} (HTTP 426)${why}
2308
+ `
2309
+ );
2310
+ return exitWith(1);
2311
+ }
2312
+ if (error instanceof HttpError) {
2313
+ writeStderr(
2314
+ hostNotice + `harness: ${clean(error.message)} (HTTP ${error.status})${errorDetails(error.body)}
2315
+ `
2316
+ );
2317
+ return exitWith(1);
2318
+ }
2319
+ if (error instanceof CommandError) {
2320
+ writeStderr(hostNotice + `harness: ${clean(error.message)}
2321
+ `);
2322
+ return exitWith(1);
2323
+ }
2324
+ throw error;
2325
+ }
2326
+ program.command("ready").description("issues ready to work, highest priority first").option(
2327
+ "--claim",
2328
+ "claim the team's next ready, unassigned issue in one server-side request (team from --team, HARNESS_TEAM or the repo config; no paging flags)"
2329
+ ).option("--team <key>", "team filter; with --claim, the team to claim from").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2330
+ if (opts.claim === true) requireReadyClaimArgs(opts);
2331
+ await withConfig(
2332
+ opts.json === true,
2333
+ opts.claim === true,
2334
+ (ctx) => ready(ctx, opts)
2335
+ );
2336
+ });
2337
+ program.command("blocked").description("issues waiting on a blocker").option("--team <key>", "team filter").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2338
+ await withConfig(
2339
+ opts.json === true,
2340
+ false,
2341
+ (ctx) => blocked(ctx, opts)
2342
+ );
2343
+ });
2344
+ program.command("list").description("list issues with filters").option("--status <status>", "open | in_progress | closed").option(
2345
+ "--assignee <who>",
2346
+ 'me, a user id, an email, or a full name; "" for unassigned'
2347
+ ).option("--created-by <who>", "me or a creator user id").option(
2348
+ "--involved <who>",
2349
+ "me for issues created by, assigned to, or mentioning you"
2350
+ ).option("--priority <name>", "urgent | high | medium | low | none").option("--label <name>", "label filter").option("--type <type>", "task | bug | feature | epic").option("--search <query>", "full-text search").option("--team <key>", "team filter").option("--parent <id>", "children of an epic").option("--scope <scope>", "all | archived").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2351
+ await withConfig(
2352
+ opts.json === true,
2353
+ false,
2354
+ (ctx) => list(ctx, opts)
2355
+ );
2356
+ });
2357
+ program.command("search").description(
2358
+ "search by meaning \u2014 ranked, exact identifier first; use list --search for paginated filtering"
2359
+ ).argument("[query...]").option("--limit <n>", "maximum results (default 10, capped at 20)", integer2).option(
2360
+ "--page <n>",
2361
+ "(deprecated, ignored \u2014 results are ranked and capped)",
2362
+ integer2
2363
+ ).option("--all", "(deprecated, ignored \u2014 results are ranked and capped)").option("--json", "emit JSON").action(
2364
+ async (query2, opts) => {
2365
+ await withConfig(
2366
+ opts.json === true,
2367
+ false,
2368
+ (ctx) => search(ctx, query2, opts)
2369
+ );
2370
+ }
2371
+ );
2372
+ program.command("show").description("full issue detail; downloads attached images locally").argument("[id]").option("--no-images", "skip attachment downloads").option("--json", "emit JSON").action(
2373
+ async (id, opts) => {
2374
+ await withConfig(opts.json === true, false, (ctx) => {
2375
+ if (id === void 0) throw new UsageError("usage: harness show <id>");
2376
+ requireIdentifiers2([id]);
2377
+ return show(ctx, id, opts);
2378
+ });
2379
+ }
2380
+ );
2381
+ program.command("activity").description("mixed issue activity: comments, creation, state, assignee, wait-condition and field changes").argument("[id]").option(
2382
+ "--limit <n>",
2383
+ `entries per page (default 20, max ${ACTIVITY_MAX_LIMIT})`,
2384
+ activityLimit
2385
+ ).option("--all", "read all older activity").option("--json", "emit JSON").action(async (id, opts) => {
2386
+ await withConfig(opts.json === true, false, (ctx) => {
2387
+ if (id === void 0) throw new UsageError("usage: harness activity <id>");
2388
+ requireIdentifiers2([id]);
2389
+ return activity(ctx, id, opts);
2390
+ });
2391
+ });
2392
+ program.command("stats").description("per-team and total counts").option("--json", "emit JSON").action(async (opts) => {
2393
+ await withConfig(opts.json === true, false, (ctx) => stats(ctx));
2394
+ });
2395
+ program.command("prime").description("workflow context for agent sessions: stats + ready queue").option("--json", "emit JSON").action(async (opts) => {
2396
+ await withConfig(opts.json === true, false, (ctx) => prime(ctx));
2397
+ });
2398
+ program.command("version").description("print CLI and server API versions").option("--json", "emit JSON").action(async (opts) => {
2399
+ const resolved = resolveConfig();
2400
+ let config = null;
2401
+ if (resolved.kind === "ok") config = resolved.config;
2402
+ else if (resolved.kind === "needs-login") {
2403
+ config = {
2404
+ baseUrl: resolved.baseUrl,
2405
+ token: "",
2406
+ actor: null,
2407
+ team: null,
2408
+ cacheDir: ""
2409
+ };
2410
+ }
2411
+ const output = await version(config);
2412
+ process.stdout.write(
2413
+ opts.json === true ? json(output.json) : output.human
2414
+ );
2415
+ await exitWith(0);
2416
+ });
2417
+ function issueTextSources(opts) {
2418
+ return [
2419
+ { name: "title", inline: opts.title, file: opts.titleFile },
2420
+ { name: "description", inline: opts.description, file: opts.descriptionFile },
2421
+ { name: "design", inline: opts.design, file: opts.designFile },
2422
+ { name: "notes", inline: opts.notes, file: opts.notesFile },
2423
+ { name: "acceptance", inline: opts.acceptance, file: opts.acceptanceFile }
2424
+ ];
2425
+ }
2426
+ function requireNonBlankInlineTitle(title) {
2427
+ if (title !== void 0 && title.trim() === "") {
2428
+ throw new UsageError("--title is empty \u2014 an issue needs a title");
2429
+ }
2430
+ }
2431
+ function requireNonBlankTitle(text, titleFile) {
2432
+ if ((text["title"] ?? "").trim() !== "") return;
2433
+ throw new UsageError(
2434
+ titleFile === void 0 ? "--title is empty \u2014 an issue needs a title" : `--title-file ${titleFile} produced an empty title`
2435
+ );
2436
+ }
2437
+ program.command("create").description("create an issue atomically after resolving requested labels").option("--title <text>", "issue title (required)").option("--description <text>", "problem statement").option("--design <text>", "design notes").option("--notes <text>", "working notes").option("--acceptance <text>", "acceptance criteria").option("--title-file <path>", 'read the title from a file ("-" for stdin)').option("--description-file <path>", 'read the description from a file ("-" for stdin)').option("--design-file <path>", 'read the design notes from a file ("-" for stdin)').option("--notes-file <path>", 'read the notes from a file ("-" for stdin)').option("--acceptance-file <path>", 'read the acceptance criteria from a file ("-" for stdin)').option(
2438
+ "--type <type>",
2439
+ "task | bug | feature | epic (applied as a type/ label)"
2440
+ ).option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", "YYYY-MM-DD").option("--parent <id>", "parent epic").option("--assignee <who>", "me, a user id, an email, or a full name").option("--team <key>", "team key (or HARNESS_TEAM)").option("--idempotency-key <key>", "reuse the same key when deliberately retrying this create").option(
2441
+ "--label <name>",
2442
+ "resolve team-first, then workspace; create missing workspace labels before the issue (repeatable)",
2443
+ collect,
2444
+ []
2445
+ ).option("--json", "emit JSON").action(async (opts) => {
2446
+ await withConfig(opts.json === true, true, (ctx) => {
2447
+ if (opts.title === void 0 && opts.titleFile === void 0) {
2448
+ throw new UsageError(
2449
+ "usage: harness create --title <title> | --title-file <path> [--team <key>] \u2026"
2450
+ );
2451
+ }
2452
+ requireNonBlankInlineTitle(opts.title);
2453
+ const text = resolveTextSources(issueTextSources(opts));
2454
+ requireNonBlankTitle(text, opts.titleFile);
2455
+ return create(ctx, { ...opts, ...text });
2456
+ });
2457
+ });
2458
+ program.command("update").description("update an issue; --claim takes it atomically").argument("[id]").option("--claim", "claim atomically and move to the first started state").option("--state <name>", "workflow state by name or type").option("--title <text>").option("--description <text>").option("--design <text>").option("--notes <text>", "replace the notes").option("--append-notes <text>", "append to the notes").option("--acceptance <text>").option("--title-file <path>", 'read the title from a file ("-" for stdin)').option("--description-file <path>", 'read the description from a file ("-" for stdin)').option("--design-file <path>", 'read the design notes from a file ("-" for stdin)').option("--notes-file <path>", 'replace the notes from a file ("-" for stdin)').option("--append-notes-file <path>", 'append to the notes from a file ("-" for stdin)').option("--acceptance-file <path>", 'read the acceptance criteria from a file ("-" for stdin)').option("--priority <name>", "urgent | high | medium | low | none").option("--due-date <date>", 'YYYY-MM-DD; "" clears').option("--parent <id>").option(
2459
+ "--assignee <who>",
2460
+ 'me, a user id, an email, or a full name; "" unassigns'
2461
+ ).option(
2462
+ "--add-label <name-or-id>",
2463
+ "add a label without replacing existing labels (repeatable)",
2464
+ collect,
2465
+ []
2466
+ ).option(
2467
+ "--remove-label <name-or-id>",
2468
+ "remove an attached label without replacing others (repeatable)",
2469
+ collect,
2470
+ []
2471
+ ).option("--json", "emit JSON").action(
2472
+ async (id, opts) => {
2473
+ await withConfig(opts.json === true, true, (ctx) => {
2474
+ if (id === void 0) {
2475
+ throw new UsageError(
2476
+ "usage: harness update <id> --claim | --title \u2026 | --state <name> | \u2026"
2477
+ );
2478
+ }
2479
+ requireIdentifiers2([id]);
2480
+ requireNonBlankInlineTitle(opts.title);
2481
+ const text = resolveTextSources([
2482
+ ...issueTextSources(opts),
2483
+ { name: "appendNotes", inline: opts.appendNotes, file: opts.appendNotesFile }
2484
+ ]);
2485
+ if (Object.hasOwn(text, "title")) requireNonBlankTitle(text, opts.titleFile);
2486
+ return update(ctx, id, { ...opts, ...text });
2487
+ });
2488
+ }
2489
+ );
2490
+ program.command("close").description("close issues; the response lists newly unblocked issues").argument("[ids...]").option("--reason <text>", "why it is done (e.g. the commit hash)").option("--reason-file <path>", 'read the reason from a file ("-" for stdin)').option("--cancel", "close as canceled instead of completed").option("--duplicate-of <id>", "close one issue as a duplicate of the canonical issue").option("--json", "emit JSON").action(
2491
+ async (ids, opts) => {
2492
+ if (ids.length === 0) {
2493
+ throw new UsageError(
2494
+ "usage: harness close <id> [<id>\u2026] [--reason <text> | --reason-file <path>] [--cancel | --duplicate-of <id>]"
2495
+ );
2496
+ }
2497
+ requireCloseArgs(ids, opts);
2498
+ await withConfig(opts.json === true, true, (ctx) => {
2499
+ const text = resolveTextSources([
2500
+ { name: "reason", inline: opts.reason, file: opts.reasonFile }
2501
+ ]);
2502
+ if (Object.hasOwn(text, "reason") && (text["reason"] ?? "").trim() === "") {
2503
+ throw new UsageError(
2504
+ opts.reasonFile === void 0 ? "--reason is empty \u2014 omit it, or give a reason" : `--reason-file ${opts.reasonFile} produced an empty reason`
2505
+ );
2506
+ }
2507
+ return close(ctx, ids, { ...opts, ...text });
2508
+ });
2509
+ }
2510
+ );
2511
+ program.command("archive").description("archive issues; --restore brings them back").argument("[ids...]").option("--restore", "restore instead of archive").option("--json", "emit JSON").action(
2512
+ async (ids, opts) => {
2513
+ await withConfig(opts.json === true, true, (ctx) => {
2514
+ if (ids.length === 0)
2515
+ throw new UsageError(
2516
+ "usage: harness archive <id> [<id>\u2026] [--restore]"
2517
+ );
2518
+ requireIdentifiers2(ids);
2519
+ return archive(ctx, ids, opts);
2520
+ });
2521
+ }
2522
+ );
2523
+ program.command("comments").description("an issue's full comment thread, untruncated").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
2524
+ await withConfig(opts.json === true, false, (ctx) => {
2525
+ if (id === void 0)
2526
+ throw new UsageError("usage: harness comments <id>");
2527
+ requireIdentifiers2([id]);
2528
+ return comments(ctx, id);
2529
+ });
2530
+ });
2531
+ program.command("dep").description("blocking dependencies: dep add|rm <issue> <blocker>").argument("[verb]").argument("[issue]").argument("[blocker]").option("--json", "emit JSON").action(
2532
+ async (verb, issue, blocker, opts) => {
2533
+ await withConfig(opts.json === true, true, (ctx) => {
2534
+ if (verb === void 0 || issue === void 0 || blocker === void 0) {
2535
+ throw new UsageError("usage: harness dep add|rm <issue> <blocker>");
2536
+ }
2537
+ requireIdentifiers2([issue, blocker]);
2538
+ return dep(ctx, verb, issue, blocker);
2539
+ });
2540
+ }
2541
+ );
2542
+ var relation = program.command("relation").description("typed issue relations; dep remains the blocking-dependency shorthand");
2543
+ relation.command("list").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
2544
+ await withConfig(opts.json === true, false, (ctx) => {
2545
+ if (issue === void 0) throw new UsageError("usage: harness relation list <issue>");
2546
+ return relationList(ctx, issue);
2547
+ });
2548
+ });
2549
+ relation.command("add").argument("[issue]").argument("[related-issue]").requiredOption("--type <type>", `relation type: ${RELATION_CREATE_TYPES.join(", ")}`).option("--json", "emit JSON").action(
2550
+ async (issue, relatedIssue, opts) => {
2551
+ await withConfig(opts.json === true, true, (ctx) => {
2552
+ if (issue === void 0 || relatedIssue === void 0) {
2553
+ throw new UsageError("usage: harness relation add <issue> <related-issue> --type <type>");
2554
+ }
2555
+ return relationAdd(ctx, issue, relatedIssue, opts.type);
2556
+ });
2557
+ }
2558
+ );
2559
+ relation.command("update").argument("[issue]").argument("[id-or-issue]").requiredOption("--type <type>", `relation type: ${RELATION_UPDATE_TYPES.join(", ")}`).option("--json", "emit JSON").action(
2560
+ async (issue, idOrIssue, opts) => {
2561
+ await withConfig(opts.json === true, true, (ctx) => {
2562
+ if (issue === void 0 || idOrIssue === void 0) {
2563
+ throw new UsageError("usage: harness relation update <issue> <relation-id-or-issue> --type <type>");
2564
+ }
2565
+ return relationUpdate(ctx, issue, idOrIssue, opts.type);
2566
+ });
2567
+ }
2568
+ );
2569
+ relation.command("rm").argument("[issue]").argument("[id-or-issue]").option("--json", "emit JSON").action(async (issue, idOrIssue, opts) => {
2570
+ await withConfig(opts.json === true, true, (ctx) => {
2571
+ if (issue === void 0 || idOrIssue === void 0) {
2572
+ throw new UsageError("usage: harness relation rm <issue> <relation-id-or-issue>");
2573
+ }
2574
+ return relationRm(ctx, issue, idOrIssue);
2575
+ });
2576
+ });
2577
+ var plan = program.command("plan").description("plans: a whole graph of issues created in one request, then released and held");
2578
+ plan.command("create").description("create a plan from a JSON or YAML file (the API's request body); it starts held").option("--file <path>", 'the plan: .json is JSON, anything else YAML ("-" for stdin)').option("--team <key>", "team of the root and of people's steps (or HARNESS_TEAM)").option("--idempotency-key <key>", "reuse the same key when deliberately retrying this create").option("--json", "emit JSON").action(async (opts) => {
2579
+ await withConfig(opts.json === true, true, (ctx) => {
2580
+ if (opts.file === void 0) {
2581
+ throw new UsageError("usage: harness plan create --file <plan.json|plan.yaml> [--team <key>]");
2582
+ }
2583
+ const team2 = requiredTeam(ctx, opts.team);
2584
+ validateIdempotencyKey(opts.idempotencyKey);
2585
+ return planCreate(ctx, readPlanFile(opts.file), { ...opts, team: team2 });
2586
+ });
2587
+ });
2588
+ plan.command("list").description("a team's plans, newest first: root, status, held, revision and title").option("--team <key>", "the team whose issues root the plans (or HARNESS_TEAM)").option("--status <status>", PLAN_STATUSES.join(" | ")).option("--limit <n>", `maximum plans (default 50, at most ${PLAN_LIST_MAX_LIMIT}; there are no pages)`, integer2).option("--json", "emit JSON").action(async (opts) => {
2589
+ requirePlanListArgs(opts);
2590
+ await withConfig(opts.json === true, false, (ctx) => planList(ctx, opts));
2591
+ });
2592
+ var PLAN_ARGUMENT = "the plan's id, or its root issue's identifier (e.g. FR-1)";
2593
+ plan.command("show").description("a plan's status, budgets, revision, steps, edges and wait conditions").argument("[plan]", PLAN_ARGUMENT).option("--json", "emit JSON").action(async (id, opts) => {
2594
+ await withConfig(opts.json === true, false, (ctx) => {
2595
+ if (id === void 0) throw new UsageError("usage: harness plan show <plan-id-or-root>");
2596
+ return planShow(ctx, id);
2597
+ });
2598
+ });
2599
+ plan.command("edit").description("change step executors and keys, and budgets, from a JSON or YAML file").argument("[plan]", PLAN_ARGUMENT).option("--file <path>", 'the edit: .json is JSON, anything else YAML ("-" for stdin)').option("--json", "emit JSON").action(async (id, opts) => {
2600
+ await withConfig(opts.json === true, true, (ctx) => {
2601
+ if (id === void 0 || opts.file === void 0) {
2602
+ throw new UsageError("usage: harness plan edit <plan-id-or-root> --file <edits.json|edits.yaml>");
2603
+ }
2604
+ requirePlanRef(id);
2605
+ return planEdit(ctx, id, readPlanFile(opts.file));
2606
+ });
2607
+ });
2608
+ plan.command("release").description("release a plan as yourself: freeze its next revision and let its ready steps run").argument("[plan]", PLAN_ARGUMENT).option("--collaborator <who>", "a person of this organization (id or email) for the plan room (repeatable)", collect, []).option("--retry-budget <n>", "retries per failed Operator step", integer2).option("--spend-budget-cents <n>", "the plan's spend budget; needed to release Operator steps", integer2).option("--json", "emit JSON").action(
2609
+ async (id, opts) => {
2610
+ await withConfig(opts.json === true, true, (ctx) => {
2611
+ if (id === void 0) throw new UsageError("usage: harness plan release <plan-id-or-root>");
2612
+ return planRelease(ctx, id, { ...opts, collaborators: opts.collaborator });
2613
+ });
2614
+ }
2615
+ );
2616
+ plan.command("hold").description("pause every step of a plan until it is released again").argument("[plan]", PLAN_ARGUMENT).option("--json", "emit JSON").action(async (id, opts) => {
2617
+ await withConfig(opts.json === true, true, (ctx) => {
2618
+ if (id === void 0) throw new UsageError("usage: harness plan hold <plan-id-or-root>");
2619
+ return planHold(ctx, id);
2620
+ });
2621
+ });
2622
+ var gate = program.command("gate").description("manual wait conditions: gate satisfy|unsatisfy <issue> <gate>");
2623
+ for (const [verb, satisfied] of [
2624
+ ["satisfy", true],
2625
+ ["unsatisfy", false]
2626
+ ]) {
2627
+ gate.command(verb).description(`${satisfied ? "tick" : "untick"} a gate on an issue, by its key, label or condition id`).argument("[issue]").argument("[gate]").option("--json", "emit JSON").action(async (issue, gateName, opts) => {
2628
+ await withConfig(opts.json === true, true, (ctx) => {
2629
+ if (issue === void 0 || gateName === void 0) {
2630
+ throw new UsageError(`usage: harness gate ${verb} <issue> <gate-key-label-or-id>`);
2631
+ }
2632
+ return gateSet(ctx, issue, gateName, satisfied);
2633
+ });
2634
+ });
2635
+ }
2636
+ program.command("conditions").description("an issue's wait conditions; with an outside expert's pxc_ credential, the gates they own on it").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
2637
+ await withConfig(opts.json === true, false, (ctx) => {
2638
+ if (issue === void 0) throw new UsageError("usage: harness conditions <issue>");
2639
+ return conditionList(ctx, issue);
2640
+ });
2641
+ });
2642
+ program.command("assignments").description("an outside expert's steps and the gates they own on them (pxc_ credentials only)").option("--json", "emit JSON").action(async (opts) => {
2643
+ await withConfig(opts.json === true, false, (ctx) => assignments(ctx));
2644
+ });
2645
+ var NO_OBJECTIVE_TEXT = "By reference only: never the objective's text, which the API does not return because it can quote what Operator read; the workbench link opens it. Approval happens in the app.";
2646
+ program.command("jobs").description("Operator jobs you see in the workbench, newest first").option(
2647
+ "--status <states>",
2648
+ `comma-separated job states (${JOB_STATES.join(", ")}) or open | waiting | done`
2649
+ ).option("--mine", "only jobs you own").option(`--limit <n>`, `jobs per page (default 20, max ${JOBS_MAX_LIMIT})`, integer2).option("--all", `follow every page (up to ${JOBS_MAX_PAGES})`).option("--json", "emit JSON").addHelpText("after", `
2650
+ ${NO_OBJECTIVE_TEXT}
2651
+ `).action(async (opts) => {
2652
+ requireJobsArgs(opts);
2653
+ await withConfig(opts.json === true, false, (ctx) => jobsList(ctx, opts));
2654
+ });
2655
+ program.command("job").description("one Operator job: status, plan, progress, window, spend, approval link, effects, artifacts, people").argument("[id]").option("--changes", "instead, its change set by reference (never values or targets), with the approval link").option("--people", "instead, who is on it by name: requester, execution principal, owner, approvers, collaborators").option("--json", "emit JSON").addHelpText("after", `
2656
+ ${NO_OBJECTIVE_TEXT}
2657
+ `).action(async (id, opts) => {
2658
+ if (id === void 0) throw new UsageError("usage: harness job <job-id> [--changes] [--people]");
2659
+ requireJobId(id);
2660
+ await withConfig(
2661
+ opts.json === true,
2662
+ false,
2663
+ (ctx) => jobShow(ctx, id, { changes: opts.changes, people: opts.people })
2664
+ );
2665
+ });
2666
+ program.command("watch").description("follow an Operator job until it completes, fails, or waits for a person").argument("[id]").option("--after <seq>", "start after this event sequence (default 0: the whole history)", integer2).option("--through-waits", "keep following while a person has to act, instead of stopping").option("--json", "emit one JSON document when the watch ends").addHelpText(
2667
+ "after",
2668
+ `
2669
+ Polls the job's events every 2 s, and every 10 s while the job is paused or
2670
+ a person has to act. A person has to act exactly when the job's waiting_on is
2671
+ set: an approval, a person, reconciliation, verification or delivery. A job
2672
+ paused with nothing for a person to do resumes by itself, so watch follows it.
2673
+ It only reads: approval happens in the app, and the approval link is printed.
2674
+ A failed poll is retried 3 times (1, 2, 4 s, or the server's Retry-After).
2675
+
2676
+ Exit codes:
2677
+ 0 the job completed (or stdout's reader went away, e.g. | head)
2678
+ ${EXIT_WAITING_FOR_PERSON} a person has to act (not with --through-waits); the link is printed
2679
+ ${EXIT_JOB_ENDED} the job failed or was cancelled
2680
+ ${EXIT_INTERRUPTED} interrupted with Ctrl-C; stderr says where to resume (--after)
2681
+ 1, 3 as for every command: an API error, or the API unreachable after the retries;
2682
+ stderr says where to resume, and --json still prints the events read so far
2683
+ 2 a usage error
2684
+ `
2685
+ ).action(async (id, opts) => {
2686
+ if (id === void 0) throw new UsageError("usage: harness watch <job-id>");
2687
+ requireJobId(id);
2688
+ const json2 = opts.json === true;
2689
+ await withConfig(
2690
+ json2,
2691
+ false,
2692
+ (ctx) => jobWatch(ctx, id, {
2693
+ after: opts.after,
2694
+ throughWaits: opts.throughWaits === true,
2695
+ collectEvents: json2,
2696
+ onLine: json2 ? void 0 : (line) => process.stdout.write(line),
2697
+ signal: watchStopSignal()
2698
+ })
2699
+ );
2700
+ });
2701
+ var NOT_RETRIED = `
2702
+ Not idempotent, so it is never retried, not even after a timeout or a server
2703
+ error. When a failure leaves the outcome unknown it says so and exits 1:
2704
+ check \`job <id>\` before trying again. Exit 3 means the request never
2705
+ reached the server, so nothing changed. It answers the job as \`job\` shows
2706
+ it. Approving stays in the app.
2707
+ `;
2708
+ program.command("steer").description("post an instruction to an Operator job, as Steer does in the app; earlier approvals stop applying").argument("[id]").argument("[instruction...]").option("--instruction-file <path>", 'read the instruction from a file ("-" for stdin)').option("--json", "emit JSON").addHelpText(
2709
+ "after",
2710
+ `
2711
+ Posts the instruction in the job's room, attributed to you, wherever the app
2712
+ offers Steer; the job's owner and collaborators may steer. Work it changes
2713
+ needs a fresh approval, and a running job stops its current run to take it.
2714
+ At most ${STEER_MAX_CHARACTERS} characters once trimmed.
2715
+
2716
+ Quote the instruction: unquoted words that look like options (--json, --help)
2717
+ are read as options, not text. Put -- before an instruction that starts with
2718
+ "-", or pass it with --instruction-file, which takes any text as it is.
2719
+ ${NOT_RETRIED}`
2720
+ ).action(async (id, words, opts) => {
2721
+ if (id === void 0 || words.length === 0 && opts.instructionFile === void 0) {
2722
+ throw new UsageError(
2723
+ "usage: harness steer <job-id> <instruction> | harness steer <job-id> --instruction-file <path>"
2724
+ );
2725
+ }
2726
+ requireJobId(id);
2727
+ const instruction = instructionFrom(words, opts.instructionFile);
2728
+ await withConfig(opts.json === true, true, (ctx) => jobSteer(ctx, id, instruction));
2729
+ });
2730
+ program.command("cancel").description("cancel an Operator job; committed effects stay committed, and their counts are printed").argument("[id]").option("--json", "emit JSON").addHelpText(
2731
+ "after",
2732
+ `
2733
+ Ends the job, as Cancel does in the app; only its execution principal may.
2734
+ Staged effects that never ran count as unknown.
2735
+ ${NOT_RETRIED}`
2736
+ ).action(async (id, opts) => {
2737
+ if (id === void 0) throw new UsageError("usage: harness cancel <job-id>");
2738
+ requireJobId(id);
2739
+ await withConfig(opts.json === true, true, (ctx) => jobCancel(ctx, id));
2740
+ });
2741
+ program.command("pause").description("pause a running Operator job for a person, keeping its active-time budget").argument("[id]").option("--json", "emit JSON").addHelpText(
2742
+ "after",
2743
+ `
2744
+ Holds the job, as Pause does in the app; only its execution principal may.
2745
+ Continue it to run again.
2746
+ ${NOT_RETRIED}`
2747
+ ).action(async (id, opts) => {
2748
+ if (id === void 0) throw new UsageError("usage: harness pause <job-id>");
2749
+ requireJobId(id);
2750
+ await withConfig(opts.json === true, true, (ctx) => jobPause(ctx, id));
2751
+ });
2752
+ var NOT_IDEMPOTENT = "Not idempotent: it is sent once and never retried, not after a timeout or a 5xx either. Exit 3 means the request never reached the server, so nothing changed. When it may have, the outcome is unknown: it exits 1 and says so. Then check";
2753
+ program.command("run").description("start an Operator job from an outcome, as Run as job does in the app; --watch follows it").argument("[outcome...]").option("--budget <usd>", "US dollars the organization's monthly Operator budget must still have (admins only)", once("--budget")).option("--skill <name>", "run one of the organization's Operator skills, by invocation name (/name)", once("--skill")).option("--watch", "follow the job like `watch --through-waits`; it never approves").option("--json", "emit JSON").addHelpText(
2754
+ "after",
2755
+ `
2756
+ Starts a job for you in a new org chat of yours, under your own current rights.
2757
+ Prints the job id, its state and its workbench link, and the approval link
2758
+ while a change set waits for approval. Approving happens in the app; the CLI
2759
+ never approves. You can start 30 jobs an hour. ${NOT_IDEMPOTENT} \`jobs --mine\` for the new job before running it again.
2760
+
2761
+ With --watch the job is followed until it completes (exit 0) or fails or is
2762
+ cancelled (exit ${EXIT_JOB_ENDED}). When it waits for a person, the approval link is printed and the
2763
+ watch keeps waiting; Ctrl-C stops it (exit ${EXIT_INTERRUPTED}) and stderr says how to resume.
2764
+ `
2765
+ ).action(async (words, opts) => {
2766
+ const request = prepareRun(words.join(" "), { budget: opts.budget, skill: opts.skill });
2767
+ const json2 = opts.json === true;
2768
+ await withConfig(
2769
+ json2,
2770
+ true,
2771
+ (ctx) => run(ctx, request, {
2772
+ watch: opts.watch === true,
2773
+ json: json2,
2774
+ onLine: (line) => process.stdout.write(line),
2775
+ signal: opts.watch === true ? watchStopSignal() : void 0
2776
+ })
2777
+ );
2778
+ });
2779
+ function once(flag) {
2780
+ return (value, previous) => {
2781
+ if (previous !== void 0) throw new InvalidArgumentError(`${flag} given twice \u2014 pass it once`);
2782
+ return value;
2783
+ };
2784
+ }
2785
+ program.command("continue").description(
2786
+ "open a new audited window for an Operator job that ran out of time or waits on a person, as Continue does in the app"
2787
+ ).argument("[id]").option("--json", "emit JSON").addHelpText(
2788
+ "after",
2789
+ `
2790
+ Only the job's execution principal, the person it runs as, may continue it. ${NOT_IDEMPOTENT} \`job <id>\` before continuing it again.
2791
+ `
2792
+ ).action(async (id, opts) => {
2793
+ if (id === void 0) throw new UsageError("usage: harness continue <job-id>");
2794
+ requireJobId(id);
2795
+ await withConfig(opts.json === true, true, (ctx) => jobContinue(ctx, id));
2796
+ });
2797
+ var procedure = program.command("procedure").description("saved Operator procedures: procedure save|show|run");
2798
+ procedure.command("save").description("save a completed, verified job as a procedure, as Save procedure does in the app (its owner only)").argument("[job-id]").option("--name <name>", "the procedure's name (trimmed, at most 200 characters)", once("--name")).option("--json", "emit JSON").addHelpText(
2799
+ "after",
2800
+ `
2801
+ Saving again saves another procedure. ${NOT_IDEMPOTENT} the job's procedures (\`job <id>\`) before saving it again.
2802
+ `
2803
+ ).action(async (jobId, opts) => {
2804
+ if (jobId === void 0) throw new UsageError('usage: harness procedure save <job-id> --name "\u2026"');
2805
+ requireJobId(jobId);
2806
+ const name = requireProcedureName(opts.name);
2807
+ await withConfig(opts.json === true, true, (ctx) => procedureSave(ctx, jobId, name));
2808
+ });
2809
+ procedure.command("show").description("one procedure: its source job, versions (digest, capabilities, actions) and the parameters a run takes").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
2810
+ if (id === void 0) throw new UsageError("usage: harness procedure show <procedure-id>");
2811
+ requireProcedureId(id);
2812
+ await withConfig(opts.json === true, false, (ctx) => procedureShow(ctx, id));
2813
+ });
2814
+ procedure.command("run").description("run a procedure as a new job, with fresh inputs, your current rights and an approval of its own in the app").argument("[id]").option("--version <n>", "the version expected to be current (default: whichever is current)", once("--version")).option("--param <name=value>", "a parameter, as a string; name:=<json> for a JSON value (repeatable)", collect, []).option("--params-file <path>", 'the parameters as one JSON object ("-" for stdin)', once("--params-file")).option("--group <id>", "the contact group to reconcile, when the procedure needs one", once("--group")).option("--source <id>", "the uploaded file to reconcile against, when the procedure needs one", once("--source")).option("--json", "emit JSON").addHelpText(
2815
+ "after",
2816
+ `
2817
+ Nothing of the saved job carries over: no grant, approval, delegation, collaborator or read material. Approving happens in the app. ${NOT_IDEMPOTENT} \`jobs\` for the new job before running it again.
2818
+ `
2819
+ ).action(
2820
+ async (id, opts) => {
2821
+ if (id === void 0) {
2822
+ throw new UsageError("usage: harness procedure run <procedure-id> [--version N] [--param name=value]\u2026");
2823
+ }
2824
+ const request = prepareProcedureRun(id, {
2825
+ version: opts.version,
2826
+ param: opts.param,
2827
+ paramsFile: opts.paramsFile,
2828
+ group: opts.group,
2829
+ source: opts.source
2830
+ });
2831
+ await withConfig(opts.json === true, true, (ctx) => startProcedureRun(ctx, request));
2832
+ }
2833
+ );
2834
+ var commentCommand = program.command("comment").description("comment on an issue").argument("[id]").argument("[text...]").option("--body-file <path>", 'read the comment body from a file ("-" for stdin)').option("--mention <name>", "notify a person (repeatable)", collect, []).option("--json", "emit JSON").action(
2835
+ async (id, text, opts) => {
2836
+ await withConfig(opts.json === true, true, (ctx) => {
2837
+ if (id === void 0 || opts.bodyFile === void 0 && text.length === 0) {
2838
+ throw new UsageError(
2839
+ "usage: harness comment <id> <text> [--mention NAME]\u2026 | harness comment <id> --body-file <path>"
2840
+ );
2841
+ }
2842
+ requireIdentifiers2([id]);
2843
+ const body = resolvedCommentBody(text, opts.bodyFile);
2844
+ const mentions = opts.mention.filter((name) => name.trim() !== "");
2845
+ return comment(ctx, id, body, mentions);
2846
+ });
2847
+ }
2848
+ );
2849
+ commentCommand.command("edit").description("edit one of your issue comments").argument("[issue]").argument("[comment-id]").argument("[text...]").option("--body-file <path>", 'read the replacement body from a file ("-" for stdin)').option("--json", "emit JSON").action(
2850
+ async (issue, commentId, text, opts) => {
2851
+ const parentOpts = commentCommand.opts();
2852
+ const bodyFile = opts.bodyFile ?? parentOpts.bodyFile;
2853
+ await withConfig(opts.json === true || parentOpts.json === true, true, (ctx) => {
2854
+ if (issue === void 0 || commentId === void 0 || bodyFile === void 0 && text.length === 0) {
2855
+ throw new UsageError(
2856
+ "usage: harness comment edit <issue> <comment-id> <text> | harness comment edit <issue> <comment-id> --body-file <path>"
2857
+ );
2858
+ }
2859
+ requireIdentifiers2([issue]);
2860
+ requireCommentId(commentId);
2861
+ return editComment(ctx, issue, commentId, resolvedCommentBody(text, bodyFile, "edit"));
2862
+ });
2863
+ }
2864
+ );
2865
+ commentCommand.command("delete").alias("rm").description("delete one of your issue comments").argument("[issue]").argument("[comment-id]").option("--json", "emit JSON").action(async (issue, commentId, opts) => {
2866
+ const parentOpts = commentCommand.opts();
2867
+ await withConfig(opts.json === true || parentOpts.json === true, true, (ctx) => {
2868
+ if (issue === void 0 || commentId === void 0) {
2869
+ throw new UsageError("usage: harness comment delete <issue> <comment-id>");
2870
+ }
2871
+ requireIdentifiers2([issue]);
2872
+ requireCommentId(commentId);
2873
+ return deleteComment(ctx, issue, commentId);
2874
+ });
2875
+ });
2876
+ program.command("attach-image").description("upload an image; prints markdown to reference it").argument("[path]").option("--json", "emit JSON").action(async (path, opts) => {
2877
+ await withConfig(opts.json === true, true, (ctx) => {
2878
+ if (path === void 0) {
2879
+ throw new UsageError(
2880
+ "usage: harness attach-image <path> (one png, jpg, webp or gif, up to 25 MB)"
2881
+ );
2882
+ }
2883
+ return attachImage(ctx, path);
2884
+ });
2885
+ });
2886
+ var project = program.command("project").description("projects");
2887
+ project.command("list").option("--search <query>", "full-text search").option("--state <state>", "project state filter").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2888
+ await withConfig(
2889
+ opts.json === true,
2890
+ false,
2891
+ (ctx) => projectList(ctx, opts)
2892
+ );
2893
+ });
2894
+ project.command("show").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
2895
+ await withConfig(opts.json === true, false, (ctx) => {
2896
+ if (id === void 0)
2897
+ throw new UsageError("usage: harness project show <id>");
2898
+ return projectShow(ctx, id);
2899
+ });
2900
+ });
2901
+ project.command("create").option("--name <name>", "project name (required)").option("--description <text>").option("--summary <text>").option("--state <state>").option("--lead <user-id>", "lead by user id").option("--start-date <date>", "YYYY-MM-DD").option("--target-date <date>", "YYYY-MM-DD").option("--json", "emit JSON").action(async (opts) => {
2902
+ await withConfig(
2903
+ opts.json === true,
2904
+ true,
2905
+ (ctx) => projectCreate(ctx, opts)
2906
+ );
2907
+ });
2908
+ project.command("update").argument("[id]").option("--name <name>").option("--description <text>").option("--summary <text>").option("--state <state>").option("--lead <user-id>", "lead by user id").option("--start-date <date>", 'YYYY-MM-DD; "" clears').option("--target-date <date>", 'YYYY-MM-DD; "" clears').option("--json", "emit JSON").action(
2909
+ async (id, opts) => {
2910
+ await withConfig(opts.json === true, true, (ctx) => {
2911
+ if (id === void 0)
2912
+ throw new UsageError(
2913
+ "usage: harness project update <id> --name \u2026 | --state \u2026 | \u2026"
2914
+ );
2915
+ return projectUpdate(ctx, id, opts);
2916
+ });
2917
+ }
2918
+ );
2919
+ var milestone = program.command("milestone").description("project milestones");
2920
+ milestone.command("list").option("--project <id>", "project id (required)").option("--json", "emit JSON").action(async (opts) => {
2921
+ await withConfig(
2922
+ opts.json === true,
2923
+ false,
2924
+ (ctx) => milestoneList(ctx, opts.project)
2925
+ );
2926
+ });
2927
+ milestone.command("create").option("--project <id>", "project id (required)").option("--name <name>", "milestone name (required)").option("--description <text>").option("--target-date <date>", "YYYY-MM-DD").option("--json", "emit JSON").action(async (opts) => {
2928
+ await withConfig(
2929
+ opts.json === true,
2930
+ true,
2931
+ (ctx) => milestoneCreate(ctx, opts)
2932
+ );
2933
+ });
2934
+ var doc = program.command("doc").description("documents");
2935
+ doc.command("list").option("--search <query>", "full-text search").option("--limit <n>", "page size", integer2).option("--page <n>", "page number", integer2).option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
2936
+ await withConfig(
2937
+ opts.json === true,
2938
+ false,
2939
+ (ctx) => docList(ctx, opts)
2940
+ );
2941
+ });
2942
+ doc.command("show").argument("[id]").option("--json", "emit JSON").action(async (id, opts) => {
2943
+ await withConfig(opts.json === true, false, (ctx) => {
2944
+ if (id === void 0)
2945
+ throw new UsageError("usage: harness doc show <id>");
2946
+ return docShow(ctx, id);
2947
+ });
2948
+ });
2949
+ doc.command("create").option("--title <title>", "document title (required)").option("--content <text>").option("--project <id>", "attach to a project").option("--json", "emit JSON").action(async (opts) => {
2950
+ await withConfig(
2951
+ opts.json === true,
2952
+ true,
2953
+ (ctx) => docCreate(ctx, opts)
2954
+ );
2955
+ });
2956
+ doc.command("update").argument("[id]").option("--title <title>").option("--content <text>").option("--project <id>").option("--json", "emit JSON").action(
2957
+ async (id, opts) => {
2958
+ await withConfig(opts.json === true, true, (ctx) => {
2959
+ if (id === void 0)
2960
+ throw new UsageError(
2961
+ "usage: harness doc update <id> --title \u2026 | --content \u2026 | --project \u2026"
2962
+ );
2963
+ return docUpdate(ctx, id, opts);
2964
+ });
2965
+ }
2966
+ );
2967
+ var link = program.command("link").description("URLs attached to an issue (distinct from dep relations)");
2968
+ link.command("list").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
2969
+ await withConfig(opts.json === true, false, (ctx) => {
2970
+ if (issue === void 0)
2971
+ throw new UsageError("usage: harness link list <issue>");
2972
+ requireIdentifiers2([issue]);
2973
+ return linkList(ctx, issue);
2974
+ });
2975
+ });
2976
+ link.command("add").argument("[issue]").argument("[url]").option("--title <title>", "link title").option("--json", "emit JSON").action(
2977
+ async (issue, url, opts) => {
2978
+ await withConfig(opts.json === true, true, (ctx) => {
2979
+ if (issue === void 0 || url === void 0) {
2980
+ throw new UsageError(
2981
+ "usage: harness link add <issue> <url> [--title <title>]"
2982
+ );
2983
+ }
2984
+ requireIdentifiers2([issue]);
2985
+ return linkAdd(ctx, issue, url, opts.title);
2986
+ });
2987
+ }
2988
+ );
2989
+ link.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(
2990
+ async (issue, idOrUrl, opts) => {
2991
+ await withConfig(opts.json === true, true, (ctx) => {
2992
+ if (issue === void 0 || idOrUrl === void 0) {
2993
+ throw new UsageError(
2994
+ "usage: harness link update <issue> <link-id-or-url> --url <url> | --title <title>"
2995
+ );
2996
+ }
2997
+ return linkUpdate(ctx, issue, idOrUrl, opts);
2998
+ });
2999
+ }
3000
+ );
3001
+ link.command("rm").argument("[issue]").argument("[id-or-url]").option("--json", "emit JSON").action(
3002
+ async (issue, idOrUrl, opts) => {
3003
+ await withConfig(opts.json === true, true, (ctx) => {
3004
+ if (issue === void 0 || idOrUrl === void 0) {
3005
+ throw new UsageError(
3006
+ "usage: harness link rm <issue> <link-id-or-url>"
3007
+ );
3008
+ }
3009
+ requireIdentifiers2([issue]);
3010
+ return linkRm(ctx, issue, idOrUrl);
3011
+ });
3012
+ }
3013
+ );
3014
+ var label2 = program.command("label").description("labels");
3015
+ label2.command("list").option("--json", "emit JSON").action(async (opts) => {
3016
+ await withConfig(
3017
+ opts.json === true,
3018
+ false,
3019
+ (ctx) => labelList(ctx)
3020
+ );
3021
+ });
3022
+ label2.command("create").argument("[name]").option("--color <color>", "label color").option("--json", "emit JSON").action(
3023
+ async (name, opts) => {
3024
+ await withConfig(
3025
+ opts.json === true,
3026
+ true,
3027
+ (ctx) => labelCreate(ctx, name, opts.color)
3028
+ );
3029
+ }
3030
+ );
3031
+ label2.command("update").description("rename or recolor a label").argument("[id]").option("--name <name>", "new name").option("--color <color>", "swatch name (red, blue, \u2026) or a hex code").option("--json", "emit JSON").action(async (id, opts) => {
3032
+ await withConfig(opts.json === true, true, (ctx) => labelUpdate(ctx, id, opts));
3033
+ });
3034
+ label2.command("delete").description("delete a label (refused while issues carry it)").argument("[id]").option("--yes", "confirm the deletion").option("--json", "emit JSON").action(async (id, opts) => {
3035
+ await withConfig(opts.json === true, true, (ctx) => labelDelete(ctx, id, opts));
3036
+ });
3037
+ var team = program.command("team").description("teams");
3038
+ team.command("list").option("--json", "emit JSON").action(async (opts) => {
3039
+ await withConfig(
3040
+ opts.json === true,
3041
+ false,
3042
+ (ctx) => teamList(ctx)
3043
+ );
3044
+ });
3045
+ team.command("create").description("create a team, seeded with the default workflow states").option(
3046
+ "--key <key>",
3047
+ "identifier prefix for the team's issues, e.g. IOS (required)"
3048
+ ).option("--name <name>", "display name (required)").option("--json", "emit JSON").action(async (opts) => {
3049
+ await withConfig(
3050
+ opts.json === true,
3051
+ true,
3052
+ (ctx) => teamCreate(ctx, opts)
3053
+ );
3054
+ });
3055
+ team.command("rename").description("rename a team (the key is create-only)").argument("[key]").option("--name <name>", "new display name").option("--json", "emit JSON").action(async (key, opts) => {
3056
+ await withConfig(opts.json === true, true, (ctx) => teamRename(ctx, key, opts.name));
3057
+ });
3058
+ team.command("set-default").description("make this team the organization's default filing target").argument("[key]").option("--json", "emit JSON").action(async (key, opts) => {
3059
+ await withConfig(opts.json === true, true, (ctx) => teamSetDefault(ctx, key));
3060
+ });
3061
+ var state = program.command("state").description("a team's workflow states");
3062
+ state.command("list").description("list a team's workflow states").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (opts) => {
3063
+ await withConfig(opts.json === true, false, (ctx) => workflowStates(ctx, opts.team));
3064
+ });
3065
+ state.command("add").description("add a workflow state, appended after the team's existing ones").option("--team <key>", "team key (or HARNESS_TEAM)").option("--name <name>", "display name (required)").option("--type <type>", "backlog | unstarted | started | completed | canceled (required)").option("--color <color>", "swatch name").option("--json", "emit JSON").action(async (opts) => {
3066
+ await withConfig(opts.json === true, true, (ctx) => stateAdd(ctx, opts));
3067
+ });
3068
+ state.command("update").description("rename, retype or recolor a workflow state").argument("[id]").option("--team <key>", "team key (or HARNESS_TEAM)").option("--name <name>", "new name").option("--type <type>", "new type").option("--color <color>", "new swatch name").option("--json", "emit JSON").action(
3069
+ async (id, opts) => {
3070
+ await withConfig(opts.json === true, true, (ctx) => stateUpdate(ctx, id, opts));
3071
+ }
3072
+ );
3073
+ state.command("order").description("reorder a team's states \u2014 pass EVERY state id, comma separated, in the order you want").argument("[ids]").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (ids, opts) => {
3074
+ await withConfig(opts.json === true, true, (ctx) => stateOrder(ctx, ids, opts));
3075
+ });
3076
+ state.command("delete").description("delete a workflow state (refused while issues sit in it)").argument("[id]").option("--team <key>", "team key (or HARNESS_TEAM)").option("--yes", "confirm the deletion").option("--json", "emit JSON").action(async (id, opts) => {
3077
+ await withConfig(opts.json === true, true, (ctx) => stateDelete(ctx, id, opts));
3078
+ });
3079
+ var member = program.command("member").description("the organization's tracker roster");
3080
+ member.command("list").description("who can reach this organization's tracker, and at what rank").option("--json", "emit JSON").action(async (opts) => {
3081
+ await withConfig(opts.json === true, false, (ctx) => memberList(ctx));
3082
+ });
3083
+ member.command("add").description("grant a rank to an existing account, minting the membership").option("--email <email>", "address of an existing account (required)").option("--role <role>", "admin | staff | collaborator (required)").option("--json", "emit JSON").action(async (opts) => {
3084
+ await withConfig(opts.json === true, true, (ctx) => memberAdd(ctx, opts));
3085
+ });
3086
+ member.command("set-role").description("change or revoke a member's rank; none revokes").argument("[user-id]").option("--role <role>", "admin | staff | collaborator | none (required)").option("--json", "emit JSON").action(async (userId, opts) => {
3087
+ await withConfig(opts.json === true, true, (ctx) => memberSetRole(ctx, userId, opts));
3088
+ });
3089
+ program.command("states").description("a team's workflow states (alias of `state list`)").option("--team <key>", "team key (or HARNESS_TEAM)").option("--json", "emit JSON").action(async (opts) => {
3090
+ await withConfig(
3091
+ opts.json === true,
3092
+ false,
3093
+ (ctx) => workflowStates(ctx, opts.team)
3094
+ );
3095
+ });
3096
+ var user = program.command("user").description("users");
3097
+ user.command("list").option("--json", "emit JSON").action(async (opts) => {
3098
+ await withConfig(
3099
+ opts.json === true,
3100
+ false,
3101
+ (ctx) => userList(ctx)
3102
+ );
3103
+ });
3104
+ program.command("upload").description("upload an image (25 MB) or H.264 MP4 video (100 MB, optional AAC audio); prints markdown").argument("[path]").option("--json", "emit JSON").action(async (path, opts) => {
3105
+ await withConfig(opts.json === true, true, (ctx) => {
3106
+ if (path === void 0)
3107
+ throw new UsageError(
3108
+ "usage: harness upload <path> (png/jpg/webp/gif up to 25 MB; H.264 MP4 with optional AAC audio up to 100 MB)"
3109
+ );
3110
+ return upload(ctx, path);
3111
+ });
3112
+ });
3113
+ var tools = program.command("tools").description(
3114
+ "the capabilities Operator can use now in this organization (switched-off ones never appear); write and send capabilities need approval in the app"
3115
+ ).option("--domain <domain>", "only this domain (`tools domains` lists them)").option("--cursor <cursor>", "continue from a page's cursor (from the paging notice)").option("--all", "follow every page").option("--json", "emit JSON").action(async (opts) => {
3116
+ requireToolsOpts(opts);
3117
+ await withConfig(opts.json === true, false, (ctx) => toolsList(ctx, opts));
3118
+ });
3119
+ tools.command("search").description("search what Operator can use now by keyword, best match first").argument("[query...]").option("--domain <domain>", "only this domain").option("--cursor <cursor>", "continue from a page's cursor (from the paging notice)").option("--all", "follow every page").option("--json", "emit JSON").action(async (query2, opts) => {
3120
+ const merged = { ...tools.opts(), ...opts };
3121
+ if (query2.length === 0) {
3122
+ throw new UsageError("usage: harness tools search <query> [--domain <domain>] [--all]");
3123
+ }
3124
+ requireSearchQuery(query2.join(" "));
3125
+ requireToolsOpts(merged);
3126
+ await withConfig(merged.json === true, false, (ctx) => toolsSearch(ctx, query2.join(" "), merged));
3127
+ });
3128
+ tools.command("domains").description("every domain Operator can use capabilities in, with how many").option("--json", "emit JSON").action(async (opts) => {
3129
+ const parent = tools.opts();
3130
+ if (parent.domain !== void 0 || parent.cursor !== void 0 || parent.all === true) {
3131
+ throw new UsageError("usage: harness tools domains [--json] (it takes no --domain, --cursor or --all)");
3132
+ }
3133
+ await withConfig(opts.json === true || parent.json === true, false, (ctx) => toolsDomains(ctx));
3134
+ });
3135
+ program.command("tool").description("one capability Operator can use, with its whole input schema (or, for a read, its result schema)").argument("[id]").option("--schema <which>", "input (the default) or result, for a read").option("--json", "emit JSON").action(async (id, opts) => {
3136
+ if (id === void 0) throw new UsageError("usage: harness tool <capability-id> [--schema input|result]");
3137
+ requireCapabilityId(id);
3138
+ requireSchemaView(opts.schema);
3139
+ await withConfig(opts.json === true, false, (ctx) => toolShow(ctx, id, opts));
3140
+ });
3141
+ program.command("skills").description("the organization's skills assigned to Operator: identity and content checksum, never content").option("--json", "emit JSON").action(async (opts) => {
3142
+ await withConfig(opts.json === true, false, (ctx) => skillsList(ctx));
3143
+ });
3144
+ program.command("mcp").description("run the MCP server over stdio (for agents)").action(async () => {
3145
+ const { startMcpServer } = await import("./server-TRXBTJSA.js");
3146
+ await startMcpServer(invokedAs);
3147
+ });
3148
+ var keys = program.command("keys").description("the org's provider API keys, handed to tooling without printing them").enablePositionalOptions();
3149
+ keys.command("list").description("provider slugs the org has keys for and the env var each maps to \u2014 never the values").option("--json", "emit JSON").action(async (opts) => {
3150
+ await withConfig(opts.json === true, false, (ctx) => list2(ctx));
3151
+ });
3152
+ keys.command("exec").description(
3153
+ "run a command with the org's keys in its environment and exit with its exit code: harness keys exec [--override] -- <command> [args...]"
3154
+ ).argument("[command...]", "the command and its arguments").option("--override", "let fetched keys replace variables already set in this shell").option(
3155
+ "--only <slugs>",
3156
+ "pass only these provider slugs (comma-separated, repeatable); default: everything stored",
3157
+ collect,
3158
+ []
3159
+ ).option("--json", "accepted for uniformity; stdout belongs to the command, so it changes nothing").passThroughOptions().action(async (command2, opts) => {
3160
+ const only = opts.only.flatMap((value) => value.split(","));
3161
+ await withConfig(
3162
+ false,
3163
+ false,
3164
+ (ctx) => exec(ctx, { command: command2, override: opts.override === true, only })
3165
+ );
3166
+ });
3167
+ var code = program.command("code").description("sync and inspect a code-index worktree overlay");
3168
+ code.command("sync").description("sync an overlay on a server-built GitHub base (needs harness:read and harness:write)").option("--path <directory>", "Git worktree (defaults to the current directory)").option("--skipped", "include the server base\u2019s skipped-files report").option("--json", "emit JSON").action(async (opts) => {
3169
+ const target = enterWorktree(opts);
3170
+ await withConfig(opts.json === true, true, (ctx) => sync(ctx, target));
3171
+ });
3172
+ code.command("status").description("inspect this worktree view; --skipped also asks for base status (needs harness:write)").option("--path <directory>", "Git worktree (defaults to the current directory)").option("--skipped", "include the server base\u2019s skipped-files report").option("--json", "emit JSON").action(async (opts) => {
3173
+ const target = enterWorktree(opts);
3174
+ await withConfig(opts.json === true, opts.skipped === true, (ctx) => status2(ctx, target));
3175
+ });
3176
+ for (const operation of ["search", "explain", "affected", "context"]) {
3177
+ const query2 = code.command(operation).description({ search: "find indexed declarations", explain: "explain an opaque symbol ID and its callers", affected: "find impact from symbol IDs or file paths", context: "retrieve indexed source snippets within a token budget" }[operation]);
3178
+ if (operation === "search" || operation === "explain" || operation === "context") query2.argument("[value]");
3179
+ if (operation === "search") query2.option("--kind <kind>").option("--path-prefix <prefix>").option("--limit <n>", "1 to 100", integer2);
3180
+ if (operation === "context") query2.option("--symbol <id>", "opaque ID from search");
3181
+ if (operation === "context" || operation === "explain") query2.option("--budget-tokens <n>", "indexed snippet token budget", integer2);
3182
+ if (operation === "affected") query2.option("--symbol <id>", "repeat for multiple IDs", (value, all) => [...all, value], []).option("--file <path>", "repeat for multiple paths", (value, all) => [...all, value], []).option("--depth <n>", "1 to 6", integer2).option("--include-ambiguous", "follow possible bindings too").option("--include-types", "follow type dependents too");
3183
+ query2.option("--path <directory>", "Git worktree").option("--json", "emit response data, meta and guidance");
3184
+ const run3 = async (value, opts) => {
3185
+ const target = enterWorktree({ path: opts["path"] });
3186
+ const params = {
3187
+ ...operation === "search" || operation === "context" ? { q: value } : {},
3188
+ ...operation === "explain" ? { symbol: value } : operation === "context" ? { symbol: opts["symbol"] } : {},
3189
+ ...operation === "affected" ? { symbols: opts["symbol"], files: opts["file"], depth: opts["depth"], includeAmbiguous: opts["includeAmbiguous"], includeTypes: opts["includeTypes"] } : {},
3190
+ kind: opts["kind"],
3191
+ pathPrefix: opts["pathPrefix"],
3192
+ limit: opts["limit"],
3193
+ budgetTokens: opts["budgetTokens"]
3194
+ };
3195
+ validateQuery(operation, params);
3196
+ await withConfig(opts["json"] === true, false, (ctx) => query(ctx, operation, params, target));
3197
+ };
3198
+ if (operation === "affected") query2.action(async (opts) => run3(void 0, opts));
3199
+ else query2.action(run3);
3200
+ }
3201
+ var review = program.command("review").description("a pull request's enforced Review Council gate, and a retry of its latest attempt");
3202
+ review.command("status").description("the gate's enforced and advisory status, and a failed latest attempt with what comes next").argument("<pr>", "owner/repo#<pr>, the pull request URL, or <pr> with --repo or this checkout's origin").option("--repo <owner/repo>", "the repository a bare pull request number belongs to").option("--json", "emit JSON").action(async (pr, opts) => {
3203
+ const ref = parsePullRequest(pr, opts.repo);
3204
+ await withConfig(opts.json === true, false, (ctx) => status(ctx, ref));
3205
+ });
3206
+ review.command("retry").description(
3207
+ "retry the latest gate attempt of the pull request's current head (needs reviews:write); the head, check and attempt are read from the gate status"
3208
+ ).argument("<pr>", "owner/repo#<pr>, the pull request URL, or <pr> with --repo or this checkout's origin").option("--repo <owner/repo>", "the repository a bare pull request number belongs to").option("--reason <text>", "why the attempt is retried, recorded with it").option("--json", "emit JSON").action(async (pr, opts) => {
3209
+ const ref = parsePullRequest(pr, opts.repo);
3210
+ await withConfig(opts.json === true, true, (ctx) => retry(ctx, ref, opts.reason));
3211
+ });
3212
+ program.command("login").description("sign in via the browser (infra.one session)").option("--url <url>", "host to sign in to").option("--no-browser", "print the URL and prompt for a pasted token instead").option(
3213
+ "--timeout <ms>",
3214
+ "how long to wait for the browser callback",
3215
+ integer2
3216
+ ).option("--json", "emit JSON").action(
3217
+ async (opts) => {
3218
+ const repoLookup = findRepoConfig();
3219
+ for (const warning of repoLookup.warnings)
3220
+ writeStderr(warning + "\n");
3221
+ const repoUrl = repoLookup.config?.url;
3222
+ const url = opts.url ?? blankToUndefined(process.env["HARNESS_API_URL"]) ?? repoUrl ?? (repoLookup.config?.org === void 0 ? storedLoginUrl() : void 0) ?? DEFAULT_BASE_URL;
3223
+ if (url === repoUrl && opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0) {
3224
+ writeStderr(
3225
+ `harness: host ${url} comes from ${repoLookup.config?.path}
3226
+ `
3227
+ );
3228
+ }
3229
+ const repoSelected = opts.url === void 0 && blankToUndefined(process.env["HARNESS_API_URL"]) === void 0 && repoUrl !== void 0;
3230
+ if (repoSelected && !isHostTrusted(url)) {
3231
+ if (!canConfirmRepoLogin()) {
3232
+ writeStderr("harness: cannot confirm a repo-selected host without a terminal \u2014 pass --url explicitly\n");
3233
+ return exitWith(2);
3234
+ }
3235
+ if (!await confirmRepoLogin(url, "repo-config", repoLookup.config?.path)) return exitWith(2);
3236
+ }
3237
+ const options = { noBrowser: !opts.browser };
3238
+ if (repoLookup.config?.org !== void 0) options.org = repoLookup.config.org;
3239
+ if (opts.timeout !== void 0) options.timeoutMs = opts.timeout;
3240
+ await finishAuth(await login(url, options), opts.json === true);
3241
+ }
3242
+ );
3243
+ program.command("logout").description("revoke and forget the stored login").option("--json", "emit JSON").action(async (opts) => {
3244
+ const selected = resolveConfig({ ...process.env, HARNESS_API_TOKEN: void 0 });
3245
+ if (selected.kind === "error") {
3246
+ writeStderr(`harness: ${selected.message}
3247
+ `);
3248
+ return exitWith(2);
3249
+ }
3250
+ const path = selected.kind === "ok" ? selected.config.credentialsPath : selected.credentialsPath;
3251
+ await finishAuth(await logout(path === void 0 ? {} : { credentialsPath: path }), opts.json === true);
3252
+ });
3253
+ program.command("whoami").description("show the resolved host and credential source").option("--json", "emit JSON").action(async (opts) => {
3254
+ const resolved = resolveConfig();
3255
+ for (const warning of resolved.warnings)
3256
+ writeStderr(warning + "\n");
3257
+ if (resolved.kind === "error") {
3258
+ writeStderr(`harness: ${resolved.message}
3259
+ `);
3260
+ return exitWith(2);
3261
+ }
3262
+ if (resolved.kind === "needs-login") {
3263
+ writeStderr(loginHelp(resolved.baseUrl));
3264
+ if (resolved.org !== void 0) writeStderr(`Choose organization ${resolved.org} when logging in.
3265
+ `);
3266
+ return exitWith(2);
3267
+ }
3268
+ const source = blankToUndefined(process.env["HARNESS_API_TOKEN"]) !== void 0 ? "environment" : "stored login";
3269
+ let result;
3270
+ try {
3271
+ result = await whoami(resolved.config, source);
3272
+ } catch (error) {
3273
+ if (!(error instanceof TransportError)) throw error;
3274
+ writeStderr(`harness: cannot reach the API: ${clean(error.message)}
3275
+ `);
3276
+ return exitWith(3);
3277
+ }
3278
+ await finishAuth(result, opts.json === true);
3279
+ });
3280
+ function storedLoginUrl() {
3281
+ return readCredentials(defaultCredentialsPath())?.url;
3282
+ }
3283
+ function blankToUndefined(value) {
3284
+ return value === void 0 || value.trim() === "" ? void 0 : value;
3285
+ }
3286
+ async function finishAuth(result, json2) {
3287
+ if (result.ok) {
3288
+ process.stdout.write(json2 ? json(result.json) : result.human);
3289
+ return exitWith(0);
3290
+ }
3291
+ writeStderr(`harness: ${clean(result.message)}
3292
+ `);
3293
+ return exitWith(1);
3294
+ }
3295
+ var argv = process.argv.slice();
3296
+ if (argv[2] === "--version" || argv[2] === "-V") argv[2] = "version";
3297
+ if (argv[2] === UPDATE_CHECK_COMMAND) {
3298
+ await performUpdateCheck();
3299
+ await exitWith(0);
3300
+ }
3301
+ try {
3302
+ const notice = updateCheck();
3303
+ if (notice !== null) writeStderr(notice);
3304
+ } catch {
3305
+ }
3306
+ if (invokedAs === "operator") {
3307
+ const deprecation = operatorDeprecationNotice();
3308
+ if (deprecation !== null) writeStderr(deprecation);
3309
+ }
3310
+ if (invokedAs === "operator") layoutOperatorHelp(program);
3311
+ if (argv.length <= 2) {
3312
+ program.outputHelp({ error: true });
3313
+ await exitWith(2);
3314
+ }
3315
+ try {
3316
+ await program.parseAsync(argv);
3317
+ } catch (error) {
3318
+ if (error instanceof UsageError) {
3319
+ writeStderr(error.message + "\n");
3320
+ await exitWith(2);
3321
+ }
3322
+ if (error instanceof CommanderError) {
3323
+ if (error.code === "commander.helpDisplayed" || error.code === "commander.help") {
3324
+ await exitWith(error.exitCode === 0 ? 0 : 2);
3325
+ }
3326
+ await exitWith(2);
3327
+ }
3328
+ throw error;
3329
+ }