@allocator-one/harness-cli 1.17.0 → 1.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -4
- package/dist/{chunk-W7BVOQQG.js → chunk-XA5UDNJI.js} +1 -1
- package/dist/cli.js +263 -88
- package/dist/{infraone-V3IKKVBG.js → infraone-7TAHY4RN.js} +1 -1
- package/dist/infraone-SLZXBJCV.js +22133 -0
- package/dist/infraone.js +3 -3
- package/package.json +1 -1
- package/dist/infraone-A3KDADRM.js +0 -249
- /package/dist/{chunk-A4WZAC7L.js → chunk-67NX7MJ2.js} +0 -0
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
performUpdateCheck,
|
|
7
7
|
selfHeal,
|
|
8
8
|
updateCheck
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-67NX7MJ2.js";
|
|
10
10
|
import {
|
|
11
11
|
ACTIVITY_MAX_LIMIT,
|
|
12
12
|
EXIT_INTERRUPTED,
|
|
@@ -513,7 +513,7 @@ async function retry(ctx, pr, reason) {
|
|
|
513
513
|
|
|
514
514
|
// src/commands/code/index.ts
|
|
515
515
|
import { resolve as resolve2 } from "path";
|
|
516
|
-
import { statSync } from "fs";
|
|
516
|
+
import { statSync, realpathSync } from "fs";
|
|
517
517
|
|
|
518
518
|
// src/core/code/http.ts
|
|
519
519
|
import { setTimeout as delay } from "timers/promises";
|
|
@@ -749,7 +749,10 @@ var HttpCodeApi = class {
|
|
|
749
749
|
return { data: result.data, meta: result.meta };
|
|
750
750
|
}
|
|
751
751
|
async getPolicy() {
|
|
752
|
-
|
|
752
|
+
const raw = (await this.request("GET", "/policy")).data;
|
|
753
|
+
if (raw && typeof raw === "object" && "deny_glob_case" in raw && raw.deny_glob_case !== void 0 && raw.deny_glob_case !== "ascii_insensitive")
|
|
754
|
+
throw new CommandError("The server returned an invalid code upload policy; upgrade the client before code sync.");
|
|
755
|
+
return decode(policy, raw);
|
|
753
756
|
}
|
|
754
757
|
async putRepository(repoKey) {
|
|
755
758
|
return decode(
|
|
@@ -791,7 +794,7 @@ var HttpCodeApi = class {
|
|
|
791
794
|
);
|
|
792
795
|
}
|
|
793
796
|
async putBlob(hash2, bytes) {
|
|
794
|
-
HASH.
|
|
797
|
+
if (!HASH.safeParse(hash2).success) throw new CommandError("Invalid code blob SHA256; no source was uploaded.");
|
|
795
798
|
await this.request("PUT", `/blobs/${hash2}`, bytes);
|
|
796
799
|
}
|
|
797
800
|
async getSync(viewId, syncId) {
|
|
@@ -822,7 +825,7 @@ import { promisify } from "util";
|
|
|
822
825
|
import { realpath } from "fs/promises";
|
|
823
826
|
var run2 = promisify(execFile);
|
|
824
827
|
var MAX_GIT_BYTES = 16 * 1024 * 1024;
|
|
825
|
-
async function gitBytes(cwd, args, maxBuffer = MAX_GIT_BYTES) {
|
|
828
|
+
async function gitBytes(cwd, args, maxBuffer = MAX_GIT_BYTES, limitMessage = "Git returned more than 16 MiB of metadata; this exceeds the code-sync client limit.") {
|
|
826
829
|
try {
|
|
827
830
|
return (await run2(
|
|
828
831
|
"git",
|
|
@@ -839,7 +842,7 @@ async function gitBytes(cwd, args, maxBuffer = MAX_GIT_BYTES) {
|
|
|
839
842
|
const code2 = error.code;
|
|
840
843
|
if (code2 === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER" || error instanceof Error && /maxBuffer/i.test(error.message))
|
|
841
844
|
throw new LocalError(
|
|
842
|
-
|
|
845
|
+
limitMessage
|
|
843
846
|
);
|
|
844
847
|
throw new LocalError(
|
|
845
848
|
"Cannot read this Git working tree. Run code commands inside a committed repository with an origin remote."
|
|
@@ -908,10 +911,13 @@ async function identity(cwd) {
|
|
|
908
911
|
await gitBytes(root, ["rev-parse", "--verify", "HEAD^{commit}"]),
|
|
909
912
|
"Cannot resolve the repository HEAD commit."
|
|
910
913
|
);
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
914
|
+
const missingRemote = "repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection.";
|
|
915
|
+
let remote;
|
|
916
|
+
try {
|
|
917
|
+
remote = decodeGitLine(await gitBytes(root, ["remote", "get-url", "origin"]), missingRemote);
|
|
918
|
+
} catch {
|
|
919
|
+
throw new LocalError(missingRemote);
|
|
920
|
+
}
|
|
915
921
|
if (/[\x00-\x1f\x7f-\x9f]/.test(remote))
|
|
916
922
|
throw new LocalError(
|
|
917
923
|
"repository_not_connected: code indexing requires an origin on github.com shared through your organization\u2019s Harness GitHub connection."
|
|
@@ -963,6 +969,23 @@ async function changedPaths(root, commit) {
|
|
|
963
969
|
(a, b) => Buffer.compare(Buffer.from(a), Buffer.from(b))
|
|
964
970
|
);
|
|
965
971
|
}
|
|
972
|
+
function decodeGitTree(output) {
|
|
973
|
+
if (output.length && output.at(-1) !== 0)
|
|
974
|
+
throw new LocalError("Code sync received incomplete Git tree metadata.");
|
|
975
|
+
const entries = [];
|
|
976
|
+
for (let start = 0, end = output.indexOf(0); end !== -1; end = output.indexOf(0, start)) {
|
|
977
|
+
const record2 = output.subarray(start, end), tab = record2.indexOf(9);
|
|
978
|
+
if (tab < 0) throw new LocalError("Code sync received invalid Git tree metadata.");
|
|
979
|
+
const header = decodeUtf8(record2.subarray(0, tab), "Code sync received invalid Git tree metadata.");
|
|
980
|
+
const match = /^(100644|100755|120000|160000) (blob|commit) ([0-9a-f]{40}|[0-9a-f]{64})$/.exec(header);
|
|
981
|
+
if (!match || match[1] === "160000" !== (match[2] === "commit"))
|
|
982
|
+
throw new LocalError("Code sync received invalid Git tree metadata.");
|
|
983
|
+
const [path] = decodeGitPaths([Buffer.concat([record2.subarray(tab + 1), Buffer.from([0])])]);
|
|
984
|
+
entries.push({ path, regular: match[1] === "100644" || match[1] === "100755" });
|
|
985
|
+
start = end + 1;
|
|
986
|
+
}
|
|
987
|
+
return entries;
|
|
988
|
+
}
|
|
966
989
|
|
|
967
990
|
// src/core/code/scan.ts
|
|
968
991
|
import { constants as constants2 } from "fs";
|
|
@@ -979,10 +1002,13 @@ import { extname } from "path";
|
|
|
979
1002
|
var MAX_PATTERN_BYTES = 256;
|
|
980
1003
|
var MAX_PATTERN_STARS = 8;
|
|
981
1004
|
var MAX_PATTERNS = 1e3;
|
|
1005
|
+
function trimIgnoreLine(line) {
|
|
1006
|
+
return line.replace(new RegExp("^\\p{White_Space}+|\\p{White_Space}+$", "gu"), "");
|
|
1007
|
+
}
|
|
982
1008
|
function compileIgnorePatterns(lines, warnings) {
|
|
983
1009
|
const patterns = [];
|
|
984
1010
|
for (const [line, raw] of lines.entries()) {
|
|
985
|
-
const trimmed = raw
|
|
1011
|
+
const trimmed = trimIgnoreLine(raw);
|
|
986
1012
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
987
1013
|
if (trimmed.startsWith("!")) {
|
|
988
1014
|
warnings.push(
|
|
@@ -1010,17 +1036,120 @@ function compileIgnorePatterns(lines, warnings) {
|
|
|
1010
1036
|
const pattern = compilePattern(trimmed);
|
|
1011
1037
|
if (pattern) patterns.push(pattern);
|
|
1012
1038
|
}
|
|
1013
|
-
return patterns;
|
|
1039
|
+
return Object.freeze(patterns);
|
|
1014
1040
|
}
|
|
1015
1041
|
function patternLimitError() {
|
|
1016
1042
|
return new Error("Code upload policy contains an invalid path pattern.");
|
|
1017
1043
|
}
|
|
1018
|
-
function
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1044
|
+
function tokens(glob) {
|
|
1045
|
+
const result = [];
|
|
1046
|
+
for (const [i, part] of glob.split("**/").entries()) {
|
|
1047
|
+
if (i) result.push("optDirs");
|
|
1048
|
+
for (const [j, piece] of part.split("/**").entries()) {
|
|
1049
|
+
if (j) result.push(47, "starAny");
|
|
1050
|
+
for (const byte of Buffer.from(piece)) {
|
|
1051
|
+
const token = byte === 42 ? "starSeg" : byte === 63 ? "anySeg" : byte;
|
|
1052
|
+
if (token !== "starSeg" || result.at(-1) !== token) result.push(token);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return result;
|
|
1057
|
+
}
|
|
1058
|
+
function mask(indices) {
|
|
1059
|
+
const top = indices.at(-1);
|
|
1060
|
+
const bytes = Buffer.alloc((top >> 3) + 1);
|
|
1061
|
+
for (const bit of indices) bytes[bytes.length - 1 - (bit >> 3)] |= 1 << (bit & 7);
|
|
1062
|
+
return BigInt("0x" + bytes.toString("hex"));
|
|
1063
|
+
}
|
|
1064
|
+
var ByteGlob = class {
|
|
1065
|
+
constructor(specs) {
|
|
1066
|
+
this.specs = specs;
|
|
1067
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1068
|
+
const add = (key, index) => {
|
|
1069
|
+
const values = positions.get(key);
|
|
1070
|
+
if (values) values.push(index);
|
|
1071
|
+
else positions.set(key, [index]);
|
|
1072
|
+
};
|
|
1073
|
+
let base2 = 0;
|
|
1074
|
+
for (const [index, spec] of specs.entries()) {
|
|
1075
|
+
const sequence = [...spec.anchored ? [] : ["optPrefix"], ...tokens(spec.glob), ...spec.descendants ? ["optTail"] : []];
|
|
1076
|
+
add("start", base2);
|
|
1077
|
+
add("finals", base2 + sequence.length);
|
|
1078
|
+
this.finalIndices.set(base2 + sequence.length, index);
|
|
1079
|
+
for (const [offset, token] of sequence.entries()) {
|
|
1080
|
+
const bit = base2 + offset;
|
|
1081
|
+
add(token, bit);
|
|
1082
|
+
if (typeof token === "number") {
|
|
1083
|
+
if (spec.fold && (token >= 65 && token <= 90 || token >= 97 && token <= 122)) add(token ^ 32, bit);
|
|
1084
|
+
} else if (token !== "anySeg") add("eps", bit);
|
|
1085
|
+
}
|
|
1086
|
+
base2 += sequence.length + 1;
|
|
1087
|
+
}
|
|
1088
|
+
const masks = new Map([...positions].map(([key, bits]) => [key, mask(bits)]));
|
|
1089
|
+
for (const [key, value] of masks) if (typeof key === "number") this.literals.set(key, value);
|
|
1090
|
+
const get = (key) => masks.get(key) ?? 0n;
|
|
1091
|
+
this.start = get("start");
|
|
1092
|
+
this.finals = get("finals");
|
|
1093
|
+
this.anySeg = get("anySeg");
|
|
1094
|
+
this.starSeg = get("starSeg");
|
|
1095
|
+
this.starAny = get("starAny");
|
|
1096
|
+
this.eps = get("eps");
|
|
1097
|
+
this.optDirs = get("optDirs");
|
|
1098
|
+
this.optPrefix = get("optPrefix");
|
|
1099
|
+
this.optTail = get("optTail");
|
|
1100
|
+
}
|
|
1101
|
+
specs;
|
|
1102
|
+
start;
|
|
1103
|
+
finals;
|
|
1104
|
+
literals = /* @__PURE__ */ new Map();
|
|
1105
|
+
anySeg;
|
|
1106
|
+
starSeg;
|
|
1107
|
+
starAny;
|
|
1108
|
+
eps;
|
|
1109
|
+
optDirs;
|
|
1110
|
+
optPrefix;
|
|
1111
|
+
optTail;
|
|
1112
|
+
finalIndices = /* @__PURE__ */ new Map();
|
|
1113
|
+
close(states, inner) {
|
|
1114
|
+
for (; ; ) {
|
|
1115
|
+
inner |= states & (this.optDirs | this.optPrefix);
|
|
1116
|
+
const closed = states | (states & this.eps) << 1n | (inner & this.optTail) << 1n;
|
|
1117
|
+
if (closed === states) return [states, inner];
|
|
1118
|
+
states = closed;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
step(byte, states, inner) {
|
|
1122
|
+
const advance = (this.literals.get(byte) ?? 0n) | (byte === 47 ? 0n : this.anySeg);
|
|
1123
|
+
const stay = (byte === 47 ? 0n : this.starSeg) | (byte === 10 ? 0n : this.starAny);
|
|
1124
|
+
let next = (states & advance) << 1n | states & stay;
|
|
1125
|
+
let nextInner = byte === 10 ? inner & this.optPrefix : inner;
|
|
1126
|
+
if (byte === 47) {
|
|
1127
|
+
next |= (inner & (this.optDirs | this.optPrefix)) << 1n;
|
|
1128
|
+
nextInner |= states & this.optTail;
|
|
1129
|
+
}
|
|
1130
|
+
return this.close(next, nextInner);
|
|
1131
|
+
}
|
|
1132
|
+
/** Highest matching pattern preserves matchingPattern's last-match result. */
|
|
1133
|
+
matchedIndex(path) {
|
|
1134
|
+
if (!this.start) return null;
|
|
1135
|
+
let [states, inner] = this.close(this.start, 0n);
|
|
1136
|
+
const bytes = Buffer.from(path);
|
|
1137
|
+
let beforeFinalNewline = 0n;
|
|
1138
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
1139
|
+
if (i === bytes.length - 1 && bytes[i] === 10) beforeFinalNewline = states;
|
|
1140
|
+
[states, inner] = this.step(bytes[i], states, inner);
|
|
1141
|
+
if (!states && !inner && !beforeFinalNewline) return null;
|
|
1142
|
+
}
|
|
1143
|
+
const final = (states | beforeFinalNewline) & this.finals;
|
|
1144
|
+
if (!final) return null;
|
|
1145
|
+
const hex = final.toString(16);
|
|
1146
|
+
const highest = (hex.length - 1) * 4 + Math.floor(Math.log2(parseInt(hex[0], 16)));
|
|
1147
|
+
return this.finalIndices.get(highest) ?? null;
|
|
1148
|
+
}
|
|
1149
|
+
test(path) {
|
|
1150
|
+
return this.matchedIndex(path) !== null;
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1024
1153
|
function compilePattern(raw, {
|
|
1025
1154
|
caseInsensitive = false,
|
|
1026
1155
|
mode = "ignore",
|
|
@@ -1029,7 +1158,7 @@ function compilePattern(raw, {
|
|
|
1029
1158
|
maxStars = MAX_PATTERN_STARS,
|
|
1030
1159
|
rejectUnsupported = false
|
|
1031
1160
|
} = {}) {
|
|
1032
|
-
let pattern = mode === "served" ? raw : raw
|
|
1161
|
+
let pattern = mode === "served" ? raw : trimIgnoreLine(raw);
|
|
1033
1162
|
if (pattern === "") return null;
|
|
1034
1163
|
if (pattern.startsWith("#")) {
|
|
1035
1164
|
if (rejectUnsupported) throw patternLimitError();
|
|
@@ -1051,28 +1180,30 @@ function compilePattern(raw, {
|
|
|
1051
1180
|
);
|
|
1052
1181
|
}
|
|
1053
1182
|
if (rejectUnsupported && /[\[\]{}]/.test(pattern)) throw patternLimitError();
|
|
1054
|
-
const
|
|
1183
|
+
const rooted = pattern.startsWith("/");
|
|
1055
1184
|
if (mode === "ignore") pattern = pattern.replace(/^\/+|\/+$/g, "");
|
|
1056
|
-
const
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
source: mode === "served" ? raw : raw.trim(),
|
|
1185
|
+
const anchored = rooted || pattern.includes("/");
|
|
1186
|
+
return Object.freeze({
|
|
1187
|
+
source: mode === "served" ? raw : trimIgnoreLine(raw),
|
|
1060
1188
|
negated: false,
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
caseInsensitive ? "i" : ""
|
|
1064
|
-
)
|
|
1065
|
-
};
|
|
1189
|
+
matcher: new ByteGlob([{ glob: pattern, anchored, descendants: mode === "ignore", fold: caseInsensitive }])
|
|
1190
|
+
});
|
|
1066
1191
|
}
|
|
1067
1192
|
function compilePatterns(lines, options = {}) {
|
|
1068
1193
|
const maxPatterns = options.maxPatterns ?? MAX_PATTERNS;
|
|
1069
1194
|
if (lines.length > maxPatterns) throw patternLimitError();
|
|
1070
|
-
return lines.map((line) => compilePattern(line, options)).filter((pattern) => pattern !== null);
|
|
1195
|
+
return Object.freeze(lines.map((line) => compilePattern(line, options)).filter((pattern) => pattern !== null));
|
|
1071
1196
|
}
|
|
1197
|
+
var matcherCache = /* @__PURE__ */ new WeakMap();
|
|
1072
1198
|
function matchingPattern(path, patterns) {
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1199
|
+
if (!patterns.length) return null;
|
|
1200
|
+
let matcher = matcherCache.get(patterns);
|
|
1201
|
+
if (!matcher) {
|
|
1202
|
+
matcher = new ByteGlob(patterns.flatMap((pattern) => [...pattern.matcher.specs]));
|
|
1203
|
+
if (Object.isFrozen(patterns)) matcherCache.set(patterns, matcher);
|
|
1204
|
+
}
|
|
1205
|
+
const index = matcher.matchedIndex(path);
|
|
1206
|
+
return index === null ? null : patterns[index];
|
|
1076
1207
|
}
|
|
1077
1208
|
|
|
1078
1209
|
// src/core/code/policy.ts
|
|
@@ -1176,7 +1307,7 @@ function buildPolicy(served, ignore, includeFloor) {
|
|
|
1176
1307
|
const literalMarkers = contentMatchers(served.content_patterns);
|
|
1177
1308
|
return {
|
|
1178
1309
|
limit: Math.min(served.max_file_bytes, MAX_FILE_BYTES),
|
|
1179
|
-
deny: [
|
|
1310
|
+
deny: Object.freeze([
|
|
1180
1311
|
...includeFloor ? compilePatterns(DENY, {
|
|
1181
1312
|
caseInsensitive: true,
|
|
1182
1313
|
maxPatterns: Infinity,
|
|
@@ -1190,7 +1321,7 @@ function buildPolicy(served, ignore, includeFloor) {
|
|
|
1190
1321
|
maxStars: Infinity,
|
|
1191
1322
|
rejectUnsupported: true
|
|
1192
1323
|
})
|
|
1193
|
-
],
|
|
1324
|
+
]),
|
|
1194
1325
|
ignore: compileIgnorePatterns(ignore.split(/\r?\n/), warnings),
|
|
1195
1326
|
patterns: SECRETS,
|
|
1196
1327
|
literalMarkers,
|
|
@@ -1221,7 +1352,7 @@ function basePolicyFor(served, ignore) {
|
|
|
1221
1352
|
function pathReason(path, p) {
|
|
1222
1353
|
if (!path || Buffer.byteLength(path) > 1024 || /[\\\x00-\x1f\x7f-\x9f]/.test(path) || Buffer.from(path, "utf8").toString("utf8") !== path || path.split("/").some((segment) => ["", ".", ".."].includes(segment)))
|
|
1223
1354
|
return "invalid_path";
|
|
1224
|
-
if (p.deny
|
|
1355
|
+
if (matchingPattern(path, p.deny)) return "excluded_path";
|
|
1225
1356
|
if (matchingPattern(path, p.ignore)) return "ignored";
|
|
1226
1357
|
return p.extensions.has(extname(path)) ? null : "unsupported_language";
|
|
1227
1358
|
}
|
|
@@ -1265,7 +1396,7 @@ async function committedIgnore(root, commit) {
|
|
|
1265
1396
|
"The committed .harnesscodeignore must be a regular file before code sync."
|
|
1266
1397
|
);
|
|
1267
1398
|
return decodeUtf8(
|
|
1268
|
-
await gitBytes(root, ["cat-file", "blob", match[2]], 65537),
|
|
1399
|
+
await gitBytes(root, ["cat-file", "blob", match[2]], 65537, "The committed .harnesscodeignore exceeds 64 KiB; reduce it before code sync."),
|
|
1269
1400
|
"The committed .harnesscodeignore must be valid UTF-8 before code sync."
|
|
1270
1401
|
);
|
|
1271
1402
|
}
|
|
@@ -1369,7 +1500,8 @@ async function readSafe(root, path, limit) {
|
|
|
1369
1500
|
} catch (error) {
|
|
1370
1501
|
const code2 = error.code;
|
|
1371
1502
|
if (code2 === "ENOENT") return { reason: "deleted" };
|
|
1372
|
-
if (code2 === "ELOOP"
|
|
1503
|
+
if (code2 === "ELOOP") return { reason: "symlink" };
|
|
1504
|
+
if (code2 === "ENOTDIR") return { reason: "not_directory" };
|
|
1373
1505
|
if (error instanceof LocalError) throw error;
|
|
1374
1506
|
throw new LocalError(
|
|
1375
1507
|
"Cannot read an overlay file safely; check permissions and retry code sync."
|
|
@@ -1379,6 +1511,8 @@ async function readSafe(root, path, limit) {
|
|
|
1379
1511
|
async function scanOverlay(id, served) {
|
|
1380
1512
|
policyFor(served, "");
|
|
1381
1513
|
const ignore = await readSafe(id.root, ".harnesscodeignore", 65536);
|
|
1514
|
+
if ("reason" in ignore && ignore.reason === "too_large")
|
|
1515
|
+
throw new LocalError("The local .harnesscodeignore exceeds 64 KiB; reduce it before code sync.");
|
|
1382
1516
|
if ("reason" in ignore && ignore.reason !== "deleted")
|
|
1383
1517
|
throw new LocalError(
|
|
1384
1518
|
"Cannot read .harnesscodeignore safely; fix or remove it before code sync."
|
|
@@ -1395,12 +1529,9 @@ async function scanOverlay(id, served) {
|
|
|
1395
1529
|
const policy2 = policyFor(served, localIgnore);
|
|
1396
1530
|
const basePolicy = basePolicyFor(served, baseIgnore);
|
|
1397
1531
|
const paths = await changedPaths(id.root, id.commit);
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
])
|
|
1402
|
-
);
|
|
1403
|
-
assertDistinctPathIdentities([...basePaths, ...paths]);
|
|
1532
|
+
const tree = decodeGitTree(await gitBytes(id.root, ["ls-tree", "-r", "-z", id.commit]));
|
|
1533
|
+
const basePaths = new Set(tree.filter((entry) => entry.regular).map((entry) => entry.path));
|
|
1534
|
+
assertDistinctPathIdentities([...tree.map((entry) => entry.path), ...paths]);
|
|
1404
1535
|
const changes = [];
|
|
1405
1536
|
const deleted = /* @__PURE__ */ new Set();
|
|
1406
1537
|
const blobs = /* @__PURE__ */ new Map();
|
|
@@ -1417,7 +1548,10 @@ async function scanOverlay(id, served) {
|
|
|
1417
1548
|
}
|
|
1418
1549
|
};
|
|
1419
1550
|
const candidates = new Set(paths);
|
|
1420
|
-
const warnings = /* @__PURE__ */ new Set([
|
|
1551
|
+
const warnings = /* @__PURE__ */ new Set([
|
|
1552
|
+
...policy2.warnings.map((warning) => warning.startsWith("This harness-cli") ? warning : `Local .harnesscodeignore: ${warning}`),
|
|
1553
|
+
...basePolicy.warnings.map((warning) => warning.startsWith("This harness-cli") ? warning : `Committed .harnesscodeignore: ${warning}`)
|
|
1554
|
+
]);
|
|
1421
1555
|
for (const path of basePaths) {
|
|
1422
1556
|
const baseReason = pathReason(path, basePolicy);
|
|
1423
1557
|
const localReason = pathReason(path, policy2);
|
|
@@ -1494,15 +1628,25 @@ import {
|
|
|
1494
1628
|
mkdir,
|
|
1495
1629
|
writeFile,
|
|
1496
1630
|
rename,
|
|
1631
|
+
link,
|
|
1497
1632
|
unlink,
|
|
1498
1633
|
rmdir
|
|
1499
1634
|
} from "fs/promises";
|
|
1500
1635
|
import { join as join2, resolve, isAbsolute } from "path";
|
|
1501
1636
|
import { randomUUID } from "crypto";
|
|
1502
|
-
function
|
|
1637
|
+
function canonicalServer(server) {
|
|
1503
1638
|
const url = new URL(server);
|
|
1504
|
-
|
|
1505
|
-
|
|
1639
|
+
return url.origin + url.pathname.replace(/\/+$/, "");
|
|
1640
|
+
}
|
|
1641
|
+
function validPending(pending) {
|
|
1642
|
+
if (pending === void 0) return true;
|
|
1643
|
+
if (!pending || typeof pending !== "object" || typeof pending.digest !== "string" || !/^[a-f0-9]{64}$/.test(pending.digest)) return false;
|
|
1644
|
+
const request = pending.request;
|
|
1645
|
+
return !!request && typeof request === "object" && request.mode === "full" && typeof request.base_commit === "string" && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(request.base_commit) && typeof request.policy_digest === "string" && request.policy_digest.length > 0 && request.policy_digest.length <= 256 && typeof request.client_sync_id === "string" && request.client_sync_id.length > 0 && Buffer.byteLength(request.client_sync_id) <= 255 && Array.isArray(request.changes) && request.changes.length <= 2e4 && request.changes.every((change) => change && typeof change === "object" && typeof change.path === "string" && (change.op === "delete" || change.op === "put" && typeof change.sha256 === "string" && /^[a-f0-9]{64}$/.test(change.sha256) && Number.isSafeInteger(change.size) && change.size >= 0 && ["100644", "100755"].includes(change.mode))) && (pending.syncId === void 0 || typeof pending.syncId === "string") && (pending.activationAttemptedAt === void 0 || Number.isSafeInteger(pending.activationAttemptedAt) && pending.activationAttemptedAt >= 0);
|
|
1646
|
+
}
|
|
1647
|
+
function statePath(server, org, root, repoKey, directory) {
|
|
1648
|
+
const canonical = canonicalServer(server);
|
|
1649
|
+
const base2 = directory ?? (process.env["HARNESS_CODE_STATE_DIR"] || void 0) ?? join2(resolveCacheHome(), "harness", "code");
|
|
1506
1650
|
if (!isAbsolute(base2))
|
|
1507
1651
|
throw new LocalError("HARNESS_CODE_STATE_DIR must be an absolute path.");
|
|
1508
1652
|
return join2(
|
|
@@ -1516,9 +1660,9 @@ async function readState(path, server, org, root, repoKey) {
|
|
|
1516
1660
|
const s = JSON.parse(await readFile(path, "utf8"));
|
|
1517
1661
|
if (typeof s !== "object" || s === null) throw new Error();
|
|
1518
1662
|
const state2 = s;
|
|
1519
|
-
if (state2.org.toLowerCase() !== org.toLowerCase() || state2.root !== root || state2.repoKey !== repoKey ||
|
|
1663
|
+
if (state2.org.toLowerCase() !== org.toLowerCase() || state2.root !== root || state2.repoKey !== repoKey || canonicalServer(state2.server) !== canonicalServer(server) || !validPending(state2.pending) || typeof state2.viewKey !== "string" || !/^[\da-f-]{36}$/.test(state2.viewKey))
|
|
1520
1664
|
throw new Error();
|
|
1521
|
-
return { ...state2, org: state2.org.toLowerCase() };
|
|
1665
|
+
return { ...state2, server: canonicalServer(state2.server), org: state2.org.toLowerCase() };
|
|
1522
1666
|
} catch (error) {
|
|
1523
1667
|
if (error.code === "ENOENT") return null;
|
|
1524
1668
|
throw new LocalError(
|
|
@@ -1529,26 +1673,31 @@ async function readState(path, server, org, root, repoKey) {
|
|
|
1529
1673
|
async function initialState(path, server, org, root, repoKey) {
|
|
1530
1674
|
const previous = await readState(path, server, org, root, repoKey);
|
|
1531
1675
|
if (previous) return previous;
|
|
1532
|
-
const state2 = { server, org: org.toLowerCase(), root, repoKey, viewKey: randomUUID() };
|
|
1676
|
+
const state2 = { server: canonicalServer(server), org: org.toLowerCase(), root, repoKey, viewKey: randomUUID() };
|
|
1533
1677
|
await mkdir(join2(path, ".."), { recursive: true, mode: 448 });
|
|
1678
|
+
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
1534
1679
|
try {
|
|
1535
|
-
await writeFile(
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
})
|
|
1539
|
-
|
|
1540
|
-
} catch (error) {
|
|
1541
|
-
if (error.code === "EEXIST") {
|
|
1680
|
+
await writeFile(tmp, JSON.stringify(state2) + "\n", { flag: "wx", mode: 384 });
|
|
1681
|
+
try {
|
|
1682
|
+
await link(tmp, path);
|
|
1683
|
+
} catch (error) {
|
|
1684
|
+
if (error.code !== "EEXIST") throw error;
|
|
1542
1685
|
const raced = await readState(path, server, org, root, repoKey);
|
|
1543
|
-
if (raced)
|
|
1686
|
+
if (!raced) throw new Error("initial state disappeared");
|
|
1687
|
+
return raced;
|
|
1544
1688
|
}
|
|
1689
|
+
return state2;
|
|
1690
|
+
} catch {
|
|
1545
1691
|
throw new LocalError("Cannot save isolated code view state.");
|
|
1692
|
+
} finally {
|
|
1693
|
+
await unlink(tmp).catch(() => {
|
|
1694
|
+
});
|
|
1546
1695
|
}
|
|
1547
1696
|
}
|
|
1548
1697
|
async function saveState(path, state2) {
|
|
1549
1698
|
const tmp = `${path}.${randomUUID()}.tmp`;
|
|
1550
1699
|
try {
|
|
1551
|
-
await writeFile(tmp, JSON.stringify({ ...state2, org: state2.org.toLowerCase() }) + "\n", {
|
|
1700
|
+
await writeFile(tmp, JSON.stringify({ ...state2, server: canonicalServer(state2.server), org: state2.org.toLowerCase() }) + "\n", {
|
|
1552
1701
|
flag: "wx",
|
|
1553
1702
|
mode: 384
|
|
1554
1703
|
});
|
|
@@ -1732,11 +1881,32 @@ async function syncCode(api, options) {
|
|
|
1732
1881
|
if (error instanceof HttpError && error.status === 404) {
|
|
1733
1882
|
if (withinActivationRetention(state2.pending?.activationAttemptedAt))
|
|
1734
1883
|
return forgetReplaced();
|
|
1884
|
+
if (state2.pending?.activationAttemptedAt === void 0)
|
|
1885
|
+
throw new CommandError("The sync disappeared without a recorded activation attempt. Check code status before retrying code sync.");
|
|
1735
1886
|
throw new CommandError("The previous activation can no longer be verified after retention. Check code status before syncing again.");
|
|
1736
1887
|
}
|
|
1737
1888
|
throw error;
|
|
1738
1889
|
}
|
|
1739
1890
|
};
|
|
1891
|
+
const reconcileFailure = async (syncId, original) => {
|
|
1892
|
+
try {
|
|
1893
|
+
const recovered = await receipt(syncId);
|
|
1894
|
+
if (!recovered && original instanceof HttpError && original.status >= 400 && original.status < 500 && original.status !== 429 && state2.pending) {
|
|
1895
|
+
delete state2.pending.activationAttemptedAt;
|
|
1896
|
+
await saveState(path, state2);
|
|
1897
|
+
}
|
|
1898
|
+
return recovered;
|
|
1899
|
+
} catch (secondary) {
|
|
1900
|
+
if (secondary instanceof ReplacedSyncError) throw secondary;
|
|
1901
|
+
const primary = secondary instanceof CommandError ? secondary : original;
|
|
1902
|
+
const cause = primary === secondary ? original : secondary;
|
|
1903
|
+
try {
|
|
1904
|
+
if (primary instanceof Error && primary.cause === void 0 && cause !== primary) primary.cause = cause;
|
|
1905
|
+
} catch {
|
|
1906
|
+
}
|
|
1907
|
+
throw primary;
|
|
1908
|
+
}
|
|
1909
|
+
};
|
|
1740
1910
|
let current = state2.pending;
|
|
1741
1911
|
const matchesCurrent = current && current.digest === overlay.digest && current.request?.mode === "full" && current.request.base_commit === id.commit && current.request.policy_digest === policy2.policy_digest && typeof current.request.client_sync_id === "string" && JSON.stringify(current.request.changes) === JSON.stringify(overlay.changes);
|
|
1742
1912
|
if (current?.syncId && current.activationAttemptedAt !== void 0 && !matchesCurrent) {
|
|
@@ -1749,7 +1919,7 @@ async function syncCode(api, options) {
|
|
|
1749
1919
|
try {
|
|
1750
1920
|
recovered = await api.activate(view2.view_id, originalSyncId);
|
|
1751
1921
|
} catch (error) {
|
|
1752
|
-
recovered = await
|
|
1922
|
+
recovered = await reconcileFailure(originalSyncId, error);
|
|
1753
1923
|
if (!recovered) throw error;
|
|
1754
1924
|
}
|
|
1755
1925
|
}
|
|
@@ -1849,9 +2019,8 @@ async function syncCode(api, options) {
|
|
|
1849
2019
|
throw new CommandError("The activation retry exceeded its retention window. Check code status before syncing again.");
|
|
1850
2020
|
return forgetReplaced();
|
|
1851
2021
|
}
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
const recovered = await receipt(opened2.sync_id);
|
|
2022
|
+
if (activating) {
|
|
2023
|
+
const recovered = await reconcileFailure(opened2.sync_id, error);
|
|
1855
2024
|
if (recovered) return finish(opened2.sync_id, recovered, missing.length);
|
|
1856
2025
|
}
|
|
1857
2026
|
throw error;
|
|
@@ -1898,7 +2067,7 @@ async function statusCode(api, options) {
|
|
|
1898
2067
|
"The server resolved a different repository; its base status cannot be trusted."
|
|
1899
2068
|
);
|
|
1900
2069
|
const repositoryId = repository?.repository_id ?? state2?.repositoryId;
|
|
1901
|
-
|
|
2070
|
+
const withoutView = async () => {
|
|
1902
2071
|
const base3 = options.skipped && repositoryId ? await api.getBase(repositoryId, id.commit, true) : void 0;
|
|
1903
2072
|
return {
|
|
1904
2073
|
state: base3?.state === "failed" ? "failed" : base3?.state === "fetching" ? "fetching" : "not_synced",
|
|
@@ -1907,8 +2076,15 @@ async function statusCode(api, options) {
|
|
|
1907
2076
|
base_commit: id.commit,
|
|
1908
2077
|
...base3 ? { base: base3 } : {}
|
|
1909
2078
|
};
|
|
2079
|
+
};
|
|
2080
|
+
if (!state2?.viewId || repository && repositoryId !== state2.repositoryId) return withoutView();
|
|
2081
|
+
let view2;
|
|
2082
|
+
try {
|
|
2083
|
+
view2 = await api.getView(state2.viewId);
|
|
2084
|
+
} catch (error) {
|
|
2085
|
+
if (error instanceof HttpError && error.status === 404) return withoutView();
|
|
2086
|
+
throw error;
|
|
1910
2087
|
}
|
|
1911
|
-
const view2 = await api.getView(state2.viewId);
|
|
1912
2088
|
if (view2.repository_id !== state2.repositoryId || view2.view_id !== state2.viewId || view2.view_key !== state2.viewKey)
|
|
1913
2089
|
throw new CommandError(
|
|
1914
2090
|
"The server view does not match this repository/worktree; its status cannot be trusted."
|
|
@@ -1917,7 +2093,7 @@ async function statusCode(api, options) {
|
|
|
1917
2093
|
const current = view2.active?.base_commit === id.commit && view2.active.overlay_digest === overlay.digest && state2.activated?.baseCommit === view2.active.base_commit && state2.activated.overlayDigest === view2.active.overlay_digest && state2.activated.pendingVersion === view2.active.version && state2.activated.policyDigest === policy2.policy_digest;
|
|
1918
2094
|
const base2 = options.skipped && state2.repositoryId ? await api.getBase(state2.repositoryId, id.commit, true) : void 0;
|
|
1919
2095
|
return {
|
|
1920
|
-
state: base2?.state === "failed" || view2.pending?.state === "failed" ? "failed" : current ? "current" : view2.pending ? "pending" : "stale",
|
|
2096
|
+
state: base2?.state === "failed" || view2.pending?.state === "failed" ? "failed" : current ? "current" : view2.pending ? "pending" : view2.active ? "stale" : "not_synced",
|
|
1921
2097
|
org_id: policy2.org_id,
|
|
1922
2098
|
repo_key: id.repoKey,
|
|
1923
2099
|
base_commit: id.commit,
|
|
@@ -2074,27 +2250,26 @@ async function queryCode(api, operation, params, options) {
|
|
|
2074
2250
|
|
|
2075
2251
|
// src/commands/code/index.ts
|
|
2076
2252
|
function enterWorktree(options) {
|
|
2077
|
-
const
|
|
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);
|
|
2253
|
+
const refused = () => options.path === void 0 ? new LocalError("Cannot enter the code worktree safely; check the current directory.") : new UsageError("Cannot enter the code worktree safely; check --path.");
|
|
2086
2254
|
try {
|
|
2087
|
-
const
|
|
2088
|
-
|
|
2089
|
-
|
|
2255
|
+
const cwd = process.cwd();
|
|
2256
|
+
const before = statSync(".");
|
|
2257
|
+
const declared = statSync(cwd);
|
|
2258
|
+
if (before.dev !== declared.dev || before.ino !== declared.ino) throw refused();
|
|
2259
|
+
const supplied = options.path ?? cwd;
|
|
2260
|
+
if (/[\x00-\x1f\x7f-\x9f]/.test(supplied) || Buffer.from(supplied, "utf8").toString("utf8") !== supplied || options.path !== void 0 && supplied.includes("\uFFFD")) throw refused();
|
|
2261
|
+
const path = resolve2(supplied), target = statSync(path);
|
|
2262
|
+
if (options.path === void 0 && (before.dev !== target.dev || before.ino !== target.ino)) throw refused();
|
|
2090
2263
|
process.chdir(path);
|
|
2091
2264
|
const entered = statSync(".");
|
|
2092
|
-
if (entered.dev !== target.dev || entered.ino !== target.ino)
|
|
2093
|
-
|
|
2265
|
+
if (entered.dev !== target.dev || entered.ino !== target.ino) throw refused();
|
|
2266
|
+
const canonical = realpathSync(".");
|
|
2267
|
+
const pinned = statSync(canonical);
|
|
2268
|
+
if (pinned.dev !== entered.dev || pinned.ino !== entered.ino) throw refused();
|
|
2269
|
+
return { ...options, path: canonical };
|
|
2094
2270
|
} catch {
|
|
2095
|
-
throw
|
|
2271
|
+
throw refused();
|
|
2096
2272
|
}
|
|
2097
|
-
return { ...options, path };
|
|
2098
2273
|
}
|
|
2099
2274
|
async function sync(ctx, options = {}) {
|
|
2100
2275
|
try {
|
|
@@ -2964,8 +3139,8 @@ doc.command("update").argument("[id]").option("--title <title>").option("--conte
|
|
|
2964
3139
|
});
|
|
2965
3140
|
}
|
|
2966
3141
|
);
|
|
2967
|
-
var
|
|
2968
|
-
|
|
3142
|
+
var link2 = program.command("link").description("URLs attached to an issue (distinct from dep relations)");
|
|
3143
|
+
link2.command("list").argument("[issue]").option("--json", "emit JSON").action(async (issue, opts) => {
|
|
2969
3144
|
await withConfig(opts.json === true, false, (ctx) => {
|
|
2970
3145
|
if (issue === void 0)
|
|
2971
3146
|
throw new UsageError("usage: harness link list <issue>");
|
|
@@ -2973,7 +3148,7 @@ link.command("list").argument("[issue]").option("--json", "emit JSON").action(as
|
|
|
2973
3148
|
return linkList(ctx, issue);
|
|
2974
3149
|
});
|
|
2975
3150
|
});
|
|
2976
|
-
|
|
3151
|
+
link2.command("add").argument("[issue]").argument("[url]").option("--title <title>", "link title").option("--json", "emit JSON").action(
|
|
2977
3152
|
async (issue, url, opts) => {
|
|
2978
3153
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
2979
3154
|
if (issue === void 0 || url === void 0) {
|
|
@@ -2986,7 +3161,7 @@ link.command("add").argument("[issue]").argument("[url]").option("--title <title
|
|
|
2986
3161
|
});
|
|
2987
3162
|
}
|
|
2988
3163
|
);
|
|
2989
|
-
|
|
3164
|
+
link2.command("update").argument("[issue]").argument("[id-or-url]").option("--url <url>", "replacement URL").option("--title <title>", "replacement title; pass an empty value to clear").option("--json", "emit JSON").action(
|
|
2990
3165
|
async (issue, idOrUrl, opts) => {
|
|
2991
3166
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
2992
3167
|
if (issue === void 0 || idOrUrl === void 0) {
|
|
@@ -2998,7 +3173,7 @@ link.command("update").argument("[issue]").argument("[id-or-url]").option("--url
|
|
|
2998
3173
|
});
|
|
2999
3174
|
}
|
|
3000
3175
|
);
|
|
3001
|
-
|
|
3176
|
+
link2.command("rm").argument("[issue]").argument("[id-or-url]").option("--json", "emit JSON").action(
|
|
3002
3177
|
async (issue, idOrUrl, opts) => {
|
|
3003
3178
|
await withConfig(opts.json === true, true, (ctx) => {
|
|
3004
3179
|
if (issue === void 0 || idOrUrl === void 0) {
|