Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
22f99d9
Add local-env foundation: result types and env-key mapping
rugpanov Jul 3, 2026
22c2902
Fix PythonMinorFromRequires to pick the lower bound in multi-clause s…
rugpanov Jul 3, 2026
5b0e0cc
Reject requires-python with no lower bound instead of picking a forbi…
rugpanov Jul 3, 2026
7af0d48
Parse requires-python by clause and install the effective (highest) l…
rugpanov Jul 3, 2026
be61b7d
Treat a strict ">" python bound as excluding the whole minor series
rugpanov Jul 3, 2026
900518b
Address review: fix patch-qualified strict python bound; guarantee --…
rugpanov Jul 6, 2026
28469e1
Add local-env compute-target resolution
rugpanov Jul 3, 2026
42a7988
Validate mutually exclusive target flags inside ResolveTarget
rugpanov Jul 3, 2026
b621532
Add local-env constraint fetch with offline cache
rugpanov Jul 3, 2026
53ed3a1
Harden constraint fetch: validate artifact, create+atomically write c…
rugpanov Jul 3, 2026
4783518
Make the constraint cache filename collision-free
rugpanov Jul 3, 2026
f7e3e3f
Match databricks-connect case-insensitively
rugpanov Jul 3, 2026
6dcf041
Normalize databricks-connect dependency name per PEP 503
rugpanov Jul 3, 2026
0ef2220
Address review: bound the constraint fetch (timeout + size cap)
rugpanov Jul 6, 2026
4f83973
Address review: source the constraint repo from an env var, no person…
rugpanov Jul 6, 2026
dcaf3a6
Add local-env formatting-preserving pyproject.toml merge
rugpanov Jul 3, 2026
babbd62
Scope pyproject merge to the dev group and preserve inline comments
rugpanov Jul 3, 2026
6480f01
Handle inline comments on TOML table headers in the pyproject merge
rugpanov Jul 3, 2026
5f9cbb2
Recognize array-of-tables headers so [tool.uv] bounds stop at its chi…
rugpanov Jul 3, 2026
41d052a
Address review: refuse to merge multi-line-string / no-[project] pypr…
rugpanov Jul 6, 2026
892250f
Add local-env pipeline, detection, and package-manager interface
rugpanov Jul 3, 2026
f727199
Harden pipeline backup safety and version-major parsing
rugpanov Jul 3, 2026
f8bde02
Skip the writability probe under --check
rugpanov Jul 3, 2026
4c38027
Skip package-manager availability under --check too
rugpanov Jul 3, 2026
be1b6b3
Construct the pipeline Result via NewResult
rugpanov Jul 6, 2026
ac062fa
Merge branch 'main' into dbconnect/05-pipeline
rugpanov Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions libs/localenv/detect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package localenv

import (
"os"
"path/filepath"
)

// manager identifies the Python package manager a project uses. P0 only
// supports uv; every other value results in a clean E_MANAGER_UNSUPPORTED exit.
type manager string

const (
managerUv manager = "uv"
managerConda manager = "conda"
managerPip manager = "pip"
)

// detectManager inspects projectDir for package-manager markers, only deep
// enough to branch uv vs. not-uv (spec §5). It emits no telemetry (spec §5).
//
// Detection is deliberately biased toward uv, because uv's native project file
// is pyproject.toml (PEP 621) — the same format this command writes and merges:
// - A uv marker (uv.lock or a [tool.uv] table) → uv.
// - A pyproject.toml with no competing marker → uv (a plain PEP 621 project is
// exactly the "existing project merge" case; uv can drive it).
// - conda (environment.yml) or pip (requirements.txt) with no pyproject.toml →
// that manager; automated setup is P1, so the caller exits cleanly.
// - Greenfield (no markers at all) → uv, the manager this command provisions.
//
// A conda/pip marker that sits alongside a pyproject.toml still resolves to uv:
// the project already has the file we drive, so we proceed rather than block.
func detectManager(projectDir string) manager {
// uv markers take precedence: an existing uv project or lockfile.
if fileExists(filepath.Join(projectDir, "uv.lock")) {
return managerUv
}
if fileExists(filepath.Join(projectDir, pyprojectFile)) {
// A pyproject.toml — with or without a [tool.uv] table — is uv-drivable.
return managerUv
}

// No pyproject.toml: a conda or pip marker means a non-uv project we cannot
// yet automate. conda before pip (environment.yml is the more specific signal).
if fileExists(filepath.Join(projectDir, "environment.yml")) ||
fileExists(filepath.Join(projectDir, "environment.yaml")) {
return managerConda
}
if fileExists(filepath.Join(projectDir, "requirements.txt")) {
return managerPip
}

// Greenfield: nothing to disambiguate; this command provisions uv.
return managerUv
}

// managerGuidance returns the actionable, non-blaming message shown when a
// non-uv manager is detected (spec §5).
func managerGuidance(m manager) string {
return "detected a " + string(m) + " project; automated setup for " + string(m) +
" is not yet available (P1). Use a uv project (add a pyproject.toml with a [tool.uv] table, or run `uv init`) to provision automatically"
}

// fileExists reports whether path exists and is a regular file.
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}

// ensureWritable verifies the process can create files in dir by creating and
// removing a temporary file. A permission failure is reported so preflight can
// stop before any real write (invariant 1).
func ensureWritable(dir string) error {
f, err := os.CreateTemp(dir, ".localenv-writecheck-*")
if err != nil {
return err
}
name := f.Name()
f.Close()
return os.Remove(name)
}
45 changes: 45 additions & 0 deletions libs/localenv/detect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package localenv

import (
"os"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestDetectManager(t *testing.T) {
write := func(t *testing.T, files ...string) string {
dir := t.TempDir()
for _, f := range files {
require.NoError(t, os.WriteFile(filepath.Join(dir, f), []byte("x"), 0o644))
}
return dir
}

cases := []struct {
name string
files []string
want manager
}{
{"greenfield", nil, managerUv},
{"uv lock", []string{"uv.lock"}, managerUv},
{"plain pyproject", []string{"pyproject.toml"}, managerUv},
{"pyproject wins over requirements", []string{"pyproject.toml", "requirements.txt"}, managerUv},
{"conda only", []string{"environment.yml"}, managerConda},
{"conda yaml", []string{"environment.yaml"}, managerConda},
{"pip only", []string{"requirements.txt"}, managerPip},
{"conda before pip", []string{"environment.yml", "requirements.txt"}, managerConda},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.want, detectManager(write(t, tc.files...)))
})
}
}

func TestEnsureWritable(t *testing.T) {
assert.NoError(t, ensureWritable(t.TempDir()))
assert.Error(t, ensureWritable(filepath.Join(t.TempDir(), "does-not-exist")))
}
Loading
Loading