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.
- checksums.yaml +7 -0
- data/.ace-defaults/nav/config.yml +33 -0
- data/.ace-defaults/nav/protocols/guide-sources/ace-support-nav.yml +7 -0
- data/.ace-defaults/nav/protocols/guide.yml +69 -0
- data/.ace-defaults/nav/protocols/prompt.yml +39 -0
- data/.ace-defaults/nav/protocols/skill-sources/ace-support-nav.yml +19 -0
- data/.ace-defaults/nav/protocols/skill.yml +22 -0
- data/.ace-defaults/nav/protocols/tmpl-sources/ace-support-nav.yml +7 -0
- data/.ace-defaults/nav/protocols/tmpl.yml +55 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-support-nav.yml +7 -0
- data/.ace-defaults/nav/protocols/wfi.yml +61 -0
- data/CHANGELOG.md +231 -0
- data/LICENSE +21 -0
- data/README.md +48 -0
- data/Rakefile +12 -0
- data/docs/demo/ace-support-nav-getting-started.gif +0 -0
- data/docs/demo/ace-support-nav-getting-started.tape.yml +28 -0
- data/exe/ace-nav +49 -0
- data/handbook/workflow-instructions/test.wfi.md +14 -0
- data/lib/ace/support/nav/atoms/extension_inferrer.rb +134 -0
- data/lib/ace/support/nav/atoms/gem_resolver.rb +59 -0
- data/lib/ace/support/nav/atoms/path_normalizer.rb +52 -0
- data/lib/ace/support/nav/atoms/uri_parser.rb +62 -0
- data/lib/ace/support/nav/cli/commands/create.rb +114 -0
- data/lib/ace/support/nav/cli/commands/list.rb +122 -0
- data/lib/ace/support/nav/cli/commands/resolve.rb +187 -0
- data/lib/ace/support/nav/cli/commands/sources.rb +112 -0
- data/lib/ace/support/nav/cli.rb +66 -0
- data/lib/ace/support/nav/models/handbook_source.rb +73 -0
- data/lib/ace/support/nav/models/protocol_source.rb +104 -0
- data/lib/ace/support/nav/models/resource.rb +46 -0
- data/lib/ace/support/nav/models/resource_uri.rb +78 -0
- data/lib/ace/support/nav/molecules/config_loader.rb +275 -0
- data/lib/ace/support/nav/molecules/handbook_scanner.rb +204 -0
- data/lib/ace/support/nav/molecules/protocol_scanner.rb +434 -0
- data/lib/ace/support/nav/molecules/resource_resolver.rb +134 -0
- data/lib/ace/support/nav/molecules/source_registry.rb +133 -0
- data/lib/ace/support/nav/organisms/command_delegator.rb +122 -0
- data/lib/ace/support/nav/organisms/navigation_engine.rb +180 -0
- data/lib/ace/support/nav/version.rb +9 -0
- data/lib/ace/support/nav.rb +104 -0
- metadata +228 -0
data/exe/ace-nav
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "ace/support/nav"
|
|
6
|
+
require "ace/support/cli"
|
|
7
|
+
|
|
8
|
+
def translate_legacy_args(args)
|
|
9
|
+
return ["--help"] if args.empty?
|
|
10
|
+
|
|
11
|
+
if args.include?("--sources")
|
|
12
|
+
return ["sources"] + args.reject { |arg| arg == "--sources" }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
return args unless args.include?("--create")
|
|
16
|
+
|
|
17
|
+
create_index = args.index("--create")
|
|
18
|
+
uri = args[create_index + 1]
|
|
19
|
+
target = args[create_index + 2]
|
|
20
|
+
translated = ["create"]
|
|
21
|
+
consumed = [create_index]
|
|
22
|
+
|
|
23
|
+
if uri && !uri.start_with?("-")
|
|
24
|
+
translated << uri
|
|
25
|
+
consumed << create_index + 1
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if target && !target.start_with?("-")
|
|
29
|
+
translated << target
|
|
30
|
+
consumed << create_index + 2
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
args.each_with_index do |arg, index|
|
|
34
|
+
translated << arg unless consumed.include?(index)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
translated
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Start CLI with exception-based exit code handling (per ADR-023)
|
|
41
|
+
begin
|
|
42
|
+
argv = translate_legacy_args(ARGV.dup)
|
|
43
|
+
Ace::Support::Cli::Runner.new(Ace::Support::Nav::CLI).call(args: argv)
|
|
44
|
+
rescue SystemExit => e
|
|
45
|
+
exit(e.status)
|
|
46
|
+
rescue Ace::Support::Cli::Error => e
|
|
47
|
+
warn e.message
|
|
48
|
+
exit(e.exit_code)
|
|
49
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Test Workflow
|
|
2
|
+
|
|
3
|
+
## Goal
|
|
4
|
+
Test the new protocol source registration system.
|
|
5
|
+
|
|
6
|
+
## Process Steps
|
|
7
|
+
1. Register sources via YAML files
|
|
8
|
+
2. Discover sources dynamically
|
|
9
|
+
3. Resolve resources using the new system
|
|
10
|
+
|
|
11
|
+
## Success Criteria
|
|
12
|
+
- [x] Sources can be registered per protocol
|
|
13
|
+
- [x] Multiple sources are discovered
|
|
14
|
+
- [x] Resources are resolved correctly
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Support
|
|
5
|
+
module Nav
|
|
6
|
+
module Atoms
|
|
7
|
+
# Infers file extensions for protocol-based resource resolution
|
|
8
|
+
# Implements DWIM (Do What I Mean) extension inference
|
|
9
|
+
class ExtensionInferrer
|
|
10
|
+
# Default extension inference order when not configured
|
|
11
|
+
DEFAULT_FALLBACK_ORDER = %w[
|
|
12
|
+
protocol_shorthand
|
|
13
|
+
protocol_full
|
|
14
|
+
generic_markdown
|
|
15
|
+
bare
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
class << self
|
|
19
|
+
# Generate candidate extensions for a pattern based on protocol config
|
|
20
|
+
# @param pattern [String] The search pattern (e.g., "markdown-style")
|
|
21
|
+
# @param protocol_extensions [Array<String>] Protocol-specific extensions (e.g., [".g.md", ".guide.md"])
|
|
22
|
+
# @param enabled [Boolean] Whether extension inference is enabled
|
|
23
|
+
# @param fallback_order [Array<String>] Order of extension types to try
|
|
24
|
+
# @return [Array<String>] List of candidate patterns to try
|
|
25
|
+
def infer_extensions(pattern, protocol_extensions: [], enabled: true, fallback_order: DEFAULT_FALLBACK_ORDER)
|
|
26
|
+
return [pattern] unless enabled
|
|
27
|
+
return [pattern] if pattern.empty?
|
|
28
|
+
|
|
29
|
+
# Guard against nil fallback_order
|
|
30
|
+
fallback_order ||= DEFAULT_FALLBACK_ORDER
|
|
31
|
+
|
|
32
|
+
# Extract shorthand extensions from protocol extensions
|
|
33
|
+
# e.g., from [".g.md", ".guide.md"] extract [".g"]
|
|
34
|
+
shorthand_extensions = extract_shorthand_extensions(protocol_extensions)
|
|
35
|
+
|
|
36
|
+
candidates = []
|
|
37
|
+
|
|
38
|
+
fallback_order.each do |fallback_type|
|
|
39
|
+
case fallback_type
|
|
40
|
+
when "protocol_shorthand"
|
|
41
|
+
# Try with shorthand extensions first (e.g., ".g")
|
|
42
|
+
shorthand_extensions.each do |ext|
|
|
43
|
+
candidate = "#{pattern}#{ext}"
|
|
44
|
+
candidates << candidate unless candidates.include?(candidate)
|
|
45
|
+
end
|
|
46
|
+
when "protocol_full"
|
|
47
|
+
# Try with full protocol extensions (e.g., ".g.md", ".guide.md")
|
|
48
|
+
protocol_extensions.each do |ext|
|
|
49
|
+
candidate = "#{pattern}#{ext}"
|
|
50
|
+
candidates << candidate unless candidates.include?(candidate)
|
|
51
|
+
end
|
|
52
|
+
when "generic_markdown"
|
|
53
|
+
# Try with generic markdown extension (e.g., ".md")
|
|
54
|
+
candidate = "#{pattern}.md"
|
|
55
|
+
candidates << candidate unless candidates.include?(candidate)
|
|
56
|
+
when "bare"
|
|
57
|
+
# Try with no extension
|
|
58
|
+
candidates << pattern unless candidates.include?(pattern)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
candidates
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Check if a pattern already includes an extension from the list
|
|
66
|
+
# @param pattern [String] The pattern to check
|
|
67
|
+
# @param extensions [Array<String>] List of extensions to check against
|
|
68
|
+
# @return [Boolean] True if pattern ends with any of the extensions
|
|
69
|
+
def has_extension?(pattern, extensions)
|
|
70
|
+
return false if pattern.nil? || pattern.empty?
|
|
71
|
+
return false if extensions.nil? || extensions.empty?
|
|
72
|
+
|
|
73
|
+
extensions.any? { |ext| pattern.end_with?(ext) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Extract shorthand extensions from full protocol extensions
|
|
77
|
+
# e.g., from [".g.md", ".guide.md"] extract [".g"]
|
|
78
|
+
# @param protocol_extensions [Array<String>] Full protocol extensions
|
|
79
|
+
# @return [Array<String>] Shorthand extensions
|
|
80
|
+
def extract_shorthand_extensions(protocol_extensions)
|
|
81
|
+
return [] if protocol_extensions.nil? || protocol_extensions.empty?
|
|
82
|
+
|
|
83
|
+
shorthand_extensions = []
|
|
84
|
+
|
|
85
|
+
protocol_extensions.each do |ext|
|
|
86
|
+
# Extract the part before the first dot after the initial dot
|
|
87
|
+
# e.g., ".g.md" -> ".g", ".guide.md" -> ".guide"
|
|
88
|
+
parts = ext.split(".")
|
|
89
|
+
if parts.length >= 2
|
|
90
|
+
shorthand = ".#{parts[1]}"
|
|
91
|
+
shorthand_extensions << shorthand unless shorthand_extensions.include?(shorthand)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
shorthand_extensions
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Get the base pattern without any extension
|
|
99
|
+
# @param pattern [String] The pattern to process
|
|
100
|
+
# @param extensions [Array<String>] List of extensions to strip
|
|
101
|
+
# @return [String] Pattern without extension
|
|
102
|
+
def strip_extension(pattern, extensions)
|
|
103
|
+
return pattern if extensions.nil? || extensions.empty?
|
|
104
|
+
|
|
105
|
+
result = pattern.dup
|
|
106
|
+
extensions.each do |ext|
|
|
107
|
+
result = result.sub(/#{Regexp.escape(ext)}\z/, "") if result.end_with?(ext)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
result
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Instance method wrappers for backwards compatibility (deprecated)
|
|
115
|
+
def infer_extensions(...)
|
|
116
|
+
self.class.infer_extensions(...)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def has_extension?(...)
|
|
120
|
+
self.class.has_extension?(...)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def extract_shorthand_extensions(...)
|
|
124
|
+
self.class.extract_shorthand_extensions(...)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def strip_extension(...)
|
|
128
|
+
self.class.strip_extension(...)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rubygems"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Support
|
|
7
|
+
module Nav
|
|
8
|
+
module Atoms
|
|
9
|
+
# Discovers ace-* gems and their handbook paths
|
|
10
|
+
class GemResolver
|
|
11
|
+
def find_ace_gems
|
|
12
|
+
ace_gems = []
|
|
13
|
+
|
|
14
|
+
Gem::Specification.each do |spec|
|
|
15
|
+
next unless spec.name.start_with?("ace-")
|
|
16
|
+
|
|
17
|
+
gem_info = {
|
|
18
|
+
name: spec.name,
|
|
19
|
+
version: spec.version.to_s,
|
|
20
|
+
path: spec.gem_dir,
|
|
21
|
+
handbook_path: File.join(spec.gem_dir, "handbook")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Check if handbook exists
|
|
25
|
+
gem_info[:has_handbook] = Dir.exist?(gem_info[:handbook_path])
|
|
26
|
+
|
|
27
|
+
ace_gems << gem_info
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
ace_gems
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def find_gem_by_name(gem_name)
|
|
34
|
+
spec = Gem::Specification.find_by_name(gem_name)
|
|
35
|
+
return nil unless spec
|
|
36
|
+
|
|
37
|
+
{
|
|
38
|
+
name: spec.name,
|
|
39
|
+
version: spec.version.to_s,
|
|
40
|
+
path: spec.gem_dir,
|
|
41
|
+
handbook_path: File.join(spec.gem_dir, "handbook"),
|
|
42
|
+
has_handbook: Dir.exist?(File.join(spec.gem_dir, "handbook"))
|
|
43
|
+
}
|
|
44
|
+
rescue Gem::LoadError
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def gem_handbook_path(gem_name)
|
|
49
|
+
gem_info = find_gem_by_name(gem_name)
|
|
50
|
+
return nil unless gem_info
|
|
51
|
+
return nil unless gem_info[:has_handbook]
|
|
52
|
+
|
|
53
|
+
gem_info[:handbook_path]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Support
|
|
5
|
+
module Nav
|
|
6
|
+
module Atoms
|
|
7
|
+
# Normalizes and expands paths
|
|
8
|
+
class PathNormalizer
|
|
9
|
+
def normalize(path)
|
|
10
|
+
return nil if path.nil? || path.empty?
|
|
11
|
+
|
|
12
|
+
# Expand home directory
|
|
13
|
+
path = File.expand_path(path) if path.start_with?("~")
|
|
14
|
+
|
|
15
|
+
# Resolve relative paths
|
|
16
|
+
path = File.expand_path(path) unless path.start_with?("/")
|
|
17
|
+
|
|
18
|
+
path
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def join_paths(*parts)
|
|
22
|
+
File.join(*parts.compact)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def dirname(path)
|
|
26
|
+
File.dirname(path)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def basename(path, suffix = nil)
|
|
30
|
+
suffix ? File.basename(path, suffix) : File.basename(path)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def extname(path)
|
|
34
|
+
File.extname(path)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def exists?(path)
|
|
38
|
+
File.exist?(path)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def directory?(path)
|
|
42
|
+
File.directory?(path)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def file?(path)
|
|
46
|
+
File.file?(path)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../molecules/config_loader"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Support
|
|
7
|
+
module Nav
|
|
8
|
+
module Atoms
|
|
9
|
+
# Parses resource URIs
|
|
10
|
+
class UriParser
|
|
11
|
+
def initialize(config_loader: nil)
|
|
12
|
+
@config_loader = config_loader || Molecules::ConfigLoader.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def parse(uri_string)
|
|
16
|
+
return nil unless uri_string.is_a?(String)
|
|
17
|
+
return nil unless uri_string.include?("://")
|
|
18
|
+
|
|
19
|
+
parts = uri_string.split("://", 2)
|
|
20
|
+
protocol = parts[0]
|
|
21
|
+
rest = parts[1]
|
|
22
|
+
|
|
23
|
+
return nil unless valid_protocol?(protocol)
|
|
24
|
+
return {protocol: protocol, source: nil, path: nil} if rest.nil? || rest.empty?
|
|
25
|
+
|
|
26
|
+
# Check for source-specific syntax (@source/path or @source)
|
|
27
|
+
if rest.start_with?("@")
|
|
28
|
+
parse_source_specific(protocol, rest)
|
|
29
|
+
else
|
|
30
|
+
{protocol: protocol, source: nil, path: rest}
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def valid_protocol?(protocol)
|
|
35
|
+
@config_loader.valid_protocol?(protocol)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def valid_protocols
|
|
39
|
+
@config_loader.valid_protocols
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def extract_protocol(uri_string)
|
|
43
|
+
return nil unless uri_string.include?("://")
|
|
44
|
+
uri_string.split("://", 2)[0]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def parse_source_specific(protocol, rest)
|
|
50
|
+
# Format: @source/path or just @source
|
|
51
|
+
if rest.include?("/")
|
|
52
|
+
source_parts = rest.split("/", 2)
|
|
53
|
+
{protocol: protocol, source: source_parts[0], path: source_parts[1]}
|
|
54
|
+
else
|
|
55
|
+
{protocol: protocol, source: rest, path: nil}
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
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 create command
|
|
13
|
+
class Create < Ace::Support::Cli::Command
|
|
14
|
+
include Ace::Support::Cli::Base
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
Create resource from template
|
|
18
|
+
|
|
19
|
+
SYNTAX:
|
|
20
|
+
ace-nav create [URI] [TARGET] [OPTIONS]
|
|
21
|
+
|
|
22
|
+
EXAMPLES:
|
|
23
|
+
|
|
24
|
+
# Create from workflow template
|
|
25
|
+
$ ace-nav create wfi://my-workflow
|
|
26
|
+
|
|
27
|
+
# Create from template to specific file
|
|
28
|
+
$ ace-nav create tmpl://custom ./output.md
|
|
29
|
+
|
|
30
|
+
CONFIGURATION:
|
|
31
|
+
|
|
32
|
+
Global config: ~/.ace/nav/config.yml
|
|
33
|
+
Project config: .ace/nav/config.yml
|
|
34
|
+
Example: ace-support-nav/.ace-defaults/nav/config.yml
|
|
35
|
+
|
|
36
|
+
OUTPUT:
|
|
37
|
+
|
|
38
|
+
Creates resource at specified path or default location
|
|
39
|
+
Exit codes: 0 (success), 1 (error)
|
|
40
|
+
DESC
|
|
41
|
+
|
|
42
|
+
example [
|
|
43
|
+
"wfi://my-workflow # Create from workflow template",
|
|
44
|
+
"tmpl://custom ./output.md # Create from template to file"
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
argument :uri, required: true, desc: "Template URI"
|
|
48
|
+
argument :target, required: false, desc: "Target file path"
|
|
49
|
+
|
|
50
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
51
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
52
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
53
|
+
|
|
54
|
+
def call(uri:, target: nil, **options)
|
|
55
|
+
# Initialize instance variables for use in private methods
|
|
56
|
+
@uri = uri
|
|
57
|
+
@target = target
|
|
58
|
+
@options = options
|
|
59
|
+
@engine = Organisms::NavigationEngine.new
|
|
60
|
+
|
|
61
|
+
execute
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def execute
|
|
65
|
+
display_config_summary
|
|
66
|
+
|
|
67
|
+
result = @engine.create(@uri, @target)
|
|
68
|
+
|
|
69
|
+
if result[:error]
|
|
70
|
+
raise Ace::Support::Cli::Error.new(result[:error])
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
puts "Created: #{result[:created]}"
|
|
74
|
+
puts "From: #{result[:from]}" if @options[:verbose]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def display_config_summary
|
|
80
|
+
return if @options[:quiet]
|
|
81
|
+
|
|
82
|
+
require "ace/core"
|
|
83
|
+
Ace::Core::Atoms::ConfigSummary.display(
|
|
84
|
+
command: "create",
|
|
85
|
+
config: load_effective_config,
|
|
86
|
+
defaults: default_config,
|
|
87
|
+
options: @options,
|
|
88
|
+
quiet: false
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def load_effective_config
|
|
93
|
+
# Use Ace::Support::Nav.config which already handles the cascade
|
|
94
|
+
require_relative "../../../nav"
|
|
95
|
+
Ace::Support::Nav.config
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def default_config
|
|
99
|
+
# Use centralized gem_root from Nav module (avoids path depth duplication)
|
|
100
|
+
defaults_path = File.join(Ace::Support::Nav.gem_root, ".ace-defaults", "nav", "config.yml")
|
|
101
|
+
|
|
102
|
+
if File.exist?(defaults_path)
|
|
103
|
+
require "yaml"
|
|
104
|
+
YAML.safe_load_file(defaults_path, permitted_classes: [Date], aliases: true) || {}
|
|
105
|
+
else
|
|
106
|
+
{}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
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 list command
|
|
13
|
+
class List < Ace::Support::Cli::Command
|
|
14
|
+
include Ace::Support::Cli::Base
|
|
15
|
+
|
|
16
|
+
desc <<~DESC.strip
|
|
17
|
+
List matching resources
|
|
18
|
+
|
|
19
|
+
SYNTAX:
|
|
20
|
+
ace-nav list [PATTERN] [OPTIONS]
|
|
21
|
+
|
|
22
|
+
EXAMPLES:
|
|
23
|
+
|
|
24
|
+
# List all workflows
|
|
25
|
+
$ ace-nav list 'wfi://*'
|
|
26
|
+
|
|
27
|
+
# List templates with pattern
|
|
28
|
+
$ ace-nav list 'tmpl://@ace-*/*'
|
|
29
|
+
|
|
30
|
+
# Tree format
|
|
31
|
+
$ ace-nav list wfi:// --tree
|
|
32
|
+
|
|
33
|
+
CONFIGURATION:
|
|
34
|
+
|
|
35
|
+
Global config: ~/.ace/nav/config.yml
|
|
36
|
+
Project config: .ace/nav/config.yml
|
|
37
|
+
Example: ace-support-nav/.ace-defaults/nav/config.yml
|
|
38
|
+
|
|
39
|
+
OUTPUT:
|
|
40
|
+
|
|
41
|
+
Table format with columns: URI, path, type
|
|
42
|
+
Use --tree for hierarchical format
|
|
43
|
+
Exit codes: 0 (success), 1 (error)
|
|
44
|
+
DESC
|
|
45
|
+
|
|
46
|
+
example [
|
|
47
|
+
"'wfi://*' # List all workflows",
|
|
48
|
+
"'tmpl://@ace-*/*' # List templates with pattern",
|
|
49
|
+
"wfi:// --tree # Tree format"
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
argument :pattern, required: true, desc: "Pattern to match resources"
|
|
53
|
+
|
|
54
|
+
option :tree, type: :boolean, desc: "Display resources in tree format"
|
|
55
|
+
option :verbose, type: :boolean, aliases: %w[-v], desc: "Show verbose output"
|
|
56
|
+
option :quiet, type: :boolean, aliases: %w[-q], desc: "Suppress non-essential output"
|
|
57
|
+
option :debug, type: :boolean, aliases: %w[-d], desc: "Show debug output"
|
|
58
|
+
|
|
59
|
+
def call(pattern:, **options)
|
|
60
|
+
# Initialize instance variables for use in private methods
|
|
61
|
+
@pattern = pattern
|
|
62
|
+
@options = options
|
|
63
|
+
@engine = Organisms::NavigationEngine.new
|
|
64
|
+
|
|
65
|
+
execute
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def execute
|
|
69
|
+
display_config_summary
|
|
70
|
+
|
|
71
|
+
resources = @engine.list(@pattern, tree: @options[:tree], verbose: @options[:verbose])
|
|
72
|
+
|
|
73
|
+
if resources.empty?
|
|
74
|
+
raise Ace::Support::Cli::Error.new("No resources found matching: #{@pattern}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if @options[:verbose]
|
|
78
|
+
require "json"
|
|
79
|
+
puts JSON.pretty_generate(resources)
|
|
80
|
+
else
|
|
81
|
+
resources.each { |resource| puts resource }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def display_config_summary
|
|
88
|
+
return if @options[:quiet]
|
|
89
|
+
|
|
90
|
+
require "ace/core"
|
|
91
|
+
Ace::Core::Atoms::ConfigSummary.display(
|
|
92
|
+
command: "list",
|
|
93
|
+
config: load_effective_config,
|
|
94
|
+
defaults: default_config,
|
|
95
|
+
options: @options,
|
|
96
|
+
quiet: false
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def load_effective_config
|
|
101
|
+
# Use Ace::Support::Nav.config which already handles the cascade
|
|
102
|
+
require_relative "../../../nav"
|
|
103
|
+
Ace::Support::Nav.config
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def default_config
|
|
107
|
+
# Use centralized gem_root from Nav module (avoids path depth duplication)
|
|
108
|
+
defaults_path = File.join(Ace::Support::Nav.gem_root, ".ace-defaults", "nav", "config.yml")
|
|
109
|
+
|
|
110
|
+
if File.exist?(defaults_path)
|
|
111
|
+
require "yaml"
|
|
112
|
+
YAML.safe_load_file(defaults_path, permitted_classes: [Date], aliases: true) || {}
|
|
113
|
+
else
|
|
114
|
+
{}
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|