ragtime-cli 0.2.2__py3-none-any.whl → 0.2.4__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.
Potentially problematic release.
This version of ragtime-cli might be problematic. Click here for more details.
- {ragtime_cli-0.2.2.dist-info → ragtime_cli-0.2.4.dist-info}/METADATA +179 -42
- {ragtime_cli-0.2.2.dist-info → ragtime_cli-0.2.4.dist-info}/RECORD +11 -9
- src/cli.py +657 -7
- src/commands/create-pr.md +389 -0
- src/commands/generate-docs.md +325 -0
- src/commands/pr-graduate.md +7 -2
- src/config.py +19 -0
- {ragtime_cli-0.2.2.dist-info → ragtime_cli-0.2.4.dist-info}/WHEEL +0 -0
- {ragtime_cli-0.2.2.dist-info → ragtime_cli-0.2.4.dist-info}/entry_points.txt +0 -0
- {ragtime_cli-0.2.2.dist-info → ragtime_cli-0.2.4.dist-info}/licenses/LICENSE +0 -0
- {ragtime_cli-0.2.2.dist-info → ragtime_cli-0.2.4.dist-info}/top_level.txt +0 -0
src/cli.py
CHANGED
|
@@ -166,7 +166,7 @@ def get_remote_branches_with_ragtime(path: Path) -> list[str]:
|
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
@click.group()
|
|
169
|
-
@click.version_option(version="0.2.
|
|
169
|
+
@click.version_option(version="0.2.4")
|
|
170
170
|
def main():
|
|
171
171
|
"""Ragtime - semantic search over code and documentation."""
|
|
172
172
|
pass
|
|
@@ -206,11 +206,45 @@ index/
|
|
|
206
206
|
"""
|
|
207
207
|
gitignore_path.write_text(gitignore_content)
|
|
208
208
|
|
|
209
|
+
# Create conventions file template
|
|
210
|
+
conventions_file = ragtime_dir / "CONVENTIONS.md"
|
|
211
|
+
if not conventions_file.exists():
|
|
212
|
+
conventions_file.write_text("""# Team Conventions
|
|
213
|
+
|
|
214
|
+
Rules and patterns that code must follow. These are checked by `/create-pr`.
|
|
215
|
+
|
|
216
|
+
## Code Style
|
|
217
|
+
|
|
218
|
+
- [ ] Example: Use async/await, not .then() chains
|
|
219
|
+
- [ ] Example: All API endpoints must use auth middleware
|
|
220
|
+
|
|
221
|
+
## Architecture
|
|
222
|
+
|
|
223
|
+
- [ ] Example: Services should not directly access repositories from other domains
|
|
224
|
+
|
|
225
|
+
## Security
|
|
226
|
+
|
|
227
|
+
- [ ] Example: Never commit .env or credentials files
|
|
228
|
+
- [ ] Example: All user input must be validated
|
|
229
|
+
|
|
230
|
+
## Testing
|
|
231
|
+
|
|
232
|
+
- [ ] Example: All new features need unit tests
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
Add your team's conventions above. Each rule should be:
|
|
237
|
+
- Clear and specific
|
|
238
|
+
- Checkable against code
|
|
239
|
+
- Actionable (what to do, not just what not to do)
|
|
240
|
+
""")
|
|
241
|
+
|
|
209
242
|
click.echo(f"\nCreated .ragtime/ structure:")
|
|
210
|
-
click.echo(f" app/
|
|
211
|
-
click.echo(f" team/
|
|
212
|
-
click.echo(f" branches/
|
|
213
|
-
click.echo(f" archive/
|
|
243
|
+
click.echo(f" app/ - Graduated knowledge (tracked)")
|
|
244
|
+
click.echo(f" team/ - Team conventions (tracked)")
|
|
245
|
+
click.echo(f" branches/ - Active branches (yours tracked, synced gitignored)")
|
|
246
|
+
click.echo(f" archive/ - Completed branches (tracked)")
|
|
247
|
+
click.echo(f" CONVENTIONS.md - Team rules checked by /create-pr")
|
|
214
248
|
|
|
215
249
|
# Check for ghp-cli
|
|
216
250
|
if check_ghp_installed():
|
|
@@ -362,6 +396,9 @@ def config(path: Path):
|
|
|
362
396
|
click.echo(f" Paths: {cfg.code.paths}")
|
|
363
397
|
click.echo(f" Languages: {cfg.code.languages}")
|
|
364
398
|
click.echo(f" Exclude: {cfg.code.exclude}")
|
|
399
|
+
click.echo("\nConventions:")
|
|
400
|
+
click.echo(f" Files: {cfg.conventions.files}")
|
|
401
|
+
click.echo(f" Also search memories: {cfg.conventions.also_search_memories}")
|
|
365
402
|
|
|
366
403
|
|
|
367
404
|
# ============================================================================
|
|
@@ -709,21 +746,54 @@ def install_commands(global_install: bool, workspace_install: bool, list_command
|
|
|
709
746
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
710
747
|
commands_dir = get_commands_dir()
|
|
711
748
|
installed = 0
|
|
749
|
+
skipped = 0
|
|
750
|
+
namespaced = 0
|
|
712
751
|
|
|
713
752
|
for cmd in to_install:
|
|
714
753
|
source = commands_dir / f"{cmd}.md"
|
|
715
754
|
target = target_dir / f"{cmd}.md"
|
|
755
|
+
namespaced_target = target_dir / f"ragtime-{cmd}.md"
|
|
716
756
|
|
|
717
757
|
if target.exists() and not force:
|
|
718
|
-
if
|
|
758
|
+
# Check if it's our file (contains ragtime marker)
|
|
759
|
+
existing_content = target.read_text()
|
|
760
|
+
is_ragtime_file = "ragtime" in existing_content.lower() and "mcp__ragtime" in existing_content
|
|
761
|
+
|
|
762
|
+
if is_ragtime_file:
|
|
763
|
+
# It's our file, safe to overwrite
|
|
719
764
|
target.write_text(source.read_text())
|
|
765
|
+
click.echo(f" ✓ {cmd}.md (updated)")
|
|
720
766
|
installed += 1
|
|
767
|
+
else:
|
|
768
|
+
# Conflict with non-ragtime command
|
|
769
|
+
click.echo(f"\n⚠️ Conflict: {cmd}.md already exists (not a ragtime command)")
|
|
770
|
+
click.echo(f" 1. Overwrite with ragtime's version")
|
|
771
|
+
click.echo(f" 2. Skip (keep existing)")
|
|
772
|
+
click.echo(f" 3. Install as ragtime-{cmd}.md")
|
|
773
|
+
|
|
774
|
+
choice = click.prompt(" Choice", type=click.Choice(["1", "2", "3"]), default="2")
|
|
775
|
+
|
|
776
|
+
if choice == "1":
|
|
777
|
+
target.write_text(source.read_text())
|
|
778
|
+
click.echo(f" ✓ {cmd}.md (overwritten)")
|
|
779
|
+
installed += 1
|
|
780
|
+
elif choice == "2":
|
|
781
|
+
click.echo(f" • {cmd}.md (skipped)")
|
|
782
|
+
skipped += 1
|
|
783
|
+
else:
|
|
784
|
+
namespaced_target.write_text(source.read_text())
|
|
785
|
+
click.echo(f" ✓ ragtime-{cmd}.md")
|
|
786
|
+
namespaced += 1
|
|
721
787
|
else:
|
|
722
788
|
target.write_text(source.read_text())
|
|
723
789
|
click.echo(f" ✓ {cmd}.md")
|
|
724
790
|
installed += 1
|
|
725
791
|
|
|
726
792
|
click.echo(f"\nInstalled {installed} commands to {target_dir}")
|
|
793
|
+
if namespaced:
|
|
794
|
+
click.echo(f" ({namespaced} installed with ragtime- prefix)")
|
|
795
|
+
if skipped:
|
|
796
|
+
click.echo(f" ({skipped} skipped due to conflicts)")
|
|
727
797
|
|
|
728
798
|
|
|
729
799
|
@main.command("setup-ghp")
|
|
@@ -1105,6 +1175,586 @@ def daemon_status(path: Path):
|
|
|
1105
1175
|
pid_file.unlink()
|
|
1106
1176
|
|
|
1107
1177
|
|
|
1178
|
+
# ============================================================================
|
|
1179
|
+
# Debug Commands
|
|
1180
|
+
# ============================================================================
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
@main.group()
|
|
1184
|
+
def debug():
|
|
1185
|
+
"""Debug and verify the vector index."""
|
|
1186
|
+
pass
|
|
1187
|
+
|
|
1188
|
+
|
|
1189
|
+
@debug.command("search")
|
|
1190
|
+
@click.argument("query")
|
|
1191
|
+
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
1192
|
+
@click.option("--limit", "-l", default=5, help="Max results")
|
|
1193
|
+
@click.option("--show-vectors", is_flag=True, help="Show vector statistics")
|
|
1194
|
+
def debug_search(query: str, path: Path, limit: int, show_vectors: bool):
|
|
1195
|
+
"""Debug a search query - show scores and ranking details."""
|
|
1196
|
+
path = Path(path).resolve()
|
|
1197
|
+
db = get_db(path)
|
|
1198
|
+
|
|
1199
|
+
results = db.search(query=query, limit=limit)
|
|
1200
|
+
|
|
1201
|
+
if not results:
|
|
1202
|
+
click.echo("No results found.")
|
|
1203
|
+
return
|
|
1204
|
+
|
|
1205
|
+
click.echo(f"\nQuery: \"{query}\"")
|
|
1206
|
+
click.echo(f"{'─' * 60}")
|
|
1207
|
+
|
|
1208
|
+
for i, result in enumerate(results, 1):
|
|
1209
|
+
meta = result["metadata"]
|
|
1210
|
+
distance = result["distance"]
|
|
1211
|
+
similarity = 1 - distance if distance else None
|
|
1212
|
+
|
|
1213
|
+
click.echo(f"\n[{i}] {meta.get('file', 'unknown')}")
|
|
1214
|
+
click.echo(f" Distance: {distance:.4f}")
|
|
1215
|
+
click.echo(f" Similarity: {similarity:.4f} ({similarity * 100:.1f}%)")
|
|
1216
|
+
click.echo(f" Namespace: {meta.get('namespace', '-')}")
|
|
1217
|
+
click.echo(f" Type: {meta.get('type', '-')}")
|
|
1218
|
+
|
|
1219
|
+
# Show content preview
|
|
1220
|
+
preview = result["content"][:100].replace("\n", " ")
|
|
1221
|
+
click.echo(f" Preview: {preview}...")
|
|
1222
|
+
|
|
1223
|
+
if show_vectors:
|
|
1224
|
+
click.echo(f"\n{'─' * 60}")
|
|
1225
|
+
click.echo("Vector Statistics:")
|
|
1226
|
+
click.echo(f" Total indexed: {db.collection.count()}")
|
|
1227
|
+
click.echo(f" Embedding model: all-MiniLM-L6-v2 (ChromaDB default)")
|
|
1228
|
+
click.echo(f" Vector dimensions: 384")
|
|
1229
|
+
click.echo(f" Distance metric: cosine")
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
@debug.command("similar")
|
|
1233
|
+
@click.argument("file_path", type=click.Path(exists=True))
|
|
1234
|
+
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
1235
|
+
@click.option("--limit", "-l", default=5, help="Max similar docs")
|
|
1236
|
+
def debug_similar(file_path: str, path: Path, limit: int):
|
|
1237
|
+
"""Find documents similar to a given file."""
|
|
1238
|
+
path = Path(path).resolve()
|
|
1239
|
+
db = get_db(path)
|
|
1240
|
+
|
|
1241
|
+
# Read the file content
|
|
1242
|
+
try:
|
|
1243
|
+
content = Path(file_path).read_text()
|
|
1244
|
+
except Exception as e:
|
|
1245
|
+
click.echo(f"✗ Could not read file: {e}", err=True)
|
|
1246
|
+
return
|
|
1247
|
+
|
|
1248
|
+
# Use the content as the query
|
|
1249
|
+
results = db.search(query=content, limit=limit + 1) # +1 to exclude self
|
|
1250
|
+
|
|
1251
|
+
click.echo(f"\nDocuments similar to: {file_path}")
|
|
1252
|
+
click.echo(f"{'─' * 60}")
|
|
1253
|
+
|
|
1254
|
+
shown = 0
|
|
1255
|
+
for result in results:
|
|
1256
|
+
# Skip the file itself
|
|
1257
|
+
if result["metadata"].get("file", "").endswith(file_path):
|
|
1258
|
+
continue
|
|
1259
|
+
|
|
1260
|
+
shown += 1
|
|
1261
|
+
if shown > limit:
|
|
1262
|
+
break
|
|
1263
|
+
|
|
1264
|
+
meta = result["metadata"]
|
|
1265
|
+
distance = result["distance"]
|
|
1266
|
+
similarity = 1 - distance if distance else None
|
|
1267
|
+
|
|
1268
|
+
click.echo(f"\n[{shown}] {meta.get('file', 'unknown')}")
|
|
1269
|
+
click.echo(f" Similarity: {similarity:.4f} ({similarity * 100:.1f}%)")
|
|
1270
|
+
|
|
1271
|
+
preview = result["content"][:100].replace("\n", " ")
|
|
1272
|
+
click.echo(f" Preview: {preview}...")
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
@debug.command("stats")
|
|
1276
|
+
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
1277
|
+
@click.option("--by-namespace", is_flag=True, help="Show counts by namespace")
|
|
1278
|
+
@click.option("--by-type", is_flag=True, help="Show counts by type")
|
|
1279
|
+
def debug_stats(path: Path, by_namespace: bool, by_type: bool):
|
|
1280
|
+
"""Show detailed index statistics."""
|
|
1281
|
+
path = Path(path).resolve()
|
|
1282
|
+
db = get_db(path)
|
|
1283
|
+
|
|
1284
|
+
total = db.collection.count()
|
|
1285
|
+
click.echo(f"\nIndex Statistics")
|
|
1286
|
+
click.echo(f"{'─' * 40}")
|
|
1287
|
+
click.echo(f"Total documents: {total}")
|
|
1288
|
+
|
|
1289
|
+
if total == 0:
|
|
1290
|
+
click.echo("\nIndex is empty. Run 'ragtime index' first.")
|
|
1291
|
+
return
|
|
1292
|
+
|
|
1293
|
+
# Get all documents for analysis
|
|
1294
|
+
all_docs = db.collection.get()
|
|
1295
|
+
|
|
1296
|
+
if by_namespace or (not by_namespace and not by_type):
|
|
1297
|
+
namespaces = {}
|
|
1298
|
+
for meta in all_docs["metadatas"]:
|
|
1299
|
+
ns = meta.get("namespace", "unknown")
|
|
1300
|
+
namespaces[ns] = namespaces.get(ns, 0) + 1
|
|
1301
|
+
|
|
1302
|
+
click.echo(f"\nBy Namespace:")
|
|
1303
|
+
for ns, count in sorted(namespaces.items(), key=lambda x: -x[1]):
|
|
1304
|
+
pct = count / total * 100
|
|
1305
|
+
click.echo(f" {ns}: {count} ({pct:.1f}%)")
|
|
1306
|
+
|
|
1307
|
+
if by_type or (not by_namespace and not by_type):
|
|
1308
|
+
types = {}
|
|
1309
|
+
for meta in all_docs["metadatas"]:
|
|
1310
|
+
t = meta.get("type", "unknown")
|
|
1311
|
+
types[t] = types.get(t, 0) + 1
|
|
1312
|
+
|
|
1313
|
+
click.echo(f"\nBy Type:")
|
|
1314
|
+
for t, count in sorted(types.items(), key=lambda x: -x[1]):
|
|
1315
|
+
pct = count / total * 100
|
|
1316
|
+
click.echo(f" {t}: {count} ({pct:.1f}%)")
|
|
1317
|
+
|
|
1318
|
+
|
|
1319
|
+
@debug.command("verify")
|
|
1320
|
+
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
1321
|
+
def debug_verify(path: Path):
|
|
1322
|
+
"""Verify index integrity with test queries."""
|
|
1323
|
+
path = Path(path).resolve()
|
|
1324
|
+
db = get_db(path)
|
|
1325
|
+
|
|
1326
|
+
total = db.collection.count()
|
|
1327
|
+
if total == 0:
|
|
1328
|
+
click.echo("✗ Index is empty. Run 'ragtime index' first.")
|
|
1329
|
+
return
|
|
1330
|
+
|
|
1331
|
+
click.echo(f"\nVerifying index ({total} documents)...")
|
|
1332
|
+
click.echo(f"{'─' * 40}")
|
|
1333
|
+
|
|
1334
|
+
issues = []
|
|
1335
|
+
|
|
1336
|
+
# Test 1: Basic search works
|
|
1337
|
+
click.echo("\n1. Testing basic search...")
|
|
1338
|
+
try:
|
|
1339
|
+
results = db.search("test", limit=1)
|
|
1340
|
+
if results:
|
|
1341
|
+
click.echo(" ✓ Search returns results")
|
|
1342
|
+
else:
|
|
1343
|
+
click.echo(" ⚠ Search returned no results (might be ok if no relevant docs)")
|
|
1344
|
+
except Exception as e:
|
|
1345
|
+
click.echo(f" ✗ Search failed: {e}")
|
|
1346
|
+
issues.append("Basic search failed")
|
|
1347
|
+
|
|
1348
|
+
# Test 2: Check for documents with missing metadata
|
|
1349
|
+
click.echo("\n2. Checking metadata integrity...")
|
|
1350
|
+
all_docs = db.collection.get()
|
|
1351
|
+
missing_namespace = 0
|
|
1352
|
+
missing_type = 0
|
|
1353
|
+
|
|
1354
|
+
for meta in all_docs["metadatas"]:
|
|
1355
|
+
if not meta.get("namespace"):
|
|
1356
|
+
missing_namespace += 1
|
|
1357
|
+
if not meta.get("type"):
|
|
1358
|
+
missing_type += 1
|
|
1359
|
+
|
|
1360
|
+
if missing_namespace:
|
|
1361
|
+
click.echo(f" ⚠ {missing_namespace} docs missing namespace")
|
|
1362
|
+
else:
|
|
1363
|
+
click.echo(" ✓ All docs have namespace")
|
|
1364
|
+
|
|
1365
|
+
if missing_type:
|
|
1366
|
+
click.echo(f" ⚠ {missing_type} docs missing type")
|
|
1367
|
+
else:
|
|
1368
|
+
click.echo(" ✓ All docs have type")
|
|
1369
|
+
|
|
1370
|
+
# Test 3: Check for duplicate IDs
|
|
1371
|
+
click.echo("\n3. Checking for duplicates...")
|
|
1372
|
+
ids = all_docs["ids"]
|
|
1373
|
+
unique_ids = set(ids)
|
|
1374
|
+
if len(ids) != len(unique_ids):
|
|
1375
|
+
dup_count = len(ids) - len(unique_ids)
|
|
1376
|
+
click.echo(f" ✗ {dup_count} duplicate IDs found")
|
|
1377
|
+
issues.append("Duplicate IDs")
|
|
1378
|
+
else:
|
|
1379
|
+
click.echo(" ✓ No duplicate IDs")
|
|
1380
|
+
|
|
1381
|
+
# Test 4: Similarity sanity check
|
|
1382
|
+
click.echo("\n4. Testing similarity consistency...")
|
|
1383
|
+
if total >= 2:
|
|
1384
|
+
# Pick first doc and find similar
|
|
1385
|
+
first_content = all_docs["documents"][0]
|
|
1386
|
+
results = db.search(first_content[:500], limit=2)
|
|
1387
|
+
if results and len(results) >= 1:
|
|
1388
|
+
top_similarity = 1 - results[0]["distance"]
|
|
1389
|
+
if top_similarity > 0.9:
|
|
1390
|
+
click.echo(f" ✓ Self-similarity check passed ({top_similarity:.2f})")
|
|
1391
|
+
else:
|
|
1392
|
+
click.echo(f" ⚠ Self-similarity lower than expected ({top_similarity:.2f})")
|
|
1393
|
+
else:
|
|
1394
|
+
click.echo(" ⚠ Could not perform similarity check")
|
|
1395
|
+
else:
|
|
1396
|
+
click.echo(" - Skipped (need at least 2 docs)")
|
|
1397
|
+
|
|
1398
|
+
# Summary
|
|
1399
|
+
click.echo(f"\n{'─' * 40}")
|
|
1400
|
+
if issues:
|
|
1401
|
+
click.echo(f"⚠ Found {len(issues)} issues:")
|
|
1402
|
+
for issue in issues:
|
|
1403
|
+
click.echo(f" - {issue}")
|
|
1404
|
+
else:
|
|
1405
|
+
click.echo("✓ Index verification passed")
|
|
1406
|
+
|
|
1407
|
+
|
|
1408
|
+
# ============================================================================
|
|
1409
|
+
# Documentation Generation
|
|
1410
|
+
# ============================================================================
|
|
1411
|
+
|
|
1412
|
+
|
|
1413
|
+
@main.command()
|
|
1414
|
+
@click.argument("code_path", type=click.Path(exists=True, path_type=Path))
|
|
1415
|
+
@click.option("--output", "-o", type=click.Path(path_type=Path), default="docs/api",
|
|
1416
|
+
help="Output directory for docs")
|
|
1417
|
+
@click.option("--stubs", is_flag=True, help="Generate stub docs with TODOs (no AI)")
|
|
1418
|
+
@click.option("--language", "-l", multiple=True,
|
|
1419
|
+
help="Languages to document (python, typescript, javascript)")
|
|
1420
|
+
@click.option("--include-private", is_flag=True, help="Include private methods (_name)")
|
|
1421
|
+
def generate(code_path: Path, output: Path, stubs: bool, language: tuple, include_private: bool):
|
|
1422
|
+
"""Generate documentation from code.
|
|
1423
|
+
|
|
1424
|
+
Creates markdown documentation from code structure.
|
|
1425
|
+
|
|
1426
|
+
Examples:
|
|
1427
|
+
ragtime generate src/ --stubs # Create stub docs
|
|
1428
|
+
ragtime generate src/ -o docs/api # Specify output
|
|
1429
|
+
ragtime generate src/ -l python # Python only
|
|
1430
|
+
"""
|
|
1431
|
+
import ast
|
|
1432
|
+
import re as re_module
|
|
1433
|
+
|
|
1434
|
+
code_path = Path(code_path).resolve()
|
|
1435
|
+
output = Path(output)
|
|
1436
|
+
|
|
1437
|
+
if not stubs:
|
|
1438
|
+
click.echo("Use --stubs for stub generation, or /generate-docs for AI-powered docs")
|
|
1439
|
+
click.echo("\nExample: ragtime generate src/ --stubs")
|
|
1440
|
+
return
|
|
1441
|
+
|
|
1442
|
+
# Determine languages
|
|
1443
|
+
if language:
|
|
1444
|
+
languages = list(language)
|
|
1445
|
+
else:
|
|
1446
|
+
languages = ["python", "typescript", "javascript"]
|
|
1447
|
+
|
|
1448
|
+
# Map extensions to languages
|
|
1449
|
+
ext_map = {
|
|
1450
|
+
"python": [".py"],
|
|
1451
|
+
"typescript": [".ts", ".tsx"],
|
|
1452
|
+
"javascript": [".js", ".jsx"],
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
extensions = []
|
|
1456
|
+
for lang in languages:
|
|
1457
|
+
extensions.extend(ext_map.get(lang, []))
|
|
1458
|
+
|
|
1459
|
+
# Find code files
|
|
1460
|
+
code_files = []
|
|
1461
|
+
for ext in extensions:
|
|
1462
|
+
code_files.extend(code_path.rglob(f"*{ext}"))
|
|
1463
|
+
|
|
1464
|
+
# Filter out common exclusions
|
|
1465
|
+
exclude_patterns = ["__pycache__", "node_modules", ".venv", "venv", "dist", "build"]
|
|
1466
|
+
code_files = [
|
|
1467
|
+
f for f in code_files
|
|
1468
|
+
if not any(ex in str(f) for ex in exclude_patterns)
|
|
1469
|
+
]
|
|
1470
|
+
|
|
1471
|
+
if not code_files:
|
|
1472
|
+
click.echo(f"No code files found in {code_path}")
|
|
1473
|
+
return
|
|
1474
|
+
|
|
1475
|
+
click.echo(f"Found {len(code_files)} code files")
|
|
1476
|
+
click.echo(f"Output: {output}/")
|
|
1477
|
+
click.echo(f"{'─' * 50}")
|
|
1478
|
+
|
|
1479
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
1480
|
+
generated = 0
|
|
1481
|
+
|
|
1482
|
+
for code_file in code_files:
|
|
1483
|
+
try:
|
|
1484
|
+
content = code_file.read_text()
|
|
1485
|
+
except Exception:
|
|
1486
|
+
continue
|
|
1487
|
+
|
|
1488
|
+
relative = code_file.relative_to(code_path)
|
|
1489
|
+
doc_path = output / relative.with_suffix(".md")
|
|
1490
|
+
|
|
1491
|
+
# Parse based on extension
|
|
1492
|
+
if code_file.suffix == ".py":
|
|
1493
|
+
doc_content = generate_python_stub(code_file, content, include_private)
|
|
1494
|
+
elif code_file.suffix in [".ts", ".tsx", ".js", ".jsx"]:
|
|
1495
|
+
doc_content = generate_typescript_stub(code_file, content, include_private)
|
|
1496
|
+
else:
|
|
1497
|
+
continue
|
|
1498
|
+
|
|
1499
|
+
if doc_content:
|
|
1500
|
+
doc_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1501
|
+
doc_path.write_text(doc_content)
|
|
1502
|
+
try:
|
|
1503
|
+
doc_display = doc_path.relative_to(Path.cwd())
|
|
1504
|
+
except ValueError:
|
|
1505
|
+
doc_display = doc_path
|
|
1506
|
+
click.echo(f" ✓ {relative} → {doc_display}")
|
|
1507
|
+
generated += 1
|
|
1508
|
+
|
|
1509
|
+
click.echo(f"\n{'─' * 50}")
|
|
1510
|
+
click.echo(f"✓ Generated {generated} documentation stubs")
|
|
1511
|
+
click.echo(f"\nNext steps:")
|
|
1512
|
+
click.echo(f" 1. Fill in the TODO placeholders")
|
|
1513
|
+
click.echo(f" 2. Or use /generate-docs for AI-generated content")
|
|
1514
|
+
click.echo(f" 3. Run 'ragtime index' to make searchable")
|
|
1515
|
+
|
|
1516
|
+
|
|
1517
|
+
def generate_python_stub(file_path: Path, content: str, include_private: bool) -> str:
|
|
1518
|
+
"""Generate markdown stub from Python code."""
|
|
1519
|
+
import ast
|
|
1520
|
+
|
|
1521
|
+
try:
|
|
1522
|
+
tree = ast.parse(content)
|
|
1523
|
+
except SyntaxError:
|
|
1524
|
+
return ""
|
|
1525
|
+
|
|
1526
|
+
lines = []
|
|
1527
|
+
lines.append(f"# {file_path.stem}")
|
|
1528
|
+
lines.append(f"\n> **File:** `{file_path}`")
|
|
1529
|
+
lines.append("\n## Overview\n")
|
|
1530
|
+
lines.append("TODO: Describe what this module does.\n")
|
|
1531
|
+
|
|
1532
|
+
# Get module docstring
|
|
1533
|
+
if ast.get_docstring(tree):
|
|
1534
|
+
lines.append(f"> {ast.get_docstring(tree)}\n")
|
|
1535
|
+
|
|
1536
|
+
classes = []
|
|
1537
|
+
functions = []
|
|
1538
|
+
|
|
1539
|
+
for node in ast.iter_child_nodes(tree):
|
|
1540
|
+
if isinstance(node, ast.ClassDef):
|
|
1541
|
+
if not include_private and node.name.startswith("_"):
|
|
1542
|
+
continue
|
|
1543
|
+
classes.append(node)
|
|
1544
|
+
elif isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
|
|
1545
|
+
if not include_private and node.name.startswith("_"):
|
|
1546
|
+
continue
|
|
1547
|
+
functions.append(node)
|
|
1548
|
+
|
|
1549
|
+
# Document classes
|
|
1550
|
+
if classes:
|
|
1551
|
+
lines.append("---\n")
|
|
1552
|
+
lines.append("## Classes\n")
|
|
1553
|
+
|
|
1554
|
+
for cls in classes:
|
|
1555
|
+
lines.append(f"### `{cls.name}`\n")
|
|
1556
|
+
if ast.get_docstring(cls):
|
|
1557
|
+
lines.append(f"{ast.get_docstring(cls)}\n")
|
|
1558
|
+
else:
|
|
1559
|
+
lines.append("TODO: Describe this class.\n")
|
|
1560
|
+
|
|
1561
|
+
# Find __init__ and methods
|
|
1562
|
+
methods = []
|
|
1563
|
+
init_node = None
|
|
1564
|
+
for item in cls.body:
|
|
1565
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
1566
|
+
if item.name == "__init__":
|
|
1567
|
+
init_node = item
|
|
1568
|
+
elif not item.name.startswith("_") or include_private:
|
|
1569
|
+
methods.append(item)
|
|
1570
|
+
|
|
1571
|
+
# Constructor
|
|
1572
|
+
if init_node:
|
|
1573
|
+
lines.append("#### Constructor\n")
|
|
1574
|
+
sig = get_function_signature(init_node)
|
|
1575
|
+
lines.append(f"```python\n{sig}\n```\n")
|
|
1576
|
+
params = get_function_params(init_node)
|
|
1577
|
+
if params:
|
|
1578
|
+
lines.append("| Parameter | Type | Default | Description |")
|
|
1579
|
+
lines.append("|-----------|------|---------|-------------|")
|
|
1580
|
+
for p in params:
|
|
1581
|
+
lines.append(f"| `{p['name']}` | `{p['type']}` | {p['default']} | TODO |")
|
|
1582
|
+
lines.append("")
|
|
1583
|
+
|
|
1584
|
+
# Methods
|
|
1585
|
+
if methods:
|
|
1586
|
+
lines.append("#### Methods\n")
|
|
1587
|
+
for method in methods:
|
|
1588
|
+
async_prefix = "async " if isinstance(method, ast.AsyncFunctionDef) else ""
|
|
1589
|
+
ret = get_return_annotation(method)
|
|
1590
|
+
lines.append(f"##### `{async_prefix}{method.name}(...) -> {ret}`\n")
|
|
1591
|
+
if ast.get_docstring(method):
|
|
1592
|
+
lines.append(f"{ast.get_docstring(method)}\n")
|
|
1593
|
+
else:
|
|
1594
|
+
lines.append("TODO: Describe this method.\n")
|
|
1595
|
+
|
|
1596
|
+
# Document functions
|
|
1597
|
+
if functions:
|
|
1598
|
+
lines.append("---\n")
|
|
1599
|
+
lines.append("## Functions\n")
|
|
1600
|
+
|
|
1601
|
+
for func in functions:
|
|
1602
|
+
async_prefix = "async " if isinstance(func, ast.AsyncFunctionDef) else ""
|
|
1603
|
+
ret = get_return_annotation(func)
|
|
1604
|
+
lines.append(f"### `{async_prefix}{func.name}(...) -> {ret}`\n")
|
|
1605
|
+
if ast.get_docstring(func):
|
|
1606
|
+
lines.append(f"{ast.get_docstring(func)}\n")
|
|
1607
|
+
else:
|
|
1608
|
+
lines.append("TODO: Describe this function.\n")
|
|
1609
|
+
|
|
1610
|
+
params = get_function_params(func)
|
|
1611
|
+
if params:
|
|
1612
|
+
lines.append("**Parameters:**\n")
|
|
1613
|
+
for p in params:
|
|
1614
|
+
lines.append(f"- `{p['name']}` (`{p['type']}`): TODO")
|
|
1615
|
+
lines.append("")
|
|
1616
|
+
|
|
1617
|
+
lines.append(f"**Returns:** `{ret}` - TODO\n")
|
|
1618
|
+
|
|
1619
|
+
return "\n".join(lines)
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
def get_function_signature(node) -> str:
|
|
1623
|
+
"""Get function signature string."""
|
|
1624
|
+
import ast
|
|
1625
|
+
|
|
1626
|
+
args = []
|
|
1627
|
+
for arg in node.args.args:
|
|
1628
|
+
if arg.arg == "self":
|
|
1629
|
+
continue
|
|
1630
|
+
type_hint = ""
|
|
1631
|
+
if arg.annotation:
|
|
1632
|
+
type_hint = f": {ast.unparse(arg.annotation)}"
|
|
1633
|
+
args.append(f"{arg.arg}{type_hint}")
|
|
1634
|
+
|
|
1635
|
+
return f"def {node.name}({', '.join(args)})"
|
|
1636
|
+
|
|
1637
|
+
|
|
1638
|
+
def get_function_params(node) -> list:
|
|
1639
|
+
"""Get function parameters with types and defaults."""
|
|
1640
|
+
import ast
|
|
1641
|
+
|
|
1642
|
+
params = []
|
|
1643
|
+
defaults = node.args.defaults
|
|
1644
|
+
num_defaults = len(defaults)
|
|
1645
|
+
num_args = len(node.args.args)
|
|
1646
|
+
|
|
1647
|
+
for i, arg in enumerate(node.args.args):
|
|
1648
|
+
if arg.arg in ("self", "cls"):
|
|
1649
|
+
continue
|
|
1650
|
+
|
|
1651
|
+
type_hint = "Any"
|
|
1652
|
+
if arg.annotation:
|
|
1653
|
+
try:
|
|
1654
|
+
type_hint = ast.unparse(arg.annotation)
|
|
1655
|
+
except:
|
|
1656
|
+
type_hint = "Any"
|
|
1657
|
+
|
|
1658
|
+
default = "-"
|
|
1659
|
+
default_idx = i - (num_args - num_defaults)
|
|
1660
|
+
if default_idx >= 0 and default_idx < len(defaults):
|
|
1661
|
+
try:
|
|
1662
|
+
default = f"`{ast.unparse(defaults[default_idx])}`"
|
|
1663
|
+
except:
|
|
1664
|
+
default = "..."
|
|
1665
|
+
|
|
1666
|
+
params.append({
|
|
1667
|
+
"name": arg.arg,
|
|
1668
|
+
"type": type_hint,
|
|
1669
|
+
"default": default,
|
|
1670
|
+
})
|
|
1671
|
+
|
|
1672
|
+
return params
|
|
1673
|
+
|
|
1674
|
+
|
|
1675
|
+
def get_return_annotation(node) -> str:
|
|
1676
|
+
"""Get return type annotation."""
|
|
1677
|
+
import ast
|
|
1678
|
+
|
|
1679
|
+
if node.returns:
|
|
1680
|
+
try:
|
|
1681
|
+
return ast.unparse(node.returns)
|
|
1682
|
+
except:
|
|
1683
|
+
return "Any"
|
|
1684
|
+
return "None"
|
|
1685
|
+
|
|
1686
|
+
|
|
1687
|
+
def generate_typescript_stub(file_path: Path, content: str, include_private: bool) -> str:
|
|
1688
|
+
"""Generate markdown stub from TypeScript/JavaScript code."""
|
|
1689
|
+
import re as re_module
|
|
1690
|
+
|
|
1691
|
+
lines = []
|
|
1692
|
+
lines.append(f"# {file_path.stem}")
|
|
1693
|
+
lines.append(f"\n> **File:** `{file_path}`")
|
|
1694
|
+
lines.append("\n## Overview\n")
|
|
1695
|
+
lines.append("TODO: Describe what this module does.\n")
|
|
1696
|
+
|
|
1697
|
+
# Find exports using regex
|
|
1698
|
+
class_pattern = r'export\s+(?:default\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?'
|
|
1699
|
+
func_pattern = r'export\s+(?:default\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*([^\{]+))?'
|
|
1700
|
+
const_pattern = r'export\s+const\s+(\w+)\s*(?::\s*([^=]+))?\s*='
|
|
1701
|
+
interface_pattern = r'export\s+(?:default\s+)?interface\s+(\w+)'
|
|
1702
|
+
type_pattern = r'export\s+type\s+(\w+)'
|
|
1703
|
+
|
|
1704
|
+
classes = re_module.findall(class_pattern, content)
|
|
1705
|
+
functions = re_module.findall(func_pattern, content)
|
|
1706
|
+
consts = re_module.findall(const_pattern, content)
|
|
1707
|
+
interfaces = re_module.findall(interface_pattern, content)
|
|
1708
|
+
types = re_module.findall(type_pattern, content)
|
|
1709
|
+
|
|
1710
|
+
# Interfaces and Types
|
|
1711
|
+
if interfaces or types:
|
|
1712
|
+
lines.append("---\n")
|
|
1713
|
+
lines.append("## Types\n")
|
|
1714
|
+
for iface in interfaces:
|
|
1715
|
+
lines.append(f"### `interface {iface}`\n")
|
|
1716
|
+
lines.append("TODO: Describe this interface.\n")
|
|
1717
|
+
for t in types:
|
|
1718
|
+
lines.append(f"### `type {t}`\n")
|
|
1719
|
+
lines.append("TODO: Describe this type.\n")
|
|
1720
|
+
|
|
1721
|
+
# Classes
|
|
1722
|
+
if classes:
|
|
1723
|
+
lines.append("---\n")
|
|
1724
|
+
lines.append("## Classes\n")
|
|
1725
|
+
for cls_name, extends in classes:
|
|
1726
|
+
lines.append(f"### `{cls_name}`")
|
|
1727
|
+
if extends:
|
|
1728
|
+
lines.append(f" extends `{extends}`")
|
|
1729
|
+
lines.append("\n")
|
|
1730
|
+
lines.append("TODO: Describe this class.\n")
|
|
1731
|
+
|
|
1732
|
+
# Functions
|
|
1733
|
+
if functions:
|
|
1734
|
+
lines.append("---\n")
|
|
1735
|
+
lines.append("## Functions\n")
|
|
1736
|
+
for func_name, params, return_type in functions:
|
|
1737
|
+
ret = return_type.strip() if return_type else "void"
|
|
1738
|
+
lines.append(f"### `{func_name}({params}) => {ret}`\n")
|
|
1739
|
+
lines.append("TODO: Describe this function.\n")
|
|
1740
|
+
|
|
1741
|
+
# Constants
|
|
1742
|
+
if consts:
|
|
1743
|
+
lines.append("---\n")
|
|
1744
|
+
lines.append("## Constants\n")
|
|
1745
|
+
lines.append("| Name | Type | Description |")
|
|
1746
|
+
lines.append("|------|------|-------------|")
|
|
1747
|
+
for const_name, const_type in consts:
|
|
1748
|
+
t = const_type.strip() if const_type else "unknown"
|
|
1749
|
+
lines.append(f"| `{const_name}` | `{t}` | TODO |")
|
|
1750
|
+
lines.append("")
|
|
1751
|
+
|
|
1752
|
+
if len(lines) <= 5: # Only header
|
|
1753
|
+
return ""
|
|
1754
|
+
|
|
1755
|
+
return "\n".join(lines)
|
|
1756
|
+
|
|
1757
|
+
|
|
1108
1758
|
@main.command()
|
|
1109
1759
|
@click.argument("docs_path", type=click.Path(exists=True, path_type=Path), default="docs")
|
|
1110
1760
|
@click.option("--path", type=click.Path(exists=True, path_type=Path), default=".")
|
|
@@ -1300,7 +1950,7 @@ def update(check: bool):
|
|
|
1300
1950
|
from urllib.request import urlopen
|
|
1301
1951
|
from urllib.error import URLError
|
|
1302
1952
|
|
|
1303
|
-
current = "0.2.
|
|
1953
|
+
current = "0.2.4"
|
|
1304
1954
|
|
|
1305
1955
|
click.echo(f"Current version: {current}")
|
|
1306
1956
|
click.echo("Checking PyPI for updates...")
|