Skip to content
Closed
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
1 change: 1 addition & 0 deletions cmd/gh-aw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ func init() {
rootCmd.AddCommand(listCmd)
rootCmd.AddCommand(newCmd)
rootCmd.AddCommand(initCmd)
rootCmd.AddCommand(cli.NewUpdateCommand(&verbose))
rootCmd.AddCommand(installCmd)
rootCmd.AddCommand(uninstallCmd)
rootCmd.AddCommand(compileCmd)
Expand Down
225 changes: 217 additions & 8 deletions pkg/cli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,24 @@ func AddWorkflowWithTracking(workflow string, number int, verbose bool, engineOv
content = updateWorkflowTitle(content, i)
}

// Add source field to the frontmatter if the workflow is from a package
if sourceInfo.IsPackage {
sourceField, err := generateSourceField(sourceInfo, workflowPath, verbose)
if err != nil {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to generate source field: %v", err)))
}
} else {
// Add source field to frontmatter
content, err = addSourceToWorkflow(content, sourceField, verbose)
if err != nil {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to add source field: %v", err)))
}
}
}
}

// Track the file based on whether it existed before (if tracker is available)
if tracker != nil {
if fileExists {
Expand Down Expand Up @@ -1321,7 +1339,7 @@ func StatusWorkflows(pattern string, verbose bool) error {
}

// Build table configuration
headers := []string{"Name", "Installed", "Up-to-date", "Status", "Time Remaining"}
headers := []string{"Name", "Installed", "Up-to-date", "Status", "Source", "Update Available"}
var rows [][]string

for _, file := range mdFiles {
Expand All @@ -1337,7 +1355,6 @@ func StatusWorkflows(pattern string, verbose bool) error {
lockFile := strings.TrimSuffix(file, ".md") + ".lock.yml"
compiled := "No"
upToDate := "N/A"
timeRemaining := "N/A"

if _, err := os.Stat(lockFile); err == nil {
compiled = "Yes"
Expand All @@ -1350,11 +1367,6 @@ func StatusWorkflows(pattern string, verbose bool) error {
} else {
upToDate = "Yes"
}

// Extract stop-time from lock file
if stopTime := extractStopTimeFromLockFile(lockFile); stopTime != "" {
timeRemaining = calculateTimeRemaining(stopTime)
}
}

// Get GitHub workflow status
Expand All @@ -1367,8 +1379,30 @@ func StatusWorkflows(pattern string, verbose bool) error {
}
}

// Check for source field and update availability
sourceInfo := "N/A"
updateAvailable := "N/A"

content, err := os.ReadFile(file)
if err == nil {
frontmatter, err := parser.ExtractFrontmatterFromContent(string(content))
if err == nil {
if source, ok := frontmatter.Frontmatter["source"].(string); ok && source != "" {
// Show first 30 chars of source to keep table readable
if len(source) > 30 {
sourceInfo = source[:27] + "..."
} else {
sourceInfo = source
}

// Check if update is available
updateAvailable = checkUpdateAvailable(file, source, verbose)
}
}
}

// Build row data
row := []string{name, compiled, upToDate, status, timeRemaining}
row := []string{name, compiled, upToDate, status, sourceInfo, updateAvailable}
rows = append(rows, row)
}

Expand Down Expand Up @@ -1445,6 +1479,90 @@ func calculateTimeRemaining(stopTimeStr string) string {
}
}

// checkUpdateAvailable checks if an update is available for a workflow with source
func checkUpdateAvailable(workflowPath string, sourceSpec string, verbose bool) string {
// Parse the source spec: "org/repo ref path.md"
parts := strings.Fields(sourceSpec)
if len(parts) != 3 {
return "Invalid"
}

repo := parts[0]
currentRef := parts[1]
workflowFile := parts[2]

// Get packages directory
packagesDir, err := getPackagesDir(false)
if err != nil {
return "Unknown"
}

packagePath := filepath.Join(packagesDir, repo)

// Check if package is installed
if _, err := os.Stat(packagePath); os.IsNotExist(err) {
return "Not installed"
}

// Get current commit SHA from package metadata
packageCommitSHA := readCommitSHAFromMetadata(packagePath)
if packageCommitSHA == "" {
return "Unknown"
}

// Compare with source ref
if currentRef == packageCommitSHA || currentRef == packageCommitSHA[:8] {
// Check if the workflow content is different
sourceFilePath := filepath.Join(packagePath, workflowFile)
sourceContent, err := os.ReadFile(sourceFilePath)
if err != nil {
return "Unknown"
}

localContent, err := os.ReadFile(workflowPath)
if err != nil {
return "Unknown"
}

// Parse both frontmatter
sourceFrontmatter, err := parser.ExtractFrontmatterFromContent(string(sourceContent))
if err != nil {
return "Unknown"
}

localFrontmatter, err := parser.ExtractFrontmatterFromContent(string(localContent))
if err != nil {
return "Unknown"
}

// Compare frontmatter (excluding source field)
localCopy := make(map[string]any)
for k, v := range localFrontmatter.Frontmatter {
if k != "source" {
localCopy[k] = v
}
}

sourceCopy := make(map[string]any)
for k, v := range sourceFrontmatter.Frontmatter {
sourceCopy[k] = v
}

// Convert to YAML for comparison
localYAML, _ := yaml.Marshal(localCopy)
sourceYAML, _ := yaml.Marshal(sourceCopy)

if string(localYAML) != string(sourceYAML) {
return "Yes"
}

return "No"
}

// Ref is different, update available
return "Yes"
}

// Helper functions

func extractWorkflowNameFromFile(filePath string) (string, error) {
Expand Down Expand Up @@ -1484,6 +1602,97 @@ func extractWorkflowNameFromFile(filePath string) (string, error) {
return strings.Join(words, " "), nil
}

// generateSourceField generates the source field value for a workflow from a package
// Format: org/repo <ref> path/to/workflow.md
func generateSourceField(sourceInfo *WorkflowSourceInfo, workflowPath string, verbose bool) (string, error) {
if !sourceInfo.IsPackage {
return "", fmt.Errorf("workflow is not from a package")
}

// Extract org/repo from PackagePath
// PackagePath is typically: ~/.aw/packages/org/repo or .aw/packages/org/repo
packagesDir, err := getPackagesDir(false) // Try global first
if err != nil {
return "", err
}

relPath, err := filepath.Rel(packagesDir, sourceInfo.PackagePath)
if err != nil {
// Try local packages directory
packagesDir, err = getPackagesDir(true)
if err != nil {
return "", err
}
relPath, err = filepath.Rel(packagesDir, sourceInfo.PackagePath)
if err != nil {
return "", fmt.Errorf("failed to determine relative package path: %w", err)
}
}

// relPath should be org/repo
orgRepo := filepath.ToSlash(relPath)

// Get the commit SHA from metadata
commitSHA := readCommitSHAFromMetadata(sourceInfo.PackagePath)
if commitSHA == "" {
commitSHA = "main" // fallback to main if no commit SHA
}

// Get the relative path to the workflow file within the package
workflowRelPath, err := filepath.Rel(sourceInfo.PackagePath, sourceInfo.SourcePath)
if err != nil {
// Use just the filename as fallback
workflowRelPath = filepath.Base(workflowPath)
}
workflowRelPath = filepath.ToSlash(workflowRelPath)

// Format: org/repo <ref> path/to/workflow.md
source := fmt.Sprintf("%s %s %s", orgRepo, commitSHA, workflowRelPath)

if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Generated source field: %s", source)))
}

return source, nil
}

// addSourceToWorkflow adds the source field to a workflow's frontmatter
func addSourceToWorkflow(content, source string, verbose bool) (string, error) {
// Parse the frontmatter
result, err := parser.ExtractFrontmatterFromContent(content)
if err != nil {
return "", fmt.Errorf("failed to parse frontmatter: %w", err)
}

// Ensure frontmatter map exists
if result.Frontmatter == nil {
result.Frontmatter = make(map[string]any)
}

// Add source field
result.Frontmatter["source"] = source

// Convert back to YAML
frontmatterYAML, err := yaml.Marshal(result.Frontmatter)
if err != nil {
return "", fmt.Errorf("failed to marshal frontmatter: %w", err)
}

// Reconstruct the workflow file
var lines []string
lines = append(lines, "---")
frontmatterStr := strings.TrimSuffix(string(frontmatterYAML), "\n")
if frontmatterStr != "" {
lines = append(lines, strings.Split(frontmatterStr, "\n")...)
}
lines = append(lines, "---")
if result.Markdown != "" {
lines = append(lines, result.Markdown)
}

return strings.Join(lines, "\n"), nil
}

func updateWorkflowTitle(content string, number int) string {
// Find and update the first H1 header
lines := strings.Split(content, "\n")
Expand Down
Loading
Loading