Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions cli/cmd/testutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,20 @@ package cmd

import (
"bytes"
"crypto/sha1"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"

"github.com/anthropics/code-index/cli/internal/client"
)

// projectHash returns the same SHA1 prefix that the client uses for URL routing.
// projectHash returns the same project URL hash the client uses for routing —
// delegated to the real implementation so the per-machine namespacing matches.
func projectHash(path string) string {
h := sha1.Sum([]byte(path))
return fmt.Sprintf("%x", h)[:16]
return client.EncodeProjectPath(path)
}

// mockServer starts a test HTTP server and registers cleanup.
Expand Down
63 changes: 60 additions & 3 deletions cli/internal/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,17 @@ package client

import (
"bytes"
"crypto/rand"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
)

Expand Down Expand Up @@ -84,13 +90,64 @@ func (c *Client) do(method, path string, body interface{}) (*http.Response, erro
return resp, nil
}

// encodeProjectPath returns SHA1 hash (first 16 hex chars) of the project path.
// This avoids all URL encoding issues with slashes in paths.
// encodeProjectPath returns the project's URL hash (first 16 hex chars of
// SHA1 of the identity key). Local projects are namespaced per machine —
// "local:{machine_id}:{path}" — so the same filesystem path on different
// machines/users maps to different projects. MUST stay byte-identical to the
// server's projects.LocalProjectKey + hashPath (server/internal/projects).
func encodeProjectPath(path string) string {
h := sha1.Sum([]byte(path))
key := "local:" + machineID() + ":" + path
h := sha1.Sum([]byte(key))
return fmt.Sprintf("%x", h)[:16]
}

// EncodeProjectPath is the exported project URL hash, for tests and tooling
// that need to mirror the client's addressing exactly.
func EncodeProjectPath(path string) string { return encodeProjectPath(path) }

var (
machineIDOnce sync.Once
machineIDVal string
)

// machineID returns a stable per-machine (per-home) identifier, persisted at
// ~/.cix/machine_id. Generated on first use. Used to namespace local project
// identity so different developers' machines never collide on the same path.
func machineID() string {
machineIDOnce.Do(func() { machineIDVal = loadOrCreateMachineID() })
return machineIDVal
}

func loadOrCreateMachineID() string {
home, err := os.UserHomeDir()
if err != nil {
return "unknown-machine"
}
path := filepath.Join(home, ".cix", "machine_id")
if b, rerr := os.ReadFile(path); rerr == nil {
if id := strings.TrimSpace(string(b)); id != "" {
return id
}
}
buf := make([]byte, 16)
if _, gerr := rand.Read(buf); gerr != nil {
return "unknown-machine"
}
id := hex.EncodeToString(buf)
_ = os.MkdirAll(filepath.Dir(path), 0o755)
_ = os.WriteFile(path, []byte(id+"\n"), 0o600)
return id
}

// machineLabel returns the OS hostname for display purposes (sent to the
// server as machine_label). Best-effort; empty when unavailable.
func machineLabel() string {
if h, err := os.Hostname(); err == nil {
return h
}
return ""
}

// parseResponse reads and unmarshals JSON response
func parseResponse(resp *http.Response, v interface{}) error {
defer resp.Body.Close()
Expand Down
8 changes: 7 additions & 1 deletion cli/internal/client/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,14 @@ func (c *Client) GetProject(path string) (*Project, error) {

// CreateProject creates a new project
func (c *Client) CreateProject(path string) (*Project, error) {
// machine_id namespaces the project's identity so the same filesystem path
// on a different machine/user is a distinct project; machine_label is the
// hostname for display. The server derives path_hash from
// local:{machine_id}:{host_path} — matching encodeProjectPath.
body := map[string]string{
"host_path": path,
"host_path": path,
"machine_id": machineID(),
"machine_label": machineLabel(),
}

resp, err := c.do("POST", "/api/v1/projects", body)
Expand Down
8 changes: 3 additions & 5 deletions cli/internal/indexer/indexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@ package indexer

import (
"context"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
Expand All @@ -18,10 +16,10 @@ import (
"github.com/anthropics/code-index/cli/internal/client"
)

// projectHash mirrors the client's URL-encoding logic (SHA1, first 16 hex chars).
// projectHash mirrors the client's project URL hash — delegated to the real
// implementation so per-machine namespacing matches.
func projectHash(path string) string {
h := sha1.Sum([]byte(path))
return fmt.Sprintf("%x", h)[:16]
return client.EncodeProjectPath(path)
}

// sha256hex computes the hex-encoded SHA-256 of b, matching discovery.hashFile.
Expand Down
8 changes: 3 additions & 5 deletions cli/internal/watcher/watcher_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package watcher

import (
"crypto/sha1"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
Expand All @@ -19,10 +17,10 @@ import (
"github.com/rjeczalik/notify"
)

// projectHash mirrors client.encodeProjectPath.
// projectHash mirrors the client's project URL hash — delegated to the real
// implementation so per-machine namespacing matches.
func projectHash(path string) string {
h := sha1.Sum([]byte(path))
return fmt.Sprintf("%x", h)[:16]
return client.EncodeProjectPath(path)
}

// mockEventInfo implements notify.EventInfo for testing.
Expand Down
Loading
Loading