Skip to content

Commit bce0d8d

Browse files
JordanCoinclaude
andcommitted
Add --stdin flag for sandboxed app integration
Accepts JSON file manifest from stdin instead of reading filesystem. Used by Lens Mac App Store app which can't grant subprocess file access. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2fe69e3 commit bce0d8d

2 files changed

Lines changed: 94 additions & 15 deletions

File tree

main.go

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ func main() {
159159
jsonMode := flag.Bool("json", false, "Output JSON (for Python renderer compatibility)")
160160
debugMode := flag.Bool("debug", false, "Show debug info (gitignore loading, paths, etc.)")
161161
watchMode := flag.Bool("watch", false, "Live file watcher daemon (experimental)")
162+
stdinMode := flag.Bool("stdin", false, "Read file manifest from stdin (use with --deps)")
162163
importersMode := flag.String("importers", "", "Check file impact: who imports it, is it a hub?")
163164
helpMode := flag.Bool("help", false, "Show help")
164165
// Short flag aliases
@@ -180,6 +181,7 @@ func main() {
180181
fmt.Println(" --depth, -d <n> Limit tree depth (0 = unlimited)")
181182
fmt.Println(" --only <exts> Only show files with these extensions (e.g., 'swift,go')")
182183
fmt.Println(" --exclude <patterns> Exclude paths matching patterns (e.g., '.xcassets,Fonts')")
184+
fmt.Println(" --stdin Read JSON file manifest from stdin (use with --deps)")
183185
fmt.Println(" --importers <file> Check file impact (who imports it, hub status)")
184186
fmt.Println()
185187
fmt.Println("Examples:")
@@ -193,6 +195,7 @@ func main() {
193195
fmt.Println(" codemap --only swift . # Just Swift files")
194196
fmt.Println(" codemap --exclude .xcassets,Fonts,.png # Hide assets")
195197
fmt.Println(" codemap --importers scanner/types.go # Check file impact")
198+
fmt.Println(" echo '{...}' | codemap --deps --stdin # Deps from file manifest")
196199
fmt.Println()
197200
fmt.Println("Remote repos (clones temporarily):")
198201
fmt.Println(" codemap github.com/user/repo # GitHub repo")
@@ -328,7 +331,7 @@ func main() {
328331
if diffInfo != nil {
329332
changedFiles = diffInfo.Changed
330333
}
331-
runDepsMode(absRoot, root, *jsonMode, *diffRef, changedFiles)
334+
runDepsMode(absRoot, root, *jsonMode, *diffRef, changedFiles, *stdinMode)
332335
return
333336
}
334337

@@ -377,17 +380,43 @@ func main() {
377380
}
378381
}
379382

380-
func runDepsMode(absRoot, root string, jsonMode bool, diffRef string, changedFiles map[string]bool) {
381-
analyses, err := scanner.ScanForDeps(root)
382-
if err != nil {
383-
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
384-
fmt.Fprintln(os.Stderr, "")
385-
fmt.Fprintln(os.Stderr, "The --deps feature requires ast-grep. Install it with:")
386-
fmt.Fprintln(os.Stderr, " brew install ast-grep # macOS/Linux (installs as 'sg')")
387-
fmt.Fprintln(os.Stderr, " cargo install ast-grep # via Rust (installs as 'ast-grep')")
388-
fmt.Fprintln(os.Stderr, " pipx install ast-grep # via Python (installs as 'ast-grep')")
389-
fmt.Fprintln(os.Stderr, "")
390-
os.Exit(1)
383+
// stdinManifest is the JSON format accepted by --stdin.
384+
type stdinManifest struct {
385+
Root string `json:"root"`
386+
Files []struct {
387+
Path string `json:"path"`
388+
Content string `json:"content"`
389+
} `json:"files"`
390+
}
391+
392+
func runDepsMode(absRoot, root string, jsonMode bool, diffRef string, changedFiles map[string]bool, stdinMode bool) {
393+
var analyses []FileAnalysis
394+
var externalDeps map[string][]string
395+
var err error
396+
397+
if stdinMode {
398+
analyses, externalDeps, err = runDepsFromStdin()
399+
if err != nil {
400+
fmt.Fprintf(os.Stderr, "Error reading stdin manifest: %v\n", err)
401+
os.Exit(1)
402+
}
403+
// Use the manifest root as absRoot if provided
404+
if externalDeps == nil {
405+
externalDeps = make(map[string][]string)
406+
}
407+
} else {
408+
analyses, err = scanForDepsWithHint(root)
409+
if err != nil {
410+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
411+
fmt.Fprintln(os.Stderr, "")
412+
fmt.Fprintln(os.Stderr, "The --deps feature requires ast-grep. Install it with:")
413+
fmt.Fprintln(os.Stderr, " brew install ast-grep # macOS/Linux (installs as 'sg')")
414+
fmt.Fprintln(os.Stderr, " cargo install ast-grep # via Rust (installs as 'ast-grep')")
415+
fmt.Fprintln(os.Stderr, " pipx install ast-grep # via Python (installs as 'ast-grep')")
416+
fmt.Fprintln(os.Stderr, "")
417+
os.Exit(1)
418+
}
419+
externalDeps = scanner.ReadExternalDeps(absRoot)
391420
}
392421

393422
// Filter to changed files if --diff specified
@@ -399,7 +428,7 @@ func runDepsMode(absRoot, root string, jsonMode bool, diffRef string, changedFil
399428
Root: absRoot,
400429
Mode: "deps",
401430
Files: analyses,
402-
ExternalDeps: scanner.ReadExternalDeps(absRoot),
431+
ExternalDeps: externalDeps,
403432
DiffRef: diffRef,
404433
}
405434

@@ -411,6 +440,56 @@ func runDepsMode(absRoot, root string, jsonMode bool, diffRef string, changedFil
411440
}
412441
}
413442

443+
// scanForDepsWithHint wraps scanner.ScanForDeps (extracted for testability).
444+
func scanForDepsWithHint(root string) ([]FileAnalysis, error) {
445+
return scanner.ScanForDeps(root)
446+
}
447+
448+
// runDepsFromStdin reads a JSON manifest from stdin, writes files to a temp
449+
// directory, runs ast-grep on it, and returns the results with paths matching
450+
// the original manifest.
451+
func runDepsFromStdin() ([]FileAnalysis, map[string][]string, error) {
452+
var manifest stdinManifest
453+
if err := json.NewDecoder(os.Stdin).Decode(&manifest); err != nil {
454+
return nil, nil, fmt.Errorf("invalid JSON: %w", err)
455+
}
456+
457+
if len(manifest.Files) == 0 {
458+
return nil, nil, nil
459+
}
460+
461+
// Create temp directory and write manifest files
462+
tempDir, err := os.MkdirTemp("", "codemap-stdin-*")
463+
if err != nil {
464+
return nil, nil, fmt.Errorf("failed to create temp dir: %w", err)
465+
}
466+
defer os.RemoveAll(tempDir)
467+
468+
for _, f := range manifest.Files {
469+
dest := filepath.Join(tempDir, f.Path)
470+
if err := os.MkdirAll(filepath.Dir(dest), 0755); err != nil {
471+
return nil, nil, fmt.Errorf("mkdir %s: %w", filepath.Dir(dest), err)
472+
}
473+
if err := os.WriteFile(dest, []byte(f.Content), 0644); err != nil {
474+
return nil, nil, fmt.Errorf("write %s: %w", f.Path, err)
475+
}
476+
}
477+
478+
// Run ast-grep on temp directory
479+
analyses, err := scanner.ScanForDeps(tempDir)
480+
if err != nil {
481+
return nil, nil, err
482+
}
483+
484+
// Read external deps from temp directory (manifest may include go.mod etc.)
485+
externalDeps := scanner.ReadExternalDeps(tempDir)
486+
487+
return analyses, externalDeps, nil
488+
}
489+
490+
// FileAnalysis is a type alias for use in main package.
491+
type FileAnalysis = scanner.FileAnalysis
492+
414493
func runWatchMode(root string, verbose bool) {
415494
fmt.Println("codemap watch - Live code graph daemon")
416495
fmt.Println()

main_more_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ func TestRunDepsModeJSONAndMainDispatchesDepsAndImporters(t *testing.T) {
309309
writeImportersFixture(t, root)
310310

311311
stdout, _ := captureMainStreams(t, func() {
312-
runDepsMode(root, root, true, "main", map[string]bool{"a/a.go": true})
312+
runDepsMode(root, root, true, "main", map[string]bool{"a/a.go": true}, false)
313313
})
314314

315315
var depsProject scanner.DepsProject
@@ -661,7 +661,7 @@ func TestRunDepsModeRenderedOutputAndMainTreeModes(t *testing.T) {
661661
writeImportersFixture(t, root)
662662

663663
stdout, _ := captureMainStreams(t, func() {
664-
runDepsMode(root, root, false, "main", nil)
664+
runDepsMode(root, root, false, "main", nil, false)
665665
})
666666
if !strings.Contains(stdout, "Dependency Flow") {
667667
t.Fatalf("expected rendered dependency graph output, got:\n%s", stdout)

0 commit comments

Comments
 (0)