nooa-cli 0.0.7__py3-none-any.whl
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.
- nooa_cli/AGENTS.md +162 -0
- nooa_cli/__init__.py +70 -0
- nooa_cli/__main__.py +8 -0
- nooa_cli/_common.py +45 -0
- nooa_cli/commands/__init__.py +106 -0
- nooa_cli/commands/_otlp_helpers.py +122 -0
- nooa_cli/commands/_template.py +89 -0
- nooa_cli/commands/config.py +356 -0
- nooa_cli/commands/delete_traces.py +63 -0
- nooa_cli/commands/eval.py +68 -0
- nooa_cli/commands/import_harbor.py +512 -0
- nooa_cli/commands/import_traces.py +189 -0
- nooa_cli/commands/start_dev.py +145 -0
- nooa_cli/commands/traces.py +316 -0
- nooa_cli/completion.py +253 -0
- nooa_cli/tools/__init__.py +0 -0
- nooa_cli/tools/_tree_sitter_backend.py +351 -0
- nooa_cli/tools/pyp/__init__.py +71 -0
- nooa_cli/tools/pyp/errors.py +92 -0
- nooa_cli/tools/pyp/sources.py +458 -0
- nooa_cli/tools/pyp/stream.py +527 -0
- nooa_cli/tools/pyp/tests/__init__.py +0 -0
- nooa_cli/tools/pyp/tests/test_pyp.py +394 -0
- nooa_cli/tools/repo_tools.py +902 -0
- nooa_cli-0.0.7.dist-info/METADATA +52 -0
- nooa_cli-0.0.7.dist-info/RECORD +28 -0
- nooa_cli-0.0.7.dist-info/WHEEL +4 -0
- nooa_cli-0.0.7.dist-info/entry_points.txt +5 -0
nooa_cli/completion.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""nooa completion — Generate shell completion scripts.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
nooa completion bash # Print bash completion script
|
|
7
|
+
nooa completion zsh # Print zsh completion script
|
|
8
|
+
nooa completion fish # Print fish completion script
|
|
9
|
+
nooa completion install # Auto-install for current shell
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import click
|
|
16
|
+
|
|
17
|
+
# Completion script templates — generated by Click 8.x, embedded to avoid
|
|
18
|
+
# subprocess overhead and capture issues.
|
|
19
|
+
_BASH_SCRIPT = r"""
|
|
20
|
+
%(complete_func)s() {
|
|
21
|
+
local IFS=$'\n'
|
|
22
|
+
COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \
|
|
23
|
+
COMP_CWORD=$COMP_CWORD \
|
|
24
|
+
%(complete_var)s=bash_complete $1 ) )
|
|
25
|
+
return 0
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
%(complete_func)ssetup() {
|
|
29
|
+
complete -o default -F %(complete_func)s %(prog_name)s
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
%(complete_func)ssetup;
|
|
33
|
+
""".strip()
|
|
34
|
+
|
|
35
|
+
_ZSH_SCRIPT = r"""
|
|
36
|
+
#compdef %(prog_name)s
|
|
37
|
+
|
|
38
|
+
%(complete_func)s() {
|
|
39
|
+
local -a completions
|
|
40
|
+
local -a completions_with_descriptions
|
|
41
|
+
local -a response
|
|
42
|
+
(( ! $+commands[%(prog_name)s] )) && return 1
|
|
43
|
+
|
|
44
|
+
response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) %(complete_var)s=zsh_complete %(prog_name)s)}")
|
|
45
|
+
|
|
46
|
+
for type key descr in ${response}; do
|
|
47
|
+
if [[ "$type" == "plain" ]]; then
|
|
48
|
+
if [[ "$descr" == "_" ]]; then
|
|
49
|
+
completions+=("$key")
|
|
50
|
+
else
|
|
51
|
+
completions_with_descriptions+=("$key":"$descr")
|
|
52
|
+
fi
|
|
53
|
+
elif [[ "$type" == "dir" ]]; then
|
|
54
|
+
_path_files -/
|
|
55
|
+
elif [[ "$type" == "file" ]]; then
|
|
56
|
+
_path_files -f
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
if [ -n "$completions_with_descriptions" ]; then
|
|
61
|
+
_describe -V unsorted completions_with_descriptions -U
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
if [ -n "$completions" ]; then
|
|
65
|
+
compadd -U -V unsorted -a completions
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if [[ $zsh_eval_context[-1] == loadautofun ]]; then
|
|
70
|
+
# autoload from fpath
|
|
71
|
+
%(complete_func)s "$@"
|
|
72
|
+
else
|
|
73
|
+
compdef %(complete_func)s %(prog_name)s
|
|
74
|
+
fi
|
|
75
|
+
""".strip()
|
|
76
|
+
|
|
77
|
+
_FISH_SCRIPT = r"""
|
|
78
|
+
complete -c %(prog_name)s -f -a "(env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) COMP_CWORD=(commandline -t) %(prog_name)s)"
|
|
79
|
+
""".strip()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _render_script(template: str) -> str:
|
|
83
|
+
"""Render a completion script template with nooa-specific values."""
|
|
84
|
+
return template % {
|
|
85
|
+
"complete_func": "_nooa_completion",
|
|
86
|
+
"complete_var": "_NOOA_COMPLETE",
|
|
87
|
+
"prog_name": "nooa",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@click.group()
|
|
92
|
+
def completion():
|
|
93
|
+
"""Generate shell completion scripts for nooa.
|
|
94
|
+
|
|
95
|
+
\b
|
|
96
|
+
Quick setup (add to your shell profile):
|
|
97
|
+
|
|
98
|
+
# bash (~/.bashrc)
|
|
99
|
+
eval "$(_NOOA_COMPLETE=bash_source nooa)"
|
|
100
|
+
|
|
101
|
+
# zsh (~/.zshrc)
|
|
102
|
+
eval "$(_NOOA_COMPLETE=zsh_source nooa)"
|
|
103
|
+
|
|
104
|
+
# fish (~/.config/fish/config.fish)
|
|
105
|
+
_NOOA_COMPLETE=fish_source nooa | source
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@completion.command()
|
|
110
|
+
def bash():
|
|
111
|
+
"""Print bash completion script.
|
|
112
|
+
|
|
113
|
+
\b
|
|
114
|
+
Usage:
|
|
115
|
+
eval "$(nooa completion bash)"
|
|
116
|
+
# Or append to profile:
|
|
117
|
+
nooa completion bash >> ~/.bashrc
|
|
118
|
+
"""
|
|
119
|
+
click.echo(_render_script(_BASH_SCRIPT))
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@completion.command()
|
|
123
|
+
def zsh():
|
|
124
|
+
"""Print zsh completion script.
|
|
125
|
+
|
|
126
|
+
\b
|
|
127
|
+
Usage:
|
|
128
|
+
eval "$(nooa completion zsh)"
|
|
129
|
+
# Or append to profile:
|
|
130
|
+
nooa completion zsh >> ~/.zshrc
|
|
131
|
+
"""
|
|
132
|
+
click.echo(_render_script(_ZSH_SCRIPT))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@completion.command()
|
|
136
|
+
def fish():
|
|
137
|
+
"""Print fish completion script.
|
|
138
|
+
|
|
139
|
+
\b
|
|
140
|
+
Usage:
|
|
141
|
+
nooa completion fish > ~/.config/fish/completions/nooa.fish
|
|
142
|
+
"""
|
|
143
|
+
click.echo(_render_script(_FISH_SCRIPT))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@completion.command()
|
|
147
|
+
def install():
|
|
148
|
+
"""Auto-detect shell and install completions.
|
|
149
|
+
|
|
150
|
+
Detects your current shell and appends the completion script
|
|
151
|
+
to the appropriate profile file.
|
|
152
|
+
"""
|
|
153
|
+
shell = _detect_shell()
|
|
154
|
+
if not shell:
|
|
155
|
+
click.secho("Could not detect your shell. Use one of:", fg="red")
|
|
156
|
+
click.echo(" nooa completion bash")
|
|
157
|
+
click.echo(" nooa completion zsh")
|
|
158
|
+
click.echo(" nooa completion fish")
|
|
159
|
+
raise SystemExit(1)
|
|
160
|
+
|
|
161
|
+
click.echo(f"Detected shell: {shell}")
|
|
162
|
+
|
|
163
|
+
if shell == "bash":
|
|
164
|
+
profile = Path.home() / ".bashrc"
|
|
165
|
+
line = 'eval "$(_NOOA_COMPLETE=bash_source nooa)"'
|
|
166
|
+
elif shell == "zsh":
|
|
167
|
+
profile = Path.home() / ".zshrc"
|
|
168
|
+
line = 'eval "$(_NOOA_COMPLETE=zsh_source nooa)"'
|
|
169
|
+
elif shell == "fish":
|
|
170
|
+
profile = Path.home() / ".config" / "fish" / "completions" / "nooa.fish"
|
|
171
|
+
line = None # Fish uses a completions file with the script directly
|
|
172
|
+
else:
|
|
173
|
+
click.secho(f"Unsupported shell: {shell}", fg="red")
|
|
174
|
+
raise SystemExit(1)
|
|
175
|
+
|
|
176
|
+
# Remove stale pre-rename fish completions file if present.
|
|
177
|
+
if shell == "fish":
|
|
178
|
+
old_fish = Path.home() / ".config" / "fish" / "completions" / "nemo.fish"
|
|
179
|
+
if old_fish.exists():
|
|
180
|
+
old_fish.unlink()
|
|
181
|
+
click.secho(f"Removed stale pre-rename completions: {old_fish}", fg="yellow")
|
|
182
|
+
|
|
183
|
+
# Check if already installed; drop stale pre-rename lines. Old installs
|
|
184
|
+
# wrote `_NEMO_COMPLETE=... nooa` — now that the `nooa` binary
|
|
185
|
+
# exists, click ignores the wrong variable and the eval'd help text
|
|
186
|
+
# errors on every shell startup.
|
|
187
|
+
if profile.exists():
|
|
188
|
+
existing = _read_preserving_newlines(profile)
|
|
189
|
+
line_ending = _detect_line_ending(existing)
|
|
190
|
+
lines = existing.splitlines()
|
|
191
|
+
stale_indices = {
|
|
192
|
+
i for i, ln in enumerate(lines) if "_NEMO_COMPLETE" in ln and "_NOOA_COMPLETE" not in ln
|
|
193
|
+
}
|
|
194
|
+
# Also mark preceding comment lines that belong to the same block
|
|
195
|
+
# (e.g. "# nemo shell completions" immediately above a stale eval).
|
|
196
|
+
for i in sorted(stale_indices):
|
|
197
|
+
prev = i - 1
|
|
198
|
+
while prev >= 0 and prev not in stale_indices:
|
|
199
|
+
stripped = lines[prev].strip()
|
|
200
|
+
if stripped.startswith("#") and "nemo" in stripped.lower():
|
|
201
|
+
stale_indices.add(prev)
|
|
202
|
+
prev -= 1
|
|
203
|
+
else:
|
|
204
|
+
break
|
|
205
|
+
if stale_indices:
|
|
206
|
+
cleaned = line_ending.join(ln for i, ln in enumerate(lines) if i not in stale_indices)
|
|
207
|
+
profile.write_text(cleaned + (line_ending if cleaned else ""))
|
|
208
|
+
existing = cleaned
|
|
209
|
+
click.secho(
|
|
210
|
+
f"Removed stale _NEMO_COMPLETE completion line(s) from {profile}",
|
|
211
|
+
fg="yellow",
|
|
212
|
+
)
|
|
213
|
+
if "_NOOA_COMPLETE" in existing:
|
|
214
|
+
click.secho(f"Completions already installed in {profile}", fg="green")
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
if shell == "fish":
|
|
218
|
+
# Fish: write completions file
|
|
219
|
+
profile.parent.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
script = _render_script(_FISH_SCRIPT)
|
|
221
|
+
profile.write_text(script + "\n")
|
|
222
|
+
click.secho(f"Completions written to {profile}", fg="green")
|
|
223
|
+
else:
|
|
224
|
+
# Bash/zsh: append eval one-liner to profile
|
|
225
|
+
with open(profile, "a") as f:
|
|
226
|
+
f.write(f"\n# nooa shell completions\n{line}\n")
|
|
227
|
+
click.secho(f"Completions added to {profile}", fg="green")
|
|
228
|
+
click.echo(f"Run: source {profile}")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _detect_line_ending(text: str) -> str:
|
|
232
|
+
"""Return the dominant line ending in *text*."""
|
|
233
|
+
if "\r\n" in text:
|
|
234
|
+
return "\r\n"
|
|
235
|
+
return "\n"
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _read_preserving_newlines(path: Path) -> str:
|
|
239
|
+
"""Read file without Python's universal newline translation."""
|
|
240
|
+
with open(path, newline="") as f:
|
|
241
|
+
return f.read()
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _detect_shell() -> str | None:
|
|
245
|
+
"""Detect the current shell."""
|
|
246
|
+
shell = os.environ.get("SHELL", "")
|
|
247
|
+
if "zsh" in shell:
|
|
248
|
+
return "zsh"
|
|
249
|
+
elif "bash" in shell:
|
|
250
|
+
return "bash"
|
|
251
|
+
elif "fish" in shell:
|
|
252
|
+
return "fish"
|
|
253
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Tree-sitter backend for RepoTools.
|
|
4
|
+
|
|
5
|
+
Provides AST-aware code analysis: symbol extraction and reference finding
|
|
6
|
+
using tree-sitter grammars. Falls back gracefully if grammars are unavailable.
|
|
7
|
+
|
|
8
|
+
Usage (internal — called by repo_tools.py)::
|
|
9
|
+
|
|
10
|
+
from nooa_cli.tools._tree_sitter_backend import (
|
|
11
|
+
ts_extract_symbols,
|
|
12
|
+
ts_find_references,
|
|
13
|
+
TREE_SITTER_AVAILABLE,
|
|
14
|
+
)
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
import tree_sitter as ts
|
|
26
|
+
|
|
27
|
+
TREE_SITTER_AVAILABLE = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
TREE_SITTER_AVAILABLE = False
|
|
30
|
+
|
|
31
|
+
# Language grammar loaders (lazy-loaded)
|
|
32
|
+
_PARSERS: dict[str, ts.Parser] = {}
|
|
33
|
+
|
|
34
|
+
# Map language name → grammar module and language loader function.
|
|
35
|
+
_GRAMMAR_MODULES = {
|
|
36
|
+
"python": ("tree_sitter_python", "language"),
|
|
37
|
+
"javascript": ("tree_sitter_javascript", "language"),
|
|
38
|
+
"typescript": ("tree_sitter_typescript", "language_typescript"),
|
|
39
|
+
"tsx": ("tree_sitter_typescript", "language_tsx"),
|
|
40
|
+
"go": ("tree_sitter_go", "language"),
|
|
41
|
+
"rust": ("tree_sitter_rust", "language"),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Tree-sitter queries for extracting definitions per language
|
|
45
|
+
_DEFINITION_QUERIES = {
|
|
46
|
+
"python": """
|
|
47
|
+
(function_definition name: (identifier) @name) @def
|
|
48
|
+
(class_definition name: (identifier) @name) @def
|
|
49
|
+
""",
|
|
50
|
+
"javascript": """
|
|
51
|
+
(function_declaration name: (identifier) @name) @def
|
|
52
|
+
(class_declaration name: (identifier) @name) @def
|
|
53
|
+
(method_definition name: (property_identifier) @name) @def
|
|
54
|
+
(lexical_declaration
|
|
55
|
+
(variable_declarator
|
|
56
|
+
name: (identifier) @name
|
|
57
|
+
value: (arrow_function))) @def
|
|
58
|
+
""",
|
|
59
|
+
"typescript": """
|
|
60
|
+
(function_declaration name: (identifier) @name) @def
|
|
61
|
+
(class_declaration name: (identifier) @name) @def
|
|
62
|
+
(interface_declaration name: (type_identifier) @name) @def
|
|
63
|
+
(type_alias_declaration name: (type_identifier) @name) @def
|
|
64
|
+
(method_definition name: (property_identifier) @name) @def
|
|
65
|
+
""",
|
|
66
|
+
"go": """
|
|
67
|
+
(function_declaration name: (identifier) @name) @def
|
|
68
|
+
(method_declaration name: (field_identifier) @name) @def
|
|
69
|
+
(type_declaration (type_spec name: (type_identifier) @name)) @def
|
|
70
|
+
""",
|
|
71
|
+
"rust": """
|
|
72
|
+
(function_item name: (identifier) @name) @def
|
|
73
|
+
(struct_item name: (type_identifier) @name) @def
|
|
74
|
+
(enum_item name: (type_identifier) @name) @def
|
|
75
|
+
(trait_item name: (type_identifier) @name) @def
|
|
76
|
+
(impl_item type: (type_identifier) @name) @def
|
|
77
|
+
""",
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Tree-sitter queries for finding references (call sites, imports, usages)
|
|
81
|
+
_DEFINITION_QUERIES["tsx"] = _DEFINITION_QUERIES["typescript"]
|
|
82
|
+
|
|
83
|
+
_REFERENCE_QUERIES = {
|
|
84
|
+
"python": """
|
|
85
|
+
(call function: (identifier) @ref)
|
|
86
|
+
(call function: (attribute attribute: (identifier) @ref))
|
|
87
|
+
(import_from_statement name: (dotted_name (identifier) @ref))
|
|
88
|
+
(identifier) @ref
|
|
89
|
+
""",
|
|
90
|
+
"javascript": """
|
|
91
|
+
(call_expression function: (identifier) @ref)
|
|
92
|
+
(call_expression function: (member_expression property: (property_identifier) @ref))
|
|
93
|
+
(identifier) @ref
|
|
94
|
+
""",
|
|
95
|
+
"typescript": """
|
|
96
|
+
(call_expression function: (identifier) @ref)
|
|
97
|
+
(call_expression function: (member_expression property: (property_identifier) @ref))
|
|
98
|
+
(type_identifier) @ref
|
|
99
|
+
(identifier) @ref
|
|
100
|
+
""",
|
|
101
|
+
"go": """
|
|
102
|
+
(call_expression function: (identifier) @ref)
|
|
103
|
+
(call_expression function: (selector_expression field: (field_identifier) @ref))
|
|
104
|
+
(identifier) @ref
|
|
105
|
+
""",
|
|
106
|
+
"rust": """
|
|
107
|
+
(call_expression function: (identifier) @ref)
|
|
108
|
+
(call_expression function: (field_expression field: (field_identifier) @ref))
|
|
109
|
+
(identifier) @ref
|
|
110
|
+
""",
|
|
111
|
+
}
|
|
112
|
+
_REFERENCE_QUERIES["tsx"] = _REFERENCE_QUERIES["typescript"]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_parser(lang: str) -> ts.Parser | None:
|
|
116
|
+
"""Get or create a tree-sitter parser for the given language."""
|
|
117
|
+
if not TREE_SITTER_AVAILABLE:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
if lang in _PARSERS:
|
|
121
|
+
return _PARSERS[lang]
|
|
122
|
+
|
|
123
|
+
grammar = _GRAMMAR_MODULES.get(lang)
|
|
124
|
+
if not grammar:
|
|
125
|
+
return None
|
|
126
|
+
mod_name, loader_name = grammar
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
import importlib
|
|
130
|
+
|
|
131
|
+
grammar_mod = importlib.import_module(mod_name)
|
|
132
|
+
language_fn = getattr(grammar_mod, loader_name)
|
|
133
|
+
language = ts.Language(language_fn())
|
|
134
|
+
parser = ts.Parser(language)
|
|
135
|
+
_PARSERS[lang] = parser
|
|
136
|
+
return parser
|
|
137
|
+
except (ImportError, AttributeError, Exception) as e:
|
|
138
|
+
logger.debug(f"tree-sitter grammar not available for {lang}: {e}")
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _query_captures(query: ts.Query, root_node: ts.Node) -> list[tuple[ts.Node, str]]:
|
|
143
|
+
legacy_captures = getattr(query, "captures", None)
|
|
144
|
+
if legacy_captures is not None:
|
|
145
|
+
return legacy_captures(root_node)
|
|
146
|
+
|
|
147
|
+
captures = ts.QueryCursor(query).captures(root_node)
|
|
148
|
+
if isinstance(captures, dict):
|
|
149
|
+
flattened = [
|
|
150
|
+
(node, capture_name) for capture_name, nodes in captures.items() for node in nodes
|
|
151
|
+
]
|
|
152
|
+
return sorted(flattened, key=lambda item: item[0].start_byte)
|
|
153
|
+
return captures
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def ts_extract_symbols(path: Path, lang: str, max_symbols: int = 200) -> list[str] | None:
|
|
157
|
+
"""Extract symbol definitions using tree-sitter AST parsing.
|
|
158
|
+
|
|
159
|
+
Returns a list of formatted symbol lines, or None if tree-sitter
|
|
160
|
+
is not available for this language (caller should fall back to regex).
|
|
161
|
+
"""
|
|
162
|
+
parser = _get_parser(lang)
|
|
163
|
+
if parser is None:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
source = path.read_bytes()
|
|
168
|
+
tree = parser.parse(source)
|
|
169
|
+
except (OSError, Exception) as e:
|
|
170
|
+
logger.debug(f"tree-sitter parse failed for {path}: {e}")
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
query_src = _DEFINITION_QUERIES.get(lang)
|
|
174
|
+
if not query_src:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
language = parser.language
|
|
179
|
+
query = language.query(query_src)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.debug(f"tree-sitter query compilation failed for {lang}: {e}")
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
symbols: list[str] = []
|
|
185
|
+
captures = _query_captures(query, tree.root_node)
|
|
186
|
+
|
|
187
|
+
# Process captures — look for @name captures paired with @def
|
|
188
|
+
seen_lines: set[int] = set()
|
|
189
|
+
for node, capture_name in captures:
|
|
190
|
+
if capture_name != "name":
|
|
191
|
+
continue
|
|
192
|
+
if len(symbols) >= max_symbols:
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
line = node.start_point[0] + 1 # 1-indexed
|
|
196
|
+
if line in seen_lines:
|
|
197
|
+
continue
|
|
198
|
+
seen_lines.add(line)
|
|
199
|
+
|
|
200
|
+
name = source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
201
|
+
|
|
202
|
+
# Determine kind from the enclosing definition node.
|
|
203
|
+
parent = node.parent
|
|
204
|
+
kind = _node_to_kind(parent, lang, source)
|
|
205
|
+
|
|
206
|
+
# Calculate indent
|
|
207
|
+
indent = node.start_point[1]
|
|
208
|
+
prefix = " " * (indent // 4) if indent > 0 else ""
|
|
209
|
+
symbols.append(f" {line:4d} {prefix}{kind} {name}")
|
|
210
|
+
|
|
211
|
+
return symbols if symbols else None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def ts_find_references(
|
|
215
|
+
path: Path, lang: str, name: str, max_results: int = 100
|
|
216
|
+
) -> list[tuple[int, str]] | None:
|
|
217
|
+
"""Find references to a symbol using tree-sitter AST parsing.
|
|
218
|
+
|
|
219
|
+
Returns a list of (line_number, line_text) tuples, or None if
|
|
220
|
+
tree-sitter is not available (caller should fall back to regex).
|
|
221
|
+
|
|
222
|
+
Filters out definition sites — only returns usages/call sites.
|
|
223
|
+
"""
|
|
224
|
+
parser = _get_parser(lang)
|
|
225
|
+
if parser is None:
|
|
226
|
+
return None
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
source = path.read_bytes()
|
|
230
|
+
tree = parser.parse(source)
|
|
231
|
+
except (OSError, Exception):
|
|
232
|
+
return None
|
|
233
|
+
|
|
234
|
+
query_src = _REFERENCE_QUERIES.get(lang)
|
|
235
|
+
if not query_src:
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
language = parser.language
|
|
240
|
+
query = language.query(query_src)
|
|
241
|
+
except Exception:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
lines = source.decode("utf-8", errors="replace").splitlines()
|
|
245
|
+
captures = _query_captures(query, tree.root_node)
|
|
246
|
+
|
|
247
|
+
# Handle qualified names (e.g. "TraceExplorer.from_file" → match "from_file")
|
|
248
|
+
search_name = name.split(".")[-1] if "." in name else name
|
|
249
|
+
qualifier = name.split(".")[0] if "." in name else None
|
|
250
|
+
|
|
251
|
+
results: list[tuple[int, str]] = []
|
|
252
|
+
seen_lines: set[int] = set()
|
|
253
|
+
|
|
254
|
+
for node, capture_name in captures:
|
|
255
|
+
if capture_name != "ref":
|
|
256
|
+
continue
|
|
257
|
+
if len(results) >= max_results:
|
|
258
|
+
break
|
|
259
|
+
|
|
260
|
+
node_text = source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
261
|
+
if node_text != search_name:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
line = node.start_point[0] + 1 # 1-indexed
|
|
265
|
+
if line in seen_lines:
|
|
266
|
+
continue
|
|
267
|
+
|
|
268
|
+
# Skip definition sites
|
|
269
|
+
parent = node.parent
|
|
270
|
+
if parent and _is_definition_node(parent.type, lang):
|
|
271
|
+
continue
|
|
272
|
+
|
|
273
|
+
# If qualified name, check that qualifier appears on the same line
|
|
274
|
+
if qualifier and line <= len(lines):
|
|
275
|
+
line_text = lines[line - 1]
|
|
276
|
+
if qualifier not in line_text:
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
seen_lines.add(line)
|
|
280
|
+
line_text = lines[line - 1] if line <= len(lines) else ""
|
|
281
|
+
results.append((line, line_text.strip()))
|
|
282
|
+
|
|
283
|
+
return results if results else None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _node_to_kind(node: ts.Node | None, lang: str, source: bytes) -> str:
|
|
287
|
+
"""Map a tree-sitter definition node to a human-readable kind string."""
|
|
288
|
+
if node is None:
|
|
289
|
+
return "symbol"
|
|
290
|
+
|
|
291
|
+
if lang == "python" and node.type == "function_definition":
|
|
292
|
+
line = source.splitlines()[node.start_point[0]].lstrip()
|
|
293
|
+
if line.startswith(b"async def "):
|
|
294
|
+
return "async function"
|
|
295
|
+
|
|
296
|
+
if lang == "go" and node.type == "type_spec":
|
|
297
|
+
for child in node.children:
|
|
298
|
+
if child.type == "struct_type":
|
|
299
|
+
return "struct"
|
|
300
|
+
if child.type == "interface_type":
|
|
301
|
+
return "interface"
|
|
302
|
+
|
|
303
|
+
return _node_type_to_kind(node.type, lang)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _node_type_to_kind(node_type: str, lang: str) -> str:
|
|
307
|
+
"""Map tree-sitter node type to a human-readable kind string."""
|
|
308
|
+
mapping = {
|
|
309
|
+
"function_definition": "function",
|
|
310
|
+
"function_declaration": "function",
|
|
311
|
+
"function_item": "function",
|
|
312
|
+
"method_definition": "method",
|
|
313
|
+
"method_declaration": "method",
|
|
314
|
+
"class_definition": "class",
|
|
315
|
+
"class_declaration": "class",
|
|
316
|
+
"struct_item": "struct",
|
|
317
|
+
"enum_item": "enum",
|
|
318
|
+
"trait_item": "trait",
|
|
319
|
+
"impl_item": "impl",
|
|
320
|
+
"interface_declaration": "interface",
|
|
321
|
+
"type_alias_declaration": "type",
|
|
322
|
+
"type_declaration": "type",
|
|
323
|
+
"type_spec": "type",
|
|
324
|
+
"lexical_declaration": "function",
|
|
325
|
+
"variable_declarator": "function",
|
|
326
|
+
}
|
|
327
|
+
return mapping.get(node_type, "symbol")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _is_definition_node(node_type: str, lang: str) -> bool:
|
|
331
|
+
"""Return True if the node type represents a definition (not a reference)."""
|
|
332
|
+
def_types = {
|
|
333
|
+
"function_definition",
|
|
334
|
+
"function_declaration",
|
|
335
|
+
"function_item",
|
|
336
|
+
"method_definition",
|
|
337
|
+
"method_declaration",
|
|
338
|
+
"class_definition",
|
|
339
|
+
"class_declaration",
|
|
340
|
+
"struct_item",
|
|
341
|
+
"enum_item",
|
|
342
|
+
"trait_item",
|
|
343
|
+
"impl_item",
|
|
344
|
+
"interface_declaration",
|
|
345
|
+
"type_alias_declaration",
|
|
346
|
+
"type_declaration",
|
|
347
|
+
"type_spec",
|
|
348
|
+
"lexical_declaration",
|
|
349
|
+
"variable_declarator",
|
|
350
|
+
}
|
|
351
|
+
return node_type in def_types
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
2
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
"""Async-native composable shell piping in Python with method-chaining streams."""
|
|
4
|
+
|
|
5
|
+
from nooa.skill import Skill
|
|
6
|
+
from nooa_cli.tools.pyp.errors import PipeError as PipeError
|
|
7
|
+
from nooa_cli.tools.pyp.errors import Result as Result
|
|
8
|
+
from nooa_cli.tools.pyp.errors import make_pipe_error as make_pipe_error
|
|
9
|
+
from nooa_cli.tools.pyp.sources import arun as arun
|
|
10
|
+
from nooa_cli.tools.pyp.sources import cat as cat
|
|
11
|
+
from nooa_cli.tools.pyp.sources import empty as empty
|
|
12
|
+
from nooa_cli.tools.pyp.sources import find as find
|
|
13
|
+
from nooa_cli.tools.pyp.sources import glob as glob
|
|
14
|
+
from nooa_cli.tools.pyp.sources import items as items
|
|
15
|
+
from nooa_cli.tools.pyp.sources import lines as lines
|
|
16
|
+
from nooa_cli.tools.pyp.sources import rg as rg
|
|
17
|
+
from nooa_cli.tools.pyp.sources import run as run
|
|
18
|
+
from nooa_cli.tools.pyp.sources import seq as seq
|
|
19
|
+
from nooa_cli.tools.pyp.sources import stdin as stdin
|
|
20
|
+
from nooa_cli.tools.pyp.stream import Stream as Stream
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Pyp(Skill):
|
|
24
|
+
"""Async-native shell piping — source().transforms().sink().
|
|
25
|
+
|
|
26
|
+
Build pipelines: pick a source, chain transforms, await a sink.
|
|
27
|
+
All streaming, all async, all non-blocking.
|
|
28
|
+
|
|
29
|
+
Examples::
|
|
30
|
+
|
|
31
|
+
# Search logs
|
|
32
|
+
errors = await self.pyp.cat("app.log").grep("ERROR").head(10).collect()
|
|
33
|
+
|
|
34
|
+
# Find files
|
|
35
|
+
files = await self.pyp.find("src", name="*.py").sort().collect()
|
|
36
|
+
|
|
37
|
+
# Ripgrep → count
|
|
38
|
+
n = await self.pyp.rg("TODO", type_filter="py").count()
|
|
39
|
+
|
|
40
|
+
# Shell command → table
|
|
41
|
+
table = await self.pyp.run("ps aux").head(5).table()
|
|
42
|
+
|
|
43
|
+
# Apply function to each line
|
|
44
|
+
out = await self.pyp.run("ls").xargs(process).collect()
|
|
45
|
+
|
|
46
|
+
# Pipeline → file
|
|
47
|
+
n = await self.pyp.cat("data.csv").grep("error").write("errors.csv")
|
|
48
|
+
|
|
49
|
+
# Stream via ShellTools
|
|
50
|
+
fails = await self.pyp.arun(self.shell, "make test").grep("FAIL").collect()
|
|
51
|
+
|
|
52
|
+
Transforms (on any Stream): .grep() .head() .tail() .sort() .uniq()
|
|
53
|
+
.cut() .sed() .map() .filter() .xargs() .wc() .skip() .strip()
|
|
54
|
+
.tee() .flatten() .take_while() .drop_while() .pipe()
|
|
55
|
+
|
|
56
|
+
Sinks: .collect() .text() .first() .last() .count() .table()
|
|
57
|
+
.json() .result() .write() .to_set() .to_dict()
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
# Expose all source functions as instance methods
|
|
61
|
+
cat = staticmethod(cat)
|
|
62
|
+
run = staticmethod(run)
|
|
63
|
+
arun = staticmethod(arun)
|
|
64
|
+
find = staticmethod(find)
|
|
65
|
+
glob = staticmethod(glob)
|
|
66
|
+
rg = staticmethod(rg)
|
|
67
|
+
seq = staticmethod(seq)
|
|
68
|
+
lines = staticmethod(lines)
|
|
69
|
+
items = staticmethod(items)
|
|
70
|
+
empty = staticmethod(empty)
|
|
71
|
+
stdin = staticmethod(stdin)
|