sxn 0.2.0 → 0.2.2
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/.parallel_rspec +2 -0
- data/.parallel_rspec_options +3 -0
- data/.rspec +2 -1
- data/CHANGELOG.md +8 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +4 -1
- data/README.md +4 -0
- data/Rakefile +41 -0
- data/docs/parallel_testing.md +124 -0
- data/lib/sxn/CLI.rb +28 -16
- data/lib/sxn/commands/sessions.rb +19 -4
- data/lib/sxn/database/session_database.rb +81 -8
- data/lib/sxn/runtime_validations.rb +5 -1
- data/lib/sxn/version.rb +1 -1
- data/sig/sxn/cli.rbs +6 -0
- data/sig/sxn/commands/sessions.rbs +4 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9758f35bc9f122c019b15dd8a46f712cda58eb86495c6e5bb8c63791b10525e
|
4
|
+
data.tar.gz: 628449e55edf8b33ade3f83cdfab4369234bd22e26e5625cc3890b3c46a74702
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7970fbb77d1b7b21da3b93d566bb54a42219c787ca081b2319ec6c3dcf7b31dee35761339da42322ac2acf393232fa458325e3573dfebec546caa2e14f9da0eb
|
7
|
+
data.tar.gz: bd4f880ea4b2aa55b1a0de1d87d6e6bcc41a787218d29c104c644c87d07d2c43048b77217ba6ceae3f3df66a2da63c531a6b7f6cc96781d1454f0a2622232d1a
|
data/.parallel_rspec
ADDED
data/.rspec
CHANGED
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,14 @@ 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.2.1] - 2025-01-20
|
9
|
+
|
10
|
+
### Fixed
|
11
|
+
- Fixed SQLite3 datatype mismatch error when listing sessions
|
12
|
+
- Fixed `sxn list` showing no sessions even when sessions exist
|
13
|
+
- Improved type coercion for database parameters
|
14
|
+
- Enhanced error logging for SQLite3 errors
|
15
|
+
|
8
16
|
## [0.2.0] - 2025-01-20
|
9
17
|
|
10
18
|
### Added
|
data/Gemfile
CHANGED
@@ -10,8 +10,9 @@ gem "rake", "~> 13.0"
|
|
10
10
|
|
11
11
|
gem "rubocop", "~> 1.21"
|
12
12
|
|
13
|
-
# Test coverage
|
13
|
+
# Test coverage and parallel testing
|
14
14
|
group :test do
|
15
|
+
gem "parallel_tests", "~> 4.0"
|
15
16
|
gem "simplecov", "~> 0.22", require: false
|
16
17
|
gem "simplecov-console", require: false
|
17
18
|
end
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
sxn (0.2.
|
4
|
+
sxn (0.2.2)
|
5
5
|
async (~> 2.0)
|
6
6
|
bcrypt (~> 3.1)
|
7
7
|
dry-configurable (~> 1.0)
|
@@ -146,6 +146,8 @@ GEM
|
|
146
146
|
openssl (3.3.0)
|
147
147
|
ostruct (0.6.3)
|
148
148
|
parallel (1.27.0)
|
149
|
+
parallel_tests (4.10.1)
|
150
|
+
parallel
|
149
151
|
parser (3.3.9.0)
|
150
152
|
ast (~> 2.4.1)
|
151
153
|
racc
|
@@ -311,6 +313,7 @@ DEPENDENCIES
|
|
311
313
|
faker (~> 3.2)
|
312
314
|
irb
|
313
315
|
memory_profiler (~> 1.0)
|
316
|
+
parallel_tests (~> 4.0)
|
314
317
|
rake (~> 13.0)
|
315
318
|
rbs (~> 3.4)
|
316
319
|
rbs_rails (~> 0.12)
|
data/README.md
CHANGED
@@ -223,3 +223,7 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/yourus
|
|
223
223
|
## License
|
224
224
|
|
225
225
|
The gem is available as open source under the terms of the [MIT License](LICENSE.txt).
|
226
|
+
|
227
|
+
## Author's Note
|
228
|
+
|
229
|
+
This is a personal project that leverages Claude Code as the primary and active developer. As we continue to refine the development process and iron out any kinks, you can expect builds to gradually become more stable. Your patience and feedback are greatly appreciated as we evolve this tool together.
|
data/Rakefile
CHANGED
@@ -13,6 +13,47 @@ rescue LoadError
|
|
13
13
|
# RSpec not available
|
14
14
|
end
|
15
15
|
|
16
|
+
# Parallel testing tasks
|
17
|
+
begin
|
18
|
+
require "parallel_tests/tasks"
|
19
|
+
rescue LoadError
|
20
|
+
# parallel_tests not available
|
21
|
+
end
|
22
|
+
|
23
|
+
namespace :parallel do
|
24
|
+
desc "Run specs in parallel"
|
25
|
+
task :spec do
|
26
|
+
sh "bundle exec parallel_rspec spec/unit spec/integration spec/performance --runtime-log tmp/parallel_runtime_rspec.log"
|
27
|
+
end
|
28
|
+
|
29
|
+
desc "Run specs in parallel with coverage"
|
30
|
+
task :spec_with_coverage do
|
31
|
+
ENV["ENABLE_SIMPLECOV"] = "true"
|
32
|
+
sh "bundle exec parallel_rspec spec/unit spec/integration spec/performance --runtime-log tmp/parallel_runtime_rspec.log"
|
33
|
+
end
|
34
|
+
|
35
|
+
desc "Setup parallel test databases (if needed)"
|
36
|
+
task :setup do
|
37
|
+
puts "No database setup needed for this project"
|
38
|
+
end
|
39
|
+
|
40
|
+
desc "Run specs in parallel with custom processor count"
|
41
|
+
task :spec_custom, [:processors] do |_t, args|
|
42
|
+
processors = args[:processors] || 4
|
43
|
+
sh "bundle exec parallel_rspec -n #{processors} spec/unit spec/integration spec/performance --runtime-log tmp/parallel_runtime_rspec.log"
|
44
|
+
end
|
45
|
+
|
46
|
+
desc "Generate parallel test runtime log"
|
47
|
+
task :generate_runtime do
|
48
|
+
puts "Generating runtime log for balanced test distribution..."
|
49
|
+
ENV["TEST_ENV_NUMBER"] = "1"
|
50
|
+
specs = "spec/unit spec/integration spec/performance"
|
51
|
+
sh "bundle exec rspec --format progress --format ParallelTests::RSpec::RuntimeLogger " \
|
52
|
+
"--out tmp/parallel_runtime_rspec.log #{specs}"
|
53
|
+
puts "Runtime log generated at tmp/parallel_runtime_rspec.log"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
16
57
|
# Type checking tasks
|
17
58
|
namespace :rbs do
|
18
59
|
desc "Validate RBS files syntax"
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# Parallel Testing Guide
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
|
5
|
+
The sxn project uses `parallel_tests` gem to run RSpec tests in parallel, significantly reducing test execution time from ~35 seconds to ~11 seconds.
|
6
|
+
|
7
|
+
## Configuration
|
8
|
+
|
9
|
+
### `.parallel_rspec` File
|
10
|
+
|
11
|
+
This file configures RSpec formatters for parallel execution:
|
12
|
+
|
13
|
+
```
|
14
|
+
--format progress
|
15
|
+
--format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log
|
16
|
+
```
|
17
|
+
|
18
|
+
The RuntimeLogger records execution times for each test file, enabling balanced test distribution across parallel processes.
|
19
|
+
|
20
|
+
### Runtime-Based Test Distribution
|
21
|
+
|
22
|
+
Tests are distributed across parallel processes based on their historical execution times, ensuring all processes finish at roughly the same time.
|
23
|
+
|
24
|
+
## Local Usage
|
25
|
+
|
26
|
+
### Run Tests in Parallel
|
27
|
+
|
28
|
+
```bash
|
29
|
+
# Default parallel execution (uses all available CPUs)
|
30
|
+
bundle exec rake parallel:spec
|
31
|
+
|
32
|
+
# With custom processor count
|
33
|
+
bundle exec rake parallel:spec_custom[2] # Use 2 processes
|
34
|
+
|
35
|
+
# With coverage
|
36
|
+
bundle exec rake parallel:spec_with_coverage
|
37
|
+
```
|
38
|
+
|
39
|
+
### Generate/Update Runtime Log
|
40
|
+
|
41
|
+
```bash
|
42
|
+
# Generate initial runtime log or update existing one
|
43
|
+
bundle exec rake parallel:generate_runtime
|
44
|
+
```
|
45
|
+
|
46
|
+
The runtime log is stored at `tmp/parallel_runtime_rspec.log` and contains execution times for each spec file.
|
47
|
+
|
48
|
+
### Direct parallel_rspec Command
|
49
|
+
|
50
|
+
```bash
|
51
|
+
# Run with runtime balancing
|
52
|
+
bundle exec parallel_rspec spec/unit spec/integration spec/performance --runtime-log tmp/parallel_runtime_rspec.log
|
53
|
+
|
54
|
+
# Verbose output to see how tests are distributed
|
55
|
+
bundle exec parallel_rspec spec --verbose-command --runtime-log tmp/parallel_runtime_rspec.log
|
56
|
+
```
|
57
|
+
|
58
|
+
## CI/CD Integration
|
59
|
+
|
60
|
+
GitHub Actions is configured to:
|
61
|
+
|
62
|
+
1. **Cache Runtime Log**: The runtime log is cached between runs using the spec files' hash as the cache key
|
63
|
+
2. **Parallel Matrix Jobs**: Tests run across 4 parallel jobs
|
64
|
+
3. **Automatic Generation**: If no cached runtime log exists, CI generates one automatically
|
65
|
+
4. **Result Aggregation**: Test results from all parallel jobs are collected and summarized
|
66
|
+
|
67
|
+
### CI Configuration
|
68
|
+
|
69
|
+
The workflow uses a matrix strategy to run tests in parallel:
|
70
|
+
|
71
|
+
```yaml
|
72
|
+
matrix:
|
73
|
+
ruby: ['3.4.5']
|
74
|
+
ci_node_total: [4]
|
75
|
+
ci_node_index: [0, 1, 2, 3]
|
76
|
+
```
|
77
|
+
|
78
|
+
Each job runs a subset of tests based on the runtime log distribution.
|
79
|
+
|
80
|
+
## Benefits
|
81
|
+
|
82
|
+
- **Speed**: ~3x faster test execution (35s → 11s)
|
83
|
+
- **Balanced Distribution**: Tests are distributed based on execution time, not file count
|
84
|
+
- **CI Optimization**: Parallel jobs in CI reduce overall build time
|
85
|
+
- **Coverage Support**: SimpleCov properly merges results from parallel processes
|
86
|
+
|
87
|
+
## Troubleshooting
|
88
|
+
|
89
|
+
### Unbalanced Test Distribution
|
90
|
+
|
91
|
+
If tests aren't evenly distributed:
|
92
|
+
|
93
|
+
1. Regenerate the runtime log: `bundle exec rake parallel:generate_runtime`
|
94
|
+
2. Check the log file exists: `ls -la tmp/parallel_runtime_rspec.log`
|
95
|
+
3. Verify the format (should be `path/to/spec.rb:execution_time`)
|
96
|
+
|
97
|
+
### Missing Runtime Log
|
98
|
+
|
99
|
+
The system will fall back to file-size-based distribution if no runtime log is found. To ensure runtime-based distribution:
|
100
|
+
|
101
|
+
```bash
|
102
|
+
# Generate if missing
|
103
|
+
[ -f tmp/parallel_runtime_rspec.log ] || bundle exec rake parallel:generate_runtime
|
104
|
+
```
|
105
|
+
|
106
|
+
### Coverage Issues
|
107
|
+
|
108
|
+
Ensure SimpleCov is configured for parallel tests in `spec/spec_helper.rb`:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
if ENV["TEST_ENV_NUMBER"]
|
112
|
+
SimpleCov.command_name "RSpec_#{ENV['TEST_ENV_NUMBER']}"
|
113
|
+
SimpleCov.merge_timeout 3600
|
114
|
+
end
|
115
|
+
```
|
116
|
+
|
117
|
+
## Performance Metrics
|
118
|
+
|
119
|
+
Current performance with 4 parallel processes:
|
120
|
+
- Sequential execution: ~35 seconds
|
121
|
+
- Parallel execution: ~11 seconds
|
122
|
+
- Speedup: ~3.2x
|
123
|
+
|
124
|
+
The runtime-based distribution ensures all processes finish within 1-2 seconds of each other, maximizing efficiency.
|
data/lib/sxn/CLI.rb
CHANGED
@@ -32,16 +32,8 @@ module Sxn
|
|
32
32
|
# steep:ignore:start - Thor dynamic argument validation handled at runtime
|
33
33
|
# Thor framework uses metaprogramming for argument parsing that can't be statically typed.
|
34
34
|
# Runtime validation ensures type safety through Thor's built-in validation.
|
35
|
-
# Validate arguments, filtering out nil values for optional arguments
|
36
|
-
args_for_validation = [folder].compact
|
37
|
-
expected_arg_count = folder.nil? ? 0 : 1
|
38
|
-
|
39
|
-
RuntimeValidations.validate_thor_arguments("init", args_for_validation, options, {
|
40
|
-
args: { count: [expected_arg_count], types: [String] },
|
41
|
-
options: { force: :boolean, auto_detect: :boolean, quiet: :boolean }
|
42
|
-
})
|
43
|
-
|
44
35
|
Commands::Init.new.init(folder)
|
36
|
+
# steep:ignore:end
|
45
37
|
rescue Sxn::Error => e
|
46
38
|
handle_error(e)
|
47
39
|
end
|
@@ -78,6 +70,30 @@ module Sxn
|
|
78
70
|
handle_error(e)
|
79
71
|
end
|
80
72
|
|
73
|
+
desc "remove [SESSION_NAME]", "Remove a session (shortcut for 'sxn sessions remove')"
|
74
|
+
option :force, type: :boolean, aliases: "-f", desc: "Force removal even with uncommitted changes"
|
75
|
+
def remove(session_name = nil)
|
76
|
+
cmd = Commands::Sessions.new
|
77
|
+
cmd.options = options
|
78
|
+
cmd.remove(session_name)
|
79
|
+
rescue Sxn::Error => e
|
80
|
+
handle_error(e)
|
81
|
+
end
|
82
|
+
|
83
|
+
desc "archive [SESSION_NAME]", "Archive a session (shortcut for 'sxn sessions archive')"
|
84
|
+
def archive(session_name = nil)
|
85
|
+
Commands::Sessions.new.archive(session_name)
|
86
|
+
rescue Sxn::Error => e
|
87
|
+
handle_error(e)
|
88
|
+
end
|
89
|
+
|
90
|
+
desc "activate [SESSION_NAME]", "Activate an archived session (shortcut for 'sxn sessions activate')"
|
91
|
+
def activate(session_name = nil)
|
92
|
+
Commands::Sessions.new.activate(session_name)
|
93
|
+
rescue Sxn::Error => e
|
94
|
+
handle_error(e)
|
95
|
+
end
|
96
|
+
|
81
97
|
desc "projects SUBCOMMAND", "Manage project configurations"
|
82
98
|
def projects(subcommand = nil, *args)
|
83
99
|
Commands::Projects.start([subcommand, *args].compact)
|
@@ -200,18 +216,14 @@ module Sxn
|
|
200
216
|
|
201
217
|
# steep:ignore:start - Safe integer to string coercion for UI display
|
202
218
|
# These integer values are safely converted to strings for display purposes.
|
203
|
-
|
204
|
-
@ui.key_value("Total
|
205
|
-
RuntimeValidations.validate_and_coerce_type(sessions.size, String, "session count display"))
|
206
|
-
@ui.key_value("Total Projects",
|
207
|
-
RuntimeValidations.validate_and_coerce_type(projects.size, String, "project count display"))
|
219
|
+
@ui.key_value("Total Sessions", sessions.size.to_s)
|
220
|
+
@ui.key_value("Total Projects", projects.size.to_s)
|
208
221
|
|
209
222
|
# Active worktrees
|
210
223
|
if current_session
|
211
224
|
worktree_manager = Sxn::Core::WorktreeManager.new(config_manager, session_manager)
|
212
225
|
worktrees = worktree_manager.list_worktrees(session_name: current_session)
|
213
|
-
@ui.key_value("Active Worktrees",
|
214
|
-
RuntimeValidations.validate_and_coerce_type(worktrees.size, String, "worktree count display"))
|
226
|
+
@ui.key_value("Active Worktrees", worktrees.size.to_s)
|
215
227
|
end
|
216
228
|
|
217
229
|
@ui.newline
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "thor"
|
4
|
+
require "time"
|
4
5
|
|
5
6
|
module Sxn
|
6
7
|
module Commands
|
@@ -79,7 +80,8 @@ module Sxn
|
|
79
80
|
name = @prompt.select("Select session to remove:", choices)
|
80
81
|
end
|
81
82
|
|
82
|
-
|
83
|
+
# Skip confirmation if force flag is used
|
84
|
+
if !options[:force] && !@prompt.confirm_deletion(name, "session")
|
83
85
|
@ui.info("Cancelled")
|
84
86
|
return
|
85
87
|
end
|
@@ -111,7 +113,7 @@ module Sxn
|
|
111
113
|
begin
|
112
114
|
sessions = @session_manager.list_sessions(
|
113
115
|
status: options[:status],
|
114
|
-
limit: options[:limit]
|
116
|
+
limit: options[:limit]&.to_i || 50
|
115
117
|
)
|
116
118
|
|
117
119
|
@ui.section("Sessions")
|
@@ -259,8 +261,8 @@ module Sxn
|
|
259
261
|
|
260
262
|
@ui.key_value("Linear Task", session[:linear_task]) if session[:linear_task]
|
261
263
|
|
262
|
-
@ui.key_value("Created", session[:created_at]
|
263
|
-
@ui.key_value("Updated", session[:updated_at]
|
264
|
+
@ui.key_value("Created", format_timestamp(session[:created_at]))
|
265
|
+
@ui.key_value("Updated", format_timestamp(session[:updated_at]))
|
264
266
|
|
265
267
|
if verbose && session[:projects]&.any?
|
266
268
|
@ui.newline
|
@@ -295,6 +297,19 @@ module Sxn
|
|
295
297
|
@ui.newline
|
296
298
|
@ui.recovery_suggestion("Create your first session with 'sxn add <session-name>'")
|
297
299
|
end
|
300
|
+
|
301
|
+
def format_timestamp(timestamp)
|
302
|
+
return "Unknown" if timestamp.nil? || timestamp.empty?
|
303
|
+
|
304
|
+
# Parse the ISO8601 timestamp and convert to local time
|
305
|
+
time = Time.parse(timestamp)
|
306
|
+
local_time = time.localtime
|
307
|
+
|
308
|
+
# Format as "YYYY-MM-DD HH:MM:SS AM/PM Timezone"
|
309
|
+
local_time.strftime("%Y-%m-%d %I:%M:%S %p %Z")
|
310
|
+
rescue ArgumentError
|
311
|
+
timestamp # Return original if parsing fails
|
312
|
+
end
|
298
313
|
end
|
299
314
|
end
|
300
315
|
end
|
@@ -25,7 +25,7 @@ module Sxn
|
|
25
25
|
# - Bulk operations: < 100ms for 100 sessions
|
26
26
|
class SessionDatabase
|
27
27
|
# Current database schema version for migrations
|
28
|
-
SCHEMA_VERSION =
|
28
|
+
SCHEMA_VERSION = 2
|
29
29
|
|
30
30
|
# Default database path relative to sxn config directory
|
31
31
|
DEFAULT_DB_PATH = ".sxn/sessions.db"
|
@@ -118,8 +118,11 @@ module Sxn
|
|
118
118
|
# @param offset [Integer] Results offset for pagination (default: 0)
|
119
119
|
# @return [Array<Hash>] Array of session hashes
|
120
120
|
def list_sessions(filters: {}, sort: {}, limit: 100, offset: 0)
|
121
|
-
# Ensure filters is a Hash
|
121
|
+
# Ensure filters is a Hash and parameters are correct types
|
122
122
|
filters ||= {}
|
123
|
+
limit = limit.to_i if limit
|
124
|
+
offset = offset.to_i if offset
|
125
|
+
|
123
126
|
query_parts = ["SELECT * FROM sessions"]
|
124
127
|
params = []
|
125
128
|
|
@@ -132,9 +135,9 @@ module Sxn
|
|
132
135
|
sort_order = sort[:order] || :desc
|
133
136
|
query_parts << "ORDER BY #{sort_field} #{sort_order.to_s.upcase}"
|
134
137
|
|
135
|
-
# Add pagination
|
138
|
+
# Add pagination - ensure these are integers
|
136
139
|
query_parts << "LIMIT ? OFFSET ?"
|
137
|
-
params.push(limit, offset)
|
140
|
+
params.push(limit || 100, offset || 0)
|
138
141
|
|
139
142
|
sql = query_parts.join(" ")
|
140
143
|
|
@@ -417,6 +420,19 @@ module Sxn
|
|
417
420
|
def setup_database
|
418
421
|
current_version = get_schema_version
|
419
422
|
|
423
|
+
# Check if this is an old database without version info
|
424
|
+
# If sessions table exists but version is 0, it's likely an old database
|
425
|
+
if current_version.zero? && table_exists?("sessions")
|
426
|
+
# Check if it's the old schema (without worktrees/projects columns)
|
427
|
+
columns = connection.execute("PRAGMA table_info(sessions)").map { |col| col["name"] }
|
428
|
+
if !columns.include?("worktrees") || !columns.include?("projects")
|
429
|
+
# This is an old database, set it to version 1 so we can migrate it
|
430
|
+
Sxn.logger.info "Detected old database schema without version info, treating as version 1"
|
431
|
+
set_schema_version(1)
|
432
|
+
current_version = 1
|
433
|
+
end
|
434
|
+
end
|
435
|
+
|
420
436
|
if current_version.zero?
|
421
437
|
create_initial_schema
|
422
438
|
set_schema_version(SCHEMA_VERSION)
|
@@ -425,6 +441,14 @@ module Sxn
|
|
425
441
|
end
|
426
442
|
end
|
427
443
|
|
444
|
+
# Check if a table exists
|
445
|
+
def table_exists?(table_name)
|
446
|
+
result = connection.execute("SELECT name FROM sqlite_master WHERE type='table' AND name=?", table_name)
|
447
|
+
!result.empty?
|
448
|
+
rescue SQLite3::Exception
|
449
|
+
false
|
450
|
+
end
|
451
|
+
|
428
452
|
# Get current schema version from database
|
429
453
|
def get_schema_version
|
430
454
|
result = connection.execute("PRAGMA user_version").first
|
@@ -498,12 +522,41 @@ module Sxn
|
|
498
522
|
end
|
499
523
|
|
500
524
|
# Run database migrations from old version to current
|
501
|
-
def run_migrations(
|
502
|
-
|
503
|
-
|
525
|
+
def run_migrations(from_version)
|
526
|
+
Sxn.logger.info "Running database migrations from version #{from_version} to #{SCHEMA_VERSION}"
|
527
|
+
|
528
|
+
# Run each migration in sequence
|
529
|
+
migrate_to_v1 if from_version < 1
|
530
|
+
|
531
|
+
migrate_to_v2 if from_version < 2
|
532
|
+
|
504
533
|
set_schema_version(SCHEMA_VERSION)
|
505
534
|
end
|
506
535
|
|
536
|
+
# Migration: Initial schema (v1)
|
537
|
+
def migrate_to_v1
|
538
|
+
# This is handled by create_initial_schema
|
539
|
+
create_initial_schema
|
540
|
+
end
|
541
|
+
|
542
|
+
# Migration: Add worktrees and projects columns (v2)
|
543
|
+
def migrate_to_v2
|
544
|
+
Sxn.logger.info "Migrating database to version 2: Adding worktrees and projects columns"
|
545
|
+
|
546
|
+
# Check if columns already exist
|
547
|
+
columns = connection.execute("PRAGMA table_info(sessions)").map { |col| col["name"] }
|
548
|
+
|
549
|
+
unless columns.include?("worktrees")
|
550
|
+
connection.execute("ALTER TABLE sessions ADD COLUMN worktrees TEXT")
|
551
|
+
Sxn.logger.info "Added worktrees column to sessions table"
|
552
|
+
end
|
553
|
+
|
554
|
+
return if columns.include?("projects")
|
555
|
+
|
556
|
+
connection.execute("ALTER TABLE sessions ADD COLUMN projects TEXT")
|
557
|
+
Sxn.logger.info "Added projects column to sessions table"
|
558
|
+
end
|
559
|
+
|
507
560
|
# Generate secure, unique session ID
|
508
561
|
def generate_session_id
|
509
562
|
SecureRandom.hex(16) # 32 character hex string
|
@@ -620,7 +673,27 @@ module Sxn
|
|
620
673
|
|
621
674
|
# Execute query with parameters and return results
|
622
675
|
def execute_query(sql, params = [])
|
623
|
-
|
676
|
+
# Ensure params are properly typed for SQLite3
|
677
|
+
sanitized_params = params.map do |param|
|
678
|
+
case param
|
679
|
+
when Integer, Float, String, NilClass
|
680
|
+
param
|
681
|
+
when TrueClass
|
682
|
+
1
|
683
|
+
when FalseClass
|
684
|
+
0
|
685
|
+
else
|
686
|
+
param.to_s
|
687
|
+
end
|
688
|
+
end
|
689
|
+
|
690
|
+
connection.execute(sql, sanitized_params)
|
691
|
+
rescue SQLite3::MismatchException => e
|
692
|
+
# Log the error with details for debugging
|
693
|
+
warn "SQLite3 datatype mismatch: #{e.message}"
|
694
|
+
warn "SQL: #{sql}"
|
695
|
+
warn "Params: #{sanitized_params.inspect}"
|
696
|
+
raise e
|
624
697
|
end
|
625
698
|
|
626
699
|
# Transaction wrapper with rollback support
|
@@ -6,6 +6,10 @@ module Sxn
|
|
6
6
|
class << self
|
7
7
|
# Validate Thor command arguments at runtime
|
8
8
|
def validate_thor_arguments(command_name, args, options, validations)
|
9
|
+
# Ensure args and options are not nil
|
10
|
+
args ||= []
|
11
|
+
options ||= {}
|
12
|
+
|
9
13
|
# Validate argument count
|
10
14
|
if validations[:args]
|
11
15
|
count_range = validations[:args][:count]
|
@@ -23,7 +27,7 @@ module Sxn
|
|
23
27
|
end
|
24
28
|
|
25
29
|
# Validate options
|
26
|
-
if validations[:options]
|
30
|
+
if validations[:options] && options.respond_to?(:each)
|
27
31
|
options.each do |key, value|
|
28
32
|
validate_option_type(command_name, key, value, validations[:options][key.to_sym]) if validations[:options][key.to_sym]
|
29
33
|
end
|
data/lib/sxn/version.rb
CHANGED
data/sig/sxn/cli.rbs
CHANGED
@@ -21,6 +21,12 @@ module Sxn
|
|
21
21
|
|
22
22
|
def current: () -> void
|
23
23
|
|
24
|
+
def remove: (?String? session_name) -> void
|
25
|
+
|
26
|
+
def archive: (?String? session_name) -> void
|
27
|
+
|
28
|
+
def activate: (?String? session_name) -> void
|
29
|
+
|
24
30
|
def projects: (?String? subcommand, *String args) -> void
|
25
31
|
|
26
32
|
def sessions: (?String? subcommand, *String args) -> void
|
@@ -57,6 +57,10 @@ module Sxn
|
|
57
57
|
|
58
58
|
# Suggest creating a session
|
59
59
|
def suggest_create_session: () -> void
|
60
|
+
|
61
|
+
# Format a timestamp string to local timezone
|
62
|
+
# @param timestamp ISO8601 timestamp string
|
63
|
+
def format_timestamp: (String? timestamp) -> String
|
60
64
|
end
|
61
65
|
end
|
62
66
|
end
|
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.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ernest Sim
|
@@ -488,6 +488,8 @@ files:
|
|
488
488
|
- ".gem_rbs_collection/sqlite3/2.0/.rbs_meta.yaml"
|
489
489
|
- ".gem_rbs_collection/sqlite3/2.0/database.rbs"
|
490
490
|
- ".gem_rbs_collection/sqlite3/2.0/pragmas.rbs"
|
491
|
+
- ".parallel_rspec"
|
492
|
+
- ".parallel_rspec_options"
|
491
493
|
- ".rspec"
|
492
494
|
- ".rubocop.yml"
|
493
495
|
- ".simplecov"
|
@@ -499,6 +501,7 @@ files:
|
|
499
501
|
- Rakefile
|
500
502
|
- Steepfile
|
501
503
|
- bin/sxn
|
504
|
+
- docs/parallel_testing.md
|
502
505
|
- lib/sxn.rb
|
503
506
|
- lib/sxn/CLI.rb
|
504
507
|
- lib/sxn/commands.rb
|