docbridge 0.8.0 → 0.9.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
@@ -411,11 +411,11 @@ var require_codegen = __commonJS((exports) => {
411
411
  const rhs = this.rhs === undefined ? "" : ` = ${this.rhs}`;
412
412
  return `${varKind} ${this.name}${rhs};` + _n;
413
413
  }
414
- optimizeNames(names, constants) {
414
+ optimizeNames(names, constants2) {
415
415
  if (!names[this.name.str])
416
416
  return;
417
417
  if (this.rhs)
418
- this.rhs = optimizeExpr(this.rhs, names, constants);
418
+ this.rhs = optimizeExpr(this.rhs, names, constants2);
419
419
  return this;
420
420
  }
421
421
  get names() {
@@ -433,10 +433,10 @@ var require_codegen = __commonJS((exports) => {
433
433
  render({ _n }) {
434
434
  return `${this.lhs} = ${this.rhs};` + _n;
435
435
  }
436
- optimizeNames(names, constants) {
436
+ optimizeNames(names, constants2) {
437
437
  if (this.lhs instanceof code_1.Name && !names[this.lhs.str] && !this.sideEffects)
438
438
  return;
439
- this.rhs = optimizeExpr(this.rhs, names, constants);
439
+ this.rhs = optimizeExpr(this.rhs, names, constants2);
440
440
  return this;
441
441
  }
442
442
  get names() {
@@ -502,8 +502,8 @@ var require_codegen = __commonJS((exports) => {
502
502
  optimizeNodes() {
503
503
  return `${this.code}` ? this : undefined;
504
504
  }
505
- optimizeNames(names, constants) {
506
- this.code = optimizeExpr(this.code, names, constants);
505
+ optimizeNames(names, constants2) {
506
+ this.code = optimizeExpr(this.code, names, constants2);
507
507
  return this;
508
508
  }
509
509
  get names() {
@@ -533,12 +533,12 @@ var require_codegen = __commonJS((exports) => {
533
533
  }
534
534
  return nodes.length > 0 ? this : undefined;
535
535
  }
536
- optimizeNames(names, constants) {
536
+ optimizeNames(names, constants2) {
537
537
  const { nodes } = this;
538
538
  let i = nodes.length;
539
539
  while (i--) {
540
540
  const n = nodes[i];
541
- if (n.optimizeNames(names, constants))
541
+ if (n.optimizeNames(names, constants2))
542
542
  continue;
543
543
  subtractNames(names, n.names);
544
544
  nodes.splice(i, 1);
@@ -595,12 +595,12 @@ var require_codegen = __commonJS((exports) => {
595
595
  return;
596
596
  return this;
597
597
  }
598
- optimizeNames(names, constants) {
598
+ optimizeNames(names, constants2) {
599
599
  var _a;
600
- this.else = (_a = this.else) === null || _a === undefined ? undefined : _a.optimizeNames(names, constants);
601
- if (!(super.optimizeNames(names, constants) || this.else))
600
+ this.else = (_a = this.else) === null || _a === undefined ? undefined : _a.optimizeNames(names, constants2);
601
+ if (!(super.optimizeNames(names, constants2) || this.else))
602
602
  return;
603
- this.condition = optimizeExpr(this.condition, names, constants);
603
+ this.condition = optimizeExpr(this.condition, names, constants2);
604
604
  return this;
605
605
  }
606
606
  get names() {
@@ -625,10 +625,10 @@ var require_codegen = __commonJS((exports) => {
625
625
  render(opts) {
626
626
  return `for(${this.iteration})` + super.render(opts);
627
627
  }
628
- optimizeNames(names, constants) {
629
- if (!super.optimizeNames(names, constants))
628
+ optimizeNames(names, constants2) {
629
+ if (!super.optimizeNames(names, constants2))
630
630
  return;
631
- this.iteration = optimizeExpr(this.iteration, names, constants);
631
+ this.iteration = optimizeExpr(this.iteration, names, constants2);
632
632
  return this;
633
633
  }
634
634
  get names() {
@@ -666,10 +666,10 @@ var require_codegen = __commonJS((exports) => {
666
666
  render(opts) {
667
667
  return `for(${this.varKind} ${this.name} ${this.loop} ${this.iterable})` + super.render(opts);
668
668
  }
669
- optimizeNames(names, constants) {
670
- if (!super.optimizeNames(names, constants))
669
+ optimizeNames(names, constants2) {
670
+ if (!super.optimizeNames(names, constants2))
671
671
  return;
672
- this.iterable = optimizeExpr(this.iterable, names, constants);
672
+ this.iterable = optimizeExpr(this.iterable, names, constants2);
673
673
  return this;
674
674
  }
675
675
  get names() {
@@ -714,11 +714,11 @@ var require_codegen = __commonJS((exports) => {
714
714
  (_b = this.finally) === null || _b === undefined || _b.optimizeNodes();
715
715
  return this;
716
716
  }
717
- optimizeNames(names, constants) {
717
+ optimizeNames(names, constants2) {
718
718
  var _a, _b;
719
- super.optimizeNames(names, constants);
720
- (_a = this.catch) === null || _a === undefined || _a.optimizeNames(names, constants);
721
- (_b = this.finally) === null || _b === undefined || _b.optimizeNames(names, constants);
719
+ super.optimizeNames(names, constants2);
720
+ (_a = this.catch) === null || _a === undefined || _a.optimizeNames(names, constants2);
721
+ (_b = this.finally) === null || _b === undefined || _b.optimizeNames(names, constants2);
722
722
  return this;
723
723
  }
724
724
  get names() {
@@ -992,7 +992,7 @@ var require_codegen = __commonJS((exports) => {
992
992
  function addExprNames(names, from) {
993
993
  return from instanceof code_1._CodeOrName ? addNames(names, from.names) : names;
994
994
  }
995
- function optimizeExpr(expr, names, constants) {
995
+ function optimizeExpr(expr, names, constants2) {
996
996
  if (expr instanceof code_1.Name)
997
997
  return replaceName(expr);
998
998
  if (!canOptimize(expr))
@@ -1007,14 +1007,14 @@ var require_codegen = __commonJS((exports) => {
1007
1007
  return items;
1008
1008
  }, []));
1009
1009
  function replaceName(n) {
1010
- const c = constants[n.str];
1010
+ const c = constants2[n.str];
1011
1011
  if (c === undefined || names[n.str] !== 1)
1012
1012
  return n;
1013
1013
  delete names[n.str];
1014
1014
  return c;
1015
1015
  }
1016
1016
  function canOptimize(e) {
1017
- return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants[c.str] !== undefined);
1017
+ return e instanceof code_1._Code && e._items.some((c) => c instanceof code_1.Name && names[c.str] === 1 && constants2[c.str] !== undefined);
1018
1018
  }
1019
1019
  }
1020
1020
  function subtractNames(names, from) {
@@ -2919,7 +2919,7 @@ var require_compile = __commonJS((exports) => {
2919
2919
  const schOrFunc = root.refs[ref];
2920
2920
  if (schOrFunc)
2921
2921
  return schOrFunc;
2922
- let _sch = resolve.call(this, root, ref);
2922
+ let _sch = resolve2.call(this, root, ref);
2923
2923
  if (_sch === undefined) {
2924
2924
  const schema = (_a = root.localRefs) === null || _a === undefined ? undefined : _a[ref];
2925
2925
  const { schemaId } = this.opts;
@@ -2946,7 +2946,7 @@ var require_compile = __commonJS((exports) => {
2946
2946
  function sameSchemaEnv(s1, s2) {
2947
2947
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
2948
2948
  }
2949
- function resolve(root, ref) {
2949
+ function resolve2(root, ref) {
2950
2950
  let sch;
2951
2951
  while (typeof (sch = this.refs[ref]) == "string")
2952
2952
  ref = sch;
@@ -3532,7 +3532,7 @@ var require_fast_uri = __commonJS((exports, module) => {
3532
3532
  }
3533
3533
  return uri;
3534
3534
  }
3535
- function resolve(baseURI, relativeURI, options) {
3535
+ function resolve2(baseURI, relativeURI, options) {
3536
3536
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
3537
3537
  const { parsed: baseParsed, malformedAuthorityOrPort: baseMalformed } = parseWithStatus(baseURI, schemelessOptions);
3538
3538
  const { parsed: relativeParsed, malformedAuthorityOrPort: relativeMalformed } = parseWithStatus(relativeURI, schemelessOptions);
@@ -3817,7 +3817,7 @@ var require_fast_uri = __commonJS((exports, module) => {
3817
3817
  var fastUri = {
3818
3818
  SCHEMES,
3819
3819
  normalize,
3820
- resolve,
3820
+ resolve: resolve2,
3821
3821
  resolveComponent,
3822
3822
  equal,
3823
3823
  serialize,
@@ -7155,7 +7155,7 @@ var require_typescript = __commonJS((exports, module) => {
7155
7155
  compact: () => compact,
7156
7156
  compareBooleans: () => compareBooleans,
7157
7157
  compareDataObjects: () => compareDataObjects,
7158
- compareDiagnostics: () => compareDiagnostics,
7158
+ compareDiagnostics: () => compareDiagnostics2,
7159
7159
  compareEmitHelpers: () => compareEmitHelpers,
7160
7160
  compareNumberOfDirectorySeparators: () => compareNumberOfDirectorySeparators,
7161
7161
  comparePaths: () => comparePaths2,
@@ -7474,7 +7474,7 @@ var require_typescript = __commonJS((exports, module) => {
7474
7474
  forEachUnique: () => forEachUnique,
7475
7475
  forEachYieldExpression: () => forEachYieldExpression,
7476
7476
  formatColorAndReset: () => formatColorAndReset,
7477
- formatDiagnostic: () => formatDiagnostic,
7477
+ formatDiagnostic: () => formatDiagnostic2,
7478
7478
  formatDiagnostics: () => formatDiagnostics,
7479
7479
  formatDiagnosticsWithColorAndContext: () => formatDiagnosticsWithColorAndContext,
7480
7480
  formatGeneratedName: () => formatGeneratedName,
@@ -14966,7 +14966,7 @@ ${lanes.join(`
14966
14966
  return process.memoryUsage().heapUsed;
14967
14967
  },
14968
14968
  getFileSize(path) {
14969
- const stat = statSync(path);
14969
+ const stat = statSync2(path);
14970
14970
  if (stat == null ? undefined : stat.isFile()) {
14971
14971
  return stat.size;
14972
14972
  }
@@ -15009,7 +15009,7 @@ ${lanes.join(`
15009
15009
  }
15010
15010
  };
15011
15011
  return nodeSystem;
15012
- function statSync(path) {
15012
+ function statSync2(path) {
15013
15013
  try {
15014
15014
  return _fs.statSync(path, statSyncOptions);
15015
15015
  } catch {
@@ -15061,7 +15061,7 @@ ${lanes.join(`
15061
15061
  activeSession.post("Profiler.stop", (err, { profile }) => {
15062
15062
  var _a;
15063
15063
  if (!err) {
15064
- if ((_a = statSync(profilePath)) == null ? undefined : _a.isDirectory()) {
15064
+ if ((_a = statSync2(profilePath)) == null ? undefined : _a.isDirectory()) {
15065
15065
  profilePath = _path.join(profilePath, `${(/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-")}+P${process.pid}.cpuprofile`);
15066
15066
  }
15067
15067
  try {
@@ -15170,7 +15170,7 @@ ${lanes.join(`
15170
15170
  let stat;
15171
15171
  if (typeof dirent === "string" || dirent.isSymbolicLink()) {
15172
15172
  const name = combinePaths(path, entry);
15173
- stat = statSync(name);
15173
+ stat = statSync2(name);
15174
15174
  if (!stat) {
15175
15175
  continue;
15176
15176
  }
@@ -15194,7 +15194,7 @@ ${lanes.join(`
15194
15194
  return matchFiles(path, extensions, excludes, includes, useCaseSensitiveFileNames2, process.cwd(), depth, getAccessibleFileSystemEntries, realpath);
15195
15195
  }
15196
15196
  function fileSystemEntryExists(path, entryKind) {
15197
- const stat = statSync(path);
15197
+ const stat = statSync2(path);
15198
15198
  if (!stat) {
15199
15199
  return false;
15200
15200
  }
@@ -15228,7 +15228,7 @@ ${lanes.join(`
15228
15228
  }
15229
15229
  function getModifiedTime3(path) {
15230
15230
  var _a;
15231
- return (_a = statSync(path)) == null ? undefined : _a.mtime;
15231
+ return (_a = statSync2(path)) == null ? undefined : _a.mtime;
15232
15232
  }
15233
15233
  function setModifiedTime(path, time) {
15234
15234
  try {
@@ -20956,7 +20956,7 @@ ${lanes.join(`
20956
20956
  return pathIsRelative(moduleName) || isRootedDiskPath(moduleName);
20957
20957
  }
20958
20958
  function sortAndDeduplicateDiagnostics(diagnostics) {
20959
- return sortAndDeduplicate(diagnostics, compareDiagnostics, diagnosticsEqualityComparer);
20959
+ return sortAndDeduplicate(diagnostics, compareDiagnostics2, diagnosticsEqualityComparer);
20960
20960
  }
20961
20961
  var targetToLibMap = /* @__PURE__ */ new Map([
20962
20962
  [99, "lib.esnext.full.d.ts"],
@@ -27716,7 +27716,7 @@ ${lanes.join(`
27716
27716
  function getDiagnosticFilePath(diagnostic) {
27717
27717
  return diagnostic.file ? diagnostic.file.path : undefined;
27718
27718
  }
27719
- function compareDiagnostics(d1, d2) {
27719
+ function compareDiagnostics2(d1, d2) {
27720
27720
  return compareDiagnosticsSkipRelatedInformation(d1, d2) || compareRelatedInformation(d1, d2) || 0;
27721
27721
  }
27722
27722
  function compareDiagnosticsSkipRelatedInformation(d1, d2) {
@@ -27731,7 +27731,7 @@ ${lanes.join(`
27731
27731
  if (d1.relatedInformation && d2.relatedInformation) {
27732
27732
  return compareValues(d2.relatedInformation.length, d1.relatedInformation.length) || forEach(d1.relatedInformation, (d1i, index) => {
27733
27733
  const d2i = d2.relatedInformation[index];
27734
- return compareDiagnostics(d1i, d2i);
27734
+ return compareDiagnostics2(d1i, d2i);
27735
27735
  }) || 0;
27736
27736
  }
27737
27737
  return d1.relatedInformation ? -1 : 1;
@@ -54786,7 +54786,7 @@ ${lanes.join(`
54786
54786
  err.relatedInformation = err.relatedInformation || [];
54787
54787
  const leadingMessage = createDiagnosticForNode(adjustedNode, Diagnostics._0_was_also_declared_here, symbolName2);
54788
54788
  const followOnMessage = createDiagnosticForNode(adjustedNode, Diagnostics.and_here);
54789
- if (length(err.relatedInformation) >= 5 || some(err.relatedInformation, (r) => compareDiagnostics(r, followOnMessage) === 0 || compareDiagnostics(r, leadingMessage) === 0))
54789
+ if (length(err.relatedInformation) >= 5 || some(err.relatedInformation, (r) => compareDiagnostics2(r, followOnMessage) === 0 || compareDiagnostics2(r, leadingMessage) === 0))
54790
54790
  continue;
54791
54791
  addRelatedInfo(err, !length(err.relatedInformation) ? leadingMessage : followOnMessage);
54792
54792
  }
@@ -87995,7 +87995,7 @@ ${lanes.join(`
87995
87995
  }
87996
87996
  const currentGlobalDiagnostics = diagnostics.getGlobalDiagnostics();
87997
87997
  if (currentGlobalDiagnostics !== previousGlobalDiagnostics) {
87998
- const deferredGlobalDiagnostics = relativeComplement(previousGlobalDiagnostics, currentGlobalDiagnostics, compareDiagnostics);
87998
+ const deferredGlobalDiagnostics = relativeComplement(previousGlobalDiagnostics, currentGlobalDiagnostics, compareDiagnostics2);
87999
87999
  return concatenate(deferredGlobalDiagnostics, semanticDiagnostics);
88000
88000
  } else if (previousGlobalDiagnosticsSize === 0 && currentGlobalDiagnostics.length > 0) {
88001
88001
  return concatenate(currentGlobalDiagnostics, semanticDiagnostics);
@@ -105133,14 +105133,14 @@ ${lanes.join(`
105133
105133
  }
105134
105134
  }
105135
105135
  function createImportCallExpressionAMD(arg, containsLexicalThis) {
105136
- const resolve = factory2.createUniqueName("resolve");
105136
+ const resolve2 = factory2.createUniqueName("resolve");
105137
105137
  const reject = factory2.createUniqueName("reject");
105138
105138
  const parameters = [
105139
- factory2.createParameterDeclaration(undefined, undefined, resolve),
105139
+ factory2.createParameterDeclaration(undefined, undefined, resolve2),
105140
105140
  factory2.createParameterDeclaration(undefined, undefined, reject)
105141
105141
  ];
105142
105142
  const body = factory2.createBlock([
105143
- factory2.createExpressionStatement(factory2.createCallExpression(factory2.createIdentifier("require"), undefined, [factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve, reject]))
105143
+ factory2.createExpressionStatement(factory2.createCallExpression(factory2.createIdentifier("require"), undefined, [factory2.createArrayLiteralExpression([arg || factory2.createOmittedExpression()]), resolve2, reject]))
105144
105144
  ]);
105145
105145
  let func;
105146
105146
  if (languageVersion >= 2) {
@@ -113910,11 +113910,11 @@ ${lanes.join(`
113910
113910
  function formatDiagnostics(diagnostics, host) {
113911
113911
  let output = "";
113912
113912
  for (const diagnostic of diagnostics) {
113913
- output += formatDiagnostic(diagnostic, host);
113913
+ output += formatDiagnostic2(diagnostic, host);
113914
113914
  }
113915
113915
  return output;
113916
113916
  }
113917
- function formatDiagnostic(diagnostic, host) {
113917
+ function formatDiagnostic2(diagnostic, host) {
113918
113918
  const errorMessage = `${diagnosticCategoryName(diagnostic)} TS${diagnostic.code}: ${flattenDiagnosticMessageText(diagnostic.messageText, host.getNewLine())}${host.getNewLine()}`;
113919
113919
  if (diagnostic.file) {
113920
113920
  const { line, character } = getLineAndCharacterOfPosition(diagnostic.file, diagnostic.start);
@@ -119897,7 +119897,7 @@ ${lanes.join(`
119897
119897
  getCanonicalFileName: createGetCanonicalFileName(system.useCaseSensitiveFileNames)
119898
119898
  };
119899
119899
  if (!pretty) {
119900
- return (diagnostic) => system.write(formatDiagnostic(diagnostic, host));
119900
+ return (diagnostic) => system.write(formatDiagnostic2(diagnostic, host));
119901
119901
  }
119902
119902
  const diagnostics = new Array(1);
119903
119903
  return (diagnostic) => {
@@ -163155,7 +163155,7 @@ ${options.prefix}` : `
163155
163155
  compact: () => compact,
163156
163156
  compareBooleans: () => compareBooleans,
163157
163157
  compareDataObjects: () => compareDataObjects,
163158
- compareDiagnostics: () => compareDiagnostics,
163158
+ compareDiagnostics: () => compareDiagnostics2,
163159
163159
  compareEmitHelpers: () => compareEmitHelpers,
163160
163160
  compareNumberOfDirectorySeparators: () => compareNumberOfDirectorySeparators,
163161
163161
  comparePaths: () => comparePaths2,
@@ -163474,7 +163474,7 @@ ${options.prefix}` : `
163474
163474
  forEachUnique: () => forEachUnique,
163475
163475
  forEachYieldExpression: () => forEachYieldExpression,
163476
163476
  formatColorAndReset: () => formatColorAndReset,
163477
- formatDiagnostic: () => formatDiagnostic,
163477
+ formatDiagnostic: () => formatDiagnostic2,
163478
163478
  formatDiagnostics: () => formatDiagnostics,
163479
163479
  formatDiagnosticsWithColorAndContext: () => formatDiagnosticsWithColorAndContext,
163480
163480
  formatGeneratedName: () => formatGeneratedName,
@@ -175315,8 +175315,8 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
175315
175315
  installPackage(options) {
175316
175316
  this.packageInstallId++;
175317
175317
  const request = { kind: "installPackage", ...options, id: this.packageInstallId };
175318
- const promise = new Promise((resolve, reject) => {
175319
- (this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map)).set(this.packageInstallId, { resolve, reject });
175318
+ const promise = new Promise((resolve2, reject) => {
175319
+ (this.packageInstalledPromise ?? (this.packageInstalledPromise = /* @__PURE__ */ new Map)).set(this.packageInstallId, { resolve: resolve2, reject });
175320
175320
  });
175321
175321
  this.installer.send(request);
175322
175322
  return promise;
@@ -175576,12 +175576,12 @@ Additional information: BADCLIENT: Bad error code, ${badCode} not found in range
175576
175576
  });
175577
175577
 
175578
175578
  // src/cli/index.ts
175579
- import { existsSync as existsSync4, readFileSync as readFileSync6, statSync as statSync4 } from "node:fs";
175580
- import { join as join9, resolve as resolve4 } from "node:path";
175579
+ import { existsSync as existsSync6, readFileSync as readFileSync9, statSync as statSync5 } from "node:fs";
175580
+ import { join as join14, resolve as resolve5 } from "node:path";
175581
175581
  // package.json
175582
175582
  var package_default = {
175583
175583
  name: "docbridge",
175584
- version: "0.8.0",
175584
+ version: "0.9.0",
175585
175585
  description: "A CLI that links Markdown specifications and code declarations. Runs on Node.js and Bun.",
175586
175586
  keywords: [
175587
175587
  "bun",
@@ -175624,6 +175624,7 @@ var package_default = {
175624
175624
  devDependencies: {
175625
175625
  "@types/bun": "^1.3.14",
175626
175626
  ajv: "^8.20.0",
175627
+ "markdown-it": "15.0.1",
175627
175628
  oxfmt: "0.57.0",
175628
175629
  oxlint: "1.73.0",
175629
175630
  typescript: "^5.9.0"
@@ -175636,20 +175637,7 @@ var package_default = {
175636
175637
 
175637
175638
  // src/core/config.ts
175638
175639
  import { readFileSync as readFileSync2 } from "node:fs";
175639
- import { join as join4 } from "node:path";
175640
-
175641
- // src/core/code-language.ts
175642
- import { accessSync, chmodSync, constants, existsSync, realpathSync, statSync } from "node:fs";
175643
- import { dirname as dirname2, join as join3, resolve } from "node:path";
175644
- import { fileURLToPath } from "node:url";
175645
-
175646
- // src/core/code-adapter-registry.ts
175647
- var codeAdapters = {};
175648
-
175649
- // src/core/error.ts
175650
- function reasonOf(error) {
175651
- return error instanceof Error ? error.message : String(error);
175652
- }
175640
+ import { join as join2 } from "node:path";
175653
175641
 
175654
175642
  // src/core/glob.ts
175655
175643
  import { lstatSync, readFileSync, readdirSync } from "node:fs";
@@ -175802,12 +175790,562 @@ function escapeRegExpChar(char) {
175802
175790
  return /[.*+?^${}()|[\]\\]/.test(char) ? `\\${char}` : char;
175803
175791
  }
175804
175792
 
175793
+ // src/core/code-language.ts
175794
+ var KNOWN_CODE_LANGUAGES = [
175795
+ "typescript",
175796
+ "swift",
175797
+ "dart",
175798
+ "rust"
175799
+ ];
175800
+ function isCodeLanguage(value) {
175801
+ return KNOWN_CODE_LANGUAGES.includes(value);
175802
+ }
175803
+ function collectCodeFiles(projectRoot, codeInclude) {
175804
+ const collected = [];
175805
+ const seen = new Set;
175806
+ for (const language of KNOWN_CODE_LANGUAGES) {
175807
+ const entry = codeInclude[language];
175808
+ if (entry === undefined) {
175809
+ continue;
175810
+ }
175811
+ for (const relPath of collectFiles(projectRoot, entry.patterns)) {
175812
+ if (seen.has(relPath)) {
175813
+ continue;
175814
+ }
175815
+ seen.add(relPath);
175816
+ collected.push({ language, relPath });
175817
+ }
175818
+ }
175819
+ collected.sort((left, right) => comparePaths(left.relPath, right.relPath));
175820
+ return collected;
175821
+ }
175822
+ function codeFileOwners(projectRoot, codeInclude) {
175823
+ const owners = new Map;
175824
+ for (const language of KNOWN_CODE_LANGUAGES) {
175825
+ const entry = codeInclude[language];
175826
+ if (entry === undefined) {
175827
+ continue;
175828
+ }
175829
+ for (const relPath of collectFiles(projectRoot, entry.patterns)) {
175830
+ const existing = owners.get(relPath);
175831
+ if (existing === undefined) {
175832
+ owners.set(relPath, [language]);
175833
+ } else if (!existing.includes(language)) {
175834
+ existing.push(language);
175835
+ }
175836
+ }
175837
+ }
175838
+ return owners;
175839
+ }
175840
+
175841
+ // src/core/config.ts
175842
+ var CONFIG_FILE_NAME = "docbridge.config.json";
175843
+ var EMPTY_CONFIG = {
175844
+ include: { code: {}, docs: [] }
175845
+ };
175846
+ var KNOWN_TOP_LEVEL_KEYS = new Set(["$schema", "include"]);
175847
+ var KNOWN_INCLUDE_KEYS = new Set(["code", "docs"]);
175848
+ var KNOWN_CODE_ENTRY_KEYS = new Set(["patterns", "visibility"]);
175849
+ var LANGUAGE_SUFFIX = {
175850
+ typescript: ".ts",
175851
+ swift: ".swift",
175852
+ dart: ".dart",
175853
+ rust: ".rs"
175854
+ };
175855
+ var LANGUAGE_VISIBILITY = {
175856
+ typescript: ["public", "protected", "private"],
175857
+ swift: ["public", "open", "internal"],
175858
+ dart: ["public"],
175859
+ rust: ["pub", "private"]
175860
+ };
175861
+ function loadConfig(projectRoot) {
175862
+ let rawText;
175863
+ try {
175864
+ rawText = readFileSync2(join2(projectRoot, CONFIG_FILE_NAME), "utf8");
175865
+ } catch {
175866
+ rawText = undefined;
175867
+ }
175868
+ const resolved = resolveConfig(rawText);
175869
+ if (!resolved.ok) {
175870
+ return resolved;
175871
+ }
175872
+ const overlapDiagnostics = detectLanguageOverlap(projectRoot, resolved.config.include.code);
175873
+ if (overlapDiagnostics.length === 0) {
175874
+ return resolved;
175875
+ }
175876
+ return {
175877
+ config: EMPTY_CONFIG,
175878
+ diagnostics: [...resolved.diagnostics, ...overlapDiagnostics],
175879
+ ok: false
175880
+ };
175881
+ }
175882
+ function resolveConfig(rawText) {
175883
+ if (rawText === undefined) {
175884
+ return {
175885
+ config: EMPTY_CONFIG,
175886
+ ok: false,
175887
+ diagnostics: [
175888
+ configDiagnostic("config_file_invalid", CONFIG_FILE_NAME, `${CONFIG_FILE_NAME} was not found. DocBridge requires a configuration file.`)
175889
+ ]
175890
+ };
175891
+ }
175892
+ let parsed;
175893
+ try {
175894
+ parsed = JSON.parse(rawText);
175895
+ } catch (error) {
175896
+ const reason = error instanceof Error ? error.message : String(error);
175897
+ return {
175898
+ config: EMPTY_CONFIG,
175899
+ ok: false,
175900
+ diagnostics: [
175901
+ configDiagnostic("config_file_invalid", CONFIG_FILE_NAME, `Failed to parse ${CONFIG_FILE_NAME}: ${reason}`)
175902
+ ]
175903
+ };
175904
+ }
175905
+ const diagnostics = [];
175906
+ if (!isPlainObject(parsed)) {
175907
+ diagnostics.push(configDiagnostic("config_invalid_value", CONFIG_FILE_NAME, "Configuration must be a JSON object."));
175908
+ return { config: EMPTY_CONFIG, diagnostics, ok: false };
175909
+ }
175910
+ for (const key of Object.keys(parsed)) {
175911
+ if (!KNOWN_TOP_LEVEL_KEYS.has(key)) {
175912
+ diagnostics.push(configDiagnostic("config_unknown_key", key, `Unknown top-level configuration key: ${key}`));
175913
+ }
175914
+ }
175915
+ if ("$schema" in parsed && typeof parsed.$schema !== "string") {
175916
+ diagnostics.push(configDiagnostic("config_invalid_value", "$schema", "`$schema` must be a string."));
175917
+ }
175918
+ const include = parsed.include;
175919
+ if (!isPlainObject(include)) {
175920
+ diagnostics.push(configDiagnostic("config_invalid_value", "include", "`include` must be an object with `code` and `docs`."));
175921
+ return { config: EMPTY_CONFIG, diagnostics, ok: false };
175922
+ }
175923
+ for (const key of Object.keys(include)) {
175924
+ if (!KNOWN_INCLUDE_KEYS.has(key)) {
175925
+ diagnostics.push(configDiagnostic("config_unknown_key", `include.${key}`, `Unknown configuration key under \`include\`: ${key}`));
175926
+ }
175927
+ }
175928
+ const code = validateCodeInclude(include.code, diagnostics);
175929
+ validatePatternArray(include.docs, "docs", ".md", false, diagnostics);
175930
+ const ok = diagnostics.length === 0;
175931
+ const config = ok ? { include: { code, docs: include.docs } } : EMPTY_CONFIG;
175932
+ return { config, diagnostics, ok };
175933
+ }
175934
+ function validateCodeInclude(value, diagnostics) {
175935
+ if (Array.isArray(value)) {
175936
+ diagnostics.push(configDiagnostic("config_invalid_value", "include.code", '`include.code` must be a language-keyed object such as `{ "typescript": { "patterns": ["src/**/*.ts"] } }`, not an array.'));
175937
+ return {};
175938
+ }
175939
+ if (!isPlainObject(value)) {
175940
+ diagnostics.push(configDiagnostic("config_invalid_value", "include.code", "`include.code` must be a language-keyed object."));
175941
+ return {};
175942
+ }
175943
+ const keys = Object.keys(value);
175944
+ if (keys.length === 0) {
175945
+ diagnostics.push(configDiagnostic("config_invalid_value", "include.code", "`include.code` must configure at least one language."));
175946
+ return {};
175947
+ }
175948
+ const result = {};
175949
+ for (const key of keys) {
175950
+ if (!isCodeLanguage(key)) {
175951
+ diagnostics.push(configDiagnostic("config_invalid_value", `include.code.${key}`, `Unknown code language: ${key}. Supported languages: ${KNOWN_CODE_LANGUAGES.join(", ")}.`));
175952
+ continue;
175953
+ }
175954
+ const entry = validateCodeEntry(key, value[key], diagnostics);
175955
+ if (entry !== undefined) {
175956
+ result[key] = entry;
175957
+ }
175958
+ }
175959
+ return result;
175960
+ }
175961
+ function validateCodeEntry(language, value, diagnostics) {
175962
+ const target = `include.code.${language}`;
175963
+ if (!isPlainObject(value)) {
175964
+ diagnostics.push(configDiagnostic("config_invalid_value", target, `\`${target}\` must be an object with a \`patterns\` array. Shorthand arrays are not supported.`));
175965
+ return;
175966
+ }
175967
+ for (const key of Object.keys(value)) {
175968
+ if (!KNOWN_CODE_ENTRY_KEYS.has(key)) {
175969
+ diagnostics.push(configDiagnostic("config_unknown_key", `${target}.${key}`, `Unknown configuration key under \`${target}\`: ${key}`));
175970
+ }
175971
+ }
175972
+ const before = diagnostics.length;
175973
+ validatePatternArray(value.patterns, `code.${language}.patterns`, LANGUAGE_SUFFIX[language], language === "typescript", diagnostics);
175974
+ if ("visibility" in value) {
175975
+ if (!isStringArray(value.visibility)) {
175976
+ diagnostics.push(configDiagnostic("config_invalid_value", `${target}.visibility`, `\`${target}.visibility\` must be an array of strings.`));
175977
+ } else {
175978
+ validateVisibilityOptions(language, value.visibility, target, diagnostics);
175979
+ }
175980
+ }
175981
+ if (diagnostics.length !== before) {
175982
+ return;
175983
+ }
175984
+ const entry = { patterns: value.patterns };
175985
+ if (isStringArray(value.visibility)) {
175986
+ entry.visibility = value.visibility;
175987
+ }
175988
+ return entry;
175989
+ }
175990
+ function validateVisibilityOptions(language, values, target, diagnostics) {
175991
+ const allowed = LANGUAGE_VISIBILITY[language];
175992
+ if (allowed.length === 0) {
175993
+ diagnostics.push(configDiagnostic("config_invalid_value", `${target}.visibility`, `\`${target}.visibility\` is not supported for ${language}.`));
175994
+ return;
175995
+ }
175996
+ for (const value of values) {
175997
+ if (!allowed.includes(value)) {
175998
+ diagnostics.push(configDiagnostic("config_invalid_value", `${target}.visibility`, `Unsupported ${language} visibility: ${value}. Supported values: ${allowed.join(", ")}.`));
175999
+ }
176000
+ }
176001
+ }
176002
+ function validatePatternArray(value, field, requiredSuffix, excludeDeclarationFiles, diagnostics) {
176003
+ const target = `include.${field}`;
176004
+ if (!Array.isArray(value)) {
176005
+ diagnostics.push(configDiagnostic("config_invalid_value", target, `\`${target}\` must be a non-empty array of strings.`));
176006
+ return;
176007
+ }
176008
+ if (value.length === 0) {
176009
+ diagnostics.push(configDiagnostic("config_invalid_value", target, `\`${target}\` must contain at least one pattern.`));
176010
+ return;
176011
+ }
176012
+ for (const pattern of value) {
176013
+ if (typeof pattern !== "string") {
176014
+ diagnostics.push(configDiagnostic("config_invalid_value", target, `\`${target}\` patterns must be strings.`));
176015
+ continue;
176016
+ }
176017
+ const globResult = validateGlobPattern(pattern);
176018
+ if (!globResult.ok) {
176019
+ diagnostics.push(configDiagnostic("config_invalid_value", pattern, `Invalid pattern in \`${target}\`: ${globResult.message}`));
176020
+ continue;
176021
+ }
176022
+ const suffixError = checkSuffix(pattern, requiredSuffix, excludeDeclarationFiles);
176023
+ if (suffixError !== undefined) {
176024
+ diagnostics.push(configDiagnostic("config_invalid_value", pattern, suffixError));
176025
+ }
176026
+ }
176027
+ }
176028
+ function checkSuffix(pattern, requiredSuffix, excludeDeclarationFiles) {
176029
+ if (!pattern.endsWith(requiredSuffix)) {
176030
+ return `Pattern must end with \`${requiredSuffix}\`.`;
176031
+ }
176032
+ if (excludeDeclarationFiles && pattern.endsWith(".d.ts")) {
176033
+ return "Pattern must not target `.d.ts` declaration files.";
176034
+ }
176035
+ return;
176036
+ }
176037
+ function detectLanguageOverlap(projectRoot, code) {
176038
+ const diagnostics = [];
176039
+ for (const [relPath, owners] of codeFileOwners(projectRoot, code)) {
176040
+ if (owners.length > 1) {
176041
+ diagnostics.push(configDiagnostic("config_invalid_value", relPath, `Code file ${relPath} matches multiple configured languages (${owners.join(", ")}). Each code file must belong to exactly one language.`));
176042
+ }
176043
+ }
176044
+ return diagnostics;
176045
+ }
176046
+ function configDiagnostic(code, target, message) {
176047
+ return { severity: "error", code, target, message };
176048
+ }
176049
+ function isPlainObject(value) {
176050
+ return typeof value === "object" && value !== null && !Array.isArray(value);
176051
+ }
176052
+ function isStringArray(value) {
176053
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
176054
+ }
176055
+
176056
+ // src/core/diagnostics.ts
176057
+ function sortDiagnostics(diagnostics) {
176058
+ return [...diagnostics].toSorted(compareDiagnostics);
176059
+ }
176060
+ function summarizeDiagnostics(diagnostics) {
176061
+ const summary = { errors: 0, warnings: 0 };
176062
+ for (const diagnostic of diagnostics) {
176063
+ if (diagnostic.severity === "error") {
176064
+ summary.errors += 1;
176065
+ } else {
176066
+ summary.warnings += 1;
176067
+ }
176068
+ }
176069
+ return summary;
176070
+ }
176071
+ function formatDiagnostic(diagnostic) {
176072
+ const location = diagnostic.location;
176073
+ if (location === undefined) {
176074
+ return `${diagnostic.target} ${diagnostic.severity} ${diagnostic.code} - ${diagnostic.message}`;
176075
+ }
176076
+ const prefix = `${location.filePath}:${location.line}:${location.column}`;
176077
+ return `${prefix} ${diagnostic.severity} ${diagnostic.code} ${diagnostic.target} - ${diagnostic.message}`;
176078
+ }
176079
+ function formatSummary(summary) {
176080
+ return `Summary: ${summary.errors} ${pluralize("error", summary.errors)}, ${summary.warnings} ${pluralize("warning", summary.warnings)}`;
176081
+ }
176082
+ function collectErroredFiles(diagnostics) {
176083
+ const errored = new Set;
176084
+ for (const diagnostic of diagnostics) {
176085
+ if (diagnostic.code === "file_read_error" || diagnostic.code === "code_parse_error" || isFileScopedScannerDiagnostic(diagnostic)) {
176086
+ errored.add(diagnostic.target);
176087
+ }
176088
+ }
176089
+ return errored;
176090
+ }
176091
+ function isFileScopedScannerDiagnostic(diagnostic) {
176092
+ return (diagnostic.code === "code_scanner_unavailable" || diagnostic.code === "code_scanner_failed") && diagnostic.language !== undefined && diagnostic.target !== diagnostic.language;
176093
+ }
176094
+ function compareDiagnostics(left, right) {
176095
+ const leftLocation = left.location;
176096
+ const rightLocation = right.location;
176097
+ if (leftLocation === undefined && rightLocation !== undefined) {
176098
+ return -1;
176099
+ }
176100
+ if (leftLocation !== undefined && rightLocation === undefined) {
176101
+ return 1;
176102
+ }
176103
+ if (leftLocation !== undefined && rightLocation !== undefined) {
176104
+ return compareString(leftLocation.filePath, rightLocation.filePath) || compareNumber(leftLocation.line, rightLocation.line) || compareNumber(leftLocation.column, rightLocation.column) || compareString(left.code, right.code) || compareString(left.target, right.target);
176105
+ }
176106
+ return compareString(left.code, right.code) || compareString(left.target, right.target);
176107
+ }
176108
+ function compareString(left, right) {
176109
+ return left.localeCompare(right);
176110
+ }
176111
+ function compareNumber(left, right) {
176112
+ return left - right;
176113
+ }
176114
+ function pluralize(word, count) {
176115
+ return count === 1 ? word : `${word}s`;
176116
+ }
176117
+
176118
+ // src/core/endpoint.ts
176119
+ function filePathOf(endpoint) {
176120
+ const hashIndex = endpoint.indexOf("#");
176121
+ return hashIndex === -1 ? endpoint : endpoint.slice(0, hashIndex);
176122
+ }
176123
+ function fragmentOf(endpoint) {
176124
+ const hashIndex = endpoint.indexOf("#");
176125
+ return hashIndex === -1 ? endpoint : endpoint.slice(hashIndex + 1);
176126
+ }
176127
+ function compareEndpointOrder(left, right) {
176128
+ return left.filePath.localeCompare(right.filePath) || left.line - right.line || left.column - right.column || left.endpoint.localeCompare(right.endpoint);
176129
+ }
176130
+ function endpointRange(element) {
176131
+ const range = element.kind === "code" ? element.nameRange : element.headingTextRange;
176132
+ if (range !== undefined) {
176133
+ return range;
176134
+ }
176135
+ const { line, column } = element.location;
176136
+ return { start: { line, column }, end: { line, column } };
176137
+ }
176138
+
176139
+ // src/core/graph.ts
176140
+ function buildLinkGraph(codeFiles, docFiles) {
176141
+ const codeByEndpoint = new Map;
176142
+ for (const file of codeFiles) {
176143
+ for (const symbol of file.symbols) {
176144
+ if (!codeByEndpoint.has(symbol.endpoint)) {
176145
+ codeByEndpoint.set(symbol.endpoint, symbol);
176146
+ }
176147
+ }
176148
+ }
176149
+ const docByEndpoint = new Map;
176150
+ for (const file of docFiles) {
176151
+ for (const anchor of file.anchors) {
176152
+ if (!docByEndpoint.has(anchor.endpoint)) {
176153
+ docByEndpoint.set(anchor.endpoint, anchor);
176154
+ }
176155
+ }
176156
+ }
176157
+ const counterparts = new Map;
176158
+ const addEdge = (a, b) => {
176159
+ addTo(counterparts, a, b);
176160
+ addTo(counterparts, b, a);
176161
+ };
176162
+ for (const file of codeFiles) {
176163
+ for (const link of file.links) {
176164
+ if (codeByEndpoint.has(link.source) && docByEndpoint.has(link.target)) {
176165
+ addEdge(link.source, link.target);
176166
+ }
176167
+ }
176168
+ }
176169
+ for (const file of docFiles) {
176170
+ for (const link of file.links) {
176171
+ if (docByEndpoint.has(link.source) && codeByEndpoint.has(link.target)) {
176172
+ addEdge(link.source, link.target);
176173
+ }
176174
+ }
176175
+ }
176176
+ return { codeByEndpoint, docByEndpoint, counterparts };
176177
+ }
176178
+ function counterpartsOf(graph, endpoint) {
176179
+ const targets = graph.counterparts.get(endpoint);
176180
+ if (targets === undefined) {
176181
+ return [];
176182
+ }
176183
+ const resolved = [];
176184
+ for (const target of targets) {
176185
+ const code = graph.codeByEndpoint.get(target);
176186
+ if (code !== undefined) {
176187
+ resolved.push(code);
176188
+ continue;
176189
+ }
176190
+ const doc = graph.docByEndpoint.get(target);
176191
+ if (doc !== undefined) {
176192
+ resolved.push(doc);
176193
+ }
176194
+ }
176195
+ return resolved.toSorted(compareEndpoints);
176196
+ }
176197
+ function addTo(map, key, value) {
176198
+ const existing = map.get(key);
176199
+ if (existing === undefined) {
176200
+ map.set(key, new Set([value]));
176201
+ return;
176202
+ }
176203
+ existing.add(value);
176204
+ }
176205
+ function compareEndpoints(left, right) {
176206
+ return compareEndpointOrder({ ...left.location, endpoint: left.endpoint }, { ...right.location, endpoint: right.endpoint });
176207
+ }
176208
+
176209
+ // src/core/code-adapter-registry.ts
176210
+ var codeAdapters = {};
176211
+
176212
+ // src/core/scanner-executable.ts
176213
+ import { accessSync, chmodSync, constants, existsSync, realpathSync, statSync } from "node:fs";
176214
+ import { dirname, join as join3, resolve } from "node:path";
176215
+ import { fileURLToPath } from "node:url";
176216
+
176217
+ // src/core/error.ts
176218
+ function reasonOf(error) {
176219
+ return error instanceof Error ? error.message : String(error);
176220
+ }
176221
+
176222
+ // src/core/scanner-executable.ts
176223
+ var SUPPORTED_SCANNER_PLATFORM_KEYS = ["darwin-arm64", "linux-x64"];
176224
+ var SCANNER_EXECUTABLE_NAMES = {
176225
+ swift: "docbridge-swift-scanner",
176226
+ dart: "docbridge_dart_scanner",
176227
+ rust: "docbridge-rust-scanner"
176228
+ };
176229
+ function scannerPlatformKey() {
176230
+ return `${process.platform}-${process.arch}`;
176231
+ }
176232
+ function scannerExecutableName(language) {
176233
+ return SCANNER_EXECUTABLE_NAMES[language];
176234
+ }
176235
+ function resolveScannerWorkerCommand(language, options = {}) {
176236
+ const platformKey = options.platformKey ?? scannerPlatformKey();
176237
+ const platformSupported = isSupportedScannerPlatformKey(platformKey);
176238
+ const candidates = scannerExecutableCandidates(language, platformKey, platformSupported, options);
176239
+ const found = candidates.find((candidate) => existsSync(candidate));
176240
+ if (found !== undefined) {
176241
+ const repair = ensureExecutable(found, options.chmod ?? chmodSync);
176242
+ if (!repair.ok) {
176243
+ return { ok: false, diagnostic: scannerUnavailableDiagnostic(language, repair.reason) };
176244
+ }
176245
+ return { ok: true, command: [found] };
176246
+ }
176247
+ if (!platformSupported) {
176248
+ return {
176249
+ ok: false,
176250
+ diagnostic: scannerUnavailableDiagnostic(language, `platform ${platformKey} is unsupported; supported platforms: ${SUPPORTED_SCANNER_PLATFORM_KEYS.join(", ")}`)
176251
+ };
176252
+ }
176253
+ return {
176254
+ ok: false,
176255
+ diagnostic: scannerUnavailableDiagnostic(language, `missing ${scannerExecutableName(language)} for platform ${platformKey}; supported platforms: ${SUPPORTED_SCANNER_PLATFORM_KEYS.join(", ")}`)
176256
+ };
176257
+ }
176258
+ function scannerExecutableCandidates(language, platformKey, platformSupported, options) {
176259
+ const sourceRoot = options.sourceRoot ?? sourceRootPath();
176260
+ const distRoot = options.distRoot ?? distRootPath();
176261
+ const executable = scannerExecutableName(language);
176262
+ if (language === "swift") {
176263
+ return [
176264
+ join3(sourceRoot, "packages/swift-scanner/.build/release", executable),
176265
+ join3(sourceRoot, "packages/swift-scanner/.build/debug", executable),
176266
+ ...platformSupported ? [join3(distRoot, "bin", platformKey, executable)] : []
176267
+ ];
176268
+ }
176269
+ if (language === "rust") {
176270
+ return [
176271
+ join3(sourceRoot, "packages/rust-scanner/target/release", executable),
176272
+ join3(sourceRoot, "packages/rust-scanner/target/debug", executable),
176273
+ ...platformSupported ? [join3(distRoot, "bin", platformKey, executable)] : []
176274
+ ];
176275
+ }
176276
+ return [
176277
+ join3(sourceRoot, "packages/dart-scanner/bin", executable),
176278
+ ...platformSupported ? [join3(distRoot, "bin", platformKey, executable)] : []
176279
+ ];
176280
+ }
176281
+ function ensureExecutable(path, chmod) {
176282
+ let mode;
176283
+ try {
176284
+ mode = statSync(path).mode;
176285
+ } catch (error) {
176286
+ return { ok: false, reason: `cannot stat ${path}: ${reasonOf(error)}` };
176287
+ }
176288
+ if (isExecutableByThisProcess(path)) {
176289
+ return { ok: true };
176290
+ }
176291
+ try {
176292
+ chmod(path, mode | 73);
176293
+ } catch (error) {
176294
+ return {
176295
+ ok: false,
176296
+ reason: `${path} is not executable (mode ${formatMode(mode)}) and the executable ` + `bit could not be restored: ${reasonOf(error)}; run \`chmod +x ${path}\` ` + `or reinstall DocBridge into a writable location`
176297
+ };
176298
+ }
176299
+ return { ok: true };
176300
+ }
176301
+ function isExecutableByThisProcess(path) {
176302
+ try {
176303
+ accessSync(path, constants.X_OK);
176304
+ return true;
176305
+ } catch {
176306
+ return false;
176307
+ }
176308
+ }
176309
+ function formatMode(mode) {
176310
+ return `0${(mode & 4095).toString(8).padStart(3, "0")}`;
176311
+ }
176312
+ function isSupportedScannerPlatformKey(platformKey) {
176313
+ return SUPPORTED_SCANNER_PLATFORM_KEYS.includes(platformKey);
176314
+ }
176315
+ function scannerRootsFromModuleUrl(moduleUrl) {
176316
+ const modulePath = fileURLToPath(moduleUrl);
176317
+ let resolved;
176318
+ try {
176319
+ resolved = realpathSync(modulePath);
176320
+ } catch {
176321
+ resolved = modulePath;
176322
+ }
176323
+ const moduleDir = dirname(resolved);
176324
+ return { distRoot: moduleDir, sourceRoot: resolve(moduleDir, "..", "..") };
176325
+ }
176326
+ function sourceRootPath() {
176327
+ return scannerRootsFromModuleUrl(import.meta.url).sourceRoot;
176328
+ }
176329
+ function distRootPath() {
176330
+ return scannerRootsFromModuleUrl(import.meta.url).distRoot;
176331
+ }
176332
+ function scannerUnavailableDiagnostic(language, reason) {
176333
+ const label = language.charAt(0).toUpperCase() + language.slice(1);
176334
+ return {
176335
+ severity: "error",
176336
+ code: "code_scanner_unavailable",
176337
+ language,
176338
+ target: language,
176339
+ message: `${label} scanner worker is unavailable: ${reason}`
176340
+ };
176341
+ }
176342
+
175805
176343
  // src/core/scanner-worker.ts
175806
176344
  var import__2020 = __toESM(require_2020(), 1);
175807
176345
  import { spawnSync } from "node:child_process";
175808
176346
  import { mkdirSync } from "node:fs";
175809
176347
  import { tmpdir } from "node:os";
175810
- import { dirname, join as join2 } from "node:path";
176348
+ import { dirname as dirname2, join as join4 } from "node:path";
175811
176349
  // schemas/common-output.schema.json
175812
176350
  var common_output_schema_default = {
175813
176351
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -175897,6 +176435,7 @@ var scanner_worker_schema_default = {
175897
176435
  canonicalId: { type: "string" },
175898
176436
  endpoint: { type: "string" },
175899
176437
  location: { $ref: "common-output.schema.json#/$defs/location" },
176438
+ isMember: { type: "boolean" },
175900
176439
  nameRange: { $ref: "common-output.schema.json#/$defs/range" },
175901
176440
  declarationRange: { $ref: "common-output.schema.json#/$defs/range" },
175902
176441
  signatureRange: { $ref: "common-output.schema.json#/$defs/range" }
@@ -176015,7 +176554,7 @@ function invokeScannerWorker(request, command, run = runScannerWorkerProcess) {
176015
176554
  if (!processResult.ok) {
176016
176555
  return {
176017
176556
  ok: false,
176018
- diagnostic: scannerUnavailableDiagnostic(request.language, processResult.error, command[0]),
176557
+ diagnostic: scannerUnavailableDiagnostic2(request.language, processResult.error, command[0]),
176019
176558
  stderr: processResult.stderr
176020
176559
  };
176021
176560
  }
@@ -176056,7 +176595,7 @@ function invokeScannerWorker(request, command, run = runScannerWorkerProcess) {
176056
176595
  }
176057
176596
  function clangModuleCachePath() {
176058
176597
  const owner = typeof process.getuid === "function" ? process.getuid() : "shared";
176059
- return join2(tmpdir(), `docbridge-clang-module-cache-${owner}`);
176598
+ return join4(tmpdir(), `docbridge-clang-module-cache-${owner}`);
176060
176599
  }
176061
176600
  function runScannerWorkerProcess(input) {
176062
176601
  try {
@@ -176132,7 +176671,7 @@ function responseFilesMatchRequest(responseFiles, requestFiles) {
176132
176671
  function isRecord(value) {
176133
176672
  return typeof value === "object" && value !== null && !Array.isArray(value);
176134
176673
  }
176135
- function scannerUnavailableDiagnostic(language, error, executable) {
176674
+ function scannerUnavailableDiagnostic2(language, error, executable) {
176136
176675
  const label = languageLabel(language);
176137
176676
  return {
176138
176677
  severity: "error",
@@ -176147,7 +176686,7 @@ function spawnFailureReason(error, executable) {
176147
176686
  if (!isExecDenied(error) || executable === undefined) {
176148
176687
  return reason;
176149
176688
  }
176150
- return `${reason}; the scanner is executable but ${dirname(executable)} refuses to ` + `execute it, which a \`noexec\` mount does. Install DocBridge as a project ` + `dependency, or point the installer cache at an exec-capable directory, ` + `instead of running through \`bunx\``;
176689
+ return `${reason}; the scanner is executable but ${dirname2(executable)} refuses to ` + `execute it, which a \`noexec\` mount does. Install DocBridge as a project ` + `dependency, or point the installer cache at an exec-capable directory, ` + `instead of running through \`bunx\``;
176151
176690
  }
176152
176691
  function isExecDenied(error) {
176153
176692
  if (!(error instanceof Error)) {
@@ -176283,7 +176822,7 @@ function scanTypeScript(filePath, content, options = {}) {
176283
176822
  for (const declaration of declarations) {
176284
176823
  const endpoint = `${filePath}#${declaration.canonicalId}`;
176285
176824
  if (!documentedEndpoints.has(endpoint)) {
176286
- if (!declaration.isMember && !undocumentedSeen.has(endpoint)) {
176825
+ if (!undocumentedSeen.has(endpoint)) {
176287
176826
  undocumentedSeen.add(endpoint);
176288
176827
  undocumentedSymbols.push(makeCodeSymbol(filePath, endpoint, declaration));
176289
176828
  }
@@ -176609,6 +177148,9 @@ function makeCodeSymbol(filePath, endpoint, declaration) {
176609
177148
  endpoint,
176610
177149
  location: declaration.location
176611
177150
  };
177151
+ if (declaration.isMember) {
177152
+ symbol.isMember = true;
177153
+ }
176612
177154
  if (declaration.nameRange !== undefined) {
176613
177155
  symbol.nameRange = declaration.nameRange;
176614
177156
  }
@@ -176693,54 +177235,13 @@ function duplicateLinkDiagnostic(source, target, location, range) {
176693
177235
  return diagnostic;
176694
177236
  }
176695
177237
 
176696
- // src/core/code-language.ts
176697
- var KNOWN_CODE_LANGUAGES = [
176698
- "typescript",
176699
- "swift",
176700
- "dart",
176701
- "rust"
176702
- ];
176703
- function isCodeLanguage(value) {
176704
- return KNOWN_CODE_LANGUAGES.includes(value);
176705
- }
177238
+ // src/core/code-scan.ts
176706
177239
  Object.assign(codeAdapters, {
176707
177240
  typescript: typeScriptAdapter,
176708
177241
  swift: createScannerWorkerAdapter("swift", (_projectRoot) => resolveScannerWorkerCommand("swift")),
176709
177242
  dart: createScannerWorkerAdapter("dart", (_projectRoot) => resolveScannerWorkerCommand("dart")),
176710
177243
  rust: createScannerWorkerAdapter("rust", (_projectRoot) => resolveScannerWorkerCommand("rust"))
176711
177244
  });
176712
- var SUPPORTED_SCANNER_PLATFORM_KEYS = ["darwin-arm64", "linux-x64"];
176713
- var SCANNER_EXECUTABLE_NAMES = {
176714
- swift: "docbridge-swift-scanner",
176715
- dart: "docbridge_dart_scanner",
176716
- rust: "docbridge-rust-scanner"
176717
- };
176718
- function scannerPlatformKey() {
176719
- return `${process.platform}-${process.arch}`;
176720
- }
176721
- function resolveScannerWorkerCommand(language, options = {}) {
176722
- const platformKey = options.platformKey ?? scannerPlatformKey();
176723
- const platformSupported = isSupportedScannerPlatformKey(platformKey);
176724
- const candidates = scannerExecutableCandidates(language, platformKey, platformSupported, options);
176725
- const found = candidates.find((candidate) => existsSync(candidate));
176726
- if (found !== undefined) {
176727
- const repair = ensureExecutable(found, options.chmod ?? chmodSync);
176728
- if (!repair.ok) {
176729
- return { ok: false, diagnostic: scannerUnavailableDiagnostic2(language, repair.reason) };
176730
- }
176731
- return { ok: true, command: [found] };
176732
- }
176733
- if (!platformSupported) {
176734
- return {
176735
- ok: false,
176736
- diagnostic: scannerUnavailableDiagnostic2(language, `platform ${platformKey} is unsupported; supported platforms: ${SUPPORTED_SCANNER_PLATFORM_KEYS.join(", ")}`)
176737
- };
176738
- }
176739
- return {
176740
- ok: false,
176741
- diagnostic: scannerUnavailableDiagnostic2(language, `missing ${scannerExecutableName(language)} for platform ${platformKey}; supported platforms: ${SUPPORTED_SCANNER_PLATFORM_KEYS.join(", ")}`)
176742
- };
176743
- }
176744
177245
  function getCodeAdapter(language) {
176745
177246
  return codeAdapters[language];
176746
177247
  }
@@ -176774,25 +177275,6 @@ function createScannerWorkerAdapter(language, command, adapterOptions = {}) {
176774
177275
  }
176775
177276
  };
176776
177277
  }
176777
- function collectCodeFiles(projectRoot, codeInclude) {
176778
- const collected = [];
176779
- const seen = new Set;
176780
- for (const language of KNOWN_CODE_LANGUAGES) {
176781
- const entry = codeInclude[language];
176782
- if (entry === undefined) {
176783
- continue;
176784
- }
176785
- for (const relPath of collectFiles(projectRoot, entry.patterns)) {
176786
- if (seen.has(relPath)) {
176787
- continue;
176788
- }
176789
- seen.add(relPath);
176790
- collected.push({ language, relPath });
176791
- }
176792
- }
176793
- collected.sort((left, right) => comparePaths(left.relPath, right.relPath));
176794
- return collected;
176795
- }
176796
177278
  function scanCodeFiles(projectRoot, files, codeInclude, read, onContent) {
176797
177279
  const codeFiles = [];
176798
177280
  const diagnostics = [];
@@ -176817,24 +177299,6 @@ function scanCodeFiles(projectRoot, files, codeInclude, read, onContent) {
176817
177299
  }
176818
177300
  return { codeFiles, diagnostics };
176819
177301
  }
176820
- function codeFileOwners(projectRoot, codeInclude) {
176821
- const owners = new Map;
176822
- for (const language of KNOWN_CODE_LANGUAGES) {
176823
- const entry = codeInclude[language];
176824
- if (entry === undefined) {
176825
- continue;
176826
- }
176827
- for (const relPath of collectFiles(projectRoot, entry.patterns)) {
176828
- const existing = owners.get(relPath);
176829
- if (existing === undefined) {
176830
- owners.set(relPath, [language]);
176831
- } else if (!existing.includes(language)) {
176832
- existing.push(language);
176833
- }
176834
- }
176835
- }
176836
- return owners;
176837
- }
176838
177302
  function emptyScan(language, filePath) {
176839
177303
  return {
176840
177304
  language,
@@ -176851,448 +177315,717 @@ function fileScopedScannerDiagnostic(diagnostic, filePath) {
176851
177315
  function normalizeScannerCommand(value) {
176852
177316
  return Array.isArray(value) ? { ok: true, command: value } : value;
176853
177317
  }
176854
- function scannerExecutableCandidates(language, platformKey, platformSupported, options) {
176855
- const sourceRoot = options.sourceRoot ?? sourceRootPath();
176856
- const distRoot = options.distRoot ?? distRootPath();
176857
- const executable = scannerExecutableName(language);
176858
- if (language === "swift") {
176859
- return [
176860
- join3(sourceRoot, "packages/swift-scanner/.build/release", executable),
176861
- join3(sourceRoot, "packages/swift-scanner/.build/debug", executable),
176862
- ...platformSupported ? [join3(distRoot, "bin", platformKey, executable)] : []
176863
- ];
176864
- }
176865
- if (language === "rust") {
176866
- return [
176867
- join3(sourceRoot, "packages/rust-scanner/target/release", executable),
176868
- join3(sourceRoot, "packages/rust-scanner/target/debug", executable),
176869
- ...platformSupported ? [join3(distRoot, "bin", platformKey, executable)] : []
176870
- ];
177318
+
177319
+ // src/core/link-manifest.ts
177320
+ import { readFileSync as readFileSync3 } from "node:fs";
177321
+ import { join as join5 } from "node:path";
177322
+
177323
+ // src/core/json-source.ts
177324
+ class JsonSyntaxError extends Error {
177325
+ position;
177326
+ constructor(message, position) {
177327
+ super(message);
177328
+ this.position = position;
176871
177329
  }
176872
- return [
176873
- join3(sourceRoot, "packages/dart-scanner/bin", executable),
176874
- ...platformSupported ? [join3(distRoot, "bin", platformKey, executable)] : []
176875
- ];
176876
177330
  }
176877
- function ensureExecutable(path, chmod) {
176878
- let mode;
177331
+ var ESCAPE_VALUES = {
177332
+ '"': '"',
177333
+ "\\": "\\",
177334
+ "/": "/",
177335
+ b: "\b",
177336
+ f: "\f",
177337
+ n: `
177338
+ `,
177339
+ r: "\r",
177340
+ t: "\t"
177341
+ };
177342
+ function parseJsonSource(text) {
177343
+ const reader = new JsonReader(text);
176879
177344
  try {
176880
- mode = statSync(path).mode;
177345
+ reader.skipWhitespace();
177346
+ const root = reader.readValue();
177347
+ reader.skipWhitespace();
177348
+ if (!reader.atEnd()) {
177349
+ reader.fail("Unexpected trailing content after the JSON value.");
177350
+ }
177351
+ return { ok: true, root };
176881
177352
  } catch (error) {
176882
- return { ok: false, reason: `cannot stat ${path}: ${reasonOf(error)}` };
177353
+ if (error instanceof JsonSyntaxError) {
177354
+ return { ok: false, message: error.message, position: error.position };
177355
+ }
177356
+ throw error;
176883
177357
  }
176884
- if (isExecutableByThisProcess(path)) {
176885
- return { ok: true };
177358
+ }
177359
+
177360
+ class JsonReader {
177361
+ text;
177362
+ index = 0;
177363
+ line = 1;
177364
+ column = 1;
177365
+ constructor(text) {
177366
+ this.text = text;
177367
+ }
177368
+ atEnd() {
177369
+ return this.index >= this.text.length;
177370
+ }
177371
+ fail(message) {
177372
+ throw new JsonSyntaxError(message, this.position());
177373
+ }
177374
+ position() {
177375
+ return { line: this.line, column: this.column };
177376
+ }
177377
+ skipWhitespace() {
177378
+ while (!this.atEnd()) {
177379
+ const char = this.text[this.index] ?? "";
177380
+ if (char !== " " && char !== "\t" && char !== `
177381
+ ` && char !== "\r") {
177382
+ return;
177383
+ }
177384
+ this.advance();
177385
+ }
177386
+ }
177387
+ readValue() {
177388
+ if (this.atEnd()) {
177389
+ this.fail("Unexpected end of input where a JSON value was expected.");
177390
+ }
177391
+ const char = this.text[this.index] ?? "";
177392
+ switch (char) {
177393
+ case "{":
177394
+ return this.readObject();
177395
+ case "[":
177396
+ return this.readArray();
177397
+ case '"':
177398
+ return this.readString();
177399
+ case "t":
177400
+ return this.readKeyword("true", true);
177401
+ case "f":
177402
+ return this.readKeyword("false", false);
177403
+ case "n":
177404
+ return this.readNull();
177405
+ default:
177406
+ return this.readNumber();
177407
+ }
176886
177408
  }
176887
- try {
176888
- chmod(path, mode | 73);
176889
- } catch (error) {
176890
- return {
176891
- ok: false,
176892
- reason: `${path} is not executable (mode ${formatMode(mode)}) and the executable ` + `bit could not be restored: ${reasonOf(error)}; run \`chmod +x ${path}\` ` + `or reinstall DocBridge into a writable location`
176893
- };
177409
+ readObject() {
177410
+ const start = this.position();
177411
+ this.expect("{");
177412
+ const members = [];
177413
+ this.skipWhitespace();
177414
+ if (this.peek() === "}") {
177415
+ this.advance();
177416
+ return { kind: "object", members, range: { start, end: this.position() } };
177417
+ }
177418
+ for (;; ) {
177419
+ this.skipWhitespace();
177420
+ if (this.peek() !== '"') {
177421
+ this.fail("Expected a quoted object key.");
177422
+ }
177423
+ const keyNode = this.readString();
177424
+ this.skipWhitespace();
177425
+ this.expect(":");
177426
+ this.skipWhitespace();
177427
+ const value = this.readValue();
177428
+ members.push({ key: keyNode.value, keyRange: keyNode.range, value });
177429
+ this.skipWhitespace();
177430
+ const next = this.peek();
177431
+ if (next === ",") {
177432
+ this.advance();
177433
+ continue;
177434
+ }
177435
+ if (next === "}") {
177436
+ this.advance();
177437
+ return { kind: "object", members, range: { start, end: this.position() } };
177438
+ }
177439
+ this.fail("Expected `,` or `}` in the object.");
177440
+ }
176894
177441
  }
176895
- return { ok: true };
176896
- }
176897
- function isExecutableByThisProcess(path) {
176898
- try {
176899
- accessSync(path, constants.X_OK);
176900
- return true;
176901
- } catch {
176902
- return false;
177442
+ readArray() {
177443
+ const start = this.position();
177444
+ this.expect("[");
177445
+ const elements = [];
177446
+ this.skipWhitespace();
177447
+ if (this.peek() === "]") {
177448
+ this.advance();
177449
+ return { kind: "array", elements, range: { start, end: this.position() } };
177450
+ }
177451
+ for (;; ) {
177452
+ this.skipWhitespace();
177453
+ elements.push(this.readValue());
177454
+ this.skipWhitespace();
177455
+ const next = this.peek();
177456
+ if (next === ",") {
177457
+ this.advance();
177458
+ continue;
177459
+ }
177460
+ if (next === "]") {
177461
+ this.advance();
177462
+ return { kind: "array", elements, range: { start, end: this.position() } };
177463
+ }
177464
+ this.fail("Expected `,` or `]` in the array.");
177465
+ }
176903
177466
  }
176904
- }
176905
- function formatMode(mode) {
176906
- return `0${(mode & 4095).toString(8).padStart(3, "0")}`;
176907
- }
176908
- function isSupportedScannerPlatformKey(platformKey) {
176909
- return SUPPORTED_SCANNER_PLATFORM_KEYS.includes(platformKey);
176910
- }
176911
- function scannerRootsFromModuleUrl(moduleUrl) {
176912
- const modulePath = fileURLToPath(moduleUrl);
176913
- let resolved;
176914
- try {
176915
- resolved = realpathSync(modulePath);
176916
- } catch {
176917
- resolved = modulePath;
177467
+ readString() {
177468
+ const start = this.position();
177469
+ this.expect('"');
177470
+ const contentStart = this.position();
177471
+ let value = "";
177472
+ for (;; ) {
177473
+ if (this.atEnd()) {
177474
+ this.fail("Unterminated string.");
177475
+ }
177476
+ const char = this.text[this.index] ?? "";
177477
+ if (char === '"') {
177478
+ const contentEnd = this.position();
177479
+ this.advance();
177480
+ return {
177481
+ kind: "string",
177482
+ value,
177483
+ contentRange: { start: contentStart, end: contentEnd },
177484
+ range: { start, end: this.position() }
177485
+ };
177486
+ }
177487
+ if (char === "\\") {
177488
+ this.advance();
177489
+ value += this.readEscape();
177490
+ continue;
177491
+ }
177492
+ if (char < " ") {
177493
+ this.fail("Unescaped control character in a string.");
177494
+ }
177495
+ this.advance();
177496
+ value += char;
177497
+ }
177498
+ }
177499
+ readEscape() {
177500
+ if (this.atEnd()) {
177501
+ this.fail("Unterminated escape sequence.");
177502
+ }
177503
+ const char = this.text[this.index] ?? "";
177504
+ const simple = ESCAPE_VALUES[char];
177505
+ if (simple !== undefined) {
177506
+ this.advance();
177507
+ return simple;
177508
+ }
177509
+ if (char !== "u") {
177510
+ this.fail(`Unknown escape sequence \`\\${char}\`.`);
177511
+ }
177512
+ this.advance();
177513
+ const digits = this.text.slice(this.index, this.index + 4);
177514
+ if (!/^[0-9a-fA-F]{4}$/.test(digits)) {
177515
+ this.fail("A `\\u` escape needs four hexadecimal digits.");
177516
+ }
177517
+ for (let offset = 0;offset < 4; offset += 1) {
177518
+ this.advance();
177519
+ }
177520
+ return String.fromCharCode(Number.parseInt(digits, 16));
177521
+ }
177522
+ readNumber() {
177523
+ const start = this.position();
177524
+ const pattern = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
177525
+ pattern.lastIndex = this.index;
177526
+ const match = pattern.exec(this.text);
177527
+ if (match === null || match[0].length === 0) {
177528
+ this.fail("Expected a JSON value.");
177529
+ }
177530
+ for (let offset = 0;offset < match[0].length; offset += 1) {
177531
+ this.advance();
177532
+ }
177533
+ if (/[0-9a-zA-Z.+-]/.test(this.peek() ?? "")) {
177534
+ this.fail("Malformed number.");
177535
+ }
177536
+ return { kind: "number", value: Number(match[0]), range: { start, end: this.position() } };
177537
+ }
177538
+ readNull() {
177539
+ const start = this.position();
177540
+ this.consumeWord("null");
177541
+ return { kind: "null", value: null, range: { start, end: this.position() } };
177542
+ }
177543
+ readKeyword(word, value) {
177544
+ const start = this.position();
177545
+ this.consumeWord(word);
177546
+ return { kind: "boolean", value, range: { start, end: this.position() } };
177547
+ }
177548
+ consumeWord(word) {
177549
+ if (this.text.slice(this.index, this.index + word.length) !== word) {
177550
+ this.fail(`Expected \`${word}\`.`);
177551
+ }
177552
+ for (let offset = 0;offset < word.length; offset += 1) {
177553
+ this.advance();
177554
+ }
177555
+ }
177556
+ peek() {
177557
+ return this.text[this.index];
177558
+ }
177559
+ expect(char) {
177560
+ if (this.peek() !== char) {
177561
+ this.fail(`Expected \`${char}\`.`);
177562
+ }
177563
+ this.advance();
177564
+ }
177565
+ advance() {
177566
+ const char = this.text[this.index] ?? "";
177567
+ this.index += 1;
177568
+ if (char === `
177569
+ `) {
177570
+ this.line += 1;
177571
+ this.column = 1;
177572
+ return;
177573
+ }
177574
+ if (char === "\r") {
177575
+ if (this.text[this.index] === `
177576
+ `) {
177577
+ this.index += 1;
177578
+ }
177579
+ this.line += 1;
177580
+ this.column = 1;
177581
+ return;
177582
+ }
177583
+ this.column += 1;
176918
177584
  }
176919
- const moduleDir = dirname2(resolved);
176920
- return { distRoot: moduleDir, sourceRoot: resolve(moduleDir, "..", "..") };
176921
- }
176922
- function sourceRootPath() {
176923
- return scannerRootsFromModuleUrl(import.meta.url).sourceRoot;
176924
- }
176925
- function distRootPath() {
176926
- return scannerRootsFromModuleUrl(import.meta.url).distRoot;
176927
- }
176928
- function scannerExecutableName(language) {
176929
- return SCANNER_EXECUTABLE_NAMES[language];
176930
- }
176931
- function scannerUnavailableDiagnostic2(language, reason) {
176932
- const label = language.charAt(0).toUpperCase() + language.slice(1);
176933
- return {
176934
- severity: "error",
176935
- code: "code_scanner_unavailable",
176936
- language,
176937
- target: language,
176938
- message: `${label} scanner worker is unavailable: ${reason}`
176939
- };
176940
177585
  }
176941
177586
 
176942
- // src/core/config.ts
176943
- var CONFIG_FILE_NAME = "docbridge.config.json";
176944
- var EMPTY_CONFIG = {
176945
- include: { code: {}, docs: [] }
176946
- };
176947
- var KNOWN_TOP_LEVEL_KEYS = new Set(["$schema", "include"]);
176948
- var KNOWN_INCLUDE_KEYS = new Set(["code", "docs"]);
176949
- var KNOWN_CODE_ENTRY_KEYS = new Set(["patterns", "visibility"]);
176950
- var LANGUAGE_SUFFIX = {
176951
- typescript: ".ts",
176952
- swift: ".swift",
176953
- dart: ".dart",
176954
- rust: ".rs"
176955
- };
176956
- var LANGUAGE_VISIBILITY = {
176957
- typescript: ["public", "protected", "private"],
176958
- swift: ["public", "open", "internal"],
176959
- dart: ["public"],
176960
- rust: ["pub", "private"]
176961
- };
176962
- function loadConfig(projectRoot) {
177587
+ // src/core/link-manifest.ts
177588
+ var LINK_MANIFEST_FILE_NAME = "docbridge.links.json";
177589
+ var KNOWN_TOP_LEVEL_KEYS2 = new Set(["$schema", "links"]);
177590
+ var KNOWN_ENTRY_KEYS = new Set(["code", "doc", "note"]);
177591
+ var EMPTY_MANIFEST = { entries: [] };
177592
+ function loadLinkManifest(projectRoot) {
176963
177593
  let rawText;
176964
177594
  try {
176965
- rawText = readFileSync2(join4(projectRoot, CONFIG_FILE_NAME), "utf8");
176966
- } catch {
177595
+ rawText = readFileSync3(join5(projectRoot, LINK_MANIFEST_FILE_NAME), "utf8");
177596
+ } catch (error) {
177597
+ if (!isMissingFileError(error)) {
177598
+ const reason = error instanceof Error ? error.message : String(error);
177599
+ return {
177600
+ ok: false,
177601
+ diagnostics: [
177602
+ manifestDiagnostic("config_file_invalid", `Failed to read ${LINK_MANIFEST_FILE_NAME}: ${reason}`)
177603
+ ]
177604
+ };
177605
+ }
176967
177606
  rawText = undefined;
176968
177607
  }
176969
- const resolved = resolveConfig(rawText);
176970
- if (!resolved.ok) {
176971
- return resolved;
176972
- }
176973
- const overlapDiagnostics = detectLanguageOverlap(projectRoot, resolved.config.include.code);
176974
- if (overlapDiagnostics.length === 0) {
176975
- return resolved;
176976
- }
176977
- return {
176978
- config: EMPTY_CONFIG,
176979
- diagnostics: [...resolved.diagnostics, ...overlapDiagnostics],
176980
- ok: false
176981
- };
177608
+ return resolveLinkManifest(rawText);
176982
177609
  }
176983
- function resolveConfig(rawText) {
177610
+ function isMissingFileError(error) {
177611
+ return error instanceof Error && error.code === "ENOENT";
177612
+ }
177613
+ function resolveLinkManifest(rawText) {
176984
177614
  if (rawText === undefined) {
176985
- return {
176986
- config: EMPTY_CONFIG,
176987
- ok: false,
176988
- diagnostics: [
176989
- configDiagnostic("config_file_invalid", CONFIG_FILE_NAME, `${CONFIG_FILE_NAME} was not found. DocBridge requires a configuration file.`)
176990
- ]
176991
- };
177615
+ return { ok: true, manifest: EMPTY_MANIFEST, diagnostics: [] };
176992
177616
  }
176993
- let parsed;
176994
- try {
176995
- parsed = JSON.parse(rawText);
176996
- } catch (error) {
176997
- const reason = error instanceof Error ? error.message : String(error);
177617
+ const parsed = parseJsonSource(rawText);
177618
+ if (!parsed.ok) {
176998
177619
  return {
176999
- config: EMPTY_CONFIG,
177000
177620
  ok: false,
177001
177621
  diagnostics: [
177002
- configDiagnostic("config_file_invalid", CONFIG_FILE_NAME, `Failed to parse ${CONFIG_FILE_NAME}: ${reason}`)
177622
+ manifestDiagnostic("config_file_invalid", `Failed to parse ${LINK_MANIFEST_FILE_NAME}: ${parsed.message}`, parsed.position)
177003
177623
  ]
177004
177624
  };
177005
177625
  }
177006
- const diagnostics = [];
177007
- if (!isPlainObject(parsed)) {
177008
- diagnostics.push(configDiagnostic("config_invalid_value", CONFIG_FILE_NAME, "Configuration must be a JSON object."));
177009
- return { config: EMPTY_CONFIG, diagnostics, ok: false };
177626
+ return validateManifest(parsed.root);
177627
+ }
177628
+ function validateManifest(root) {
177629
+ if (root.kind !== "object") {
177630
+ return { ok: false, diagnostics: [invalidValue("", "an object", root)] };
177010
177631
  }
177011
- for (const key of Object.keys(parsed)) {
177012
- if (!KNOWN_TOP_LEVEL_KEYS.has(key)) {
177013
- diagnostics.push(configDiagnostic("config_unknown_key", key, `Unknown top-level configuration key: ${key}`));
177632
+ const diagnostics = [];
177633
+ for (const member of root.members) {
177634
+ if (!KNOWN_TOP_LEVEL_KEYS2.has(member.key)) {
177635
+ diagnostics.push(unknownKey(member.key, member));
177014
177636
  }
177015
177637
  }
177016
- if ("$schema" in parsed && typeof parsed.$schema !== "string") {
177017
- diagnostics.push(configDiagnostic("config_invalid_value", "$schema", "`$schema` must be a string."));
177638
+ reportDuplicateKeys(root, "", diagnostics);
177639
+ const links = root.members.find((member) => member.key === "links");
177640
+ if (links === undefined) {
177641
+ return {
177642
+ ok: false,
177643
+ diagnostics: [...diagnostics, invalidValue("links", "present", root)]
177644
+ };
177018
177645
  }
177019
- const include = parsed.include;
177020
- if (!isPlainObject(include)) {
177021
- diagnostics.push(configDiagnostic("config_invalid_value", "include", "`include` must be an object with `code` and `docs`."));
177022
- return { config: EMPTY_CONFIG, diagnostics, ok: false };
177646
+ if (links.value.kind !== "array") {
177647
+ return {
177648
+ ok: false,
177649
+ diagnostics: [...diagnostics, invalidValue("links", "an array", links.value)]
177650
+ };
177023
177651
  }
177024
- for (const key of Object.keys(include)) {
177025
- if (!KNOWN_INCLUDE_KEYS.has(key)) {
177026
- diagnostics.push(configDiagnostic("config_unknown_key", `include.${key}`, `Unknown configuration key under \`include\`: ${key}`));
177652
+ const entries = [];
177653
+ for (const [index, element] of links.value.elements.entries()) {
177654
+ const outcome = readEntry(element, index);
177655
+ diagnostics.push(...outcome.diagnostics);
177656
+ if (outcome.entry !== undefined) {
177657
+ entries.push(outcome.entry);
177027
177658
  }
177028
177659
  }
177029
- const code = validateCodeInclude(include.code, diagnostics);
177030
- validatePatternArray(include.docs, "docs", ".md", false, diagnostics);
177031
- const ok = diagnostics.length === 0;
177032
- const config = ok ? { include: { code, docs: include.docs } } : EMPTY_CONFIG;
177033
- return { config, diagnostics, ok };
177034
- }
177035
- function validateCodeInclude(value, diagnostics) {
177036
- if (Array.isArray(value)) {
177037
- diagnostics.push(configDiagnostic("config_invalid_value", "include.code", '`include.code` must be a language-keyed object such as `{ "typescript": { "patterns": ["src/**/*.ts"] } }`, not an array.'));
177038
- return {};
177039
- }
177040
- if (!isPlainObject(value)) {
177041
- diagnostics.push(configDiagnostic("config_invalid_value", "include.code", "`include.code` must be a language-keyed object."));
177042
- return {};
177043
- }
177044
- const keys = Object.keys(value);
177045
- if (keys.length === 0) {
177046
- diagnostics.push(configDiagnostic("config_invalid_value", "include.code", "`include.code` must configure at least one language."));
177047
- return {};
177048
- }
177049
- const result = {};
177050
- for (const key of keys) {
177051
- if (!isCodeLanguage(key)) {
177052
- diagnostics.push(configDiagnostic("config_invalid_value", `include.code.${key}`, `Unknown code language: ${key}. Supported languages: ${KNOWN_CODE_LANGUAGES.join(", ")}.`));
177053
- continue;
177054
- }
177055
- const entry = validateCodeEntry(key, value[key], diagnostics);
177056
- if (entry !== undefined) {
177057
- result[key] = entry;
177058
- }
177660
+ if (diagnostics.some(isBlocking)) {
177661
+ return { ok: false, diagnostics };
177059
177662
  }
177060
- return result;
177663
+ return { ok: true, manifest: { entries }, diagnostics };
177061
177664
  }
177062
- function validateCodeEntry(language, value, diagnostics) {
177063
- const target = `include.code.${language}`;
177064
- if (!isPlainObject(value)) {
177065
- diagnostics.push(configDiagnostic("config_invalid_value", target, `\`${target}\` must be an object with a \`patterns\` array. Shorthand arrays are not supported.`));
177066
- return;
177067
- }
177068
- for (const key of Object.keys(value)) {
177069
- if (!KNOWN_CODE_ENTRY_KEYS.has(key)) {
177070
- diagnostics.push(configDiagnostic("config_unknown_key", `${target}.${key}`, `Unknown configuration key under \`${target}\`: ${key}`));
177071
- }
177072
- }
177073
- const before = diagnostics.length;
177074
- validatePatternArray(value.patterns, `code.${language}.patterns`, LANGUAGE_SUFFIX[language], language === "typescript", diagnostics);
177075
- if ("visibility" in value) {
177076
- if (!isStringArray(value.visibility)) {
177077
- diagnostics.push(configDiagnostic("config_invalid_value", `${target}.visibility`, `\`${target}.visibility\` must be an array of strings.`));
177078
- } else {
177079
- validateVisibilityOptions(language, value.visibility, target, diagnostics);
177080
- }
177081
- }
177082
- if (diagnostics.length !== before) {
177083
- return;
177665
+ function readEntry(element, index) {
177666
+ const path = `links[${index}]`;
177667
+ if (element.kind !== "object") {
177668
+ return { diagnostics: [invalidValue(path, "an object", element)] };
177084
177669
  }
177085
- const entry = { patterns: value.patterns };
177086
- if (isStringArray(value.visibility)) {
177087
- entry.visibility = value.visibility;
177670
+ const diagnostics = [];
177671
+ for (const member of element.members) {
177672
+ if (!KNOWN_ENTRY_KEYS.has(member.key)) {
177673
+ diagnostics.push(unknownKey(`${path}.${member.key}`, member));
177674
+ }
177675
+ }
177676
+ reportDuplicateKeys(element, `${path}.`, diagnostics);
177677
+ const code = readString(element, "code", path, diagnostics);
177678
+ const doc = readString(element, "doc", path, diagnostics);
177679
+ const note = readOptionalNote(element, path, diagnostics);
177680
+ if (code === undefined || doc === undefined || diagnostics.some(isBlocking)) {
177681
+ return { diagnostics };
177682
+ }
177683
+ const codeTarget = parseTarget(code, diagnostics);
177684
+ const docTarget = parseTarget(doc, diagnostics);
177685
+ if (codeTarget === undefined || docTarget === undefined) {
177686
+ return { diagnostics };
177687
+ }
177688
+ const entry = {
177689
+ code: codeTarget,
177690
+ doc: docTarget,
177691
+ codeEndpoint: code.value,
177692
+ docEndpoint: doc.value,
177693
+ location: positionOf(element.range),
177694
+ range: element.range,
177695
+ codeRange: code.contentRange,
177696
+ docRange: doc.contentRange
177697
+ };
177698
+ if (note !== undefined) {
177699
+ entry.note = note;
177088
177700
  }
177089
- return entry;
177701
+ return { entry, diagnostics };
177090
177702
  }
177091
- function validateVisibilityOptions(language, values, target, diagnostics) {
177092
- const allowed = LANGUAGE_VISIBILITY[language];
177093
- if (allowed.length === 0) {
177094
- diagnostics.push(configDiagnostic("config_invalid_value", `${target}.visibility`, `\`${target}.visibility\` is not supported for ${language}.`));
177703
+ function readString(entry, key, path, diagnostics) {
177704
+ const member = entry.members.find((candidate) => candidate.key === key);
177705
+ if (member === undefined) {
177706
+ diagnostics.push(invalidValue(`${path}.${key}`, "present", entry));
177095
177707
  return;
177096
177708
  }
177097
- for (const value of values) {
177098
- if (!allowed.includes(value)) {
177099
- diagnostics.push(configDiagnostic("config_invalid_value", `${target}.visibility`, `Unsupported ${language} visibility: ${value}. Supported values: ${allowed.join(", ")}.`));
177100
- }
177709
+ if (member.value.kind !== "string") {
177710
+ diagnostics.push(invalidValue(`${path}.${key}`, "a string", member.value));
177711
+ return;
177101
177712
  }
177713
+ return member.value;
177102
177714
  }
177103
- function validatePatternArray(value, field, requiredSuffix, excludeDeclarationFiles, diagnostics) {
177104
- const target = `include.${field}`;
177105
- if (!Array.isArray(value)) {
177106
- diagnostics.push(configDiagnostic("config_invalid_value", target, `\`${target}\` must be a non-empty array of strings.`));
177715
+ function readOptionalNote(entry, path, diagnostics) {
177716
+ const member = entry.members.find((candidate) => candidate.key === "note");
177717
+ if (member === undefined) {
177107
177718
  return;
177108
177719
  }
177109
- if (value.length === 0) {
177110
- diagnostics.push(configDiagnostic("config_invalid_value", target, `\`${target}\` must contain at least one pattern.`));
177720
+ if (member.value.kind !== "string") {
177721
+ diagnostics.push(invalidValue(`${path}.note`, "a string", member.value));
177111
177722
  return;
177112
177723
  }
177113
- for (const pattern of value) {
177114
- if (typeof pattern !== "string") {
177115
- diagnostics.push(configDiagnostic("config_invalid_value", target, `\`${target}\` patterns must be strings.`));
177116
- continue;
177117
- }
177118
- const globResult = validateGlobPattern(pattern);
177119
- if (!globResult.ok) {
177120
- diagnostics.push(configDiagnostic("config_invalid_value", pattern, `Invalid pattern in \`${target}\`: ${globResult.message}`));
177121
- continue;
177122
- }
177123
- const suffixError = checkSuffix(pattern, requiredSuffix, excludeDeclarationFiles);
177124
- if (suffixError !== undefined) {
177125
- diagnostics.push(configDiagnostic("config_invalid_value", pattern, suffixError));
177126
- }
177127
- }
177724
+ return member.value.value;
177128
177725
  }
177129
- function checkSuffix(pattern, requiredSuffix, excludeDeclarationFiles) {
177130
- if (!pattern.endsWith(requiredSuffix)) {
177131
- return `Pattern must end with \`${requiredSuffix}\`.`;
177132
- }
177133
- if (excludeDeclarationFiles && pattern.endsWith(".d.ts")) {
177134
- return "Pattern must not target `.d.ts` declaration files.";
177726
+ function parseTarget(node, diagnostics) {
177727
+ const result = parseLinkTarget(node.value, {
177728
+ location: positionOf(node.contentRange),
177729
+ targetRange: node.contentRange
177730
+ });
177731
+ if (result.ok) {
177732
+ return result.target;
177135
177733
  }
177734
+ diagnostics.push(result.diagnostic);
177136
177735
  return;
177137
177736
  }
177138
- function detectLanguageOverlap(projectRoot, code) {
177139
- const diagnostics = [];
177140
- for (const [relPath, owners] of codeFileOwners(projectRoot, code)) {
177141
- if (owners.length > 1) {
177142
- diagnostics.push(configDiagnostic("config_invalid_value", relPath, `Code file ${relPath} matches multiple configured languages (${owners.join(", ")}). Each code file must belong to exactly one language.`));
177737
+ function reportDuplicateKeys(object, pathPrefix, diagnostics) {
177738
+ const seen = new Set;
177739
+ for (const member of object.members) {
177740
+ if (seen.has(member.key)) {
177741
+ diagnostics.push(manifestDiagnostic("config_invalid_value", `Duplicate key ${pathPrefix}${member.key} in ${LINK_MANIFEST_FILE_NAME}.`, positionOf(member.keyRange), member.keyRange));
177143
177742
  }
177743
+ seen.add(member.key);
177144
177744
  }
177145
- return diagnostics;
177146
177745
  }
177147
- function configDiagnostic(code, target, message) {
177148
- return { severity: "error", code, target, message };
177746
+ function isBlocking(diagnostic) {
177747
+ return diagnostic.code !== "invalid_link_target";
177149
177748
  }
177150
- function isPlainObject(value) {
177151
- return typeof value === "object" && value !== null && !Array.isArray(value);
177749
+ function unknownKey(path, member) {
177750
+ return manifestDiagnostic("config_unknown_key", `Unknown key ${path} in ${LINK_MANIFEST_FILE_NAME}.`, positionOf(member.keyRange), member.keyRange);
177152
177751
  }
177153
- function isStringArray(value) {
177154
- return Array.isArray(value) && value.every((item) => typeof item === "string");
177155
- }
177156
-
177157
- // src/core/diagnostics.ts
177158
- function sortDiagnostics(diagnostics) {
177159
- return [...diagnostics].toSorted(compareDiagnostics);
177752
+ function invalidValue(path, expectation, node) {
177753
+ const subject = path === "" ? LINK_MANIFEST_FILE_NAME : path;
177754
+ return manifestDiagnostic("config_invalid_value", `${subject} must be ${expectation}.`, positionOf(node.range), node.range);
177160
177755
  }
177161
- function summarizeDiagnostics(diagnostics) {
177162
- const summary = { errors: 0, warnings: 0 };
177163
- for (const diagnostic of diagnostics) {
177164
- if (diagnostic.severity === "error") {
177165
- summary.errors += 1;
177166
- } else {
177167
- summary.warnings += 1;
177168
- }
177756
+ function manifestDiagnostic(code, message, position, range) {
177757
+ const diagnostic = {
177758
+ severity: "error",
177759
+ code,
177760
+ target: LINK_MANIFEST_FILE_NAME,
177761
+ message
177762
+ };
177763
+ if (position !== undefined) {
177764
+ diagnostic.location = {
177765
+ filePath: LINK_MANIFEST_FILE_NAME,
177766
+ line: position.line,
177767
+ column: position.column
177768
+ };
177169
177769
  }
177170
- return summary;
177171
- }
177172
- function formatDiagnostic(diagnostic) {
177173
- const location = diagnostic.location;
177174
- if (location === undefined) {
177175
- return `${diagnostic.target} ${diagnostic.severity} ${diagnostic.code} - ${diagnostic.message}`;
177770
+ if (range !== undefined) {
177771
+ diagnostic.range = range;
177176
177772
  }
177177
- const prefix = `${location.filePath}:${location.line}:${location.column}`;
177178
- return `${prefix} ${diagnostic.severity} ${diagnostic.code} ${diagnostic.target} - ${diagnostic.message}`;
177773
+ return diagnostic;
177179
177774
  }
177180
- function formatSummary(summary) {
177181
- return `Summary: ${summary.errors} ${pluralize("error", summary.errors)}, ${summary.warnings} ${pluralize("warning", summary.warnings)}`;
177775
+ function positionOf(range) {
177776
+ return {
177777
+ filePath: LINK_MANIFEST_FILE_NAME,
177778
+ line: range.start.line,
177779
+ column: range.start.column
177780
+ };
177182
177781
  }
177183
- function compareDiagnostics(left, right) {
177184
- const leftLocation = left.location;
177185
- const rightLocation = right.location;
177186
- if (leftLocation === undefined && rightLocation !== undefined) {
177187
- return -1;
177188
- }
177189
- if (leftLocation !== undefined && rightLocation === undefined) {
177190
- return 1;
177782
+
177783
+ // src/core/suggest.ts
177784
+ function nearestMatch(input, candidates) {
177785
+ const ranked = candidates.map((candidate, index) => ({
177786
+ candidate,
177787
+ distance: editDistance(input, candidate),
177788
+ index
177789
+ })).toSorted((left, right) => left.distance - right.distance || left.index - right.index);
177790
+ const abbreviation = ranked.find(({ candidate }) => isOrderedAbbreviation(input, candidate));
177791
+ if (abbreviation !== undefined) {
177792
+ return abbreviation.candidate;
177191
177793
  }
177192
- if (leftLocation !== undefined && rightLocation !== undefined) {
177193
- return compareString(leftLocation.filePath, rightLocation.filePath) || compareNumber(leftLocation.line, rightLocation.line) || compareNumber(leftLocation.column, rightLocation.column) || compareString(left.code, right.code) || compareString(left.target, right.target);
177794
+ const best = ranked[0];
177795
+ if (best === undefined) {
177796
+ return;
177194
177797
  }
177195
- return compareString(left.code, right.code) || compareString(left.target, right.target);
177196
- }
177197
- function compareString(left, right) {
177198
- return left.localeCompare(right);
177798
+ const closeEnough = best.distance <= Math.max(1, Math.floor(best.candidate.length / 2));
177799
+ return closeEnough ? best.candidate : undefined;
177199
177800
  }
177200
- function compareNumber(left, right) {
177201
- return left - right;
177801
+ function isOrderedAbbreviation(input, candidate) {
177802
+ if (input.length < 3 || input.length >= candidate.length) {
177803
+ return false;
177804
+ }
177805
+ let candidateIndex = 0;
177806
+ for (const character of input) {
177807
+ const matchIndex = candidate.indexOf(character, candidateIndex);
177808
+ if (matchIndex === -1) {
177809
+ return false;
177810
+ }
177811
+ candidateIndex = matchIndex + 1;
177812
+ }
177813
+ return true;
177202
177814
  }
177203
- function pluralize(word, count) {
177204
- return count === 1 ? word : `${word}s`;
177815
+ function editDistance(left, right) {
177816
+ let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
177817
+ for (let leftIndex = 1;leftIndex <= left.length; leftIndex += 1) {
177818
+ const current = [leftIndex];
177819
+ for (let rightIndex = 1;rightIndex <= right.length; rightIndex += 1) {
177820
+ const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
177821
+ current[rightIndex] = Math.min((current[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + substitutionCost);
177822
+ }
177823
+ previous = current;
177824
+ }
177825
+ return previous[right.length] ?? 0;
177205
177826
  }
177206
177827
 
177207
- // src/core/endpoint.ts
177208
- function filePathOf(endpoint) {
177209
- const hashIndex = endpoint.indexOf("#");
177210
- return hashIndex === -1 ? endpoint : endpoint.slice(0, hashIndex);
177211
- }
177212
- function fragmentOf(endpoint) {
177213
- const hashIndex = endpoint.indexOf("#");
177214
- return hashIndex === -1 ? endpoint : endpoint.slice(hashIndex + 1);
177215
- }
177216
- function compareEndpointOrder(left, right) {
177217
- return left.filePath.localeCompare(right.filePath) || left.line - right.line || left.column - right.column || left.endpoint.localeCompare(right.endpoint);
177218
- }
177219
- function endpointRange(element) {
177220
- const range = element.kind === "code" ? element.nameRange : element.headingTextRange;
177221
- if (range !== undefined) {
177222
- return range;
177828
+ // src/core/link-manifest-apply.ts
177829
+ function applyLinkManifest(input) {
177830
+ if (input.manifest.entries.length === 0) {
177831
+ return { codeFiles: input.codeFiles, docFiles: input.docFiles, diagnostics: [] };
177223
177832
  }
177224
- const { line, column } = element.location;
177225
- return { start: { line, column }, end: { line, column } };
177833
+ const state = new ApplyState(input);
177834
+ for (const entry of input.manifest.entries) {
177835
+ state.applyEntry(entry);
177836
+ }
177837
+ return state.result();
177226
177838
  }
177227
177839
 
177228
- // src/core/graph.ts
177229
- function buildLinkGraph(codeFiles, docFiles) {
177230
- const codeByEndpoint = new Map;
177231
- for (const file of codeFiles) {
177232
- for (const symbol of file.symbols) {
177233
- if (!codeByEndpoint.has(symbol.endpoint)) {
177234
- codeByEndpoint.set(symbol.endpoint, symbol);
177235
- }
177840
+ class ApplyState {
177841
+ erroredFiles;
177842
+ codeStates = new Map;
177843
+ docStates = new Map;
177844
+ codeOrder;
177845
+ docOrder;
177846
+ diagnostics = [];
177847
+ constructor(input) {
177848
+ this.erroredFiles = collectErroredFiles(input.scanDiagnostics);
177849
+ this.codeOrder = input.codeFiles;
177850
+ this.docOrder = input.docFiles;
177851
+ for (const file of input.codeFiles) {
177852
+ const byEndpoint = new Map;
177853
+ for (const symbol of [...file.symbols, ...file.undocumentedSymbols]) {
177854
+ if (!byEndpoint.has(symbol.endpoint)) {
177855
+ byEndpoint.set(symbol.endpoint, symbol);
177856
+ }
177857
+ }
177858
+ this.codeStates.set(file.filePath, {
177859
+ file,
177860
+ symbols: file.symbols,
177861
+ undocumentedSymbols: file.undocumentedSymbols,
177862
+ links: file.links,
177863
+ byEndpoint,
177864
+ outgoing: new Set(file.links.map((annotation) => pairKey(annotation.source, annotation.target))),
177865
+ changed: false
177866
+ });
177867
+ }
177868
+ for (const file of input.docFiles) {
177869
+ this.docStates.set(file.filePath, {
177870
+ file,
177871
+ headings: file.headings,
177872
+ links: file.links,
177873
+ anchors: new Set(file.anchors.map((anchor) => anchor.endpoint)),
177874
+ outgoing: new Set(file.links.map((annotation) => pairKey(annotation.source, annotation.target))),
177875
+ changed: false
177876
+ });
177236
177877
  }
177237
177878
  }
177238
- const docByEndpoint = new Map;
177239
- for (const file of docFiles) {
177240
- for (const anchor of file.anchors) {
177241
- if (!docByEndpoint.has(anchor.endpoint)) {
177242
- docByEndpoint.set(anchor.endpoint, anchor);
177243
- }
177879
+ applyEntry(entry) {
177880
+ if (this.erroredFiles.has(entry.code.filePath) || this.erroredFiles.has(entry.doc.filePath)) {
177881
+ return;
177882
+ }
177883
+ const codeResolved = this.resolveCodeSide(entry);
177884
+ const docResolved = this.resolveDocSide(entry);
177885
+ if (!codeResolved || !docResolved) {
177886
+ return;
177244
177887
  }
177888
+ this.addLinks(entry);
177245
177889
  }
177246
- const counterparts = new Map;
177247
- const addEdge = (a, b) => {
177248
- addTo(counterparts, a, b);
177249
- addTo(counterparts, b, a);
177250
- };
177251
- for (const file of codeFiles) {
177252
- for (const link of file.links) {
177253
- if (codeByEndpoint.has(link.source) && docByEndpoint.has(link.target)) {
177254
- addEdge(link.source, link.target);
177255
- }
177890
+ resolveCodeSide(entry) {
177891
+ const state = this.codeStates.get(entry.code.filePath);
177892
+ if (state === undefined) {
177893
+ this.report("code_file_not_found", entry.codeEndpoint, `Code file ${entry.code.filePath} declared in the link manifest is not in the managed code set.`, entry, "code");
177894
+ return false;
177895
+ }
177896
+ const symbol = state.byEndpoint.get(entry.codeEndpoint);
177897
+ if (symbol === undefined) {
177898
+ this.report("code_symbol_not_found", entry.codeEndpoint, this.symbolNotFoundMessage(entry, state), entry, "code", state.file.language);
177899
+ return false;
177256
177900
  }
177901
+ this.markDocumented(state, symbol);
177902
+ return true;
177257
177903
  }
177258
- for (const file of docFiles) {
177259
- for (const link of file.links) {
177260
- if (docByEndpoint.has(link.source) && codeByEndpoint.has(link.target)) {
177261
- addEdge(link.source, link.target);
177262
- }
177904
+ resolveDocSide(entry) {
177905
+ const state = this.docStates.get(entry.doc.filePath);
177906
+ if (state === undefined) {
177907
+ this.report("doc_file_not_found", entry.docEndpoint, `Doc file ${entry.doc.filePath} declared in the link manifest is not in the managed docs set.`, entry, "doc");
177908
+ return false;
177909
+ }
177910
+ if (!state.anchors.has(entry.docEndpoint)) {
177911
+ this.report("doc_anchor_not_found", entry.docEndpoint, `Doc anchor ${entry.docEndpoint} declared in the link manifest does not exist.`, entry, "doc");
177912
+ return false;
177263
177913
  }
177914
+ this.markAnnotated(state, entry.docEndpoint);
177915
+ return true;
177264
177916
  }
177265
- return { codeByEndpoint, docByEndpoint, counterparts };
177266
- }
177267
- function counterpartsOf(graph, endpoint) {
177268
- const targets = graph.counterparts.get(endpoint);
177269
- if (targets === undefined) {
177270
- return [];
177917
+ addLinks(entry) {
177918
+ const codeState = this.codeStates.get(entry.code.filePath);
177919
+ const docState = this.docStates.get(entry.doc.filePath);
177920
+ if (codeState === undefined || docState === undefined) {
177921
+ return;
177922
+ }
177923
+ const forward = pairKey(entry.codeEndpoint, entry.docEndpoint);
177924
+ const backward = pairKey(entry.docEndpoint, entry.codeEndpoint);
177925
+ const hasForward = codeState.outgoing.has(forward);
177926
+ const hasBackward = docState.outgoing.has(backward);
177927
+ if (hasForward || hasBackward) {
177928
+ this.diagnostics.push({
177929
+ severity: "warning",
177930
+ code: "duplicate_link",
177931
+ source: entry.codeEndpoint,
177932
+ target: entry.docEndpoint,
177933
+ message: `Link ${entry.codeEndpoint} to ${entry.docEndpoint} is already declared.`,
177934
+ location: entry.location,
177935
+ range: entry.range
177936
+ });
177937
+ }
177938
+ if (!hasForward) {
177939
+ codeState.links = [
177940
+ ...codeState.links,
177941
+ manifestLink(entry.codeEndpoint, entry.docEndpoint, entry, "doc")
177942
+ ];
177943
+ codeState.outgoing.add(forward);
177944
+ codeState.changed = true;
177945
+ }
177946
+ if (!hasBackward) {
177947
+ docState.links = [
177948
+ ...docState.links,
177949
+ manifestLink(entry.docEndpoint, entry.codeEndpoint, entry, "code")
177950
+ ];
177951
+ docState.outgoing.add(backward);
177952
+ docState.changed = true;
177953
+ }
177271
177954
  }
177272
- const resolved = [];
177273
- for (const target of targets) {
177274
- const code = graph.codeByEndpoint.get(target);
177275
- if (code !== undefined) {
177276
- resolved.push(code);
177277
- continue;
177955
+ markDocumented(state, symbol) {
177956
+ if (!state.undocumentedSymbols.includes(symbol)) {
177957
+ return;
177278
177958
  }
177279
- const doc = graph.docByEndpoint.get(target);
177280
- if (doc !== undefined) {
177281
- resolved.push(doc);
177959
+ state.undocumentedSymbols = state.undocumentedSymbols.filter((candidate) => candidate !== symbol);
177960
+ state.symbols = [...state.symbols, symbol];
177961
+ state.changed = true;
177962
+ }
177963
+ markAnnotated(state, endpoint) {
177964
+ const index = state.headings.findIndex((heading2) => heading2.anchor?.endpoint === endpoint);
177965
+ const heading = state.headings[index];
177966
+ if (heading === undefined || heading.hasCodeAnnotation) {
177967
+ return;
177282
177968
  }
177969
+ state.headings = state.headings.toSpliced(index, 1, {
177970
+ ...heading,
177971
+ hasCodeAnnotation: true
177972
+ });
177973
+ state.changed = true;
177283
177974
  }
177284
- return resolved.toSorted(compareEndpoints);
177285
- }
177286
- function addTo(map, key, value) {
177287
- const existing = map.get(key);
177288
- if (existing === undefined) {
177289
- map.set(key, new Set([value]));
177290
- return;
177975
+ symbolNotFoundMessage(entry, state) {
177976
+ const base = `Code symbol ${entry.codeEndpoint} declared in the link manifest does not exist.`;
177977
+ const candidates = [...state.byEndpoint.values()].map((symbol) => symbol.canonicalId);
177978
+ const suggestion = nearestMatch(entry.code.fragment, candidates);
177979
+ return suggestion === undefined ? base : `${base} Did you mean \`${suggestion}\`?`;
177980
+ }
177981
+ report(code, target, message, entry, side, language) {
177982
+ const range = side === "code" ? entry.codeRange : entry.docRange;
177983
+ const diagnostic = {
177984
+ severity: "error",
177985
+ code,
177986
+ target,
177987
+ message,
177988
+ location: { ...entry.location, line: range.start.line, column: range.start.column },
177989
+ range
177990
+ };
177991
+ if (language !== undefined) {
177992
+ diagnostic.language = language;
177993
+ }
177994
+ this.diagnostics.push(diagnostic);
177995
+ }
177996
+ result() {
177997
+ const codeFiles = this.codeOrder.map((file) => {
177998
+ const state = this.codeStates.get(file.filePath);
177999
+ if (state === undefined || !state.changed) {
178000
+ return file;
178001
+ }
178002
+ return {
178003
+ ...file,
178004
+ symbols: state.symbols,
178005
+ undocumentedSymbols: state.undocumentedSymbols,
178006
+ links: state.links
178007
+ };
178008
+ });
178009
+ const docFiles = this.docOrder.map((file) => {
178010
+ const state = this.docStates.get(file.filePath);
178011
+ if (state === undefined || !state.changed) {
178012
+ return file;
178013
+ }
178014
+ return { ...file, headings: state.headings, links: state.links };
178015
+ });
178016
+ return { codeFiles, docFiles, diagnostics: this.diagnostics };
177291
178017
  }
177292
- existing.add(value);
177293
178018
  }
177294
- function compareEndpoints(left, right) {
177295
- return compareEndpointOrder({ ...left.location, endpoint: left.endpoint }, { ...right.location, endpoint: right.endpoint });
178019
+ function pairKey(source, target) {
178020
+ return `${source} -> ${target}`;
178021
+ }
178022
+ function manifestLink(source, target, entry, side) {
178023
+ return {
178024
+ source,
178025
+ target,
178026
+ location: entry.location,
178027
+ targetRange: side === "code" ? entry.codeRange : entry.docRange
178028
+ };
177296
178029
  }
177297
178030
 
177298
178031
  // src/core/markdown-syntax.ts
@@ -177565,10 +178298,17 @@ function scanProject(options) {
177565
178298
  if (!configResult.ok) {
177566
178299
  return { ok: false, diagnostics: configResult.diagnostics };
177567
178300
  }
178301
+ const manifestResult = loadLinkManifest(options.projectRoot);
178302
+ if (!manifestResult.ok) {
178303
+ return { ok: false, diagnostics: [...configResult.diagnostics, ...manifestResult.diagnostics] };
178304
+ }
177568
178305
  const collectCode = options.collectCode ?? collectCodeFiles;
177569
178306
  const collectDocs = options.collectDocs ?? collectFiles;
177570
178307
  const readFile = options.readFile ?? ((relPath) => readManagedFile(options.projectRoot, relPath));
177571
- const diagnostics = [...configResult.diagnostics];
178308
+ const diagnostics = [
178309
+ ...configResult.diagnostics,
178310
+ ...manifestResult.diagnostics
178311
+ ];
177572
178312
  const contentByFile = options.keepContent ? new Map : undefined;
177573
178313
  const codeScan = scanCodeFiles(options.projectRoot, collectCode(options.projectRoot, configResult.config.include.code), configResult.config.include.code, readFile, contentByFile === undefined ? undefined : (relPath, content) => contentByFile.set(relPath, content));
177574
178314
  diagnostics.push(...codeScan.diagnostics);
@@ -177584,17 +178324,24 @@ function scanProject(options) {
177584
178324
  diagnostics.push(...scan2.diagnostics);
177585
178325
  docFiles.push(scan2);
177586
178326
  }
177587
- const codeFiles = codeScan.codeFiles;
178327
+ const applied = applyLinkManifest({
178328
+ manifest: manifestResult.manifest,
178329
+ codeFiles: codeScan.codeFiles,
178330
+ docFiles,
178331
+ scanDiagnostics: diagnostics
178332
+ });
178333
+ diagnostics.push(...applied.diagnostics);
178334
+ const codeFiles = applied.codeFiles;
177588
178335
  const scan = {
177589
178336
  codeFiles,
177590
- docFiles,
178337
+ docFiles: applied.docFiles,
177591
178338
  diagnostics
177592
178339
  };
177593
178340
  if (contentByFile !== undefined) {
177594
178341
  scan.contentByFile = contentByFile;
177595
178342
  }
177596
178343
  if (options.buildGraph === true) {
177597
- scan.graph = buildLinkGraph(codeFiles, docFiles);
178344
+ scan.graph = buildLinkGraph(codeFiles, applied.docFiles);
177598
178345
  }
177599
178346
  return {
177600
178347
  ok: true,
@@ -177743,7 +178490,7 @@ function resolveLinks(input) {
177743
178490
  continue;
177744
178491
  }
177745
178492
  for (const link of file.links) {
177746
- docToCodePairs.add(pairKey(link.source, link.target));
178493
+ docToCodePairs.add(pairKey2(link.source, link.target));
177747
178494
  }
177748
178495
  }
177749
178496
  const codeToDocPairs = new Set;
@@ -177752,7 +178499,7 @@ function resolveLinks(input) {
177752
178499
  continue;
177753
178500
  }
177754
178501
  for (const link of file.links) {
177755
- codeToDocPairs.add(pairKey(link.source, link.target));
178502
+ codeToDocPairs.add(pairKey2(link.source, link.target));
177756
178503
  }
177757
178504
  }
177758
178505
  for (const file of input.codeFiles) {
@@ -177773,7 +178520,7 @@ function resolveLinks(input) {
177773
178520
  diagnostics.push(relationshipDiagnostic("doc_anchor_not_found", link.source, docEndpoint, `Doc anchor ${docEndpoint} referenced by ${link.source} does not exist.`, link));
177774
178521
  continue;
177775
178522
  }
177776
- if (!docToCodePairs.has(pairKey(docEndpoint, link.source))) {
178523
+ if (!docToCodePairs.has(pairKey2(docEndpoint, link.source))) {
177777
178524
  diagnostics.push(relationshipDiagnostic("doc_backlink_not_found", link.source, docEndpoint, `Doc anchor ${docEndpoint} has no matching @code backlink to ${link.source}.`, link));
177778
178525
  }
177779
178526
  }
@@ -177792,7 +178539,7 @@ function resolveLinks(input) {
177792
178539
  diagnostics.push(relationshipDiagnostic("code_file_not_found", link.source, codeEndpoint, `Code file ${codeFilePath} referenced by ${link.source} is not in the managed code set.`, link));
177793
178540
  continue;
177794
178541
  }
177795
- if (!codeToDocPairs.has(pairKey(codeEndpoint, link.source))) {
178542
+ if (!codeToDocPairs.has(pairKey2(codeEndpoint, link.source))) {
177796
178543
  diagnostics.push(relationshipDiagnostic("code_backlink_not_found", link.source, codeEndpoint, `Code endpoint ${codeEndpoint} has no matching @doc pair back to ${link.source}.`, link));
177797
178544
  }
177798
178545
  }
@@ -177827,6 +178574,9 @@ function auditUndocumentedSymbols(input, erroredFiles) {
177827
178574
  continue;
177828
178575
  }
177829
178576
  for (const symbol of file.undocumentedSymbols) {
178577
+ if (symbol.isMember === true) {
178578
+ continue;
178579
+ }
177830
178580
  diagnostics.push({
177831
178581
  severity: "warning",
177832
178582
  code: "undocumented_symbol",
@@ -177898,19 +178648,7 @@ function unlinkedDocSectionDiagnostic(node, anchor) {
177898
178648
  }
177899
178649
  return diagnostic;
177900
178650
  }
177901
- function collectErroredFiles(diagnostics) {
177902
- const errored = new Set;
177903
- for (const diagnostic of diagnostics) {
177904
- if (diagnostic.code === "file_read_error" || diagnostic.code === "code_parse_error" || isFileScopedScannerDiagnostic(diagnostic)) {
177905
- errored.add(diagnostic.target);
177906
- }
177907
- }
177908
- return errored;
177909
- }
177910
- function isFileScopedScannerDiagnostic(diagnostic) {
177911
- return (diagnostic.code === "code_scanner_unavailable" || diagnostic.code === "code_scanner_failed") && diagnostic.language !== undefined && diagnostic.target !== diagnostic.language;
177912
- }
177913
- function pairKey(source, target) {
178651
+ function pairKey2(source, target) {
177914
178652
  return `${source}->${target}`;
177915
178653
  }
177916
178654
  function relationshipDiagnostic(code, source, target, message, link) {
@@ -178498,8 +179236,33 @@ function comparePairs(left, right) {
178498
179236
  return left.docEndpoint.localeCompare(right.docEndpoint) || left.codeEndpoint.localeCompare(right.codeEndpoint);
178499
179237
  }
178500
179238
 
179239
+ // src/core/package-root.ts
179240
+ import { existsSync as existsSync2, realpathSync as realpathSync2 } from "node:fs";
179241
+ import { dirname as dirname3, join as join6 } from "node:path";
179242
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
179243
+ function resolvePackageRoot(moduleUrl = import.meta.url) {
179244
+ const modulePath = fileURLToPath2(moduleUrl);
179245
+ let resolved;
179246
+ try {
179247
+ resolved = realpathSync2(modulePath);
179248
+ } catch {
179249
+ resolved = modulePath;
179250
+ }
179251
+ let dir = dirname3(resolved);
179252
+ for (;; ) {
179253
+ if (existsSync2(join6(dir, "templates", "skills"))) {
179254
+ return dir;
179255
+ }
179256
+ const parent = dirname3(dir);
179257
+ if (parent === dir) {
179258
+ return dir;
179259
+ }
179260
+ dir = parent;
179261
+ }
179262
+ }
179263
+
178501
179264
  // src/lsp/server.ts
178502
- import { fileURLToPath as fileURLToPath3 } from "node:url";
179265
+ import { fileURLToPath as fileURLToPath4 } from "node:url";
178503
179266
 
178504
179267
  // src/lsp/position.ts
178505
179268
  function toLspPosition(position) {
@@ -178677,11 +179440,11 @@ function counterpartLocators(state, filePath, position) {
178677
179440
 
178678
179441
  // src/lsp/paths.ts
178679
179442
  import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep } from "node:path";
178680
- import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
179443
+ import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
178681
179444
  function uriToRelativePath(projectRoot, uri) {
178682
179445
  let absolute;
178683
179446
  try {
178684
- absolute = fileURLToPath2(uri);
179447
+ absolute = fileURLToPath3(uri);
178685
179448
  } catch {
178686
179449
  return;
178687
179450
  }
@@ -179022,523 +179785,380 @@ class Server {
179022
179785
  }
179023
179786
  scheduleFlush() {
179024
179787
  this.dirty = true;
179025
- this.clearTimer();
179026
- this.timer = setTimeout(() => {
179027
- this.timer = null;
179028
- this.flush();
179029
- }, this.debounceMs);
179030
- }
179031
- flush() {
179032
- this.clearTimer();
179033
- if (this.project === null || !this.dirty) {
179034
- return;
179035
- }
179036
- this.dirty = false;
179037
- const state = this.project.resolve();
179038
- for (const rel of this.openFiles) {
179039
- const content = state.contentByFile.get(rel) ?? "";
179040
- this.publish(rel, diagnosticsForFile(state.diagnostics, rel, content));
179041
- }
179042
- }
179043
- publish(rel, diagnostics) {
179044
- if (this.project === null) {
179045
- return;
179046
- }
179047
- this.send({
179048
- jsonrpc: "2.0",
179049
- method: "textDocument/publishDiagnostics",
179050
- params: { uri: relativePathToUri(this.project.root, rel), diagnostics }
179051
- });
179052
- }
179053
- relPath(uri) {
179054
- if (uri === undefined || this.project === null) {
179055
- return;
179056
- }
179057
- return uriToRelativePath(this.project.root, uri);
179058
- }
179059
- respond(id, result) {
179060
- if (id === undefined) {
179061
- return;
179062
- }
179063
- this.send({ jsonrpc: "2.0", id, result });
179064
- }
179065
- clearTimer() {
179066
- if (this.timer !== null) {
179067
- clearTimeout(this.timer);
179068
- this.timer = null;
179069
- }
179070
- }
179071
- }
179072
- function resolveRoot(params) {
179073
- const record = params;
179074
- const folderUri = record?.workspaceFolders?.[0]?.uri;
179075
- if (typeof folderUri === "string") {
179076
- const path = uriToPath(folderUri);
179077
- if (path !== undefined) {
179078
- return path;
179079
- }
179080
- }
179081
- if (typeof record?.rootUri === "string") {
179082
- const path = uriToPath(record.rootUri);
179083
- if (path !== undefined) {
179084
- return path;
179085
- }
179086
- }
179087
- if (typeof record?.rootPath === "string") {
179088
- return record.rootPath;
179089
- }
179090
- return process.cwd();
179091
- }
179092
- function runLspServer() {
179093
- const reader = new MessageReader;
179094
- const server2 = new Server((message) => {
179095
- process.stdout.write(encodeMessage(message));
179096
- });
179097
- process.stdin.on("data", (chunk) => {
179098
- reader.append(chunk);
179099
- for (const message of reader.read()) {
179100
- server2.handle(message);
179101
- }
179102
- });
179103
- process.stdin.resume();
179104
- }
179105
- function uriToPath(uri) {
179106
- try {
179107
- return fileURLToPath3(uri);
179108
- } catch {
179109
- return;
179110
- }
179111
- }
179112
- function toLocation(root, locator) {
179113
- return {
179114
- uri: relativePathToUri(root, locator.filePath),
179115
- range: toLspRange(locator.range)
179116
- };
179117
- }
179118
- function textDocument(params) {
179119
- const record = params;
179120
- const doc = record?.textDocument;
179121
- if (doc === undefined) {
179122
- return;
179123
- }
179124
- const result = {};
179125
- if (typeof doc.uri === "string") {
179126
- result.uri = doc.uri;
179127
- }
179128
- if (typeof doc.text === "string") {
179129
- result.text = doc.text;
179130
- }
179131
- return result;
179132
- }
179133
- function fullChangeText(changes) {
179134
- if (!Array.isArray(changes) || changes.length === 0) {
179135
- return;
179136
- }
179137
- const last = changes[changes.length - 1];
179138
- return typeof last.text === "string" ? last.text : undefined;
179139
- }
179140
-
179141
- // src/cli/docs.ts
179142
- import { readdirSync as readdirSync3, readFileSync as readFileSync4 } from "node:fs";
179143
- import { join as join6 } from "node:path";
179144
-
179145
- // src/core/init-plan.ts
179146
- import { existsSync as existsSync2, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, realpathSync as realpathSync2 } from "node:fs";
179147
- import { dirname as dirname3, join as join5, relative as relative3 } from "node:path";
179148
- import { fileURLToPath as fileURLToPath4 } from "node:url";
179149
- var CONFIG_FILE_NAME2 = "docbridge.config.json";
179150
- var INIT_SKILL_NAMES = ["docbridge"];
179151
- var LEGACY_SKILL_NAMES = [
179152
- "docbridge-adopt",
179153
- "docbridge-annotate",
179154
- "docbridge-link",
179155
- "docbridge-review",
179156
- "docbridge-sync"
179157
- ];
179158
- function planInitCommand(input) {
179159
- const packageRoot = input.packageRoot ?? resolvePackageRoot();
179160
- const existingConfig = readExistingConfig(input.projectRoot);
179161
- const messages = [];
179162
- const nextSteps = [];
179163
- let exitCode = 0;
179164
- const agentResolution = input.command === "init" ? resolveInitAgentTarget(input.discovery, input.options) : resolveInitWithAgentTarget(input.discovery, input.options);
179165
- if (agentResolution.error !== undefined) {
179166
- return {
179167
- command: input.command,
179168
- configOps: [],
179169
- skillOps: [],
179170
- messages: [agentResolution.error],
179171
- nextSteps: [],
179172
- agentGuidance: [],
179173
- exitCode: 1
179174
- };
179175
- }
179176
- const agentTarget = agentResolution.target ?? "none";
179177
- const configOps = input.command === "init" ? planConfigOperations({
179178
- projectRoot: input.projectRoot,
179179
- options: input.options,
179180
- discovery: input.discovery,
179181
- confirmedScope: input.confirmedScope,
179182
- existingConfig,
179183
- messages
179184
- }) : [];
179185
- if (configOps.some((operation) => operation.action === "create" || operation.action === "would-create")) {
179186
- nextSteps.push("Review docbridge.config.json and adjust include.docs or include.code if needed.");
179187
- nextSteps.push("Add @doc and @code annotations to link code and documentation.");
179188
- nextSteps.push("Run docbridge check when links exist.");
179189
- } else if (existingConfig.ok) {
179190
- messages.push("Existing docbridge.config.json was left unchanged.");
179191
- summarizeExistingConfig(existingConfig.config, messages);
179192
- } else if (existingConfig.rawText !== undefined) {
179193
- messages.push("Existing docbridge.config.json is invalid and was not modified.");
179194
- for (const diagnostic of existingConfig.diagnostics) {
179195
- messages.push(diagnostic.message);
179196
- }
179197
- messages.push("Repair the config manually or delete it before re-running init.");
179198
- exitCode = 1;
179199
- }
179200
- const skillOps = planSkillOperations({
179201
- projectRoot: input.projectRoot,
179202
- packageRoot,
179203
- agentTarget,
179204
- options: input.options,
179205
- messages
179206
- });
179207
- const agentGuidance = input.command === "init-with-agent" && agentTarget !== "none" ? buildAgentGuidance(input.projectRoot, agentTarget) : [];
179208
- if (input.command === "init-with-agent") {
179209
- nextSteps.push("Run the printed one-shot command or fallback prompt in your agent.");
179210
- nextSteps.push("Let the docbridge skill confirm scope and create or improve docbridge.config.json.");
179211
- }
179212
- if (input.options.yes && input.command === "init" && configOps.length === 0 && existingConfig.rawText === undefined && (input.discovery.docs.ambiguous || input.discovery.code.ambiguous)) {
179213
- exitCode = 1;
179214
- }
179215
- return {
179216
- command: input.command,
179217
- configOps,
179218
- skillOps,
179219
- messages,
179220
- nextSteps,
179221
- agentGuidance,
179222
- exitCode
179223
- };
179224
- }
179225
- function buildConfigFromScope(scope) {
179226
- const code = {};
179227
- for (const language of scope.languages) {
179228
- code[language.language] = { patterns: [...language.patterns] };
179229
- }
179230
- return {
179231
- include: {
179232
- code,
179233
- docs: [scope.docsPattern]
179234
- }
179235
- };
179236
- }
179237
- function resolvePackageRoot(moduleUrl = import.meta.url) {
179238
- const modulePath = fileURLToPath4(moduleUrl);
179239
- let resolved;
179240
- try {
179241
- resolved = realpathSync2(modulePath);
179242
- } catch {
179243
- resolved = modulePath;
179788
+ this.clearTimer();
179789
+ this.timer = setTimeout(() => {
179790
+ this.timer = null;
179791
+ this.flush();
179792
+ }, this.debounceMs);
179244
179793
  }
179245
- let dir = dirname3(resolved);
179246
- for (;; ) {
179247
- if (existsSync2(join5(dir, "templates", "skills"))) {
179248
- return dir;
179794
+ flush() {
179795
+ this.clearTimer();
179796
+ if (this.project === null || !this.dirty) {
179797
+ return;
179249
179798
  }
179250
- const parent = dirname3(dir);
179251
- if (parent === dir) {
179252
- return dir;
179799
+ this.dirty = false;
179800
+ const state = this.project.resolve();
179801
+ for (const rel of this.openFiles) {
179802
+ const content = state.contentByFile.get(rel) ?? "";
179803
+ this.publish(rel, diagnosticsForFile(state.diagnostics, rel, content));
179253
179804
  }
179254
- dir = parent;
179255
- }
179256
- }
179257
- function listDistributableSkills(packageRoot) {
179258
- const skillsRoot = join5(packageRoot, "templates", "skills");
179259
- let entries;
179260
- try {
179261
- entries = readdirSync2(skillsRoot);
179262
- } catch {
179263
- return [];
179264
- }
179265
- return entries.filter((entry) => entry.startsWith("docbridge")).filter((entry) => existsSync2(join5(skillsRoot, entry, "SKILL.md"))).toSorted();
179266
- }
179267
- function formatInitPlan(plan) {
179268
- const lines = [];
179269
- for (const message of plan.messages) {
179270
- lines.push(message);
179271
179805
  }
179272
- if (plan.configOps.length > 0) {
179273
- lines.push("");
179274
- lines.push("Config:");
179275
- for (const operation of plan.configOps) {
179276
- lines.push(formatFileOp(operation));
179277
- if (operation.content !== undefined) {
179278
- lines.push(operation.content);
179279
- }
179806
+ publish(rel, diagnostics) {
179807
+ if (this.project === null) {
179808
+ return;
179280
179809
  }
179810
+ this.send({
179811
+ jsonrpc: "2.0",
179812
+ method: "textDocument/publishDiagnostics",
179813
+ params: { uri: relativePathToUri(this.project.root, rel), diagnostics }
179814
+ });
179281
179815
  }
179282
- if (plan.skillOps.length > 0) {
179283
- lines.push("");
179284
- lines.push("Skills:");
179285
- for (const operation of plan.skillOps) {
179286
- lines.push(formatFileOp(operation));
179816
+ relPath(uri) {
179817
+ if (uri === undefined || this.project === null) {
179818
+ return;
179287
179819
  }
179820
+ return uriToRelativePath(this.project.root, uri);
179288
179821
  }
179289
- if (plan.agentGuidance.length > 0) {
179290
- lines.push("");
179291
- lines.push("Agent setup:");
179292
- for (const guidance of plan.agentGuidance) {
179293
- lines.push("");
179294
- lines.push(`${capitalize(guidance.agent)} destination: ${guidance.destination}`);
179295
- lines.push(`One-shot command: ${guidance.oneShotCommand}`);
179296
- lines.push("Fallback prompt:");
179297
- lines.push(guidance.fallbackPrompt);
179822
+ respond(id, result) {
179823
+ if (id === undefined) {
179824
+ return;
179298
179825
  }
179826
+ this.send({ jsonrpc: "2.0", id, result });
179299
179827
  }
179300
- if (plan.nextSteps.length > 0) {
179301
- lines.push("");
179302
- lines.push("Next steps:");
179303
- for (const step of plan.nextSteps) {
179304
- lines.push(`- ${step}`);
179828
+ clearTimer() {
179829
+ if (this.timer !== null) {
179830
+ clearTimeout(this.timer);
179831
+ this.timer = null;
179305
179832
  }
179306
179833
  }
179307
- return `${lines.join(`
179308
- `)}
179309
- `;
179310
179834
  }
179311
- function planConfigOperations(input) {
179312
- if (input.existingConfig.rawText !== undefined) {
179313
- return [];
179314
- }
179315
- const scope = input.confirmedScope ?? inferConfirmedScope(input.discovery, input.options);
179316
- if (scope === undefined) {
179317
- if (input.discovery.docs.message !== undefined) {
179318
- input.messages.push(input.discovery.docs.message);
179319
- }
179320
- if (input.discovery.code.message !== undefined) {
179321
- input.messages.push(input.discovery.code.message);
179835
+ function resolveRoot(params) {
179836
+ const record = params;
179837
+ const folderUri = record?.workspaceFolders?.[0]?.uri;
179838
+ if (typeof folderUri === "string") {
179839
+ const path = uriToPath(folderUri);
179840
+ if (path !== undefined) {
179841
+ return path;
179322
179842
  }
179323
- if (input.options.yes) {
179324
- input.messages.push("Automatic setup stopped because docs or code scope is ambiguous. Re-run interactively or confirm scope explicitly.");
179843
+ }
179844
+ if (typeof record?.rootUri === "string") {
179845
+ const path = uriToPath(record.rootUri);
179846
+ if (path !== undefined) {
179847
+ return path;
179325
179848
  }
179326
- return [];
179327
179849
  }
179328
- const config = buildConfigFromScope(scope);
179329
- const content = `${JSON.stringify({ $schema: "./schemas/docbridge.schema.json", ...config }, null, 2)}
179330
- `;
179331
- const action = input.options.dryRun ? "would-create" : "create";
179332
- input.messages.push(`Docs scope: ${scope.docsPattern}`);
179333
- input.messages.push(`Code scope: ${scope.languages.map((language) => `${language.language} (${language.patterns.join(", ")})`).join("; ")}`);
179334
- return [
179335
- {
179336
- action,
179337
- path: CONFIG_FILE_NAME2,
179338
- content,
179339
- reason: "Create docbridge.config.json from discovered scope."
179850
+ if (typeof record?.rootPath === "string") {
179851
+ return record.rootPath;
179852
+ }
179853
+ return process.cwd();
179854
+ }
179855
+ function runLspServer() {
179856
+ const reader = new MessageReader;
179857
+ const server2 = new Server((message) => {
179858
+ process.stdout.write(encodeMessage(message));
179859
+ });
179860
+ process.stdin.on("data", (chunk) => {
179861
+ reader.append(chunk);
179862
+ for (const message of reader.read()) {
179863
+ server2.handle(message);
179340
179864
  }
179341
- ];
179865
+ });
179866
+ process.stdin.resume();
179342
179867
  }
179343
- function inferConfirmedScope(discovery, options) {
179344
- if (discovery.docs.ambiguous || discovery.code.ambiguous) {
179868
+ function uriToPath(uri) {
179869
+ try {
179870
+ return fileURLToPath4(uri);
179871
+ } catch {
179345
179872
  return;
179346
179873
  }
179347
- if (discovery.docs.recommended === undefined || discovery.code.languages.length === 0) {
179874
+ }
179875
+ function toLocation(root, locator) {
179876
+ return {
179877
+ uri: relativePathToUri(root, locator.filePath),
179878
+ range: toLspRange(locator.range)
179879
+ };
179880
+ }
179881
+ function textDocument(params) {
179882
+ const record = params;
179883
+ const doc = record?.textDocument;
179884
+ if (doc === undefined) {
179348
179885
  return;
179349
179886
  }
179350
- if (!options.yes) {
179887
+ const result = {};
179888
+ if (typeof doc.uri === "string") {
179889
+ result.uri = doc.uri;
179890
+ }
179891
+ if (typeof doc.text === "string") {
179892
+ result.text = doc.text;
179893
+ }
179894
+ return result;
179895
+ }
179896
+ function fullChangeText(changes) {
179897
+ if (!Array.isArray(changes) || changes.length === 0) {
179351
179898
  return;
179352
179899
  }
179353
- return {
179354
- docsPattern: discovery.docs.recommended.pattern,
179355
- languages: discovery.code.languages
179356
- };
179900
+ const last = changes[changes.length - 1];
179901
+ return typeof last.text === "string" ? last.text : undefined;
179357
179902
  }
179358
- function planSkillOperations(input) {
179359
- if (input.agentTarget === "none") {
179360
- return [];
179903
+
179904
+ // src/setup/update-cache.ts
179905
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "node:fs";
179906
+ import { homedir } from "node:os";
179907
+ import { dirname as dirname4, join as join7 } from "node:path";
179908
+ var UPDATE_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
179909
+ var UPDATE_CACHE_SCHEMA_VERSION = 1;
179910
+ function resolveUpdateCachePath(env = process.env, home = homedir()) {
179911
+ const explicit = env.DOCBRIDGE_UPDATE_CACHE;
179912
+ if (explicit !== undefined && explicit.length > 0) {
179913
+ return explicit;
179914
+ }
179915
+ const cacheRoot = env.XDG_CACHE_HOME !== undefined && env.XDG_CACHE_HOME.length > 0 ? env.XDG_CACHE_HOME : join7(home, ".cache");
179916
+ return join7(cacheRoot, "docbridge", "update-check.json");
179917
+ }
179918
+ function readUpdateCache(path) {
179919
+ let rawText;
179920
+ try {
179921
+ rawText = readFileSync4(path, "utf8");
179922
+ } catch {
179923
+ return;
179361
179924
  }
179362
- const destinations = agentDestinations(input.agentTarget);
179363
- const availableSkills = new Set(listDistributableSkills(input.packageRoot));
179364
- const operations = [];
179365
- for (const destination of destinations) {
179366
- for (const skillName of INIT_SKILL_NAMES) {
179367
- if (!availableSkills.has(skillName)) {
179368
- continue;
179369
- }
179370
- const destinationDir = join5(input.projectRoot, destination, skillName);
179371
- const path = relative3(input.projectRoot, destinationDir);
179372
- if (isSymlink(destinationDir)) {
179373
- input.messages.push(`Skill directory ${path} is a symlink and was left in place.`);
179374
- continue;
179375
- }
179376
- const exists = existsSync2(destinationDir);
179377
- let action;
179378
- let reason;
179379
- if (exists && input.options.force) {
179380
- action = input.options.dryRun ? "would-overwrite" : "overwrite";
179381
- reason = "Overwrite existing skill directory.";
179382
- } else if (exists) {
179383
- action = "skip";
179384
- reason = "Skill directory already exists.";
179385
- } else {
179386
- action = input.options.dryRun ? "would-create" : "create";
179387
- reason = "Install distributable skill template.";
179388
- }
179389
- operations.push({
179390
- action,
179391
- path,
179392
- reason
179393
- });
179394
- }
179395
- operations.push(...planLegacySkillOperations({
179396
- projectRoot: input.projectRoot,
179397
- destination,
179398
- options: input.options,
179399
- messages: input.messages
179400
- }));
179925
+ let parsed;
179926
+ try {
179927
+ parsed = JSON.parse(rawText);
179928
+ } catch {
179929
+ return;
179401
179930
  }
179402
- return operations;
179931
+ return toUpdateCacheRecord(parsed);
179403
179932
  }
179404
- function planLegacySkillOperations(input) {
179405
- const operations = [];
179406
- const leftover = [];
179407
- for (const skillName of LEGACY_SKILL_NAMES) {
179408
- const destinationDir = join5(input.projectRoot, input.destination, skillName);
179409
- const path = relative3(input.projectRoot, destinationDir);
179410
- if (isSymlink(destinationDir)) {
179411
- input.messages.push(`Skill directory ${path} is a symlink and was left in place.`);
179412
- continue;
179413
- }
179414
- if (!existsSync2(destinationDir)) {
179415
- continue;
179416
- }
179417
- if (input.options.force) {
179418
- operations.push({
179419
- action: input.options.dryRun ? "would-remove" : "remove",
179420
- path,
179421
- reason: "Remove leftover skill from the previous five-skill layout."
179422
- });
179423
- } else {
179424
- leftover.push(path);
179425
- }
179933
+ function writeUpdateCache(path, record) {
179934
+ try {
179935
+ mkdirSync2(dirname4(path), { recursive: true });
179936
+ writeFileSync(path, `${JSON.stringify(record, null, 2)}
179937
+ `, "utf8");
179938
+ } catch {}
179939
+ }
179940
+ function isUpdateCacheFresh(record, now, ttlMs = UPDATE_CACHE_TTL_MS) {
179941
+ const age = now - record.checkedAt;
179942
+ return age >= 0 && age < ttlMs;
179943
+ }
179944
+ function toUpdateCacheRecord(value) {
179945
+ if (typeof value !== "object" || value === null) {
179946
+ return;
179426
179947
  }
179427
- if (leftover.length > 0) {
179428
- input.messages.push(`Legacy DocBridge skill directories were left in place (${leftover.join(", ")}). Re-run with --force to remove them after reviewing local edits.`);
179948
+ const candidate = value;
179949
+ if (candidate.schema !== UPDATE_CACHE_SCHEMA_VERSION) {
179950
+ return;
179429
179951
  }
179430
- return operations;
179952
+ if (typeof candidate.checkedAt !== "number" || !Number.isFinite(candidate.checkedAt)) {
179953
+ return;
179954
+ }
179955
+ if (candidate.latest !== null && typeof candidate.latest !== "string") {
179956
+ return;
179957
+ }
179958
+ return {
179959
+ schema: UPDATE_CACHE_SCHEMA_VERSION,
179960
+ checkedAt: candidate.checkedAt,
179961
+ latest: candidate.latest
179962
+ };
179431
179963
  }
179432
- function buildAgentGuidance(projectRoot, agentTarget) {
179433
- const guidance = [];
179434
- if (agentTarget === "codex" || agentTarget === "both") {
179435
- guidance.push({
179436
- agent: "codex",
179437
- destination: ".agents/skills/docbridge/",
179438
- oneShotCommand: `cd ${projectRoot} && codex --prompt "Use the docbridge skill to adopt DocBridge in this repository."`,
179439
- fallbackPrompt: buildFallbackPrompt(projectRoot, "codex")
179440
- });
179964
+
179965
+ // src/setup/version.ts
179966
+ var STABLE_VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
179967
+ function parseStableVersion(value) {
179968
+ const match = STABLE_VERSION_PATTERN.exec(value.trim());
179969
+ if (match === null) {
179970
+ return;
179441
179971
  }
179442
- if (agentTarget === "claude" || agentTarget === "both") {
179443
- guidance.push({
179444
- agent: "claude",
179445
- destination: ".claude/skills/docbridge/",
179446
- oneShotCommand: `cd ${projectRoot} && /docbridge adopt DocBridge in this repository`,
179447
- fallbackPrompt: buildFallbackPrompt(projectRoot, "claude")
179448
- });
179972
+ const [, major, minor, patch] = match;
179973
+ if (major === undefined || minor === undefined || patch === undefined) {
179974
+ return;
179449
179975
  }
179450
- return guidance;
179976
+ return {
179977
+ major: Number.parseInt(major, 10),
179978
+ minor: Number.parseInt(minor, 10),
179979
+ patch: Number.parseInt(patch, 10)
179980
+ };
179451
179981
  }
179452
- function buildFallbackPrompt(projectRoot, agent) {
179453
- const invocation = agent === "claude" ? "Use the docbridge skill" : "Use the docbridge skill from .agents/skills/docbridge";
179454
- return `${invocation} to adopt DocBridge in ${projectRoot}. Confirm docs and code scope, create or improve docbridge.config.json, and suggest the next linking steps.`;
179982
+ function compareStableVersions(left, right) {
179983
+ return left.major - right.major || left.minor - right.minor || left.patch - right.patch;
179455
179984
  }
179456
- function resolveInitAgentTarget(discovery, options) {
179457
- if (options.agentTarget !== undefined) {
179458
- return { target: options.agentTarget, error: undefined };
179459
- }
179460
- if (options.yes) {
179461
- return { target: discovery.agent.defaultTarget, error: undefined };
179985
+ function isNewerStableVersion(candidate, current) {
179986
+ const parsedCandidate = parseStableVersion(candidate);
179987
+ const parsedCurrent = parseStableVersion(current);
179988
+ if (parsedCandidate === undefined || parsedCurrent === undefined) {
179989
+ return false;
179462
179990
  }
179463
- return { target: discovery.agent.recommendedTarget, error: undefined };
179991
+ return compareStableVersions(parsedCandidate, parsedCurrent) > 0;
179464
179992
  }
179465
- function resolveInitWithAgentTarget(discovery, options) {
179466
- if (options.agentTarget !== undefined) {
179467
- if (options.agentTarget === "none") {
179468
- return {
179469
- target: undefined,
179470
- error: "init-with-agent requires an agent target other than none."
179471
- };
179472
- }
179473
- return { target: options.agentTarget, error: undefined };
179993
+
179994
+ // src/setup/registry.ts
179995
+ var NPM_REGISTRY_LATEST_URL = "https://registry.npmjs.org/docbridge/latest";
179996
+ var REGISTRY_TIMEOUT_MS = 1500;
179997
+ function selectStableVersion(payload) {
179998
+ if (typeof payload !== "object" || payload === null) {
179999
+ return;
179474
180000
  }
179475
- if (discovery.agent.defaultTarget === "none") {
179476
- if (options.yes) {
179477
- return {
179478
- target: undefined,
179479
- error: "init-with-agent --yes requires an explicit agent target when no .agents/ or .claude/ directory exists. Pass --agent-target codex, claude, or both."
179480
- };
179481
- }
179482
- return { target: discovery.agent.recommendedTarget, error: undefined };
180001
+ const version = payload.version;
180002
+ if (typeof version !== "string") {
180003
+ return;
179483
180004
  }
179484
- return { target: discovery.agent.defaultTarget, error: undefined };
180005
+ return parseStableVersion(version) === undefined ? undefined : version.trim();
179485
180006
  }
179486
- function readExistingConfig(projectRoot) {
179487
- const configPath = join5(projectRoot, CONFIG_FILE_NAME2);
179488
- let rawText;
180007
+ function resolveRegistryUrl(env = process.env) {
180008
+ const override = env.DOCBRIDGE_REGISTRY_URL;
180009
+ return override !== undefined && override.length > 0 ? override : NPM_REGISTRY_LATEST_URL;
180010
+ }
180011
+ async function fetchLatestStableVersion(options = {}) {
180012
+ const fetchImpl = options.fetchImpl ?? fetch;
180013
+ const url = options.url ?? resolveRegistryUrl();
179489
180014
  try {
179490
- rawText = readFileSync3(configPath, "utf8");
180015
+ const response = await fetchImpl(url, {
180016
+ signal: AbortSignal.timeout(options.timeoutMs ?? REGISTRY_TIMEOUT_MS),
180017
+ headers: { accept: "application/json" }
180018
+ });
180019
+ if (!response.ok) {
180020
+ return;
180021
+ }
180022
+ return selectStableVersion(await response.json());
179491
180023
  } catch {
179492
- rawText = undefined;
180024
+ return;
179493
180025
  }
179494
- const resolved = resolveConfig(rawText);
180026
+ }
180027
+ async function resolveLatestStableVersion(options = {}) {
180028
+ const now = options.now ?? Date.now();
180029
+ const cachePath = options.cachePath ?? resolveUpdateCachePath();
180030
+ if (options.forceRefresh !== true) {
180031
+ const cached = readUpdateCache(cachePath);
180032
+ if (cached !== undefined && isUpdateCacheFresh(cached, now)) {
180033
+ return cached.latest === null ? { status: "unavailable", source: "cache" } : { status: "ok", latest: cached.latest, source: "cache" };
180034
+ }
180035
+ }
180036
+ const fetched = await fetchLatestStableVersion({
180037
+ ...options.url !== undefined ? { url: options.url } : {},
180038
+ ...options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {},
180039
+ ...options.fetchImpl !== undefined ? { fetchImpl: options.fetchImpl } : {}
180040
+ });
180041
+ writeUpdateCache(cachePath, {
180042
+ schema: UPDATE_CACHE_SCHEMA_VERSION,
180043
+ checkedAt: now,
180044
+ latest: fetched ?? null
180045
+ });
180046
+ return fetched === undefined ? { status: "unavailable", source: "network" } : { status: "ok", latest: fetched, source: "network" };
180047
+ }
180048
+
180049
+ // src/setup/upgrade-guidance.ts
180050
+ import { sep as sep2 } from "node:path";
180051
+ var PACKAGE_MANAGERS = ["bun", "npm", "pnpm", "yarn"];
180052
+ var PROJECT_COMMANDS = {
180053
+ bun: "bun add -d docbridge@latest",
180054
+ npm: "npm install --save-dev docbridge@latest",
180055
+ pnpm: "pnpm add -D docbridge@latest",
180056
+ yarn: "yarn add -D docbridge@latest"
180057
+ };
180058
+ var GLOBAL_COMMANDS = {
180059
+ bun: "bun add -g docbridge@latest",
180060
+ npm: "npm install -g docbridge@latest",
180061
+ pnpm: "pnpm add -g docbridge@latest",
180062
+ yarn: "yarn global add docbridge@latest"
180063
+ };
180064
+ function detectPackageManager(env = process.env) {
180065
+ const userAgent = env.npm_config_user_agent;
180066
+ if (userAgent === undefined || userAgent.length === 0) {
180067
+ return;
180068
+ }
180069
+ const name = userAgent.trim().split("/", 1)[0]?.toLowerCase();
180070
+ return PACKAGE_MANAGERS.find((manager) => manager === name);
180071
+ }
180072
+ function detectInstallScope(packageRoot, candidateRoots) {
180073
+ const installBase = resolveInstallBase(packageRoot);
180074
+ if (installBase === undefined) {
180075
+ return "unknown";
180076
+ }
180077
+ return candidateRoots.some((root) => isSameOrAncestor(installBase, root)) ? "project" : "global";
180078
+ }
180079
+ function detectUpgradeGuidance(input) {
180080
+ const packageManager = detectPackageManager(input.env ?? process.env);
180081
+ const scope = detectInstallScope(input.packageRoot, [
180082
+ input.projectRoot,
180083
+ input.currentDirectory ?? process.cwd()
180084
+ ]);
180085
+ const commands = scope === "global" ? GLOBAL_COMMANDS : PROJECT_COMMANDS;
180086
+ const primaryManager = packageManager ?? "npm";
179495
180087
  return {
179496
- rawText,
179497
- ok: resolved.ok,
179498
- config: resolved.config,
179499
- diagnostics: resolved.diagnostics
180088
+ packageManager,
180089
+ scope,
180090
+ primaryCommand: commands[primaryManager],
180091
+ alternativeCommands: PACKAGE_MANAGERS.filter((manager) => manager !== primaryManager).map((manager) => commands[manager])
179500
180092
  };
179501
180093
  }
179502
- function summarizeExistingConfig(config, messages) {
179503
- messages.push(`Docs scope: ${config.include.docs.join(", ")}`);
179504
- const codeSummary = Object.entries(config.include.code).map(([language, entry]) => `${language}: ${entry?.patterns.join(", ") ?? ""}`).join("; ");
179505
- messages.push(`Code scope: ${codeSummary}`);
180094
+ function formatUpgradeGuidance(guidance) {
180095
+ const scopeLabel = guidance.scope === "unknown" ? "install scope not detected" : `${guidance.scope} install`;
180096
+ return [
180097
+ `Upgrade command (${scopeLabel}): ${guidance.primaryCommand}`,
180098
+ `Other package managers: ${guidance.alternativeCommands.join(", ")}`
180099
+ ];
179506
180100
  }
179507
- function agentDestinations(agentTarget) {
179508
- switch (agentTarget) {
179509
- case "codex":
179510
- return [".agents/skills"];
179511
- case "claude":
179512
- return [".claude/skills"];
179513
- case "both":
179514
- return [".agents/skills", ".claude/skills"];
179515
- default:
179516
- return [];
179517
- }
180101
+ function resolveInstallBase(packageRoot) {
180102
+ const marker = `${sep2}node_modules${sep2}`;
180103
+ const markerIndex = withTrailingSeparator(packageRoot).indexOf(marker);
180104
+ return markerIndex === -1 ? undefined : packageRoot.slice(0, markerIndex);
179518
180105
  }
179519
- function isSymlink(path) {
179520
- try {
179521
- return lstatSync2(path).isSymbolicLink();
179522
- } catch {
179523
- return false;
180106
+ function isSameOrAncestor(candidate, descendant) {
180107
+ return withTrailingSeparator(descendant).startsWith(withTrailingSeparator(candidate));
180108
+ }
180109
+ function withTrailingSeparator(path) {
180110
+ return path.endsWith(sep2) ? path : `${path}${sep2}`;
180111
+ }
180112
+
180113
+ // src/setup/update-notice.ts
180114
+ var SUPPRESSED_COMMANDS = new Set(["lsp", "upgrade"]);
180115
+ function isUpdateCheckOptedOut(env = process.env) {
180116
+ return isEnabledFlag(env.DOCBRIDGE_NO_UPDATE_CHECK);
180117
+ }
180118
+ function decideUpdateCheck(input) {
180119
+ const env = input.env ?? process.env;
180120
+ if (isUpdateCheckOptedOut(env)) {
180121
+ return { enabled: false, reason: "opt-out" };
180122
+ }
180123
+ if (isEnabledFlag(env.CI)) {
180124
+ return { enabled: false, reason: "ci" };
180125
+ }
180126
+ if (!input.isTty) {
180127
+ return { enabled: false, reason: "non-tty" };
179524
180128
  }
180129
+ if (input.argv.includes("--json")) {
180130
+ return { enabled: false, reason: "json-output" };
180131
+ }
180132
+ const command = input.argv[0];
180133
+ if (command !== undefined && SUPPRESSED_COMMANDS.has(command)) {
180134
+ return { enabled: false, reason: "machine-command" };
180135
+ }
180136
+ return { enabled: true };
179525
180137
  }
179526
- function formatFileOp(operation) {
179527
- let label = operation.action;
179528
- if (operation.action === "would-create") {
179529
- label = "would create";
179530
- } else if (operation.action === "would-overwrite") {
179531
- label = "would overwrite";
179532
- } else if (operation.action === "would-remove") {
179533
- label = "would remove";
180138
+ function formatUpdateNotice(input) {
180139
+ if (!isNewerStableVersion(input.latest, input.current)) {
180140
+ return;
179534
180141
  }
179535
- const reason = operation.reason ? ` (${operation.reason})` : "";
179536
- return `- ${label} ${operation.path}${reason}`;
180142
+ return [
180143
+ `Update available: docbridge ${input.current} -> ${input.latest}`,
180144
+ formatUpgradeGuidance(input.guidance)[0] ?? "",
180145
+ "Run `docbridge upgrade --check` for details, or set DOCBRIDGE_NO_UPDATE_CHECK=1 to silence this notice.",
180146
+ ""
180147
+ ].join(`
180148
+ `);
179537
180149
  }
179538
- function capitalize(value) {
179539
- return value.charAt(0).toUpperCase() + value.slice(1);
180150
+ function isEnabledFlag(value) {
180151
+ if (value === undefined) {
180152
+ return false;
180153
+ }
180154
+ const normalized = value.trim().toLowerCase();
180155
+ return normalized.length > 0 && normalized !== "0" && normalized !== "false";
179540
180156
  }
179541
180157
 
180158
+ // src/cli/docs.ts
180159
+ import { readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
180160
+ import { join as join8 } from "node:path";
180161
+
179542
180162
  // src/cli/errors.ts
179543
180163
  class CliError extends Error {
179544
180164
  guidance;
@@ -179599,6 +180219,19 @@ function agentTargetGuidance(command) {
179599
180219
  ].join(`
179600
180220
  `);
179601
180221
  }
180222
+ function upgradeConfirmationGuidance() {
180223
+ return [
180224
+ "Confirm the migration explicitly:",
180225
+ "",
180226
+ " docbridge upgrade --force --yes",
180227
+ "",
180228
+ "Or inspect it first:",
180229
+ "",
180230
+ " docbridge upgrade --check",
180231
+ " docbridge upgrade --force --dry-run"
180232
+ ].join(`
180233
+ `);
180234
+ }
179602
180235
  function includeContentGuidance() {
179603
180236
  return [
179604
180237
  "Use --include-content together with --json:",
@@ -179625,6 +180258,9 @@ function configSetupGuidance() {
179625
180258
  function configRepairGuidance() {
179626
180259
  return "Repair or delete docbridge.config.json, then re-run `docbridge check`.";
179627
180260
  }
180261
+ function manifestRepairGuidance() {
180262
+ return "Repair or delete docbridge.links.json, then re-run `docbridge check`.";
180263
+ }
179628
180264
  function formatCliError(error) {
179629
180265
  if (!(error instanceof CliError)) {
179630
180266
  return error instanceof Error ? error.message : String(error);
@@ -179692,11 +180328,11 @@ function parseDocument(name, source) {
179692
180328
  };
179693
180329
  }
179694
180330
  function createFileDocumentationReader(packageRoot) {
179695
- const docsRoot = join6(packageRoot, "docs", "user");
180331
+ const docsRoot = join8(packageRoot, "docs", "user");
179696
180332
  function documents() {
179697
180333
  let fileNames;
179698
180334
  try {
179699
- fileNames = readdirSync3(docsRoot).filter((fileName) => fileName.endsWith(".md")).toSorted();
180335
+ fileNames = readdirSync2(docsRoot).filter((fileName) => fileName.endsWith(".md")).toSorted();
179700
180336
  } catch {
179701
180337
  throw documentationUnavailableError();
179702
180338
  }
@@ -179705,7 +180341,7 @@ function createFileDocumentationReader(packageRoot) {
179705
180341
  }
179706
180342
  return fileNames.map((fileName) => {
179707
180343
  const name = fileName.slice(0, -".md".length);
179708
- return parseDocument(name, readFileSync4(join6(docsRoot, fileName), "utf8"));
180344
+ return parseDocument(name, readFileSync5(join8(docsRoot, fileName), "utf8"));
179709
180345
  });
179710
180346
  }
179711
180347
  return {
@@ -179776,6 +180412,7 @@ var SUBCOMMANDS = [
179776
180412
  "docs",
179777
180413
  "init",
179778
180414
  "init-with-agent",
180415
+ "upgrade",
179779
180416
  "lsp"
179780
180417
  ];
179781
180418
  var ROOT_OPTION = {
@@ -179901,6 +180538,42 @@ var COMMAND_DOCS = {
179901
180538
  `),
179902
180539
  options: [JSON_OPTION]
179903
180540
  },
180541
+ upgrade: {
180542
+ usage: "[options]",
180543
+ summary: "Use to check for a newer release and migrate managed skills.",
180544
+ description: [
180545
+ "Report DocBridge version drift and reconcile the managed agent skill.",
180546
+ "Use it after a DocBridge release, or when a project still carries assets",
180547
+ "from an older layout. `--check` is read-only. Applying changes touches",
180548
+ "only the managed docbridge skill directory and the known legacy skill",
180549
+ "directories; it never edits config, hooks, CI recipes, or user code, and",
180550
+ "it never runs a package manager for you."
180551
+ ].join(`
180552
+ `),
180553
+ options: [
180554
+ {
180555
+ flag: "--root <path>",
180556
+ description: "Project root to inspect. Defaults to current directory."
180557
+ },
180558
+ {
180559
+ flag: "--check",
180560
+ description: "Report version and managed asset state without writing files."
180561
+ },
180562
+ {
180563
+ flag: "--dry-run",
180564
+ description: "Print the operations that would be applied without writing files."
180565
+ },
180566
+ { flag: "--yes", description: "Confirm destructive operations without prompting." },
180567
+ {
180568
+ flag: "--force",
180569
+ description: "Replace the managed skill and remove known legacy skill directories."
180570
+ },
180571
+ {
180572
+ flag: "--agent-target <target>",
180573
+ description: "Agent target: codex, claude, both, or none."
180574
+ }
180575
+ ]
180576
+ },
179904
180577
  init: {
179905
180578
  usage: "[options]",
179906
180579
  summary: "Use once per project to set up config and the agent skill.",
@@ -179968,7 +180641,7 @@ function commandList() {
179968
180641
  return SUBCOMMANDS.map((command) => ` ${command.padEnd(width)}${COMMAND_DOCS[command].summary}`).join(`
179969
180642
  `);
179970
180643
  }
179971
- var GLOBAL_OPTION_SECTIONS = ["check", "related", "context", "graph", "docs", "init"].map((command) => {
180644
+ var GLOBAL_OPTION_SECTIONS = ["check", "related", "context", "graph", "docs", "init", "upgrade"].map((command) => {
179972
180645
  const label = `${command.charAt(0).toUpperCase()}${command.slice(1)}`;
179973
180646
  return `${label} options:
179974
180647
  ${renderOptions(COMMAND_DOCS[command].options)}`;
@@ -180065,20 +180738,12 @@ function hasHelpFlag(args) {
180065
180738
  }
180066
180739
 
180067
180740
  // src/cli/init.ts
180068
- import {
180069
- cpSync,
180070
- lstatSync as lstatSync3,
180071
- mkdirSync as mkdirSync2,
180072
- readFileSync as readFileSync5,
180073
- rmSync,
180074
- statSync as statSync3,
180075
- writeFileSync
180076
- } from "node:fs";
180077
- import { join as join8, resolve as resolve3 } from "node:path";
180741
+ import { readFileSync as readFileSync8, statSync as statSync3, writeFileSync as writeFileSync2 } from "node:fs";
180742
+ import { join as join12, resolve as resolve3 } from "node:path";
180078
180743
 
180079
- // src/core/init-discovery.ts
180080
- import { existsSync as existsSync3, readdirSync as readdirSync4, statSync as statSync2 } from "node:fs";
180081
- import { basename, dirname as dirname4, join as join7 } from "node:path";
180744
+ // src/setup/init-discovery.ts
180745
+ import { existsSync as existsSync3, readdirSync as readdirSync3, statSync as statSync2 } from "node:fs";
180746
+ import { basename, dirname as dirname5, join as join9 } from "node:path";
180082
180747
  var HIGH_CONFIDENCE_DIR_NAMES = new Set([
180083
180748
  "docs/specs",
180084
180749
  "specs",
@@ -180143,7 +180808,7 @@ function discoverDocsScope(projectRoot) {
180143
180808
  }
180144
180809
  const grouped = new Map;
180145
180810
  for (const filePath of eligible) {
180146
- const directory = dirname4(filePath);
180811
+ const directory = dirname5(filePath);
180147
180812
  const existing = grouped.get(directory) ?? [];
180148
180813
  existing.push(filePath);
180149
180814
  grouped.set(directory, existing);
@@ -180205,8 +180870,8 @@ function discoverCodeScope(projectRoot) {
180205
180870
  };
180206
180871
  }
180207
180872
  function discoverAgentTarget(projectRoot) {
180208
- const hasAgentsDir = existsSync3(join7(projectRoot, ".agents"));
180209
- const hasClaudeDir = existsSync3(join7(projectRoot, ".claude"));
180873
+ const hasAgentsDir = existsSync3(join9(projectRoot, ".agents"));
180874
+ const hasClaudeDir = existsSync3(join9(projectRoot, ".claude"));
180210
180875
  let defaultTarget = "none";
180211
180876
  let recommendedTarget = "none";
180212
180877
  let message;
@@ -180233,10 +180898,10 @@ function discoverAgentTarget(projectRoot) {
180233
180898
  };
180234
180899
  }
180235
180900
  function collectMarkdownFiles(projectRoot, currentDir = ".") {
180236
- const absoluteDir = join7(projectRoot, currentDir);
180901
+ const absoluteDir = join9(projectRoot, currentDir);
180237
180902
  let entries;
180238
180903
  try {
180239
- entries = readdirSync4(absoluteDir);
180904
+ entries = readdirSync3(absoluteDir);
180240
180905
  } catch {
180241
180906
  return [];
180242
180907
  }
@@ -180247,7 +180912,7 @@ function collectMarkdownFiles(projectRoot, currentDir = ".") {
180247
180912
  if (segments.some((segment) => IGNORED_WALK_SEGMENTS.has(segment) || segment.startsWith("."))) {
180248
180913
  continue;
180249
180914
  }
180250
- const absolutePath = join7(projectRoot, relPath);
180915
+ const absolutePath = join9(projectRoot, relPath);
180251
180916
  let stats;
180252
180917
  try {
180253
180918
  stats = statSync2(absolutePath);
@@ -180334,6 +180999,491 @@ function isExcludedCodeFile(filePath, language) {
180334
180999
  return lower.includes(".generated.");
180335
181000
  }
180336
181001
 
181002
+ // src/setup/init-plan.ts
181003
+ import { existsSync as existsSync4, readdirSync as readdirSync5, readFileSync as readFileSync7 } from "node:fs";
181004
+ import { join as join11, relative as relative3 } from "node:path";
181005
+
181006
+ // src/setup/skill-assets.ts
181007
+ import { cpSync, lstatSync as lstatSync2, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync6, rmSync } from "node:fs";
181008
+ import { join as join10 } from "node:path";
181009
+ function classifyManagedPath(projectRoot, relativePath) {
181010
+ const segments = relativePath.split(/[/\\]/).filter((segment) => segment.length > 0);
181011
+ let current = projectRoot;
181012
+ for (const [index, segment] of segments.entries()) {
181013
+ current = join10(current, segment);
181014
+ const isLast = index === segments.length - 1;
181015
+ let stats;
181016
+ try {
181017
+ stats = lstatSync2(current);
181018
+ } catch {
181019
+ return "absent";
181020
+ }
181021
+ if (stats.isSymbolicLink()) {
181022
+ return isLast ? "symlink" : "symlinked-parent";
181023
+ }
181024
+ if (!stats.isDirectory()) {
181025
+ return isLast ? "non-directory" : "blocked-parent";
181026
+ }
181027
+ }
181028
+ return segments.length === 0 ? "blocked-parent" : "directory";
181029
+ }
181030
+ function unmanageablePathMessage(path, kind) {
181031
+ switch (kind) {
181032
+ case "symlink":
181033
+ return `Skill directory ${path} is a symlink and was left in place.`;
181034
+ case "symlinked-parent":
181035
+ return `Skill directory ${path} sits under a symlinked directory and was left in place, because writing through it would leave the project root.`;
181036
+ case "non-directory":
181037
+ return `${path} exists but is not a directory, so it was left in place.`;
181038
+ case "blocked-parent":
181039
+ return `Skill directory ${path} cannot be reached because a parent path component is not a directory, so it was left in place.`;
181040
+ default:
181041
+ return `Skill directory ${path} was left in place.`;
181042
+ }
181043
+ }
181044
+ function listSkillFiles(directory) {
181045
+ const files = [];
181046
+ collectSkillFiles(directory, "", files);
181047
+ return files.toSorted();
181048
+ }
181049
+ function compareSkillTree(installedDir, templateDir) {
181050
+ const installedFiles = new Set(listSkillFiles(installedDir));
181051
+ const templateFiles = listSkillFiles(templateDir);
181052
+ const modified = [];
181053
+ const missing = [];
181054
+ for (const file of templateFiles) {
181055
+ if (!installedFiles.has(file)) {
181056
+ missing.push(file);
181057
+ continue;
181058
+ }
181059
+ installedFiles.delete(file);
181060
+ if (readFileOrUndefined(join10(installedDir, file)) !== readFileOrUndefined(join10(templateDir, file))) {
181061
+ modified.push(file);
181062
+ }
181063
+ }
181064
+ return { modified, missing, extra: [...installedFiles].toSorted() };
181065
+ }
181066
+ function applySkillOperation(projectRoot, packageRoot, operation) {
181067
+ if (operation.action !== "create" && operation.action !== "overwrite" && operation.action !== "remove") {
181068
+ return;
181069
+ }
181070
+ const kind = classifyManagedPath(projectRoot, operation.path);
181071
+ const destinationDir = join10(projectRoot, operation.path);
181072
+ if (operation.action === "remove") {
181073
+ if (kind === "directory") {
181074
+ rmSync(destinationDir, { recursive: true, force: true });
181075
+ }
181076
+ return;
181077
+ }
181078
+ if (kind !== "absent" && kind !== "directory") {
181079
+ return;
181080
+ }
181081
+ const skillName = operation.path.split(/[/\\]/).at(-1);
181082
+ if (skillName === undefined || skillName.length === 0) {
181083
+ return;
181084
+ }
181085
+ if (operation.action === "overwrite" && kind === "directory") {
181086
+ rmSync(destinationDir, { recursive: true, force: true });
181087
+ }
181088
+ mkdirSync3(destinationDir, { recursive: true });
181089
+ cpSync(join10(packageRoot, "templates", "skills", skillName), destinationDir, {
181090
+ recursive: true,
181091
+ force: true
181092
+ });
181093
+ }
181094
+ function collectSkillFiles(root, prefix, files) {
181095
+ let entries;
181096
+ try {
181097
+ entries = readdirSync4(join10(root, prefix), { withFileTypes: true });
181098
+ } catch {
181099
+ return;
181100
+ }
181101
+ for (const entry of entries.toSorted((left, right) => left.name < right.name ? -1 : 1)) {
181102
+ const relativePath = prefix === "" ? entry.name : `${prefix}/${entry.name}`;
181103
+ if (entry.isDirectory()) {
181104
+ collectSkillFiles(root, relativePath, files);
181105
+ continue;
181106
+ }
181107
+ files.push(relativePath);
181108
+ }
181109
+ }
181110
+ function readFileOrUndefined(path) {
181111
+ try {
181112
+ return readFileSync6(path, "utf8");
181113
+ } catch {
181114
+ return;
181115
+ }
181116
+ }
181117
+
181118
+ // src/setup/init-plan.ts
181119
+ var CONFIG_FILE_NAME2 = "docbridge.config.json";
181120
+ var INIT_SKILL_NAMES = ["docbridge"];
181121
+ var LEGACY_SKILL_NAMES = [
181122
+ "docbridge-adopt",
181123
+ "docbridge-annotate",
181124
+ "docbridge-link",
181125
+ "docbridge-review",
181126
+ "docbridge-sync"
181127
+ ];
181128
+ function planInitCommand(input) {
181129
+ const packageRoot = input.packageRoot ?? resolvePackageRoot();
181130
+ const existingConfig = readExistingConfig(input.projectRoot);
181131
+ const messages = [];
181132
+ const nextSteps = [];
181133
+ let exitCode = 0;
181134
+ const agentResolution = input.command === "init" ? resolveInitAgentTarget(input.discovery, input.options) : resolveInitWithAgentTarget(input.discovery, input.options);
181135
+ if (agentResolution.error !== undefined) {
181136
+ return {
181137
+ command: input.command,
181138
+ configOps: [],
181139
+ skillOps: [],
181140
+ messages: [agentResolution.error],
181141
+ nextSteps: [],
181142
+ agentGuidance: [],
181143
+ exitCode: 1
181144
+ };
181145
+ }
181146
+ const agentTarget = agentResolution.target ?? "none";
181147
+ const configOps = input.command === "init" ? planConfigOperations({
181148
+ projectRoot: input.projectRoot,
181149
+ options: input.options,
181150
+ discovery: input.discovery,
181151
+ confirmedScope: input.confirmedScope,
181152
+ existingConfig,
181153
+ messages
181154
+ }) : [];
181155
+ if (configOps.some((operation) => operation.action === "create" || operation.action === "would-create")) {
181156
+ nextSteps.push("Review docbridge.config.json and adjust include.docs or include.code if needed.");
181157
+ nextSteps.push("Add @doc and @code annotations to link code and documentation.");
181158
+ nextSteps.push("Run docbridge check when links exist.");
181159
+ } else if (existingConfig.ok) {
181160
+ messages.push("Existing docbridge.config.json was left unchanged.");
181161
+ summarizeExistingConfig(existingConfig.config, messages);
181162
+ } else if (existingConfig.rawText !== undefined) {
181163
+ messages.push("Existing docbridge.config.json is invalid and was not modified.");
181164
+ for (const diagnostic of existingConfig.diagnostics) {
181165
+ messages.push(diagnostic.message);
181166
+ }
181167
+ messages.push("Repair the config manually or delete it before re-running init.");
181168
+ exitCode = 1;
181169
+ }
181170
+ const skillOps = planSkillOperations({
181171
+ projectRoot: input.projectRoot,
181172
+ packageRoot,
181173
+ agentTarget,
181174
+ options: input.options,
181175
+ messages
181176
+ });
181177
+ const agentGuidance = input.command === "init-with-agent" && agentTarget !== "none" ? buildAgentGuidance(input.projectRoot, agentTarget) : [];
181178
+ if (input.command === "init-with-agent") {
181179
+ nextSteps.push("Run the printed one-shot command or fallback prompt in your agent.");
181180
+ nextSteps.push("Let the docbridge skill confirm scope and create or improve docbridge.config.json.");
181181
+ }
181182
+ if (input.options.yes && input.command === "init" && configOps.length === 0 && existingConfig.rawText === undefined && (input.discovery.docs.ambiguous || input.discovery.code.ambiguous)) {
181183
+ exitCode = 1;
181184
+ }
181185
+ return {
181186
+ command: input.command,
181187
+ configOps,
181188
+ skillOps,
181189
+ messages,
181190
+ nextSteps,
181191
+ agentGuidance,
181192
+ exitCode
181193
+ };
181194
+ }
181195
+ function buildConfigFromScope(scope) {
181196
+ const code = {};
181197
+ for (const language of scope.languages) {
181198
+ code[language.language] = { patterns: [...language.patterns] };
181199
+ }
181200
+ return {
181201
+ include: {
181202
+ code,
181203
+ docs: [scope.docsPattern]
181204
+ }
181205
+ };
181206
+ }
181207
+ function listDistributableSkills(packageRoot) {
181208
+ const skillsRoot = join11(packageRoot, "templates", "skills");
181209
+ let entries;
181210
+ try {
181211
+ entries = readdirSync5(skillsRoot);
181212
+ } catch {
181213
+ return [];
181214
+ }
181215
+ return entries.filter((entry) => entry.startsWith("docbridge")).filter((entry) => existsSync4(join11(skillsRoot, entry, "SKILL.md"))).toSorted();
181216
+ }
181217
+ function formatInitPlan(plan) {
181218
+ const lines = [];
181219
+ for (const message of plan.messages) {
181220
+ lines.push(message);
181221
+ }
181222
+ if (plan.configOps.length > 0) {
181223
+ lines.push("");
181224
+ lines.push("Config:");
181225
+ for (const operation of plan.configOps) {
181226
+ lines.push(formatFileOp(operation));
181227
+ if (operation.content !== undefined) {
181228
+ lines.push(operation.content);
181229
+ }
181230
+ }
181231
+ }
181232
+ if (plan.skillOps.length > 0) {
181233
+ lines.push("");
181234
+ lines.push("Skills:");
181235
+ for (const operation of plan.skillOps) {
181236
+ lines.push(formatFileOp(operation));
181237
+ }
181238
+ }
181239
+ if (plan.agentGuidance.length > 0) {
181240
+ lines.push("");
181241
+ lines.push("Agent setup:");
181242
+ for (const guidance of plan.agentGuidance) {
181243
+ lines.push("");
181244
+ lines.push(`${capitalize(guidance.agent)} destination: ${guidance.destination}`);
181245
+ lines.push(`One-shot command: ${guidance.oneShotCommand}`);
181246
+ lines.push("Fallback prompt:");
181247
+ lines.push(guidance.fallbackPrompt);
181248
+ }
181249
+ }
181250
+ if (plan.nextSteps.length > 0) {
181251
+ lines.push("");
181252
+ lines.push("Next steps:");
181253
+ for (const step of plan.nextSteps) {
181254
+ lines.push(`- ${step}`);
181255
+ }
181256
+ }
181257
+ return `${lines.join(`
181258
+ `)}
181259
+ `;
181260
+ }
181261
+ function planConfigOperations(input) {
181262
+ if (input.existingConfig.rawText !== undefined) {
181263
+ return [];
181264
+ }
181265
+ const scope = input.confirmedScope ?? inferConfirmedScope(input.discovery, input.options);
181266
+ if (scope === undefined) {
181267
+ if (input.discovery.docs.message !== undefined) {
181268
+ input.messages.push(input.discovery.docs.message);
181269
+ }
181270
+ if (input.discovery.code.message !== undefined) {
181271
+ input.messages.push(input.discovery.code.message);
181272
+ }
181273
+ if (input.options.yes) {
181274
+ input.messages.push("Automatic setup stopped because docs or code scope is ambiguous. Re-run interactively or confirm scope explicitly.");
181275
+ }
181276
+ return [];
181277
+ }
181278
+ const config = buildConfigFromScope(scope);
181279
+ const content = `${JSON.stringify({ $schema: "./schemas/docbridge.schema.json", ...config }, null, 2)}
181280
+ `;
181281
+ const action = input.options.dryRun ? "would-create" : "create";
181282
+ input.messages.push(`Docs scope: ${scope.docsPattern}`);
181283
+ input.messages.push(`Code scope: ${scope.languages.map((language) => `${language.language} (${language.patterns.join(", ")})`).join("; ")}`);
181284
+ return [
181285
+ {
181286
+ action,
181287
+ path: CONFIG_FILE_NAME2,
181288
+ content,
181289
+ reason: "Create docbridge.config.json from discovered scope."
181290
+ }
181291
+ ];
181292
+ }
181293
+ function inferConfirmedScope(discovery, options) {
181294
+ if (discovery.docs.ambiguous || discovery.code.ambiguous) {
181295
+ return;
181296
+ }
181297
+ if (discovery.docs.recommended === undefined || discovery.code.languages.length === 0) {
181298
+ return;
181299
+ }
181300
+ if (!options.yes) {
181301
+ return;
181302
+ }
181303
+ return {
181304
+ docsPattern: discovery.docs.recommended.pattern,
181305
+ languages: discovery.code.languages
181306
+ };
181307
+ }
181308
+ function planSkillOperations(input) {
181309
+ if (input.agentTarget === "none") {
181310
+ return [];
181311
+ }
181312
+ const destinations = agentDestinations(input.agentTarget);
181313
+ const availableSkills = new Set(listDistributableSkills(input.packageRoot));
181314
+ const operations = [];
181315
+ for (const destination of destinations) {
181316
+ for (const skillName of INIT_SKILL_NAMES) {
181317
+ if (!availableSkills.has(skillName)) {
181318
+ continue;
181319
+ }
181320
+ const destinationDir = join11(input.projectRoot, destination, skillName);
181321
+ const path = relative3(input.projectRoot, destinationDir);
181322
+ const kind = classifyManagedPath(input.projectRoot, path);
181323
+ if (kind !== "absent" && kind !== "directory") {
181324
+ input.messages.push(unmanageablePathMessage(path, kind));
181325
+ continue;
181326
+ }
181327
+ const exists = kind === "directory";
181328
+ let action;
181329
+ let reason;
181330
+ if (exists && input.options.force) {
181331
+ action = input.options.dryRun ? "would-overwrite" : "overwrite";
181332
+ reason = "Overwrite existing skill directory.";
181333
+ } else if (exists) {
181334
+ action = "skip";
181335
+ reason = "Skill directory already exists.";
181336
+ } else {
181337
+ action = input.options.dryRun ? "would-create" : "create";
181338
+ reason = "Install distributable skill template.";
181339
+ }
181340
+ operations.push({
181341
+ action,
181342
+ path,
181343
+ reason
181344
+ });
181345
+ }
181346
+ operations.push(...planLegacySkillOperations({
181347
+ projectRoot: input.projectRoot,
181348
+ destination,
181349
+ options: input.options,
181350
+ messages: input.messages
181351
+ }));
181352
+ }
181353
+ return operations;
181354
+ }
181355
+ function planLegacySkillOperations(input) {
181356
+ const operations = [];
181357
+ const leftover = [];
181358
+ for (const skillName of LEGACY_SKILL_NAMES) {
181359
+ const destinationDir = join11(input.projectRoot, input.destination, skillName);
181360
+ const path = relative3(input.projectRoot, destinationDir);
181361
+ const kind = classifyManagedPath(input.projectRoot, path);
181362
+ if (kind === "absent") {
181363
+ continue;
181364
+ }
181365
+ if (kind !== "directory") {
181366
+ input.messages.push(unmanageablePathMessage(path, kind));
181367
+ continue;
181368
+ }
181369
+ if (input.options.force) {
181370
+ operations.push({
181371
+ action: input.options.dryRun ? "would-remove" : "remove",
181372
+ path,
181373
+ reason: "Remove leftover skill from the previous five-skill layout."
181374
+ });
181375
+ } else {
181376
+ leftover.push(path);
181377
+ }
181378
+ }
181379
+ if (leftover.length > 0) {
181380
+ input.messages.push(`Legacy DocBridge skill directories were left in place (${leftover.join(", ")}). Re-run with --force to remove them after reviewing local edits.`);
181381
+ }
181382
+ return operations;
181383
+ }
181384
+ function buildAgentGuidance(projectRoot, agentTarget) {
181385
+ const guidance = [];
181386
+ if (agentTarget === "codex" || agentTarget === "both") {
181387
+ guidance.push({
181388
+ agent: "codex",
181389
+ destination: ".agents/skills/docbridge/",
181390
+ oneShotCommand: `cd ${projectRoot} && codex --prompt "Use the docbridge skill to adopt DocBridge in this repository."`,
181391
+ fallbackPrompt: buildFallbackPrompt(projectRoot, "codex")
181392
+ });
181393
+ }
181394
+ if (agentTarget === "claude" || agentTarget === "both") {
181395
+ guidance.push({
181396
+ agent: "claude",
181397
+ destination: ".claude/skills/docbridge/",
181398
+ oneShotCommand: `cd ${projectRoot} && /docbridge adopt DocBridge in this repository`,
181399
+ fallbackPrompt: buildFallbackPrompt(projectRoot, "claude")
181400
+ });
181401
+ }
181402
+ return guidance;
181403
+ }
181404
+ function buildFallbackPrompt(projectRoot, agent) {
181405
+ const invocation = agent === "claude" ? "Use the docbridge skill" : "Use the docbridge skill from .agents/skills/docbridge";
181406
+ return `${invocation} to adopt DocBridge in ${projectRoot}. Confirm docs and code scope, create or improve docbridge.config.json, and suggest the next linking steps.`;
181407
+ }
181408
+ function resolveInitAgentTarget(discovery, options) {
181409
+ if (options.agentTarget !== undefined) {
181410
+ return { target: options.agentTarget, error: undefined };
181411
+ }
181412
+ if (options.yes) {
181413
+ return { target: discovery.agent.defaultTarget, error: undefined };
181414
+ }
181415
+ return { target: discovery.agent.recommendedTarget, error: undefined };
181416
+ }
181417
+ function resolveInitWithAgentTarget(discovery, options) {
181418
+ if (options.agentTarget !== undefined) {
181419
+ if (options.agentTarget === "none") {
181420
+ return {
181421
+ target: undefined,
181422
+ error: "init-with-agent requires an agent target other than none."
181423
+ };
181424
+ }
181425
+ return { target: options.agentTarget, error: undefined };
181426
+ }
181427
+ if (discovery.agent.defaultTarget === "none") {
181428
+ if (options.yes) {
181429
+ return {
181430
+ target: undefined,
181431
+ error: "init-with-agent --yes requires an explicit agent target when no .agents/ or .claude/ directory exists. Pass --agent-target codex, claude, or both."
181432
+ };
181433
+ }
181434
+ return { target: discovery.agent.recommendedTarget, error: undefined };
181435
+ }
181436
+ return { target: discovery.agent.defaultTarget, error: undefined };
181437
+ }
181438
+ function readExistingConfig(projectRoot) {
181439
+ const configPath = join11(projectRoot, CONFIG_FILE_NAME2);
181440
+ let rawText;
181441
+ try {
181442
+ rawText = readFileSync7(configPath, "utf8");
181443
+ } catch {
181444
+ rawText = undefined;
181445
+ }
181446
+ const resolved = resolveConfig(rawText);
181447
+ return {
181448
+ rawText,
181449
+ ok: resolved.ok,
181450
+ config: resolved.config,
181451
+ diagnostics: resolved.diagnostics
181452
+ };
181453
+ }
181454
+ function summarizeExistingConfig(config, messages) {
181455
+ messages.push(`Docs scope: ${config.include.docs.join(", ")}`);
181456
+ const codeSummary = Object.entries(config.include.code).map(([language, entry]) => `${language}: ${entry?.patterns.join(", ") ?? ""}`).join("; ");
181457
+ messages.push(`Code scope: ${codeSummary}`);
181458
+ }
181459
+ function agentDestinations(agentTarget) {
181460
+ switch (agentTarget) {
181461
+ case "codex":
181462
+ return [".agents/skills"];
181463
+ case "claude":
181464
+ return [".claude/skills"];
181465
+ case "both":
181466
+ return [".agents/skills", ".claude/skills"];
181467
+ default:
181468
+ return [];
181469
+ }
181470
+ }
181471
+ function formatFileOp(operation) {
181472
+ let label = operation.action;
181473
+ if (operation.action === "would-create") {
181474
+ label = "would create";
181475
+ } else if (operation.action === "would-overwrite") {
181476
+ label = "would overwrite";
181477
+ } else if (operation.action === "would-remove") {
181478
+ label = "would remove";
181479
+ }
181480
+ const reason = operation.reason ? ` (${operation.reason})` : "";
181481
+ return `- ${label} ${operation.path}${reason}`;
181482
+ }
181483
+ function capitalize(value) {
181484
+ return value.charAt(0).toUpperCase() + value.slice(1);
181485
+ }
181486
+
180337
181487
  // src/cli/init.ts
180338
181488
  var AGENT_TARGETS = new Set(["codex", "claude", "both", "none"]);
180339
181489
  function parseInitOptions(args, command) {
@@ -180400,7 +181550,7 @@ function createDefaultPrompts(isInteractive = process.stdin.isTTY) {
180400
181550
  return defaultValue;
180401
181551
  }
180402
181552
  process.stdout.write(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"} `);
180403
- const answer = readFileSync5(0, "utf8").trim().toLowerCase();
181553
+ const answer = readFileSync8(0, "utf8").trim().toLowerCase();
180404
181554
  if (answer.length === 0) {
180405
181555
  return defaultValue;
180406
181556
  }
@@ -180417,7 +181567,7 @@ function createDefaultPrompts(isInteractive = process.stdin.isTTY) {
180417
181567
  `);
180418
181568
  }
180419
181569
  process.stdout.write(`Choice [${defaultChoice}]: `);
180420
- const answer = readFileSync5(0, "utf8").trim();
181570
+ const answer = readFileSync8(0, "utf8").trim();
180421
181571
  if (answer.length === 0) {
180422
181572
  return defaultChoice;
180423
181573
  }
@@ -180533,57 +181683,365 @@ function executePlan(projectRoot, plan, packageRoot) {
180533
181683
  if (operation.action === "skip") {
180534
181684
  continue;
180535
181685
  }
180536
- executeSkillOperation(projectRoot, packageRoot, operation);
181686
+ applySkillOperation(projectRoot, packageRoot, operation);
180537
181687
  }
180538
181688
  }
180539
181689
  function executeFileOperation(projectRoot, operation) {
180540
- const absolutePath = join8(projectRoot, operation.path);
181690
+ const absolutePath = join12(projectRoot, operation.path);
180541
181691
  if (operation.action === "create" && operation.content !== undefined) {
180542
- writeFileSync(absolutePath, operation.content, "utf8");
181692
+ writeFileSync2(absolutePath, operation.content, "utf8");
180543
181693
  }
180544
181694
  if (operation.action === "overwrite" && operation.content !== undefined) {
180545
- writeFileSync(absolutePath, operation.content, "utf8");
181695
+ writeFileSync2(absolutePath, operation.content, "utf8");
180546
181696
  }
180547
181697
  }
180548
- function executeSkillOperation(projectRoot, packageRoot, operation) {
180549
- const destinationDir = join8(projectRoot, operation.path);
180550
- if (operation.action === "remove") {
180551
- try {
180552
- if (lstatSync3(destinationDir).isSymbolicLink()) {
180553
- return;
180554
- }
180555
- } catch {
180556
- return;
181698
+ function resolveProjectRoot(root, command) {
181699
+ const projectRoot = resolve3(root);
181700
+ let stats;
181701
+ try {
181702
+ stats = statSync3(projectRoot);
181703
+ } catch {
181704
+ throw new InitCliError(`Root path does not exist: ${root}`, rootPathGuidance(command));
181705
+ }
181706
+ if (!stats.isDirectory()) {
181707
+ throw new InitCliError(`Root path is not a directory: ${root}`, rootPathGuidance(command));
181708
+ }
181709
+ return projectRoot;
181710
+ }
181711
+
181712
+ // src/cli/upgrade.ts
181713
+ import { statSync as statSync4 } from "node:fs";
181714
+ import { resolve as resolve4 } from "node:path";
181715
+
181716
+ // src/setup/upgrade-plan.ts
181717
+ import { existsSync as existsSync5 } from "node:fs";
181718
+ import { join as join13, relative as relative4 } from "node:path";
181719
+ var MANAGED_SKILL_NAME = INIT_SKILL_NAMES[0];
181720
+ function reportCliVersion(currentVersion, latest) {
181721
+ if (latest.status !== "ok") {
181722
+ return { current: currentVersion, latest: undefined, status: "unknown" };
181723
+ }
181724
+ if (isNewerStableVersion(latest.latest, currentVersion)) {
181725
+ return { current: currentVersion, latest: latest.latest, status: "outdated" };
181726
+ }
181727
+ if (isNewerStableVersion(currentVersion, latest.latest)) {
181728
+ return { current: currentVersion, latest: latest.latest, status: "ahead" };
181729
+ }
181730
+ return { current: currentVersion, latest: latest.latest, status: "up-to-date" };
181731
+ }
181732
+ function resolveUpgradeAgentTarget(discovery, explicitTarget) {
181733
+ return explicitTarget ?? discovery.agent.defaultTarget;
181734
+ }
181735
+ function inspectManagedSkill(input) {
181736
+ const destinationDir = join13(input.projectRoot, input.destination, MANAGED_SKILL_NAME);
181737
+ const path = toRootRelative(input.projectRoot, destinationDir);
181738
+ const empty = {
181739
+ destination: input.destination,
181740
+ path,
181741
+ modifiedFiles: [],
181742
+ missingFiles: [],
181743
+ extraFiles: []
181744
+ };
181745
+ const kind = classifyManagedPath(input.projectRoot, path);
181746
+ if (kind !== "directory") {
181747
+ return { ...empty, state: kind };
181748
+ }
181749
+ if (!input.templateAvailable) {
181750
+ return { ...empty, state: "template-missing" };
181751
+ }
181752
+ const comparison = compareSkillTree(destinationDir, join13(input.packageRoot, "templates", "skills", MANAGED_SKILL_NAME));
181753
+ const drifted = comparison.modified.length > 0 || comparison.missing.length > 0 || comparison.extra.length > 0;
181754
+ return {
181755
+ destination: input.destination,
181756
+ path,
181757
+ state: drifted ? "modified" : "up-to-date",
181758
+ modifiedFiles: comparison.modified,
181759
+ missingFiles: comparison.missing,
181760
+ extraFiles: comparison.extra
181761
+ };
181762
+ }
181763
+ function inspectLegacySkills(projectRoot, destination) {
181764
+ const reports = [];
181765
+ for (const skillName of LEGACY_SKILL_NAMES) {
181766
+ const destinationDir = join13(projectRoot, destination, skillName);
181767
+ const path = toRootRelative(projectRoot, destinationDir);
181768
+ const kind = classifyManagedPath(projectRoot, path);
181769
+ if (kind === "absent") {
181770
+ continue;
180557
181771
  }
180558
- rmSync(destinationDir, { recursive: true, force: true });
181772
+ if (kind === "symlinked-parent" && !existsSync5(destinationDir)) {
181773
+ continue;
181774
+ }
181775
+ reports.push({ path, kind });
181776
+ }
181777
+ return reports;
181778
+ }
181779
+ function planUpgrade(input) {
181780
+ const mode = input.options.check ? "check" : "apply";
181781
+ const cli = reportCliVersion(input.currentVersion, input.latest);
181782
+ const agentTarget = resolveUpgradeAgentTarget(input.discovery, input.options.agentTarget);
181783
+ const templateAvailable = listDistributableSkills(input.packageRoot).includes(MANAGED_SKILL_NAME);
181784
+ const messages = [];
181785
+ const destinations = agentDestinations(agentTarget);
181786
+ const managedSkills = destinations.map((destination) => inspectManagedSkill({
181787
+ projectRoot: input.projectRoot,
181788
+ packageRoot: input.packageRoot,
181789
+ destination,
181790
+ templateAvailable
181791
+ }));
181792
+ const legacySkills = destinations.flatMap((destination) => inspectLegacySkills(input.projectRoot, destination));
181793
+ if (destinations.length === 0) {
181794
+ messages.push("No agent skill directory was selected, so no managed assets were inspected. Pass --agent-target codex, claude, or both to reconcile them.");
181795
+ }
181796
+ if (!templateAvailable && destinations.length > 0) {
181797
+ messages.push("The installed DocBridge package ships no docbridge skill template, so managed skills cannot be reconciled. Reinstall DocBridge.");
181798
+ }
181799
+ addVersionMessages(cli, input.guidance, messages);
181800
+ const pending = [];
181801
+ const operations = mode === "check" ? [] : planOperations({ managedSkills, legacySkills, options: input.options, pending });
181802
+ for (const report of managedSkills) {
181803
+ if (isUnmanageableState(report.state)) {
181804
+ messages.push(unmanageablePathMessage(report.path, report.state));
181805
+ }
181806
+ }
181807
+ for (const legacy of legacySkills) {
181808
+ if (legacy.kind !== "directory") {
181809
+ messages.push(unmanageablePathMessage(legacy.path, legacy.kind));
181810
+ }
181811
+ }
181812
+ return {
181813
+ mode,
181814
+ cli,
181815
+ guidance: input.guidance,
181816
+ managedSkills,
181817
+ legacySkills,
181818
+ operations,
181819
+ pending,
181820
+ messages,
181821
+ nextSteps: buildNextSteps({ mode, cli, options: input.options, pending }),
181822
+ requiresConfirmation: mode === "apply" && !input.options.dryRun && !input.options.yes && operations.some((operation) => operation.action === "overwrite" || operation.action === "remove"),
181823
+ exitCode: 0
181824
+ };
181825
+ }
181826
+ function planOperations(input) {
181827
+ const operations = [];
181828
+ for (const report of input.managedSkills) {
181829
+ if (report.state === "template-missing" || isUnmanageableState(report.state)) {
181830
+ continue;
181831
+ }
181832
+ if (report.state === "absent") {
181833
+ operations.push({
181834
+ action: input.options.dryRun ? "would-create" : "create",
181835
+ path: report.path,
181836
+ reason: "Install the managed docbridge skill."
181837
+ });
181838
+ continue;
181839
+ }
181840
+ if (!input.options.force) {
181841
+ input.pending.push(`${report.path} is ${report.state === "modified" ? "locally modified" : "already installed"} and was preserved. Re-run with --force to replace it with the packaged template.`);
181842
+ continue;
181843
+ }
181844
+ operations.push({
181845
+ action: input.options.dryRun ? "would-overwrite" : "overwrite",
181846
+ path: report.path,
181847
+ reason: "Replace the managed docbridge skill with the packaged template."
181848
+ });
181849
+ }
181850
+ for (const legacy of input.legacySkills) {
181851
+ if (legacy.kind !== "directory") {
181852
+ continue;
181853
+ }
181854
+ if (!input.options.force) {
181855
+ input.pending.push(`${legacy.path} is a leftover directory from the previous five-skill layout. Re-run with --force to remove it after reviewing local edits.`);
181856
+ continue;
181857
+ }
181858
+ operations.push({
181859
+ action: input.options.dryRun ? "would-remove" : "remove",
181860
+ path: legacy.path,
181861
+ reason: "Remove leftover skill from the previous five-skill layout."
181862
+ });
181863
+ }
181864
+ return operations;
181865
+ }
181866
+ function isUnmanageableState(state) {
181867
+ return state === "symlink" || state === "symlinked-parent" || state === "non-directory" || state === "blocked-parent";
181868
+ }
181869
+ function addVersionMessages(cli, guidance, messages) {
181870
+ if (cli.status === "outdated") {
181871
+ messages.push(`The running binary is older than the latest stable release. DocBridge does not upgrade itself: run \`${guidance.primaryCommand}\`, then re-run \`docbridge upgrade\` so managed assets are reconciled from the new binary.`);
180559
181872
  return;
180560
181873
  }
180561
- if (operation.action !== "create" && operation.action !== "overwrite") {
181874
+ if (cli.status === "unknown") {
181875
+ messages.push("The latest stable version could not be determined, so only local asset state is reported.");
181876
+ }
181877
+ }
181878
+ function buildNextSteps(input) {
181879
+ const nextSteps = [];
181880
+ if (input.cli.status === "outdated") {
181881
+ nextSteps.push("Upgrade the CLI with the command above, then re-run `docbridge upgrade`.");
181882
+ }
181883
+ if (input.mode === "check") {
181884
+ nextSteps.push("Run `docbridge upgrade --dry-run` to see the operations that would be applied.");
181885
+ }
181886
+ if (input.pending.length > 0) {
181887
+ nextSteps.push("Re-run with --force to apply the pending migrations listed above.");
181888
+ }
181889
+ if (input.mode === "apply" && input.options.dryRun) {
181890
+ nextSteps.push("Re-run without --dry-run to apply the plan.");
181891
+ }
181892
+ return nextSteps;
181893
+ }
181894
+ function formatUpgradePlan(plan) {
181895
+ const lines = [
181896
+ `DocBridge ${plan.cli.current} (latest stable: ${plan.cli.latest ?? "unknown"})`,
181897
+ `Status: ${plan.cli.status}`,
181898
+ ...formatUpgradeGuidance(plan.guidance)
181899
+ ];
181900
+ if (plan.messages.length > 0) {
181901
+ lines.push("", ...plan.messages);
181902
+ }
181903
+ appendSection(lines, "Managed skills:", plan.managedSkills.flatMap(formatManagedSkill));
181904
+ appendSection(lines, "Legacy skills:", plan.legacySkills.map((legacy) => `- ${legacy.path} (${legacy.kind})`));
181905
+ appendSection(lines, "Operations:", plan.operations.map((operation) => formatFileOp(operation)));
181906
+ appendSection(lines, "Pending migration:", plan.pending.map((entry) => `- ${entry}`));
181907
+ appendSection(lines, "Next steps:", plan.nextSteps.map((step) => `- ${step}`));
181908
+ return `${lines.join(`
181909
+ `)}
181910
+ `;
181911
+ }
181912
+ function formatManagedSkill(report) {
181913
+ const lines = [`- ${report.path}: ${report.state}`];
181914
+ appendFileList(lines, "changed", report.modifiedFiles);
181915
+ appendFileList(lines, "missing", report.missingFiles);
181916
+ appendFileList(lines, "extra", report.extraFiles);
181917
+ return lines;
181918
+ }
181919
+ function appendFileList(lines, label, files) {
181920
+ if (files.length > 0) {
181921
+ lines.push(` ${label}: ${files.join(", ")}`);
181922
+ }
181923
+ }
181924
+ function appendSection(lines, heading, entries) {
181925
+ if (entries.length === 0) {
180562
181926
  return;
180563
181927
  }
180564
- try {
180565
- if (lstatSync3(destinationDir).isSymbolicLink()) {
180566
- return;
181928
+ lines.push("", heading, ...entries);
181929
+ }
181930
+ function toRootRelative(projectRoot, absolutePath) {
181931
+ return relative4(projectRoot, absolutePath).split("\\").join("/");
181932
+ }
181933
+
181934
+ // src/cli/upgrade.ts
181935
+ var AGENT_TARGETS2 = new Set(["codex", "claude", "both", "none"]);
181936
+ function parseUpgradeOptions(args) {
181937
+ const options = {
181938
+ root: ".",
181939
+ agentTarget: undefined,
181940
+ check: false,
181941
+ dryRun: false,
181942
+ yes: false,
181943
+ force: false
181944
+ };
181945
+ for (let index = 0;index < args.length; index += 1) {
181946
+ const arg = args[index];
181947
+ if (arg === undefined) {
181948
+ continue;
180567
181949
  }
180568
- } catch {}
180569
- const skillName = operation.path.split("/").at(-1);
180570
- if (skillName === undefined) {
180571
- return;
181950
+ if (arg === "--check") {
181951
+ options.check = true;
181952
+ continue;
181953
+ }
181954
+ if (arg === "--dry-run") {
181955
+ options.dryRun = true;
181956
+ continue;
181957
+ }
181958
+ if (arg === "--yes") {
181959
+ options.yes = true;
181960
+ continue;
181961
+ }
181962
+ if (arg === "--force") {
181963
+ options.force = true;
181964
+ continue;
181965
+ }
181966
+ if (arg === "--root") {
181967
+ const root = args[index + 1];
181968
+ if (root === undefined) {
181969
+ throw new CliError("--root requires a path.", rootPathGuidance("upgrade"));
181970
+ }
181971
+ options.root = root;
181972
+ index += 1;
181973
+ continue;
181974
+ }
181975
+ if (arg === "--agent-target") {
181976
+ options.agentTarget = parseAgentTarget(args[index + 1]);
181977
+ index += 1;
181978
+ continue;
181979
+ }
181980
+ if (arg.startsWith("--")) {
181981
+ throw new CliError(`Unknown option: ${arg}`, commandHelpGuidance("upgrade"));
181982
+ }
181983
+ throw new CliError(`Unexpected argument: ${arg}`, commandHelpGuidance("upgrade"));
180572
181984
  }
180573
- const sourceDir = join8(packageRoot, "templates", "skills", skillName);
180574
- mkdirSync2(destinationDir, { recursive: true });
180575
- cpSync(sourceDir, destinationDir, { recursive: true, force: true });
181985
+ return options;
180576
181986
  }
180577
- function resolveProjectRoot(root, command) {
180578
- const projectRoot = resolve3(root);
181987
+ function runUpgrade(options, io, initRuntime, runtime) {
181988
+ const projectRoot = resolveProjectRoot2(options.root);
181989
+ const packageRoot = initRuntime.packageRoot ?? resolvePackageRoot();
181990
+ const plan = planUpgrade({
181991
+ projectRoot,
181992
+ packageRoot,
181993
+ currentVersion: runtime.currentVersion,
181994
+ latest: runtime.latest,
181995
+ guidance: detectUpgradeGuidance({
181996
+ packageRoot,
181997
+ projectRoot,
181998
+ ...runtime.currentDirectory !== undefined ? { currentDirectory: runtime.currentDirectory } : {},
181999
+ ...runtime.env !== undefined ? { env: runtime.env } : {}
182000
+ }),
182001
+ discovery: discoverRepository(projectRoot),
182002
+ options
182003
+ });
182004
+ if (plan.mode === "check" || options.dryRun) {
182005
+ io.stdout(formatUpgradePlan(plan));
182006
+ return plan.exitCode;
182007
+ }
182008
+ return applyUpgrade(projectRoot, packageRoot, plan, io, initRuntime);
182009
+ }
182010
+ function applyUpgrade(projectRoot, packageRoot, plan, io, initRuntime) {
182011
+ if (plan.requiresConfirmation && !initRuntime.prompts.isInteractive) {
182012
+ throw new CliError("Replacing or removing managed skill directories requires confirmation.", upgradeConfirmationGuidance());
182013
+ }
182014
+ if (plan.requiresConfirmation && !initRuntime.prompts.confirm(`Apply ${plan.operations.length} operation(s) to managed skill directories?`, false)) {
182015
+ io.stdout(formatUpgradePlan(plan));
182016
+ io.stdout(`No changes were applied.
182017
+ `);
182018
+ return 0;
182019
+ }
182020
+ for (const operation of plan.operations) {
182021
+ applySkillOperation(projectRoot, packageRoot, operation);
182022
+ }
182023
+ io.stdout(formatUpgradePlan(plan));
182024
+ return plan.exitCode;
182025
+ }
182026
+ function parseAgentTarget(value) {
182027
+ if (value === undefined) {
182028
+ throw new CliError("--agent-target requires a value.", agentTargetGuidance("upgrade"));
182029
+ }
182030
+ if (!AGENT_TARGETS2.has(value)) {
182031
+ throw new CliError(`Unknown agent target: ${value}. Supported values: codex, claude, both, none.`, agentTargetGuidance("upgrade"));
182032
+ }
182033
+ return value;
182034
+ }
182035
+ function resolveProjectRoot2(root) {
182036
+ const projectRoot = resolve4(root);
180579
182037
  let stats;
180580
182038
  try {
180581
- stats = statSync3(projectRoot);
182039
+ stats = statSync4(projectRoot);
180582
182040
  } catch {
180583
- throw new InitCliError(`Root path does not exist: ${root}`, rootPathGuidance(command));
182041
+ throw new CliError(`Root path does not exist: ${root}`, rootPathGuidance("upgrade"));
180584
182042
  }
180585
182043
  if (!stats.isDirectory()) {
180586
- throw new InitCliError(`Root path is not a directory: ${root}`, rootPathGuidance(command));
182044
+ throw new CliError(`Root path is not a directory: ${root}`, rootPathGuidance("upgrade"));
180587
182045
  }
180588
182046
  return projectRoot;
180589
182047
  }
@@ -180602,11 +182060,11 @@ function parseContextOptions(args) {
180602
182060
  function parseGraphOptions(args) {
180603
182061
  return parseCommandOptions("graph", args);
180604
182062
  }
180605
- function resolveProjectRoot2(root, command) {
180606
- const projectRoot = resolve4(root);
182063
+ function resolveProjectRoot3(root, command) {
182064
+ const projectRoot = resolve5(root);
180607
182065
  let stats;
180608
182066
  try {
180609
- stats = statSync4(projectRoot);
182067
+ stats = statSync5(projectRoot);
180610
182068
  } catch {
180611
182069
  throw new CliError(`Root path does not exist: ${root}`, rootPathGuidance(command));
180612
182070
  }
@@ -180616,7 +182074,7 @@ function resolveProjectRoot2(root, command) {
180616
182074
  return projectRoot;
180617
182075
  }
180618
182076
  function runCheck(options, io) {
180619
- const projectRoot = resolveProjectRoot2(options.root, "check");
182077
+ const projectRoot = resolveProjectRoot3(options.root, "check");
180620
182078
  const result = check({ projectRoot, audit: options.audit });
180621
182079
  if (options.json) {
180622
182080
  io.stdout(`${JSON.stringify(result, null, 2)}
@@ -180630,8 +182088,8 @@ function runCheck(options, io) {
180630
182088
  io.stdout(`${body}${formatSummary(result.summary)}
180631
182089
  See \`docbridge docs show troubleshooting\` for diagnostic codes and fixes.
180632
182090
  `);
180633
- if (result.diagnostics.some((diagnostic) => diagnostic.code === "config_file_invalid")) {
180634
- const guidance = existsSync4(join9(projectRoot, CONFIG_FILE_NAME)) ? configRepairGuidance() : configSetupGuidance();
182091
+ const guidance = repairGuidanceFor(result.diagnostics, projectRoot);
182092
+ if (guidance !== undefined) {
180635
182093
  io.stderr(`${guidance}
180636
182094
  `);
180637
182095
  }
@@ -180639,10 +182097,10 @@ See \`docbridge docs show troubleshooting\` for diagnostic codes and fixes.
180639
182097
  return result.summary.errors > 0 ? 1 : 0;
180640
182098
  }
180641
182099
  function runFileCommand(command, options, io, execute, render) {
180642
- const projectRoot = resolveProjectRoot2(options.root, command);
182100
+ const projectRoot = resolveProjectRoot3(options.root, command);
180643
182101
  const inputFiles = [...options.files];
180644
182102
  if (options.stdin) {
180645
- const readStdin = io.stdin ?? (() => readFileSync6(0, "utf8"));
182103
+ const readStdin = io.stdin ?? (() => readFileSync9(0, "utf8"));
180646
182104
  inputFiles.push(...readStdin().split(`
180647
182105
  `));
180648
182106
  }
@@ -180722,8 +182180,18 @@ function runGraph(options, io) {
180722
182180
  return 0;
180723
182181
  });
180724
182182
  }
182183
+ function repairGuidanceFor(diagnostics, projectRoot) {
182184
+ const invalid = diagnostics.filter((diagnostic) => diagnostic.code === "config_file_invalid");
182185
+ if (invalid.some((diagnostic) => diagnostic.target === LINK_MANIFEST_FILE_NAME)) {
182186
+ return manifestRepairGuidance();
182187
+ }
182188
+ if (invalid.length === 0) {
182189
+ return;
182190
+ }
182191
+ return existsSync6(join14(projectRoot, CONFIG_FILE_NAME)) ? configRepairGuidance() : configSetupGuidance();
182192
+ }
180725
182193
  function unknownCommandGuidance(command) {
180726
- const suggestion = nearestSubcommand(command);
182194
+ const suggestion = nearestMatch(command, SUBCOMMANDS);
180727
182195
  const lines = ["Available commands:", ` ${SUBCOMMANDS.join(", ")}`];
180728
182196
  if (suggestion !== undefined) {
180729
182197
  lines.push("", `Did you mean \`${suggestion}\`?`);
@@ -180732,49 +182200,6 @@ function unknownCommandGuidance(command) {
180732
182200
  return lines.join(`
180733
182201
  `);
180734
182202
  }
180735
- function nearestSubcommand(input) {
180736
- const ranked = SUBCOMMANDS.map((command, index) => ({
180737
- command,
180738
- distance: editDistance(input, command),
180739
- index
180740
- })).toSorted((left, right) => left.distance - right.distance || left.index - right.index);
180741
- const abbreviation = ranked.find(({ command }) => isOrderedAbbreviation(input, command));
180742
- if (abbreviation !== undefined) {
180743
- return abbreviation.command;
180744
- }
180745
- const best = ranked[0];
180746
- if (best === undefined) {
180747
- return;
180748
- }
180749
- const closeEnough = best.distance <= Math.max(1, Math.floor(best.command.length / 2));
180750
- return closeEnough ? best.command : undefined;
180751
- }
180752
- function isOrderedAbbreviation(input, command) {
180753
- if (input.length < 3 || input.length >= command.length) {
180754
- return false;
180755
- }
180756
- let commandIndex = 0;
180757
- for (const character of input) {
180758
- const matchIndex = command.indexOf(character, commandIndex);
180759
- if (matchIndex === -1) {
180760
- return false;
180761
- }
180762
- commandIndex = matchIndex + 1;
180763
- }
180764
- return true;
180765
- }
180766
- function editDistance(left, right) {
180767
- let previous = Array.from({ length: right.length + 1 }, (_, index) => index);
180768
- for (let leftIndex = 1;leftIndex <= left.length; leftIndex += 1) {
180769
- const current = [leftIndex];
180770
- for (let rightIndex = 1;rightIndex <= right.length; rightIndex += 1) {
180771
- const substitutionCost = left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1;
180772
- current[rightIndex] = Math.min((current[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex] ?? Number.POSITIVE_INFINITY) + 1, (previous[rightIndex - 1] ?? Number.POSITIVE_INFINITY) + substitutionCost);
180773
- }
180774
- previous = current;
180775
- }
180776
- return previous[right.length] ?? 0;
180777
- }
180778
182203
  var COMMAND_HANDLERS = {
180779
182204
  check: (args, io) => runCheck(parseCheckOptions(args), io),
180780
182205
  related: (args, io) => runRelated(parseRelatedOptions(args), io),
@@ -180783,6 +182208,12 @@ var COMMAND_HANDLERS = {
180783
182208
  docs: (args, io) => runDocs(parseDocsCommand(args), io),
180784
182209
  init: (args, io, initRuntime) => runInit(parseInitOptions(args, "init"), io, initRuntime),
180785
182210
  "init-with-agent": (args, io, initRuntime) => runInitWithAgent(parseInitOptions(args, "init-with-agent"), io, initRuntime),
182211
+ upgrade: (args, io, initRuntime, cliRuntime) => runUpgrade(parseUpgradeOptions(args), io, initRuntime, {
182212
+ latest: cliRuntime.latest ?? { status: "unavailable", source: "network" },
182213
+ currentVersion: VERSION,
182214
+ ...cliRuntime.env !== undefined ? { env: cliRuntime.env } : {},
182215
+ ...cliRuntime.currentDirectory !== undefined ? { currentDirectory: cliRuntime.currentDirectory } : {}
182216
+ }),
180786
182217
  lsp: (args) => {
180787
182218
  if (args.length > 0) {
180788
182219
  throw new CliError("lsp takes no options.", commandHelpGuidance("lsp"));
@@ -180794,11 +182225,12 @@ var COMMAND_HANDLERS = {
180794
182225
  function run(argv, io = {
180795
182226
  stdout: (text) => process.stdout.write(text),
180796
182227
  stderr: (text) => process.stderr.write(text)
180797
- }, initRuntime = { prompts: createDefaultPrompts() }) {
182228
+ }, initRuntime = { prompts: createDefaultPrompts() }, cliRuntime = {}) {
180798
182229
  const [command, ...rest] = argv;
180799
182230
  try {
180800
182231
  if (command === undefined || command === "--help" || command === "-h") {
180801
182232
  io.stdout(GLOBAL_HELP);
182233
+ writeUpdateNotice(argv, io, initRuntime, cliRuntime);
180802
182234
  return 0;
180803
182235
  }
180804
182236
  if (isSubcommand(command) && hasHelpFlag(rest)) {
@@ -180808,10 +182240,13 @@ function run(argv, io = {
180808
182240
  if (command === "--version" || command === "-v") {
180809
182241
  io.stdout(`${VERSION}
180810
182242
  `);
182243
+ writeUpdateNotice(argv, io, initRuntime, cliRuntime);
180811
182244
  return 0;
180812
182245
  }
180813
182246
  if (isSubcommand(command)) {
180814
- return COMMAND_HANDLERS[command](rest, io, initRuntime);
182247
+ const exitCode = COMMAND_HANDLERS[command](rest, io, initRuntime, cliRuntime);
182248
+ writeUpdateNotice(argv, io, initRuntime, cliRuntime);
182249
+ return exitCode;
180815
182250
  }
180816
182251
  throw new CliError(`Unknown command: ${command}`, unknownCommandGuidance(command));
180817
182252
  } catch (error) {
@@ -180825,8 +182260,59 @@ function run(argv, io = {
180825
182260
  return 1;
180826
182261
  }
180827
182262
  }
182263
+ function rootOptionOf(argv, currentDirectory) {
182264
+ const flagIndex = argv.indexOf("--root");
182265
+ const value = flagIndex === -1 ? undefined : argv[flagIndex + 1];
182266
+ return value === undefined ? currentDirectory : resolve5(currentDirectory, value);
182267
+ }
182268
+ function writeUpdateNotice(argv, io, initRuntime, cliRuntime) {
182269
+ const latest = cliRuntime.latest;
182270
+ if (latest === undefined || latest.status !== "ok") {
182271
+ return;
182272
+ }
182273
+ if (!decideUpdateCheck({
182274
+ argv,
182275
+ ...cliRuntime.env !== undefined ? { env: cliRuntime.env } : {},
182276
+ isTty: cliRuntime.isTty ?? false
182277
+ }).enabled) {
182278
+ return;
182279
+ }
182280
+ const packageRoot = initRuntime.packageRoot ?? resolvePackageRoot();
182281
+ const currentDirectory = cliRuntime.currentDirectory ?? process.cwd();
182282
+ const notice = formatUpdateNotice({
182283
+ current: VERSION,
182284
+ latest: latest.latest,
182285
+ guidance: detectUpgradeGuidance({
182286
+ packageRoot,
182287
+ projectRoot: rootOptionOf(argv, currentDirectory),
182288
+ currentDirectory,
182289
+ ...cliRuntime.env !== undefined ? { env: cliRuntime.env } : {}
182290
+ })
182291
+ });
182292
+ if (notice !== undefined) {
182293
+ io.stderr(notice);
182294
+ }
182295
+ }
182296
+ async function resolveLatestForInvocation(argv, env, isTty) {
182297
+ if (isUpdateCheckOptedOut(env)) {
182298
+ return;
182299
+ }
182300
+ if (argv[0] === "upgrade" && !hasHelpFlag(argv.slice(1))) {
182301
+ return resolveLatestStableVersion({ forceRefresh: true });
182302
+ }
182303
+ if (!decideUpdateCheck({ argv, env, isTty }).enabled) {
182304
+ return;
182305
+ }
182306
+ return resolveLatestStableVersion();
182307
+ }
180828
182308
  if (__require.main == __require.module) {
180829
- process.exitCode = run(process.argv.slice(2));
182309
+ const argv = process.argv.slice(2);
182310
+ const isTty = process.stderr.isTTY === true;
182311
+ const latest = await resolveLatestForInvocation(argv, process.env, isTty);
182312
+ process.exitCode = run(argv, {
182313
+ stdout: (text) => process.stdout.write(text),
182314
+ stderr: (text) => process.stderr.write(text)
182315
+ }, { prompts: createDefaultPrompts() }, { ...latest !== undefined ? { latest } : {}, env: process.env, isTty });
180830
182316
  }
180831
182317
  export {
180832
182318
  run,