@neat.is/core 0.6.2-dev.20260723 → 0.6.2

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
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-64JJOXES.js";
4
+ } from "./chunk-MSZ4NALT.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,7 +37,7 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-ZZ3VUCWL.js";
40
+ } from "./chunk-AOWANF2K.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
43
  } from "./chunk-OY7FGJAX.js";
package/dist/neatd.cjs CHANGED
@@ -5631,7 +5631,7 @@ function keywordArrayStrings(argsNode, key) {
5631
5631
  }
5632
5632
  return [];
5633
5633
  }
5634
- function collectApiRouterPrefixes(root) {
5634
+ function collectPythonRouterPrefixes(root) {
5635
5635
  const prefixes = /* @__PURE__ */ new Map();
5636
5636
  walk(root, (node) => {
5637
5637
  if (node.type !== "assignment") return;
@@ -5640,7 +5640,8 @@ function collectApiRouterPrefixes(root) {
5640
5640
  const fn = right.childForFieldName("function");
5641
5641
  if (!fn) return;
5642
5642
  const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
5643
- if (ctor !== "APIRouter") return;
5643
+ const prefixKey = ctor === "APIRouter" ? "prefix" : ctor === "Blueprint" ? "url_prefix" : null;
5644
+ if (!prefixKey) return;
5644
5645
  const left = node.childForFieldName("left");
5645
5646
  if (!left || left.type !== "identifier") return;
5646
5647
  const args = right.childForFieldName("arguments");
@@ -5648,7 +5649,7 @@ function collectApiRouterPrefixes(root) {
5648
5649
  for (let i = 0; i < args.namedChildCount; i++) {
5649
5650
  const arg = args.namedChild(i);
5650
5651
  if (arg?.type !== "keyword_argument") continue;
5651
- if (arg.childForFieldName("name")?.text !== "prefix") continue;
5652
+ if (arg.childForFieldName("name")?.text !== prefixKey) continue;
5652
5653
  const val = arg.childForFieldName("value");
5653
5654
  const p = val ? pyStaticStringText(val) : null;
5654
5655
  if (p !== null) prefixes.set(left.text, p);
@@ -5656,9 +5657,9 @@ function collectApiRouterPrefixes(root) {
5656
5657
  });
5657
5658
  return prefixes;
5658
5659
  }
5659
- function fastapiRoutesFromSource(source, parser) {
5660
+ function pythonRoutesFromSource(source, parser, framework) {
5660
5661
  const tree = parseSource2(parser, source);
5661
- const prefixes = collectApiRouterPrefixes(tree.rootNode);
5662
+ const prefixes = collectPythonRouterPrefixes(tree.rootNode);
5662
5663
  const out = [];
5663
5664
  walk(tree.rootNode, (node) => {
5664
5665
  if (node.type !== "decorator") return;
@@ -5669,7 +5670,9 @@ function fastapiRoutesFromSource(source, parser) {
5669
5670
  const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
5670
5671
  if (!method) return;
5671
5672
  const isVerb = FASTAPI_METHODS.has(method);
5672
- if (!isVerb && method !== "api_route") return;
5673
+ const isFlaskRoute = method === "route";
5674
+ const isApiRoute = method === "api_route";
5675
+ if (!isVerb && !isFlaskRoute && !isApiRoute) return;
5673
5676
  const args = call.childForFieldName("arguments");
5674
5677
  const first = args?.namedChild(0);
5675
5678
  if (!first || first.type !== "string") return;
@@ -5680,17 +5683,41 @@ function fastapiRoutesFromSource(source, parser) {
5680
5683
  const pathTemplate = canonicalizeTemplate(prefix + rawPath);
5681
5684
  const line = node.startPosition.row + 1;
5682
5685
  if (isVerb) {
5683
- out.push({ method: method.toUpperCase(), pathTemplate, line, framework: "fastapi" });
5686
+ out.push({ method: method.toUpperCase(), pathTemplate, line, framework });
5684
5687
  return;
5685
5688
  }
5686
5689
  const methods = keywordArrayStrings(args, "methods");
5687
- const list = methods.length > 0 ? methods : ["ALL"];
5690
+ const list = methods.length > 0 ? methods : isFlaskRoute ? ["GET"] : ["ALL"];
5688
5691
  for (const m of list) {
5692
+ out.push({ method: m === "ALL" ? "ALL" : m.toUpperCase(), pathTemplate, line, framework });
5693
+ }
5694
+ });
5695
+ return out;
5696
+ }
5697
+ function djangoRoutesFromSource(source, parser) {
5698
+ const tree = parseSource2(parser, source);
5699
+ const out = [];
5700
+ walk(tree.rootNode, (node) => {
5701
+ if (node.type !== "assignment") return;
5702
+ if (node.childForFieldName("left")?.text !== "urlpatterns") return;
5703
+ const list = node.childForFieldName("right");
5704
+ if (!list || list.type !== "list") return;
5705
+ for (let i = 0; i < list.namedChildCount; i++) {
5706
+ const el = list.namedChild(i);
5707
+ if (el?.type !== "call") continue;
5708
+ if (el.childForFieldName("function")?.text !== "path") continue;
5709
+ const args = el.childForFieldName("arguments");
5710
+ const first = args?.namedChild(0);
5711
+ if (first?.type !== "string") continue;
5712
+ const raw = pyStaticStringText(first);
5713
+ if (raw === null) continue;
5714
+ const second = args?.namedChild(1);
5715
+ if (second?.type === "call" && second.childForFieldName("function")?.text === "include") continue;
5689
5716
  out.push({
5690
- method: m === "ALL" ? "ALL" : m.toUpperCase(),
5691
- pathTemplate,
5692
- line,
5693
- framework: "fastapi"
5717
+ method: "ALL",
5718
+ pathTemplate: canonicalizeTemplate(raw),
5719
+ line: el.startPosition.row + 1,
5720
+ framework: "django"
5694
5721
  });
5695
5722
  }
5696
5723
  });
@@ -5711,7 +5738,10 @@ async function addRoutes(graph, services) {
5711
5738
  const hasHono = deps["hono"] !== void 0;
5712
5739
  const hasNext = deps["next"] !== void 0;
5713
5740
  const hasFastapi = deps["fastapi"] !== void 0;
5714
- if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi) continue;
5741
+ const hasFlask = deps["flask"] !== void 0;
5742
+ const hasDjango = deps["django"] !== void 0;
5743
+ if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi && !hasFlask && !hasDjango)
5744
+ continue;
5715
5745
  const files = await loadSourceFiles(service.dir);
5716
5746
  for (const file of files) {
5717
5747
  if (isTestPath(file.path)) continue;
@@ -5722,7 +5752,8 @@ async function addRoutes(graph, services) {
5722
5752
  let routes;
5723
5753
  try {
5724
5754
  if (isPy) {
5725
- routes = hasFastapi ? fastapiRoutesFromSource(file.content, pyParser) : [];
5755
+ routes = hasFastapi || hasFlask ? pythonRoutesFromSource(file.content, pyParser, hasFastapi ? "fastapi" : "flask") : [];
5756
+ if (hasDjango) routes = routes.concat(djangoRoutesFromSource(file.content, pyParser));
5726
5757
  } else if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5727
5758
  routes = nextRoutesFromFile(file.content, relFile, jsParser);
5728
5759
  } else if (hasExpress || hasFastify || hasHono) {
@@ -10267,6 +10298,9 @@ async function buildApi(opts) {
10267
10298
  // src/daemon.ts
10268
10299
  init_otel();
10269
10300
 
10301
+ // src/connectors/registry.ts
10302
+ init_cjs_shims();
10303
+
10270
10304
  // src/connectors/index.ts
10271
10305
  init_cjs_shims();
10272
10306
  var import_types36 = require("@neat.is/types");
@@ -10378,6 +10412,7 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
10378
10412
  }
10379
10413
  })();
10380
10414
  };
10415
+ tick();
10381
10416
  const interval = setInterval(tick, intervalMs);
10382
10417
  if (typeof interval.unref === "function") interval.unref();
10383
10418
  return () => {
@@ -10386,9 +10421,6 @@ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options =
10386
10421
  };
10387
10422
  }
10388
10423
 
10389
- // src/connectors/registry.ts
10390
- init_cjs_shims();
10391
-
10392
10424
  // src/connectors/junction.ts
10393
10425
  init_cjs_shims();
10394
10426
  var buckets = /* @__PURE__ */ new Map();
@@ -10996,17 +11028,22 @@ function readRailwayToken(credentials) {
10996
11028
  }
10997
11029
  return token;
10998
11030
  }
10999
- function projectAccessTokenHeader(token) {
11000
- return { "Project-Access-Token": token };
11031
+ var RAILWAY_AUTH_STYLES = ["bearer", "project-access-token"];
11032
+ function railwayAuthHeader(style, token) {
11033
+ return style === "bearer" ? { Authorization: `Bearer ${token}` } : { "Project-Access-Token": token };
11001
11034
  }
11002
- async function railwayGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
11035
+ var resolvedRailwayAuthStyle = /* @__PURE__ */ new Map();
11036
+ function isRailwayNotAuthorized(err) {
11037
+ return err instanceof Error && /not authorized/i.test(err.message);
11038
+ }
11039
+ async function railwayGraphQLOnce(apiUrl, style, token, query, variables, accountKey, fetchImpl) {
11003
11040
  const res = await junctionFetch(
11004
11041
  apiUrl,
11005
11042
  {
11006
11043
  method: "POST",
11007
11044
  headers: {
11008
11045
  "Content-Type": "application/json",
11009
- ...projectAccessTokenHeader(token)
11046
+ ...railwayAuthHeader(style, token)
11010
11047
  },
11011
11048
  body: JSON.stringify({ query, variables })
11012
11049
  },
@@ -11022,6 +11059,26 @@ async function railwayGraphQL(apiUrl, token, query, variables, accountKey, fetch
11022
11059
  if (!body.data) throw new Error("Railway GraphQL response carried no data");
11023
11060
  return body.data;
11024
11061
  }
11062
+ async function railwayGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
11063
+ const known = resolvedRailwayAuthStyle.get(token);
11064
+ const styles = known ? [known] : RAILWAY_AUTH_STYLES;
11065
+ let lastNotAuthorized;
11066
+ for (let i = 0; i < styles.length; i++) {
11067
+ const style = styles[i];
11068
+ try {
11069
+ const data = await railwayGraphQLOnce(apiUrl, style, token, query, variables, accountKey, fetchImpl);
11070
+ resolvedRailwayAuthStyle.set(token, style);
11071
+ return data;
11072
+ } catch (err) {
11073
+ if (isRailwayNotAuthorized(err) && i < styles.length - 1) {
11074
+ lastNotAuthorized = err;
11075
+ continue;
11076
+ }
11077
+ throw err;
11078
+ }
11079
+ }
11080
+ throw lastNotAuthorized ?? new Error("Railway GraphQL: no auth style resolved");
11081
+ }
11025
11082
  var HTTP_LOGS_QUERY = `
11026
11083
  query HttpLogs($deploymentId: String!, $startDate: String, $endDate: String, $limit: Int) {
11027
11084
  httpLogs(deploymentId: $deploymentId, startDate: $startDate, endDate: $endDate, limit: $limit) {
@@ -12128,6 +12185,27 @@ async function loadConnectorRegistrations(input) {
12128
12185
  }
12129
12186
  return registrations;
12130
12187
  }
12188
+ async function startConnectorPolling(input) {
12189
+ const fileConnectors = input.home ? await loadConnectorRegistrations({
12190
+ project: input.project,
12191
+ graph: input.graph,
12192
+ home: input.home,
12193
+ ...input.onSkip ? { onSkip: input.onSkip } : {}
12194
+ }) : [];
12195
+ const all = [...input.extra ?? [], ...fileConnectors];
12196
+ const stopFns = all.map(
12197
+ (registration) => startConnectorPollLoop(
12198
+ registration.connector,
12199
+ { projectDir: input.projectDir, credentials: registration.credentials },
12200
+ input.graph,
12201
+ registration.resolveTarget,
12202
+ { intervalMs: registration.intervalMs, connectorId: registration.id }
12203
+ )
12204
+ );
12205
+ return () => {
12206
+ for (const stop of stopFns) stop();
12207
+ };
12208
+ }
12131
12209
 
12132
12210
  // src/daemon.ts
12133
12211
  init_auth();
@@ -12333,31 +12411,16 @@ async function bootstrapProject(entry2, connectors = [], neatHome4) {
12333
12411
  staleEventsPath: paths.staleEventsPath,
12334
12412
  project: entry2.name
12335
12413
  });
12336
- const fileConnectors = neatHome4 ? await loadConnectorRegistrations({
12414
+ const stopConnectors = await startConnectorPolling({
12337
12415
  project: entry2.name,
12338
12416
  graph,
12339
- home: neatHome4,
12417
+ projectDir: entry2.path,
12418
+ ...neatHome4 ? { home: neatHome4 } : {},
12419
+ extra: connectors,
12340
12420
  onSkip: (skipped, reason) => console.warn(
12341
12421
  `neatd: connector "${skipped.id}" (${skipped.provider}) skipped for project "${entry2.name}" \u2014 ${reason}`
12342
12422
  )
12343
- }) : [];
12344
- const allConnectors = [...connectors, ...fileConnectors];
12345
- const stopFns = allConnectors.map(
12346
- (registration) => startConnectorPollLoop(
12347
- registration.connector,
12348
- { projectDir: entry2.path, credentials: registration.credentials },
12349
- graph,
12350
- registration.resolveTarget,
12351
- // `connectorId` is threaded through so every tick lands in the
12352
- // in-process status tracker the connector-status endpoint reads
12353
- // (ADR-136). Undefined for a programmatic registration, which records
12354
- // nothing.
12355
- { intervalMs: registration.intervalMs, connectorId: registration.id }
12356
- )
12357
- );
12358
- const stopConnectors = () => {
12359
- for (const stop of stopFns) stop();
12360
- };
12423
+ });
12361
12424
  await touchLastSeen(entry2.name).catch(() => {
12362
12425
  });
12363
12426
  return {