border-customs 0.4.2 → 0.5.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/index.js CHANGED
@@ -13029,7 +13029,7 @@ var require_micromatch = __commonJS({
13029
13029
 
13030
13030
  // src/index.ts
13031
13031
  import { realpathSync } from "node:fs";
13032
- import { fileURLToPath as fileURLToPath5, pathToFileURL } from "node:url";
13032
+ import { fileURLToPath as fileURLToPath6, pathToFileURL } from "node:url";
13033
13033
 
13034
13034
  // src/cli.ts
13035
13035
  import { parseArgs } from "node:util";
@@ -37940,12 +37940,12 @@ function packOnce(o) {
37940
37940
  throw new EngineRunError(`npm pack reported '${report.filename}' but no tarball landed in ${o.distDir}`, null);
37941
37941
  }
37942
37942
  const data = readFileSync3(tarballAbs);
37943
- const sha2563 = createHash3("sha256").update(data).digest("hex");
37943
+ const sha2564 = createHash3("sha256").update(data).digest("hex");
37944
37944
  const sha1 = createHash3("sha1").update(data).digest("hex");
37945
37945
  if (sha1 !== report.shasum || data.byteLength !== report.size) {
37946
37946
  throw new EngineRunError(`npm pack report disagrees with the tarball bytes for ${report.filename} \u2014 corrupted or swapped artifact, failing closed`, null);
37947
37947
  }
37948
- return { filename: report.filename, sha256: sha2563, bytes: data.byteLength, tarballAbs };
37948
+ return { filename: report.filename, sha256: sha2564, bytes: data.byteLength, tarballAbs };
37949
37949
  }
37950
37950
  function publintFinding(o, tarballAbs, identity) {
37951
37951
  const candidates = o.publintBinPath !== void 0 ? [o.publintBinPath] : [
@@ -38950,7 +38950,7 @@ async function runNpmArtifactStage(o) {
38950
38950
  const stateDir = join10(repoDir, BORDER_STATE_DIR);
38951
38951
  const distDir = join10(stateDir, "dist");
38952
38952
  mkdirSync3(distDir, { recursive: true });
38953
- const { filename, sha256: sha2563, bytes, tarballAbs } = packOnce({
38953
+ const { filename, sha256: sha2564, bytes, tarballAbs } = packOnce({
38954
38954
  repoDir,
38955
38955
  distDir,
38956
38956
  ...o.env !== void 0 ? { env: o.env } : {},
@@ -38958,7 +38958,7 @@ async function runNpmArtifactStage(o) {
38958
38958
  ...o.npmTimeoutMs !== void 0 ? { npmTimeoutMs: o.npmTimeoutMs } : {}
38959
38959
  });
38960
38960
  const findings = [];
38961
- const extractDir = join10(stateDir, "tmp", sha2563.slice(0, 8));
38961
+ const extractDir = join10(stateDir, "tmp", sha2564.slice(0, 8));
38962
38962
  rmSync3(extractDir, { recursive: true, force: true });
38963
38963
  mkdirSync3(extractDir, { recursive: true });
38964
38964
  try {
@@ -39063,7 +39063,7 @@ async function runNpmArtifactStage(o) {
39063
39063
  if (publint !== null) findings.push(publint);
39064
39064
  return {
39065
39065
  findings,
39066
- artifact: { file: `${BORDER_STATE_DIR}/dist/${filename}`, sha256: sha2563, bytes }
39066
+ artifact: { file: `${BORDER_STATE_DIR}/dist/${filename}`, sha256: sha2564, bytes }
39067
39067
  };
39068
39068
  }
39069
39069
 
@@ -40145,9 +40145,9 @@ async function runRubygemsArtifactStage(o) {
40145
40145
  throw new EngineRunError(`gem build produced no ${gemName} in ${relative3(repoDir, distDir)} \u2014 failing closed (was the gemspec's literal name/version re-read?)`, null);
40146
40146
  }
40147
40147
  const bytes = readFileSync7(dist);
40148
- const sha2563 = createHash6("sha256").update(bytes).digest("hex");
40148
+ const sha2564 = createHash6("sha256").update(bytes).digest("hex");
40149
40149
  const findings = [];
40150
- const extractDir = join12(stateDir, "tmp", sha2563.slice(0, 8));
40150
+ const extractDir = join12(stateDir, "tmp", sha2564.slice(0, 8));
40151
40151
  rmSync4(extractDir, { recursive: true, force: true });
40152
40152
  mkdirSync4(extractDir, { recursive: true });
40153
40153
  const dataDir = `${extractDir}-data`;
@@ -40225,7 +40225,7 @@ async function runRubygemsArtifactStage(o) {
40225
40225
  }
40226
40226
  return {
40227
40227
  findings,
40228
- artifact: { file: `${BORDER_STATE_DIR}/dist/${gemName}`, sha256: sha2563, bytes: bytes.length }
40228
+ artifact: { file: `${BORDER_STATE_DIR}/dist/${gemName}`, sha256: sha2564, bytes: bytes.length }
40229
40229
  };
40230
40230
  }
40231
40231
 
@@ -40891,17 +40891,17 @@ function requirePassedRecord(kind, i) {
40891
40891
  function rehashRecordedArtifacts(kind, extensions, record2, repoDir) {
40892
40892
  const conventions = extensions;
40893
40893
  const entries = (record2.artifacts ?? []).filter((a) => conventions.some((ext) => a.file.endsWith(ext)));
40894
- const fail3 = (detail) => ({ ok: false, message: `artifact changed since check \u2014 ${detail}` });
40894
+ const fail4 = (detail) => ({ ok: false, message: `artifact changed since check \u2014 ${detail}` });
40895
40895
  if (entries.length === 0) {
40896
- return fail3(`the PASSED check recorded no ${kind} artifacts (${conventions.join("/")}) to re-verify`);
40896
+ return fail4(`the PASSED check recorded no ${kind} artifacts (${conventions.join("/")}) to re-verify`);
40897
40897
  }
40898
40898
  const files = [];
40899
40899
  for (const a of entries) {
40900
40900
  const rel = join17(DIST_DIR, basename4(a.file));
40901
40901
  const abs = join17(repoDir, rel);
40902
- if (!existsSync11(abs)) return fail3(`${rel} is missing from the working tree`);
40902
+ if (!existsSync11(abs)) return fail4(`${rel} is missing from the working tree`);
40903
40903
  const digest2 = createHash10("sha256").update(readFileSync10(abs)).digest("hex");
40904
- if (digest2 !== a.sha256) return fail3(`${rel} no longer hashes to the digest recorded by the check`);
40904
+ if (digest2 !== a.sha256) return fail4(`${rel} no longer hashes to the digest recorded by the check`);
40905
40905
  files.push(rel);
40906
40906
  }
40907
40907
  return { ok: true, files: files.sort() };
@@ -41360,7 +41360,7 @@ function npmExposureCoords(cfg, read) {
41360
41360
  return { name, version: version2 };
41361
41361
  }
41362
41362
  function classifyNpmVersionView(o) {
41363
- const fail3 = (why) => {
41363
+ const fail4 = (why) => {
41364
41364
  throw new EngineRunError(
41365
41365
  `npm registry probe inconclusive (${why}); refusing to assume the version is absent \u2014 registry unreachable or returned unexpected output, push blocked until the probe succeeds`,
41366
41366
  o.status
@@ -41368,19 +41368,19 @@ function classifyNpmVersionView(o) {
41368
41368
  };
41369
41369
  if (o.status === 0) {
41370
41370
  const text = o.stdout.trim();
41371
- if (text.length === 0) return fail3("exit 0 with empty stdout");
41371
+ if (text.length === 0) return fail4("exit 0 with empty stdout");
41372
41372
  let body;
41373
41373
  try {
41374
41374
  body = JSON.parse(text);
41375
41375
  } catch {
41376
- return fail3("exit 0 with malformed JSON body");
41376
+ return fail4("exit 0 with malformed JSON body");
41377
41377
  }
41378
- if (typeof body !== "object" || body === null) return fail3("exit 0 with non-object JSON body");
41378
+ if (typeof body !== "object" || body === null) return fail4("exit 0 with non-object JSON body");
41379
41379
  return "present";
41380
41380
  }
41381
41381
  if (/code E404\b/.test(o.stderr) || /\b404 Not Found\b/.test(o.stderr)) return "absent";
41382
41382
  const m = /code (E[A-Z0-9_]+|ERR_[A-Z0-9_]+)/.exec(o.stderr);
41383
- return fail3(m === null ? `exit ${String(o.status)} without a 404 marker` : `npm error ${m[1]}`);
41383
+ return fail4(m === null ? `exit ${String(o.status)} without a 404 marker` : `npm error ${m[1]}`);
41384
41384
  }
41385
41385
  function npmNameClaimed(o) {
41386
41386
  if (o.status === 0) return true;
@@ -41787,9 +41787,9 @@ async function runCargoArtifactStage(o) {
41787
41787
  const dist = join18(distDir, crateName);
41788
41788
  renameSync(packed, dist);
41789
41789
  const bytes = readFileSync11(dist);
41790
- const sha2563 = createHash11("sha256").update(bytes).digest("hex");
41790
+ const sha2564 = createHash11("sha256").update(bytes).digest("hex");
41791
41791
  const findings = [];
41792
- const extractDir = join18(stateDir, "tmp", sha2563.slice(0, 8));
41792
+ const extractDir = join18(stateDir, "tmp", sha2564.slice(0, 8));
41793
41793
  rmSync8(extractDir, { recursive: true, force: true });
41794
41794
  mkdirSync8(extractDir, { recursive: true });
41795
41795
  try {
@@ -41842,7 +41842,7 @@ async function runCargoArtifactStage(o) {
41842
41842
  }
41843
41843
  return {
41844
41844
  findings,
41845
- artifact: { file: `${BORDER_STATE_DIR}/dist/${crateName}`, sha256: sha2563, bytes: bytes.length }
41845
+ artifact: { file: `${BORDER_STATE_DIR}/dist/${crateName}`, sha256: sha2564, bytes: bytes.length }
41846
41846
  };
41847
41847
  } finally {
41848
41848
  rmSync8(targetDir, { recursive: true, force: true });
@@ -43700,6 +43700,193 @@ async function runLlmRequest(ctx) {
43700
43700
  return runLlmRequestCore(ctx);
43701
43701
  }
43702
43702
 
43703
+ // src/commands/opencode.ts
43704
+ import { createHash as createHash15 } from "node:crypto";
43705
+ import { existsSync as existsSync17, lstatSync, mkdirSync as mkdirSync11, readFileSync as readFileSync16, rmSync as rmSync12, writeFileSync as writeFileSync7 } from "node:fs";
43706
+ import os3 from "node:os";
43707
+ import { dirname as dirname12, join as join26 } from "node:path";
43708
+ import { fileURLToPath as fileURLToPath5 } from "node:url";
43709
+ var PLUGIN_MARKER = "// border-opencode-plugin v";
43710
+ var COMMAND_MARKER = "<!-- border-opencode-command -->";
43711
+ function opencodeConfigRoot(env2) {
43712
+ const xdg = env2["XDG_CONFIG_HOME"] ?? "";
43713
+ if (xdg.length > 0) return xdg;
43714
+ const home = env2["HOME"] ?? "";
43715
+ return join26(home.length > 0 ? home : os3.homedir(), ".config");
43716
+ }
43717
+ function resolvePluginAsset(moduleUrl, fileName) {
43718
+ const here = dirname12(fileURLToPath5(moduleUrl));
43719
+ const candidates = [
43720
+ join26(here, "..", "plugin", fileName),
43721
+ // dist mode (bundled): <root>/plugin/<f>
43722
+ join26(here, "..", "..", "plugin", fileName)
43723
+ // src mode: <root>/plugin/<f>
43724
+ ];
43725
+ return candidates.find((p) => existsSync17(p)) ?? candidates[1];
43726
+ }
43727
+ function targetsFor(env2) {
43728
+ const root = join26(opencodeConfigRoot(env2), "opencode");
43729
+ return [
43730
+ { fileName: "border.ts", destination: join26(root, "plugins", "border.ts"), marker: PLUGIN_MARKER },
43731
+ { fileName: "border-command.md", destination: join26(root, "commands", "border.md"), marker: COMMAND_MARKER }
43732
+ ];
43733
+ }
43734
+ function sha2563(bytes) {
43735
+ return createHash15("sha256").update(bytes).digest("hex");
43736
+ }
43737
+ function firstLine(bytes) {
43738
+ const text = bytes.toString("utf8");
43739
+ const newline = text.indexOf("\n");
43740
+ return newline < 0 ? text : text.slice(0, newline);
43741
+ }
43742
+ function carriesMarker(bytes, marker) {
43743
+ return firstLine(bytes).startsWith(marker);
43744
+ }
43745
+ function fail(ctx, message) {
43746
+ ctx.stderr(`border: opencode: ${message}`);
43747
+ return EXIT_ERROR;
43748
+ }
43749
+ function readPackagedAsset(ctx, fileName) {
43750
+ const source = resolvePluginAsset(import.meta.url, fileName);
43751
+ try {
43752
+ return readFileSync16(source);
43753
+ } catch {
43754
+ ctx.stderr(`border: opencode: packaged asset is missing (${source})`);
43755
+ ctx.stderr("border: opencode: the border install looks broken \u2014 `npm i -g border-customs` or a fresh checkout should carry plugin/.");
43756
+ return null;
43757
+ }
43758
+ }
43759
+ function readTarget(destination) {
43760
+ try {
43761
+ return readFileSync16(destination);
43762
+ } catch {
43763
+ return null;
43764
+ }
43765
+ }
43766
+ function requirePlainDestination(ctx, destination) {
43767
+ let shape;
43768
+ try {
43769
+ shape = lstatSync(destination);
43770
+ } catch {
43771
+ return null;
43772
+ }
43773
+ if (!shape.isFile() || shape.isSymbolicLink()) {
43774
+ return fail(
43775
+ ctx,
43776
+ `refusing to touch ${destination} \u2014 the path exists but is not a plain regular file (directories and symlinks are never followed, entered, or destroyed)`
43777
+ );
43778
+ }
43779
+ return null;
43780
+ }
43781
+ function classify(target, current, packaged) {
43782
+ if (current === null) return "absent";
43783
+ if (!carriesMarker(current, target.marker)) return "foreign";
43784
+ return current.equals(packaged) ? "up-to-date" : "outdated";
43785
+ }
43786
+ function requireNoForeign(ctx, targets) {
43787
+ for (const target of targets) {
43788
+ const shape = requirePlainDestination(ctx, target.destination);
43789
+ if (shape !== null) return shape;
43790
+ const current = readTarget(target.destination);
43791
+ if (current !== null && !carriesMarker(current, target.marker)) {
43792
+ return fail(
43793
+ ctx,
43794
+ `refusing to touch ${target.destination} \u2014 the file exists but is not ours (its first line lacks the marker '${target.marker}'); move it aside manually, nothing was written anywhere`
43795
+ );
43796
+ }
43797
+ }
43798
+ return null;
43799
+ }
43800
+ function prepareTargets(ctx, targets) {
43801
+ const prepared = [];
43802
+ for (const target of targets) {
43803
+ const packaged = readPackagedAsset(ctx, target.fileName);
43804
+ if (packaged === null) return null;
43805
+ prepared.push({ target, packaged });
43806
+ }
43807
+ return prepared;
43808
+ }
43809
+ function opencodeInstall(ctx) {
43810
+ const targets = targetsFor(ctx.env);
43811
+ const prepared = prepareTargets(ctx, targets);
43812
+ if (prepared === null) return EXIT_ERROR;
43813
+ const blocked = requireNoForeign(ctx, targets);
43814
+ if (blocked !== null) return blocked;
43815
+ for (const { target, packaged } of prepared) {
43816
+ const current = readTarget(target.destination);
43817
+ const state = classify(target, current, packaged);
43818
+ if (state === "foreign") {
43819
+ return fail(
43820
+ ctx,
43821
+ `refusing to touch ${target.destination} \u2014 the file exists but is not ours (its first line lacks the marker '${target.marker}'); move it aside manually, nothing was written anywhere`
43822
+ );
43823
+ }
43824
+ if (state === "up-to-date") {
43825
+ ctx.stdout(`${target.destination}: up to date`);
43826
+ continue;
43827
+ }
43828
+ mkdirSync11(dirname12(target.destination), { recursive: true });
43829
+ writeFileSync7(target.destination, packaged);
43830
+ ctx.stdout(`${target.destination}: ${state === "outdated" ? "updated" : "installed"}`);
43831
+ }
43832
+ ctx.stdout("note: restart opencode to load the plugin \u2014 tools and commands are scanned at startup.");
43833
+ return EXIT_PASS;
43834
+ }
43835
+ function opencodeStatus(ctx) {
43836
+ const targets = targetsFor(ctx.env);
43837
+ const prepared = prepareTargets(ctx, targets);
43838
+ if (prepared === null) return EXIT_ERROR;
43839
+ for (const { target, packaged } of prepared) {
43840
+ const current = readTarget(target.destination);
43841
+ const state = classify(target, current, packaged);
43842
+ const installedSha = current === null ? "-" : sha2563(current);
43843
+ ctx.stdout(`${target.destination}: ${state} installed=${installedSha} packaged=${sha2563(packaged)}`);
43844
+ }
43845
+ return EXIT_PASS;
43846
+ }
43847
+ function opencodeUninstall(ctx) {
43848
+ const targets = targetsFor(ctx.env);
43849
+ const blocked = requireNoForeign(ctx, targets);
43850
+ if (blocked !== null) return blocked;
43851
+ for (const target of targets) {
43852
+ const current = readTarget(target.destination);
43853
+ if (current === null) {
43854
+ ctx.stdout(`${target.destination}: absent (nothing to remove)`);
43855
+ continue;
43856
+ }
43857
+ if (!carriesMarker(current, target.marker)) {
43858
+ return fail(
43859
+ ctx,
43860
+ `refusing to remove ${target.destination} \u2014 the file is not ours (its first line lacks the marker '${target.marker}'); nothing was removed anywhere`
43861
+ );
43862
+ }
43863
+ rmSync12(target.destination);
43864
+ ctx.stdout(`${target.destination}: removed`);
43865
+ }
43866
+ ctx.stdout("note: restart opencode for the tool and command to disappear.");
43867
+ return EXIT_PASS;
43868
+ }
43869
+ function runOpencode(ctx) {
43870
+ const [sub, ...rest] = ctx.positionals;
43871
+ if (rest.length > 0) {
43872
+ throw new UnknownArgError(
43873
+ `opencode ${sub ?? "<none>"}: unexpected argument '${rest[0] ?? ""}' \u2014 usage: border opencode install | status | uninstall`
43874
+ );
43875
+ }
43876
+ switch (sub) {
43877
+ case "install":
43878
+ return opencodeInstall(ctx);
43879
+ case "status":
43880
+ return opencodeStatus(ctx);
43881
+ case "uninstall":
43882
+ return opencodeUninstall(ctx);
43883
+ default:
43884
+ throw new UnknownArgError(
43885
+ `opencode: unknown subcommand '${sub ?? "<none>"}' \u2014 usage: border opencode install | status | uninstall`
43886
+ );
43887
+ }
43888
+ }
43889
+
43703
43890
  // src/push/git.ts
43704
43891
  import { spawnSync as spawnSync15 } from "node:child_process";
43705
43892
  var DIVERGED_MESSAGE = "history diverged; border never force-pushes; resolve manually";
@@ -43965,17 +44152,17 @@ async function executeYesPush(ctx, loaded) {
43965
44152
  }
43966
44153
 
43967
44154
  // src/roundtrip/orchestrate.ts
43968
- import { mkdtempSync as mkdtempSync5, rmSync as rmSync12, writeFileSync as writeFileSync7 } from "node:fs";
44155
+ import { mkdtempSync as mkdtempSync5, rmSync as rmSync13, writeFileSync as writeFileSync8 } from "node:fs";
43969
44156
  import { randomBytes } from "node:crypto";
43970
44157
  import { tmpdir as tmpdir3 } from "node:os";
43971
- import { join as join26 } from "node:path";
44158
+ import { join as join27 } from "node:path";
43972
44159
 
43973
44160
  // src/scan/fetch.ts
43974
- import { createHash as createHash15 } from "node:crypto";
44161
+ import { createHash as createHash16 } from "node:crypto";
43975
44162
  import { basename as basename5 } from "node:path";
43976
44163
  var SCAN_MAX_ARTIFACT_BYTES = 200 * 1024 * 1024;
43977
44164
  var SCAN_REQUEST_TIMEOUT_MS = 6e4;
43978
- var SCAN_USER_AGENT = "border-customs/0.4.2 (+https://github.com/sumteclab/border; border scan)";
44165
+ var SCAN_USER_AGENT = "border-customs/0.5.0 (+https://github.com/sumteclab/border; border scan)";
43979
44166
  var CRATES_DOWNLOAD_HOST = "https://static.crates.io";
43980
44167
  var defaultScanFetcher = async (url2, init) => {
43981
44168
  if (!/^https?:\/\//.test(url2)) throw new EngineRunError(`border scan refuses non-http(s) URL: ${url2}`, null);
@@ -44000,19 +44187,19 @@ var defaultScanFetcher = async (url2, init) => {
44000
44187
  }
44001
44188
  };
44002
44189
  };
44003
- var fail = (why) => {
44190
+ var fail2 = (why) => {
44004
44191
  throw new EngineRunError(`border scan could not fetch the artifact (${why}) \u2014 refusing to scan a partial or absent download (fail-closed)`, null);
44005
44192
  };
44006
44193
  async function readCapped(res, url2, cap = SCAN_MAX_ARTIFACT_BYTES) {
44007
- if (res.status !== 200) fail(`HTTP ${String(res.status)} for ${url2}`);
44194
+ if (res.status !== 200) fail2(`HTTP ${String(res.status)} for ${url2}`);
44008
44195
  if (res.contentLength !== null && res.contentLength > cap) {
44009
- fail(`${url2} announces ${String(res.contentLength)} bytes, over the 200 MiB scan size cap`);
44196
+ fail2(`${url2} announces ${String(res.contentLength)} bytes, over the 200 MiB scan size cap`);
44010
44197
  }
44011
44198
  const parts = [];
44012
44199
  let total = 0;
44013
44200
  for await (const chunk of res.chunks()) {
44014
44201
  total += chunk.byteLength;
44015
- if (total > cap) fail(`${url2} streams past the 200 MiB scan size cap after ${String(total)} bytes`);
44202
+ if (total > cap) fail2(`${url2} streams past the 200 MiB scan size cap after ${String(total)} bytes`);
44016
44203
  parts.push(Buffer.from(chunk));
44017
44204
  }
44018
44205
  return Buffer.concat(parts);
@@ -44026,7 +44213,7 @@ async function fetchJson(fetcher, url2, timeoutMs) {
44026
44213
  try {
44027
44214
  return JSON.parse(body.toString("utf8"));
44028
44215
  } catch {
44029
- fail(`metadata response from ${url2} is not valid JSON`);
44216
+ fail2(`metadata response from ${url2} is not valid JSON`);
44030
44217
  }
44031
44218
  }
44032
44219
  var isRecord2 = (v) => typeof v === "object" && v !== null;
@@ -44062,11 +44249,11 @@ async function fetchArtifact(spec, o) {
44062
44249
  const meta3 = await fetchJson(o.fetcher, npmMetadataUrl(spec.name, spec.version), timeoutMs);
44063
44250
  const dist = isRecord2(meta3) && isRecord2(meta3["dist"]) ? meta3["dist"] : {};
44064
44251
  const tarball = dist["tarball"];
44065
- if (typeof tarball !== "string" || tarball.length === 0) fail(`npm metadata for ${label} has no dist.tarball`);
44066
- if (!/^https?:\/\//.test(tarball)) fail(`npm dist.tarball for ${label} is not an http(s) URL`);
44252
+ if (typeof tarball !== "string" || tarball.length === 0) fail2(`npm metadata for ${label} has no dist.tarball`);
44253
+ if (!/^https?:\/\//.test(tarball)) fail2(`npm dist.tarball for ${label} is not an http(s) URL`);
44067
44254
  const bytes = await fetchBytes(o.fetcher, tarball, timeoutMs);
44068
44255
  const filename = safeArtifactName(tarball, `${spec.name.replace("@", "").replace("/", "-")}-${spec.version}.tgz`);
44069
- if (!filename.endsWith(".tgz") && !filename.endsWith(".tar.gz")) fail(`npm tarball ${filename} is not a .tgz/.tar.gz`);
44256
+ if (!filename.endsWith(".tgz") && !filename.endsWith(".tar.gz")) fail2(`npm tarball ${filename} is not a .tgz/.tar.gz`);
44070
44257
  return { bytes, filename };
44071
44258
  }
44072
44259
  case "pypi": {
@@ -44076,11 +44263,11 @@ async function fetchArtifact(spec, o) {
44076
44263
  const sdists = files.filter((f) => f["packagetype"] === "sdist");
44077
44264
  const sdist = sdists.find((f) => typeof f["filename"] === "string" && f["filename"].endsWith(".tar.gz"));
44078
44265
  if (sdist === void 0) {
44079
- fail(`no .tar.gz sdist published for ${label} \u2014 wheels are pre-built artifacts and out of scan scope (setup code lives in the sdist)`);
44266
+ fail2(`no .tar.gz sdist published for ${label} \u2014 wheels are pre-built artifacts and out of scan scope (setup code lives in the sdist)`);
44080
44267
  }
44081
44268
  const url2 = sdist["url"];
44082
44269
  const name = sdist["filename"];
44083
- if (typeof url2 !== "string" || typeof name !== "string") fail(`PyPI sdist entry for ${label} is malformed`);
44270
+ if (typeof url2 !== "string" || typeof name !== "string") fail2(`PyPI sdist entry for ${label} is malformed`);
44084
44271
  const bytes = await fetchBytes(o.fetcher, url2, timeoutMs);
44085
44272
  return { bytes, filename: safeArtifactName(url2, name) };
44086
44273
  }
@@ -44101,7 +44288,7 @@ async function fetchArtifact(spec, o) {
44101
44288
  }
44102
44289
  }
44103
44290
  function sha256Hex2(data) {
44104
- return createHash15("sha256").update(data).digest("hex");
44291
+ return createHash16("sha256").update(data).digest("hex");
44105
44292
  }
44106
44293
 
44107
44294
  // src/scan/spec.ts
@@ -44144,8 +44331,8 @@ function parseScanSpec(raw) {
44144
44331
  }
44145
44332
 
44146
44333
  // src/roundtrip/localArtifact.ts
44147
- import { createReadStream, existsSync as existsSync17, statSync as statSync2 } from "node:fs";
44148
- import { createHash as createHash16 } from "node:crypto";
44334
+ import { createReadStream, existsSync as existsSync18, statSync as statSync2 } from "node:fs";
44335
+ import { createHash as createHash17 } from "node:crypto";
44149
44336
  import { basename as basename6, resolve as resolve15 } from "node:path";
44150
44337
  var EXT_TABLE = /* @__PURE__ */ new Map([
44151
44338
  [".whl", "pypi"],
@@ -44193,7 +44380,7 @@ var GRAMMAR_HINT = {
44193
44380
  };
44194
44381
  function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
44195
44382
  const abs = resolve15(cwd, raw);
44196
- if (existsSync17(abs)) {
44383
+ if (existsSync18(abs)) {
44197
44384
  const st = statSync2(abs);
44198
44385
  if (st.isDirectory()) {
44199
44386
  bad3(`'${raw}' resolves to a directory (${abs}) \u2014 local mode needs a single artifact FILE, not a directory`);
@@ -44228,7 +44415,7 @@ function classifyLocalInput(raw, cwd, parsesAsRegistrySpec) {
44228
44415
  return null;
44229
44416
  }
44230
44417
  async function readLocalArtifact(absPath, cap = SCAN_MAX_ARTIFACT_BYTES) {
44231
- const hash2 = createHash16("sha256");
44418
+ const hash2 = createHash17("sha256");
44232
44419
  const parts = [];
44233
44420
  let total = 0;
44234
44421
  for await (const chunk of createReadStream(absPath)) {
@@ -44454,7 +44641,7 @@ var CLAIM_SCANNER_PY = [
44454
44641
  ].join("\n");
44455
44642
 
44456
44643
  // src/roundtrip/manifest.ts
44457
- import { createHash as createHash17 } from "node:crypto";
44644
+ import { createHash as createHash18 } from "node:crypto";
44458
44645
  var RT_ENGINE = "roundtrip";
44459
44646
  var RT_RESIDUE_RULE = "roundtrip-residue-file";
44460
44647
  var RT_MODIFY_RULE = "roundtrip-modified-file";
@@ -44505,7 +44692,7 @@ var PERSISTENCE_PATTERNS = [
44505
44692
  function isPersistenceSurface(path4) {
44506
44693
  return PERSISTENCE_PATTERNS.some((re) => re.test(path4));
44507
44694
  }
44508
- var digestOf = (what) => createHash17("sha256").update(what).digest("hex");
44695
+ var digestOf = (what) => createHash18("sha256").update(what).digest("hex");
44509
44696
  function classifyResidue(label, diff, attribution = null) {
44510
44697
  const findings = [];
44511
44698
  for (const e of diff.added) {
@@ -44707,13 +44894,13 @@ async function runRoundtripCore(ctx, deps = {}) {
44707
44894
  ctx.stderr("roundtrip: docker unavailable \u2014 cannot verify");
44708
44895
  return EXIT_ERROR;
44709
44896
  }
44710
- const baseDir = mkdtempSync5(join26(tmpdir3(), "border-roundtrip-"));
44897
+ const baseDir = mkdtempSync5(join27(tmpdir3(), "border-roundtrip-"));
44711
44898
  try {
44712
44899
  const localRead = local !== null ? await readLocalArtifact(local.absPath) : null;
44713
44900
  const bytes = localRead?.bytes ?? (await fetchArtifact(spec, { fetcher: deps.fetcher ?? defaultScanFetcher })).bytes;
44714
44901
  const artifactSha256 = localRead !== null ? localRead.sha256 : sha256Hex2(bytes);
44715
- const hostPath = join26(baseDir, "artifact");
44716
- writeFileSync7(hostPath, bytes);
44902
+ const hostPath = join27(baseDir, "artifact");
44903
+ writeFileSync8(hostPath, bytes);
44717
44904
  const container = containerNameFor(label);
44718
44905
  const closure = spec.ecosystem === "pypi" ? resolvePipClosure(exec, profile, hostPath, label, spec) : null;
44719
44906
  const { findings, installDelta, baselineDigest } = runPipeline2(exec, profile, spec, label, hostPath, container, closure, (msg) => ctx.stderr(msg));
@@ -44757,7 +44944,7 @@ async function runRoundtripCore(ctx, deps = {}) {
44757
44944
  }
44758
44945
  return exitCodeFromVerdict(verdict);
44759
44946
  } finally {
44760
- rmSync12(baseDir, { recursive: true, force: true });
44947
+ rmSync13(baseDir, { recursive: true, force: true });
44761
44948
  }
44762
44949
  }
44763
44950
  function parsesRegistrySpec(raw) {
@@ -44815,7 +45002,7 @@ var runRoundtrip = (ctx) => runRoundtripCore(ctx);
44815
45002
  // src/scan/scan.ts
44816
45003
  import { mkdtempSync as mkdtempSync6 } from "node:fs";
44817
45004
  import { tmpdir as tmpdir4 } from "node:os";
44818
- import { join as join28 } from "node:path";
45005
+ import { join as join29 } from "node:path";
44819
45006
 
44820
45007
  // src/scan/config.ts
44821
45008
  function buildScanConfigYaml(eco) {
@@ -44842,10 +45029,10 @@ function synthesizeScanConfig(eco) {
44842
45029
  }
44843
45030
 
44844
45031
  // src/scan/materialize.ts
44845
- import { existsSync as existsSync18, mkdirSync as mkdirSync11, readdirSync as readdirSync10, readFileSync as readFileSync16, rmSync as rmSync13, writeFileSync as writeFileSync8 } from "node:fs";
44846
- import { join as join27 } from "node:path";
45032
+ import { existsSync as existsSync19, mkdirSync as mkdirSync12, readdirSync as readdirSync10, readFileSync as readFileSync17, rmSync as rmSync14, writeFileSync as writeFileSync9 } from "node:fs";
45033
+ import { join as join28 } from "node:path";
44847
45034
  var CRATES_ENVELOPE_FILES = ["Cargo.toml.orig", ".cargo-ok", ".cargo_vcs_info.json"];
44848
- var fail2 = (why) => {
45035
+ var fail3 = (why) => {
44849
45036
  throw new Error(`border scan cannot materialize the artifact (${why}) \u2014 refusing to stage an unusable tree (fail-closed)`);
44850
45037
  };
44851
45038
  var oneLine = (err) => (err instanceof Error ? err.message : String(err)).replace(/\s+/g, " ").trim();
@@ -44853,20 +45040,20 @@ function extractFlat(archive, destDir) {
44853
45040
  try {
44854
45041
  extractArchive(archive, destDir);
44855
45042
  } catch (err) {
44856
- fail2(`extraction of ${archive} failed: ${oneLine(err)}`);
45043
+ fail3(`extraction of ${archive} failed: ${oneLine(err)}`);
44857
45044
  }
44858
45045
  }
44859
45046
  function unwrapSingleDir(dir) {
44860
45047
  const entries = readdirSync10(dir, { withFileTypes: true });
44861
45048
  const dirs = entries.filter((e) => e.isDirectory());
44862
45049
  if (entries.length === 1 && dirs.length === 1 && dirs[0] !== void 0) {
44863
- return join27(dir, dirs[0].name);
45050
+ return join28(dir, dirs[0].name);
44864
45051
  }
44865
45052
  return dir;
44866
45053
  }
44867
45054
  function requireManifest(repoDir, ecosystem, candidates) {
44868
- if (!candidates.some((rel) => existsSync18(join27(repoDir, rel)))) {
44869
- fail2(`${ecosystem} package tree has none of [${candidates.join(", ")}] at its root`);
45055
+ if (!candidates.some((rel) => existsSync19(join28(repoDir, rel)))) {
45056
+ fail3(`${ecosystem} package tree has none of [${candidates.join(", ")}] at its root`);
44870
45057
  }
44871
45058
  }
44872
45059
  function materializePackage(o) {
@@ -44875,19 +45062,19 @@ function materializePackage(o) {
44875
45062
  `);
44876
45063
  });
44877
45064
  const safe = safeArtifactName(o.filename, `${o.ecosystem}-artifact`);
44878
- const archive = join27(o.baseDir, safe);
44879
- writeFileSync8(archive, o.bytes);
44880
- const extractDir = join27(o.baseDir, "extract");
44881
- mkdirSync11(extractDir, { recursive: true });
45065
+ const archive = join28(o.baseDir, safe);
45066
+ writeFileSync9(archive, o.bytes);
45067
+ const extractDir = join28(o.baseDir, "extract");
45068
+ mkdirSync12(extractDir, { recursive: true });
44882
45069
  extractFlat(archive, extractDir);
44883
45070
  let repoDir = unwrapSingleDir(extractDir);
44884
45071
  if (o.ecosystem === "rubygems") {
44885
45072
  const outer = repoDir;
44886
45073
  const dataCandidates = findNativeMissArchives(outer).filter((f) => f.endsWith("data.tar.gz"));
44887
- const inner = dataCandidates[0] ?? join27(outer, "data.tar.gz");
44888
- if (!existsSync18(inner)) fail2(".gem archive contains no data.tar.gz");
44889
- const gemTree = join27(o.baseDir, "gem-tree");
44890
- mkdirSync11(gemTree, { recursive: true });
45074
+ const inner = dataCandidates[0] ?? join28(outer, "data.tar.gz");
45075
+ if (!existsSync19(inner)) fail3(".gem archive contains no data.tar.gz");
45076
+ const gemTree = join28(o.baseDir, "gem-tree");
45077
+ mkdirSync12(gemTree, { recursive: true });
44891
45078
  extractFlat(inner, gemTree);
44892
45079
  repoDir = unwrapSingleDir(gemTree);
44893
45080
  } else if (o.ecosystem === "npm") {
@@ -44897,10 +45084,10 @@ function materializePackage(o) {
44897
45084
  } else {
44898
45085
  requireManifest(repoDir, o.ecosystem, ["Cargo.toml"]);
44899
45086
  for (const file2 of CRATES_ENVELOPE_FILES) {
44900
- const path4 = join27(repoDir, file2);
44901
- if (!existsSync18(path4)) continue;
44902
- const original = readFileSync16(path4);
44903
- rmSync13(path4, { force: true });
45087
+ const path4 = join28(repoDir, file2);
45088
+ if (!existsSync19(path4)) continue;
45089
+ const original = readFileSync17(path4);
45090
+ rmSync14(path4, { force: true });
44904
45091
  note(`border scan: crates envelope normalized: ${file2} sha256=${sha256Hex2(original)}`);
44905
45092
  }
44906
45093
  }
@@ -44927,7 +45114,7 @@ async function runScanCore(ctx, deps = {}) {
44927
45114
  }
44928
45115
  const cfg = synthesizeScanConfig(spec.ecosystem);
44929
45116
  const fetcher = deps.fetcher ?? defaultScanFetcher;
44930
- const baseDir = mkdtempSync6(join28(tmpdir4(), "border-scan-"));
45117
+ const baseDir = mkdtempSync6(join29(tmpdir4(), "border-scan-"));
44931
45118
  try {
44932
45119
  const { bytes, filename } = await fetchArtifact(spec, { fetcher });
44933
45120
  const env2 = { ...ctx.env };
@@ -45052,11 +45239,12 @@ var handlers = {
45052
45239
  "llm-request": runLlmRequest,
45053
45240
  "llm-ingest": runLlmIngest,
45054
45241
  scan: runScan2,
45055
- roundtrip: runRoundtrip
45242
+ roundtrip: runRoundtrip,
45243
+ opencode: runOpencode
45056
45244
  };
45057
45245
 
45058
45246
  // src/cli/types.ts
45059
- var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan", "roundtrip"];
45247
+ var SUBCOMMANDS = ["check", "push", "status", "llm-request", "llm-ingest", "scan", "roundtrip", "opencode"];
45060
45248
 
45061
45249
  // src/cli.ts
45062
45250
  var VALID_TARGETS = targetIds();
@@ -45074,6 +45262,7 @@ function usage() {
45074
45262
  " llm-ingest record provenance captured by the harness",
45075
45263
  " scan inspect a third-party package ([ecosystem:]name@version) for residue",
45076
45264
  " roundtrip install+uninstall a package (registry spec or local artifact file) in a throwaway container, report uninstall residue",
45265
+ " opencode install|status|uninstall the official opencode plugin adapter (files under $XDG_CONFIG_HOME/opencode)",
45077
45266
  "",
45078
45267
  "flags:",
45079
45268
  " --config <path> config file (default: ./border.yaml, then git-remote fallback)",
@@ -45088,7 +45277,7 @@ function usage() {
45088
45277
  "",
45089
45278
  "exit codes: 0 pass (MEDIUM/INFO/LOW allowed) | 1 gate-blocked (CRITICAL/HIGH) or partial push | 2 config/tool error",
45090
45279
  "",
45091
- "subcommands: check, push, status, llm-request, llm-ingest, scan, roundtrip"
45280
+ "subcommands: check, push, status, llm-request, llm-ingest, scan, roundtrip, opencode"
45092
45281
  ].join("\n");
45093
45282
  }
45094
45283
  function isSubcommand(value) {
@@ -45212,7 +45401,7 @@ function isMainModule({ argv1, moduleUrl, realpath }) {
45212
45401
  return p;
45213
45402
  }
45214
45403
  };
45215
- return pathToFileURL(canonical(argv1)).href === pathToFileURL(canonical(fileURLToPath5(moduleUrl))).href;
45404
+ return pathToFileURL(canonical(argv1)).href === pathToFileURL(canonical(fileURLToPath6(moduleUrl))).href;
45216
45405
  }
45217
45406
  if (isMainModule({ argv1: process.argv[1], moduleUrl: import.meta.url, realpath: realpathSync.native })) {
45218
45407
  const outcome = run(process.argv.slice(2));
package/package.json CHANGED
@@ -1,14 +1,20 @@
1
1
  {
2
2
  "name": "border-customs",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "//": "publint is a runtime dep (exact pin): the npm-stage engine is invoked via bin resolution against the installed layout, so consumers get the publint-fail leg out of the box; the dev suite resolves the same node_modules/.bin/publint — no devDependencies entry is needed for it to stay green.",
5
5
  "description": "Fail-closed git push gate: secret scanning + supply-chain checks before any remote leaves the machine",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "border": "dist/index.js"
9
9
  },
10
+ "exports": {
11
+ ".": "./dist/index.js",
12
+ "./server": "./plugin/border.ts",
13
+ "./package.json": "./package.json"
14
+ },
10
15
  "files": [
11
- "dist"
16
+ "dist",
17
+ "plugin"
12
18
  ],
13
19
  "engines": {
14
20
  "node": ">=22.0.0"
@@ -26,6 +32,7 @@
26
32
  "test": "node --import ./tools/register-ts.mjs --test --test-reporter=tap test/*.test.ts"
27
33
  },
28
34
  "dependencies": {
35
+ "@opencode-ai/plugin": "^1.17.4",
29
36
  "@secretlint/core": "13.0.5",
30
37
  "@secretlint/secretlint-rule-no-dotenv": "13.0.5",
31
38
  "@secretlint/secretlint-rule-no-homedir": "13.0.5",