@basou/cli 0.26.0 → 0.28.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
@@ -3365,6 +3365,7 @@ async function assertWorkspaceInitialized7(basouRoot) {
3365
3365
  // src/commands/project.ts
3366
3366
  import {
3367
3367
  closeSync,
3368
+ copyFileSync,
3368
3369
  existsSync,
3369
3370
  constants as fsConstants,
3370
3371
  ftruncateSync,
@@ -3385,10 +3386,12 @@ import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute3,
3385
3386
  import {
3386
3387
  appendBasouGitignore as appendBasouGitignore2,
3387
3388
  basouPaths as basouPaths10,
3389
+ classifyRetrofit,
3388
3390
  createManifest as createManifest2,
3389
3391
  ensureBasouDirectory as ensureBasouDirectory2,
3390
3392
  GENERATED_END,
3391
3393
  GENERATED_START,
3394
+ instructionMode,
3392
3395
  isGitNotFound,
3393
3396
  parseMarkers,
3394
3397
  pathBasename,
@@ -3505,6 +3508,17 @@ function registerProjectCommand(program2) {
3505
3508
  ).option("--apply", "Run every step in apply mode (default: dry-run preview)").option("-v, --verbose", "Show error causes").action(async (opts) => {
3506
3509
  await runProjectDerive(opts);
3507
3510
  });
3511
+ project.command("retrofit").argument(
3512
+ "<repo>",
3513
+ "The declared roster repo whose hand-authored AGENTS.md to relocate (e.g. ../foo)"
3514
+ ).description(
3515
+ "Fold an existing repo's hand-authored AGENTS.md into the project topology: move the repo's regular-file `AGENTS.md` to the anchor canonical (`agents/<repo>/AGENTS.md`) and replace it with a symlink, so the prose lives at the single source of truth. Dry-run by default; pass --apply to relocate. The onboarding counterpart to `new` for a repo that already carries its own AGENTS.md \u2014 run it before `basou project derive`, which then adds the preset block, the CLAUDE.md / Copilot spokes, and the .gitignore. Non-destructive: it refuses when the destination canonical already exists (it never clobbers it), and skips a repo whose AGENTS.md is already a symlink or absent. The anchor (`.`) is refused"
3516
+ ).option(
3517
+ "--apply",
3518
+ "Relocate the AGENTS.md to the canonical and recreate the symlink (default: dry-run preview)"
3519
+ ).option("--json", "Output the result as JSON").option("-v, --verbose", "Show error causes").action(async (repo, opts) => {
3520
+ await runProjectRetrofit(repo, opts);
3521
+ });
3508
3522
  }
3509
3523
  async function runProjectCheck(options, ctx = {}) {
3510
3524
  try {
@@ -3767,7 +3781,8 @@ async function isTrackedByGit(repoRoot, relPath) {
3767
3781
  async function gatherRepoWiring(repositoryRoot, entry) {
3768
3782
  const base = {
3769
3783
  path: entry.path,
3770
- ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
3784
+ ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
3785
+ ...instructionMode(entry) === "self" ? { self: true } : {}
3771
3786
  };
3772
3787
  let real;
3773
3788
  try {
@@ -3850,6 +3865,13 @@ function renderProjectWiring(result) {
3850
3865
  for (const p of result.unknown) lines.push(`- ${p}`);
3851
3866
  lines.push("");
3852
3867
  }
3868
+ if (result.self.length > 0) {
3869
+ lines.push(
3870
+ `## instructions: self (${result.self.length}) \u2014 committed instruction files are intentional (no leak risk)`
3871
+ );
3872
+ for (const p of result.self) lines.push(`- ${p}`);
3873
+ lines.push("");
3874
+ }
3853
3875
  if (result.incomplete.length > 0) {
3854
3876
  lines.push(
3855
3877
  `## Missing instruction files (${result.incomplete.length}) \u2014 to be filled by a later generation slice`
@@ -3878,7 +3900,8 @@ async function runProjectGitignore(options, ctx = {}) {
3878
3900
  function gatherRepoGitignore(repositoryRoot, entry) {
3879
3901
  const base = {
3880
3902
  path: entry.path,
3881
- ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {}
3903
+ ...entry.visibility !== void 0 ? { visibility: entry.visibility } : {},
3904
+ ...instructionMode(entry) === "self" ? { self: true } : {}
3882
3905
  };
3883
3906
  let real;
3884
3907
  try {
@@ -3973,6 +3996,13 @@ function renderProjectGitignore(result) {
3973
3996
  for (const p of result.unknown) lines.push(`- ${p}`);
3974
3997
  lines.push("");
3975
3998
  }
3999
+ if (result.self.length > 0) {
4000
+ lines.push(
4001
+ `## instructions: self (${result.self.length}) \u2014 skipped by design; their committed instruction files are shared, never gitignored`
4002
+ );
4003
+ for (const p of result.self) lines.push(`- ${p}`);
4004
+ lines.push("");
4005
+ }
3976
4006
  if (result.unreachable.length > 0) {
3977
4007
  lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
3978
4008
  for (const p of result.unreachable) lines.push(`- ${p}`);
@@ -3994,12 +4024,13 @@ async function runProjectSymlinks(options, ctx = {}) {
3994
4024
  process.exitCode = 1;
3995
4025
  }
3996
4026
  }
3997
- function expectedSymlinkTargets(repoDirReal, canonicalFile) {
3998
- return [
3999
- { name: "AGENTS.md", target: relative2(repoDirReal, canonicalFile) },
4027
+ function expectedSymlinkTargets(repoDirReal, canonicalFile, mode = "hub") {
4028
+ const spokes = [
4000
4029
  { name: "CLAUDE.md", target: CANONICAL_FILE },
4001
4030
  { name: ".github/copilot-instructions.md", target: `../${CANONICAL_FILE}` }
4002
4031
  ];
4032
+ if (mode === "self") return spokes;
4033
+ return [{ name: "AGENTS.md", target: relative2(repoDirReal, canonicalFile) }, ...spokes];
4003
4034
  }
4004
4035
  function inspectSymlink(filePath, expectedTarget) {
4005
4036
  let isLink;
@@ -4014,7 +4045,9 @@ function inspectSymlink(filePath, expectedTarget) {
4014
4045
  return actual === expectedTarget ? { state: "correct" } : { state: "mismatch", actualTarget: actual };
4015
4046
  }
4016
4047
  function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4017
- const base = { path: entry.path };
4048
+ const mode = instructionMode(entry);
4049
+ const isSelf = mode === "self";
4050
+ const base = { path: entry.path, ...isSelf ? { self: true } : {} };
4018
4051
  let real;
4019
4052
  try {
4020
4053
  real = realpathSync(resolve7(repositoryRoot, entry.path));
@@ -4027,11 +4060,11 @@ function gatherRepoSymlinks(repositoryRoot, anchorReal, entry) {
4027
4060
  if (!existsSync(join7(real, ".git"))) {
4028
4061
  return { ...base, isAnchor: false, reachable: false, canonicalPresent: false, files: [] };
4029
4062
  }
4030
- const canonicalFile = join7(anchorReal, "agents", basename4(real), CANONICAL_FILE);
4063
+ const canonicalFile = isSelf ? join7(real, CANONICAL_FILE) : join7(anchorReal, "agents", basename4(real), CANONICAL_FILE);
4031
4064
  if (!existsSync(canonicalFile)) {
4032
4065
  return { ...base, isAnchor: false, reachable: true, canonicalPresent: false, files: [] };
4033
4066
  }
4034
- const files = expectedSymlinkTargets(real, canonicalFile).map(
4067
+ const files = expectedSymlinkTargets(real, canonicalFile, mode).map(
4035
4068
  (spec) => {
4036
4069
  const { state, actualTarget } = inspectSymlink(join7(real, spec.name), spec.target);
4037
4070
  return {
@@ -4184,6 +4217,13 @@ function renderProjectSymlinks(result) {
4184
4217
  for (const p of result.missingCanonical) lines.push(`- ${p}`);
4185
4218
  lines.push("");
4186
4219
  }
4220
+ if (result.selfAgentsMissing.length > 0) {
4221
+ lines.push(
4222
+ `## AGENTS.md missing (${result.selfAgentsMissing.length}) \u2014 these \`instructions: self\` repos have no committed AGENTS.md yet; author it, then re-run to wire the spokes`
4223
+ );
4224
+ for (const p of result.selfAgentsMissing) lines.push(`- ${p}`);
4225
+ lines.push("");
4226
+ }
4187
4227
  if (result.unreachable.length > 0) {
4188
4228
  lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
4189
4229
  for (const p of result.unreachable) lines.push(`- ${p}`);
@@ -4534,6 +4574,9 @@ async function gatherRepoPreset(repositoryRoot, anchorReal, entry) {
4534
4574
  ...entry.language !== void 0 ? { language: entry.language } : {},
4535
4575
  ...entry.publishes !== void 0 ? { publishes: entry.publishes } : {}
4536
4576
  };
4577
+ if (instructionMode(entry) === "self") {
4578
+ return { ...declared, self: true, isAnchor: false, reachable: true, canonicalPresent: false };
4579
+ }
4537
4580
  let real;
4538
4581
  try {
4539
4582
  real = realpathSync(resolve7(repositoryRoot, entry.path));
@@ -4741,6 +4784,13 @@ function renderProjectPreset(result) {
4741
4784
  for (const p of result.anchors) lines.push(`- ${p}`);
4742
4785
  lines.push("");
4743
4786
  }
4787
+ if (result.self.length > 0) {
4788
+ lines.push(
4789
+ `## instructions: self (${result.self.length}) \u2014 hands-off; their hand-authored AGENTS.md is never written by basou`
4790
+ );
4791
+ for (const p of result.self) lines.push(`- ${p}`);
4792
+ lines.push("");
4793
+ }
4744
4794
  if (result.unreachable.length > 0) {
4745
4795
  lines.push(`## Unreachable (${result.unreachable.length}) \u2014 path unresolved / not a git repo`);
4746
4796
  for (const p of result.unreachable) lines.push(`- ${p}`);
@@ -4846,13 +4896,15 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4846
4896
  const targetAbs = resolve7(repositoryRoot, target);
4847
4897
  const canonicalName = basename4(repoReal ?? targetAbs);
4848
4898
  const roster = manifest.repos ?? [];
4849
- const inRoster = roster.some((r) => {
4899
+ const declaredEntry = roster.find((r) => {
4850
4900
  try {
4851
4901
  return realpathSync(resolve7(repositoryRoot, r.path)) === (repoReal ?? "\0");
4852
4902
  } catch {
4853
4903
  return resolve7(repositoryRoot, r.path) === targetAbs;
4854
4904
  }
4855
4905
  });
4906
+ const inRoster = declaredEntry !== void 0;
4907
+ const isSelf = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
4856
4908
  const cnFold = canonicalName.toLowerCase();
4857
4909
  const canonicalShared = roster.some((r) => {
4858
4910
  let rReal = null;
@@ -4874,6 +4926,16 @@ function gatherRepoTeardown(repositoryRoot, manifest, target) {
4874
4926
  if (repoReal !== void 0) {
4875
4927
  for (const spec of teardownExpectedTargets(repoReal, anchorReal, canonicalName)) {
4876
4928
  const { state, actualTarget } = inspectSymlink(join7(repoReal, spec.name), spec.target);
4929
+ if (isSelf) {
4930
+ if (state !== "missing")
4931
+ items.push({
4932
+ kind: "instruction-symlink",
4933
+ label: spec.name,
4934
+ state: "foreign",
4935
+ note: "instructions: self \u2014 committed, left untouched"
4936
+ });
4937
+ continue;
4938
+ }
4877
4939
  if (state === "correct")
4878
4940
  items.push({ kind: "instruction-symlink", label: spec.name, state: "removable" });
4879
4941
  else if (state === "mismatch")
@@ -5683,6 +5745,250 @@ async function doRunProjectDerive(options, ctx) {
5683
5745
  apply ? "\u2705 Ran every step (each is idempotent, so a partial apply recovers on re-run)." : "\u2139\uFE0F Dry-run preview. Pass --apply to write the changes, then re-run."
5684
5746
  );
5685
5747
  }
5748
+ async function runProjectRetrofit(repo, options, ctx = {}) {
5749
+ try {
5750
+ await doRunProjectRetrofit(repo, options, ctx);
5751
+ } catch (error) {
5752
+ renderCliError(error, { verbose: isVerbose(options) });
5753
+ process.exitCode = 1;
5754
+ }
5755
+ }
5756
+ function inspectAgentsState(filePath) {
5757
+ let st;
5758
+ try {
5759
+ st = lstatSync(filePath);
5760
+ } catch (error) {
5761
+ if (hasErrorCode(error) && error.code === "ENOENT") return "absent";
5762
+ return "blocked";
5763
+ }
5764
+ if (st.isSymbolicLink()) return "symlink";
5765
+ return st.isFile() ? "regular-file" : "blocked";
5766
+ }
5767
+ function regularFileSpokes(repoReal) {
5768
+ const out = [];
5769
+ for (const spoke of ["CLAUDE.md", ".github/copilot-instructions.md"]) {
5770
+ try {
5771
+ const st = lstatSync(join7(repoReal, spoke));
5772
+ if (!st.isSymbolicLink() && st.isFile()) out.push(spoke);
5773
+ } catch {
5774
+ }
5775
+ }
5776
+ return out;
5777
+ }
5778
+ function pathPresent(p) {
5779
+ try {
5780
+ lstatSync(p);
5781
+ return true;
5782
+ } catch {
5783
+ return false;
5784
+ }
5785
+ }
5786
+ function gatherRetrofit(repositoryRoot, anchorReal, roster, argPath, argAbs, argReal) {
5787
+ const declaredEntry = roster.find((entry) => {
5788
+ const entryAbs = resolve7(repositoryRoot, entry.path);
5789
+ if (argReal !== void 0) {
5790
+ try {
5791
+ if (realpathSync(entryAbs) === argReal) return true;
5792
+ } catch {
5793
+ }
5794
+ }
5795
+ return entryAbs === argAbs;
5796
+ });
5797
+ const declared = declaredEntry !== void 0;
5798
+ const self = declaredEntry !== void 0 && instructionMode(declaredEntry) === "self";
5799
+ const displayRel = argReal !== void 0 ? relative2(anchorReal, argReal) : relative2(repositoryRoot, argAbs);
5800
+ const path = displayRel === "" ? "." : displayRel;
5801
+ const canonicalName = basename4(argReal ?? argAbs);
5802
+ if (argReal === void 0) {
5803
+ return {
5804
+ path,
5805
+ declared,
5806
+ ...self ? { self: true } : {},
5807
+ isAnchor: false,
5808
+ reachable: false,
5809
+ canonicalName,
5810
+ agentsState: "absent",
5811
+ canonicalExists: false,
5812
+ regularSpokes: []
5813
+ };
5814
+ }
5815
+ const isAnchor = argReal === anchorReal;
5816
+ const reachable = existsSync(join7(argReal, ".git"));
5817
+ const canonicalFile = join7(anchorReal, "agents", canonicalName, CANONICAL_FILE);
5818
+ return {
5819
+ path,
5820
+ declared,
5821
+ ...self ? { self: true } : {},
5822
+ isAnchor,
5823
+ reachable,
5824
+ canonicalName,
5825
+ agentsState: inspectAgentsState(join7(argReal, CANONICAL_FILE)),
5826
+ canonicalExists: pathPresent(canonicalFile),
5827
+ regularSpokes: regularFileSpokes(argReal)
5828
+ };
5829
+ }
5830
+ function relocateAgentsFile(repoReal, canonicalFile) {
5831
+ const agentsFile = join7(repoReal, CANONICAL_FILE);
5832
+ try {
5833
+ mkdirSync(dirname2(canonicalFile), { recursive: true });
5834
+ } catch (error) {
5835
+ return { ok: false, message: failureReason(error), partial: false };
5836
+ }
5837
+ try {
5838
+ copyFileSync(agentsFile, canonicalFile, fsConstants.COPYFILE_EXCL);
5839
+ } catch (error) {
5840
+ const message = hasErrorCode(error) && error.code === "EEXIST" ? "canonical-exists" : failureReason(error);
5841
+ return { ok: false, message, partial: false };
5842
+ }
5843
+ try {
5844
+ unlinkSync(agentsFile);
5845
+ symlinkSync(relative2(repoReal, canonicalFile), agentsFile);
5846
+ return { ok: true };
5847
+ } catch (error) {
5848
+ return { ok: false, message: failureReason(error), partial: true };
5849
+ }
5850
+ }
5851
+ async function doRunProjectRetrofit(repo, options, ctx) {
5852
+ const cwd = ctx.cwd ?? process.cwd();
5853
+ const repositoryRoot = await resolveBasouRootForCommand(cwd, "project retrofit");
5854
+ const paths = basouPaths10(repositoryRoot);
5855
+ const manifest = await readManifest6(paths);
5856
+ const roster = manifest.repos ?? [];
5857
+ const anchorReal = realpathSync(repositoryRoot);
5858
+ const argAbs = resolve7(repositoryRoot, repo);
5859
+ let argReal;
5860
+ try {
5861
+ argReal = realpathSync(argAbs);
5862
+ } catch {
5863
+ argReal = void 0;
5864
+ }
5865
+ const facts = gatherRetrofit(repositoryRoot, anchorReal, roster, repo, argAbs, argReal);
5866
+ const plan = classifyRetrofit(facts);
5867
+ let applied = false;
5868
+ let failure;
5869
+ let partial = false;
5870
+ if (options.apply === true && plan.action === "relocate" && argReal !== void 0) {
5871
+ const canonicalFile = join7(anchorReal, "agents", plan.canonicalName, CANONICAL_FILE);
5872
+ const res = relocateAgentsFile(argReal, canonicalFile);
5873
+ if (res.ok) {
5874
+ applied = true;
5875
+ } else {
5876
+ failure = res.message;
5877
+ partial = res.partial;
5878
+ }
5879
+ }
5880
+ const result = {
5881
+ ...plan,
5882
+ hasRoster: roster.length > 0,
5883
+ applied,
5884
+ ...failure !== void 0 ? { failure } : {},
5885
+ ...partial ? { partial } : {}
5886
+ };
5887
+ if (options.json === true) {
5888
+ console.log(JSON.stringify(result));
5889
+ } else {
5890
+ console.log(renderProjectRetrofit(result));
5891
+ }
5892
+ return result;
5893
+ }
5894
+ function appendSpokeChecklist(lines, spokes) {
5895
+ if (spokes.length === 0) return;
5896
+ lines.push(
5897
+ `## Spoke files to reconcile (${spokes.length}) \u2014 regular files that would block clean wiring`
5898
+ );
5899
+ for (const s of spokes) {
5900
+ lines.push(
5901
+ `- ${s}: a regular file. If it duplicates AGENTS.md, remove it; if it carries unique content, merge it into AGENTS.md. Then run \`basou project symlinks\`.`
5902
+ );
5903
+ }
5904
+ lines.push("");
5905
+ }
5906
+ function renderProjectRetrofit(result) {
5907
+ const lines = [];
5908
+ lines.push(
5909
+ "# Retrofit an existing AGENTS.md into the project (relocate to the anchor canonical)"
5910
+ );
5911
+ lines.push("");
5912
+ if (!result.hasRoster) {
5913
+ lines.push(
5914
+ "\u2139\uFE0F No repo roster declared (manifest `repos`). Declare the repo first with `basou project new` (or `basou project adopt`), then re-run."
5915
+ );
5916
+ return lines.join("\n");
5917
+ }
5918
+ const canonical2 = `agents/${result.canonicalName}/${CANONICAL_FILE}`;
5919
+ if (result.action === "refuse") {
5920
+ if (result.reason === "not-declared") {
5921
+ lines.push(
5922
+ `\u2139\uFE0F \`${result.path}\` is not declared in the roster (manifest \`repos\`). Add it first with \`basou project new\` / \`basou project adopt\`, then re-run.`
5923
+ );
5924
+ } else if (result.reason === "self") {
5925
+ lines.push(
5926
+ `\u2139\uFE0F \`${result.path}\` declares \`instructions: self\` \u2014 its \`${CANONICAL_FILE}\` is a hand-authored committed file that stays in the repo, so there is no anchor canonical to relocate it to. Retrofit does not apply (its CLAUDE.md / Copilot spokes are wired by \`basou project symlinks\`).`
5927
+ );
5928
+ } else if (result.reason === "anchor") {
5929
+ lines.push(
5930
+ `\u26A0\uFE0F \`${result.path}\` is the anchor (the project root). It owns its canonical directly \u2014 there is nothing to relocate.`
5931
+ );
5932
+ } else if (result.reason === "unreachable") {
5933
+ lines.push(
5934
+ `\u26A0\uFE0F \`${result.path}\` does not resolve to a git repository. There is nothing to retrofit.`
5935
+ );
5936
+ } else if (result.reason === "blocked") {
5937
+ lines.push(
5938
+ `\u26A0\uFE0F \`${result.path}/${CANONICAL_FILE}\` could not be inspected (a parent component is not a directory, a permission error, or the path is neither a regular file nor a symlink). Resolve it by hand, then re-run.`
5939
+ );
5940
+ } else if (result.reason === "canonical-exists") {
5941
+ lines.push(
5942
+ `\u26A0\uFE0F The destination canonical \`${canonical2}\` already exists. Not relocating, to avoid clobbering it. If the canonical is the source of truth, the repo's AGENTS.md is redundant (remove it, then run \`basou project symlinks\`); otherwise reconcile the two by hand.`
5943
+ );
5944
+ }
5945
+ return lines.join("\n");
5946
+ }
5947
+ if (result.action === "skip") {
5948
+ if (result.reason === "already-symlink") {
5949
+ lines.push(
5950
+ `\u2705 \`${result.path}/${CANONICAL_FILE}\` is already a symlink (already wired). Nothing to retrofit.`
5951
+ );
5952
+ } else {
5953
+ lines.push(
5954
+ `\u2139\uFE0F \`${result.path}\` has no regular-file \`${CANONICAL_FILE}\` to relocate. If it should have a canonical, run \`basou project derive\` to generate one from the manifest.`
5955
+ );
5956
+ }
5957
+ lines.push("");
5958
+ appendSpokeChecklist(lines, result.regularSpokes);
5959
+ return lines.join("\n").trimEnd();
5960
+ }
5961
+ if (result.failure !== void 0) {
5962
+ lines.push(
5963
+ `Could not relocate \`${result.path}/${CANONICAL_FILE}\` to \`${canonical2}\`: ${result.failure}.`
5964
+ );
5965
+ if (result.partial === true) {
5966
+ lines.push(
5967
+ `The canonical \`${canonical2}\` was written, but \`${result.path}/${CANONICAL_FILE}\` may be absent. Run \`basou project symlinks --apply\` (or \`basou project derive --apply\`) to recreate the missing link, then verify.`
5968
+ );
5969
+ } else {
5970
+ lines.push("Nothing was changed. Resolve the cause and re-run.");
5971
+ }
5972
+ return lines.join("\n");
5973
+ }
5974
+ if (result.applied) {
5975
+ lines.push(
5976
+ `\u2705 Relocated \`${result.path}/${CANONICAL_FILE}\` to \`${canonical2}\` and left a symlink in its place.`
5977
+ );
5978
+ } else {
5979
+ lines.push(
5980
+ `To relocate \`${result.path}/${CANONICAL_FILE}\` and replace it with a symlink (dry-run; pass --apply to write):`
5981
+ );
5982
+ lines.push(` move ${result.path}/${CANONICAL_FILE} -> ${canonical2}`);
5983
+ lines.push(` symlink ${result.path}/${CANONICAL_FILE} -> the canonical`);
5984
+ }
5985
+ lines.push("");
5986
+ appendSpokeChecklist(lines, result.regularSpokes);
5987
+ lines.push(
5988
+ result.applied ? "Next: run `basou project derive --apply` to add the preset block, the CLAUDE.md / Copilot spokes, and the .gitignore." : "After applying, run `basou project derive --apply` to finish the wiring (preset block, CLAUDE.md / Copilot spokes, .gitignore)."
5989
+ );
5990
+ return lines.join("\n");
5991
+ }
5686
5992
 
5687
5993
  // src/commands/protocol.ts
5688
5994
  import { readFile as readFile4 } from "fs/promises";