ace-support-nav 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.ace-defaults/nav/config.yml +33 -0
  3. data/.ace-defaults/nav/protocols/guide-sources/ace-support-nav.yml +7 -0
  4. data/.ace-defaults/nav/protocols/guide.yml +69 -0
  5. data/.ace-defaults/nav/protocols/prompt.yml +39 -0
  6. data/.ace-defaults/nav/protocols/skill-sources/ace-support-nav.yml +19 -0
  7. data/.ace-defaults/nav/protocols/skill.yml +22 -0
  8. data/.ace-defaults/nav/protocols/tmpl-sources/ace-support-nav.yml +7 -0
  9. data/.ace-defaults/nav/protocols/tmpl.yml +55 -0
  10. data/.ace-defaults/nav/protocols/wfi-sources/ace-support-nav.yml +7 -0
  11. data/.ace-defaults/nav/protocols/wfi.yml +61 -0
  12. data/CHANGELOG.md +231 -0
  13. data/LICENSE +21 -0
  14. data/README.md +48 -0
  15. data/Rakefile +12 -0
  16. data/docs/demo/ace-support-nav-getting-started.gif +0 -0
  17. data/docs/demo/ace-support-nav-getting-started.tape.yml +28 -0
  18. data/exe/ace-nav +49 -0
  19. data/handbook/workflow-instructions/test.wfi.md +14 -0
  20. data/lib/ace/support/nav/atoms/extension_inferrer.rb +134 -0
  21. data/lib/ace/support/nav/atoms/gem_resolver.rb +59 -0
  22. data/lib/ace/support/nav/atoms/path_normalizer.rb +52 -0
  23. data/lib/ace/support/nav/atoms/uri_parser.rb +62 -0
  24. data/lib/ace/support/nav/cli/commands/create.rb +114 -0
  25. data/lib/ace/support/nav/cli/commands/list.rb +122 -0
  26. data/lib/ace/support/nav/cli/commands/resolve.rb +187 -0
  27. data/lib/ace/support/nav/cli/commands/sources.rb +112 -0
  28. data/lib/ace/support/nav/cli.rb +66 -0
  29. data/lib/ace/support/nav/models/handbook_source.rb +73 -0
  30. data/lib/ace/support/nav/models/protocol_source.rb +104 -0
  31. data/lib/ace/support/nav/models/resource.rb +46 -0
  32. data/lib/ace/support/nav/models/resource_uri.rb +78 -0
  33. data/lib/ace/support/nav/molecules/config_loader.rb +275 -0
  34. data/lib/ace/support/nav/molecules/handbook_scanner.rb +204 -0
  35. data/lib/ace/support/nav/molecules/protocol_scanner.rb +434 -0
  36. data/lib/ace/support/nav/molecules/resource_resolver.rb +134 -0
  37. data/lib/ace/support/nav/molecules/source_registry.rb +133 -0
  38. data/lib/ace/support/nav/organisms/command_delegator.rb +122 -0
  39. data/lib/ace/support/nav/organisms/navigation_engine.rb +180 -0
  40. data/lib/ace/support/nav/version.rb +9 -0
  41. data/lib/ace/support/nav.rb +104 -0
  42. metadata +228 -0
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "../../molecules/config_loader"
6
+ require_relative "../../organisms/navigation_engine"
7
+ require_relative "../../organisms/command_delegator"
8
+
9
+ module Ace
10
+ module Support
11
+ module Nav
12
+ module CLI
13
+ module Commands
14
+ # ace-support-cli Command class for the resolve command
15
+ class Resolve < Ace::Support::Cli::Command
16
+ include Ace::Support::Cli::Base
17
+
18
+ desc <<~DESC.strip
19
+ Resolve resource path or content
20
+
21
+ SYNTAX:
22
+ ace-nav resolve [URI] [OPTIONS]
23
+
24
+ Automatically detects list mode for wildcards, patterns ending with /,
25
+ and protocol-only URIs (e.g., wfi://, tmpl://).
26
+
27
+ Magic Wildcard Routing:
28
+ Wildcard patterns are auto-routed to 'list' command:
29
+ ace-nav resolve wfi://* → ace-nav list wfi://*
30
+ ace-nav resolve tmpl://@ace-*/* → ace-nav list tmpl://@ace-*
31
+ ace-nav resolve wfi:// → ace-nav list wfi:// (protocol-only)
32
+ Recognized patterns: *, ?, trailing /, protocol-only
33
+
34
+ EXAMPLES:
35
+
36
+ # Resolve URI to path
37
+ $ ace-nav resolve wfi://setup
38
+
39
+ # Display content
40
+ $ ace-nav resolve wfi://setup --content
41
+
42
+ # Wildcard patterns auto-route to list
43
+ $ ace-nav resolve wfi://*
44
+
45
+ # Protocol-only URIs auto-route to list
46
+ $ ace-nav resolve tmpl:///
47
+
48
+ CONFIGURATION:
49
+
50
+ Global config: ~/.ace/nav/config.yml
51
+ Project config: .ace/nav/config.yml
52
+ Example: ace-support-nav/.ace-defaults/nav/config.yml
53
+
54
+ Sources configured via nav.sources in config
55
+
56
+ OUTPUT:
57
+
58
+ By default, displays resolved path
59
+ Use --content to display resource content
60
+ Exit codes: 0 (success), 1 (error)
61
+ DESC
62
+
63
+ example [
64
+ "wfi://setup # Resolve workflow",
65
+ "wfi://* # List workflows (auto-routing)",
66
+ "tmpl:// # List templates (auto-routing)",
67
+ "wfi://setup --content # Display resource content"
68
+ ]
69
+
70
+ argument :uri, required: true, desc: "Resource URI to resolve"
71
+
72
+ option :path, type: :boolean, desc: "Display resource path"
73
+ option :content, type: :boolean, desc: "Display resource content"
74
+ option :tree, type: :boolean, desc: "Display resources in tree format (passed through to cmd protocols)"
75
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
76
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
77
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
78
+
79
+ def call(uri:, **options)
80
+ # Normalize bare protocol names to protocol:// format for listing
81
+ uri = normalize_protocol_shorthand(uri)
82
+
83
+ # Handle magic patterns (wildcards → list)
84
+ if magic_wildcard_pattern?(uri)
85
+ # Delegate to list command
86
+ require_relative "list"
87
+ list_cmd = List.new
88
+ return list_cmd.call(pattern: uri, **options)
89
+ end
90
+
91
+ # Initialize instance variables for use in private methods
92
+ @uri = uri
93
+ @options = options
94
+ @engine = Organisms::NavigationEngine.new
95
+
96
+ execute
97
+ end
98
+
99
+ def execute
100
+ display_config_summary
101
+
102
+ # Check for cmd:// protocol delegation
103
+ if @uri.include?("://")
104
+ protocol = @uri.split("://").first
105
+ if @engine.cmd_protocol?(protocol)
106
+ delegator = Organisms::CommandDelegator.new
107
+ return delegator.delegate(@uri, @options)
108
+ end
109
+ end
110
+
111
+ result = @engine.resolve(@uri, content: @options[:content], verbose: @options[:verbose])
112
+
113
+ if result.nil?
114
+ raise Ace::Support::Cli::Error.new("Resource not found: #{@uri}")
115
+ end
116
+
117
+ if @options[:verbose] && result.is_a?(Hash)
118
+ require "json"
119
+ puts JSON.pretty_generate(result)
120
+ elsif @options[:path]
121
+ # Show path only
122
+ puts result.is_a?(Hash) ? result[:path] || result : result
123
+ else
124
+ puts result
125
+ end
126
+ end
127
+
128
+ private
129
+
130
+ # Normalize bare protocol names (e.g., "wfi") to protocol:// format
131
+ # This allows users to type "ace-nav wfi" instead of "ace-nav wfi://"
132
+ def normalize_protocol_shorthand(uri)
133
+ return uri if uri.include?("://")
134
+
135
+ # Check if input is a known protocol name
136
+ config_loader = Molecules::ConfigLoader.new
137
+ if config_loader.valid_protocol?(uri)
138
+ "#{uri}://"
139
+ else
140
+ uri
141
+ end
142
+ end
143
+
144
+ # Check if URI pattern should trigger list mode
145
+ def magic_wildcard_pattern?(uri)
146
+ return true if uri.include?("*") || uri.include?("?")
147
+ return true if uri.match?(%r{/$})
148
+ return true if uri.match?(/^\w+:\/\/$/)
149
+ false
150
+ end
151
+
152
+ def display_config_summary
153
+ return if @options[:quiet]
154
+
155
+ require "ace/core"
156
+ Ace::Core::Atoms::ConfigSummary.display(
157
+ command: "resolve",
158
+ config: load_effective_config,
159
+ defaults: default_config,
160
+ options: @options,
161
+ quiet: false
162
+ )
163
+ end
164
+
165
+ def load_effective_config
166
+ # Use Ace::Support::Nav.config which already handles the cascade
167
+ require_relative "../../../nav"
168
+ Ace::Support::Nav.config
169
+ end
170
+
171
+ def default_config
172
+ # Use centralized gem_root from Nav module (avoids path depth duplication)
173
+ defaults_path = File.join(Ace::Support::Nav.gem_root, ".ace-defaults", "nav", "config.yml")
174
+
175
+ if File.exist?(defaults_path)
176
+ require "yaml"
177
+ YAML.safe_load_file(defaults_path, permitted_classes: [Date], aliases: true) || {}
178
+ else
179
+ {}
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "../../organisms/navigation_engine"
6
+
7
+ module Ace
8
+ module Support
9
+ module Nav
10
+ module CLI
11
+ module Commands
12
+ # ace-support-cli Command class for the sources command
13
+ class Sources < Ace::Support::Cli::Command
14
+ include Ace::Support::Cli::Base
15
+
16
+ desc <<~DESC.strip
17
+ Show available sources
18
+
19
+ Show all available sources for resources.
20
+
21
+ EXAMPLES:
22
+
23
+ # Show all sources
24
+ $ ace-nav sources
25
+
26
+ # Verbose JSON output
27
+ $ ace-nav sources --verbose
28
+
29
+ CONFIGURATION:
30
+
31
+ Sources configured in: .ace/nav/config.yml
32
+ Global config: ~/.ace/nav/config.yml
33
+ Project config: .ace/nav/config.yml
34
+
35
+ OUTPUT:
36
+
37
+ Table format with source details
38
+ Use --verbose for JSON output
39
+ Exit codes: 0 (success), 1 (error)
40
+ DESC
41
+
42
+ example [
43
+ " # Show all sources",
44
+ "--verbose # Show detailed information (JSON)"
45
+ ]
46
+
47
+ option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
48
+ option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
49
+ option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
50
+
51
+ def call(**options)
52
+ # Initialize instance variables for use in private methods
53
+ @options = options
54
+ @engine = Organisms::NavigationEngine.new
55
+
56
+ execute
57
+ end
58
+
59
+ def execute
60
+ display_config_summary
61
+
62
+ sources = @engine.sources(verbose: @options[:verbose])
63
+
64
+ if @options[:verbose]
65
+ require "json"
66
+ puts JSON.pretty_generate(sources)
67
+ else
68
+ puts "Available sources:"
69
+ sources.each { |source| puts " #{source}" }
70
+ end
71
+
72
+ 0
73
+ end
74
+
75
+ private
76
+
77
+ def display_config_summary
78
+ return if @options[:quiet]
79
+
80
+ require "ace/core"
81
+ Ace::Core::Atoms::ConfigSummary.display(
82
+ command: "sources",
83
+ config: load_effective_config,
84
+ defaults: default_config,
85
+ options: @options,
86
+ quiet: false
87
+ )
88
+ end
89
+
90
+ def load_effective_config
91
+ # Use Ace::Support::Nav.config which already handles the cascade
92
+ require_relative "../../../nav"
93
+ Ace::Support::Nav.config
94
+ end
95
+
96
+ def default_config
97
+ # Use centralized gem_root from Nav module (avoids path depth duplication)
98
+ defaults_path = File.join(Ace::Support::Nav.gem_root, ".ace-defaults", "nav", "config.yml")
99
+
100
+ if File.exist?(defaults_path)
101
+ require "yaml"
102
+ YAML.safe_load_file(defaults_path, permitted_classes: [Date], aliases: true) || {}
103
+ else
104
+ {}
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "../nav"
6
+ # Commands (Hanami pattern: CLI::Commands::)
7
+ require_relative "cli/commands/resolve"
8
+ require_relative "cli/commands/list"
9
+ require_relative "cli/commands/create"
10
+ require_relative "cli/commands/sources"
11
+
12
+ module Ace
13
+ module Support
14
+ module Nav
15
+ # ace-support-cli based CLI registry for ace-nav
16
+ module CLI
17
+ extend Ace::Support::Cli::RegistryDsl
18
+
19
+ PROGRAM_NAME = "ace-nav"
20
+
21
+ REGISTERED_COMMANDS = [
22
+ ["resolve", "Resolve resource path or content"],
23
+ ["list", "List matching resources"],
24
+ ["create", "Create resource from template"],
25
+ ["sources", "Show available sources"]
26
+ ].freeze
27
+
28
+ HELP_EXAMPLES = [
29
+ "ace-nav resolve wfi://task/work # Get workflow file path",
30
+ "ace-nav list 'wfi://*' # Browse all workflows",
31
+ "ace-nav sources # Show registered sources"
32
+ ].freeze
33
+
34
+ # Register the resolve command (default) - Hanami pattern: CLI::Commands::
35
+ register "resolve", CLI::Commands::Resolve.new
36
+
37
+ # Register the list command
38
+ register "list", CLI::Commands::List.new
39
+
40
+ # Register the create command
41
+ register "create", CLI::Commands::Create.new
42
+
43
+ # Register the sources command
44
+ register "sources", CLI::Commands::Sources.new
45
+
46
+ # Register version command
47
+ version_cmd = Ace::Support::Cli::VersionCommand.build(
48
+ gem_name: "ace-support-nav",
49
+ version: Ace::Support::Nav::VERSION
50
+ )
51
+ register "version", version_cmd
52
+ register "--version", version_cmd
53
+
54
+ help_cmd = Ace::Support::Cli::HelpCommand.build(
55
+ program_name: PROGRAM_NAME,
56
+ version: Ace::Support::Nav::VERSION,
57
+ commands: REGISTERED_COMMANDS,
58
+ examples: HELP_EXAMPLES
59
+ )
60
+ register "help", help_cmd
61
+ register "--help", help_cmd
62
+ register "-h", help_cmd
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Nav
6
+ module Models
7
+ # Represents a source of handbook resources
8
+ class HandbookSource
9
+ attr_reader :name, :path, :alias_name, :type, :priority, :resource_root
10
+
11
+ def initialize(name:, path:, alias_name: nil, type: :gem, priority: 100, resource_root: nil)
12
+ @name = name
13
+ @path = path
14
+ @alias_name = alias_name || derive_alias(name, type)
15
+ @type = type # :project, :user, :gem, :custom
16
+ @priority = priority
17
+ @resource_root = resource_root || default_resource_root
18
+ end
19
+
20
+ def project?
21
+ type == :project
22
+ end
23
+
24
+ def user?
25
+ type == :user
26
+ end
27
+
28
+ def gem?
29
+ type == :gem
30
+ end
31
+
32
+ def custom?
33
+ type == :custom
34
+ end
35
+
36
+ def handbook_path
37
+ resource_root
38
+ end
39
+
40
+ def exists?
41
+ Dir.exist?(handbook_path)
42
+ end
43
+
44
+ def to_h
45
+ {
46
+ name: name,
47
+ path: path,
48
+ alias: alias_name,
49
+ type: type,
50
+ priority: priority,
51
+ exists: exists?
52
+ }
53
+ end
54
+
55
+ private
56
+
57
+ def default_resource_root
58
+ gem? ? File.join(path, "handbook") : path
59
+ end
60
+
61
+ def derive_alias(name, type)
62
+ case type
63
+ when :project then "@project"
64
+ when :user then "@user"
65
+ when :gem then "@#{name}"
66
+ else "@#{name.downcase.gsub(/[^a-z0-9-]/, "-")}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Nav
6
+ module Models
7
+ # Represents a source registration for a protocol
8
+ class ProtocolSource
9
+ attr_reader :name, :type, :path, :priority, :description, :origin, :config_file, :alias_name, :config_dir, :config
10
+
11
+ def initialize(name:, type:, path:, priority:, description: nil, origin: nil, config_file: nil, config_dir: nil, config: nil)
12
+ @name = name
13
+ @type = type
14
+ @path = path
15
+ @priority = priority
16
+ @description = description
17
+ @origin = origin
18
+ @config_file = config_file
19
+ @config_dir = config_dir
20
+ @config = config
21
+ @alias_name = "@#{name}"
22
+ end
23
+
24
+ # Get the full path for this source
25
+ def full_path
26
+ case @type
27
+ when "gem"
28
+ # For gem type, resolve through RubyGems
29
+ require "rubygems"
30
+ begin
31
+ spec = Gem::Specification.find_by_name(@name)
32
+ gem_dir = spec.gem_dir
33
+
34
+ # Get relative path from config, or use default
35
+ relative = @config&.dig("relative_path") || "handbook/workflow-instructions"
36
+
37
+ File.join(gem_dir, relative)
38
+ rescue Gem::LoadError => e
39
+ # Gem not found, return a placeholder path
40
+ warn "Gem '#{@name}' not found: #{e.message}" if ENV["VERBOSE"]
41
+ "/gem-not-found/#{@name}"
42
+ end
43
+ else
44
+ # Original path resolution logic for directory/path types
45
+ return path if path&.start_with?("/")
46
+
47
+ if config_dir && path
48
+ # Resolve relative paths from project root (parent of .ace directory)
49
+ project_dir = find_project_root(config_dir)
50
+ if project_dir
51
+ File.expand_path(File.join(project_dir, path))
52
+ else
53
+ File.expand_path(path)
54
+ end
55
+ elsif path
56
+ # Fallback to expand from current directory
57
+ File.expand_path(path)
58
+ else
59
+ # No path specified
60
+ "/no-path-specified"
61
+ end
62
+ end
63
+ end
64
+
65
+ def exists?
66
+ Dir.exist?(full_path)
67
+ end
68
+
69
+ def to_h
70
+ {
71
+ name: name,
72
+ type: type,
73
+ path: path,
74
+ full_path: full_path,
75
+ priority: priority,
76
+ description: description,
77
+ origin: origin,
78
+ exists: exists?
79
+ }
80
+ end
81
+
82
+ def to_s
83
+ "#{name} (#{type}): #{full_path}"
84
+ end
85
+
86
+ private
87
+
88
+ # Walk up from config_dir to find the .ace directory, return its parent (project root)
89
+ # config_dir is like /path/to/project/.ace/protocols/wfi-sources/local.yml
90
+ def find_project_root(dir)
91
+ ace_dir = dir
92
+ while ace_dir && !ace_dir.end_with?("/.ace")
93
+ parent = File.dirname(ace_dir)
94
+ break if parent == ace_dir # Reached filesystem root
95
+ ace_dir = parent
96
+ end
97
+
98
+ File.dirname(ace_dir) if ace_dir&.end_with?("/.ace")
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Support
5
+ module Nav
6
+ module Models
7
+ # Represents a resource within a handbook
8
+ class Resource
9
+ attr_reader :uri, :path, :source, :protocol, :resource_path
10
+
11
+ def initialize(uri:, path:, source:, protocol:, resource_path:)
12
+ @uri = uri
13
+ @path = path
14
+ @source = source
15
+ @protocol = protocol
16
+ @resource_path = resource_path
17
+ end
18
+
19
+ def content
20
+ return nil unless File.exist?(path)
21
+ File.read(path)
22
+ end
23
+
24
+ def exists?
25
+ File.exist?(path)
26
+ end
27
+
28
+ def directory?
29
+ File.directory?(path)
30
+ end
31
+
32
+ def to_h
33
+ {
34
+ uri: uri,
35
+ path: path,
36
+ source: source.to_h,
37
+ protocol: protocol,
38
+ resource_path: resource_path,
39
+ exists: exists?
40
+ }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../molecules/config_loader"
4
+
5
+ module Ace
6
+ module Support
7
+ module Nav
8
+ module Models
9
+ # Represents a parsed resource URI
10
+ class ResourceUri
11
+ attr_reader :protocol, :source, :path, :raw
12
+
13
+ def initialize(raw_uri, config_loader: nil)
14
+ @raw = raw_uri
15
+ @config_loader = config_loader || Molecules::ConfigLoader.new
16
+ parse_uri(raw_uri)
17
+ end
18
+
19
+ def valid?
20
+ !protocol.nil? && @config_loader.valid_protocol?(protocol)
21
+ end
22
+
23
+ def source_specific?
24
+ !source.nil?
25
+ end
26
+
27
+ def cascade_search?
28
+ source.nil?
29
+ end
30
+
31
+ def to_s
32
+ raw
33
+ end
34
+
35
+ def to_h
36
+ {
37
+ raw: raw,
38
+ protocol: protocol,
39
+ source: source,
40
+ path: path,
41
+ source_specific: source_specific?,
42
+ cascade_search: cascade_search?
43
+ }
44
+ end
45
+
46
+ private
47
+
48
+ def parse_uri(raw_uri)
49
+ return unless raw_uri.include?("://")
50
+
51
+ parts = raw_uri.split("://", 2)
52
+ @protocol = parts[0]
53
+
54
+ return unless parts[1]
55
+
56
+ # Check for @ prefix indicating source-specific lookup
57
+ if parts[1].start_with?("@")
58
+ # Extract source and path
59
+ # Format: @source/path or just @source
60
+ if parts[1].include?("/")
61
+ source_parts = parts[1].split("/", 2)
62
+ @source = source_parts[0] # includes @
63
+ @path = source_parts[1]
64
+ else
65
+ @source = parts[1] # just @source
66
+ @path = nil
67
+ end
68
+ else
69
+ # No source specified - cascade search
70
+ @source = nil
71
+ @path = parts[1].empty? ? nil : parts[1]
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end