sxn 0.2.5 → 0.3.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 +16 -0
- data/Gemfile.lock +1 -1
- data/lib/sxn/CLI.rb +8 -0
- data/lib/sxn/commands/sessions.rb +114 -4
- data/lib/sxn/commands/templates.rb +230 -0
- data/lib/sxn/commands.rb +1 -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/ui/table.rb +18 -1
- data/lib/sxn/version.rb +1 -1
- metadata +4 -2
- data/sxn.gemspec +0 -85
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 52c6ed94883c7981bdbc6bc0a2faa9d800dbe81fd4af0ee7af41dda777e95421
|
|
4
|
+
data.tar.gz: 1724ced66383451b1d914285b996a8d2c8bc82643fb08a0eb674a8c0c23a6e06
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ec072a5436b75710595263f5667828a83d0c5c42aa44f8cde622550762b30fd1ce3b244636bbf57df70b89eeda2c765abbfd04b6b80ff3c4998358e8bad4afe2
|
|
7
|
+
data.tar.gz: d1d7fa0c3acc68746767376f8f201004c74e8930ce8069b69aa4ad080b7338d4f21da7743cfd704e20746e942ddeca497fc7baa7f81c3a4d7c638e3b6a8437eb
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2025-12-16
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Session templates support for creating sessions from predefined configurations
|
|
12
|
+
- `sxn templates list` to view available templates
|
|
13
|
+
- `sxn templates show <name>` to view template details
|
|
14
|
+
- `--template` option for `sxn sessions add` to create sessions from templates
|
|
15
|
+
- TemplateManager for template operations and validation
|
|
16
|
+
- TemplatesConfig for loading templates from `templates.yml`
|
|
17
|
+
- Session template error classes for better error handling
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Project rules now correctly apply when creating sessions from templates
|
|
21
|
+
|
|
8
22
|
## [0.2.5] - 2025-11-30
|
|
9
23
|
|
|
10
24
|
### Added
|
|
@@ -101,6 +115,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
101
115
|
- Initial placeholder release
|
|
102
116
|
- Basic gem structure
|
|
103
117
|
|
|
118
|
+
[0.3.0]: https://github.com/idl3/sxn/compare/v0.2.5...v0.3.0
|
|
119
|
+
[0.2.5]: https://github.com/idl3/sxn/compare/v0.2.4...v0.2.5
|
|
104
120
|
[0.2.4]: https://github.com/idl3/sxn/compare/v0.2.3...v0.2.4
|
|
105
121
|
[0.2.3]: https://github.com/idl3/sxn/compare/v0.2.1...v0.2.3
|
|
106
122
|
[0.2.1]: https://github.com/idl3/sxn/compare/v0.2.0...v0.2.1
|
data/Gemfile.lock
CHANGED
data/lib/sxn/CLI.rb
CHANGED
|
@@ -42,6 +42,7 @@ module Sxn
|
|
|
42
42
|
option :description, type: :string, aliases: "-d", desc: "Session description"
|
|
43
43
|
option :linear_task, type: :string, aliases: "-l", desc: "Linear task ID"
|
|
44
44
|
option :branch, type: :string, aliases: "-b", desc: "Default branch for worktrees"
|
|
45
|
+
option :template, type: :string, aliases: "-t", desc: "Template to use for worktree creation"
|
|
45
46
|
def add(session_name)
|
|
46
47
|
cmd = Commands::Sessions.new
|
|
47
48
|
cmd.options = options
|
|
@@ -217,6 +218,13 @@ module Sxn
|
|
|
217
218
|
handle_error(e)
|
|
218
219
|
end
|
|
219
220
|
|
|
221
|
+
desc "templates SUBCOMMAND", "Manage session templates"
|
|
222
|
+
def templates(subcommand = nil, *args)
|
|
223
|
+
Commands::Templates.start([subcommand, *args].compact)
|
|
224
|
+
rescue Sxn::Error => e
|
|
225
|
+
handle_error(e)
|
|
226
|
+
end
|
|
227
|
+
|
|
220
228
|
desc "status", "Show overall sxn status"
|
|
221
229
|
def status
|
|
222
230
|
show_status
|
|
@@ -19,12 +19,14 @@ module Sxn
|
|
|
19
19
|
@session_manager = Sxn::Core::SessionManager.new(@config_manager)
|
|
20
20
|
@project_manager = Sxn::Core::ProjectManager.new(@config_manager)
|
|
21
21
|
@worktree_manager = Sxn::Core::WorktreeManager.new(@config_manager, @session_manager)
|
|
22
|
+
@template_manager = Sxn::Core::TemplateManager.new(@config_manager)
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
desc "add NAME", "Create a new session"
|
|
25
26
|
option :description, type: :string, aliases: "-d", desc: "Session description"
|
|
26
27
|
option :linear_task, type: :string, aliases: "-l", desc: "Linear task ID"
|
|
27
28
|
option :branch, type: :string, aliases: "-b", desc: "Default branch for worktrees"
|
|
29
|
+
option :template, type: :string, aliases: "-t", desc: "Template to use for worktree creation"
|
|
28
30
|
option :activate, type: :boolean, default: true, desc: "Activate session after creation"
|
|
29
31
|
option :skip_worktree, type: :boolean, default: false, desc: "Skip worktree creation wizard"
|
|
30
32
|
|
|
@@ -37,9 +39,29 @@ module Sxn
|
|
|
37
39
|
name = @prompt.session_name(existing_sessions: existing_sessions)
|
|
38
40
|
end
|
|
39
41
|
|
|
42
|
+
template_id = options[:template]
|
|
43
|
+
|
|
44
|
+
# Validate template if specified
|
|
45
|
+
if template_id
|
|
46
|
+
begin
|
|
47
|
+
@template_manager.validate_template(template_id)
|
|
48
|
+
rescue Sxn::SessionTemplateNotFoundError, Sxn::SessionTemplateValidationError => e
|
|
49
|
+
@ui.error(e.message)
|
|
50
|
+
exit(1)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
40
54
|
# Get default branch - use provided option, or prompt interactively
|
|
55
|
+
# Branch is required when using a template
|
|
41
56
|
default_branch = options[:branch]
|
|
42
|
-
default_branch
|
|
57
|
+
if default_branch.nil?
|
|
58
|
+
if template_id
|
|
59
|
+
@ui.error("Branch is required when using a template. Use -b/--branch to specify.")
|
|
60
|
+
exit(1)
|
|
61
|
+
else
|
|
62
|
+
default_branch = @prompt.default_branch(session_name: name)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
43
65
|
|
|
44
66
|
begin
|
|
45
67
|
@ui.progress_start("Creating session '#{name}'")
|
|
@@ -48,7 +70,8 @@ module Sxn
|
|
|
48
70
|
name,
|
|
49
71
|
description: options[:description],
|
|
50
72
|
linear_task: options[:linear_task],
|
|
51
|
-
default_branch: default_branch
|
|
73
|
+
default_branch: default_branch,
|
|
74
|
+
template_id: template_id
|
|
52
75
|
)
|
|
53
76
|
|
|
54
77
|
@ui.progress_done
|
|
@@ -60,8 +83,13 @@ module Sxn
|
|
|
60
83
|
|
|
61
84
|
display_session_info(session)
|
|
62
85
|
|
|
63
|
-
#
|
|
64
|
-
|
|
86
|
+
# Apply template if specified (with atomic rollback on failure)
|
|
87
|
+
if template_id
|
|
88
|
+
apply_template_to_session(name, template_id, default_branch)
|
|
89
|
+
elsif !options[:skip_worktree]
|
|
90
|
+
# Offer to add a worktree unless skipped or template was used
|
|
91
|
+
offer_worktree_wizard(name)
|
|
92
|
+
end
|
|
65
93
|
rescue Sxn::Error => e
|
|
66
94
|
@ui.progress_failed
|
|
67
95
|
@ui.error(e.message)
|
|
@@ -275,6 +303,88 @@ module Sxn
|
|
|
275
303
|
|
|
276
304
|
private
|
|
277
305
|
|
|
306
|
+
def apply_template_to_session(session_name, template_id, default_branch)
|
|
307
|
+
template = @template_manager.get_template(template_id)
|
|
308
|
+
projects = template["projects"] || []
|
|
309
|
+
created_worktrees = []
|
|
310
|
+
|
|
311
|
+
@ui.newline
|
|
312
|
+
@ui.section("Applying Template '#{template_id}'")
|
|
313
|
+
@ui.info("Creating #{projects.size} worktree(s)...")
|
|
314
|
+
|
|
315
|
+
begin
|
|
316
|
+
projects.each_with_index do |project_config, index|
|
|
317
|
+
project_name = project_config["name"]
|
|
318
|
+
# Use project-specific branch override, or fall back to session default
|
|
319
|
+
branch = project_config["branch"] || default_branch
|
|
320
|
+
|
|
321
|
+
@ui.progress_start("Creating worktree #{index + 1}/#{projects.size}: #{project_name}")
|
|
322
|
+
|
|
323
|
+
worktree = @worktree_manager.add_worktree(
|
|
324
|
+
project_name,
|
|
325
|
+
branch,
|
|
326
|
+
session_name: session_name
|
|
327
|
+
)
|
|
328
|
+
created_worktrees << worktree
|
|
329
|
+
|
|
330
|
+
@ui.progress_done
|
|
331
|
+
|
|
332
|
+
# Apply project's default rules first
|
|
333
|
+
apply_project_rules(project_name, session_name)
|
|
334
|
+
|
|
335
|
+
# Apply template-specific rule overrides if defined (in addition to project defaults)
|
|
336
|
+
apply_template_rules(session_name, project_name, worktree[:path], project_config["rules"]) if project_config["rules"]
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
@ui.newline
|
|
340
|
+
@ui.success("Template applied: #{created_worktrees.size} worktree(s) created")
|
|
341
|
+
|
|
342
|
+
# Display created worktrees
|
|
343
|
+
@ui.newline
|
|
344
|
+
@ui.subsection("Created Worktrees")
|
|
345
|
+
created_worktrees.each do |wt|
|
|
346
|
+
@ui.list_item("#{wt[:project]} (#{wt[:branch]}) → #{wt[:path]}")
|
|
347
|
+
end
|
|
348
|
+
rescue StandardError => e
|
|
349
|
+
# ATOMIC ROLLBACK: Remove all created worktrees and the session
|
|
350
|
+
@ui.progress_failed
|
|
351
|
+
@ui.newline
|
|
352
|
+
@ui.warning("Template application failed, rolling back...")
|
|
353
|
+
|
|
354
|
+
rollback_template_application(session_name, created_worktrees)
|
|
355
|
+
|
|
356
|
+
raise Sxn::SessionTemplateApplicationError.new(template_id, e.message)
|
|
357
|
+
end
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def rollback_template_application(session_name, created_worktrees)
|
|
361
|
+
# Remove created worktrees
|
|
362
|
+
created_worktrees.each do |wt|
|
|
363
|
+
@worktree_manager.remove_worktree(wt[:project], session_name: session_name)
|
|
364
|
+
rescue StandardError
|
|
365
|
+
# Best effort cleanup, continue with rollback
|
|
366
|
+
nil
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Remove the session
|
|
370
|
+
@session_manager.remove_session(session_name, force: true)
|
|
371
|
+
@ui.info("Session '#{session_name}' has been rolled back")
|
|
372
|
+
rescue StandardError => e
|
|
373
|
+
@ui.warning("Rollback encountered errors: #{e.message}")
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def apply_template_rules(_session_name, project_name, worktree_path, rules)
|
|
377
|
+
return unless rules.is_a?(Hash)
|
|
378
|
+
|
|
379
|
+
rules_manager = Sxn::Core::RulesManager.new(@config_manager, worktree_path)
|
|
380
|
+
|
|
381
|
+
rules_manager.apply_copy_files_rules(rules["copy_files"]) if rules["copy_files"]
|
|
382
|
+
|
|
383
|
+
rules_manager.apply_setup_commands(rules["setup_commands"]) if rules["setup_commands"]
|
|
384
|
+
rescue StandardError => e
|
|
385
|
+
@ui.warning("Failed to apply rules for #{project_name}: #{e.message}")
|
|
386
|
+
end
|
|
387
|
+
|
|
278
388
|
def ensure_initialized!
|
|
279
389
|
return if @config_manager.initialized?
|
|
280
390
|
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module Sxn
|
|
6
|
+
module Commands
|
|
7
|
+
# Manage session templates
|
|
8
|
+
class Templates < Thor
|
|
9
|
+
include Thor::Actions
|
|
10
|
+
|
|
11
|
+
def initialize(args = ARGV, local_options = {}, config = {})
|
|
12
|
+
super
|
|
13
|
+
@ui = Sxn::UI::Output.new
|
|
14
|
+
@prompt = Sxn::UI::Prompt.new
|
|
15
|
+
@table = Sxn::UI::Table.new
|
|
16
|
+
@config_manager = Sxn::Core::ConfigManager.new
|
|
17
|
+
@template_manager = Sxn::Core::TemplateManager.new(@config_manager)
|
|
18
|
+
@project_manager = Sxn::Core::ProjectManager.new(@config_manager)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc "list", "List available session templates"
|
|
22
|
+
|
|
23
|
+
def list
|
|
24
|
+
ensure_initialized!
|
|
25
|
+
|
|
26
|
+
templates = @template_manager.list_templates
|
|
27
|
+
|
|
28
|
+
@ui.section("Session Templates")
|
|
29
|
+
|
|
30
|
+
if templates.empty?
|
|
31
|
+
@ui.empty_state("No templates defined")
|
|
32
|
+
@ui.newline
|
|
33
|
+
@ui.recovery_suggestion("Create a template with 'sxn templates create'")
|
|
34
|
+
@ui.recovery_suggestion("Or manually edit .sxn/templates.yml")
|
|
35
|
+
else
|
|
36
|
+
display_templates_table(templates)
|
|
37
|
+
@ui.newline
|
|
38
|
+
@ui.info("Total: #{templates.size} template(s)")
|
|
39
|
+
end
|
|
40
|
+
rescue Sxn::Error => e
|
|
41
|
+
@ui.error(e.message)
|
|
42
|
+
exit(e.exit_code)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "show NAME", "Show details of a session template"
|
|
46
|
+
|
|
47
|
+
def show(name)
|
|
48
|
+
ensure_initialized!
|
|
49
|
+
|
|
50
|
+
template = @template_manager.get_template(name)
|
|
51
|
+
|
|
52
|
+
@ui.section("Template: #{name}")
|
|
53
|
+
@ui.key_value("Description", template["description"] || "(none)")
|
|
54
|
+
@ui.key_value("Projects", (template["projects"] || []).size.to_s)
|
|
55
|
+
|
|
56
|
+
@ui.newline
|
|
57
|
+
@ui.subsection("Projects")
|
|
58
|
+
|
|
59
|
+
projects = template["projects"] || []
|
|
60
|
+
if projects.empty?
|
|
61
|
+
@ui.empty_state("No projects in this template")
|
|
62
|
+
else
|
|
63
|
+
projects.each do |project_config|
|
|
64
|
+
project_name = project_config["name"]
|
|
65
|
+
project = begin
|
|
66
|
+
@project_manager.get_project(project_name)
|
|
67
|
+
rescue Sxn::ProjectNotFoundError
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
status = project ? "✓" : "✗ (not found)"
|
|
71
|
+
|
|
72
|
+
details = []
|
|
73
|
+
details << "branch: #{project_config["branch"]}" if project_config["branch"]
|
|
74
|
+
details << "has custom rules" if project_config["rules"]
|
|
75
|
+
|
|
76
|
+
if details.any?
|
|
77
|
+
@ui.list_item("#{project_name} #{status} (#{details.join(", ")})")
|
|
78
|
+
else
|
|
79
|
+
@ui.list_item("#{project_name} #{status}")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
@ui.newline
|
|
85
|
+
@ui.subsection("Usage")
|
|
86
|
+
@ui.command_example(
|
|
87
|
+
"sxn add my-session -t #{name} -b my-branch",
|
|
88
|
+
"Create session with this template"
|
|
89
|
+
)
|
|
90
|
+
rescue Sxn::SessionTemplateNotFoundError => e
|
|
91
|
+
@ui.error(e.message)
|
|
92
|
+
exit(1)
|
|
93
|
+
rescue Sxn::Error => e
|
|
94
|
+
@ui.error(e.message)
|
|
95
|
+
exit(e.exit_code)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
desc "create", "Create a new session template"
|
|
99
|
+
option :name, type: :string, aliases: "-n", desc: "Template name"
|
|
100
|
+
option :description, type: :string, aliases: "-d", desc: "Template description"
|
|
101
|
+
|
|
102
|
+
def create
|
|
103
|
+
ensure_initialized!
|
|
104
|
+
|
|
105
|
+
# Get template name
|
|
106
|
+
name = options[:name]
|
|
107
|
+
if name.nil?
|
|
108
|
+
name = @prompt.ask("Template name:")
|
|
109
|
+
if name.nil? || name.strip.empty?
|
|
110
|
+
@ui.error("Template name is required")
|
|
111
|
+
exit(1)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check if template already exists
|
|
116
|
+
if @template_manager.template_exists?(name)
|
|
117
|
+
@ui.error("Template '#{name}' already exists")
|
|
118
|
+
@ui.recovery_suggestion("Use a different name or remove existing template with 'sxn templates remove #{name}'")
|
|
119
|
+
exit(1)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Get description
|
|
123
|
+
description = options[:description]
|
|
124
|
+
description ||= @prompt.ask("Description (optional):")
|
|
125
|
+
|
|
126
|
+
# Select projects
|
|
127
|
+
projects = @project_manager.list_projects
|
|
128
|
+
if projects.empty?
|
|
129
|
+
@ui.error("No projects configured")
|
|
130
|
+
@ui.recovery_suggestion("Add projects with 'sxn projects add <name> <path>' first")
|
|
131
|
+
exit(1)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
@ui.newline
|
|
135
|
+
@ui.info("Select projects to include in the template:")
|
|
136
|
+
@ui.info("(Use space to select, enter to confirm)")
|
|
137
|
+
@ui.newline
|
|
138
|
+
|
|
139
|
+
project_choices = projects.map do |p|
|
|
140
|
+
{ name: "#{p[:name]} (#{p[:type] || "unknown"})", value: p[:name] }
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
selected_projects = @prompt.multi_select("Projects:", project_choices)
|
|
144
|
+
|
|
145
|
+
if selected_projects.empty?
|
|
146
|
+
@ui.error("At least one project must be selected")
|
|
147
|
+
exit(1)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Create the template
|
|
151
|
+
@ui.progress_start("Creating template '#{name}'")
|
|
152
|
+
|
|
153
|
+
@template_manager.create_template(
|
|
154
|
+
name,
|
|
155
|
+
description: description,
|
|
156
|
+
projects: selected_projects
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
@ui.progress_done
|
|
160
|
+
@ui.success("Template '#{name}' created with #{selected_projects.size} project(s)")
|
|
161
|
+
|
|
162
|
+
@ui.newline
|
|
163
|
+
@ui.subsection("Usage")
|
|
164
|
+
@ui.command_example(
|
|
165
|
+
"sxn add my-session -t #{name} -b my-branch",
|
|
166
|
+
"Create session with this template"
|
|
167
|
+
)
|
|
168
|
+
rescue Sxn::SessionTemplateValidationError => e
|
|
169
|
+
@ui.progress_failed
|
|
170
|
+
@ui.error(e.message)
|
|
171
|
+
exit(1)
|
|
172
|
+
rescue Sxn::Error => e
|
|
173
|
+
@ui.progress_failed
|
|
174
|
+
@ui.error(e.message)
|
|
175
|
+
exit(e.exit_code)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
desc "remove NAME", "Remove a session template"
|
|
179
|
+
option :force, type: :boolean, aliases: "-f", desc: "Skip confirmation"
|
|
180
|
+
|
|
181
|
+
def remove(name = nil)
|
|
182
|
+
ensure_initialized!
|
|
183
|
+
|
|
184
|
+
# Interactive selection if name not provided
|
|
185
|
+
if name.nil?
|
|
186
|
+
templates = @template_manager.list_templates
|
|
187
|
+
if templates.empty?
|
|
188
|
+
@ui.empty_state("No templates to remove")
|
|
189
|
+
return
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
choices = templates.map { |t| { name: "#{t[:name]} - #{t[:description] || "(no description)"}", value: t[:name] } }
|
|
193
|
+
name = @prompt.select("Select template to remove:", choices)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Verify template exists
|
|
197
|
+
@template_manager.get_template(name)
|
|
198
|
+
|
|
199
|
+
# Confirm deletion unless force flag is used
|
|
200
|
+
return if !options[:force] && !@prompt.confirm_deletion(name, "template")
|
|
201
|
+
|
|
202
|
+
@ui.progress_start("Removing template '#{name}'")
|
|
203
|
+
@template_manager.remove_template(name)
|
|
204
|
+
@ui.progress_done
|
|
205
|
+
@ui.success("Template '#{name}' removed")
|
|
206
|
+
rescue Sxn::SessionTemplateNotFoundError => e
|
|
207
|
+
@ui.error(e.message)
|
|
208
|
+
exit(1)
|
|
209
|
+
rescue Sxn::Error => e
|
|
210
|
+
@ui.progress_failed
|
|
211
|
+
@ui.error(e.message)
|
|
212
|
+
exit(e.exit_code)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
private
|
|
216
|
+
|
|
217
|
+
def ensure_initialized!
|
|
218
|
+
return if @config_manager.initialized?
|
|
219
|
+
|
|
220
|
+
@ui.error("Project not initialized")
|
|
221
|
+
@ui.recovery_suggestion("Run 'sxn init' to initialize sxn in this project")
|
|
222
|
+
exit(1)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def display_templates_table(templates)
|
|
226
|
+
@table.templates(templates)
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
data/lib/sxn/commands.rb
CHANGED
|
@@ -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
|
data/lib/sxn/ui/table.rb
CHANGED
|
@@ -89,6 +89,21 @@ module Sxn
|
|
|
89
89
|
render_table(headers, rows)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
+
def templates(templates)
|
|
93
|
+
return empty_table("No templates defined") if templates.empty?
|
|
94
|
+
|
|
95
|
+
headers = %w[Name Description Projects]
|
|
96
|
+
rows = templates.map do |template|
|
|
97
|
+
[
|
|
98
|
+
template[:name],
|
|
99
|
+
template[:description] || "-",
|
|
100
|
+
template[:project_count].to_s
|
|
101
|
+
]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
render_table(headers, rows)
|
|
105
|
+
end
|
|
106
|
+
|
|
92
107
|
# Add a header to the table output
|
|
93
108
|
def header(title)
|
|
94
109
|
puts "\n#{@pastel.bold.underline(title)}"
|
|
@@ -99,7 +114,9 @@ module Sxn
|
|
|
99
114
|
|
|
100
115
|
def render_table(headers, rows)
|
|
101
116
|
table = TTY::Table.new(header: headers, rows: rows)
|
|
102
|
-
|
|
117
|
+
# Use basic renderer to avoid terminal width detection issues
|
|
118
|
+
renderer = $stdout.tty? ? :unicode : :basic
|
|
119
|
+
puts table.render(renderer, padding: [0, 1])
|
|
103
120
|
end
|
|
104
121
|
|
|
105
122
|
def empty_table(message)
|
data/lib/sxn/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sxn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ernest Sim
|
|
@@ -509,17 +509,20 @@ files:
|
|
|
509
509
|
- lib/sxn/commands/projects.rb
|
|
510
510
|
- lib/sxn/commands/rules.rb
|
|
511
511
|
- lib/sxn/commands/sessions.rb
|
|
512
|
+
- lib/sxn/commands/templates.rb
|
|
512
513
|
- lib/sxn/commands/worktrees.rb
|
|
513
514
|
- lib/sxn/config.rb
|
|
514
515
|
- lib/sxn/config/config_cache.rb
|
|
515
516
|
- lib/sxn/config/config_discovery.rb
|
|
516
517
|
- lib/sxn/config/config_validator.rb
|
|
518
|
+
- lib/sxn/config/templates_config.rb
|
|
517
519
|
- lib/sxn/core.rb
|
|
518
520
|
- lib/sxn/core/config_manager.rb
|
|
519
521
|
- lib/sxn/core/project_manager.rb
|
|
520
522
|
- lib/sxn/core/rules_manager.rb
|
|
521
523
|
- lib/sxn/core/session_config.rb
|
|
522
524
|
- lib/sxn/core/session_manager.rb
|
|
525
|
+
- lib/sxn/core/template_manager.rb
|
|
523
526
|
- lib/sxn/core/worktree_manager.rb
|
|
524
527
|
- lib/sxn/database.rb
|
|
525
528
|
- lib/sxn/database/errors.rb
|
|
@@ -611,7 +614,6 @@ files:
|
|
|
611
614
|
- sig/sxn/ui/prompt.rbs
|
|
612
615
|
- sig/sxn/ui/table.rbs
|
|
613
616
|
- sig/sxn/version.rbs
|
|
614
|
-
- sxn.gemspec
|
|
615
617
|
homepage: https://github.com/idl3/sxn
|
|
616
618
|
licenses:
|
|
617
619
|
- MIT
|
data/sxn.gemspec
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "lib/sxn/version"
|
|
4
|
-
|
|
5
|
-
Gem::Specification.new do |spec|
|
|
6
|
-
spec.name = "sxn"
|
|
7
|
-
spec.version = Sxn::VERSION
|
|
8
|
-
spec.authors = ["Ernest Sim"]
|
|
9
|
-
spec.email = ["ernest.codes@gmail.com"]
|
|
10
|
-
|
|
11
|
-
spec.summary = "Session management for multi-repository development"
|
|
12
|
-
spec.description = "Sxn simplifies git worktree management with intelligent project rules and secure automation"
|
|
13
|
-
spec.homepage = "https://github.com/idl3/sxn"
|
|
14
|
-
spec.license = "MIT"
|
|
15
|
-
spec.required_ruby_version = ">= 3.2.0"
|
|
16
|
-
|
|
17
|
-
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
|
18
|
-
spec.metadata["homepage_uri"] = spec.homepage
|
|
19
|
-
spec.metadata["source_code_uri"] = spec.homepage
|
|
20
|
-
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
21
|
-
spec.metadata["rubygems_mfa_required"] = "true"
|
|
22
|
-
|
|
23
|
-
# Specify which files should be added to the gem when it is released.
|
|
24
|
-
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
25
|
-
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
26
|
-
`git ls-files -z`.split("\x0").reject do |f|
|
|
27
|
-
(f == __FILE__) ||
|
|
28
|
-
f.match(%r{\A(?:(?:test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) ||
|
|
29
|
-
f.match(/\.db-(?:shm|wal)\z/) || # Exclude SQLite temp files
|
|
30
|
-
f.match(/\.gem\z/) # Exclude gem files
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
spec.bindir = "bin"
|
|
35
|
-
spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
|
|
36
|
-
spec.require_paths = ["lib"]
|
|
37
|
-
|
|
38
|
-
# Core CLI dependencies
|
|
39
|
-
spec.add_dependency "pastel", "~> 0.8" # Terminal colors
|
|
40
|
-
spec.add_dependency "thor", "~> 1.3" # CLI framework
|
|
41
|
-
spec.add_dependency "tty-progressbar", "~> 0.18" # Progress bars
|
|
42
|
-
spec.add_dependency "tty-prompt", "~> 0.23" # Interactive prompts
|
|
43
|
-
spec.add_dependency "tty-table", "~> 0.12" # Table formatting
|
|
44
|
-
|
|
45
|
-
# Configuration and data management
|
|
46
|
-
spec.add_dependency "dry-configurable", "~> 1.0" # Configuration management
|
|
47
|
-
spec.add_dependency "sqlite3", "~> 1.6" # Session database
|
|
48
|
-
spec.add_dependency "zeitwerk", "~> 2.6" # Code loading
|
|
49
|
-
|
|
50
|
-
# Template engine (secure, sandboxed)
|
|
51
|
-
spec.add_dependency "liquid", "~> 5.4" # Safe template processing
|
|
52
|
-
|
|
53
|
-
# MCP server dependencies
|
|
54
|
-
spec.add_dependency "async", "~> 2.0" # Async operations
|
|
55
|
-
spec.add_dependency "json-schema", "~> 4.0" # Schema validation
|
|
56
|
-
|
|
57
|
-
# Security and encryption
|
|
58
|
-
spec.add_dependency "bcrypt", "~> 3.1" # Password hashing
|
|
59
|
-
spec.add_dependency "openssl", ">= 3.0" # Encryption support
|
|
60
|
-
spec.add_dependency "ostruct" # OpenStruct for Ruby 3.5+ compatibility
|
|
61
|
-
|
|
62
|
-
# File system operations
|
|
63
|
-
spec.add_dependency "listen", "~> 3.8" # File watching for config cache
|
|
64
|
-
spec.add_dependency "parallel", "~> 1.23" # Parallel execution
|
|
65
|
-
|
|
66
|
-
# Development dependencies
|
|
67
|
-
spec.add_development_dependency "aruba", "~> 2.1" # CLI testing
|
|
68
|
-
spec.add_development_dependency "benchmark" # Benchmark for Ruby 3.5+ compatibility
|
|
69
|
-
spec.add_development_dependency "benchmark-ips", "~> 2.12" # Performance benchmarking
|
|
70
|
-
spec.add_development_dependency "bundler", "~> 2.4"
|
|
71
|
-
spec.add_development_dependency "climate_control", "~> 1.2" # Environment variable testing
|
|
72
|
-
spec.add_development_dependency "faker", "~> 3.2" # Test data generation
|
|
73
|
-
spec.add_development_dependency "memory_profiler", "~> 1.0" # Memory profiling
|
|
74
|
-
spec.add_development_dependency "rake", "~> 13.0"
|
|
75
|
-
spec.add_development_dependency "rspec", "~> 3.12"
|
|
76
|
-
spec.add_development_dependency "rubocop", "~> 1.50" # Code linting
|
|
77
|
-
spec.add_development_dependency "rubocop-performance", "~> 1.16"
|
|
78
|
-
spec.add_development_dependency "rubocop-rspec", "~> 2.19"
|
|
79
|
-
spec.add_development_dependency "simplecov", "~> 0.22" # Code coverage
|
|
80
|
-
spec.add_development_dependency "vcr", "~> 6.2" # HTTP interaction recording
|
|
81
|
-
spec.add_development_dependency "webmock", "~> 3.19" # HTTP mocking for MCP tests
|
|
82
|
-
|
|
83
|
-
# For more information and examples about making a new gem, check out our
|
|
84
|
-
# guide at: https://bundler.io/guides/creating_gem.html
|
|
85
|
-
end
|