hiiro 0.1.307 → 0.1.308.pre.1
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 +123 -1
- data/CLAUDE.md +52 -0
- data/bin/h-branch +132 -55
- data/bin/h-db +178 -0
- data/bin/h-link +56 -15
- data/bin/h-pane +26 -3
- data/bin/h-pr +42 -0
- data/docs/coupling-and-extraction-plan.txt +1160 -0
- data/hiiro.gemspec +2 -0
- data/lib/hiiro/app_record.rb +19 -0
- data/lib/hiiro/assignment.rb +18 -0
- data/lib/hiiro/branch.rb +29 -0
- data/lib/hiiro/db.rb +365 -0
- data/lib/hiiro/effects.rb +152 -0
- data/lib/hiiro/git.rb +7 -6
- data/lib/hiiro/invocation.rb +130 -0
- data/lib/hiiro/link.rb +59 -0
- data/lib/hiiro/options.rb +5 -0
- data/lib/hiiro/pane_home.rb +24 -0
- data/lib/hiiro/pin_record.rb +31 -0
- data/lib/hiiro/pinned_pr.rb +181 -0
- data/lib/hiiro/pinned_pr_manager.rb +59 -37
- data/lib/hiiro/project.rb +19 -0
- data/lib/hiiro/projects.rb +23 -0
- data/lib/hiiro/reminder.rb +24 -0
- data/lib/hiiro/tags.rb +103 -29
- data/lib/hiiro/task_record.rb +37 -0
- data/lib/hiiro/tasks.rb +65 -20
- data/lib/hiiro/tmux.rb +5 -4
- data/lib/hiiro/todo.rb +46 -44
- data/lib/hiiro/tracked_pr.rb +30 -0
- data/lib/hiiro/version.rb +1 -1
- data/lib/hiiro.rb +21 -0
- data/plugins/pins.rb +38 -7
- data/script/publish +1 -1
- data/side-effect-separation-plan.md +177 -0
- metadata +46 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5133c5b05e6f2040b092c49a8ff75a1e665f9506d5a6b7afc9078dcd9c3d20a2
|
|
4
|
+
data.tar.gz: bea25caa37de50a810b76b72b8eebe5eb1fd37141542aaea21da1932e3dbae9d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 321291e9a574a533d3647c7c4a54040fdbf993c174923958a4d1fdb7936bee373c838c689ca10fb50b48940d4a9aefc662c4a72714442271c7180eccf82d748f
|
|
7
|
+
data.tar.gz: 15d46bd5f44affa4fb6418f6f76e61cef848a4ade2a975cba450825aeb730da8362fbc0a885241bad31bae4969f752063de95d9143ab72127576850cf3c4e804
|
data/CHANGELOG.md
CHANGED
|
@@ -1 +1,123 @@
|
|
|
1
|
-
|
|
1
|
+
```markdown
|
|
2
|
+
# Changelog
|
|
3
|
+
|
|
4
|
+
## [0.1.308.pre.1] - 2026-03-31
|
|
5
|
+
|
|
6
|
+
### Added
|
|
7
|
+
- `Hiiro::Effects` injectable interface for testable file system and command execution
|
|
8
|
+
- `null_fs` to `TestHarness` for testing without side effects
|
|
9
|
+
- Effects helpers and accessors to `TestHarness` for controlled effect simulation
|
|
10
|
+
- `Hiiro::Invocation` and `Hiiro::InvocationResolution` tracking in PaneHome SQLite migration
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Refactor effects layer: expose `executor` and `fs` as accessors on `Hiiro::Effects`
|
|
14
|
+
- `h-db` command now includes h-pane in SQLite migration
|
|
15
|
+
- Gem version handling: treat non-main branches as pre-release in publish script
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `h-branch co` and `h-branch rm` restore extra argument pass-through
|
|
19
|
+
- Test suite: add missing `TestHarness` stubs and fix pre-existing test failures
|
|
20
|
+
- Test fixtures: anchor `load_bin` path to project root instead of `Dir.pwd`
|
|
21
|
+
|
|
22
|
+
### Deprecated
|
|
23
|
+
- `SystemCallCapture` — use `Hiiro::Effects` helpers in `TestHarness` instead
|
|
24
|
+
|
|
25
|
+
## [Unreleased]
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
- `Hiiro::DB` — SQLite foundation via Sequel; `DB.setup!` creates all tables, `DB.connection` establishes connection eagerly at load time; supports `HIIRO_TEST_DB=sqlite::memory:` for tests
|
|
29
|
+
- `lib/hiiro/db.rb` — one-time YAML→SQLite migration (`migrate_yaml!`) guarded by `schema_migrations` table; dual-write mode (`dual_write?` / `disable_dual_write!`) for gradual cutover
|
|
30
|
+
- `lib/hiiro/branch.rb` — `Hiiro::Branch` Sequel model for worktree branch records
|
|
31
|
+
- `lib/hiiro/tracked_pr.rb` — `Hiiro::TrackedPr` Sequel model for tracked PR records (`:prs` table)
|
|
32
|
+
- `lib/hiiro/link.rb` — `Hiiro::Link` Sequel model with `matches?`, `display_string`, `to_h` helpers
|
|
33
|
+
- `lib/hiiro/project.rb` — `Hiiro::Project` Sequel model
|
|
34
|
+
- `lib/hiiro/pane_home.rb` — `Hiiro::PaneHome` Sequel model with `data_json` JSON blob
|
|
35
|
+
- `lib/hiiro/pin_record.rb` — `Hiiro::PinRecord` Sequel model for per-command key-value pin storage
|
|
36
|
+
- `lib/hiiro/task_record.rb` — `Hiiro::TaskRecord` Sequel model for task metadata
|
|
37
|
+
- `lib/hiiro/app_record.rb` — `Hiiro::AppRecord` Sequel model for app directory mappings
|
|
38
|
+
- `lib/hiiro/assignment.rb` — `Hiiro::Assignment` Sequel model for worktree→branch assignments
|
|
39
|
+
- `lib/hiiro/reminder.rb` — `Hiiro::Reminder` Sequel model
|
|
40
|
+
- `lib/hiiro/invocation.rb` — `Hiiro::Invocation` and `Hiiro::InvocationResolution` Sequel models; every CLI invocation is recorded to SQLite for history/analytics
|
|
41
|
+
- `bin/h-db` — new subcommand: `h db status`, `h db tables`, `h db q <sql>`, `h db migrate`, `h db restore`
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
- `lib/hiiro/todo.rb` — `TodoItem` is now a `Sequel::Model`; `TodoManager` reads/writes via SQLite with YAML dual-write fallback
|
|
45
|
+
- `lib/hiiro/tags.rb` — `Tag` is now a `Sequel::Model`; tag operations persist to SQLite with YAML dual-write fallback
|
|
46
|
+
- `lib/hiiro/pinned_pr_manager.rb` — `PinnedPR` is now a `Sequel::Model` (`lib/hiiro/pinned_pr.rb`); `PinnedPRManager` reads/writes via SQLite with YAML dual-write
|
|
47
|
+
- `lib/hiiro/projects.rb` — `Projects#from_config` reads from `Hiiro::Project` SQLite model with YAML fallback
|
|
48
|
+
- `lib/hiiro/tasks.rb` — `TaskManager::Config` reads/writes tasks and apps via `Hiiro::TaskRecord` and `Hiiro::AppRecord` SQLite models
|
|
49
|
+
- `bin/h-branch` — `BranchManager` reads/writes via `Hiiro::Branch` and `Hiiro::TrackedPr` SQLite models with YAML dual-write fallback; adds `q`/`query` subcommands for raw SQL inspection
|
|
50
|
+
- `bin/h-link` — reads/writes links via `Hiiro::Link` SQLite model with YAML dual-write fallback; adds `q`/`query` subcommands
|
|
51
|
+
- `bin/h-pane` — load/save pane homes via `Hiiro::PaneHome` model with YAML dual-write
|
|
52
|
+
- `bin/h-pr` — adds `q`/`query` subcommands for inspecting PR records via raw SQL
|
|
53
|
+
- `plugins/pins.rb` — `Pin` class reads/writes via `Hiiro::PinRecord` SQLite model with YAML dual-write fallback
|
|
54
|
+
|
|
55
|
+
## [0.1.306] - 2026-03-30
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
- Increase delayed_update sleep duration from 5s to 15s
|
|
59
|
+
- Add logging for delayed_update invocation in publish script
|
|
60
|
+
|
|
61
|
+
## [0.1.305] - 2026-03-30
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- Refactor: use delayed_update subcommand instead of direct update call
|
|
65
|
+
- Improve gem version matching regex in version check
|
|
66
|
+
|
|
67
|
+
## [0.1.304] - 2026-03-30
|
|
68
|
+
|
|
69
|
+
### Changed
|
|
70
|
+
- h-notify: use universal log instead of per-session logging
|
|
71
|
+
- Todo output simplified
|
|
72
|
+
|
|
73
|
+
## [0.1.302] - 2026-03-30
|
|
74
|
+
|
|
75
|
+
### Fixed
|
|
76
|
+
- Truncate output lines to terminal width in tasks plugin
|
|
77
|
+
|
|
78
|
+
## [0.1.301]
|
|
79
|
+
|
|
80
|
+
### Added
|
|
81
|
+
- Check version delayed update functionality
|
|
82
|
+
|
|
83
|
+
### Changed
|
|
84
|
+
- h-claude: add verbose flags and refactor glob_path handling
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
- Use exact session matching to prevent tmux prefix ambiguity
|
|
88
|
+
|
|
89
|
+
## [0.1.300]
|
|
90
|
+
|
|
91
|
+
### Added
|
|
92
|
+
- h-claude: fulltext search option for agents/commands/skills
|
|
93
|
+
|
|
94
|
+
### Changed
|
|
95
|
+
- Refactor h-claude directory traversal and file globbing
|
|
96
|
+
|
|
97
|
+
## [0.1.299]
|
|
98
|
+
|
|
99
|
+
### Added
|
|
100
|
+
- h-pr open: support opening multiple PRs
|
|
101
|
+
|
|
102
|
+
## [0.1.298]
|
|
103
|
+
|
|
104
|
+
### Changed
|
|
105
|
+
- Use Pathname to walk up directory tree
|
|
106
|
+
- h-claude agents/commands/skills walk from pwd up to home
|
|
107
|
+
|
|
108
|
+
## [0.1.297]
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
- h rnext subcommand
|
|
112
|
+
|
|
113
|
+
## [0.1.296]
|
|
114
|
+
|
|
115
|
+
### Changed
|
|
116
|
+
- Refactor PR filter logic to pinned_pr_manager
|
|
117
|
+
- Move PR filter logic to Pr#matches_filters?
|
|
118
|
+
|
|
119
|
+
## [0.1.295]
|
|
120
|
+
|
|
121
|
+
### Changed
|
|
122
|
+
- Filter logic changes for PR management
|
|
123
|
+
```
|
data/CLAUDE.md
CHANGED
|
@@ -221,6 +221,38 @@ git.move_worktree(from, to) # Rename worktree
|
|
|
221
221
|
git.current_pr # Get current PR info
|
|
222
222
|
```
|
|
223
223
|
|
|
224
|
+
### Hiiro::DB (lib/hiiro/db.rb)
|
|
225
|
+
|
|
226
|
+
SQLite persistence layer backed by Sequel. All data is stored in `~/.config/hiiro/hiiro.db`.
|
|
227
|
+
|
|
228
|
+
**Setup:** `Hiiro::DB.setup!` is called at startup — creates any missing tables, then runs a one-time YAML→SQLite migration if the DB is new.
|
|
229
|
+
|
|
230
|
+
**Model registration:** Each Sequel model calls `Hiiro::DB.register(self)` so `setup!` can create its table:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
class Hiiro::MyModel < Sequel::Model(:my_table)
|
|
234
|
+
Hiiro::DB.register(self)
|
|
235
|
+
|
|
236
|
+
def self.create_table!(db)
|
|
237
|
+
db.create_table?(:my_table) do
|
|
238
|
+
primary_key :id
|
|
239
|
+
String :name, null: false
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**Dual-write:** During rollout, models write to both SQLite and YAML. Once migration is stable, call `Hiiro::DB.disable_dual_write!` to stop YAML writes.
|
|
246
|
+
|
|
247
|
+
**Test isolation:** Set `ENV['HIIRO_TEST_DB'] = 'sqlite::memory:'` before requiring `hiiro` to get a clean in-memory DB per test run. Call `Hiiro::DB.setup!` after require.
|
|
248
|
+
|
|
249
|
+
**`h db` subcommand:** Inspect and manage the database:
|
|
250
|
+
- `h db status` — show connection info and migration state
|
|
251
|
+
- `h db tables` — list all tables with row counts
|
|
252
|
+
- `h db q <sql>` — run raw SQL and print results
|
|
253
|
+
- `h db migrate` — re-run YAML import (if not yet migrated)
|
|
254
|
+
- `h db restore` — restore YAML files from SQLite data
|
|
255
|
+
|
|
224
256
|
### Hiiro::Fuzzyfind (lib/hiiro/fuzzyfind.rb)
|
|
225
257
|
|
|
226
258
|
Integration with `sk` (skim) or `fzf` fuzzy finders:
|
|
@@ -246,6 +278,26 @@ tm.active # Items not done/skipped
|
|
|
246
278
|
tm.filter_by_task("feature") # Items for specific task
|
|
247
279
|
```
|
|
248
280
|
|
|
281
|
+
### Invocation Tracking (lib/hiiro/invocation.rb)
|
|
282
|
+
|
|
283
|
+
Every CLI invocation is automatically recorded to SQLite via `Hiiro::Invocation` and `Hiiro::InvocationResolution`. This happens in `Hiiro.init` — no extra setup needed.
|
|
284
|
+
|
|
285
|
+
**Schema:**
|
|
286
|
+
- `Hiiro::Invocation` — records `bin_name`, `argv_json`, `cwd`, `invoked_at`
|
|
287
|
+
- `Hiiro::InvocationResolution` — linked to an invocation; records `resolved_name`, `resolution_type` (exact/prefix/abbreviated), `subcmd`
|
|
288
|
+
|
|
289
|
+
**Query recent invocations:**
|
|
290
|
+
```ruby
|
|
291
|
+
Hiiro::Invocation.order(Sequel.desc(:invoked_at)).limit(20).each do |inv|
|
|
292
|
+
puts "#{inv.invoked_at} #{inv.bin_name} #{JSON.parse(inv.argv_json).join(' ')}"
|
|
293
|
+
end
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Or via `h db q`:
|
|
297
|
+
```bash
|
|
298
|
+
h db q "SELECT bin_name, argv_json, invoked_at FROM invocations ORDER BY invoked_at DESC LIMIT 10"
|
|
299
|
+
```
|
|
300
|
+
|
|
249
301
|
### Hiiro::Shell (lib/hiiro/shell.rb)
|
|
250
302
|
|
|
251
303
|
Utility for piping content to external commands:
|
data/bin/h-branch
CHANGED
|
@@ -25,23 +25,20 @@ class BranchManager
|
|
|
25
25
|
puts "WARNING: Not in a task session, saving without task info"
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
data['branches'] ||= []
|
|
30
|
-
|
|
31
|
-
existing = data['branches'].find do |b|
|
|
32
|
-
b['name'] == branch_name && b['task'] == entry[:task]
|
|
33
|
-
end
|
|
34
|
-
|
|
28
|
+
existing = Hiiro::Branch.find_by_name(branch_name)
|
|
35
29
|
if existing
|
|
36
|
-
existing.merge!(entry.transform_keys(&:to_s))
|
|
37
|
-
existing['updated_at'] = Time.now.iso8601
|
|
38
30
|
puts "Updated branch '#{branch_name}' for task '#{entry[:task]}'"
|
|
39
31
|
else
|
|
40
|
-
data['branches'] << entry.transform_keys(&:to_s).merge('created_at' => Time.now.iso8601)
|
|
41
32
|
puts "Saved branch '#{branch_name}' for task '#{entry[:task]}'"
|
|
42
33
|
end
|
|
43
34
|
|
|
44
|
-
|
|
35
|
+
save_branch(
|
|
36
|
+
name: entry[:name],
|
|
37
|
+
worktree: entry[:worktree],
|
|
38
|
+
task: entry[:task],
|
|
39
|
+
tmux: entry[:tmux],
|
|
40
|
+
sha: entry[:sha]
|
|
41
|
+
)
|
|
45
42
|
show_entry(entry)
|
|
46
43
|
true
|
|
47
44
|
end
|
|
@@ -79,53 +76,58 @@ class BranchManager
|
|
|
79
76
|
end
|
|
80
77
|
|
|
81
78
|
def rename_saved(old_name, new_name)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
b['updated_at'] = Time.now.iso8601
|
|
88
|
-
changed = true
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
save_data(data) if changed
|
|
92
|
-
changed
|
|
79
|
+
branch = Hiiro::Branch.find_by_name(old_name)
|
|
80
|
+
return false unless branch
|
|
81
|
+
branch.update(name: new_name, updated_at: Time.now.iso8601)
|
|
82
|
+
save_yaml_backup
|
|
83
|
+
true
|
|
93
84
|
end
|
|
94
85
|
|
|
95
86
|
def remove_saved(branch_name)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
87
|
+
branch = Hiiro::Branch.find_by_name(branch_name)
|
|
88
|
+
return unless branch
|
|
89
|
+
branch.destroy
|
|
90
|
+
save_yaml_backup
|
|
100
91
|
end
|
|
101
92
|
|
|
102
93
|
def get_note(branch_name = nil)
|
|
103
94
|
branch_name ||= current_branch
|
|
104
|
-
|
|
95
|
+
Hiiro::Branch.find_by_name(branch_name)&.note
|
|
105
96
|
end
|
|
106
97
|
|
|
107
98
|
def set_note(text, branch_name = nil)
|
|
108
99
|
branch_name ||= current_branch
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
save_data(data)
|
|
100
|
+
branch = Hiiro::Branch.find_by_name(branch_name)
|
|
101
|
+
unless branch
|
|
102
|
+
entry = build_entry(branch_name)
|
|
103
|
+
branch = Hiiro::Branch.create(
|
|
104
|
+
name: branch_name,
|
|
105
|
+
worktree: entry[:worktree],
|
|
106
|
+
task: entry[:task],
|
|
107
|
+
tmux_json: Hiiro::DB::JSON.dump(entry[:tmux]),
|
|
108
|
+
sha: entry[:sha],
|
|
109
|
+
created_at: Time.now.iso8601
|
|
110
|
+
)
|
|
111
|
+
end
|
|
112
|
+
branch.update(note: text, updated_at: Time.now.iso8601)
|
|
113
|
+
save_yaml_backup
|
|
124
114
|
end
|
|
125
115
|
|
|
126
116
|
def load_data
|
|
127
|
-
|
|
128
|
-
|
|
117
|
+
Hiiro::Branch.ordered.all.each_with_object({ 'branches' => [] }) do |b, data|
|
|
118
|
+
data['branches'] << {
|
|
119
|
+
'name' => b.name,
|
|
120
|
+
'worktree' => b.worktree,
|
|
121
|
+
'task' => b.task,
|
|
122
|
+
'tmux' => b.tmux.empty? ? nil : b.tmux,
|
|
123
|
+
'sha' => b.sha,
|
|
124
|
+
'note' => b.note,
|
|
125
|
+
'created_at' => b.created_at
|
|
126
|
+
}.compact
|
|
127
|
+
end
|
|
128
|
+
rescue => e
|
|
129
|
+
warn "Branch DB read failed: #{e}"
|
|
130
|
+
load_yaml_fallback
|
|
129
131
|
end
|
|
130
132
|
|
|
131
133
|
def build_entry(branch_name)
|
|
@@ -134,11 +136,11 @@ class BranchManager
|
|
|
134
136
|
sha = `git rev-parse HEAD 2>/dev/null`.strip
|
|
135
137
|
|
|
136
138
|
{
|
|
137
|
-
name:
|
|
138
|
-
sha:
|
|
139
|
+
name: branch_name,
|
|
140
|
+
sha: sha.empty? ? nil : sha,
|
|
139
141
|
worktree: current_task&.tree_name,
|
|
140
|
-
task:
|
|
141
|
-
tmux:
|
|
142
|
+
task: current_task&.name,
|
|
143
|
+
tmux: tmux_info
|
|
142
144
|
}
|
|
143
145
|
end
|
|
144
146
|
|
|
@@ -153,8 +155,8 @@ class BranchManager
|
|
|
153
155
|
|
|
154
156
|
{
|
|
155
157
|
'session' => `tmux display-message -p '#S'`.strip,
|
|
156
|
-
'window'
|
|
157
|
-
'pane'
|
|
158
|
+
'window' => `tmux display-message -p '#W'`.strip,
|
|
159
|
+
'pane' => ENV['TMUX_PANE']
|
|
158
160
|
}
|
|
159
161
|
end
|
|
160
162
|
|
|
@@ -170,13 +172,46 @@ class BranchManager
|
|
|
170
172
|
end
|
|
171
173
|
end
|
|
172
174
|
|
|
173
|
-
def
|
|
174
|
-
|
|
175
|
+
def save_branch(name:, worktree:, task:, tmux: nil, sha: nil)
|
|
176
|
+
existing = Hiiro::Branch.find_by_name(name)
|
|
177
|
+
if existing
|
|
178
|
+
existing.update(
|
|
179
|
+
worktree: worktree,
|
|
180
|
+
task: task,
|
|
181
|
+
tmux_json: Hiiro::DB::JSON.dump(tmux),
|
|
182
|
+
sha: sha,
|
|
183
|
+
updated_at: Time.now.iso8601
|
|
184
|
+
)
|
|
185
|
+
else
|
|
186
|
+
Hiiro::Branch.create(
|
|
187
|
+
name: name,
|
|
188
|
+
worktree: worktree,
|
|
189
|
+
task: task,
|
|
190
|
+
tmux_json: Hiiro::DB::JSON.dump(tmux),
|
|
191
|
+
sha: sha,
|
|
192
|
+
created_at: Time.now.iso8601
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
save_yaml_backup
|
|
175
196
|
end
|
|
176
197
|
|
|
177
|
-
def
|
|
198
|
+
def save_yaml_backup
|
|
199
|
+
data = load_data
|
|
178
200
|
FileUtils.mkdir_p(File.dirname(data_file))
|
|
179
201
|
File.write(data_file, YAML.dump(data))
|
|
202
|
+
rescue => e
|
|
203
|
+
warn "Branch YAML backup failed: #{e}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def load_yaml_fallback
|
|
207
|
+
return {} unless File.exist?(data_file)
|
|
208
|
+
YAML.safe_load_file(data_file) || {}
|
|
209
|
+
rescue
|
|
210
|
+
{}
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def data_file
|
|
214
|
+
File.join(Dir.home, '.config', 'hiiro', 'branches.yml')
|
|
180
215
|
end
|
|
181
216
|
end
|
|
182
217
|
|
|
@@ -492,7 +527,7 @@ Hiiro.run(*ARGV) do
|
|
|
492
527
|
branch = selected.strip
|
|
493
528
|
end
|
|
494
529
|
|
|
495
|
-
system('git', 'checkout', branch)
|
|
530
|
+
system('git', 'checkout', branch, *opts.args[1..])
|
|
496
531
|
end
|
|
497
532
|
|
|
498
533
|
add_subcmd(:rm, :remove) do |*rm_args|
|
|
@@ -510,7 +545,7 @@ Hiiro.run(*ARGV) do
|
|
|
510
545
|
branch = selected.strip
|
|
511
546
|
end
|
|
512
547
|
|
|
513
|
-
system('git', 'branch', '-d', branch)
|
|
548
|
+
system('git', 'branch', '-d', branch, *opts.args[1..])
|
|
514
549
|
end
|
|
515
550
|
|
|
516
551
|
add_subcmd(:rename) do |new_name = nil, old_name = nil|
|
|
@@ -913,6 +948,48 @@ Hiiro.run(*ARGV) do
|
|
|
913
948
|
system('git', 'log', '--oneline', '--decorate', "#{forkpoint}..HEAD")
|
|
914
949
|
}
|
|
915
950
|
|
|
951
|
+
add_subcmd(:q, :query) { |*args|
|
|
952
|
+
def branch_query(args)
|
|
953
|
+
opts = Hiiro::Options.parse(args) { flag(:all, short: :a, desc: 'show all rows (no limit)') }
|
|
954
|
+
query_args = opts.args
|
|
955
|
+
db = Hiiro::DB.connection
|
|
956
|
+
limit = opts.all ? nil : 50
|
|
957
|
+
|
|
958
|
+
if query_args.empty?
|
|
959
|
+
ds = db[:branches]
|
|
960
|
+
ds = ds.limit(limit) if limit
|
|
961
|
+
rows = ds.all
|
|
962
|
+
elsif query_args[0].include?(' ') || query_args[0].upcase =~ /\A(SELECT|INSERT|UPDATE|DELETE|WITH)/
|
|
963
|
+
sql = query_args.join(' ')
|
|
964
|
+
rows = db.fetch(sql).all
|
|
965
|
+
rows = rows.first(limit) if limit
|
|
966
|
+
else
|
|
967
|
+
conditions = query_args.each_with_object({}) do |pair, h|
|
|
968
|
+
k, v = pair.split('=', 2)
|
|
969
|
+
h[k] = v if k && v
|
|
970
|
+
end
|
|
971
|
+
ds = db[:branches]
|
|
972
|
+
conditions.each { |k, v| ds = ds.where(k.to_sym => v) }
|
|
973
|
+
ds = ds.limit(limit) if limit
|
|
974
|
+
rows = ds.all
|
|
975
|
+
end
|
|
976
|
+
|
|
977
|
+
return puts "(no results)" if rows.empty?
|
|
978
|
+
keys = rows.first.keys
|
|
979
|
+
col_widths = keys.map { |k| [k.to_s.length, rows.map { |r| r[k].to_s.length }.max].max.clamp(0, 60) }
|
|
980
|
+
header = keys.each_with_index.map { |k, i| k.to_s.ljust(col_widths[i]) }.join(" ")
|
|
981
|
+
puts header
|
|
982
|
+
puts "-" * header.length
|
|
983
|
+
rows.each do |row|
|
|
984
|
+
puts keys.each_with_index.map { |k, i|
|
|
985
|
+
val = row[k].to_s; val = val[0..57] + "..." if val.length > 60; val.ljust(col_widths[i])
|
|
986
|
+
}.join(" ")
|
|
987
|
+
end
|
|
988
|
+
puts "(#{rows.length} rows)"
|
|
989
|
+
end
|
|
990
|
+
branch_query(args)
|
|
991
|
+
}
|
|
992
|
+
|
|
916
993
|
add_subcmd(:forkpoint) { |upstream = nil, branch = nil|
|
|
917
994
|
branch ||= 'HEAD'
|
|
918
995
|
upstream ||= %w[origin/master master origin/main main].find { |ref|
|
data/bin/h-db
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'hiiro'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
def print_table(rows)
|
|
7
|
+
return puts "(no results)" if rows.empty?
|
|
8
|
+
|
|
9
|
+
keys = rows.first.keys
|
|
10
|
+
col_widths = keys.map { |k|
|
|
11
|
+
[k.to_s.length, rows.map { |r| r[k].to_s.length }.max].max.clamp(0, 60)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
header = keys.each_with_index.map { |k, i| k.to_s.ljust(col_widths[i]) }.join(" ")
|
|
15
|
+
puts header
|
|
16
|
+
puts "-" * header.length
|
|
17
|
+
rows.each do |row|
|
|
18
|
+
puts keys.each_with_index.map { |k, i|
|
|
19
|
+
val = row[k].to_s
|
|
20
|
+
val = val[0..57] + "..." if val.length > 60
|
|
21
|
+
val.ljust(col_widths[i])
|
|
22
|
+
}.join(" ")
|
|
23
|
+
end
|
|
24
|
+
puts "(#{rows.length} rows)"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run_query(args)
|
|
28
|
+
opts = Hiiro::Options.parse(args) { flag(:all, short: :a, desc: 'show all rows (no limit)') }
|
|
29
|
+
query_args = opts.args
|
|
30
|
+
|
|
31
|
+
db = Hiiro::DB.connection
|
|
32
|
+
limit = opts.all ? nil : 50
|
|
33
|
+
|
|
34
|
+
if query_args.empty?
|
|
35
|
+
puts "Usage: h db q <table> [key=value ...] OR h db q \"SQL\""
|
|
36
|
+
return
|
|
37
|
+
elsif query_args[0].include?(' ') || query_args[0].upcase =~ /\A(SELECT|INSERT|UPDATE|DELETE|WITH)/
|
|
38
|
+
sql = query_args.join(' ')
|
|
39
|
+
rows = db.fetch(sql).all
|
|
40
|
+
rows = rows.first(limit) if limit
|
|
41
|
+
print_table(rows)
|
|
42
|
+
else
|
|
43
|
+
table = query_args[0]
|
|
44
|
+
conditions = query_args[1..].each_with_object({}) do |pair, h|
|
|
45
|
+
k, v = pair.split('=', 2)
|
|
46
|
+
h[k] = v if k && v
|
|
47
|
+
end
|
|
48
|
+
ds = db[table.to_sym]
|
|
49
|
+
conditions.each { |k, v| ds = ds.where(k.to_sym => v) }
|
|
50
|
+
ds = ds.limit(limit) if limit
|
|
51
|
+
rows = ds.all
|
|
52
|
+
print_table(rows)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
Hiiro.run(*ARGV) do
|
|
57
|
+
add_subcmd(:status) do
|
|
58
|
+
db_path = Hiiro::DB::DB_FILE
|
|
59
|
+
db_size = File.exist?(db_path) ? File.size(db_path) : 0
|
|
60
|
+
db_size_str = if db_size >= 1_048_576
|
|
61
|
+
"#{(db_size / 1_048_576.0).round(1)} MB"
|
|
62
|
+
elsif db_size >= 1024
|
|
63
|
+
"#{(db_size / 1024.0).round(1)} KB"
|
|
64
|
+
else
|
|
65
|
+
"#{db_size} B"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
puts "DB: #{db_path} (#{db_size_str})"
|
|
69
|
+
|
|
70
|
+
migrated = Hiiro::DB.migrated?
|
|
71
|
+
if migrated
|
|
72
|
+
ran_at = begin
|
|
73
|
+
Hiiro::DB.connection[:schema_migrations].where(name: 'yaml_import').first&.[](:ran_at)
|
|
74
|
+
rescue
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
puts "Migration: complete#{ran_at ? " (#{ran_at})" : ""}"
|
|
78
|
+
else
|
|
79
|
+
puts "Migration: not run"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
puts "Dual-write: #{Hiiro::DB.dual_write? ? 'active' : 'disabled'}"
|
|
83
|
+
puts
|
|
84
|
+
|
|
85
|
+
db = Hiiro::DB.connection
|
|
86
|
+
tables = db.tables - [:schema_migrations]
|
|
87
|
+
if tables.any?
|
|
88
|
+
puts "Table counts:"
|
|
89
|
+
max_len = tables.map(&:to_s).map(&:length).max
|
|
90
|
+
tables.sort.each do |t|
|
|
91
|
+
count = db[t].count rescue '?'
|
|
92
|
+
puts " #{t.to_s.ljust(max_len)} #{count.to_s.rjust(6)}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
config_dir = File.expand_path('~/.config/hiiro')
|
|
97
|
+
archives = Dir.glob(File.join(config_dir, 'sqlite-migration.*.tar.gz')).sort
|
|
98
|
+
puts
|
|
99
|
+
if archives.empty?
|
|
100
|
+
puts "Backups: (none)"
|
|
101
|
+
else
|
|
102
|
+
puts "Backups:"
|
|
103
|
+
archives.each { |a| puts " #{a}" }
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
add_subcmd(:tables) do
|
|
108
|
+
Hiiro::DB.connection.tables.sort.each { |t| puts t }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
add_subcmd(:q) { |*args| run_query(args) }
|
|
112
|
+
add_subcmd(:query) { |*args| run_query(args) }
|
|
113
|
+
|
|
114
|
+
add_subcmd(:migrate) do
|
|
115
|
+
unless Hiiro::DB.migrated?
|
|
116
|
+
puts "Migration has not run yet. Run `h db status` for details."
|
|
117
|
+
next
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
unless Hiiro::DB.dual_write?
|
|
121
|
+
puts "Already fully migrated. Dual-write is already disabled."
|
|
122
|
+
next
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
print "This will archive and remove all YAML files. Continue? [y/N] "
|
|
126
|
+
response = $stdin.gets.chomp
|
|
127
|
+
unless response.downcase == 'y'
|
|
128
|
+
puts "Aborted."
|
|
129
|
+
next
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
config_dir = File.expand_path('~/.config/hiiro')
|
|
133
|
+
timestamp = Time.now.strftime('%Y%m%d%H%M%S')
|
|
134
|
+
archive = File.join(config_dir, "sqlite-migration.#{timestamp}.tar.gz")
|
|
135
|
+
|
|
136
|
+
yml_files = Dir.glob(File.join(config_dir, '**/*.yml'))
|
|
137
|
+
.reject { |f| f.end_with?('.yml.bak') }
|
|
138
|
+
.map { |f| Pathname.new(f).relative_path_from(config_dir).to_s }
|
|
139
|
+
|
|
140
|
+
if yml_files.empty?
|
|
141
|
+
puts "No YAML files found to back up."
|
|
142
|
+
else
|
|
143
|
+
puts "Creating backup: #{archive}"
|
|
144
|
+
Dir.chdir(config_dir) do
|
|
145
|
+
system('tar', 'cvzf', archive, *yml_files)
|
|
146
|
+
end
|
|
147
|
+
puts "\nRemoving YAML files..."
|
|
148
|
+
yml_files.each do |rel_path|
|
|
149
|
+
full_path = File.join(config_dir, rel_path)
|
|
150
|
+
puts " rm #{full_path}"
|
|
151
|
+
File.delete(full_path)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
Hiiro::DB.disable_dual_write!
|
|
156
|
+
|
|
157
|
+
puts
|
|
158
|
+
puts "Full migration complete."
|
|
159
|
+
puts "Backup saved: #{archive}"
|
|
160
|
+
puts
|
|
161
|
+
puts "To restore YAML files:"
|
|
162
|
+
puts " cd #{config_dir}"
|
|
163
|
+
puts " tar xvzf #{File.basename(archive)}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
add_subcmd(:restore) do
|
|
167
|
+
config_dir = File.expand_path('~/.config/hiiro')
|
|
168
|
+
archives = Dir.glob(File.join(config_dir, 'sqlite-migration.*.tar.gz')).sort
|
|
169
|
+
if archives.empty?
|
|
170
|
+
puts "No backup archives found in #{config_dir}"
|
|
171
|
+
next
|
|
172
|
+
end
|
|
173
|
+
latest = archives.last
|
|
174
|
+
puts "Restoring from #{File.basename(latest)}..."
|
|
175
|
+
Dir.chdir(config_dir) { system('tar', 'xvzf', latest) }
|
|
176
|
+
puts "Done. YAML files restored."
|
|
177
|
+
end
|
|
178
|
+
end
|