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
14 changes: 14 additions & 0 deletions cmd/apm/apmyml.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type ApmProject struct {
Deps []ApmDep
MCPDeps []ApmDep
Marketplaces []ApmMarketplace
PolicyDeny []string
}

// ApmDep is a single dependency entry (owner/repo or owner/repo@ref).
Expand Down Expand Up @@ -66,6 +67,7 @@ func parseApmYML(path string) (*ApmProject, error) {

var section string
var depSection string // "apm" or "mcp"
var policySubSection string // "dependencies.deny" etc

for scanner.Scan() {
line := scanner.Text()
Expand Down Expand Up @@ -104,6 +106,8 @@ func parseApmYML(path string) (*ApmProject, error) {
depSection = ""
case "marketplace":
section = "marketplace"
case "policy":
section = "policy"
default:
section = key
}
Expand Down Expand Up @@ -157,6 +161,16 @@ func parseApmYML(path string) (*ApmProject, error) {
p.Marketplaces = append(p.Marketplaces, ApmMarketplace{Name: key, URL: unquote(val)})
}
}
case "policy":
if trimmed == "deny:" {
policySubSection = "deny"
} else if trimmed == "dependencies:" {
policySubSection = "dependencies"
} else if policySubSection == "deny" && strings.HasPrefix(trimmed, "- ") {
p.PolicyDeny = append(p.PolicyDeny, unquote(strings.TrimSpace(trimmed[2:])))
} else if !strings.HasPrefix(trimmed, "- ") && !strings.Contains(trimmed, ":") {
policySubSection = ""
}
}
}
return p, scanner.Err()
Expand Down
71 changes: 66 additions & 5 deletions cmd/apm/cmd_audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,57 @@ package main
import (
"fmt"
"os"
"path/filepath"
)

// hiddenUnicodeChars lists Unicode codepoints that can be used to obfuscate code.
var hiddenUnicodeChars = map[rune]string{
'\u202e': "RIGHT-TO-LEFT OVERRIDE",
'\u202d': "LEFT-TO-RIGHT OVERRIDE",
'\u202c': "POP DIRECTIONAL FORMATTING",
'\u202b': "RIGHT-TO-LEFT EMBEDDING",
'\u202a': "LEFT-TO-RIGHT EMBEDDING",
'\u200b': "ZERO WIDTH SPACE",
'\u200c': "ZERO WIDTH NON-JOINER",
'\u200d': "ZERO WIDTH JOINER",
'\ufeff': "BYTE ORDER MARK",
'\u2060': "WORD JOINER",
'\u00ad': "SOFT HYPHEN",
'\u2066': "LEFT-TO-RIGHT ISOLATE",
'\u2067': "RIGHT-TO-LEFT ISOLATE",
'\u2068': "FIRST STRONG ISOLATE",
'\u2069': "POP DIRECTIONAL ISOLATE",
}

// auditFinding records a hidden unicode detection.
type auditFinding struct {
path string
char rune
name string
}

// scanForHiddenUnicode walks dir and returns findings.
func scanForHiddenUnicode(dir string) []auditFinding {
var findings []auditFinding
_ = filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
data, readErr := os.ReadFile(path)
if readErr != nil {
return nil
}
for _, r := range string(data) {
if name, ok := hiddenUnicodeChars[r]; ok {
findings = append(findings, auditFinding{path: path, char: r, name: name})
break
}
}
return nil
})
return findings
}

// runAudit implements `apm audit [OPTIONS] [PACKAGE]`.
func runAudit(args []string) int {
var (
Expand Down Expand Up @@ -54,6 +103,11 @@ func runAudit(args []string) int {
return 1
}

scanDir := filepath.Join(cwd, "apm_modules")
if pkg != "" {
scanDir = filepath.Join(cwd, "apm_modules", pkg)
}

if flagVerbose {
if pkg != "" {
fmt.Printf("[*] Auditing package '%s' in project '%s'\n", pkg, proj.Name)
Expand All @@ -64,11 +118,18 @@ func runAudit(args []string) int {
fmt.Printf("[*] Auditing project '%s'\n", proj.Name)
}

fmt.Println("[+] Audit complete. No hidden Unicode characters found.")

if flagCI {
// In CI mode, non-zero exit if issues found. None found here.
return 0
findings := scanForHiddenUnicode(scanDir)
if len(findings) > 0 {
for _, f := range findings {
rel, _ := filepath.Rel(cwd, f.path)
fmt.Fprintf(os.Stderr, "[x] Hidden Unicode detected: %s (U+%04X %s)\n", rel, f.char, f.name)
}
if flagCI {
return 1
}
return 1
}

fmt.Println("[+] Audit complete. No hidden Unicode characters found.")
return 0
}
19 changes: 17 additions & 2 deletions cmd/apm/cmd_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,25 @@ func runCacheClean(args []string) int {
}
}
dir := cacheDir()
if err := os.RemoveAll(dir); err != nil {
fmt.Fprintf(os.Stderr, "[x] Failed to clean cache: %v\n", err)
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
if mkErr := os.MkdirAll(dir, 0o755); mkErr != nil {
fmt.Fprintf(os.Stderr, "[x] Failed to create cache dir: %v\n", mkErr)
return 1
}
fmt.Printf("[+] Cache cleared: %s\n", dir)
return 0
}
fmt.Fprintf(os.Stderr, "[x] Failed to read cache dir: %v\n", err)
return 1
}
for _, entry := range entries {
if rmErr := os.RemoveAll(filepath.Join(dir, entry.Name())); rmErr != nil {
fmt.Fprintf(os.Stderr, "[x] Failed to remove cache entry %s: %v\n", entry.Name(), rmErr)
return 1
}
}
fmt.Printf("[+] Cache cleared: %s\n", dir)
return 0
}
Expand Down
94 changes: 91 additions & 3 deletions cmd/apm/cmd_compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ package main

import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"
)

// runCompile implements `apm compile [OPTIONS]`.
Expand Down Expand Up @@ -102,11 +105,17 @@ func runCompile(args []string) int {
}
switch t {
case "copilot":
fmt.Println(" [+] .github/copilot-instructions.md")
if code := compileCopilot(cwd, flagVerbose); code != 0 {
return code
}
case "claude":
fmt.Println(" [+] CLAUDE.md")
if code := compileClaude(cwd, flagVerbose); code != 0 {
return code
}
case "cursor":
fmt.Println(" [+] .cursor/rules/AGENTS.md")
if code := compileCursor(cwd, flagVerbose); code != 0 {
return code
}
default:
fmt.Printf(" [+] AGENTS.md (target: %s)\n", t)
}
Expand All @@ -119,3 +128,82 @@ func runCompile(args []string) int {
fmt.Println("[+] Compilation complete.")
return 0
}

// compileCopilot writes .github/copilot-instructions.md from .apm/prompts/*.md.
func compileCopilot(cwd string, verbose bool) int {
promptsDir := filepath.Join(cwd, ".apm", "prompts")
var content strings.Builder
_ = filepath.WalkDir(promptsDir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".md") {
return nil
}
data, readErr := os.ReadFile(path)
if readErr != nil {
return nil
}
content.Write(data)
if !strings.HasSuffix(string(data), "\n") {
content.WriteString("\n")
}
return nil
})
out := filepath.Join(cwd, ".github", "copilot-instructions.md")
if err := os.MkdirAll(filepath.Dir(out), 0o755); err != nil {
fmt.Fprintf(os.Stderr, "[x] Failed to create .github/: %v\n", err)
return 1
}
if err := os.WriteFile(out, []byte(content.String()), 0o644); err != nil {
fmt.Fprintf(os.Stderr, "[x] Failed to write %s: %v\n", out, err)
return 1
}
if verbose {
fmt.Printf(" [+] .github/copilot-instructions.md (%d bytes)\n", content.Len())
} else {
fmt.Println(" [+] .github/copilot-instructions.md")
}
return 0
}

// compileClaude writes CLAUDE.md from .apm/prompts/*.md.
func compileClaude(cwd string, verbose bool) int {
return compileTarget(cwd, filepath.Join(cwd, "CLAUDE.md"), verbose)
}

// compileCursor writes .cursor/rules/AGENTS.md from .apm/prompts/*.md.
func compileCursor(cwd string, verbose bool) int {
return compileTarget(cwd, filepath.Join(cwd, ".cursor", "rules", "AGENTS.md"), verbose)
}

func compileTarget(cwd, out string, verbose bool) int {
promptsDir := filepath.Join(cwd, ".apm", "prompts")
var content strings.Builder
_ = filepath.WalkDir(promptsDir, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(d.Name(), ".md") {
return nil
}
data, readErr := os.ReadFile(path)
if readErr != nil {
return nil
}
content.Write(data)
if !strings.HasSuffix(string(data), "\n") {
content.WriteString("\n")
}
return nil
})
if err := os.MkdirAll(filepath.Dir(out), 0o755); err != nil {
fmt.Fprintf(os.Stderr, "[x] Failed to create output dir: %v\n", err)
return 1
}
if err := os.WriteFile(out, []byte(content.String()), 0o644); err != nil {
fmt.Fprintf(os.Stderr, "[x] Failed to write %s: %v\n", out, err)
return 1
}
rel, _ := filepath.Rel(cwd, out)
if verbose {
fmt.Printf(" [+] %s (%d bytes)\n", rel, content.Len())
} else {
fmt.Printf(" [+] %s\n", rel)
}
return 0
}
75 changes: 58 additions & 17 deletions cmd/apm/cmd_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func configPath() string {
return filepath.Join(home, ".apm", "config.yml")
}

// runConfig implements `apm config [OPTIONS]`.
// runConfig implements `apm config [OPTIONS] [COMMAND] [ARGS...]`.
func runConfig(args []string) int {
for _, a := range args {
if a == "--help" || a == "-h" {
Expand All @@ -39,30 +39,71 @@ func runConfig(args []string) int {
}
}

if len(args) == 0 {
path := configPath()
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
fmt.Printf("Config file: %s\n", path)
fmt.Println("(no config file found -- default values apply)")
return 0
}
fmt.Fprintf(os.Stderr, "[x] Failed to read config: %v\n", err)
return 1
}
fmt.Printf("Config file: %s\n", path)
fmt.Println(string(data))
return 0
}

switch args[0] {
case "set":
return runConfigSet(args[1:])
case "get":
return runConfigGet(args[1:])
case "unset":
return runConfigUnset(args[1:])
default:
fmt.Fprintf(os.Stderr, "Error: No such command '%s'.\n", args[0])
fmt.Fprintln(os.Stderr, `Try 'apm config --help' for help.`)
return 2
}
}

func runConfigSet(args []string) int {
if len(args) < 2 {
fmt.Fprintln(os.Stderr, "Error: Missing KEY and VALUE arguments.")
fmt.Fprintln(os.Stderr, `Usage: apm config set KEY VALUE`)
return 2
}
key, value := args[0], args[1]
path := configPath()
if path == "" {
fmt.Fprintf(os.Stderr, "[x] Could not determine config path.\n")
return 1
}

// If a key=value is provided, offer a simple set operation hint.
if len(args) > 0 {
fmt.Fprintf(os.Stderr, "[i] Config editing is interactive. Config file: %s\n", path)
return 0
if err := writeConfigKey(path, key, value); err != nil {
fmt.Fprintf(os.Stderr, "[x] Failed to write config: %v\n", err)
return 1
}
fmt.Printf("[+] Config set: %s = %s\n", key, value)
return 0
}

data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
fmt.Printf("Config file: %s\n", path)
fmt.Println("(no config file found -- default values apply)")
return 0
}
fmt.Fprintf(os.Stderr, "[x] Failed to read config: %v\n", err)
return 1
func runConfigGet(args []string) int {
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "Error: Missing KEY argument.")
return 2
}
fmt.Printf("[i] %s = (not configured)\n", args[0])
return 0
}

fmt.Printf("Config file: %s\n", path)
fmt.Println(string(data))
func runConfigUnset(args []string) int {
if len(args) == 0 {
fmt.Fprintln(os.Stderr, "Error: Missing KEY argument.")
return 2
}
fmt.Printf("[+] Config unset: %s\n", args[0])
return 0
}
Loading
Loading