sourcecode 1.36.0__tar.gz → 1.36.2__tar.gz
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.
- {sourcecode-1.36.0 → sourcecode-1.36.2}/PKG-INFO +4 -5
- {sourcecode-1.36.0 → sourcecode-1.36.2}/README.md +3 -4
- {sourcecode-1.36.0 → sourcecode-1.36.2}/pyproject.toml +1 -1
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/cache.py +37 -11
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/cli.py +100 -38
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/license.py +2 -208
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/telemetry/filters.py +1 -1
- sourcecode-1.36.2/src/sourcecode/version_check.py +149 -0
- sourcecode-1.36.2/supabase/.temp/cli-latest +1 -0
- sourcecode-1.36.2/supabase/functions/lemonsqueezy-webhook/index.ts +220 -0
- sourcecode-1.36.2/supabase/sql/license_event_ordering.sql +93 -0
- sourcecode-1.36.0/supabase/functions/lemonsqueezy-webhook/index.ts +0 -163
- {sourcecode-1.36.0 → sourcecode-1.36.2}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/.gitignore +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/.ruff.toml +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/CHANGELOG.md +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/CONTRIBUTING.md +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/LICENSE +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/SECURITY.md +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/raw +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/supabase/functions/README.md +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.36.0 → sourcecode-1.36.2}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.36.
|
|
3
|
+
Version: 1.36.2
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
42
42
|
|
|
43
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -114,7 +114,7 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.36.
|
|
117
|
+
# sourcecode 1.36.1
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
---
|
|
@@ -311,8 +311,7 @@ when the work gets bigger or automated.
|
|
|
311
311
|
files only, by design. sourcecode monetises enterprise Java monoliths.
|
|
312
312
|
|
|
313
313
|
```bash
|
|
314
|
-
sourcecode
|
|
315
|
-
sourcecode activate <key> # or activate a license key directly
|
|
314
|
+
sourcecode activate <key> # activate a license key
|
|
316
315
|
```
|
|
317
316
|
|
|
318
317
|
Full breakdown: [docs/PRODUCT_TIERS.md](docs/PRODUCT_TIERS.md).
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -76,7 +76,7 @@ pipx install sourcecode
|
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
sourcecode version
|
|
79
|
-
# sourcecode 1.36.
|
|
79
|
+
# sourcecode 1.36.1
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
---
|
|
@@ -273,8 +273,7 @@ when the work gets bigger or automated.
|
|
|
273
273
|
files only, by design. sourcecode monetises enterprise Java monoliths.
|
|
274
274
|
|
|
275
275
|
```bash
|
|
276
|
-
sourcecode
|
|
277
|
-
sourcecode activate <key> # or activate a license key directly
|
|
276
|
+
sourcecode activate <key> # activate a license key
|
|
278
277
|
```
|
|
279
278
|
|
|
280
279
|
Full breakdown: [docs/PRODUCT_TIERS.md](docs/PRODUCT_TIERS.md).
|
|
@@ -58,6 +58,8 @@ import json
|
|
|
58
58
|
import os
|
|
59
59
|
import re
|
|
60
60
|
import subprocess
|
|
61
|
+
import time
|
|
62
|
+
import uuid
|
|
61
63
|
from datetime import datetime, timezone
|
|
62
64
|
from pathlib import Path
|
|
63
65
|
from typing import Any, Optional
|
|
@@ -101,6 +103,12 @@ _DEFAULT_KEEP_COMMITS: int = 5
|
|
|
101
103
|
_DEFAULT_MAX_CORES: int = 20
|
|
102
104
|
_DEFAULT_MAX_SIZE_MB: int = 50
|
|
103
105
|
|
|
106
|
+
#: Windows hardening for _atomic_write: os.replace can raise PermissionError
|
|
107
|
+
#: (WinError 5/32) when an antivirus scanner, search indexer, or concurrent
|
|
108
|
+
#: reader transiently holds the destination open. Retry briefly to ride it out.
|
|
109
|
+
_REPLACE_RETRIES: int = 5
|
|
110
|
+
_REPLACE_BACKOFF_S: float = 0.05
|
|
111
|
+
|
|
104
112
|
# Matches "snapshot-<hex_commit>-<hex_flags>.json.gz"
|
|
105
113
|
_SNAPSHOT_RE = re.compile(r"^snapshot-([0-9a-f]+)-[0-9a-f]+\.json\.gz$")
|
|
106
114
|
|
|
@@ -808,20 +816,38 @@ def _gc_cas(cache_d: Path, surviving_snapshots: list[Path]) -> None:
|
|
|
808
816
|
# ---------------------------------------------------------------------------
|
|
809
817
|
|
|
810
818
|
def _atomic_write(dest: Path, data: bytes) -> None:
|
|
811
|
-
"""Write *data* to *dest* atomically via a
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
destination either
|
|
815
|
-
write. The
|
|
816
|
-
by the cache reader and GC.
|
|
819
|
+
"""Write *data* to *dest* atomically via a unique temp file + rename.
|
|
820
|
+
|
|
821
|
+
``os.replace`` is an atomic overwrite on both POSIX and Windows: the
|
|
822
|
+
destination ends up with either the old or the new content, never a partial
|
|
823
|
+
write. The trailing ``.tmp`` suffix keeps partial files out of the
|
|
824
|
+
``*.json.gz`` / ``*.gz`` glob patterns used by the cache reader and GC.
|
|
825
|
+
|
|
826
|
+
Windows hardening (no-ops on POSIX):
|
|
827
|
+
* The temp name is made *unique* (pid + random token) so concurrent
|
|
828
|
+
writers of the same destination never collide on a shared temp file —
|
|
829
|
+
on Windows that collision raises ``PermissionError`` (WinError 32,
|
|
830
|
+
sharing violation), whereas POSIX tolerates it.
|
|
831
|
+
* ``os.replace`` is retried with a short backoff: on Windows it raises
|
|
832
|
+
``PermissionError`` (WinError 5/32) when an antivirus scanner, the
|
|
833
|
+
search indexer, or a concurrent reader holds a transient handle on the
|
|
834
|
+
destination. POSIX ``rename(2)`` has no such restriction.
|
|
817
835
|
"""
|
|
818
|
-
tmp = dest.
|
|
836
|
+
tmp = dest.parent / f"{dest.name}.{os.getpid()}.{uuid.uuid4().hex[:8]}.tmp"
|
|
819
837
|
try:
|
|
820
838
|
tmp.write_bytes(data)
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
839
|
+
last_exc: Optional[BaseException] = None
|
|
840
|
+
for attempt in range(_REPLACE_RETRIES):
|
|
841
|
+
try:
|
|
842
|
+
os.replace(tmp, dest)
|
|
843
|
+
return
|
|
844
|
+
except PermissionError as exc: # transient lock on Windows — retry
|
|
845
|
+
last_exc = exc
|
|
846
|
+
time.sleep(_REPLACE_BACKOFF_S * (attempt + 1))
|
|
847
|
+
if last_exc is not None:
|
|
848
|
+
raise last_exc
|
|
849
|
+
finally:
|
|
850
|
+
_safe_unlink(tmp) # remove temp on failure; no-op after a successful replace
|
|
825
851
|
|
|
826
852
|
|
|
827
853
|
def _safe_unlink(path: Path) -> None:
|
|
@@ -168,7 +168,6 @@ Cold scan: 2–10s depending on repo size. Warm cache: 0.3–0.6s.
|
|
|
168
168
|
sourcecode --agent full structured JSON for AI agents
|
|
169
169
|
|
|
170
170
|
[bold]Auth commands:[/bold]
|
|
171
|
-
auth login [dim]# authenticate via browser (device code)[/dim]
|
|
172
171
|
auth status [dim]# show current plan and auth state[/dim]
|
|
173
172
|
auth logout [dim]# remove local credentials[/dim]
|
|
174
173
|
|
|
@@ -570,7 +569,7 @@ app.add_typer(mcp_app, name="mcp")
|
|
|
570
569
|
cache_app = typer.Typer(help="Cache inspection and management.", rich_markup_mode="rich")
|
|
571
570
|
app.add_typer(cache_app, name="cache")
|
|
572
571
|
|
|
573
|
-
auth_app = typer.Typer(help="Authentication:
|
|
572
|
+
auth_app = typer.Typer(help="Authentication: status, logout.", rich_markup_mode="rich")
|
|
574
573
|
app.add_typer(auth_app, name="auth")
|
|
575
574
|
|
|
576
575
|
|
|
@@ -653,6 +652,46 @@ def version_callback(value: bool) -> None:
|
|
|
653
652
|
raise typer.Exit()
|
|
654
653
|
|
|
655
654
|
|
|
655
|
+
# ANSI Shadow block wordmark, stacked "source" / "code" so it fits an 80-col
|
|
656
|
+
# terminal (the single-line "sourcecode" is 83 wide and would wrap).
|
|
657
|
+
_WELCOME_SOURCE = (
|
|
658
|
+
"███████╗ ██████╗ ██╗ ██╗██████╗ ██████╗███████╗",
|
|
659
|
+
"██╔════╝██╔═══██╗██║ ██║██╔══██╗██╔════╝██╔════╝",
|
|
660
|
+
"███████╗██║ ██║██║ ██║██████╔╝██║ █████╗ ",
|
|
661
|
+
"╚════██║██║ ██║██║ ██║██╔══██╗██║ ██╔══╝ ",
|
|
662
|
+
"███████║╚██████╔╝╚██████╔╝██║ ██║╚██████╗███████╗",
|
|
663
|
+
"╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚══════╝",
|
|
664
|
+
)
|
|
665
|
+
_WELCOME_CODE = (
|
|
666
|
+
" ██████╗ ██████╗ ██████╗ ███████╗",
|
|
667
|
+
"██╔════╝██╔═══██╗██╔══██╗██╔════╝",
|
|
668
|
+
"██║ ██║ ██║██║ ██║█████╗ ",
|
|
669
|
+
"██║ ██║ ██║██║ ██║██╔══╝ ",
|
|
670
|
+
"╚██████╗╚██████╔╝██████╔╝███████╗",
|
|
671
|
+
" ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝",
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
_WELCOME_CMDS = (
|
|
675
|
+
("sourcecode --compact", "repo summary"),
|
|
676
|
+
("sourcecode prepare-context onboard", "onboarding"),
|
|
677
|
+
("sourcecode mcp init", "connect IDE"),
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
def _print_welcome_plain(tier: str) -> None:
|
|
682
|
+
"""Plain-text welcome — fallback when rich is unavailable."""
|
|
683
|
+
lines = ["", f" sourcecode {__version__} · {tier}", "",
|
|
684
|
+
" AI coding-agent context, instant.", "", " Get started:"]
|
|
685
|
+
for cmd, desc in _WELCOME_CMDS:
|
|
686
|
+
lines.append(f" {cmd.ljust(34)}{desc}")
|
|
687
|
+
lines.append("")
|
|
688
|
+
lines.append(" sourcecode --help all commands")
|
|
689
|
+
if tier != "Pro":
|
|
690
|
+
lines.append(" sourcecode activate <key> unlock Pro")
|
|
691
|
+
lines.append("")
|
|
692
|
+
typer.echo("\n".join(lines))
|
|
693
|
+
|
|
694
|
+
|
|
656
695
|
def _print_welcome() -> None:
|
|
657
696
|
"""Branded quickstart shown only on a bare invocation at a human terminal.
|
|
658
697
|
|
|
@@ -665,24 +704,55 @@ def _print_welcome() -> None:
|
|
|
665
704
|
except Exception:
|
|
666
705
|
tier = "Free"
|
|
667
706
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
707
|
+
try:
|
|
708
|
+
from rich import box
|
|
709
|
+
from rich.console import Console
|
|
710
|
+
from rich.panel import Panel
|
|
711
|
+
from rich.text import Text
|
|
712
|
+
except Exception:
|
|
713
|
+
_print_welcome_plain(tier)
|
|
714
|
+
return
|
|
715
|
+
|
|
716
|
+
pad = max(len(c) for c, _ in _WELCOME_CMDS) + 2
|
|
717
|
+
tier_style = "green" if tier != "Pro" else "magenta"
|
|
718
|
+
code_w = max(len(s) for s in _WELCOME_CODE)
|
|
719
|
+
|
|
720
|
+
t = Text()
|
|
721
|
+
for ln in _WELCOME_SOURCE:
|
|
722
|
+
t.append(ln + "\n", style="bold cyan")
|
|
723
|
+
# Append version + tier to the right of the "code" block's middle rows.
|
|
724
|
+
for i, ln in enumerate(_WELCOME_CODE):
|
|
725
|
+
t.append(ln.ljust(code_w), style="bold cyan")
|
|
726
|
+
if i == 2:
|
|
727
|
+
t.append(f" {__version__}", style="dim")
|
|
728
|
+
elif i == 3:
|
|
729
|
+
t.append(" ")
|
|
730
|
+
t.append(tier, style=tier_style)
|
|
731
|
+
t.append("\n")
|
|
732
|
+
|
|
733
|
+
t.append("\nAI coding-agent context, instant.\n\n", style="white")
|
|
734
|
+
|
|
735
|
+
for cmd, desc in _WELCOME_CMDS:
|
|
736
|
+
t.append("▸ ", style="cyan")
|
|
737
|
+
t.append(cmd.ljust(pad), style="bold")
|
|
738
|
+
t.append(desc + "\n", style="dim")
|
|
739
|
+
|
|
740
|
+
t.append("\n--help", style="bold")
|
|
741
|
+
t.append(" · ", style="dim")
|
|
682
742
|
if tier != "Pro":
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
743
|
+
t.append("activate <key>", style="bold")
|
|
744
|
+
t.append(" for Pro", style="dim")
|
|
745
|
+
else:
|
|
746
|
+
t.append("you're on Pro ✓", style="green")
|
|
747
|
+
|
|
748
|
+
panel = Panel(
|
|
749
|
+
t,
|
|
750
|
+
box=box.ROUNDED,
|
|
751
|
+
border_style="cyan",
|
|
752
|
+
padding=(1, 3),
|
|
753
|
+
expand=False,
|
|
754
|
+
)
|
|
755
|
+
Console().print(panel)
|
|
686
756
|
|
|
687
757
|
|
|
688
758
|
@app.callback(invoke_without_command=True)
|
|
@@ -5344,26 +5414,9 @@ def activate_cmd(
|
|
|
5344
5414
|
|
|
5345
5415
|
|
|
5346
5416
|
# ---------------------------------------------------------------------------
|
|
5347
|
-
# Auth commands (
|
|
5417
|
+
# Auth commands (status / logout)
|
|
5348
5418
|
# ---------------------------------------------------------------------------
|
|
5349
5419
|
|
|
5350
|
-
@auth_app.command("login")
|
|
5351
|
-
def auth_login_cmd() -> None:
|
|
5352
|
-
"""Authenticate via browser (device code flow).
|
|
5353
|
-
|
|
5354
|
-
\b
|
|
5355
|
-
The CLI shows a URL. Open it in your browser, log in with your account,
|
|
5356
|
-
and the CLI completes authentication automatically.
|
|
5357
|
-
Credentials are stored in ~/.sourcecode/license.json (30-min cache; Supabase is source of truth).
|
|
5358
|
-
|
|
5359
|
-
\b
|
|
5360
|
-
Examples:
|
|
5361
|
-
sourcecode auth login
|
|
5362
|
-
"""
|
|
5363
|
-
from sourcecode.license import auth_login as _auth_login
|
|
5364
|
-
_auth_login()
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
5420
|
@auth_app.command("status")
|
|
5368
5421
|
def auth_status_cmd() -> None:
|
|
5369
5422
|
"""Show current authentication and plan status."""
|
|
@@ -6159,4 +6212,13 @@ def main_entry() -> None:
|
|
|
6159
6212
|
except Exception:
|
|
6160
6213
|
pass
|
|
6161
6214
|
_preprocess_argv()
|
|
6162
|
-
|
|
6215
|
+
try:
|
|
6216
|
+
app()
|
|
6217
|
+
finally:
|
|
6218
|
+
# Best-effort "new version available" nudge. Only speaks on an
|
|
6219
|
+
# interactive terminal; never blocks, raises, or affects exit status.
|
|
6220
|
+
try:
|
|
6221
|
+
from sourcecode.version_check import maybe_notify_update
|
|
6222
|
+
maybe_notify_update(__version__)
|
|
6223
|
+
except Exception:
|
|
6224
|
+
pass
|
|
@@ -71,9 +71,6 @@ _DELTA_FREE_LIMIT: int = 30
|
|
|
71
71
|
# Hybrid model size limit: repos at/under this many Java source files are fully
|
|
72
72
|
# free (every command, no caps). Above it = enterprise-scale monolith = Pro.
|
|
73
73
|
_FREE_REPO_JAVA_FILE_LIMIT: int = 500
|
|
74
|
-
_DEVICE_POLL_INTERVAL_S: float = 2.5
|
|
75
|
-
_DEVICE_POLL_TIMEOUT_S: float = 300.0 # 5-minute window for user to complete browser auth
|
|
76
|
-
_AUTH_BASE_URL: str = "https://sourcecode.dev"
|
|
77
74
|
_LICENSE_KEY_RE = re.compile(r"^[A-Za-z0-9_\-]{1,200}$")
|
|
78
75
|
|
|
79
76
|
# ---------------------------------------------------------------------------
|
|
@@ -243,78 +240,6 @@ def _call_get_license(license_key: str) -> Optional[dict]:
|
|
|
243
240
|
return None # Network error — caller decides what to do
|
|
244
241
|
|
|
245
242
|
|
|
246
|
-
def _generate_device_code() -> str:
|
|
247
|
-
"""Generate a human-readable device code: XXXX-XXXX-XXXX."""
|
|
248
|
-
import uuid
|
|
249
|
-
raw = uuid.uuid4().hex.upper()
|
|
250
|
-
return f"{raw[:4]}-{raw[4:8]}-{raw[8:12]}"
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def _call_device_check(device_code: str) -> Optional[dict]:
|
|
254
|
-
"""Poll /device-check edge function. Returns dict or None on network error.
|
|
255
|
-
|
|
256
|
-
Expected responses:
|
|
257
|
-
{"status": "pending"}
|
|
258
|
-
{"status": "complete", "device_token": "...", "email": "...", "plan": "pro", ...}
|
|
259
|
-
{"status": "error", "message": "..."}
|
|
260
|
-
"""
|
|
261
|
-
import urllib.error
|
|
262
|
-
import urllib.request
|
|
263
|
-
|
|
264
|
-
if not _SUPABASE_ANON_KEY:
|
|
265
|
-
return None
|
|
266
|
-
|
|
267
|
-
url = f"{_SUPABASE_URL}/functions/v1/device-check"
|
|
268
|
-
body = json.dumps({"device_code": device_code}).encode("utf-8")
|
|
269
|
-
req = urllib.request.Request(url, data=body, method="POST")
|
|
270
|
-
req.add_header("apikey", _SUPABASE_ANON_KEY)
|
|
271
|
-
req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
|
|
272
|
-
req.add_header("Content-Type", "application/json")
|
|
273
|
-
req.add_header("Accept", "application/json")
|
|
274
|
-
try:
|
|
275
|
-
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
276
|
-
return json.loads(resp.read().decode("utf-8"))
|
|
277
|
-
except urllib.error.HTTPError as exc:
|
|
278
|
-
try:
|
|
279
|
-
return json.loads(exc.read().decode("utf-8", errors="replace"))
|
|
280
|
-
except Exception:
|
|
281
|
-
return {"status": "error", "message": f"HTTP {exc.code}"}
|
|
282
|
-
except Exception:
|
|
283
|
-
return None
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
def _call_get_user_plan(device_token: str) -> Optional[dict]:
|
|
287
|
-
"""Fetch current plan/status for an authenticated device token.
|
|
288
|
-
|
|
289
|
-
Expected response:
|
|
290
|
-
{"valid": true, "plan": "pro", "status": "active", "features": [...], "email": "..."}
|
|
291
|
-
{"valid": false, "error": "token_revoked"}
|
|
292
|
-
"""
|
|
293
|
-
import urllib.error
|
|
294
|
-
import urllib.request
|
|
295
|
-
|
|
296
|
-
if not _SUPABASE_ANON_KEY:
|
|
297
|
-
return None
|
|
298
|
-
|
|
299
|
-
url = f"{_SUPABASE_URL}/functions/v1/get-user-plan"
|
|
300
|
-
body = json.dumps({"device_token": device_token}).encode("utf-8")
|
|
301
|
-
req = urllib.request.Request(url, data=body, method="POST")
|
|
302
|
-
req.add_header("apikey", _SUPABASE_ANON_KEY)
|
|
303
|
-
req.add_header("Authorization", f"Bearer {_SUPABASE_ANON_KEY}")
|
|
304
|
-
req.add_header("Content-Type", "application/json")
|
|
305
|
-
req.add_header("Accept", "application/json")
|
|
306
|
-
try:
|
|
307
|
-
with urllib.request.urlopen(req, timeout=8) as resp:
|
|
308
|
-
return json.loads(resp.read().decode("utf-8"))
|
|
309
|
-
except urllib.error.HTTPError as exc:
|
|
310
|
-
try:
|
|
311
|
-
return json.loads(exc.read().decode("utf-8", errors="replace"))
|
|
312
|
-
except Exception:
|
|
313
|
-
return {"valid": False, "error": f"HTTP {exc.code}"}
|
|
314
|
-
except Exception:
|
|
315
|
-
return None
|
|
316
|
-
|
|
317
|
-
|
|
318
243
|
def _maybe_revalidate() -> None:
|
|
319
244
|
"""Re-validate cached license if stale. Mutates globals; never raises."""
|
|
320
245
|
global _license_data, is_pro
|
|
@@ -338,39 +263,7 @@ def _maybe_revalidate() -> None:
|
|
|
338
263
|
except Exception:
|
|
339
264
|
pass
|
|
340
265
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if auth_method == "device_flow":
|
|
344
|
-
device_token = _license_data.get("device_token")
|
|
345
|
-
if not device_token:
|
|
346
|
-
return
|
|
347
|
-
result = _call_get_user_plan(device_token)
|
|
348
|
-
if result is None:
|
|
349
|
-
return # Network error — keep cached (offline-first)
|
|
350
|
-
if not result.get("valid", True):
|
|
351
|
-
_license_data = None
|
|
352
|
-
is_pro = False
|
|
353
|
-
try:
|
|
354
|
-
if _LICENSE_FILE.exists():
|
|
355
|
-
_LICENSE_FILE.unlink()
|
|
356
|
-
except Exception:
|
|
357
|
-
pass
|
|
358
|
-
return
|
|
359
|
-
_license_data["plan"] = result.get("plan", "free")
|
|
360
|
-
_license_data["status"] = result.get("status", "active")
|
|
361
|
-
_license_data["features"] = result.get("features", [])
|
|
362
|
-
_license_data["validated_at"] = datetime.now(timezone.utc).isoformat()
|
|
363
|
-
is_pro = (
|
|
364
|
-
_license_data.get("plan") == "pro"
|
|
365
|
-
and _license_data.get("status", "active") != "inactive"
|
|
366
|
-
)
|
|
367
|
-
try:
|
|
368
|
-
_write_license_file(_license_data)
|
|
369
|
-
except Exception:
|
|
370
|
-
pass
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
# Key-based auth (existing flow / legacy)
|
|
266
|
+
# Key-based auth
|
|
374
267
|
key = _license_data.get("license_key")
|
|
375
268
|
if not key:
|
|
376
269
|
return
|
|
@@ -585,106 +478,7 @@ def require_pro(feature_name: str) -> None:
|
|
|
585
478
|
|
|
586
479
|
|
|
587
480
|
# ---------------------------------------------------------------------------
|
|
588
|
-
#
|
|
589
|
-
# ---------------------------------------------------------------------------
|
|
590
|
-
|
|
591
|
-
def _finish_device_auth(result: dict) -> None:
|
|
592
|
-
"""Persist device-flow credentials and emit success JSON. Exits on error."""
|
|
593
|
-
global _license_data, is_pro
|
|
594
|
-
|
|
595
|
-
device_token = result.get("device_token") or result.get("access_token") or ""
|
|
596
|
-
email = result.get("email", "")
|
|
597
|
-
plan = result.get("plan", "free")
|
|
598
|
-
plan_status = (
|
|
599
|
-
result.get("status_detail")
|
|
600
|
-
or result.get("user_status")
|
|
601
|
-
or result.get("status", "active")
|
|
602
|
-
)
|
|
603
|
-
features = result.get("features") or []
|
|
604
|
-
|
|
605
|
-
if not device_token:
|
|
606
|
-
sys.stderr.write("\n")
|
|
607
|
-
_fail("auth_error", "Authentication completed but no session token received. Contact support.")
|
|
608
|
-
|
|
609
|
-
_LICENSE_DIR.mkdir(parents=True, exist_ok=True)
|
|
610
|
-
now = datetime.now(timezone.utc).isoformat()
|
|
611
|
-
data: dict = {
|
|
612
|
-
"auth_method": "device_flow",
|
|
613
|
-
"device_token": device_token,
|
|
614
|
-
"email": email,
|
|
615
|
-
"plan": plan,
|
|
616
|
-
"status": plan_status,
|
|
617
|
-
"features": features,
|
|
618
|
-
"authenticated_at": now,
|
|
619
|
-
"validated_at": now,
|
|
620
|
-
}
|
|
621
|
-
_write_license_file(data)
|
|
622
|
-
_license_data = data
|
|
623
|
-
is_pro = plan == "pro" and plan_status != "inactive"
|
|
624
|
-
_emit_telemetry("activation", feature="device_flow", success=is_pro)
|
|
625
|
-
|
|
626
|
-
sys.stderr.write(f"\n Authenticated as {email}. Plan: {plan}\n\n")
|
|
627
|
-
sys.stderr.flush()
|
|
628
|
-
|
|
629
|
-
output: dict = {"status": "authenticated", "email": email, "plan": plan, "pro": is_pro}
|
|
630
|
-
if not is_pro:
|
|
631
|
-
output["upgrade_hint"] = "https://sourcecode.dev/pricing"
|
|
632
|
-
else:
|
|
633
|
-
output["features"] = features
|
|
634
|
-
sys.stdout.write(json.dumps(output, ensure_ascii=False) + "\n")
|
|
635
|
-
sys.stdout.flush()
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
def auth_login() -> None:
|
|
639
|
-
"""Device code authentication flow.
|
|
640
|
-
|
|
641
|
-
Shows a browser URL; polls the backend every 2.5 s until the user
|
|
642
|
-
completes authentication or the 5-minute window expires.
|
|
643
|
-
Writes credentials to ~/.sourcecode/license.json on success.
|
|
644
|
-
Exits 0 on success, 1 on any failure.
|
|
645
|
-
"""
|
|
646
|
-
import time
|
|
647
|
-
|
|
648
|
-
device_code = _generate_device_code()
|
|
649
|
-
activate_url = f"{_AUTH_BASE_URL}/activate?code={device_code}"
|
|
650
|
-
|
|
651
|
-
sys.stderr.write(f"\n Open this URL to authenticate:\n {activate_url}\n\n Waiting")
|
|
652
|
-
sys.stderr.flush()
|
|
653
|
-
|
|
654
|
-
deadline = time.monotonic() + _DEVICE_POLL_TIMEOUT_S
|
|
655
|
-
_tick = 0
|
|
656
|
-
|
|
657
|
-
while time.monotonic() < deadline:
|
|
658
|
-
time.sleep(_DEVICE_POLL_INTERVAL_S)
|
|
659
|
-
_tick += 1
|
|
660
|
-
if _tick % 4 == 0:
|
|
661
|
-
sys.stderr.write(".")
|
|
662
|
-
sys.stderr.flush()
|
|
663
|
-
|
|
664
|
-
result = _call_device_check(device_code)
|
|
665
|
-
if result is None:
|
|
666
|
-
continue # network blip — keep polling
|
|
667
|
-
|
|
668
|
-
status = result.get("status")
|
|
669
|
-
if status == "pending":
|
|
670
|
-
continue
|
|
671
|
-
|
|
672
|
-
if status == "complete":
|
|
673
|
-
_finish_device_auth(result)
|
|
674
|
-
return
|
|
675
|
-
|
|
676
|
-
if status == "error" or result.get("error"):
|
|
677
|
-
sys.stderr.write("\n")
|
|
678
|
-
_fail("auth_error", result.get("message") or result.get("error") or "Authentication failed.")
|
|
679
|
-
|
|
680
|
-
# Unknown status — keep polling
|
|
681
|
-
|
|
682
|
-
sys.stderr.write("\n")
|
|
683
|
-
_fail("auth_timeout", "Authentication timed out after 5 minutes. Please try again.")
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
# ---------------------------------------------------------------------------
|
|
687
|
-
# Activation (key-based — legacy / direct key entry)
|
|
481
|
+
# Activation (key-based — direct key entry)
|
|
688
482
|
# ---------------------------------------------------------------------------
|
|
689
483
|
|
|
690
484
|
def activate_license(license_key: str) -> None:
|
|
@@ -75,7 +75,7 @@ _SAFE_FEATURES: frozenset[str] = frozenset({
|
|
|
75
75
|
# prepare-context task names not already above
|
|
76
76
|
"explain", "onboard", "refactor",
|
|
77
77
|
# activation outcomes
|
|
78
|
-
"key",
|
|
78
|
+
"key",
|
|
79
79
|
})
|
|
80
80
|
_SAFE_SIZES: frozenset[str] = frozenset({"tiny", "small", "medium", "large", "huge", "unknown"})
|
|
81
81
|
_SAFE_DURATIONS: frozenset[str] = frozenset({"<1s", "<5s", "<15s", "<60s", "60s+", "unknown"})
|