bf1efb2099
- Migration 007: repo_url on apps table + analysis table with FTS5 - Analysis struct, parser, CRUD, validation, hash computation - Selective purge: remote-only apps/analysis preserved across fn index - CLI: fn app list/clone/pull, fn analysis list/clone/pull - search/show/list now include analysis results - Apps removed from git tracking (content lives in Gitea repos) - .gitkeep for apps/ and analysis/ dirs - Bash functions: jupyter analysis pipeline, shell utilities - Browser domain: CDP functions moved from infra to browser Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
160 lines
3.4 KiB
Go
160 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"text/tabwriter"
|
|
)
|
|
|
|
func cmdApp(args []string) {
|
|
if len(args) < 1 {
|
|
printAppUsage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
switch args[0] {
|
|
case "list":
|
|
appList()
|
|
case "clone":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "usage: fn app clone <id>")
|
|
os.Exit(1)
|
|
}
|
|
appClone(args[1])
|
|
case "pull":
|
|
if len(args) < 2 {
|
|
fmt.Fprintln(os.Stderr, "usage: fn app pull <id>")
|
|
os.Exit(1)
|
|
}
|
|
appPull(args[1])
|
|
case "help", "-h":
|
|
printAppUsage()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown app command: %s\n", args[0])
|
|
printAppUsage()
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func printAppUsage() {
|
|
fmt.Println(`fn app — manage registry apps
|
|
|
|
Usage:
|
|
fn app list List all apps (local + remote)
|
|
fn app clone <id> Clone app repo to apps/<name>/
|
|
fn app pull <id> Git pull in existing clone`)
|
|
}
|
|
|
|
func appList() {
|
|
db := openDB()
|
|
defer db.Close()
|
|
|
|
apps, err := db.ListAllApps()
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if len(apps) == 0 {
|
|
fmt.Println("No apps registered.")
|
|
return
|
|
}
|
|
|
|
r := root()
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(w, "ID\tLANG\tDOMAIN\tSTATUS\tREPO_URL")
|
|
for _, a := range apps {
|
|
status := "local"
|
|
if a.RepoURL != "" {
|
|
dirPath := filepath.Join(r, "apps", a.Name)
|
|
if _, err := os.Stat(dirPath); os.IsNotExist(err) {
|
|
status = "remote"
|
|
} else {
|
|
status = "cloned"
|
|
}
|
|
}
|
|
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", a.ID, a.Lang, a.Domain, status, a.RepoURL)
|
|
}
|
|
w.Flush()
|
|
}
|
|
|
|
func appClone(id string) {
|
|
db := openDB()
|
|
defer db.Close()
|
|
|
|
a, err := db.GetApp(id)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
if a.RepoURL == "" {
|
|
fmt.Fprintf(os.Stderr, "app %s has no repo_url set\n", id)
|
|
os.Exit(1)
|
|
}
|
|
|
|
r := root()
|
|
target := filepath.Join(r, "apps", a.Name)
|
|
if _, err := os.Stat(target); err == nil {
|
|
fmt.Fprintf(os.Stderr, "directory already exists: %s\n", target)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("Cloning %s → %s\n", a.RepoURL, target)
|
|
cmd := exec.Command("git", "clone", a.RepoURL, target)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "git clone failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("Done. Run 'fn index' to re-index.")
|
|
}
|
|
|
|
func appPull(id string) {
|
|
db := openDB()
|
|
defer db.Close()
|
|
|
|
a, err := db.GetApp(id)
|
|
if err != nil {
|
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
r := root()
|
|
dir := filepath.Join(r, "apps", a.Name)
|
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
|
fmt.Fprintf(os.Stderr, "app not cloned locally: %s\n", dir)
|
|
fmt.Fprintln(os.Stderr, "Run 'fn app clone "+id+"' first.")
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("Pulling %s\n", dir)
|
|
cmd := exec.Command("git", "-C", dir, "pull")
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Run(); err != nil {
|
|
fmt.Fprintf(os.Stderr, "git pull failed: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println("Done. Run 'fn index' to re-index.")
|
|
}
|
|
|
|
// formatRepoURL ensures the URL does not contain inline credentials for display.
|
|
func formatRepoURL(url string) string {
|
|
if strings.Contains(url, "@") && strings.Contains(url, "://") {
|
|
// Mask credentials in URLs like https://user:pass@host/...
|
|
parts := strings.SplitN(url, "://", 2)
|
|
if len(parts) == 2 {
|
|
atIdx := strings.LastIndex(parts[1], "@")
|
|
if atIdx >= 0 {
|
|
return parts[0] + "://" + parts[1][atIdx+1:]
|
|
}
|
|
}
|
|
}
|
|
return url
|
|
}
|