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
2 changes: 1 addition & 1 deletion .github/workflows/dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ safe-outputs:

# Poem Generator

@include shared/use-emojis.md
{{#import shared/use-emojis.md}}

{{#if ${{ github.event.inputs.funny }}}}
Be funny and creative! Make the poem humorous and entertaining.
Expand Down
38 changes: 26 additions & 12 deletions docs/src/content/docs/guides/packaging-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,27 +269,23 @@ Import directives allow you to modularize and reuse workflow components across m
### Basic Import Syntax

```aw wrap
@import relative/path/to/file.md
{{#import relative/path/to/file.md}}
```

Imports files relative to the current markdown file's location.

:::note
`@import` and `@include` are aliases - you can use either keyword interchangeably.
:::

### Optional Imports

```aw wrap
@import? relative/path/to/file.md
{{#import? relative/path/to/file.md}}
```

Imports files optionally - if the file doesn't exist, no error occurs and a friendly informational comment is added to the workflow. The optional file will be watched for changes in `gh aw compile --watch` mode, so creating the file later will automatically import it.

### Section-Specific Imports

```aw wrap
@import filename.md#Section
{{#import filename.md#Section}}
```

Imports only a specific section from a markdown file using the section header.
Expand All @@ -315,7 +311,7 @@ tools:
allowed: [get_issue]
---

@import shared/extra-tools.md # Adds more GitHub tools
{{#import: shared/extra-tools.md}} # Adds more GitHub tools
```

```aw wrap
Expand All @@ -332,16 +328,16 @@ tools:

### Import Processing During Add

When adding a workflow with the `add` command, local file references in `@include` directives are automatically converted to workflow specifications:
When adding a workflow with the `add` command, local file references in import directives are automatically converted to workflow specifications:

**Before (in source repository):**
```aw wrap
@include shared/security-notice.md
{{#import: shared/security-notice.md}}
```

**After (in your repository):**
```aw wrap
@include githubnext/agentics/shared/security-notice.md@abc123def
{{#import: githubnext/agentics/shared/security-notice.md@abc123def}}
```

This ensures that included files continue to reference the source repository, maintaining consistency and enabling updates.
Expand Down Expand Up @@ -380,6 +376,24 @@ imports:

This maintains references to the source repository and enables proper version tracking.

### Legacy Syntax (Deprecated)

:::caution[Deprecated]
The `@include` and `@import` syntax is deprecated. Use `{{#import path}}` instead. The old syntax will continue to work but will display deprecation warnings during compilation.

**Migration example:**
```diff
- @include shared/tools.md
+ {{#import shared/tools.md}}

- @include? shared/optional.md
+ {{#import? shared/optional.md}}

- @import shared/config.md#Section
+ {{#import shared/config.md#Section}}
```
:::

## Practical Examples

### Example 1: Adding a Versioned Workflow
Expand Down Expand Up @@ -480,7 +494,7 @@ permissions:

# Issue Analyzer

@include shared/security-notice.md
{{#import: shared/security-notice.md}}

Analyze the issue and provide helpful feedback.
```
Expand Down
14 changes: 8 additions & 6 deletions pkg/cli/imports.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,8 +218,9 @@ func processIncludesWithWorkflowSpec(content string, workflow *WorkflowSpec, com
for nestedScanner.Scan() {
line := nestedScanner.Text()

if matches := parser.IncludeDirectivePattern.FindStringSubmatch(line); matches != nil {
includePath := strings.TrimSpace(matches[2])
directive := parser.ParseImportDirective(line)
if directive != nil {
includePath := directive.Path

// Handle section references
var nestedFilePath string
Expand Down Expand Up @@ -268,10 +269,11 @@ func processIncludesInContent(content string, workflow *WorkflowSpec, commitSHA
for scanner.Scan() {
line := scanner.Text()

// Check if this line is an @include or @import directive
if matches := parser.IncludeDirectivePattern.FindStringSubmatch(line); matches != nil {
isOptional := matches[1] == "?"
includePath := strings.TrimSpace(matches[2])
// Parse import directive
directive := parser.ParseImportDirective(line)
if directive != nil {
isOptional := directive.IsOptional
includePath := directive.Path

// Skip if it's already a workflowspec (contains repo/path format)
if isWorkflowSpecFormat(includePath) {
Expand Down
7 changes: 4 additions & 3 deletions pkg/cli/remove_command.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ func cleanupAllIncludes(verbose bool) error {
return err
}

// findIncludesInContent finds all @include and @import references in content
// findIncludesInContent finds all import references in content
func findIncludesInContent(content, baseDir string, verbose bool) ([]string, error) {
_ = baseDir // unused parameter for now, keeping for potential future use
_ = verbose // unused parameter for now, keeping for potential future use
Expand All @@ -376,8 +376,9 @@ func findIncludesInContent(content, baseDir string, verbose bool) ([]string, err
scanner := bufio.NewScanner(strings.NewReader(content))
for scanner.Scan() {
line := scanner.Text()
if matches := parser.IncludeDirectivePattern.FindStringSubmatch(line); matches != nil {
includePath := strings.TrimSpace(matches[2])
directive := parser.ParseImportDirective(line)
if directive != nil {
includePath := directive.Path

// Handle section references (file.md#Section)
var filePath string
Expand Down
84 changes: 71 additions & 13 deletions pkg/parser/frontmatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,56 @@ import (
"github.com/goccy/go-yaml"
)

// IncludeDirectivePattern matches @include or @import directives
var IncludeDirectivePattern = regexp.MustCompile(`^@(?:include|import)(\?)?\s+(.+)$`)
// IncludeDirectivePattern matches @include, @import (deprecated), or {{#import (new) directives
// The colon after #import is optional and ignored if present
var IncludeDirectivePattern = regexp.MustCompile(`^(?:@(?:include|import)(\?)?\s+(.+)|{{#import(\?)?\s*:?\s*(.+?)\s*}})$`)

// LegacyIncludeDirectivePattern matches only the deprecated @include and @import directives
var LegacyIncludeDirectivePattern = regexp.MustCompile(`^@(?:include|import)(\?)?\s+(.+)$`)

// ImportDirectiveMatch holds the parsed components of an import directive
type ImportDirectiveMatch struct {
IsOptional bool
Path string
IsLegacy bool
Original string
}

// ParseImportDirective parses an import directive and returns its components
func ParseImportDirective(line string) *ImportDirectiveMatch {
trimmedLine := strings.TrimSpace(line)

// Check if it matches the import pattern at all
matches := IncludeDirectivePattern.FindStringSubmatch(trimmedLine)
if matches == nil {
return nil
}

// Check if it's legacy syntax
isLegacy := LegacyIncludeDirectivePattern.MatchString(trimmedLine)

var isOptional bool
var path string

if isLegacy {
// Legacy syntax: @include? path or @import? path
// Group 1: optional marker, Group 2: path
isOptional = matches[1] == "?"
path = strings.TrimSpace(matches[2])
} else {
// New syntax: {{#import?: path}} or {{#import: path}} (colon is optional)
// Group 3: optional marker, Group 4: path
isOptional = matches[3] == "?"
path = strings.TrimSpace(matches[4])
}

return &ImportDirectiveMatch{
IsOptional: isOptional,
Path: path,
IsLegacy: isLegacy,
Original: trimmedLine,
}
}

// isMCPType checks if a type string represents an MCP-compatible type
func isMCPType(typeStr string) bool {
Expand Down Expand Up @@ -379,25 +427,34 @@ func ProcessImportsFromFrontmatterWithManifest(frontmatter map[string]any, baseD
return toolsBuilder.String(), engines, processedFiles, nil
}

// ProcessIncludes processes @include and @import directives in markdown content
// ProcessIncludes processes @include, @import (deprecated), and {{#import: directives in markdown content
// This matches the bash process_includes function behavior
func ProcessIncludes(content, baseDir string, extractTools bool) (string, error) {
visited := make(map[string]bool)
return processIncludesWithVisited(content, baseDir, extractTools, visited)
}

// processIncludesWithVisited processes @include and @import directives with cycle detection
// processIncludesWithVisited processes import directives with cycle detection
func processIncludesWithVisited(content, baseDir string, extractTools bool, visited map[string]bool) (string, error) {
scanner := bufio.NewScanner(strings.NewReader(content))
var result bytes.Buffer

for scanner.Scan() {
line := scanner.Text()

// Check if this line is an @include or @import directive
if matches := IncludeDirectivePattern.FindStringSubmatch(line); matches != nil {
isOptional := matches[1] == "?"
includePath := strings.TrimSpace(matches[2])
// Parse import directive
directive := ParseImportDirective(line)
if directive != nil {
// Emit deprecation warning for legacy syntax
if directive.IsLegacy {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Deprecated syntax: '%s'. Use '{{#import%s %s}}' instead.",
directive.Original,
map[bool]string{true: "?", false: ""}[directive.IsOptional],
directive.Path)))
}

isOptional := directive.IsOptional
includePath := directive.Path

// Handle section references (file.md#Section)
var filePath, sectionName string
Expand Down Expand Up @@ -828,7 +885,7 @@ func ExpandIncludesForEngines(content, baseDir string) ([]string, error) {
return engines, nil
}

// ProcessIncludesForEngines processes @include and @import directives to extract engine configurations
// ProcessIncludesForEngines processes import directives to extract engine configurations
func ProcessIncludesForEngines(content, baseDir string) ([]string, string, error) {
scanner := bufio.NewScanner(strings.NewReader(content))
var result bytes.Buffer
Expand All @@ -837,10 +894,11 @@ func ProcessIncludesForEngines(content, baseDir string) ([]string, string, error
for scanner.Scan() {
line := scanner.Text()

// Check if this line is an @include or @import directive
if matches := IncludeDirectivePattern.FindStringSubmatch(line); matches != nil {
isOptional := matches[1] == "?"
includePath := strings.TrimSpace(matches[2])
// Parse import directive
directive := ParseImportDirective(line)
if directive != nil {
isOptional := directive.IsOptional
includePath := directive.Path

// Handle section references (file.md#Section) - for engines, we ignore sections
var filePath string
Expand Down
Loading