@neat.is/core 0.10.4 → 0.10.5-dev.20261001

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/cli.cjs CHANGED
@@ -3536,6 +3536,10 @@ function languageForPath(relPath) {
3536
3536
  return void 0;
3537
3537
  }
3538
3538
  }
3539
+ var currentSource;
3540
+ function setExtractionSource(source) {
3541
+ currentSource = source;
3542
+ }
3539
3543
  function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3540
3544
  let nodesAdded = 0;
3541
3545
  let edgesAdded = 0;
@@ -3548,10 +3552,16 @@ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3548
3552
  service: serviceName,
3549
3553
  path: relPath,
3550
3554
  ...language ? { language } : {},
3555
+ ...currentSource ? { source: currentSource } : {},
3551
3556
  discoveredVia: "static"
3552
3557
  };
3553
3558
  graph.addNode(fileNodeId, node);
3554
3559
  nodesAdded++;
3560
+ } else if (currentSource) {
3561
+ const existing = graph.getNodeAttributes(fileNodeId);
3562
+ if (existing.source !== currentSource) {
3563
+ graph.replaceNodeAttributes(fileNodeId, { ...existing, source: currentSource });
3564
+ }
3555
3565
  }
3556
3566
  const containsId = (0, import_types5.extractedEdgeId)(serviceNodeId, fileNodeId, import_types5.EdgeType.CONTAINS);
3557
3567
  if (!graph.hasEdge(containsId)) {
@@ -16612,10 +16622,12 @@ init_cjs_shims();
16612
16622
  var import_node_fs34 = require("fs");
16613
16623
  var import_node_path69 = __toESM(require("path"), 1);
16614
16624
  var import_types59 = require("@neat.is/types");
16615
- function dropOrphanedFileNodes(graph) {
16625
+ function dropOrphanedFileNodes(graph, source) {
16616
16626
  const orphans = [];
16617
16627
  graph.forEachNode((id, attrs) => {
16618
- if (attrs.type !== import_types59.NodeType.FileNode) return;
16628
+ const node = attrs;
16629
+ if (node.type !== import_types59.NodeType.FileNode) return;
16630
+ if (source && node.source !== source) return;
16619
16631
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
16620
16632
  orphans.push(id);
16621
16633
  }
@@ -16636,12 +16648,23 @@ function retireEdgesByFile(graph, file) {
16636
16648
  dropOrphanedFileNodes(graph);
16637
16649
  return toDrop.length;
16638
16650
  }
16639
- function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
16651
+ function outOfScope(graph, edge, source) {
16652
+ if (!source) return false;
16653
+ for (const endpoint2 of [edge.source, edge.target]) {
16654
+ if (!graph.hasNode(endpoint2)) continue;
16655
+ const node = graph.getNodeAttributes(endpoint2);
16656
+ if (node.type !== import_types59.NodeType.FileNode) continue;
16657
+ return node.source !== source;
16658
+ }
16659
+ return false;
16660
+ }
16661
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = [], source) {
16640
16662
  const toDrop = [];
16641
16663
  const bases = [scanPath, ...serviceDirs];
16642
16664
  graph.forEachEdge((id, attrs) => {
16643
16665
  const edge = attrs;
16644
16666
  if (edge.provenance !== import_types59.Provenance.EXTRACTED) return;
16667
+ if (outOfScope(graph, edge, source)) return;
16645
16668
  const evidenceFile = edge.evidence?.file;
16646
16669
  if (!evidenceFile) return;
16647
16670
  if (import_node_path69.default.isAbsolute(evidenceFile)) {
@@ -16652,7 +16675,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
16652
16675
  if (!found) toDrop.push(id);
16653
16676
  });
16654
16677
  for (const id of toDrop) graph.dropEdge(id);
16655
- dropOrphanedFileNodes(graph);
16678
+ dropOrphanedFileNodes(graph, source);
16656
16679
  return toDrop.length;
16657
16680
  }
16658
16681
 
@@ -16661,6 +16684,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16661
16684
  await ensureCompatLoaded();
16662
16685
  drainExtractionErrors();
16663
16686
  drainSkippedFiles();
16687
+ setExtractionSource(opts.source);
16664
16688
  const services = await discoverServices(scanPath);
16665
16689
  const phase1Nodes = addServiceNodes(graph, services);
16666
16690
  await addServiceAliases(graph, scanPath, services);
@@ -16681,7 +16705,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16681
16705
  const ghostsRetired = retireExtractedEdgesByMissingFile(
16682
16706
  graph,
16683
16707
  scanPath,
16684
- services.map((s) => s.dir)
16708
+ services.map((s) => s.dir),
16709
+ opts.source
16685
16710
  );
16686
16711
  const frontiersPromoted = promoteFrontierNodes(graph);
16687
16712
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
@@ -16716,6 +16741,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16716
16741
  );
16717
16742
  }
16718
16743
  }
16744
+ setExtractionSource(void 0);
16719
16745
  const result = {
16720
16746
  nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + actionPhase.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + tableEdges.nodesAdded + phase5.nodesAdded + zodPhase.nodesAdded + firestoreRulesPhase.nodesAdded,
16721
16747
  edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + actionPhase.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + tableEdges.edgesAdded + phase5.edgesAdded + zodPhase.edgesAdded + firestoreRulesPhase.edgesAdded,
@@ -30658,6 +30684,52 @@ async function waitForFirstProject(fetchImpl, cpUrl, accessToken, deps) {
30658
30684
  if (deps.signal?.aborted) return { kind: "aborted" };
30659
30685
  }
30660
30686
  }
30687
+ function projectNameForRepo(repo) {
30688
+ const slug = repo.name.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
30689
+ return slug || "repo";
30690
+ }
30691
+ async function detailOf(res) {
30692
+ const body = await res.text().catch(() => "");
30693
+ try {
30694
+ const parsed = JSON.parse(body);
30695
+ if (parsed.error) return parsed.error;
30696
+ } catch {
30697
+ }
30698
+ return body.trim() || `HTTP ${res.status}`;
30699
+ }
30700
+ async function createProjectForRepo(fetchImpl, cpUrl, accessToken, name) {
30701
+ const headers = { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" };
30702
+ let project;
30703
+ try {
30704
+ const res = await fetchImpl(`${cpUrl}/me/projects`, {
30705
+ method: "POST",
30706
+ headers,
30707
+ body: JSON.stringify({ name }),
30708
+ signal: AbortSignal.timeout(CP_TIMEOUT_MS)
30709
+ });
30710
+ if (!res.ok) return { ok: false, reason: "not-created", detail: await detailOf(res) };
30711
+ project = await res.json();
30712
+ } catch (err) {
30713
+ return { ok: false, reason: "not-created", detail: err instanceof Error ? err.message : String(err) };
30714
+ }
30715
+ try {
30716
+ const res = await fetchImpl(`${cpUrl}/me/projects/${encodeURIComponent(project.id)}/provision`, {
30717
+ method: "POST",
30718
+ headers,
30719
+ signal: AbortSignal.timeout(CP_TIMEOUT_MS)
30720
+ });
30721
+ if (res.status === 402) return { ok: false, reason: "needs-plan", project, detail: await detailOf(res) };
30722
+ if (!res.ok) return { ok: false, reason: "not-provisioned", project, detail: await detailOf(res) };
30723
+ } catch (err) {
30724
+ return {
30725
+ ok: false,
30726
+ reason: "not-provisioned",
30727
+ project,
30728
+ detail: err instanceof Error ? err.message : String(err)
30729
+ };
30730
+ }
30731
+ return { ok: true, project };
30732
+ }
30661
30733
  function bindUrl(webUrl, projectId) {
30662
30734
  return `${webUrl}/config/repos?project=${encodeURIComponent(projectId)}`;
30663
30735
  }
@@ -30683,7 +30755,8 @@ async function runPostLoginConnect(input) {
30683
30755
  const repo = await detect6(cwd);
30684
30756
  out("");
30685
30757
  const cmd = commandPrefix();
30686
- const syncLine = `${cmd} sync --to ${input.project.name}`;
30758
+ const pushCmd = input.endpoint ? `${cmd} sync --to ${input.endpoint}${input.pushToken ? ` --token ${input.pushToken}` : ""}` : null;
30759
+ const pushInto = (label) => pushCmd ? [` ${pushCmd}`] : [` Its daemon URL and token are on ${label} in the console: ${input.webUrl}`];
30687
30760
  if (!repo) {
30688
30761
  out("This directory has no GitHub remote, so it can't be bound to the project.");
30689
30762
  const hasGraph = await (d.hasLocalGraph ?? hasLocalSnapshot)(cwd);
@@ -30693,16 +30766,17 @@ async function runPostLoginConnect(input) {
30693
30766
  out("");
30694
30767
  if (built) {
30695
30768
  out(`Now push it to ${input.project.name}:`);
30696
- out(` ${syncLine}`);
30769
+ for (const line of pushInto(input.project.name)) out(line);
30697
30770
  } else {
30698
30771
  out(`When you're ready, build it and push it to ${input.project.name}:`);
30699
30772
  out(` ${cmd}`);
30700
- out(` ${syncLine}`);
30773
+ for (const line of pushInto(input.project.name)) out(line);
30701
30774
  }
30702
30775
  out(` Manage projects and repos: ${input.webUrl}`);
30703
30776
  return;
30704
30777
  }
30705
- out(` Push its graph instead: ${syncLine}`);
30778
+ out(` Push its graph to ${input.project.name}:`);
30779
+ for (const line of pushInto(input.project.name)) out(line);
30706
30780
  out(` Manage projects and repos: ${input.webUrl}`);
30707
30781
  return;
30708
30782
  }
@@ -30715,39 +30789,87 @@ async function runPostLoginConnect(input) {
30715
30789
  out(` See the graph: ${input.webUrl}`);
30716
30790
  return;
30717
30791
  }
30718
- out(`${slug} isn't part of ${input.project.name} yet \u2014 logging in connected your account, not this repo.`);
30792
+ out(`${slug} isn't in ${input.project.name} \u2014 logging in connected your account, not this repo.`);
30719
30793
  out("");
30720
- const installed = input.me?.github?.installed;
30721
- const bindLine = installed === false ? "1) Install the GitHub App and bind this repo \u2014 hosted NEAT keeps it in sync" : installed === true ? "1) Bind this repo \u2014 hosted NEAT clones and extracts it, and keeps it in sync" : "1) Bind this repo through the console \u2014 hosted NEAT keeps it in sync";
30722
- out(bindLine);
30723
- out(`2) Push the graph on this machine now \u2014 a snapshot, not a subscription`);
30794
+ const suggested = projectNameForRepo(repo);
30795
+ out("Where should this repo go?");
30796
+ out("");
30797
+ out(`1) A project of its own, called ${suggested}`);
30798
+ out(`2) ${input.project.name} \u2014 the project this login connected to`);
30799
+ for (const line of sharedProjectCaveat(bound)) out(` ${line}`);
30724
30800
  out("3) Neither, for now");
30801
+ out("");
30725
30802
  const answer = readLine ? (await readLine("Choose 1, 2 or 3 (Enter skips): "))?.trim() : void 0;
30726
30803
  if (answer === "1") {
30727
- const url = bindUrl(input.webUrl, input.project.id);
30728
- const opened = (d.openBrowser ?? (() => false))(url);
30729
30804
  out("");
30730
- out(opened ? `Opened ${url}` : `Open this to bind the repo:
30731
- ${url}`);
30805
+ const outcome = await createProjectForRepo(fetchImpl, input.cpUrl, input.accessToken, suggested);
30806
+ reportCreate(outcome, input.webUrl, cmd, input.project.name, out);
30732
30807
  return;
30733
30808
  }
30734
30809
  if (answer === "2") {
30735
- const hasGraph = await (d.hasLocalGraph ?? hasLocalSnapshot)(cwd);
30736
30810
  out("");
30737
- if (!hasGraph) {
30738
- out("There is no local graph to push yet. Build one, then push it:");
30739
- out(` ${cmd}`);
30740
- out(` ${syncLine}`);
30741
- return;
30811
+ out(`Two ways into ${input.project.name} \u2014 either is fine, and you can do both:`);
30812
+ out("");
30813
+ const installed = input.me?.github?.installed;
30814
+ out(
30815
+ installed === false ? " Bind it (install the GitHub App first) \u2014 hosted NEAT keeps it in sync:" : installed === true ? " Bind it \u2014 hosted NEAT clones and extracts it, and keeps it in sync:" : " Bind it through the console \u2014 hosted NEAT keeps it in sync:"
30816
+ );
30817
+ out(` ${bindUrl(input.webUrl, input.project.id)}`);
30818
+ out("");
30819
+ const hasGraph = await (d.hasLocalGraph ?? hasLocalSnapshot)(cwd);
30820
+ if (hasGraph) {
30821
+ out(" Or push the graph on this machine now \u2014 a snapshot, not a subscription:");
30822
+ for (const line of pushInto(input.project.name)) out(` ${line}`);
30823
+ } else {
30824
+ out(" Or build a graph here and push it \u2014 a snapshot, not a subscription:");
30825
+ out(` ${cmd}`);
30826
+ for (const line of pushInto(input.project.name)) out(` ${line}`);
30742
30827
  }
30743
- out("Push the local graph with:");
30744
- out(` ${syncLine}`);
30745
30828
  return;
30746
30829
  }
30747
30830
  out("");
30748
30831
  out("No problem \u2014 when you want this repo in the graph:");
30749
- out(` Bind it: ${bindUrl(input.webUrl, input.project.id)}`);
30750
- out(` Or push it: ${syncLine}`);
30832
+ out(` Its own project: ${input.webUrl}`);
30833
+ out(` Or into ${input.project.name}: ${bindUrl(input.webUrl, input.project.id)}`);
30834
+ }
30835
+ function sharedProjectCaveat(bound) {
30836
+ const hazard = "Two repos in one project can currently retire each other's files (#1294),";
30837
+ if (!bound) return [hazard, "so a graph there may end up short of code until that is fixed."];
30838
+ if (bound.length === 0) return ["Nothing else is bound there yet.", `Adding a second repo later would hit #1294 \u2014 ${hazard.toLowerCase()}`];
30839
+ const names = bound.map((r) => `${r.owner}/${r.name}`).join(", ");
30840
+ return [
30841
+ `${bound.length} already there: ${names}.`,
30842
+ hazard,
30843
+ "so both graphs may end up short of code until that is fixed."
30844
+ ];
30845
+ }
30846
+ function reportCreate(outcome, webUrl, cmd, connectedTo, out) {
30847
+ if (outcome.ok) {
30848
+ out(`Created ${outcome.project.name} and started its daemon.`);
30849
+ out("");
30850
+ out(`This CLI is still pointed at ${connectedTo}. Switch it to the new project with:`);
30851
+ out(` ${cmd} login --project ${outcome.project.name}`);
30852
+ out("");
30853
+ out("Then bind this repo to it:");
30854
+ out(` ${bindUrl(webUrl, outcome.project.id)}`);
30855
+ return;
30856
+ }
30857
+ if (outcome.reason === "needs-plan") {
30858
+ out(`Created ${outcome.project.name}, but starting a daemon for it needs a plan.`);
30859
+ out(` Choose one: ${webUrl}/checkout`);
30860
+ out("");
30861
+ out("The project is made and keeps its name, so there's nothing to redo here afterwards.");
30862
+ return;
30863
+ }
30864
+ if (outcome.reason === "not-provisioned") {
30865
+ out(`Created ${outcome.project.name}, but its daemon didn't start: ${outcome.detail}`);
30866
+ out("");
30867
+ out("The project exists \u2014 start it from the console rather than making a second one:");
30868
+ out(` ${webUrl}`);
30869
+ return;
30870
+ }
30871
+ out(`Couldn't create the project: ${outcome.detail}`);
30872
+ out(`Nothing was made, so this is safe to retry, or do it in the console: ${webUrl}`);
30751
30873
  }
30752
30874
 
30753
30875
  // src/login-sso.ts
@@ -30984,6 +31106,10 @@ async function runSsoLogin(opts, deps) {
30984
31106
  webUrl: opts.webUrl,
30985
31107
  accessToken,
30986
31108
  project: { id: project.id, name: project.name },
31109
+ // What a push actually needs: `sync --to` takes the daemon's URL, and its
31110
+ // token comes from the flag rather than the profile (#1302).
31111
+ endpoint: cred.endpoint,
31112
+ pushToken: cred.authToken,
30987
31113
  me,
30988
31114
  deps: {
30989
31115
  fetchImpl,