appydave-tools 0.70.0 → 0.71.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/.claude/commands/brainstorming-agent.md +227 -0
- data/.claude/commands/cli-test.md +251 -0
- data/.claude/commands/dev.md +234 -0
- data/.claude/commands/po.md +227 -0
- data/.claude/commands/progress.md +51 -0
- data/.claude/commands/uat.md +321 -0
- data/.rubocop.yml +9 -0
- data/AGENTS.md +43 -0
- data/CHANGELOG.md +12 -0
- data/CLAUDE.md +26 -3
- data/README.md +15 -0
- data/bin/dam +21 -1
- data/bin/jump.rb +29 -0
- data/bin/subtitle_processor.rb +54 -1
- data/bin/zsh_history.rb +846 -0
- data/docs/README.md +162 -69
- data/docs/architecture/cli/exe-bin-convention.md +434 -0
- data/docs/architecture/cli-patterns.md +631 -0
- data/docs/architecture/gpt-context/gpt-context-architecture.md +325 -0
- data/docs/architecture/gpt-context/gpt-context-implementation-guide.md +419 -0
- data/docs/architecture/gpt-context/gpt-context-vision.md +179 -0
- data/docs/architecture/testing/testing-patterns.md +762 -0
- data/docs/backlog.md +120 -0
- data/docs/cli-tests/FR-3-jump-location-tool.md +515 -0
- data/docs/specs/fr-002-gpt-context-help-system.md +265 -0
- data/docs/specs/fr-003-jump-location-tool.md +779 -0
- data/docs/specs/zsh-history-tool.md +820 -0
- data/docs/uat/FR-3-jump-location-tool.md +741 -0
- data/exe/jump +11 -0
- data/exe/{subtitle_manager → subtitle_processor} +1 -1
- data/exe/zsh_history +11 -0
- data/lib/appydave/tools/configuration/openai.rb +1 -1
- data/lib/appydave/tools/dam/file_helper.rb +28 -0
- data/lib/appydave/tools/dam/project_listing.rb +4 -30
- data/lib/appydave/tools/dam/s3_operations.rb +2 -1
- data/lib/appydave/tools/dam/ssd_status.rb +226 -0
- data/lib/appydave/tools/dam/status.rb +3 -51
- data/lib/appydave/tools/jump/cli.rb +561 -0
- data/lib/appydave/tools/jump/commands/add.rb +52 -0
- data/lib/appydave/tools/jump/commands/base.rb +43 -0
- data/lib/appydave/tools/jump/commands/generate.rb +153 -0
- data/lib/appydave/tools/jump/commands/remove.rb +58 -0
- data/lib/appydave/tools/jump/commands/report.rb +214 -0
- data/lib/appydave/tools/jump/commands/update.rb +42 -0
- data/lib/appydave/tools/jump/commands/validate.rb +54 -0
- data/lib/appydave/tools/jump/config.rb +233 -0
- data/lib/appydave/tools/jump/formatters/base.rb +48 -0
- data/lib/appydave/tools/jump/formatters/json_formatter.rb +19 -0
- data/lib/appydave/tools/jump/formatters/paths_formatter.rb +21 -0
- data/lib/appydave/tools/jump/formatters/table_formatter.rb +183 -0
- data/lib/appydave/tools/jump/location.rb +134 -0
- data/lib/appydave/tools/jump/path_validator.rb +47 -0
- data/lib/appydave/tools/jump/search.rb +230 -0
- data/lib/appydave/tools/subtitle_processor/transcript.rb +51 -0
- data/lib/appydave/tools/version.rb +1 -1
- data/lib/appydave/tools/zsh_history/command.rb +37 -0
- data/lib/appydave/tools/zsh_history/config.rb +235 -0
- data/lib/appydave/tools/zsh_history/filter.rb +184 -0
- data/lib/appydave/tools/zsh_history/formatter.rb +75 -0
- data/lib/appydave/tools/zsh_history/parser.rb +101 -0
- data/lib/appydave/tools.rb +25 -0
- data/package.json +1 -1
- metadata +51 -4
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appydave
|
|
4
|
+
module Tools
|
|
5
|
+
module Jump
|
|
6
|
+
# Search provides fuzzy search across location metadata
|
|
7
|
+
#
|
|
8
|
+
# Scoring algorithm based on match type:
|
|
9
|
+
# - Exact key match: 100 points
|
|
10
|
+
# - Key contains term: 50 points
|
|
11
|
+
# - Brand/client alias match: 40 points
|
|
12
|
+
# - Tag match: 30 points
|
|
13
|
+
# - Type match: 20 points
|
|
14
|
+
# - Description contains: 10 points
|
|
15
|
+
# - Path contains: 5 points
|
|
16
|
+
#
|
|
17
|
+
# @example Basic search
|
|
18
|
+
# search = Search.new(config)
|
|
19
|
+
# results = search.search('appydave ruby')
|
|
20
|
+
# results[:success] # => true
|
|
21
|
+
# results[:results] # => Array of scored location hashes
|
|
22
|
+
class Search
|
|
23
|
+
SCORE_EXACT_KEY = 100
|
|
24
|
+
SCORE_KEY_CONTAINS = 50
|
|
25
|
+
SCORE_BRAND_CLIENT_ALIAS = 40
|
|
26
|
+
SCORE_TAG_MATCH = 30
|
|
27
|
+
SCORE_TYPE_MATCH = 20
|
|
28
|
+
SCORE_DESCRIPTION_CONTAINS = 10
|
|
29
|
+
SCORE_PATH_CONTAINS = 5
|
|
30
|
+
|
|
31
|
+
attr_reader :config
|
|
32
|
+
|
|
33
|
+
def initialize(config)
|
|
34
|
+
@config = config
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Search for locations matching query terms
|
|
38
|
+
#
|
|
39
|
+
# @param query [String] Space-separated search terms
|
|
40
|
+
# @return [Hash] Search results with success, count, and results
|
|
41
|
+
def search(query)
|
|
42
|
+
terms = parse_query(query)
|
|
43
|
+
|
|
44
|
+
return empty_result if terms.empty?
|
|
45
|
+
|
|
46
|
+
scored_locations = config.locations.map do |location|
|
|
47
|
+
score = calculate_score(location, terms)
|
|
48
|
+
next if score.zero?
|
|
49
|
+
|
|
50
|
+
location_to_result(location, score)
|
|
51
|
+
end.compact
|
|
52
|
+
|
|
53
|
+
# Sort by score descending, then by key alphabetically
|
|
54
|
+
sorted = scored_locations.sort_by { |r| [-r[:score], r[:key]] }
|
|
55
|
+
|
|
56
|
+
# Add index numbers
|
|
57
|
+
sorted.each_with_index { |result, i| result[:index] = i + 1 }
|
|
58
|
+
|
|
59
|
+
{
|
|
60
|
+
success: true,
|
|
61
|
+
count: sorted.size,
|
|
62
|
+
results: sorted
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get a location by exact key
|
|
67
|
+
#
|
|
68
|
+
# @param key [String] Exact key to find
|
|
69
|
+
# @return [Hash] Result with success and result/error
|
|
70
|
+
def get(key)
|
|
71
|
+
location = config.find(key)
|
|
72
|
+
|
|
73
|
+
if location
|
|
74
|
+
{
|
|
75
|
+
success: true,
|
|
76
|
+
result: location_to_result(location, SCORE_EXACT_KEY)
|
|
77
|
+
}
|
|
78
|
+
else
|
|
79
|
+
suggestions = find_suggestions(key)
|
|
80
|
+
{
|
|
81
|
+
success: false,
|
|
82
|
+
error: 'Location not found',
|
|
83
|
+
code: 'NOT_FOUND',
|
|
84
|
+
suggestion: suggestions.empty? ? nil : "Did you mean: #{suggestions.join(', ')}?"
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# List all locations
|
|
90
|
+
#
|
|
91
|
+
# @return [Hash] All locations with success and count
|
|
92
|
+
def list
|
|
93
|
+
results = config.locations.map.with_index(1) do |location, index|
|
|
94
|
+
result = location_to_result(location, 0)
|
|
95
|
+
result[:index] = index
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
{
|
|
100
|
+
success: true,
|
|
101
|
+
count: results.size,
|
|
102
|
+
results: results
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def parse_query(query)
|
|
109
|
+
return [] if query.nil? || query.strip.empty?
|
|
110
|
+
|
|
111
|
+
query.downcase.split(/\s+/).reject(&:empty?)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def empty_result
|
|
115
|
+
{
|
|
116
|
+
success: true,
|
|
117
|
+
count: 0,
|
|
118
|
+
results: []
|
|
119
|
+
}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def calculate_score(location, terms)
|
|
123
|
+
total_score = 0
|
|
124
|
+
|
|
125
|
+
terms.each do |term|
|
|
126
|
+
total_score += score_for_term(location, term)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
total_score
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
133
|
+
def score_for_term(location, term)
|
|
134
|
+
score = 0
|
|
135
|
+
|
|
136
|
+
# Exact key match
|
|
137
|
+
if location.key == term
|
|
138
|
+
score += SCORE_EXACT_KEY
|
|
139
|
+
elsif location.key&.include?(term)
|
|
140
|
+
score += SCORE_KEY_CONTAINS
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Brand/client alias match
|
|
144
|
+
score += SCORE_BRAND_CLIENT_ALIAS if brand_alias_match?(location.brand, term)
|
|
145
|
+
score += SCORE_BRAND_CLIENT_ALIAS if client_alias_match?(location.client, term)
|
|
146
|
+
|
|
147
|
+
# Tag match
|
|
148
|
+
score += SCORE_TAG_MATCH if location.tags.any? { |tag| tag.downcase == term }
|
|
149
|
+
|
|
150
|
+
# Type match
|
|
151
|
+
score += SCORE_TYPE_MATCH if location.type&.downcase == term
|
|
152
|
+
|
|
153
|
+
# Description contains
|
|
154
|
+
score += SCORE_DESCRIPTION_CONTAINS if location.description&.downcase&.include?(term)
|
|
155
|
+
|
|
156
|
+
# Path contains
|
|
157
|
+
score += SCORE_PATH_CONTAINS if location.path&.downcase&.include?(term)
|
|
158
|
+
|
|
159
|
+
score
|
|
160
|
+
end
|
|
161
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
162
|
+
|
|
163
|
+
def brand_alias_match?(brand_key, term)
|
|
164
|
+
return false unless brand_key
|
|
165
|
+
|
|
166
|
+
return true if brand_key.downcase == term
|
|
167
|
+
|
|
168
|
+
brand_info = config.brands[brand_key]
|
|
169
|
+
return false unless brand_info
|
|
170
|
+
|
|
171
|
+
aliases = brand_info['aliases'] || brand_info[:aliases] || []
|
|
172
|
+
aliases.any? { |a| a.downcase == term }
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def client_alias_match?(client_key, term)
|
|
176
|
+
return false unless client_key
|
|
177
|
+
|
|
178
|
+
return true if client_key.downcase == term
|
|
179
|
+
|
|
180
|
+
client_info = config.clients[client_key]
|
|
181
|
+
return false unless client_info
|
|
182
|
+
|
|
183
|
+
aliases = client_info['aliases'] || client_info[:aliases] || []
|
|
184
|
+
aliases.any? { |a| a.downcase == term }
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def location_to_result(location, score)
|
|
188
|
+
{
|
|
189
|
+
key: location.key,
|
|
190
|
+
path: expand_path(location.path),
|
|
191
|
+
jump: location.jump,
|
|
192
|
+
brand: location.brand,
|
|
193
|
+
client: location.client,
|
|
194
|
+
type: location.type,
|
|
195
|
+
tags: location.tags,
|
|
196
|
+
description: location.description,
|
|
197
|
+
score: score
|
|
198
|
+
}.compact
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def expand_path(path)
|
|
202
|
+
return path unless path
|
|
203
|
+
|
|
204
|
+
File.expand_path(path)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def find_suggestions(key)
|
|
208
|
+
return [] unless key
|
|
209
|
+
|
|
210
|
+
# Find keys that are similar (contain or start with same letters)
|
|
211
|
+
config.locations
|
|
212
|
+
.map(&:key)
|
|
213
|
+
.select { |k| similar?(k, key) }
|
|
214
|
+
.first(3)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def similar?(candidate, query)
|
|
218
|
+
# Simple similarity: shares first 2 chars or contains query
|
|
219
|
+
return true if candidate.start_with?(query[0..1])
|
|
220
|
+
return true if candidate.include?(query)
|
|
221
|
+
return true if query.include?(candidate)
|
|
222
|
+
|
|
223
|
+
# Levenshtein-like: at least 50% chars in common
|
|
224
|
+
common = (candidate.chars & query.chars).size
|
|
225
|
+
common >= [candidate.length, query.length].min * 0.5
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appydave
|
|
4
|
+
module Tools
|
|
5
|
+
module SubtitleProcessor
|
|
6
|
+
# Convert SRT to plain text transcript
|
|
7
|
+
# Strips timestamps and indices, keeping only the spoken text
|
|
8
|
+
class Transcript
|
|
9
|
+
attr_reader :content
|
|
10
|
+
|
|
11
|
+
def initialize(file_path: nil, srt_content: nil)
|
|
12
|
+
if file_path && srt_content
|
|
13
|
+
raise ArgumentError, 'You cannot provide both a file path and an SRT content stream.'
|
|
14
|
+
elsif file_path.nil? && srt_content.nil?
|
|
15
|
+
raise ArgumentError, 'You must provide either a file path or an SRT content stream.'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@content = if file_path
|
|
19
|
+
File.read(file_path, encoding: 'UTF-8')
|
|
20
|
+
else
|
|
21
|
+
srt_content
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Convert SRT to plain text transcript
|
|
26
|
+
# @param paragraph_gap [Integer] Number of newlines between subtitle blocks (default: 1 = single newline)
|
|
27
|
+
# @return [String] Plain text transcript
|
|
28
|
+
def extract(paragraph_gap: 1)
|
|
29
|
+
parser = Join::SRTParser.new
|
|
30
|
+
subtitles = parser.parse(@content)
|
|
31
|
+
|
|
32
|
+
separator = "\n" * paragraph_gap
|
|
33
|
+
subtitles.map(&:text).join(separator)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Write transcript to file
|
|
37
|
+
# @param output_file [String] Path to output file
|
|
38
|
+
# @param paragraph_gap [Integer] Number of newlines between subtitle blocks
|
|
39
|
+
def write(output_file, paragraph_gap: 1)
|
|
40
|
+
transcript = extract(paragraph_gap: paragraph_gap)
|
|
41
|
+
File.write(output_file, transcript, encoding: 'UTF-8')
|
|
42
|
+
puts "Transcript written to #{output_file}"
|
|
43
|
+
rescue Errno::EACCES
|
|
44
|
+
puts "Permission denied: Unable to write to #{output_file}"
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
puts "An error occurred while writing to the file: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appydave
|
|
4
|
+
module Tools
|
|
5
|
+
module ZshHistory
|
|
6
|
+
# Represents a single command from ZSH history
|
|
7
|
+
Command = Struct.new(
|
|
8
|
+
:timestamp, # Integer - Unix timestamp from history
|
|
9
|
+
:datetime, # Time - Parsed datetime
|
|
10
|
+
:text, # String - Full command text (multi-line joined)
|
|
11
|
+
:is_multiline, # Boolean - Was this a continuation command?
|
|
12
|
+
:category, # Symbol - :wanted, :unwanted, :unsure
|
|
13
|
+
:raw_lines, # Array<String> - Original lines from file
|
|
14
|
+
:matched_pattern, # String - Pattern that matched (for verbose output)
|
|
15
|
+
keyword_init: true
|
|
16
|
+
) do
|
|
17
|
+
def formatted_datetime(format = '%Y-%m-%d %H:%M:%S')
|
|
18
|
+
datetime&.strftime(format) || 'unknown'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_history_format
|
|
22
|
+
# Reconstruct in ZSH history format for writing back
|
|
23
|
+
raw_lines.join("\n")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Result of filtering operations
|
|
28
|
+
FilterResult = Struct.new(
|
|
29
|
+
:wanted, # Array<Command>
|
|
30
|
+
:unwanted, # Array<Command>
|
|
31
|
+
:unsure, # Array<Command>
|
|
32
|
+
:stats, # Hash - { total:, wanted:, unwanted:, unsure: }
|
|
33
|
+
keyword_init: true
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Appydave
|
|
4
|
+
module Tools
|
|
5
|
+
module ZshHistory
|
|
6
|
+
# Loads zsh_history configuration from ~/.config/appydave/zsh_history/
|
|
7
|
+
#
|
|
8
|
+
# Directory structure:
|
|
9
|
+
# ~/.config/appydave/zsh_history/
|
|
10
|
+
# config.txt # default_profile=crash-recovery
|
|
11
|
+
# base_exclude.txt # Always excluded (typos, output lines)
|
|
12
|
+
# profiles/
|
|
13
|
+
# crash-recovery/
|
|
14
|
+
# exclude.txt # Profile-specific excludes
|
|
15
|
+
# include.txt # Profile-specific includes
|
|
16
|
+
#
|
|
17
|
+
# Pattern files: One regex pattern per line, # for comments, blank lines ignored
|
|
18
|
+
#
|
|
19
|
+
class Config
|
|
20
|
+
DEFAULT_CONFIG_PATH = File.expand_path('~/.config/appydave/zsh_history')
|
|
21
|
+
|
|
22
|
+
attr_reader :config_path, :profile_name
|
|
23
|
+
|
|
24
|
+
def initialize(config_path: nil, profile: nil)
|
|
25
|
+
@config_path = config_path || DEFAULT_CONFIG_PATH
|
|
26
|
+
@profile_name = profile || default_profile
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns merged exclude patterns (base + profile)
|
|
30
|
+
def exclude_patterns
|
|
31
|
+
patterns = load_base_exclude
|
|
32
|
+
patterns += load_profile_patterns('exclude') if profile_name
|
|
33
|
+
patterns.empty? ? nil : patterns
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns profile include patterns
|
|
37
|
+
def include_patterns
|
|
38
|
+
patterns = load_profile_patterns('include')
|
|
39
|
+
patterns.empty? ? nil : patterns
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Check if config directory exists
|
|
43
|
+
def configured?
|
|
44
|
+
Dir.exist?(config_path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Check if specific profile exists
|
|
48
|
+
def profile_exists?(name = profile_name)
|
|
49
|
+
return false unless name
|
|
50
|
+
|
|
51
|
+
profile_path = File.join(config_path, 'profiles', name)
|
|
52
|
+
Dir.exist?(profile_path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# List available profiles
|
|
56
|
+
def available_profiles
|
|
57
|
+
profiles_dir = File.join(config_path, 'profiles')
|
|
58
|
+
return [] unless Dir.exist?(profiles_dir)
|
|
59
|
+
|
|
60
|
+
Dir.children(profiles_dir)
|
|
61
|
+
.select { |f| File.directory?(File.join(profiles_dir, f)) }
|
|
62
|
+
.sort
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get default profile from config.txt
|
|
66
|
+
def default_profile
|
|
67
|
+
config_file = File.join(config_path, 'config.txt')
|
|
68
|
+
return nil unless File.exist?(config_file)
|
|
69
|
+
|
|
70
|
+
File.readlines(config_file).each do |line|
|
|
71
|
+
line = line.strip
|
|
72
|
+
next if line.empty? || line.start_with?('#')
|
|
73
|
+
|
|
74
|
+
key, value = line.split('=', 2)
|
|
75
|
+
return value.strip if key.strip == 'default_profile'
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Create default config structure with example files
|
|
82
|
+
def self.create_default_config(config_path = DEFAULT_CONFIG_PATH)
|
|
83
|
+
FileUtils.mkdir_p(config_path)
|
|
84
|
+
FileUtils.mkdir_p(File.join(config_path, 'profiles', 'crash-recovery'))
|
|
85
|
+
|
|
86
|
+
# Create config.txt
|
|
87
|
+
write_if_missing(File.join(config_path, 'config.txt'), <<~CONFIG)
|
|
88
|
+
# ZSH History Configuration
|
|
89
|
+
# Set default profile (used when --profile not specified)
|
|
90
|
+
default_profile=crash-recovery
|
|
91
|
+
CONFIG
|
|
92
|
+
|
|
93
|
+
# Create base_exclude.txt
|
|
94
|
+
write_if_missing(File.join(config_path, 'base_exclude.txt'), <<~PATTERNS)
|
|
95
|
+
# Base exclude patterns - always applied
|
|
96
|
+
# These are noise in ANY scenario
|
|
97
|
+
|
|
98
|
+
# Typos and single-letter commands
|
|
99
|
+
^[a-z]$
|
|
100
|
+
^[a-z]{2}$
|
|
101
|
+
|
|
102
|
+
# Output lines accidentally captured
|
|
103
|
+
^\\[\\d+\\]
|
|
104
|
+
^zsh: command not found
|
|
105
|
+
^X Process completed
|
|
106
|
+
^Coverage report
|
|
107
|
+
^Line Coverage:
|
|
108
|
+
^Finished in \\d
|
|
109
|
+
^\\d+ examples, \\d+ failures
|
|
110
|
+
|
|
111
|
+
# Process listing output
|
|
112
|
+
^davidcruwys\\s+\\d+
|
|
113
|
+
PATTERNS
|
|
114
|
+
|
|
115
|
+
# Create crash-recovery profile
|
|
116
|
+
profile_path = File.join(config_path, 'profiles', 'crash-recovery')
|
|
117
|
+
|
|
118
|
+
write_if_missing(File.join(profile_path, 'exclude.txt'), <<~PATTERNS)
|
|
119
|
+
# Crash Recovery - Exclude patterns
|
|
120
|
+
# Navigation and quick-check commands (noise when finding what you were working on)
|
|
121
|
+
|
|
122
|
+
# Basic navigation
|
|
123
|
+
^ls$
|
|
124
|
+
^ls -
|
|
125
|
+
^pwd$
|
|
126
|
+
^clear$
|
|
127
|
+
^exit$
|
|
128
|
+
^x$
|
|
129
|
+
^cd$
|
|
130
|
+
^cd -$
|
|
131
|
+
^cd \\.\\.
|
|
132
|
+
^\\.\\.
|
|
133
|
+
|
|
134
|
+
# Git quick checks (not actual work)
|
|
135
|
+
^git status$
|
|
136
|
+
^git diff$
|
|
137
|
+
^git log$
|
|
138
|
+
^git pull$
|
|
139
|
+
^gs$
|
|
140
|
+
^gd$
|
|
141
|
+
^gl$
|
|
142
|
+
|
|
143
|
+
# History and lookups
|
|
144
|
+
^h$
|
|
145
|
+
^history
|
|
146
|
+
^which
|
|
147
|
+
^type
|
|
148
|
+
|
|
149
|
+
# File viewing
|
|
150
|
+
^cat
|
|
151
|
+
^head
|
|
152
|
+
^tail
|
|
153
|
+
^echo \\$
|
|
154
|
+
PATTERNS
|
|
155
|
+
|
|
156
|
+
write_if_missing(File.join(profile_path, 'include.txt'), <<~PATTERNS)
|
|
157
|
+
# Crash Recovery - Include patterns
|
|
158
|
+
# Commands that represent actual work
|
|
159
|
+
|
|
160
|
+
# Jump aliases (navigation to projects)
|
|
161
|
+
^j[a-z]
|
|
162
|
+
|
|
163
|
+
# Tools
|
|
164
|
+
^dam
|
|
165
|
+
^vat
|
|
166
|
+
^claude
|
|
167
|
+
^c-sonnet
|
|
168
|
+
|
|
169
|
+
# JavaScript/Node
|
|
170
|
+
^bun run
|
|
171
|
+
^bun dev$
|
|
172
|
+
^bun web:
|
|
173
|
+
^bun worker:
|
|
174
|
+
^bun convex:
|
|
175
|
+
^npm run
|
|
176
|
+
|
|
177
|
+
# Ruby
|
|
178
|
+
^rake
|
|
179
|
+
^bundle
|
|
180
|
+
|
|
181
|
+
# Git commits (actual work, not checks)
|
|
182
|
+
^git commit
|
|
183
|
+
^git push
|
|
184
|
+
^git add
|
|
185
|
+
^gac
|
|
186
|
+
^kfeat
|
|
187
|
+
^kfix
|
|
188
|
+
|
|
189
|
+
# Docker
|
|
190
|
+
^docker
|
|
191
|
+
^docker-compose
|
|
192
|
+
|
|
193
|
+
# Package installation
|
|
194
|
+
^brew install
|
|
195
|
+
^gem install
|
|
196
|
+
^npm install
|
|
197
|
+
PATTERNS
|
|
198
|
+
|
|
199
|
+
config_path
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
class << self
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
def write_if_missing(path, content)
|
|
206
|
+
return if File.exist?(path)
|
|
207
|
+
|
|
208
|
+
File.write(path, content)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
|
|
214
|
+
def load_base_exclude
|
|
215
|
+
load_patterns_file(File.join(config_path, 'base_exclude.txt'))
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def load_profile_patterns(type)
|
|
219
|
+
return [] unless profile_name
|
|
220
|
+
|
|
221
|
+
file_path = File.join(config_path, 'profiles', profile_name, "#{type}.txt")
|
|
222
|
+
load_patterns_file(file_path)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def load_patterns_file(file_path)
|
|
226
|
+
return [] unless File.exist?(file_path)
|
|
227
|
+
|
|
228
|
+
File.readlines(file_path)
|
|
229
|
+
.map(&:strip)
|
|
230
|
+
.reject { |line| line.empty? || line.start_with?('#') }
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|