sxn 0.2.5 → 0.4.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 +4 -4
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +6 -1
- data/bin/sxn-mcp +58 -0
- data/docs/MCP_IMPLEMENTATION.md +425 -0
- data/lib/sxn/CLI.rb +15 -0
- data/lib/sxn/commands/mcp.rb +219 -0
- data/lib/sxn/commands/sessions.rb +111 -4
- data/lib/sxn/commands/templates.rb +226 -0
- data/lib/sxn/commands.rb +2 -0
- data/lib/sxn/config/templates_config.rb +153 -0
- data/lib/sxn/config.rb +1 -0
- data/lib/sxn/core/session_config.rb +6 -1
- data/lib/sxn/core/session_manager.rb +10 -4
- data/lib/sxn/core/template_manager.rb +187 -0
- data/lib/sxn/core.rb +1 -0
- data/lib/sxn/errors.rb +24 -1
- data/lib/sxn/mcp/prompts/workflow_prompts.rb +107 -0
- data/lib/sxn/mcp/resources/session_resources.rb +145 -0
- data/lib/sxn/mcp/server.rb +127 -0
- data/lib/sxn/mcp/tools/base_tool.rb +96 -0
- data/lib/sxn/mcp/tools/projects.rb +144 -0
- data/lib/sxn/mcp/tools/rules.rb +108 -0
- data/lib/sxn/mcp/tools/sessions.rb +375 -0
- data/lib/sxn/mcp/tools/templates.rb +119 -0
- data/lib/sxn/mcp/tools/worktrees.rb +168 -0
- data/lib/sxn/mcp.rb +20 -0
- data/lib/sxn/ui/table.rb +18 -1
- data/lib/sxn/version.rb +1 -1
- data/lib/sxn.rb +1 -0
- data/sxn.gemspec +1 -0
- data/test.txt +1 -0
- metadata +33 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "pathname"
|
|
5
|
+
|
|
6
|
+
module Sxn
|
|
7
|
+
module Config
|
|
8
|
+
# Handles loading and saving session templates from .sxn/templates.yml
|
|
9
|
+
#
|
|
10
|
+
# Templates define collections of projects (worktree configurations)
|
|
11
|
+
# that can be applied when creating a session.
|
|
12
|
+
class TemplatesConfig
|
|
13
|
+
TEMPLATES_FILE = "templates.yml"
|
|
14
|
+
|
|
15
|
+
attr_reader :sxn_path, :templates_file_path
|
|
16
|
+
|
|
17
|
+
def initialize(sxn_path)
|
|
18
|
+
@sxn_path = Pathname.new(sxn_path)
|
|
19
|
+
@templates_file_path = @sxn_path / TEMPLATES_FILE
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Load templates configuration from file
|
|
23
|
+
# @return [Hash] Templates configuration
|
|
24
|
+
def load
|
|
25
|
+
return default_config unless templates_file_path.exist?
|
|
26
|
+
|
|
27
|
+
content = File.read(templates_file_path)
|
|
28
|
+
config = YAML.safe_load(content, permitted_classes: [], permitted_symbols: [], aliases: false) || {}
|
|
29
|
+
normalize_config(config)
|
|
30
|
+
rescue Psych::SyntaxError => e
|
|
31
|
+
raise ConfigurationError, "Invalid YAML in #{templates_file_path}: #{e.message}"
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
raise ConfigurationError, "Failed to load templates file #{templates_file_path}: #{e.message}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Save templates configuration to file
|
|
37
|
+
# @param config [Hash] Templates configuration
|
|
38
|
+
def save(config)
|
|
39
|
+
ensure_directory_exists!
|
|
40
|
+
|
|
41
|
+
# Ensure version is set
|
|
42
|
+
config["version"] ||= 1
|
|
43
|
+
config["templates"] ||= {}
|
|
44
|
+
|
|
45
|
+
File.write(templates_file_path, YAML.dump(stringify_keys(config)))
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Check if templates file exists
|
|
49
|
+
# @return [Boolean]
|
|
50
|
+
def exists?
|
|
51
|
+
templates_file_path.exist?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Get a specific template by name
|
|
55
|
+
# @param name [String] Template name
|
|
56
|
+
# @return [Hash, nil] Template configuration or nil if not found
|
|
57
|
+
def get_template(name)
|
|
58
|
+
config = load
|
|
59
|
+
templates = config["templates"] || {}
|
|
60
|
+
template = templates[name]
|
|
61
|
+
|
|
62
|
+
return nil unless template
|
|
63
|
+
|
|
64
|
+
# Normalize project entries to hashes
|
|
65
|
+
normalize_template(name, template)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# List all template names
|
|
69
|
+
# @return [Array<String>] Template names
|
|
70
|
+
def list_template_names
|
|
71
|
+
config = load
|
|
72
|
+
(config["templates"] || {}).keys
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Add or update a template
|
|
76
|
+
# @param name [String] Template name
|
|
77
|
+
# @param template [Hash] Template configuration
|
|
78
|
+
def set_template(name, template)
|
|
79
|
+
config = load
|
|
80
|
+
config["templates"] ||= {}
|
|
81
|
+
config["templates"][name] = template
|
|
82
|
+
save(config)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Remove a template
|
|
86
|
+
# @param name [String] Template name
|
|
87
|
+
# @return [Boolean] True if template was removed
|
|
88
|
+
def remove_template(name)
|
|
89
|
+
config = load
|
|
90
|
+
templates = config["templates"] || {}
|
|
91
|
+
|
|
92
|
+
return false unless templates.key?(name)
|
|
93
|
+
|
|
94
|
+
templates.delete(name)
|
|
95
|
+
save(config)
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def default_config
|
|
102
|
+
{
|
|
103
|
+
"version" => 1,
|
|
104
|
+
"templates" => {}
|
|
105
|
+
}
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def normalize_config(config)
|
|
109
|
+
config["version"] ||= 1
|
|
110
|
+
config["templates"] ||= {}
|
|
111
|
+
config
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def normalize_template(name, template)
|
|
115
|
+
{
|
|
116
|
+
"name" => name,
|
|
117
|
+
"description" => template["description"],
|
|
118
|
+
"projects" => normalize_projects(template["projects"] || [])
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def normalize_projects(projects)
|
|
123
|
+
projects.map do |project|
|
|
124
|
+
if project.is_a?(String)
|
|
125
|
+
{ "name" => project }
|
|
126
|
+
elsif project.is_a?(Hash)
|
|
127
|
+
# Ensure name key exists
|
|
128
|
+
project["name"] ||= project[:name]
|
|
129
|
+
stringify_keys(project)
|
|
130
|
+
else
|
|
131
|
+
{ "name" => project.to_s }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def stringify_keys(hash)
|
|
137
|
+
return hash unless hash.is_a?(Hash)
|
|
138
|
+
|
|
139
|
+
hash.transform_keys(&:to_s).transform_values do |value|
|
|
140
|
+
case value
|
|
141
|
+
when Hash then stringify_keys(value)
|
|
142
|
+
when Array then value.map { |v| v.is_a?(Hash) ? stringify_keys(v) : v }
|
|
143
|
+
else value
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def ensure_directory_exists!
|
|
149
|
+
FileUtils.mkdir_p(sxn_path) unless sxn_path.exist?
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/sxn/config.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Sxn
|
|
|
15
15
|
@config_path = File.join(session_path, FILENAME)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def create(parent_sxn_path:, default_branch:, session_name:)
|
|
18
|
+
def create(parent_sxn_path:, default_branch:, session_name:, template_id: nil)
|
|
19
19
|
config = {
|
|
20
20
|
"version" => 1,
|
|
21
21
|
"parent_sxn_path" => parent_sxn_path,
|
|
@@ -23,6 +23,7 @@ module Sxn
|
|
|
23
23
|
"session_name" => session_name,
|
|
24
24
|
"created_at" => Time.now.iso8601
|
|
25
25
|
}
|
|
26
|
+
config["template_id"] = template_id if template_id
|
|
26
27
|
File.write(@config_path, YAML.dump(config))
|
|
27
28
|
config
|
|
28
29
|
end
|
|
@@ -51,6 +52,10 @@ module Sxn
|
|
|
51
52
|
read&.dig("session_name")
|
|
52
53
|
end
|
|
53
54
|
|
|
55
|
+
def template_id
|
|
56
|
+
read&.dig("template_id")
|
|
57
|
+
end
|
|
58
|
+
|
|
54
59
|
def project_root
|
|
55
60
|
parent_path = parent_sxn_path
|
|
56
61
|
return nil unless parent_path
|
|
@@ -13,7 +13,7 @@ module Sxn
|
|
|
13
13
|
@database = initialize_database
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def create_session(name, description: nil, linear_task: nil, default_branch: nil)
|
|
16
|
+
def create_session(name, description: nil, linear_task: nil, default_branch: nil, template_id: nil)
|
|
17
17
|
validate_session_name!(name)
|
|
18
18
|
ensure_sessions_folder_exists!
|
|
19
19
|
|
|
@@ -31,10 +31,14 @@ module Sxn
|
|
|
31
31
|
session_config.create(
|
|
32
32
|
parent_sxn_path: @config_manager.sxn_folder_path,
|
|
33
33
|
default_branch: branch,
|
|
34
|
-
session_name: name
|
|
34
|
+
session_name: name,
|
|
35
|
+
template_id: template_id
|
|
35
36
|
)
|
|
36
37
|
|
|
37
|
-
# Create session record
|
|
38
|
+
# Create session record with template_id in metadata
|
|
39
|
+
metadata = {}
|
|
40
|
+
metadata["template_id"] = template_id if template_id
|
|
41
|
+
|
|
38
42
|
session_data = {
|
|
39
43
|
id: session_id,
|
|
40
44
|
name: name,
|
|
@@ -46,7 +50,8 @@ module Sxn
|
|
|
46
50
|
linear_task: linear_task,
|
|
47
51
|
default_branch: branch,
|
|
48
52
|
projects: [],
|
|
49
|
-
worktrees: {}
|
|
53
|
+
worktrees: {},
|
|
54
|
+
metadata: metadata
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
@database.create_session(session_data)
|
|
@@ -244,6 +249,7 @@ module Sxn
|
|
|
244
249
|
description: metadata["description"] || db_row[:description],
|
|
245
250
|
linear_task: metadata["linear_task"] || db_row[:linear_task],
|
|
246
251
|
default_branch: metadata["default_branch"] || db_row[:default_branch],
|
|
252
|
+
template_id: metadata["template_id"],
|
|
247
253
|
# Support both metadata and database columns for backward compatibility
|
|
248
254
|
projects: db_row[:projects] || metadata["projects"] || [],
|
|
249
255
|
worktrees: db_row[:worktrees] || metadata["worktrees"] || {}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../config/templates_config"
|
|
4
|
+
|
|
5
|
+
module Sxn
|
|
6
|
+
module Core
|
|
7
|
+
# Manages session templates - collections of projects that can be
|
|
8
|
+
# applied when creating a session to automatically create multiple worktrees.
|
|
9
|
+
class TemplateManager
|
|
10
|
+
attr_reader :config_manager, :templates_config
|
|
11
|
+
|
|
12
|
+
def initialize(config_manager)
|
|
13
|
+
@config_manager = config_manager
|
|
14
|
+
@templates_config = Config::TemplatesConfig.new(config_manager.sxn_folder_path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# List all available templates
|
|
18
|
+
# @return [Array<Hash>] Array of template info hashes
|
|
19
|
+
def list_templates
|
|
20
|
+
config = templates_config.load
|
|
21
|
+
templates = config["templates"] || {}
|
|
22
|
+
|
|
23
|
+
templates.map do |name, template|
|
|
24
|
+
{
|
|
25
|
+
name: name,
|
|
26
|
+
description: template["description"],
|
|
27
|
+
project_count: (template["projects"] || []).size
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Get a specific template by name
|
|
33
|
+
# @param name [String] Template name
|
|
34
|
+
# @return [Hash] Template configuration
|
|
35
|
+
# @raise [SessionTemplateNotFoundError] If template not found
|
|
36
|
+
def get_template(name)
|
|
37
|
+
template = templates_config.get_template(name)
|
|
38
|
+
|
|
39
|
+
unless template
|
|
40
|
+
available = list_template_names
|
|
41
|
+
raise SessionTemplateNotFoundError.new(name, available: available)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
template
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get list of template names
|
|
48
|
+
# @return [Array<String>] Template names
|
|
49
|
+
def list_template_names
|
|
50
|
+
templates_config.list_template_names
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Validate a template before use
|
|
54
|
+
# @param name [String] Template name
|
|
55
|
+
# @raise [SessionTemplateValidationError] If template is invalid
|
|
56
|
+
# @raise [SessionTemplateNotFoundError] If template not found
|
|
57
|
+
def validate_template(name)
|
|
58
|
+
template = get_template(name)
|
|
59
|
+
projects = template["projects"] || []
|
|
60
|
+
errors = []
|
|
61
|
+
|
|
62
|
+
errors << "Template has no projects defined" if projects.empty?
|
|
63
|
+
|
|
64
|
+
# Validate each project exists in config
|
|
65
|
+
projects.each do |project_config|
|
|
66
|
+
project_name = project_config["name"]
|
|
67
|
+
project = config_manager.get_project(project_name)
|
|
68
|
+
|
|
69
|
+
errors << "Project '#{project_name}' not found in configuration" unless project
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
raise SessionTemplateValidationError.new(name, errors.join("; ")) if errors.any?
|
|
73
|
+
|
|
74
|
+
true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Create a new template
|
|
78
|
+
# @param name [String] Template name
|
|
79
|
+
# @param description [String] Template description
|
|
80
|
+
# @param projects [Array<String>] Array of project names
|
|
81
|
+
# @return [Hash] Created template
|
|
82
|
+
def create_template(name, description: nil, projects: [])
|
|
83
|
+
validate_template_name!(name)
|
|
84
|
+
|
|
85
|
+
raise SessionTemplateValidationError.new(name, "Template already exists") if template_exists?(name)
|
|
86
|
+
|
|
87
|
+
# Validate all projects exist
|
|
88
|
+
projects.each do |project_name|
|
|
89
|
+
project = config_manager.get_project(project_name)
|
|
90
|
+
raise SessionTemplateValidationError.new(name, "Project '#{project_name}' not found") unless project
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
template = {
|
|
94
|
+
"description" => description,
|
|
95
|
+
"projects" => projects.map { |p| { "name" => p } }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
templates_config.set_template(name, template)
|
|
99
|
+
|
|
100
|
+
get_template(name)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Update an existing template
|
|
104
|
+
# @param name [String] Template name
|
|
105
|
+
# @param description [String, nil] New description
|
|
106
|
+
# @param projects [Array<String>, nil] New project list
|
|
107
|
+
# @return [Hash] Updated template
|
|
108
|
+
def update_template(name, description: nil, projects: nil)
|
|
109
|
+
template = get_template(name)
|
|
110
|
+
|
|
111
|
+
# Update description if provided
|
|
112
|
+
template["description"] = description if description
|
|
113
|
+
|
|
114
|
+
# Update projects if provided
|
|
115
|
+
if projects
|
|
116
|
+
# Validate all projects exist
|
|
117
|
+
projects.each do |project_name|
|
|
118
|
+
project = config_manager.get_project(project_name)
|
|
119
|
+
raise SessionTemplateValidationError.new(name, "Project '#{project_name}' not found") unless project
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
template["projects"] = projects.map { |p| { "name" => p } }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Save back to config
|
|
126
|
+
config = templates_config.load
|
|
127
|
+
config["templates"] ||= {}
|
|
128
|
+
config["templates"][name] = {
|
|
129
|
+
"description" => template["description"],
|
|
130
|
+
"projects" => template["projects"]
|
|
131
|
+
}
|
|
132
|
+
templates_config.save(config)
|
|
133
|
+
|
|
134
|
+
get_template(name)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Remove a template
|
|
138
|
+
# @param name [String] Template name
|
|
139
|
+
# @return [Boolean] True if removed
|
|
140
|
+
def remove_template(name)
|
|
141
|
+
# Verify template exists
|
|
142
|
+
get_template(name)
|
|
143
|
+
|
|
144
|
+
templates_config.remove_template(name)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Check if a template exists
|
|
148
|
+
# @param name [String] Template name
|
|
149
|
+
# @return [Boolean]
|
|
150
|
+
def template_exists?(name)
|
|
151
|
+
templates_config.get_template(name) != nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Get project configurations for a template with branch resolution
|
|
155
|
+
# @param name [String] Template name
|
|
156
|
+
# @param default_branch [String] Default branch to use if not specified per-project
|
|
157
|
+
# @return [Array<Hash>] Array of project configs with resolved branches
|
|
158
|
+
def get_template_projects(name, default_branch:)
|
|
159
|
+
template = get_template(name)
|
|
160
|
+
projects = template["projects"] || []
|
|
161
|
+
|
|
162
|
+
projects.map do |project_config|
|
|
163
|
+
project_name = project_config["name"]
|
|
164
|
+
project = config_manager.get_project(project_name)
|
|
165
|
+
|
|
166
|
+
{
|
|
167
|
+
name: project_name,
|
|
168
|
+
path: project[:path],
|
|
169
|
+
branch: project_config["branch"] || default_branch,
|
|
170
|
+
rules: project_config["rules"]
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
private
|
|
176
|
+
|
|
177
|
+
def validate_template_name!(name)
|
|
178
|
+
return if name.match?(/\A[a-zA-Z0-9_-]+\z/)
|
|
179
|
+
|
|
180
|
+
raise SessionTemplateValidationError.new(
|
|
181
|
+
name,
|
|
182
|
+
"Template name must contain only letters, numbers, hyphens, and underscores"
|
|
183
|
+
)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
data/lib/sxn/core.rb
CHANGED
data/lib/sxn/errors.rb
CHANGED
|
@@ -68,11 +68,34 @@ module Sxn
|
|
|
68
68
|
class ApplicationError < Error; end
|
|
69
69
|
class RollbackError < Error; end
|
|
70
70
|
|
|
71
|
-
# Template processing errors
|
|
71
|
+
# Template processing errors (Liquid templates)
|
|
72
72
|
class TemplateError < Error; end
|
|
73
73
|
class TemplateNotFoundError < TemplateError; end
|
|
74
74
|
class TemplateProcessingError < TemplateError; end
|
|
75
75
|
|
|
76
|
+
# Session template errors (worktree templates)
|
|
77
|
+
class SessionTemplateError < Error; end
|
|
78
|
+
|
|
79
|
+
class SessionTemplateNotFoundError < SessionTemplateError
|
|
80
|
+
def initialize(name, available: [])
|
|
81
|
+
message = "Session template '#{name}' not found"
|
|
82
|
+
message += ". Available templates: #{available.join(", ")}" if available.any?
|
|
83
|
+
super(message)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class SessionTemplateValidationError < SessionTemplateError
|
|
88
|
+
def initialize(name, message)
|
|
89
|
+
super("Invalid session template '#{name}': #{message}")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class SessionTemplateApplicationError < SessionTemplateError
|
|
94
|
+
def initialize(template_name, message)
|
|
95
|
+
super("Failed to apply template '#{template_name}': #{message}. Session has been rolled back.")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
76
99
|
# Database errors
|
|
77
100
|
class DatabaseError < Error; end
|
|
78
101
|
class DatabaseConnectionError < DatabaseError; end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sxn
|
|
4
|
+
module MCP
|
|
5
|
+
module Prompts
|
|
6
|
+
# Guided new session creation workflow
|
|
7
|
+
class NewSession < ::MCP::Prompt
|
|
8
|
+
prompt_name "new-session"
|
|
9
|
+
description "Guided workflow for creating a new development session"
|
|
10
|
+
|
|
11
|
+
arguments [
|
|
12
|
+
::MCP::Prompt::Argument.new(
|
|
13
|
+
name: "task_description",
|
|
14
|
+
description: "Brief description of what you're working on",
|
|
15
|
+
required: false
|
|
16
|
+
),
|
|
17
|
+
::MCP::Prompt::Argument.new(
|
|
18
|
+
name: "projects",
|
|
19
|
+
description: "Comma-separated list of projects to include",
|
|
20
|
+
required: false
|
|
21
|
+
)
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def template(args = {}, _server_context: nil)
|
|
26
|
+
# Handle both string and symbol keys
|
|
27
|
+
task_description = args["task_description"] || args[:task_description]
|
|
28
|
+
projects = args["projects"] || args[:projects]
|
|
29
|
+
project_list = projects ? projects.split(",").map(&:strip).join(", ") : "Not specified"
|
|
30
|
+
|
|
31
|
+
<<~PROMPT
|
|
32
|
+
# Create a New Development Session
|
|
33
|
+
|
|
34
|
+
Help me create a new sxn development session.
|
|
35
|
+
|
|
36
|
+
## Task Information
|
|
37
|
+
- Description: #{task_description || "Not provided"}
|
|
38
|
+
- Requested projects: #{project_list}
|
|
39
|
+
|
|
40
|
+
## Steps to Complete
|
|
41
|
+
|
|
42
|
+
1. **Generate session name** based on the task description
|
|
43
|
+
2. **Create the session** using sxn_sessions_create
|
|
44
|
+
3. **Add worktrees** for each requested project using sxn_worktrees_add
|
|
45
|
+
4. **Navigate to the session** using sxn_sessions_swap
|
|
46
|
+
|
|
47
|
+
## Guidelines
|
|
48
|
+
- Session names should be descriptive but concise (e.g., "user-auth", "api-refactor")
|
|
49
|
+
- Use alphanumeric characters, hyphens, and underscores only
|
|
50
|
+
- Apply project rules automatically when creating worktrees
|
|
51
|
+
PROMPT
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Multi-repo setup workflow
|
|
57
|
+
class MultiRepoSetup < ::MCP::Prompt
|
|
58
|
+
prompt_name "multi-repo-setup"
|
|
59
|
+
description "Set up a multi-repository development environment"
|
|
60
|
+
|
|
61
|
+
arguments [
|
|
62
|
+
::MCP::Prompt::Argument.new(
|
|
63
|
+
name: "feature_name",
|
|
64
|
+
description: "Name of the feature being developed across repos",
|
|
65
|
+
required: true
|
|
66
|
+
),
|
|
67
|
+
::MCP::Prompt::Argument.new(
|
|
68
|
+
name: "repos",
|
|
69
|
+
description: "Comma-separated list of repository names to include",
|
|
70
|
+
required: false
|
|
71
|
+
)
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
class << self
|
|
75
|
+
def template(args = {}, _server_context: nil)
|
|
76
|
+
# Handle both string and symbol keys
|
|
77
|
+
feature_name = args["feature_name"] || args[:feature_name]
|
|
78
|
+
repos = args["repos"] || args[:repos]
|
|
79
|
+
repo_list = repos ? repos.split(",").map(&:strip) : []
|
|
80
|
+
|
|
81
|
+
<<~PROMPT
|
|
82
|
+
# Multi-Repository Development Setup
|
|
83
|
+
|
|
84
|
+
Set up a coordinated development environment for: **#{feature_name}**
|
|
85
|
+
|
|
86
|
+
## Repositories to Include
|
|
87
|
+
#{repo_list.empty? ? "- (Will use sxn_projects_list to find available projects)" : repo_list.map { |r| "- #{r}" }.join("\n")}
|
|
88
|
+
|
|
89
|
+
## Setup Process
|
|
90
|
+
|
|
91
|
+
1. **Check registered projects** with sxn_projects_list
|
|
92
|
+
2. **Create the session** with sxn_sessions_create
|
|
93
|
+
- Name: #{feature_name.downcase.gsub(/\s+/, "-")}
|
|
94
|
+
3. **Add worktrees** for each repository with sxn_worktrees_add
|
|
95
|
+
4. **Apply rules** with sxn_rules_apply for each project
|
|
96
|
+
5. **Navigate** using sxn_sessions_swap
|
|
97
|
+
|
|
98
|
+
## Best Practices
|
|
99
|
+
- Use the same branch name across all repos
|
|
100
|
+
- Apply rules to copy environment files
|
|
101
|
+
PROMPT
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|