ace-git-worktree 0.19.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/git/worktree.yml +250 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-git-worktree.yml +19 -0
- data/CHANGELOG.md +957 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +14 -0
- data/docs/demo/ace-git-worktree-getting-started.gif +0 -0
- data/docs/demo/ace-git-worktree-getting-started.tape.yml +28 -0
- data/docs/demo/fixtures/README.md +3 -0
- data/docs/demo/fixtures/sample.txt +1 -0
- data/docs/getting-started.md +114 -0
- data/docs/handbook.md +38 -0
- data/docs/usage.md +334 -0
- data/exe/ace-git-worktree +24 -0
- data/handbook/agents/worktree.ag.md +189 -0
- data/handbook/skills/as-git-worktree/SKILL.md +27 -0
- data/handbook/skills/as-git-worktree-create/SKILL.md +21 -0
- data/handbook/skills/as-git-worktree-manage/SKILL.md +20 -0
- data/handbook/workflow-instructions/git/worktree-create.wf.md +262 -0
- data/handbook/workflow-instructions/git/worktree-manage.wf.md +384 -0
- data/handbook/workflow-instructions/git/worktree.wf.md +224 -0
- data/lib/ace/git/worktree/atoms/git_command.rb +121 -0
- data/lib/ace/git/worktree/atoms/path_expander.rb +189 -0
- data/lib/ace/git/worktree/atoms/slug_generator.rb +235 -0
- data/lib/ace/git/worktree/atoms/task_id_extractor.rb +91 -0
- data/lib/ace/git/worktree/cli/commands/config.rb +50 -0
- data/lib/ace/git/worktree/cli/commands/create.rb +80 -0
- data/lib/ace/git/worktree/cli/commands/list.rb +76 -0
- data/lib/ace/git/worktree/cli/commands/prune.rb +43 -0
- data/lib/ace/git/worktree/cli/commands/remove.rb +48 -0
- data/lib/ace/git/worktree/cli/commands/shared_helpers.rb +66 -0
- data/lib/ace/git/worktree/cli/commands/switch.rb +44 -0
- data/lib/ace/git/worktree/cli.rb +103 -0
- data/lib/ace/git/worktree/commands/config_command.rb +351 -0
- data/lib/ace/git/worktree/commands/create_command.rb +961 -0
- data/lib/ace/git/worktree/commands/list_command.rb +247 -0
- data/lib/ace/git/worktree/commands/prune_command.rb +260 -0
- data/lib/ace/git/worktree/commands/remove_command.rb +522 -0
- data/lib/ace/git/worktree/commands/switch_command.rb +249 -0
- data/lib/ace/git/worktree/configuration.rb +167 -0
- data/lib/ace/git/worktree/models/worktree_config.rb +502 -0
- data/lib/ace/git/worktree/models/worktree_info.rb +303 -0
- data/lib/ace/git/worktree/models/worktree_metadata.rb +294 -0
- data/lib/ace/git/worktree/molecules/config_loader.rb +125 -0
- data/lib/ace/git/worktree/molecules/current_task_linker.rb +136 -0
- data/lib/ace/git/worktree/molecules/hook_executor.rb +361 -0
- data/lib/ace/git/worktree/molecules/parent_task_resolver.rb +186 -0
- data/lib/ace/git/worktree/molecules/pr_creator.rb +253 -0
- data/lib/ace/git/worktree/molecules/task_committer.rb +329 -0
- data/lib/ace/git/worktree/molecules/task_fetcher.rb +244 -0
- data/lib/ace/git/worktree/molecules/task_pusher.rb +183 -0
- data/lib/ace/git/worktree/molecules/task_status_updater.rb +447 -0
- data/lib/ace/git/worktree/molecules/worktree_creator.rb +832 -0
- data/lib/ace/git/worktree/molecules/worktree_lister.rb +337 -0
- data/lib/ace/git/worktree/molecules/worktree_remover.rb +416 -0
- data/lib/ace/git/worktree/organisms/task_worktree_orchestrator.rb +906 -0
- data/lib/ace/git/worktree/organisms/worktree_manager.rb +714 -0
- data/lib/ace/git/worktree/version.rb +9 -0
- data/lib/ace/git/worktree.rb +215 -0
- metadata +218 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Worktree
|
|
6
|
+
module Commands
|
|
7
|
+
# Switch command
|
|
8
|
+
#
|
|
9
|
+
# Switches to a worktree by various identifiers (task ID, branch name,
|
|
10
|
+
# directory name, or path). Outputs the path for navigation.
|
|
11
|
+
#
|
|
12
|
+
# @example Switch by task ID
|
|
13
|
+
# SwitchCommand.new.run(["081"])
|
|
14
|
+
#
|
|
15
|
+
# @example Switch by branch name
|
|
16
|
+
# SwitchCommand.new.run(["feature-branch"])
|
|
17
|
+
#
|
|
18
|
+
# @example Switch and change directory
|
|
19
|
+
# cd $(ace-git-worktree switch 081)
|
|
20
|
+
class SwitchCommand
|
|
21
|
+
# Initialize a new SwitchCommand
|
|
22
|
+
#
|
|
23
|
+
# @param manager [Object] Optional manager dependency for testing
|
|
24
|
+
def initialize(manager: nil)
|
|
25
|
+
@manager = manager || Organisms::WorktreeManager.new
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Run the switch command
|
|
29
|
+
#
|
|
30
|
+
# @param args [Array<String>] Command arguments
|
|
31
|
+
# @return [Integer] Exit code (0 for success, 1 for error)
|
|
32
|
+
def run(args = [])
|
|
33
|
+
options = parse_arguments(args)
|
|
34
|
+
return show_help if options[:help]
|
|
35
|
+
|
|
36
|
+
validate_options(options)
|
|
37
|
+
|
|
38
|
+
# Handle list option
|
|
39
|
+
if options[:list]
|
|
40
|
+
result = @manager.list_all(format: :simple)
|
|
41
|
+
if result[:success] && result[:worktrees].any?
|
|
42
|
+
result[:worktrees].each do |worktree|
|
|
43
|
+
prefix = worktree.task_associated? ? "Task #{worktree.task_id}: " : ""
|
|
44
|
+
puts " #{prefix}#{worktree.branch || "detached"} (#{worktree.path})"
|
|
45
|
+
end
|
|
46
|
+
return 0
|
|
47
|
+
else
|
|
48
|
+
puts "No worktrees found. Use 'ace-git-worktree create' to create one."
|
|
49
|
+
return 0
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
result = @manager.switch(options[:identifier])
|
|
54
|
+
|
|
55
|
+
if result[:success]
|
|
56
|
+
display_switch_result(result, options)
|
|
57
|
+
0
|
|
58
|
+
else
|
|
59
|
+
puts "Failed to switch worktree: #{result[:error]}"
|
|
60
|
+
display_alternatives(options[:identifier]) unless result[:error].include?("not found")
|
|
61
|
+
1
|
|
62
|
+
end
|
|
63
|
+
rescue ArgumentError => e
|
|
64
|
+
puts "Error: #{e.message}"
|
|
65
|
+
puts
|
|
66
|
+
show_help
|
|
67
|
+
1
|
|
68
|
+
rescue => e
|
|
69
|
+
puts "Error: #{e.message}"
|
|
70
|
+
1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Show help for the switch command
|
|
74
|
+
#
|
|
75
|
+
# @return [Integer] Exit code
|
|
76
|
+
def show_help
|
|
77
|
+
puts <<~HELP
|
|
78
|
+
ace-git-worktree switch - Switch to a worktree
|
|
79
|
+
|
|
80
|
+
USAGE:
|
|
81
|
+
ace-git-worktree switch <identifier>
|
|
82
|
+
|
|
83
|
+
IDENTIFIERS:
|
|
84
|
+
Task ID: 081, task.081, v.0.9.0+081
|
|
85
|
+
Branch name: feature-branch, main
|
|
86
|
+
Directory name: task.081, feature-branch
|
|
87
|
+
Full path: /path/to/worktree
|
|
88
|
+
|
|
89
|
+
OPTIONS:
|
|
90
|
+
--help, -h Show this help message
|
|
91
|
+
--list, -l List available worktrees
|
|
92
|
+
--verbose, -v Show detailed information
|
|
93
|
+
|
|
94
|
+
EXAMPLES:
|
|
95
|
+
# Switch by task ID
|
|
96
|
+
ace-git-worktree switch 081
|
|
97
|
+
|
|
98
|
+
# Switch by branch name
|
|
99
|
+
ace-git-worktree switch feature-branch
|
|
100
|
+
|
|
101
|
+
# Switch and change directory
|
|
102
|
+
cd $(ace-git-worktree switch 081)
|
|
103
|
+
|
|
104
|
+
# List available worktrees
|
|
105
|
+
ace-git-worktree switch --list
|
|
106
|
+
|
|
107
|
+
OUTPUT:
|
|
108
|
+
The command outputs the worktree path for use with cd:
|
|
109
|
+
$ ace-git-worktree switch 081
|
|
110
|
+
/project/.ace-wt/task.081
|
|
111
|
+
|
|
112
|
+
To change directory:
|
|
113
|
+
$ cd $(ace-git-worktree switch 081)
|
|
114
|
+
|
|
115
|
+
CONFIGURATION:
|
|
116
|
+
Worktree paths and naming are controlled by .ace/git/worktree.yml
|
|
117
|
+
HELP
|
|
118
|
+
0
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# Parse command line arguments
|
|
124
|
+
#
|
|
125
|
+
# @param args [Array<String>] Command arguments
|
|
126
|
+
# @return [Hash] Parsed options
|
|
127
|
+
def parse_arguments(args)
|
|
128
|
+
options = {
|
|
129
|
+
identifier: nil,
|
|
130
|
+
list: false,
|
|
131
|
+
verbose: false,
|
|
132
|
+
help: false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
i = 0
|
|
136
|
+
while i < args.length
|
|
137
|
+
arg = args[i]
|
|
138
|
+
|
|
139
|
+
case arg
|
|
140
|
+
when "--list", "-l"
|
|
141
|
+
options[:list] = true
|
|
142
|
+
when "--branch"
|
|
143
|
+
i += 1
|
|
144
|
+
options[:identifier] = args[i]
|
|
145
|
+
when "--task"
|
|
146
|
+
i += 1
|
|
147
|
+
options[:identifier] = args[i]
|
|
148
|
+
when "--verbose", "-v"
|
|
149
|
+
options[:verbose] = true
|
|
150
|
+
when "--help", "-h"
|
|
151
|
+
options[:help] = true
|
|
152
|
+
when /^--/
|
|
153
|
+
raise ArgumentError, "Unknown option: #{arg}"
|
|
154
|
+
else
|
|
155
|
+
# Positional argument - worktree identifier
|
|
156
|
+
if options[:identifier]
|
|
157
|
+
raise ArgumentError, "Multiple identifiers specified: #{options[:identifier]} and #{arg}"
|
|
158
|
+
end
|
|
159
|
+
options[:identifier] = arg
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
i += 1
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
options
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Validate parsed options
|
|
169
|
+
#
|
|
170
|
+
# @param options [Hash] Parsed options
|
|
171
|
+
def validate_options(options)
|
|
172
|
+
if !options[:list] && !options[:identifier]
|
|
173
|
+
raise ArgumentError, "Must specify <identifier> or use --list option"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if options[:list] && options[:identifier]
|
|
177
|
+
raise ArgumentError, "Cannot specify both identifier and --list option"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Security validation for identifiers (can be task ID, branch, or path)
|
|
181
|
+
if options[:identifier] && contains_dangerous_patterns?(options[:identifier])
|
|
182
|
+
raise ArgumentError, "Identifier contains potentially dangerous characters"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Check if a string contains dangerous patterns
|
|
187
|
+
#
|
|
188
|
+
# Matches TaskFetcher's validation to ensure consistent security boundaries.
|
|
189
|
+
# Rejects shell metacharacters, null bytes, newlines, redirects, and path traversal.
|
|
190
|
+
#
|
|
191
|
+
# @param value [String] Value to check
|
|
192
|
+
# @return [Boolean] true if dangerous patterns found
|
|
193
|
+
def contains_dangerous_patterns?(value)
|
|
194
|
+
return false if value.nil?
|
|
195
|
+
|
|
196
|
+
# Patterns from TaskFetcher.valid_task_reference? for consistency
|
|
197
|
+
dangerous_patterns = [
|
|
198
|
+
/[;&|`$(){}\[\]]/, # Shell metacharacters
|
|
199
|
+
/\x00/, # Null bytes
|
|
200
|
+
/[\r\n]/, # Newlines
|
|
201
|
+
/[<>]/, # Redirects
|
|
202
|
+
/\.\./ # Path traversal
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
dangerous_patterns.any? { |pattern| value.match?(pattern) }
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Display switch result
|
|
209
|
+
#
|
|
210
|
+
# @param result [Hash] Switch result
|
|
211
|
+
# @param options [Hash] Command options
|
|
212
|
+
def display_switch_result(result, options)
|
|
213
|
+
# Just output the path for use with cd
|
|
214
|
+
puts result[:worktree_path]
|
|
215
|
+
|
|
216
|
+
if options[:verbose]
|
|
217
|
+
puts "\nWorktree details:"
|
|
218
|
+
puts " Branch: #{result[:branch]}"
|
|
219
|
+
puts " Task ID: #{result[:task_id]}" if result[:task_id]
|
|
220
|
+
puts " Description: #{result[:description]}"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Display alternatives when worktree not found
|
|
225
|
+
#
|
|
226
|
+
# @param identifier [String] The identifier that wasn't found
|
|
227
|
+
def display_alternatives(identifier)
|
|
228
|
+
puts "\nAvailable worktrees:"
|
|
229
|
+
|
|
230
|
+
result = @manager.list_all(format: :simple)
|
|
231
|
+
if result[:success] && result[:worktrees].any?
|
|
232
|
+
result[:worktrees].each do |worktree|
|
|
233
|
+
prefix = worktree.task_associated? ? "Task #{worktree.task_id}: " : ""
|
|
234
|
+
puts " #{prefix}#{worktree.branch || "detached"} (#{worktree.path})"
|
|
235
|
+
end
|
|
236
|
+
else
|
|
237
|
+
puts " No worktrees found. Use 'ace-git-worktree create' to create one."
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
puts "\nSuggestions:"
|
|
241
|
+
puts " • Check the worktree identifier spelling"
|
|
242
|
+
puts " • Use 'ace-git-worktree list' to see available worktrees"
|
|
243
|
+
puts " • Use 'ace-git-worktree create' to create a new worktree"
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Git
|
|
5
|
+
module Worktree
|
|
6
|
+
# Configuration module
|
|
7
|
+
#
|
|
8
|
+
# Provides constants and utility methods for the ace-git-worktree gem.
|
|
9
|
+
# Configuration values are loaded from .ace/git/worktree.yml via ace-core cascade.
|
|
10
|
+
# Default values are defined in WorktreeConfig::DEFAULT_CONFIG.
|
|
11
|
+
module Configuration
|
|
12
|
+
# Gem name and identifier
|
|
13
|
+
GEM_NAME = "ace-git-worktree"
|
|
14
|
+
|
|
15
|
+
# File and directory names
|
|
16
|
+
MISE_CONFIG_FILE = "mise.toml"
|
|
17
|
+
CONFIG_FILE = "worktree.yml"
|
|
18
|
+
|
|
19
|
+
# Configuration paths
|
|
20
|
+
CONFIG_NAMESPACE = ["git", "worktree"].freeze
|
|
21
|
+
|
|
22
|
+
# Template variable patterns
|
|
23
|
+
TEMPLATE_VARIABLES = %w[id task_id slug].freeze
|
|
24
|
+
|
|
25
|
+
# Git branch name restrictions
|
|
26
|
+
FORBIDDEN_BRANCH_CHARS = /[~\^:*?\[\]]/
|
|
27
|
+
SEPARATOR_CHARS = /[ ._\/\\]+/
|
|
28
|
+
|
|
29
|
+
# Task status values
|
|
30
|
+
TASK_STATUSES = %w[pending in-progress done blocked].freeze
|
|
31
|
+
|
|
32
|
+
# Task priority values
|
|
33
|
+
TASK_PRIORITIES = %w[high medium low].freeze
|
|
34
|
+
|
|
35
|
+
# Output formats
|
|
36
|
+
OUTPUT_FORMATS = %w[table json simple].freeze
|
|
37
|
+
|
|
38
|
+
# CLI command names and aliases
|
|
39
|
+
CLI_COMMANDS = {
|
|
40
|
+
"create" => "Create a new worktree",
|
|
41
|
+
"list" => "List all worktrees",
|
|
42
|
+
"switch" => "Switch to a worktree",
|
|
43
|
+
"remove" => "Remove a worktree",
|
|
44
|
+
"prune" => "Clean up deleted worktrees",
|
|
45
|
+
"config" => "Show/manage configuration"
|
|
46
|
+
}.freeze
|
|
47
|
+
|
|
48
|
+
CLI_ALIASES = {
|
|
49
|
+
"ls" => "list",
|
|
50
|
+
"rm" => "remove",
|
|
51
|
+
"cd" => "switch"
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# Help text templates
|
|
55
|
+
HELP_TEMPLATES = {
|
|
56
|
+
usage: "ace-git-worktree <command> [OPTIONS]",
|
|
57
|
+
examples: "See 'ace-git-worktree <command> --help' for examples",
|
|
58
|
+
config_help: "See 'ace-git-worktree config --files' for configuration locations"
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# Error messages
|
|
62
|
+
ERROR_MESSAGES = {
|
|
63
|
+
not_git_repo: "Not in a git repository",
|
|
64
|
+
task_not_found: "Task not found",
|
|
65
|
+
worktree_not_found: "Worktree not found",
|
|
66
|
+
config_invalid: "Invalid configuration",
|
|
67
|
+
command_failed: "Command failed",
|
|
68
|
+
unexpected_error: "Unexpected error"
|
|
69
|
+
}.freeze
|
|
70
|
+
|
|
71
|
+
# Success messages
|
|
72
|
+
SUCCESS_MESSAGES = {
|
|
73
|
+
worktree_created: "Worktree created successfully",
|
|
74
|
+
worktree_removed: "Worktree removed successfully",
|
|
75
|
+
config_valid: "Configuration is valid",
|
|
76
|
+
cleanup_completed: "Cleanup completed successfully"
|
|
77
|
+
}.freeze
|
|
78
|
+
|
|
79
|
+
# Warning messages
|
|
80
|
+
WARNING_MESSAGES = {
|
|
81
|
+
mise_trust_failed: "Failed to trust mise configuration",
|
|
82
|
+
task_commit_failed: "Failed to commit task changes",
|
|
83
|
+
metadata_add_failed: "Failed to add worktree metadata",
|
|
84
|
+
uncommitted_changes: "Worktree has uncommitted changes"
|
|
85
|
+
}.freeze
|
|
86
|
+
|
|
87
|
+
# Validate template variables
|
|
88
|
+
#
|
|
89
|
+
# @param template [String] Template string to validate
|
|
90
|
+
# @return [Array<String>] Array of missing variables (empty if valid)
|
|
91
|
+
def self.validate_template_variables(template)
|
|
92
|
+
return [] unless template.is_a?(String)
|
|
93
|
+
|
|
94
|
+
used_variables = template.scan(/\{([^}]+)\}/).flatten
|
|
95
|
+
TEMPLATE_VARIABLES - used_variables
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Validate configuration hash
|
|
99
|
+
#
|
|
100
|
+
# @param config [Hash] Configuration to validate
|
|
101
|
+
# @return [Array<String>] Array of error messages (empty if valid)
|
|
102
|
+
def self.validate_configuration(config)
|
|
103
|
+
errors = []
|
|
104
|
+
|
|
105
|
+
# Validate root_path
|
|
106
|
+
unless config["root_path"].is_a?(String) && !config["root_path"].empty?
|
|
107
|
+
errors << "root_path must be a non-empty string"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Validate task section
|
|
111
|
+
task_config = config["task"] || {}
|
|
112
|
+
|
|
113
|
+
unless task_config["directory_format"].is_a?(String) && !task_config["directory_format"].empty?
|
|
114
|
+
errors << "task.directory_format must be a non-empty string"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
unless task_config["branch_format"].is_a?(String) && !task_config["branch_format"].empty?
|
|
118
|
+
errors << "task.branch_format must be a non-empty string"
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Validate template variables
|
|
122
|
+
[task_config["directory_format"], task_config["branch_format"], task_config["commit_message_format"]].each do |template|
|
|
123
|
+
next unless template.is_a?(String)
|
|
124
|
+
|
|
125
|
+
missing = validate_template_variables(template)
|
|
126
|
+
if missing.any?
|
|
127
|
+
errors << "#{template} should include variables: #{missing.join(", ")}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
errors
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Get command description
|
|
135
|
+
#
|
|
136
|
+
# @param command_name [String] Command name
|
|
137
|
+
# @return [String] Command description or nil if not found
|
|
138
|
+
def self.get_command_description(command_name)
|
|
139
|
+
CLI_COMMANDS[command_name] || CLI_ALIASES[command_name]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Check if command exists
|
|
143
|
+
#
|
|
144
|
+
# @param command_name [String] Command name
|
|
145
|
+
# @return [Boolean] true if command exists
|
|
146
|
+
def self.command_exists?(command_name)
|
|
147
|
+
CLI_COMMANDS.key?(command_name) || CLI_ALIASES.key?(command_name)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Resolve command alias
|
|
151
|
+
#
|
|
152
|
+
# @param command_name [String] Command name or alias
|
|
153
|
+
# @return [String] Resolved command name
|
|
154
|
+
def self.resolve_command_alias(command_name)
|
|
155
|
+
CLI_ALIASES[command_name] || command_name
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Get all available commands
|
|
159
|
+
#
|
|
160
|
+
# @return [Array<String>] Array of command names
|
|
161
|
+
def self.available_commands
|
|
162
|
+
(CLI_COMMANDS.keys + CLI_ALIASES.keys).sort
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|