@driftless-sh/cli 0.1.44 → 0.1.46

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
@@ -213426,6 +213426,65 @@ var require_nestjs_extractor = __commonJS({
213426
213426
  }
213427
213427
  return null;
213428
213428
  }
213429
+ function argText(arg) {
213430
+ if (typescript_1.default.isStringLiteralLike(arg))
213431
+ return [arg.text];
213432
+ if (typescript_1.default.isIdentifier(arg))
213433
+ return [arg.text];
213434
+ if (typescript_1.default.isObjectLiteralExpression(arg)) {
213435
+ const out = [];
213436
+ for (const prop of arg.properties) {
213437
+ if (typescript_1.default.isPropertyAssignment(prop) && typescript_1.default.isIdentifier(prop.name) && prop.name.text === "path" && typescript_1.default.isStringLiteralLike(prop.initializer)) {
213438
+ out.push(prop.initializer.text);
213439
+ }
213440
+ }
213441
+ return out;
213442
+ }
213443
+ return [];
213444
+ }
213445
+ function decoratorArgsOf(decs) {
213446
+ const map = {};
213447
+ for (const d of decs || []) {
213448
+ const name = getDecoratorName(d);
213449
+ if (!name)
213450
+ continue;
213451
+ const args = [];
213452
+ if (typescript_1.default.isCallExpression(d.expression)) {
213453
+ for (const a of d.expression.arguments)
213454
+ args.push(...argText(a));
213455
+ }
213456
+ if (!(name in map))
213457
+ map[name] = args;
213458
+ }
213459
+ return map;
213460
+ }
213461
+ function moduleMetaOf(decs) {
213462
+ for (const d of decs || []) {
213463
+ if (getDecoratorName(d) !== "Module")
213464
+ continue;
213465
+ if (!typescript_1.default.isCallExpression(d.expression) || d.expression.arguments.length === 0)
213466
+ return { imports: [], providers: [], controllers: [] };
213467
+ const obj = d.expression.arguments[0];
213468
+ const meta = { imports: [], providers: [], controllers: [] };
213469
+ if (typescript_1.default.isObjectLiteralExpression(obj)) {
213470
+ for (const prop of obj.properties) {
213471
+ if (!typescript_1.default.isPropertyAssignment(prop) || !typescript_1.default.isIdentifier(prop.name))
213472
+ continue;
213473
+ const key = prop.name.text;
213474
+ if (key !== "imports" && key !== "providers" && key !== "controllers")
213475
+ continue;
213476
+ if (!typescript_1.default.isArrayLiteralExpression(prop.initializer))
213477
+ continue;
213478
+ for (const el of prop.initializer.elements) {
213479
+ if (typescript_1.default.isIdentifier(el))
213480
+ meta[key].push(el.text);
213481
+ }
213482
+ }
213483
+ }
213484
+ return meta;
213485
+ }
213486
+ return void 0;
213487
+ }
213429
213488
  function extractTypeNode(node) {
213430
213489
  if (!node)
213431
213490
  return "unknown";
@@ -213505,6 +213564,7 @@ var require_nestjs_extractor = __commonJS({
213505
213564
  name: member.name.text,
213506
213565
  line: sourceFile.getLineAndCharacterOfPosition(member.getStart(sourceFile)).line + 1,
213507
213566
  decorators,
213567
+ decoratorArgs: decoratorArgsOf(typescript_1.default.getDecorators(member)),
213508
213568
  params,
213509
213569
  returnType
213510
213570
  });
@@ -213554,10 +213614,13 @@ var require_nestjs_extractor = __commonJS({
213554
213614
  }
213555
213615
  }
213556
213616
  }
213617
+ const classDecs = typescript_1.default.getDecorators(node);
213557
213618
  facts.classes.push({
213558
213619
  name: className,
213559
213620
  line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
213560
213621
  decorators,
213622
+ decoratorArgs: decoratorArgsOf(classDecs),
213623
+ moduleMeta: moduleMetaOf(classDecs),
213561
213624
  implements: implementsList,
213562
213625
  constructorParams: extractConstructorParams(node),
213563
213626
  methods: extractMethods(node),
@@ -213594,58 +213657,16 @@ var require_nestjs_extractor = __commonJS({
213594
213657
  visit(sourceFile);
213595
213658
  return facts;
213596
213659
  }
213597
- function buildSymbolIndex(allFacts) {
213598
- const index = /* @__PURE__ */ new Map();
213599
- for (const facts of allFacts) {
213600
- for (const cls of facts.classes) {
213601
- index.set(cls.name, { filePath: facts.filePath, kind: "class", decorators: cls.decorators, implements: cls.implements });
213602
- }
213603
- for (const fn of facts.functions) {
213604
- index.set(fn.name, { filePath: facts.filePath, kind: "function", decorators: fn.decorators, implements: [] });
213605
- }
213606
- for (const iface of facts.interfaces) {
213607
- index.set(iface.name, { filePath: facts.filePath, kind: "interface", decorators: [], implements: [] });
213608
- }
213660
+ function indexFileSymbols(facts, index) {
213661
+ for (const cls of facts.classes) {
213662
+ index.set(cls.name, { filePath: facts.filePath, kind: "class", decorators: cls.decorators, implements: cls.implements });
213609
213663
  }
213610
- return index;
213611
- }
213612
- function buildReferenceIndex(allFacts, symbolIndex) {
213613
- const index = /* @__PURE__ */ new Map();
213614
- for (const facts of allFacts) {
213615
- const importedNames = /* @__PURE__ */ new Map();
213616
- for (const imp of facts.imports) {
213617
- importedNames.set(imp.localName, imp.importedName);
213618
- }
213619
- for (const cls of facts.classes) {
213620
- for (const param of cls.constructorParams) {
213621
- const resolvedType = normalizeTypeName(param.type);
213622
- if (symbolIndex.has(resolvedType) && !isPrimitive(resolvedType)) {
213623
- const entries = index.get(resolvedType) || [];
213624
- entries.push({ filePath: facts.filePath, context: "constructor-param", ownerSymbol: cls.name });
213625
- index.set(resolvedType, entries);
213626
- }
213627
- }
213628
- for (const method of cls.methods) {
213629
- for (const param of method.params) {
213630
- const resolvedType = normalizeTypeName(param.type);
213631
- if (symbolIndex.has(resolvedType) && !isPrimitive(resolvedType)) {
213632
- const entries = index.get(resolvedType) || [];
213633
- entries.push({ filePath: facts.filePath, context: "method-param", ownerSymbol: `${cls.name}.${method.name}` });
213634
- index.set(resolvedType, entries);
213635
- }
213636
- }
213637
- }
213638
- }
213639
- for (const imp of facts.imports) {
213640
- const targetName = imp.importedName !== "default" ? imp.importedName : imp.localName;
213641
- if (symbolIndex.has(targetName)) {
213642
- const entries = index.get(targetName) || [];
213643
- entries.push({ filePath: facts.filePath, context: "import" });
213644
- index.set(targetName, entries);
213645
- }
213646
- }
213664
+ for (const fn of facts.functions) {
213665
+ index.set(fn.name, { filePath: facts.filePath, kind: "function", decorators: fn.decorators, implements: [] });
213666
+ }
213667
+ for (const iface of facts.interfaces) {
213668
+ index.set(iface.name, { filePath: facts.filePath, kind: "interface", decorators: [], implements: [] });
213647
213669
  }
213648
- return index;
213649
213670
  }
213650
213671
  function normalizeTypeName(typeText) {
213651
213672
  return typeText.replace(/^typeof\s+/, "").replace(/^import\(".*"\)\./, "").replace(/<.*>$/, "").replace(/\[\]$/, "").split("|")[0].trim();
@@ -213653,15 +213674,6 @@ var require_nestjs_extractor = __commonJS({
213653
213674
  function isPrimitive(typeName) {
213654
213675
  return ["string", "number", "boolean", "object", "unknown", "any", "void", "Promise", "Date", "Array", "Record", "Partial", "Required", "Pick", "Omit", "Readonly"].includes(typeName);
213655
213676
  }
213656
- function extractModuleFromPath(filePath) {
213657
- const segments = filePath.split("/");
213658
- const srcIndex = segments.indexOf("src");
213659
- if (srcIndex >= 0 && srcIndex + 1 < segments.length) {
213660
- const next = segments[srcIndex + 1];
213661
- return next.replace(/\.module\.ts$/, "").replace(/\.ts$/, "");
213662
- }
213663
- return "root";
213664
- }
213665
213677
  function isInSourceDir(filePath) {
213666
213678
  if (filePath.includes("node_modules") || filePath.includes("/dist/"))
213667
213679
  return false;
@@ -213767,74 +213779,27 @@ var require_nestjs_extractor = __commonJS({
213767
213779
  }
213768
213780
  return false;
213769
213781
  }
213770
- function extractControllerPath(decorators, facts, clsName) {
213771
- const fullPath = node_path_1.default.join(process.cwd(), facts.filePath);
213772
- try {
213773
- const content = node_fs_1.default.readFileSync(fullPath, "utf8");
213774
- const match = content.match(/@Controller\s*\(\s*['"]([^'"]+)['"]\s*\)/);
213775
- if (match)
213776
- return match[1];
213777
- } catch {
213778
- }
213779
- return "";
213782
+ function joinRoute(base, sub) {
213783
+ const parts = [base, sub].map((s) => (s || "").trim().replace(/^\/+|\/+$/g, "")).filter(Boolean);
213784
+ const joined = parts.join("/").replace(/\/{2,}/g, "/");
213785
+ return "/" + joined;
213786
+ }
213787
+ function extractControllerPath(cls) {
213788
+ return cls.decoratorArgs["Controller"]?.[0] ?? "";
213780
213789
  }
213781
- function extractEndpointPath(methodDecorators, facts, clsPath, methodName) {
213790
+ function extractEndpointPath(method, clsPath) {
213782
213791
  const results = [];
213783
- const fullPath = node_path_1.default.join(process.cwd(), facts.filePath);
213784
- try {
213785
- const content = node_fs_1.default.readFileSync(fullPath, "utf8");
213786
- const lines = content.split("\n");
213787
- for (let i = 0; i < lines.length; i++) {
213788
- const line = lines[i];
213789
- if (line.includes(methodName) && (line.includes("(") || line.includes(":"))) {
213790
- for (let j = i - 1; j >= Math.max(0, i - 10); j--) {
213791
- const decoratorLine = lines[j].trim();
213792
- for (const [decoratorName, httpMethod] of Object.entries(HTTP_METHODS)) {
213793
- const match = decoratorLine.match(new RegExp(`@${decoratorName}\\s*\\(\\s*['"]([^'"]+)['"]\\s*\\)`));
213794
- if (match) {
213795
- results.push({ method: httpMethod, path: clsPath + match[1] });
213796
- } else if (decoratorLine.match(new RegExp(`@${decoratorName}\\s*\\(\\s*\\)`)) || decoratorLine.match(new RegExp(`@${decoratorName}$`))) {
213797
- results.push({ method: httpMethod, path: clsPath || "/" });
213798
- }
213799
- }
213800
- }
213801
- break;
213802
- }
213803
- }
213804
- } catch {
213805
- for (const dec of methodDecorators) {
213806
- const httpMethod = HTTP_METHODS[dec];
213807
- if (httpMethod) {
213808
- results.push({ method: httpMethod, path: clsPath || "/" });
213809
- }
213810
- }
213792
+ for (const dec of method.decorators) {
213793
+ const httpMethod = HTTP_METHODS[dec];
213794
+ if (!httpMethod)
213795
+ continue;
213796
+ const sub = method.decoratorArgs[dec]?.[0] ?? "";
213797
+ results.push({ method: httpMethod, path: joinRoute(clsPath, sub) });
213811
213798
  }
213812
- return results.length > 0 ? results : methodDecorators.map((d) => HTTP_METHODS[d]).filter(Boolean).map((m) => ({ method: m, path: clsPath || "/" }));
213799
+ return results;
213813
213800
  }
213814
- function extractGuardsFromMethod(facts, methodName) {
213815
- const guards = [];
213816
- const fullPath = node_path_1.default.join(process.cwd(), facts.filePath);
213817
- try {
213818
- const content = node_fs_1.default.readFileSync(fullPath, "utf8");
213819
- const lines = content.split("\n");
213820
- for (let i = 0; i < lines.length; i++) {
213821
- const line = lines[i];
213822
- if (line.includes(methodName) && (line.includes("(") || line.includes(":"))) {
213823
- for (let j = i - 1; j >= Math.max(0, i - 10); j--) {
213824
- const decoratorLine = lines[j].trim();
213825
- const useGuardsMatch = decoratorLine.match(/@UseGuards\s*\((.+)\)/);
213826
- if (useGuardsMatch) {
213827
- const guardNames = useGuardsMatch[1].match(/[A-Z]\w*Guard/g);
213828
- if (guardNames)
213829
- guards.push(...guardNames);
213830
- }
213831
- }
213832
- break;
213833
- }
213834
- }
213835
- } catch {
213836
- }
213837
- return guards;
213801
+ function extractGuardsFromMethod(method) {
213802
+ return (method.decoratorArgs["UseGuards"] ?? []).filter((g) => /^[A-Z]\w*$/.test(g));
213838
213803
  }
213839
213804
  function extractEndpointParams(method) {
213840
213805
  const params = [];
@@ -213871,35 +213836,48 @@ var require_nestjs_extractor = __commonJS({
213871
213836
  };
213872
213837
  });
213873
213838
  }
213839
+ function stamp(c, confidence) {
213840
+ c.metadata.evidence = { file: c.file_path, line: c.line_number };
213841
+ c.metadata.confidence = confidence;
213842
+ for (const r of c.relations)
213843
+ if (r.confidence === void 0)
213844
+ r.confidence = 1;
213845
+ return c;
213846
+ }
213874
213847
  function extractNestJS(rootPath) {
213875
213848
  const components = [];
213876
- const files = discoverFiles(rootPath);
213849
+ const files = discoverFiles(rootPath).filter((f) => isInSourceDir(node_path_1.default.relative(rootPath, f))).sort();
213877
213850
  if (files.length === 0)
213878
213851
  return components;
213879
- const allFacts = [];
213852
+ const symbolIndex = /* @__PURE__ */ new Map();
213853
+ const moduleByDir = /* @__PURE__ */ new Map();
213880
213854
  for (const file of files) {
213881
- const relative = node_path_1.default.relative(rootPath, file);
213882
- if (!isInSourceDir(relative))
213883
- continue;
213884
213855
  const facts = parseFile(file, rootPath);
213885
- if (facts)
213886
- allFacts.push(facts);
213856
+ if (!facts)
213857
+ continue;
213858
+ indexFileSymbols(facts, symbolIndex);
213859
+ for (const cls of facts.classes) {
213860
+ if (cls.decorators.includes("Module")) {
213861
+ moduleByDir.set(node_path_1.default.dirname(facts.filePath), cls.name);
213862
+ }
213863
+ }
213887
213864
  }
213888
- const symbolIndex = buildSymbolIndex(allFacts);
213889
- const referenceIndex = buildReferenceIndex(allFacts, symbolIndex);
213890
213865
  const guardNames = /* @__PURE__ */ new Set();
213891
- for (const facts of allFacts) {
213866
+ for (const file of files) {
213867
+ const facts = parseFile(file, rootPath);
213868
+ if (!facts)
213869
+ continue;
213892
213870
  for (const cls of facts.classes) {
213893
213871
  const { decorators, name: className, line, constructorParams, methods, properties, implements: implementsList } = cls;
213894
213872
  if (decorators.includes("Controller")) {
213895
213873
  const controllerName = className || "UnknownController";
213896
- const controllerPath = extractControllerPath(decorators, facts, controllerName);
213897
- const moduleName = findModuleName(facts.filePath, allFacts);
213874
+ const controllerPath = extractControllerPath(cls);
213875
+ const moduleName = moduleByDir.get(node_path_1.default.dirname(facts.filePath)) ?? null;
213898
213876
  for (const method of methods) {
213899
- const endpointDecorators = extractEndpointPath(method.decorators, facts, controllerPath, method.name);
213877
+ const endpointDecorators = extractEndpointPath(method, controllerPath);
213900
213878
  if (endpointDecorators.length === 0)
213901
213879
  continue;
213902
- const methodGuards = extractGuardsFromMethod(facts, method.name);
213880
+ const methodGuards = extractGuardsFromMethod(method);
213903
213881
  methodGuards.forEach((g) => guardNames.add(g));
213904
213882
  const { params, bodyDto, return_type } = extractEndpointParams(method);
213905
213883
  const paramInfos = params.filter((p) => p.source === "param");
@@ -213919,7 +213897,7 @@ var require_nestjs_extractor = __commonJS({
213919
213897
  }
213920
213898
  for (const { method: httpMethod, path: p } of endpointDecorators) {
213921
213899
  const fullPath = p.replace(/\/\//g, "/") || "/";
213922
- components.push({
213900
+ components.push(stamp({
213923
213901
  type: "endpoint",
213924
213902
  name: `${controllerName}.${method.name}`,
213925
213903
  file_path: facts.filePath,
@@ -213935,7 +213913,7 @@ var require_nestjs_extractor = __commonJS({
213935
213913
  return_type: return_type || void 0
213936
213914
  },
213937
213915
  relations: relationGuards
213938
- });
213916
+ }, 1));
213939
213917
  }
213940
213918
  }
213941
213919
  const controllerRelations = [];
@@ -213950,7 +213928,7 @@ var require_nestjs_extractor = __commonJS({
213950
213928
  });
213951
213929
  }
213952
213930
  }
213953
- components.push({
213931
+ components.push(stamp({
213954
213932
  type: "controller",
213955
213933
  name: controllerName,
213956
213934
  file_path: facts.filePath,
@@ -213960,7 +213938,7 @@ var require_nestjs_extractor = __commonJS({
213960
213938
  module_name: moduleName || void 0
213961
213939
  },
213962
213940
  relations: dedupeRelations(controllerRelations)
213963
- });
213941
+ }, 1));
213964
213942
  }
213965
213943
  if (decorators.includes("Injectable")) {
213966
213944
  const serviceRelations = [];
@@ -213975,28 +213953,28 @@ var require_nestjs_extractor = __commonJS({
213975
213953
  });
213976
213954
  }
213977
213955
  }
213978
- components.push({
213956
+ components.push(stamp({
213979
213957
  type: "service",
213980
213958
  name: className || "UnknownService",
213981
213959
  file_path: facts.filePath,
213982
213960
  line_number: line,
213983
213961
  metadata: {},
213984
213962
  relations: dedupeRelations(serviceRelations)
213985
- });
213963
+ }, 1));
213986
213964
  }
213987
213965
  if (decorators.includes("Module")) {
213988
- components.push({
213966
+ components.push(stamp({
213989
213967
  type: "module",
213990
213968
  name: className || "UnknownModule",
213991
213969
  file_path: facts.filePath,
213992
213970
  line_number: line,
213993
213971
  metadata: {},
213994
- relations: extractModuleRelations(facts, cls, symbolIndex)
213995
- });
213972
+ relations: extractModuleRelations(cls, symbolIndex)
213973
+ }, 1));
213996
213974
  }
213997
213975
  if (className.endsWith("Guard") && !decorators.includes("Controller")) {
213998
213976
  guardNames.add(className);
213999
- components.push({
213977
+ components.push(stamp({
214000
213978
  type: "guard",
214001
213979
  name: className,
214002
213980
  file_path: facts.filePath,
@@ -214005,11 +213983,12 @@ var require_nestjs_extractor = __commonJS({
214005
213983
  implements_can_activate: implementsList.includes("CanActivate") ? true : void 0
214006
213984
  },
214007
213985
  relations: []
214008
- });
213986
+ }, 0.7));
214009
213987
  }
214010
213988
  if (isDTOClass(className, decorators, properties)) {
214011
213989
  const dtoFields = extractDTOFields(properties);
214012
- components.push({
213990
+ const dtoConfidence = dtoFields.some((f) => f.validations.length > 0) ? 0.9 : 0.6;
213991
+ components.push(stamp({
214013
213992
  type: "dto",
214014
213993
  name: className,
214015
213994
  file_path: facts.filePath,
@@ -214018,7 +213997,7 @@ var require_nestjs_extractor = __commonJS({
214018
213997
  dto_fields: dtoFields.length > 0 ? dtoFields : void 0
214019
213998
  },
214020
213999
  relations: []
214021
- });
214000
+ }, dtoConfidence));
214022
214001
  }
214023
214002
  }
214024
214003
  }
@@ -214035,94 +214014,28 @@ var require_nestjs_extractor = __commonJS({
214035
214014
  }
214036
214015
  }
214037
214016
  }
214038
- enrichWithReferenceIndex(components, referenceIndex);
214039
214017
  return components;
214040
214018
  }
214041
- function extractModuleRelations(facts, cls, symbolIndex) {
214019
+ function extractModuleRelations(cls, symbolIndex) {
214020
+ const meta = cls.moduleMeta;
214021
+ if (!meta)
214022
+ return [];
214042
214023
  const relations = [];
214043
- const fullPath = node_path_1.default.join(process.cwd(), facts.filePath);
214044
- try {
214045
- const content = node_fs_1.default.readFileSync(fullPath, "utf8");
214046
- const importsMatch = content.match(/imports\s*:\s*\[([\s\S]*?)\]/);
214047
- if (importsMatch) {
214048
- const names = importsMatch[1].match(/[A-Z]\w*/g) || [];
214049
- for (const name of names) {
214050
- const entry = symbolIndex.get(name);
214051
- relations.push({
214052
- type: "module_imports",
214053
- target_name: name,
214054
- target_file: entry?.filePath || ""
214055
- });
214056
- }
214057
- }
214058
- const providersMatch = content.match(/providers\s*:\s*\[([\s\S]*?)\]/);
214059
- if (providersMatch) {
214060
- const names = providersMatch[1].match(/[A-Z]\w*/g) || [];
214061
- for (const name of names) {
214062
- const entry = symbolIndex.get(name);
214063
- relations.push({
214064
- type: "declares_provider",
214065
- target_name: name,
214066
- target_file: entry?.filePath || ""
214067
- });
214068
- }
214069
- }
214070
- const controllersMatch = content.match(/controllers\s*:\s*\[([\s\S]*?)\]/);
214071
- if (controllersMatch) {
214072
- const names = controllersMatch[1].match(/[A-Z]\w*/g) || [];
214073
- for (const name of names) {
214074
- const entry = symbolIndex.get(name);
214075
- relations.push({
214076
- type: "declares_provider",
214077
- target_name: name,
214078
- target_file: entry?.filePath || ""
214079
- });
214080
- }
214081
- }
214082
- } catch {
214083
- }
214084
- return dedupeRelations(relations);
214085
- }
214086
- function findModuleName(filePath, allFacts) {
214087
- const dir = node_path_1.default.dirname(filePath);
214088
- for (const facts of allFacts) {
214089
- const factsDir = node_path_1.default.dirname(facts.filePath);
214090
- if (factsDir === dir) {
214091
- for (const cls of facts.classes) {
214092
- if (cls.decorators.includes("Module")) {
214093
- return cls.name;
214094
- }
214095
- }
214096
- }
214024
+ for (const name of meta.imports) {
214025
+ relations.push({
214026
+ type: "module_imports",
214027
+ target_name: name,
214028
+ target_file: symbolIndex.get(name)?.filePath || ""
214029
+ });
214097
214030
  }
214098
- return null;
214099
- }
214100
- function enrichWithReferenceIndex(components, referenceIndex) {
214101
- for (const comp of components) {
214102
- if (comp.type !== "service" && comp.type !== "controller" && comp.type !== "guard" && comp.type !== "dto")
214103
- continue;
214104
- const refs = referenceIndex.get(comp.name) || [];
214105
- const usedByModules = /* @__PURE__ */ new Set();
214106
- for (const ref of refs) {
214107
- if (!isInSourceDir(ref.filePath))
214108
- continue;
214109
- if (ref.filePath === comp.file_path)
214110
- continue;
214111
- const refModule = extractModuleFromPath(ref.filePath);
214112
- if (refModule && refModule !== "root") {
214113
- usedByModules.add(refModule);
214114
- }
214115
- }
214116
- if (usedByModules.size > 0) {
214117
- for (const mod of usedByModules) {
214118
- comp.relations.push({
214119
- type: "depends_on",
214120
- target_name: mod,
214121
- target_file: `src/${mod}/`
214122
- });
214123
- }
214124
- }
214031
+ for (const name of [...meta.providers, ...meta.controllers]) {
214032
+ relations.push({
214033
+ type: "declares_provider",
214034
+ target_name: name,
214035
+ target_file: symbolIndex.get(name)?.filePath || ""
214036
+ });
214125
214037
  }
214038
+ return dedupeRelations(relations);
214126
214039
  }
214127
214040
  function dedupeRelations(relations) {
214128
214041
  const seen = /* @__PURE__ */ new Set();
@@ -214538,6 +214451,11 @@ var AGENTS_BLOCK = `
214538
214451
  Before touching files:
214539
214452
  driftless context load --files "<files you're about to edit>"
214540
214453
 
214454
+ Understand the code graph before changing it (deterministic, no LLM):
214455
+ driftless graph file <path> # entrypoints, upstream, downstream, contracts
214456
+ driftless graph impact --files "a,b" # blast radius \u2014 what this change can break
214457
+ Add --json for machine-readable output, --depth N (1-6) to widen traversal.
214458
+
214541
214459
  Before pushing:
214542
214460
  driftless scan --diff
214543
214461
 
@@ -214642,7 +214560,7 @@ async function installSkillCommand() {
214642
214560
  // src/commands/init.ts
214643
214561
  function getVersion() {
214644
214562
  try {
214645
- return "0.1.44";
214563
+ return "0.1.46";
214646
214564
  } catch {
214647
214565
  return "0.0.0";
214648
214566
  }
@@ -215062,8 +214980,23 @@ async function initCommand(args) {
215062
214980
  step("uploading baseline", "failed (continuing)");
215063
214981
  }
215064
214982
  if (components.length > 0) {
214983
+ const CHUNK_THRESHOLD = 2e3;
214984
+ const CHUNK_SIZE = 1e3;
215065
214985
  try {
215066
- await api.post(`/workspaces/${workspaceSlug}/repos/${repo.id}/components`, { components });
214986
+ if (components.length <= CHUNK_THRESHOLD) {
214987
+ await api.post(`/workspaces/${workspaceSlug}/repos/${repo.id}/components`, {
214988
+ schema_version: 2,
214989
+ components
214990
+ });
214991
+ } else {
214992
+ for (let i = 0; i < components.length; i += CHUNK_SIZE) {
214993
+ await api.post(`/workspaces/${workspaceSlug}/repos/${repo.id}/components`, {
214994
+ schema_version: 2,
214995
+ replace: i === 0,
214996
+ components: components.slice(i, i + CHUNK_SIZE)
214997
+ });
214998
+ }
214999
+ }
215067
215000
  step("uploading components", `${components.length} components \xB7 ${relationCount} relations \u2713`);
215068
215001
  } catch {
215069
215002
  step("uploading components", "failed (continuing)");
@@ -216634,8 +216567,109 @@ function pad2(s, n) {
216634
216567
  return s + " ".repeat(n - s.length);
216635
216568
  }
216636
216569
 
216570
+ // src/commands/graph.ts
216571
+ init_api_client();
216572
+ function emitJSON4(data) {
216573
+ console.log(JSON.stringify(data, null, 2));
216574
+ }
216575
+ async function graphCommand(args) {
216576
+ if (!isGitRepo()) {
216577
+ console.error("Error: not a git repository.");
216578
+ process.exit(1);
216579
+ }
216580
+ const sub = args[0];
216581
+ const isJSON = args.includes("--json");
216582
+ const depthIdx = args.indexOf("--depth");
216583
+ const depth = depthIdx !== -1 && args[depthIdx + 1] ? `&depth=${encodeURIComponent(args[depthIdx + 1])}` : "";
216584
+ if (sub !== "file" && sub !== "impact") {
216585
+ console.error('Usage:\n driftless graph file <path> [--depth N] [--json]\n driftless graph impact --files "a,b" [--depth N] [--json]');
216586
+ process.exit(1);
216587
+ }
216588
+ const resolution = await resolveRepo();
216589
+ if (!resolution.ok) {
216590
+ if (resolution.reason === "not_linked") {
216591
+ console.error(notLinkedMessage(resolution.remote, resolution.workspaceSlug));
216592
+ } else {
216593
+ console.error("Error: could not resolve workspace. Run `driftless doctor`.");
216594
+ }
216595
+ process.exit(1);
216596
+ }
216597
+ const { workspaceSlug, repoId } = resolution;
216598
+ const base = `/workspaces/${workspaceSlug}/repos/${repoId}/graph`;
216599
+ try {
216600
+ if (sub === "file") {
216601
+ const path = args[1];
216602
+ if (!path || path.startsWith("--")) {
216603
+ console.error("Usage: driftless graph file <path>");
216604
+ process.exit(1);
216605
+ }
216606
+ const g2 = await api.get(`${base}/file?path=${encodeURIComponent(path)}${depth}`);
216607
+ if (isJSON) {
216608
+ emitJSON4(g2);
216609
+ process.exit(0);
216610
+ }
216611
+ if (!g2.found) {
216612
+ console.log(`No scanned components for ${path}. Run \`driftless init\` (or --src for monorepos).`);
216613
+ process.exit(0);
216614
+ }
216615
+ const header = g2.file && g2.file !== g2.query_path ? `\u258C ${path} \u2192 ${g2.file}` : `\u258C ${path}`;
216616
+ console.log(`${header}
216617
+ `);
216618
+ if (g2.entrypoints.length) {
216619
+ console.log("entrypoints (routes that reach this):");
216620
+ for (const e of g2.entrypoints) {
216621
+ const guards = e.guards && e.guards.length ? ` [${e.guards.join(", ")}]` : "";
216622
+ console.log(` ${e.method ?? "?"} ${e.path ?? "?"} \u2192 ${e.handler}${guards}`);
216623
+ }
216624
+ console.log("");
216625
+ }
216626
+ console.log(`upstream (${g2.upstream.length}): ${g2.upstream.slice(0, 12).join(", ")}${g2.upstream.length > 12 ? " \u2026" : ""}`);
216627
+ console.log(`downstream (${g2.downstream.length}): ${g2.downstream.slice(0, 12).join(", ")}${g2.downstream.length > 12 ? " \u2026" : ""}`);
216628
+ if (g2.contracts.length) console.log(`contracts (${g2.contracts.length}): ${g2.contracts.join(", ")}`);
216629
+ process.exit(0);
216630
+ }
216631
+ const filesIdx = args.indexOf("--files");
216632
+ const filesCsv = filesIdx !== -1 ? args[filesIdx + 1] : "";
216633
+ if (!filesCsv) {
216634
+ console.error('Usage: driftless graph impact --files "a,b"');
216635
+ process.exit(1);
216636
+ }
216637
+ const dirIdx = args.indexOf("--direction");
216638
+ const dir = dirIdx !== -1 && args[dirIdx + 1] ? `&direction=${encodeURIComponent(args[dirIdx + 1])}` : "";
216639
+ const g = await api.get(`${base}/impact?files=${encodeURIComponent(filesCsv)}${depth}${dir}`);
216640
+ if (isJSON) {
216641
+ emitJSON4(g);
216642
+ process.exit(0);
216643
+ }
216644
+ if (!g.found) {
216645
+ console.log("No scanned components for those files. Run `driftless init`.");
216646
+ process.exit(0);
216647
+ }
216648
+ const consumers = g.impacted_consumers ?? [];
216649
+ const eps = g.impacted_entrypoints ?? [];
216650
+ console.log(`Blast radius of ${g.files.length} file(s) \u2014 what can BREAK if you change them:
216651
+ `);
216652
+ console.log(` ${consumers.length} consumer component(s) across ${g.impacted_files.length} file(s)`);
216653
+ if (eps.length) {
216654
+ console.log(` ${eps.length} reachable endpoint(s):`);
216655
+ for (const e of eps.slice(0, 15)) console.log(` ${e.method ?? "?"} ${e.path ?? "?"} \u2192 ${e.handler}`);
216656
+ if (eps.length > 15) console.log(` \u2026 and ${eps.length - 15} more`);
216657
+ }
216658
+ console.log("");
216659
+ for (const f of g.impacted_files.slice(0, 25)) console.log(` ${f}`);
216660
+ if (g.impacted_files.length > 25) console.log(` \u2026 and ${g.impacted_files.length - 25} more`);
216661
+ if ((g.depends_on ?? []).length) console.log(`
216662
+ (depends_on: ${g.depends_on.length} downstream \u2014 use --direction both for detail)`);
216663
+ process.exit(0);
216664
+ } catch (e) {
216665
+ console.error(`graph failed: ${formatError(e)}`);
216666
+ console.error("Run `driftless doctor` to diagnose.");
216667
+ process.exit(1);
216668
+ }
216669
+ }
216670
+
216637
216671
  // src/index.ts
216638
- var VERSION = "0.1.44";
216672
+ var VERSION = "0.1.46";
216639
216673
  var HELP_TEXT = `Driftless CLI v${VERSION} \u2014 Living repo context for humans and coding agents
216640
216674
 
216641
216675
  Install: npm install -g @driftless-sh/cli
@@ -216664,6 +216698,7 @@ Commands:
216664
216698
  context Live repo context (topics, search, anchors)
216665
216699
  install-skill Install AGENTS.md into current repo
216666
216700
  doctor Check environment health (auth, API, git, workspace, repo, baseline)
216701
+ graph Deterministic code graph: graph file <path> \xB7 graph impact --files "a,b"
216667
216702
 
216668
216703
  Context subcommands:
216669
216704
  list [flags] List context topics
@@ -216835,6 +216870,22 @@ Exits 1 if any check fails.
216835
216870
 
216836
216871
  Example:
216837
216872
  driftless doctor
216873
+ `,
216874
+ graph: `driftless graph <subcommand>
216875
+
216876
+ Deterministic code graph from the scanned components (no LLM).
216877
+
216878
+ Subcommands:
216879
+ file <path> Entrypoints, upstream, downstream, contracts for a file
216880
+ impact --files "a,b" Blast radius: components/files a change can break
216881
+
216882
+ Flags:
216883
+ --depth N Traversal depth (1-6, default 3)
216884
+ --json Machine-readable output (agents/CI)
216885
+
216886
+ Examples:
216887
+ driftless graph file src/roulette/roulette.service.ts
216888
+ driftless graph impact --files "src/auth/auth.guard.ts" --json
216838
216889
  `
216839
216890
  };
216840
216891
  if (help[cmd]) {
@@ -216896,6 +216947,13 @@ async function main() {
216896
216947
  await doctorCommand(args.slice(1));
216897
216948
  }
216898
216949
  break;
216950
+ case "graph":
216951
+ if (args[1] === "--help") {
216952
+ showCommandHelp("graph");
216953
+ } else {
216954
+ await graphCommand(args.slice(1));
216955
+ }
216956
+ break;
216899
216957
  case "help":
216900
216958
  if (args[1]) {
216901
216959
  showCommandHelp(args[1]);