chore(ax): pass 2 AX compliance sweep
Resolve all deferred items from pass 1 and deeper violations found in this pass: rename abbreviated variables (a, e, f, s, ts, inp, inputStr, firstTS/lastTS, argv, id) to full descriptive names; add usage-example comments to all unexported helpers (parseFromReader, extractToolInput, extractResultContent, truncate, shortID, formatDuration, generateTape, extractCommand, lookupExecutable, isExecutablePath, runCommand, resultError, repeatString, containsAny, indexOf, trimQuotes); rename single-letter helper parameters to match AX principle 1. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
294892c83b
commit
a10e30d9db
5 changed files with 153 additions and 109 deletions
28
analytics.go
28
analytics.go
|
|
@ -31,7 +31,7 @@ type SessionAnalytics struct {
|
|||
// Example:
|
||||
// analytics := session.Analyse(sess)
|
||||
func Analyse(sess *Session) *SessionAnalytics {
|
||||
a := &SessionAnalytics{
|
||||
analytics := &SessionAnalytics{
|
||||
ToolCounts: make(map[string]int),
|
||||
ErrorCounts: make(map[string]int),
|
||||
AvgLatency: make(map[string]time.Duration),
|
||||
|
|
@ -39,11 +39,11 @@ func Analyse(sess *Session) *SessionAnalytics {
|
|||
}
|
||||
|
||||
if sess == nil {
|
||||
return a
|
||||
return analytics
|
||||
}
|
||||
|
||||
a.Duration = sess.EndTime.Sub(sess.StartTime)
|
||||
a.EventCount = len(sess.Events)
|
||||
analytics.Duration = sess.EndTime.Sub(sess.StartTime)
|
||||
analytics.EventCount = len(sess.Events)
|
||||
|
||||
// Track totals for latency averaging
|
||||
type latencyAccum struct {
|
||||
|
|
@ -57,23 +57,23 @@ func Analyse(sess *Session) *SessionAnalytics {
|
|||
|
||||
for evt := range sess.EventsSeq() {
|
||||
// Token estimation: ~4 chars per token
|
||||
a.EstimatedInputTokens += len(evt.Input) / 4
|
||||
a.EstimatedOutputTokens += len(evt.Output) / 4
|
||||
analytics.EstimatedInputTokens += len(evt.Input) / 4
|
||||
analytics.EstimatedOutputTokens += len(evt.Output) / 4
|
||||
|
||||
if evt.Type != "tool_use" {
|
||||
continue
|
||||
}
|
||||
|
||||
totalToolCalls++
|
||||
a.ToolCounts[evt.Tool]++
|
||||
analytics.ToolCounts[evt.Tool]++
|
||||
|
||||
if !evt.Success {
|
||||
totalErrors++
|
||||
a.ErrorCounts[evt.Tool]++
|
||||
analytics.ErrorCounts[evt.Tool]++
|
||||
}
|
||||
|
||||
// Active time: sum of tool call durations
|
||||
a.ActiveTime += evt.Duration
|
||||
analytics.ActiveTime += evt.Duration
|
||||
|
||||
// Latency tracking
|
||||
if _, ok := latencies[evt.Tool]; !ok {
|
||||
|
|
@ -82,24 +82,24 @@ func Analyse(sess *Session) *SessionAnalytics {
|
|||
latencies[evt.Tool].total += evt.Duration
|
||||
latencies[evt.Tool].count++
|
||||
|
||||
if evt.Duration > a.MaxLatency[evt.Tool] {
|
||||
a.MaxLatency[evt.Tool] = evt.Duration
|
||||
if evt.Duration > analytics.MaxLatency[evt.Tool] {
|
||||
analytics.MaxLatency[evt.Tool] = evt.Duration
|
||||
}
|
||||
}
|
||||
|
||||
// Compute averages
|
||||
for tool, accumulator := range latencies {
|
||||
if accumulator.count > 0 {
|
||||
a.AvgLatency[tool] = accumulator.total / time.Duration(accumulator.count)
|
||||
analytics.AvgLatency[tool] = accumulator.total / time.Duration(accumulator.count)
|
||||
}
|
||||
}
|
||||
|
||||
// Success rate
|
||||
if totalToolCalls > 0 {
|
||||
a.SuccessRate = float64(totalToolCalls-totalErrors) / float64(totalToolCalls)
|
||||
analytics.SuccessRate = float64(totalToolCalls-totalErrors) / float64(totalToolCalls)
|
||||
}
|
||||
|
||||
return a
|
||||
return analytics
|
||||
}
|
||||
|
||||
// FormatAnalytics returns a tabular text summary suitable for CLI display.
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ func (m rawJSON) MarshalJSON() ([]byte, error) {
|
|||
return m, nil
|
||||
}
|
||||
|
||||
// resultError extracts the error from a core.Result, or constructs one if absent.
|
||||
//
|
||||
// err := resultError(hostFS.Write(path, content))
|
||||
func resultError(result core.Result) error {
|
||||
if result.OK {
|
||||
return nil
|
||||
|
|
@ -36,32 +39,44 @@ func resultError(result core.Result) error {
|
|||
return core.E("resultError", "unexpected core result failure", nil)
|
||||
}
|
||||
|
||||
func repeatString(s string, count int) string {
|
||||
if s == "" || count <= 0 {
|
||||
// repeatString returns text repeated count times.
|
||||
//
|
||||
// repeatString("=", 50) // "=================================================="
|
||||
func repeatString(text string, count int) string {
|
||||
if text == "" || count <= 0 {
|
||||
return ""
|
||||
}
|
||||
return string(bytes.Repeat([]byte(s), count))
|
||||
return string(bytes.Repeat([]byte(text), count))
|
||||
}
|
||||
|
||||
func containsAny(s, chars string) bool {
|
||||
// containsAny reports whether text contains any rune in chars.
|
||||
//
|
||||
// containsAny("/tmp/file", `/\`) // true
|
||||
func containsAny(text, chars string) bool {
|
||||
for _, character := range chars {
|
||||
if bytes.IndexRune([]byte(s), character) >= 0 {
|
||||
if bytes.IndexRune([]byte(text), character) >= 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func indexOf(s, substr string) int {
|
||||
return bytes.Index([]byte(s), []byte(substr))
|
||||
// indexOf returns the byte offset of substr in text, or -1 if not found.
|
||||
//
|
||||
// indexOf("hello world", "world") // 6
|
||||
func indexOf(text, substr string) int {
|
||||
return bytes.Index([]byte(text), []byte(substr))
|
||||
}
|
||||
|
||||
func trimQuotes(s string) string {
|
||||
if len(s) < 2 {
|
||||
return s
|
||||
// trimQuotes strips a single layer of matching double-quotes or back-ticks.
|
||||
//
|
||||
// trimQuotes(`"hello"`) // "hello"
|
||||
func trimQuotes(text string) string {
|
||||
if len(text) < 2 {
|
||||
return text
|
||||
}
|
||||
if (s[0] == '"' && s[len(s)-1] == '"') || (s[0] == '`' && s[len(s)-1] == '`') {
|
||||
return s[1 : len(s)-1]
|
||||
if (text[0] == '"' && text[len(text)-1] == '"') || (text[0] == '`' && text[len(text)-1] == '`') {
|
||||
return text[1 : len(text)-1]
|
||||
}
|
||||
return s
|
||||
return text
|
||||
}
|
||||
|
|
|
|||
12
html.go
12
html.go
|
|
@ -21,10 +21,10 @@ func RenderHTML(sess *Session, outputPath string) error {
|
|||
duration := sess.EndTime.Sub(sess.StartTime)
|
||||
toolCount := 0
|
||||
errorCount := 0
|
||||
for e := range sess.EventsSeq() {
|
||||
if e.Type == "tool_use" {
|
||||
for evt := range sess.EventsSeq() {
|
||||
if evt.Type == "tool_use" {
|
||||
toolCount++
|
||||
if !e.Success {
|
||||
if !evt.Success {
|
||||
errorCount++
|
||||
}
|
||||
}
|
||||
|
|
@ -246,6 +246,9 @@ document.addEventListener('keydown', e => {
|
|||
return nil
|
||||
}
|
||||
|
||||
// shortID returns the first 8 characters of an ID for display purposes.
|
||||
//
|
||||
// shortID("abc123def456") // "abc123de"
|
||||
func shortID(id string) string {
|
||||
if len(id) > 8 {
|
||||
return id[:8]
|
||||
|
|
@ -253,6 +256,9 @@ func shortID(id string) string {
|
|||
return id
|
||||
}
|
||||
|
||||
// formatDuration renders a duration as a compact human-readable string.
|
||||
//
|
||||
// formatDuration(5*time.Minute + 30*time.Second) // "5m30s"
|
||||
func formatDuration(d time.Duration) string {
|
||||
if d < time.Second {
|
||||
return core.Sprintf("%dms", d.Milliseconds())
|
||||
|
|
|
|||
153
parser.go
153
parser.go
|
|
@ -165,7 +165,7 @@ func ListSessionsSeq(projectsDir string) iter.Seq[Session] {
|
|||
continue
|
||||
}
|
||||
|
||||
s := Session{
|
||||
sess := Session{
|
||||
ID: id,
|
||||
Path: filePath,
|
||||
}
|
||||
|
|
@ -175,14 +175,14 @@ func ListSessionsSeq(projectsDir string) iter.Seq[Session] {
|
|||
if !openResult.OK {
|
||||
continue
|
||||
}
|
||||
f, ok := openResult.Value.(io.ReadCloser)
|
||||
fileHandle, ok := openResult.Value.(io.ReadCloser)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner := bufio.NewScanner(fileHandle)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
var firstTS, lastTS string
|
||||
var firstTimestamp, lastTimestamp string
|
||||
for scanner.Scan() {
|
||||
var entry rawEntry
|
||||
if !core.JSONUnmarshal(scanner.Bytes(), &entry).OK {
|
||||
|
|
@ -191,36 +191,36 @@ func ListSessionsSeq(projectsDir string) iter.Seq[Session] {
|
|||
if entry.Timestamp == "" {
|
||||
continue
|
||||
}
|
||||
if firstTS == "" {
|
||||
firstTS = entry.Timestamp
|
||||
if firstTimestamp == "" {
|
||||
firstTimestamp = entry.Timestamp
|
||||
}
|
||||
lastTS = entry.Timestamp
|
||||
lastTimestamp = entry.Timestamp
|
||||
}
|
||||
f.Close()
|
||||
fileHandle.Close()
|
||||
|
||||
if firstTS != "" {
|
||||
if t, err := time.Parse(time.RFC3339Nano, firstTS); err == nil {
|
||||
s.StartTime = t
|
||||
if firstTimestamp != "" {
|
||||
if parsedTime, err := time.Parse(time.RFC3339Nano, firstTimestamp); err == nil {
|
||||
sess.StartTime = parsedTime
|
||||
}
|
||||
}
|
||||
if lastTS != "" {
|
||||
if t, err := time.Parse(time.RFC3339Nano, lastTS); err == nil {
|
||||
s.EndTime = t
|
||||
if lastTimestamp != "" {
|
||||
if parsedTime, err := time.Parse(time.RFC3339Nano, lastTimestamp); err == nil {
|
||||
sess.EndTime = parsedTime
|
||||
}
|
||||
}
|
||||
if s.StartTime.IsZero() {
|
||||
s.StartTime = info.ModTime()
|
||||
if sess.StartTime.IsZero() {
|
||||
sess.StartTime = info.ModTime()
|
||||
}
|
||||
|
||||
sessions = append(sessions, s)
|
||||
sessions = append(sessions, sess)
|
||||
}
|
||||
|
||||
slices.SortFunc(sessions, func(i, j Session) int {
|
||||
return j.StartTime.Compare(i.StartTime)
|
||||
})
|
||||
|
||||
for _, s := range sessions {
|
||||
if !yield(s) {
|
||||
for _, sess := range sessions {
|
||||
if !yield(sess) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -292,16 +292,16 @@ func ParseTranscript(filePath string) (*Session, *ParseStats, error) {
|
|||
if !openResult.OK {
|
||||
return nil, nil, core.E("ParseTranscript", "open transcript", resultError(openResult))
|
||||
}
|
||||
f, ok := openResult.Value.(io.ReadCloser)
|
||||
fileHandle, ok := openResult.Value.(io.ReadCloser)
|
||||
if !ok {
|
||||
return nil, nil, core.E("ParseTranscript", "unexpected file handle type", nil)
|
||||
}
|
||||
defer f.Close()
|
||||
defer fileHandle.Close()
|
||||
|
||||
base := path.Base(filePath)
|
||||
id := core.TrimSuffix(base, ".jsonl")
|
||||
|
||||
sess, stats, err := parseFromReader(f, id)
|
||||
sess, stats, err := parseFromReader(fileHandle, id)
|
||||
if sess != nil {
|
||||
sess.Path = filePath
|
||||
}
|
||||
|
|
@ -325,9 +325,9 @@ func ParseTranscriptReader(r io.Reader, id string) (*Session, *ParseStats, error
|
|||
return sess, stats, nil
|
||||
}
|
||||
|
||||
// parseFromReader is the shared implementation for both file-based and
|
||||
// reader-based parsing. It scans line-by-line using bufio.Scanner with
|
||||
// an 8 MiB buffer, gracefully skipping malformed lines.
|
||||
// parseFromReader scans r line-by-line and returns parsed session events.
|
||||
//
|
||||
// sess, stats, err := parseFromReader(f, "abc123")
|
||||
func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) {
|
||||
sess := &Session{
|
||||
ID: id,
|
||||
|
|
@ -375,17 +375,17 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) {
|
|||
continue
|
||||
}
|
||||
|
||||
ts, err := time.Parse(time.RFC3339Nano, entry.Timestamp)
|
||||
entryTime, err := time.Parse(time.RFC3339Nano, entry.Timestamp)
|
||||
if err != nil {
|
||||
stats.Warnings = append(stats.Warnings, core.Sprintf("line %d: bad timestamp %q: %v", lineNum, entry.Timestamp, err))
|
||||
continue
|
||||
}
|
||||
|
||||
if sess.StartTime.IsZero() && !ts.IsZero() {
|
||||
sess.StartTime = ts
|
||||
if sess.StartTime.IsZero() && !entryTime.IsZero() {
|
||||
sess.StartTime = entryTime
|
||||
}
|
||||
if !ts.IsZero() {
|
||||
sess.EndTime = ts
|
||||
if !entryTime.IsZero() {
|
||||
sess.EndTime = entryTime
|
||||
}
|
||||
|
||||
switch entry.Type {
|
||||
|
|
@ -406,18 +406,18 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) {
|
|||
case "text":
|
||||
if text := core.Trim(block.Text); text != "" {
|
||||
sess.Events = append(sess.Events, Event{
|
||||
Timestamp: ts,
|
||||
Timestamp: entryTime,
|
||||
Type: "assistant",
|
||||
Input: truncate(text, 500),
|
||||
})
|
||||
}
|
||||
|
||||
case "tool_use":
|
||||
inputStr := extractToolInput(block.Name, block.Input)
|
||||
toolInput := extractToolInput(block.Name, block.Input)
|
||||
pendingTools[block.ID] = toolUse{
|
||||
timestamp: ts,
|
||||
timestamp: entryTime,
|
||||
tool: block.Name,
|
||||
input: inputStr,
|
||||
input: toolInput,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -447,7 +447,7 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) {
|
|||
ToolID: block.ToolUseID,
|
||||
Input: pendingTool.input,
|
||||
Output: truncate(output, 2000),
|
||||
Duration: ts.Sub(pendingTool.timestamp),
|
||||
Duration: entryTime.Sub(pendingTool.timestamp),
|
||||
Success: !isError,
|
||||
}
|
||||
if isError {
|
||||
|
|
@ -460,7 +460,7 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) {
|
|||
case "text":
|
||||
if text := core.Trim(block.Text); text != "" {
|
||||
sess.Events = append(sess.Events, Event{
|
||||
Timestamp: ts,
|
||||
Timestamp: entryTime,
|
||||
Type: "user",
|
||||
Input: truncate(text, 500),
|
||||
})
|
||||
|
|
@ -492,6 +492,9 @@ func parseFromReader(r io.Reader, id string) (*Session, *ParseStats, error) {
|
|||
return sess, stats, nil
|
||||
}
|
||||
|
||||
// extractToolInput decodes a tool's raw JSON input to a human-readable string.
|
||||
//
|
||||
// label := extractToolInput("Bash", raw) // "ls -la # list files"
|
||||
func extractToolInput(toolName string, raw rawJSON) string {
|
||||
if raw == nil {
|
||||
return ""
|
||||
|
|
@ -499,64 +502,67 @@ func extractToolInput(toolName string, raw rawJSON) string {
|
|||
|
||||
switch toolName {
|
||||
case "Bash":
|
||||
var inp bashInput
|
||||
if core.JSONUnmarshal(raw, &inp).OK {
|
||||
desc := inp.Description
|
||||
var input bashInput
|
||||
if core.JSONUnmarshal(raw, &input).OK {
|
||||
desc := input.Description
|
||||
if desc != "" {
|
||||
desc = " # " + desc
|
||||
}
|
||||
return inp.Command + desc
|
||||
return input.Command + desc
|
||||
}
|
||||
case "Read":
|
||||
var inp readInput
|
||||
if core.JSONUnmarshal(raw, &inp).OK {
|
||||
return inp.FilePath
|
||||
var input readInput
|
||||
if core.JSONUnmarshal(raw, &input).OK {
|
||||
return input.FilePath
|
||||
}
|
||||
case "Edit":
|
||||
var inp editInput
|
||||
if core.JSONUnmarshal(raw, &inp).OK {
|
||||
return core.Sprintf("%s (edit)", inp.FilePath)
|
||||
var input editInput
|
||||
if core.JSONUnmarshal(raw, &input).OK {
|
||||
return core.Sprintf("%s (edit)", input.FilePath)
|
||||
}
|
||||
case "Write":
|
||||
var inp writeInput
|
||||
if core.JSONUnmarshal(raw, &inp).OK {
|
||||
return core.Sprintf("%s (%d bytes)", inp.FilePath, len(inp.Content))
|
||||
var input writeInput
|
||||
if core.JSONUnmarshal(raw, &input).OK {
|
||||
return core.Sprintf("%s (%d bytes)", input.FilePath, len(input.Content))
|
||||
}
|
||||
case "Grep":
|
||||
var inp grepInput
|
||||
if core.JSONUnmarshal(raw, &inp).OK {
|
||||
path := inp.Path
|
||||
if path == "" {
|
||||
path = "."
|
||||
var input grepInput
|
||||
if core.JSONUnmarshal(raw, &input).OK {
|
||||
grepPath := input.Path
|
||||
if grepPath == "" {
|
||||
grepPath = "."
|
||||
}
|
||||
return core.Sprintf("/%s/ in %s", inp.Pattern, path)
|
||||
return core.Sprintf("/%s/ in %s", input.Pattern, grepPath)
|
||||
}
|
||||
case "Glob":
|
||||
var inp globInput
|
||||
if core.JSONUnmarshal(raw, &inp).OK {
|
||||
return inp.Pattern
|
||||
var input globInput
|
||||
if core.JSONUnmarshal(raw, &input).OK {
|
||||
return input.Pattern
|
||||
}
|
||||
case "Task":
|
||||
var inp taskInput
|
||||
if core.JSONUnmarshal(raw, &inp).OK {
|
||||
desc := inp.Description
|
||||
var input taskInput
|
||||
if core.JSONUnmarshal(raw, &input).OK {
|
||||
desc := input.Description
|
||||
if desc == "" {
|
||||
desc = truncate(inp.Prompt, 80)
|
||||
desc = truncate(input.Prompt, 80)
|
||||
}
|
||||
return core.Sprintf("[%s] %s", inp.SubagentType, desc)
|
||||
return core.Sprintf("[%s] %s", input.SubagentType, desc)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: show raw JSON keys
|
||||
var m map[string]any
|
||||
if core.JSONUnmarshal(raw, &m).OK {
|
||||
parts := slices.Sorted(maps.Keys(m))
|
||||
var jsonFields map[string]any
|
||||
if core.JSONUnmarshal(raw, &jsonFields).OK {
|
||||
parts := slices.Sorted(maps.Keys(jsonFields))
|
||||
return core.Join(", ", parts...)
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractResultContent coerces a tool_result content value to a plain string.
|
||||
//
|
||||
// text := extractResultContent(block.Content) // "total 42\n..."
|
||||
func extractResultContent(content any) string {
|
||||
switch v := content.(type) {
|
||||
case string:
|
||||
|
|
@ -564,8 +570,8 @@ func extractResultContent(content any) string {
|
|||
case []any:
|
||||
var parts []string
|
||||
for _, item := range v {
|
||||
if m, ok := item.(map[string]any); ok {
|
||||
if text, ok := m["text"].(string); ok {
|
||||
if contentMap, ok := item.(map[string]any); ok {
|
||||
if text, ok := contentMap["text"].(string); ok {
|
||||
parts = append(parts, text)
|
||||
}
|
||||
}
|
||||
|
|
@ -579,9 +585,12 @@ func extractResultContent(content any) string {
|
|||
return core.Sprint(content)
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
// truncate clips text to at most maxLen bytes, appending "..." if clipped.
|
||||
//
|
||||
// truncate("hello world", 5) // "hello..."
|
||||
func truncate(text string, maxLen int) string {
|
||||
if len(text) <= maxLen {
|
||||
return text
|
||||
}
|
||||
return s[:max] + "..."
|
||||
return text[:maxLen] + "..."
|
||||
}
|
||||
|
|
|
|||
28
video.go
28
video.go
|
|
@ -40,6 +40,9 @@ func RenderMP4(sess *Session, outputPath string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// generateTape produces a VHS .tape script from session events.
|
||||
//
|
||||
// tape := generateTape(sess, "/tmp/session.mp4")
|
||||
func generateTape(sess *Session, outputPath string) string {
|
||||
builder := core.NewBuilder()
|
||||
|
||||
|
|
@ -53,12 +56,12 @@ func generateTape(sess *Session, outputPath string) string {
|
|||
builder.WriteString("\n")
|
||||
|
||||
// Title frame
|
||||
id := sess.ID
|
||||
if len(id) > 8 {
|
||||
id = id[:8]
|
||||
sessionID := sess.ID
|
||||
if len(sessionID) > 8 {
|
||||
sessionID = sessionID[:8]
|
||||
}
|
||||
builder.WriteString(core.Sprintf("Type \"# Session %s | %s\"\n",
|
||||
id, sess.StartTime.Format("2006-01-02 15:04")))
|
||||
sessionID, sess.StartTime.Format("2006-01-02 15:04")))
|
||||
builder.WriteString("Enter\n")
|
||||
builder.WriteString("Sleep 2s\n")
|
||||
builder.WriteString("\n")
|
||||
|
|
@ -121,14 +124,19 @@ func generateTape(sess *Session, outputPath string) string {
|
|||
return builder.String()
|
||||
}
|
||||
|
||||
// extractCommand strips the description suffix from a Bash input string.
|
||||
//
|
||||
// extractCommand("ls -la # list files") // "ls -la"
|
||||
func extractCommand(input string) string {
|
||||
// Remove description suffix (after " # ")
|
||||
if index := indexOf(input, " # "); index > 0 {
|
||||
return input[:index]
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
// lookupExecutable searches PATH for an executable with the given name.
|
||||
//
|
||||
// lookupExecutable("vhs") // "/usr/local/bin/vhs"
|
||||
func lookupExecutable(name string) string {
|
||||
if name == "" {
|
||||
return ""
|
||||
|
|
@ -152,6 +160,9 @@ func lookupExecutable(name string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// isExecutablePath reports whether filePath is a regular executable file.
|
||||
//
|
||||
// isExecutablePath("/usr/bin/vhs") // true
|
||||
func isExecutablePath(filePath string) bool {
|
||||
statResult := hostFS.Stat(filePath)
|
||||
if !statResult.OK {
|
||||
|
|
@ -164,14 +175,17 @@ func isExecutablePath(filePath string) bool {
|
|||
return info.Mode()&0111 != 0
|
||||
}
|
||||
|
||||
// runCommand executes command with args, inheriting stdio, and waits for exit.
|
||||
//
|
||||
// err := runCommand("/usr/local/bin/vhs", "/tmp/session.tape")
|
||||
func runCommand(command string, args ...string) error {
|
||||
argv := append([]string{command}, args...)
|
||||
arguments := append([]string{command}, args...)
|
||||
procAttr := &syscall.ProcAttr{
|
||||
Env: syscall.Environ(),
|
||||
Files: []uintptr{0, 1, 2},
|
||||
}
|
||||
|
||||
pid, err := syscall.ForkExec(command, argv, procAttr)
|
||||
pid, err := syscall.ForkExec(command, arguments, procAttr)
|
||||
if err != nil {
|
||||
return core.E("runCommand", "fork exec command", err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue