invar-tools 1.8.0__py3-none-any.whl → 1.10.0__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.
- invar/__init__.py +8 -0
- invar/core/language.py +88 -0
- invar/core/models.py +106 -0
- invar/core/patterns/detector.py +6 -1
- invar/core/patterns/p0_exhaustive.py +15 -3
- invar/core/patterns/p0_literal.py +15 -3
- invar/core/patterns/p0_newtype.py +15 -3
- invar/core/patterns/p0_nonempty.py +15 -3
- invar/core/patterns/p0_validation.py +15 -3
- invar/core/patterns/registry.py +5 -1
- invar/core/patterns/types.py +5 -1
- invar/core/property_gen.py +4 -0
- invar/core/rules.py +84 -18
- invar/core/sync_helpers.py +27 -1
- invar/core/ts_parsers.py +286 -0
- invar/core/ts_sig_parser.py +307 -0
- invar/node_tools/MANIFEST +7 -0
- invar/node_tools/__init__.py +51 -0
- invar/node_tools/fc-runner/cli.js +77 -0
- invar/node_tools/quick-check/cli.js +28 -0
- invar/node_tools/ts-analyzer/cli.js +480 -0
- invar/shell/claude_hooks.py +35 -12
- invar/shell/commands/guard.py +36 -1
- invar/shell/commands/init.py +82 -3
- invar/shell/commands/perception.py +157 -33
- invar/shell/commands/skill.py +187 -0
- invar/shell/commands/template_sync.py +65 -13
- invar/shell/commands/uninstall.py +60 -12
- invar/shell/commands/update.py +6 -14
- invar/shell/contract_coverage.py +1 -0
- invar/shell/fs.py +66 -13
- invar/shell/pi_hooks.py +6 -0
- invar/shell/prove/guard_ts.py +899 -0
- invar/shell/skill_manager.py +353 -0
- invar/shell/template_engine.py +28 -4
- invar/shell/templates.py +4 -4
- invar/templates/claude-md/python/critical-rules.md +33 -0
- invar/templates/claude-md/python/quick-reference.md +24 -0
- invar/templates/claude-md/typescript/critical-rules.md +40 -0
- invar/templates/claude-md/typescript/quick-reference.md +24 -0
- invar/templates/claude-md/universal/check-in.md +25 -0
- invar/templates/claude-md/universal/skills.md +73 -0
- invar/templates/claude-md/universal/workflow.md +55 -0
- invar/templates/commands/{audit.md → audit.md.jinja} +18 -1
- invar/templates/config/AGENT.md.jinja +58 -0
- invar/templates/config/CLAUDE.md.jinja +16 -209
- invar/templates/config/context.md.jinja +19 -0
- invar/templates/examples/{README.md → python/README.md} +2 -0
- invar/templates/examples/{conftest.py → python/conftest.py} +1 -1
- invar/templates/examples/{contracts.py → python/contracts.py} +81 -4
- invar/templates/examples/python/core_shell.py +227 -0
- invar/templates/examples/python/functional.py +613 -0
- invar/templates/examples/typescript/README.md +31 -0
- invar/templates/examples/typescript/contracts.ts +163 -0
- invar/templates/examples/typescript/core_shell.ts +374 -0
- invar/templates/examples/typescript/functional.ts +601 -0
- invar/templates/examples/typescript/workflow.md +95 -0
- invar/templates/hooks/PostToolUse.sh.jinja +10 -1
- invar/templates/hooks/PreToolUse.sh.jinja +38 -0
- invar/templates/hooks/Stop.sh.jinja +1 -1
- invar/templates/hooks/UserPromptSubmit.sh.jinja +7 -0
- invar/templates/hooks/pi/invar.ts.jinja +9 -0
- invar/templates/manifest.toml +7 -6
- invar/templates/onboard/assessment.md.jinja +214 -0
- invar/templates/onboard/patterns/python.md +347 -0
- invar/templates/onboard/patterns/typescript.md +452 -0
- invar/templates/onboard/roadmap.md.jinja +168 -0
- invar/templates/protocol/INVAR.md.jinja +51 -0
- invar/templates/protocol/python/architecture-examples.md +41 -0
- invar/templates/protocol/python/contracts-syntax.md +56 -0
- invar/templates/protocol/python/markers.md +44 -0
- invar/templates/protocol/python/tools.md +24 -0
- invar/templates/protocol/python/troubleshooting.md +38 -0
- invar/templates/protocol/typescript/architecture-examples.md +52 -0
- invar/templates/protocol/typescript/contracts-syntax.md +73 -0
- invar/templates/protocol/typescript/markers.md +48 -0
- invar/templates/protocol/typescript/tools.md +65 -0
- invar/templates/protocol/typescript/troubleshooting.md +104 -0
- invar/templates/protocol/universal/architecture.md +36 -0
- invar/templates/protocol/universal/completion.md +14 -0
- invar/templates/protocol/universal/contracts-concept.md +37 -0
- invar/templates/protocol/universal/header.md +17 -0
- invar/templates/protocol/universal/session.md +17 -0
- invar/templates/protocol/universal/six-laws.md +10 -0
- invar/templates/protocol/universal/usbv.md +14 -0
- invar/templates/protocol/universal/visible-workflow.md +25 -0
- invar/templates/skills/develop/SKILL.md.jinja +39 -3
- invar/templates/skills/extensions/_registry.yaml +93 -0
- invar/templates/skills/extensions/acceptance/SKILL.md +383 -0
- invar/templates/skills/extensions/invar-onboard/SKILL.md +448 -0
- invar/templates/skills/extensions/invar-onboard/patterns/python.md +347 -0
- invar/templates/skills/extensions/invar-onboard/patterns/typescript.md +452 -0
- invar/templates/skills/extensions/invar-onboard/templates/assessment.md.jinja +214 -0
- invar/templates/skills/extensions/invar-onboard/templates/roadmap.md.jinja +168 -0
- invar/templates/skills/extensions/security/SKILL.md +382 -0
- invar/templates/skills/extensions/security/patterns/_common.yaml +126 -0
- invar/templates/skills/extensions/security/patterns/python.yaml +155 -0
- invar/templates/skills/extensions/security/patterns/typescript.yaml +194 -0
- invar/templates/skills/review/SKILL.md.jinja +331 -71
- {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/METADATA +304 -12
- invar_tools-1.10.0.dist-info/RECORD +173 -0
- invar/templates/examples/core_shell.py +0 -127
- invar/templates/protocol/INVAR.md +0 -310
- invar_tools-1.8.0.dist-info/RECORD +0 -116
- /invar/templates/examples/{workflow.md → python/workflow.md} +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.8.0.dist-info → invar_tools-1.10.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"""TypeScript Signature Extraction (Core).
|
|
2
|
+
|
|
3
|
+
Pure logic for extracting function/class signatures from TypeScript source.
|
|
4
|
+
Part of LX-06 TypeScript tooling support.
|
|
5
|
+
|
|
6
|
+
Note: This is a regex-based MVP. Phase 2 can upgrade to tree-sitter for
|
|
7
|
+
more robust parsing.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Literal
|
|
15
|
+
|
|
16
|
+
from deal import post, pre
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class TSSymbol:
|
|
21
|
+
"""Represents a TypeScript symbol (function, class, interface, type)."""
|
|
22
|
+
|
|
23
|
+
name: str
|
|
24
|
+
kind: Literal["function", "class", "interface", "type", "const", "method"]
|
|
25
|
+
signature: str
|
|
26
|
+
line: int
|
|
27
|
+
docstring: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# Regex patterns for TypeScript constructs
|
|
31
|
+
# Note: These are simplified patterns suitable for common cases
|
|
32
|
+
_FUNCTION_PATTERN = re.compile(
|
|
33
|
+
r"^\s*(?:@\w+(?:\([^)]*\))?\s*\n\s*)*" # Optional decorators
|
|
34
|
+
r"(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*"
|
|
35
|
+
r"(<[^>]*>)?" # Optional generics
|
|
36
|
+
r"\(([^)]*)\)" # Parameters
|
|
37
|
+
r"(?:\s*:\s*([^\n{]+))?" # Optional return type
|
|
38
|
+
r"\s*\{",
|
|
39
|
+
re.MULTILINE,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_ARROW_FUNCTION_PATTERN = re.compile(
|
|
43
|
+
r"^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*"
|
|
44
|
+
r"(?::\s*[^=]+)?\s*=\s*"
|
|
45
|
+
r"(?:async\s+)?\([^)]*\)\s*"
|
|
46
|
+
r"(?::\s*[^\n=>]+)?\s*=>",
|
|
47
|
+
re.MULTILINE,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_CLASS_PATTERN = re.compile(
|
|
51
|
+
r"^\s*(?:@\w+(?:\([^)]*\))?\s*\n\s*)*" # Optional decorators
|
|
52
|
+
r"(?:export\s+)?(?:abstract\s+)?class\s+(\w+)"
|
|
53
|
+
r"(?:<[^{]*>)?" # Optional generics (allow nested <> by stopping at {)
|
|
54
|
+
r"(?:\s+extends\s+[^\s{]+)?" # Optional extends
|
|
55
|
+
r"(?:\s+implements\s+[^\s{]+)?" # Optional implements
|
|
56
|
+
r"\s*\{",
|
|
57
|
+
re.MULTILINE,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
_INTERFACE_PATTERN = re.compile(
|
|
61
|
+
r"^\s*(?:export\s+)?interface\s+(\w+)"
|
|
62
|
+
r"(?:<[^{]*>)?" # Optional generics (allow nested <> by stopping at {)
|
|
63
|
+
r"(?:\s+extends\s+[^\s{]+)?" # Optional extends
|
|
64
|
+
r"\s*\{",
|
|
65
|
+
re.MULTILINE,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
_TYPE_ALIAS_PATTERN = re.compile(
|
|
69
|
+
r"^\s*(?:export\s+)?type\s+(\w+)"
|
|
70
|
+
r"(?:<[^=]*>)?" # Optional generics (allow nested <> by stopping at =)
|
|
71
|
+
r"\s*=",
|
|
72
|
+
re.MULTILINE,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
_JSDOC_PATTERN = re.compile(
|
|
76
|
+
r"/\*\*\s*(.*?)\s*\*/",
|
|
77
|
+
re.DOTALL,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# @invar:allow function_size: Regex extraction inherently repetitive per TS construct type
|
|
82
|
+
@pre(lambda source: source is not None) # Accepts any string including empty
|
|
83
|
+
@post(lambda result: all(s.line > 0 for s in result)) # All symbols have valid line numbers
|
|
84
|
+
def extract_ts_signatures(source: str) -> list[TSSymbol]:
|
|
85
|
+
"""Extract TypeScript symbols from source code.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
source: TypeScript source code.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
List of TSSymbol objects representing functions, classes, etc.
|
|
92
|
+
|
|
93
|
+
>>> code = '''
|
|
94
|
+
... function greet(name: string): string {
|
|
95
|
+
... return `Hello, ${name}`;
|
|
96
|
+
... }
|
|
97
|
+
... '''
|
|
98
|
+
>>> symbols = extract_ts_signatures(code)
|
|
99
|
+
>>> len(symbols)
|
|
100
|
+
1
|
|
101
|
+
>>> symbols[0].name
|
|
102
|
+
'greet'
|
|
103
|
+
>>> symbols[0].kind
|
|
104
|
+
'function'
|
|
105
|
+
|
|
106
|
+
>>> code2 = '''
|
|
107
|
+
... export class User {
|
|
108
|
+
... constructor(public name: string) {}
|
|
109
|
+
... }
|
|
110
|
+
... '''
|
|
111
|
+
>>> symbols2 = extract_ts_signatures(code2)
|
|
112
|
+
>>> symbols2[0].name
|
|
113
|
+
'User'
|
|
114
|
+
>>> symbols2[0].kind
|
|
115
|
+
'class'
|
|
116
|
+
"""
|
|
117
|
+
# Early return for empty source (CrossHair-friendly)
|
|
118
|
+
if not source:
|
|
119
|
+
return []
|
|
120
|
+
|
|
121
|
+
symbols: list[TSSymbol] = []
|
|
122
|
+
lines = source.split("\n")
|
|
123
|
+
|
|
124
|
+
# Find JSDoc comments for association
|
|
125
|
+
jsdoc_positions: dict[int, str] = {}
|
|
126
|
+
for match in _JSDOC_PATTERN.finditer(source):
|
|
127
|
+
# Find line number of end of JSDoc
|
|
128
|
+
end_pos = match.end()
|
|
129
|
+
line_num = source[:end_pos].count("\n")
|
|
130
|
+
jsdoc_positions[line_num] = match.group(1).strip()
|
|
131
|
+
|
|
132
|
+
def get_jsdoc(line: int) -> str | None:
|
|
133
|
+
"""Get JSDoc for a symbol at given line."""
|
|
134
|
+
# Check previous lines for JSDoc
|
|
135
|
+
for i in range(max(0, line - 5), line):
|
|
136
|
+
if i in jsdoc_positions:
|
|
137
|
+
return jsdoc_positions[i]
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def get_line_number(pos: int) -> int:
|
|
141
|
+
"""Convert character position to line number (1-indexed)."""
|
|
142
|
+
return source[:pos].count("\n") + 1
|
|
143
|
+
|
|
144
|
+
# Extract functions
|
|
145
|
+
for match in _FUNCTION_PATTERN.finditer(source):
|
|
146
|
+
name = match.group(1)
|
|
147
|
+
# Find actual function line (skip decorators)
|
|
148
|
+
match_text = match.group(0)
|
|
149
|
+
func_keyword_pos = match_text.find("function ")
|
|
150
|
+
actual_start = match.start() + func_keyword_pos if func_keyword_pos >= 0 else match.start()
|
|
151
|
+
line = get_line_number(actual_start)
|
|
152
|
+
# Use line content for signature (handles complex types better than regex groups)
|
|
153
|
+
line_content = lines[line - 1].strip() if line <= len(lines) else ""
|
|
154
|
+
signature = line_content.rstrip("{").rstrip()
|
|
155
|
+
symbols.append(
|
|
156
|
+
TSSymbol(
|
|
157
|
+
name=name,
|
|
158
|
+
kind="function",
|
|
159
|
+
signature=signature,
|
|
160
|
+
line=line,
|
|
161
|
+
docstring=get_jsdoc(line - 1),
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Extract arrow functions (const declarations)
|
|
166
|
+
for match in _ARROW_FUNCTION_PATTERN.finditer(source):
|
|
167
|
+
name = match.group(1)
|
|
168
|
+
line = get_line_number(match.start())
|
|
169
|
+
# For arrow functions, extract the full line as signature approximation
|
|
170
|
+
line_content = lines[line - 1].strip() if line <= len(lines) else ""
|
|
171
|
+
signature = line_content.rstrip("{").rstrip()
|
|
172
|
+
symbols.append(
|
|
173
|
+
TSSymbol(
|
|
174
|
+
name=name,
|
|
175
|
+
kind="const",
|
|
176
|
+
signature=signature,
|
|
177
|
+
line=line,
|
|
178
|
+
docstring=get_jsdoc(line - 1),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Extract classes
|
|
183
|
+
for match in _CLASS_PATTERN.finditer(source):
|
|
184
|
+
name = match.group(1)
|
|
185
|
+
# Find actual class line (skip decorators)
|
|
186
|
+
match_text = match.group(0)
|
|
187
|
+
class_keyword_pos = match_text.find("class ")
|
|
188
|
+
actual_start = match.start() + class_keyword_pos if class_keyword_pos >= 0 else match.start()
|
|
189
|
+
line = get_line_number(actual_start)
|
|
190
|
+
line_content = lines[line - 1].strip() if line <= len(lines) else ""
|
|
191
|
+
signature = line_content.rstrip("{").rstrip()
|
|
192
|
+
symbols.append(
|
|
193
|
+
TSSymbol(
|
|
194
|
+
name=name,
|
|
195
|
+
kind="class",
|
|
196
|
+
signature=signature,
|
|
197
|
+
line=line,
|
|
198
|
+
docstring=get_jsdoc(line - 1),
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Extract interfaces
|
|
203
|
+
for match in _INTERFACE_PATTERN.finditer(source):
|
|
204
|
+
name = match.group(1)
|
|
205
|
+
line = get_line_number(match.start())
|
|
206
|
+
line_content = lines[line - 1].strip() if line <= len(lines) else ""
|
|
207
|
+
signature = line_content.rstrip("{").rstrip()
|
|
208
|
+
symbols.append(
|
|
209
|
+
TSSymbol(
|
|
210
|
+
name=name,
|
|
211
|
+
kind="interface",
|
|
212
|
+
signature=signature,
|
|
213
|
+
line=line,
|
|
214
|
+
docstring=get_jsdoc(line - 1),
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Extract type aliases
|
|
219
|
+
for match in _TYPE_ALIAS_PATTERN.finditer(source):
|
|
220
|
+
name = match.group(1)
|
|
221
|
+
line = get_line_number(match.start())
|
|
222
|
+
line_content = lines[line - 1].strip() if line <= len(lines) else ""
|
|
223
|
+
# Include the full type definition
|
|
224
|
+
signature = line_content.rstrip(";").strip()
|
|
225
|
+
symbols.append(
|
|
226
|
+
TSSymbol(
|
|
227
|
+
name=name,
|
|
228
|
+
kind="type",
|
|
229
|
+
signature=signature,
|
|
230
|
+
line=line,
|
|
231
|
+
docstring=get_jsdoc(line - 1),
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Sort by line number
|
|
236
|
+
symbols.sort(key=lambda s: s.line)
|
|
237
|
+
|
|
238
|
+
return symbols
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@pre(lambda symbols, file_path="": all(s.line > 0 for s in symbols)) # All symbols have valid line numbers
|
|
242
|
+
@post(lambda result: "file" in result and "symbols" in result)
|
|
243
|
+
def format_ts_signatures_json(
|
|
244
|
+
symbols: list[TSSymbol], file_path: str = ""
|
|
245
|
+
) -> dict:
|
|
246
|
+
"""Format TypeScript symbols as JSON output.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
symbols: List of TSSymbol objects.
|
|
250
|
+
file_path: Source file path.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
JSON-serializable dictionary.
|
|
254
|
+
|
|
255
|
+
>>> symbols = [TSSymbol("foo", "function", "function foo(): void", 1)]
|
|
256
|
+
>>> result = format_ts_signatures_json(symbols, "test.ts")
|
|
257
|
+
>>> result["file"]
|
|
258
|
+
'test.ts'
|
|
259
|
+
>>> len(result["symbols"])
|
|
260
|
+
1
|
|
261
|
+
"""
|
|
262
|
+
return {
|
|
263
|
+
"file": file_path,
|
|
264
|
+
"symbols": [
|
|
265
|
+
{
|
|
266
|
+
"name": s.name,
|
|
267
|
+
"kind": s.kind,
|
|
268
|
+
"signature": s.signature,
|
|
269
|
+
"line": s.line,
|
|
270
|
+
"docstring": s.docstring,
|
|
271
|
+
}
|
|
272
|
+
for s in symbols
|
|
273
|
+
],
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@pre(lambda symbols, file_path="": all(s.line > 0 for s in symbols)) # All symbols have valid line numbers
|
|
278
|
+
@post(lambda result: len(result) > 0) # Always produces output (at least header)
|
|
279
|
+
def format_ts_signatures_text(
|
|
280
|
+
symbols: list[TSSymbol], file_path: str = ""
|
|
281
|
+
) -> str:
|
|
282
|
+
"""Format TypeScript symbols as human-readable text.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
symbols: List of TSSymbol objects.
|
|
286
|
+
file_path: Source file path.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Formatted text output.
|
|
290
|
+
|
|
291
|
+
>>> symbols = [TSSymbol("foo", "function", "function foo(): void", 1)]
|
|
292
|
+
>>> text = format_ts_signatures_text(symbols, "test.ts")
|
|
293
|
+
>>> "foo" in text
|
|
294
|
+
True
|
|
295
|
+
"""
|
|
296
|
+
lines = [f"# {file_path}" if file_path else "# TypeScript Signatures", ""]
|
|
297
|
+
|
|
298
|
+
for symbol in symbols:
|
|
299
|
+
lines.append(f"[{symbol.kind}] {symbol.name} (line {symbol.line})")
|
|
300
|
+
lines.append(f" {symbol.signature}")
|
|
301
|
+
if symbol.docstring:
|
|
302
|
+
# Truncate long docstrings
|
|
303
|
+
doc = symbol.docstring[:100] + "..." if len(symbol.docstring) > 100 else symbol.docstring
|
|
304
|
+
lines.append(f" /** {doc} */")
|
|
305
|
+
lines.append("")
|
|
306
|
+
|
|
307
|
+
return "\n".join(lines)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Embedded Node.js tools for TypeScript verification.
|
|
2
|
+
|
|
3
|
+
This module provides access to @invar/* Node tools that are embedded
|
|
4
|
+
in the Python package for zero-configuration TypeScript support.
|
|
5
|
+
|
|
6
|
+
The tools are built from typescript/packages/* and copied here during
|
|
7
|
+
the Python package build process. Users who install invar-tools via pip
|
|
8
|
+
get these tools automatically without needing npm.
|
|
9
|
+
|
|
10
|
+
Workflow:
|
|
11
|
+
1. Developer: typescript/packages/*/src/ -> pnpm build -> dist/
|
|
12
|
+
2. Release: scripts/embed_node_tools.py copies dist/ -> src/invar/node_tools/
|
|
13
|
+
3. User: pip install invar-tools (includes node_tools/)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# @invar:allow shell_result: Simple path lookup, None is clear return value
|
|
20
|
+
def get_tool_path(tool_name: str) -> Path | None:
|
|
21
|
+
"""Get path to an embedded Node tool.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
tool_name: Tool name without @invar/ prefix (e.g., "ts-analyzer")
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Path to cli.js if embedded, None otherwise.
|
|
28
|
+
|
|
29
|
+
Examples:
|
|
30
|
+
>>> # When tools are embedded (after running embed script)
|
|
31
|
+
>>> path = get_tool_path("ts-analyzer")
|
|
32
|
+
>>> # Returns: Path(".../node_tools/ts-analyzer/cli.js") or None
|
|
33
|
+
"""
|
|
34
|
+
tool_dir = Path(__file__).parent / tool_name
|
|
35
|
+
cli_js = tool_dir / "cli.js"
|
|
36
|
+
return cli_js if cli_js.exists() else None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# @invar:allow shell_result: Directory listing for embedded tools discovery
|
|
40
|
+
def list_available_tools() -> list[str]:
|
|
41
|
+
"""List all embedded tools.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
List of tool names that have cli.js available.
|
|
45
|
+
"""
|
|
46
|
+
tools_dir = Path(__file__).parent
|
|
47
|
+
available = []
|
|
48
|
+
for subdir in tools_dir.iterdir():
|
|
49
|
+
if subdir.is_dir() and (subdir / "cli.js").exists():
|
|
50
|
+
available.append(subdir.name)
|
|
51
|
+
return sorted(available)
|