appydave-tools 0.69.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/brainstorming-agent.md +227 -0
  3. data/.claude/commands/cli-test.md +251 -0
  4. data/.claude/commands/dev.md +234 -0
  5. data/.claude/commands/po.md +227 -0
  6. data/.claude/commands/progress.md +51 -0
  7. data/.claude/commands/uat.md +321 -0
  8. data/.rubocop.yml +11 -0
  9. data/AGENTS.md +43 -0
  10. data/CHANGELOG.md +24 -0
  11. data/CLAUDE.md +96 -3
  12. data/README.md +15 -0
  13. data/bin/dam +39 -7
  14. data/bin/jump.rb +29 -0
  15. data/bin/subtitle_processor.rb +54 -1
  16. data/bin/zsh_history.rb +846 -0
  17. data/docs/README.md +162 -68
  18. data/docs/architecture/cli/exe-bin-convention.md +434 -0
  19. data/docs/architecture/cli-patterns.md +631 -0
  20. data/docs/architecture/gpt-context/gpt-context-architecture.md +325 -0
  21. data/docs/architecture/gpt-context/gpt-context-implementation-guide.md +419 -0
  22. data/docs/architecture/gpt-context/gpt-context-vision.md +179 -0
  23. data/docs/architecture/testing/testing-patterns.md +762 -0
  24. data/docs/backlog.md +120 -0
  25. data/docs/cli-tests/FR-3-jump-location-tool.md +515 -0
  26. data/docs/dam/batch-s3-listing-requirements.md +780 -0
  27. data/docs/guides/tools/video-file-namer.md +400 -0
  28. data/docs/specs/fr-002-gpt-context-help-system.md +265 -0
  29. data/docs/specs/fr-003-jump-location-tool.md +779 -0
  30. data/docs/specs/zsh-history-tool.md +820 -0
  31. data/docs/uat/FR-3-jump-location-tool.md +741 -0
  32. data/exe/jump +11 -0
  33. data/exe/{subtitle_manager → subtitle_processor} +1 -1
  34. data/exe/zsh_history +11 -0
  35. data/lib/appydave/tools/configuration/openai.rb +1 -1
  36. data/lib/appydave/tools/dam/file_helper.rb +28 -0
  37. data/lib/appydave/tools/dam/project_listing.rb +220 -138
  38. data/lib/appydave/tools/dam/s3_operations.rb +112 -60
  39. data/lib/appydave/tools/dam/ssd_status.rb +226 -0
  40. data/lib/appydave/tools/dam/status.rb +3 -51
  41. data/lib/appydave/tools/jump/cli.rb +561 -0
  42. data/lib/appydave/tools/jump/commands/add.rb +52 -0
  43. data/lib/appydave/tools/jump/commands/base.rb +43 -0
  44. data/lib/appydave/tools/jump/commands/generate.rb +153 -0
  45. data/lib/appydave/tools/jump/commands/remove.rb +58 -0
  46. data/lib/appydave/tools/jump/commands/report.rb +214 -0
  47. data/lib/appydave/tools/jump/commands/update.rb +42 -0
  48. data/lib/appydave/tools/jump/commands/validate.rb +54 -0
  49. data/lib/appydave/tools/jump/config.rb +233 -0
  50. data/lib/appydave/tools/jump/formatters/base.rb +48 -0
  51. data/lib/appydave/tools/jump/formatters/json_formatter.rb +19 -0
  52. data/lib/appydave/tools/jump/formatters/paths_formatter.rb +21 -0
  53. data/lib/appydave/tools/jump/formatters/table_formatter.rb +183 -0
  54. data/lib/appydave/tools/jump/location.rb +134 -0
  55. data/lib/appydave/tools/jump/path_validator.rb +47 -0
  56. data/lib/appydave/tools/jump/search.rb +230 -0
  57. data/lib/appydave/tools/subtitle_processor/transcript.rb +51 -0
  58. data/lib/appydave/tools/version.rb +1 -1
  59. data/lib/appydave/tools/zsh_history/command.rb +37 -0
  60. data/lib/appydave/tools/zsh_history/config.rb +235 -0
  61. data/lib/appydave/tools/zsh_history/filter.rb +184 -0
  62. data/lib/appydave/tools/zsh_history/formatter.rb +75 -0
  63. data/lib/appydave/tools/zsh_history/parser.rb +101 -0
  64. data/lib/appydave/tools.rb +25 -0
  65. data/package.json +1 -1
  66. metadata +53 -4
data/exe/jump ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+
6
+ require 'appydave/tools'
7
+
8
+ # Set $PROGRAM_NAME to the bin/jump.rb file so the guard passes
9
+ $PROGRAM_NAME = File.expand_path('../bin/jump.rb', __dir__)
10
+
11
+ load $PROGRAM_NAME
@@ -3,4 +3,4 @@
3
3
 
4
4
  require 'appydave/tools'
5
5
 
6
- load File.expand_path('../bin/subtitle_manager.rb', __dir__)
6
+ load File.expand_path('../bin/subtitle_processor.rb', __dir__)
data/exe/zsh_history ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
+
6
+ require 'appydave/tools'
7
+
8
+ # Set $PROGRAM_NAME to the bin file so the guard passes
9
+ $PROGRAM_NAME = File.expand_path('../bin/zsh_history.rb', __dir__)
10
+
11
+ load $PROGRAM_NAME
@@ -8,7 +8,7 @@ OpenAI.configure do |config|
8
8
  tools_enabled = ENV.fetch('TOOLS_ENABLED', 'false')
9
9
 
10
10
  if tools_enabled == 'true'
11
- puts 'Tools are enabled, OpenAI will allow net connections'
11
+ # puts 'Tools are enabled, OpenAI will allow net connections'
12
12
  config.access_token = ENV.fetch('OPENAI_ACCESS_TOKEN')
13
13
  config.organization_id = ENV.fetch('OPENAI_ORGANIZATION_ID', nil)
14
14
  end
@@ -37,6 +37,34 @@ module Appydave
37
37
 
38
38
  format('%<size>.1f %<unit>s', size: bytes.to_f / (1024**exp), unit: units[exp])
39
39
  end
40
+
41
+ # Format time as relative age (e.g., "3d", "2w", "1mo")
42
+ # @param time [Time, nil] Time to format
43
+ # @return [String] Relative age string
44
+ def format_age(time)
45
+ return 'N/A' if time.nil?
46
+
47
+ seconds = Time.now - time
48
+ return 'just now' if seconds < 60
49
+
50
+ minutes = seconds / 60
51
+ return "#{minutes.round}m" if minutes < 60
52
+
53
+ hours = minutes / 60
54
+ return "#{hours.round}h" if hours < 24
55
+
56
+ days = hours / 24
57
+ return "#{days.round}d" if days < 7
58
+
59
+ weeks = days / 7
60
+ return "#{weeks.round}w" if weeks < 4
61
+
62
+ months = days / 30
63
+ return "#{months.round}mo" if months < 12
64
+
65
+ years = days / 365
66
+ "#{years.round}y"
67
+ end
40
68
  end
41
69
  end
42
70
  end
@@ -11,7 +11,7 @@ module Appydave
11
11
  class ProjectListing
12
12
  # List all brands with summary table
13
13
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
14
- def self.list_brands_with_counts(detailed: false)
14
+ def self.list_brands_with_counts(detailed: false, s3: false)
15
15
  brands = Config.available_brands
16
16
 
17
17
  if brands.empty?
@@ -19,64 +19,113 @@ module Appydave
19
19
  return
20
20
  end
21
21
 
22
- # Gather brand data
23
- brand_data = brands.map { |brand| collect_brand_data(brand, detailed: detailed) }
22
+ # Gather brand data (skip S3 if not requested)
23
+ brand_data = brands.map { |brand| collect_brand_data(brand, detailed: detailed, s3: s3) }
24
24
 
25
25
  if detailed
26
26
  # Detailed view with additional columns
27
- header = 'BRAND KEY PROJECTS SIZE LAST MODIFIED ' \
28
- 'GIT S3 SYNC PATH SSD BACKUP ' \
29
- 'WORKFLOW ACTIVE'
30
- puts header
31
- puts '-' * 200
27
+ if s3
28
+ header = 'BRAND KEY PROJECTS SIZE LAST MODIFIED ' \
29
+ 'GIT S3 SYNC PATH SSD BACKUP ' \
30
+ 'WORKFLOW ACTIVE'
31
+ puts header
32
+ puts '-' * 200
33
+ else
34
+ header = 'BRAND KEY PROJECTS SIZE LAST MODIFIED ' \
35
+ 'GIT PATH SSD BACKUP ' \
36
+ 'WORKFLOW ACTIVE'
37
+ puts header
38
+ puts '-' * 189
39
+ end
32
40
 
33
41
  brand_data.each do |data|
34
42
  brand_display = "#{data[:shortcut]} - #{data[:name]}"
35
43
 
36
- puts format(
37
- '%-30s %-12s %10d %12s %20s %-15s %-10s %-35s %-30s %-10s %6d',
38
- brand_display,
39
- data[:key],
40
- data[:count],
41
- format_size(data[:size]),
42
- format_date(data[:modified]),
43
- data[:git_status],
44
- data[:s3_sync],
45
- shorten_path(data[:path]),
46
- data[:ssd_backup] || 'N/A',
47
- data[:workflow] || 'N/A',
48
- data[:active_count] || 0
49
- )
44
+ if s3
45
+ puts format(
46
+ '%-30s %-12s %10d %12s %20s %-15s %-10s %-35s %-30s %-10s %6d',
47
+ brand_display,
48
+ data[:key],
49
+ data[:count],
50
+ format_size(data[:size]),
51
+ format_date(data[:modified]),
52
+ data[:git_status],
53
+ data[:s3_sync],
54
+ shorten_path(data[:path]),
55
+ data[:ssd_backup] || 'N/A',
56
+ data[:workflow] || 'N/A',
57
+ data[:active_count] || 0
58
+ )
59
+ else
60
+ puts format(
61
+ '%-30s %-12s %10d %12s %20s %-15s %-35s %-30s %-10s %6d',
62
+ brand_display,
63
+ data[:key],
64
+ data[:count],
65
+ format_size(data[:size]),
66
+ format_date(data[:modified]),
67
+ data[:git_status],
68
+ shorten_path(data[:path]),
69
+ data[:ssd_backup] || 'N/A',
70
+ data[:workflow] || 'N/A',
71
+ data[:active_count] || 0
72
+ )
73
+ end
50
74
  end
51
75
  else
52
76
  # Default view - use same format for header and data
53
77
  # rubocop:disable Style/RedundantFormat
54
- puts format(
55
- '%-30s %-15s %10s %12s %20s %-15s %-10s',
56
- 'BRAND',
57
- 'KEY',
58
- 'PROJECTS',
59
- 'SIZE',
60
- 'LAST MODIFIED',
61
- 'GIT',
62
- 'S3 SYNC'
63
- )
78
+ if s3
79
+ puts format(
80
+ '%-30s %-15s %10s %12s %20s %-15s %-10s',
81
+ 'BRAND',
82
+ 'KEY',
83
+ 'PROJECTS',
84
+ 'SIZE',
85
+ 'LAST MODIFIED',
86
+ 'GIT',
87
+ 'S3 SYNC'
88
+ )
89
+ puts '-' * 133
90
+ else
91
+ puts format(
92
+ '%-30s %-15s %10s %12s %20s %-15s',
93
+ 'BRAND',
94
+ 'KEY',
95
+ 'PROJECTS',
96
+ 'SIZE',
97
+ 'LAST MODIFIED',
98
+ 'GIT'
99
+ )
100
+ puts '-' * 122
101
+ end
64
102
  # rubocop:enable Style/RedundantFormat
65
- puts '-' * 133
66
103
 
67
104
  brand_data.each do |data|
68
105
  brand_display = "#{data[:shortcut]} - #{data[:name]}"
69
106
 
70
- puts format(
71
- '%-30s %-15s %10d %12s %20s %-15s %-10s',
72
- brand_display,
73
- data[:key],
74
- data[:count],
75
- format_size(data[:size]),
76
- format_date(data[:modified]),
77
- data[:git_status],
78
- data[:s3_sync]
79
- )
107
+ if s3
108
+ puts format(
109
+ '%-30s %-15s %10d %12s %20s %-15s %-10s',
110
+ brand_display,
111
+ data[:key],
112
+ data[:count],
113
+ format_size(data[:size]),
114
+ format_date(data[:modified]),
115
+ data[:git_status],
116
+ data[:s3_sync]
117
+ )
118
+ else
119
+ puts format(
120
+ '%-30s %-15s %10d %12s %20s %-15s',
121
+ brand_display,
122
+ data[:key],
123
+ data[:count],
124
+ format_size(data[:size]),
125
+ format_date(data[:modified]),
126
+ data[:git_status]
127
+ )
128
+ end
80
129
  end
81
130
  end
82
131
 
@@ -93,7 +142,7 @@ module Appydave
93
142
 
94
143
  # List all projects for a specific brand (Mode 3)
95
144
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
96
- def self.list_brand_projects(brand_arg, detailed: false)
145
+ def self.list_brand_projects(brand_arg, detailed: false, s3: false)
97
146
  # ProjectResolver expects the original brand key/shortcut, not the expanded v-* version
98
147
  projects = ProjectResolver.list_projects(brand_arg)
99
148
 
@@ -114,13 +163,13 @@ module Appydave
114
163
  return
115
164
  end
116
165
 
117
- # Gather project data
166
+ # Gather project data (skip S3 if not requested)
118
167
  brand_path = Config.brand_path(brand_arg)
119
168
  brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand_arg)
120
169
  is_git_repo = Dir.exist?(File.join(brand_path, '.git'))
121
170
 
122
171
  project_data = projects.map do |project|
123
- collect_project_data(brand_arg, brand_path, brand_info, project, is_git_repo, detailed: detailed)
172
+ collect_project_data(brand_arg, brand_path, brand_info, project, is_git_repo, detailed: detailed, s3: s3)
124
173
  end
125
174
 
126
175
  # Print common header
@@ -132,67 +181,119 @@ module Appydave
132
181
  if detailed
133
182
  # Detailed view with additional columns - use same format for header and data
134
183
  # rubocop:disable Style/RedundantFormat
135
- puts format(
136
- '%-45s %12s %15s %-15s %-12s %-65s %-18s %-18s %-30s %-15s %-15s',
137
- 'PROJECT',
138
- 'SIZE',
139
- 'AGE',
140
- 'GIT',
141
- 'S3',
142
- 'PATH',
143
- 'HEAVY FILES',
144
- 'LIGHT FILES',
145
- 'SSD BACKUP',
146
- 'S3 ↑ UPLOAD',
147
- 'S3 DOWNLOAD'
148
- )
184
+ if s3
185
+ puts format(
186
+ '%-45s %12s %15s %-15s %-12s %-65s %-18s %-18s %-30s %-15s %-15s',
187
+ 'PROJECT',
188
+ 'SIZE',
189
+ 'AGE',
190
+ 'GIT',
191
+ 'S3',
192
+ 'PATH',
193
+ 'HEAVY FILES',
194
+ 'LIGHT FILES',
195
+ 'SSD BACKUP',
196
+ 'S3 UPLOAD',
197
+ 'S3 ↓ DOWNLOAD'
198
+ )
199
+ puts '-' * 280
200
+ else
201
+ puts format(
202
+ '%-45s %12s %15s %-15s %-65s %-18s %-18s %-30s',
203
+ 'PROJECT',
204
+ 'SIZE',
205
+ 'AGE',
206
+ 'GIT',
207
+ 'PATH',
208
+ 'HEAVY FILES',
209
+ 'LIGHT FILES',
210
+ 'SSD BACKUP'
211
+ )
212
+ puts '-' * 239
213
+ end
149
214
  # rubocop:enable Style/RedundantFormat
150
- puts '-' * 280
151
215
 
152
216
  project_data.each do |data|
153
217
  age_display = data[:stale] ? "#{data[:age]} ⚠️" : data[:age]
154
- s3_upload = data[:s3_last_upload] ? format_age(data[:s3_last_upload]) : 'N/A'
155
- s3_download = data[:s3_last_download] ? format_age(data[:s3_last_download]) : 'N/A'
156
218
 
157
- puts format(
158
- '%-45s %12s %15s %-15s %-12s %-65s %-18s %-18s %-30s %-15s %-15s',
159
- data[:name],
160
- format_size(data[:size]),
161
- age_display,
162
- data[:git_status],
163
- data[:s3_sync],
164
- shorten_path(data[:path]),
165
- data[:heavy_files] || 'N/A',
166
- data[:light_files] || 'N/A',
167
- data[:ssd_backup] || 'N/A',
168
- s3_upload,
169
- s3_download
170
- )
219
+ if s3
220
+ s3_upload = data[:s3_last_upload] ? FileHelper.format_age(data[:s3_last_upload]) : 'N/A'
221
+ s3_download = data[:s3_last_download] ? FileHelper.format_age(data[:s3_last_download]) : 'N/A'
222
+
223
+ puts format(
224
+ '%-45s %12s %15s %-15s %-12s %-65s %-18s %-18s %-30s %-15s %-15s',
225
+ data[:name],
226
+ format_size(data[:size]),
227
+ age_display,
228
+ data[:git_status],
229
+ data[:s3_sync],
230
+ shorten_path(data[:path]),
231
+ data[:heavy_files] || 'N/A',
232
+ data[:light_files] || 'N/A',
233
+ data[:ssd_backup] || 'N/A',
234
+ s3_upload,
235
+ s3_download
236
+ )
237
+ else
238
+ puts format(
239
+ '%-45s %12s %15s %-15s %-65s %-18s %-18s %-30s',
240
+ data[:name],
241
+ format_size(data[:size]),
242
+ age_display,
243
+ data[:git_status],
244
+ shorten_path(data[:path]),
245
+ data[:heavy_files] || 'N/A',
246
+ data[:light_files] || 'N/A',
247
+ data[:ssd_backup] || 'N/A'
248
+ )
249
+ end
171
250
  end
172
251
  else
173
252
  # Default view - use same format for header and data
174
253
  # rubocop:disable Style/RedundantFormat
175
- puts format(
176
- '%-45s %12s %15s %-15s %-12s',
177
- 'PROJECT',
178
- 'SIZE',
179
- 'AGE',
180
- 'GIT',
181
- 'S3'
182
- )
254
+ if s3
255
+ puts format(
256
+ '%-45s %12s %15s %-15s %-12s',
257
+ 'PROJECT',
258
+ 'SIZE',
259
+ 'AGE',
260
+ 'GIT',
261
+ 'S3'
262
+ )
263
+ puts '-' * 130
264
+ else
265
+ puts format(
266
+ '%-45s %12s %15s %-15s',
267
+ 'PROJECT',
268
+ 'SIZE',
269
+ 'AGE',
270
+ 'GIT'
271
+ )
272
+ puts '-' * 117
273
+ end
183
274
  # rubocop:enable Style/RedundantFormat
184
- puts '-' * 130
185
275
 
186
276
  project_data.each do |data|
187
277
  age_display = data[:stale] ? "#{data[:age]} ⚠️" : data[:age]
188
- puts format(
189
- '%-45s %12s %15s %-15s %-12s',
190
- data[:name],
191
- format_size(data[:size]),
192
- age_display,
193
- data[:git_status],
194
- data[:s3_sync]
195
- )
278
+
279
+ if s3
280
+ puts format(
281
+ '%-45s %12s %15s %-15s %-12s',
282
+ data[:name],
283
+ format_size(data[:size]),
284
+ age_display,
285
+ data[:git_status],
286
+ data[:s3_sync]
287
+ )
288
+ else
289
+ puts format(
290
+ '%-45s %12s %15s %-15s',
291
+ data[:name],
292
+ format_size(data[:size]),
293
+ age_display,
294
+ data[:git_status]
295
+ )
296
+ end
196
297
  end
197
298
  end
198
299
 
@@ -237,7 +338,7 @@ module Appydave
237
338
  path: project_path,
238
339
  size: size,
239
340
  modified: modified,
240
- age: format_age(modified),
341
+ age: FileHelper.format_age(modified),
241
342
  stale: stale?(modified)
242
343
  }
243
344
  end
@@ -313,7 +414,7 @@ module Appydave
313
414
 
314
415
  # Collect brand data for display
315
416
  # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
316
- def self.collect_brand_data(brand, detailed: false)
417
+ def self.collect_brand_data(brand, detailed: false, s3: false)
317
418
  Appydave::Tools::Configuration::Config.configure
318
419
  brand_info = Appydave::Tools::Configuration::Config.brands.get_brand(brand)
319
420
  brand_path = Config.brand_path(brand)
@@ -331,8 +432,8 @@ module Appydave
331
432
  # Get git status
332
433
  git_status = calculate_git_status(brand_path)
333
434
 
334
- # Get S3 sync status (count of projects with s3-staging)
335
- s3_sync_status = calculate_s3_sync_status(brand, projects)
435
+ # Get S3 sync status (count of projects with s3-staging) - only if requested
436
+ s3_sync_status = s3 ? calculate_s3_sync_status(brand, projects) : 'N/A'
336
437
 
337
438
  result = {
338
439
  shortcut: shortcut || key,
@@ -449,7 +550,7 @@ module Appydave
449
550
 
450
551
  # Collect project data for display
451
552
  # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/ParameterLists
452
- def self.collect_project_data(brand_arg, brand_path, brand_info, project, is_git_repo, detailed: false)
553
+ def self.collect_project_data(brand_arg, brand_path, brand_info, project, is_git_repo, detailed: false, s3: false)
453
554
  project_path = Config.project_path(brand_arg, project)
454
555
  size = FileHelper.calculate_directory_size(project_path)
455
556
  modified = File.mtime(project_path)
@@ -461,15 +562,15 @@ module Appydave
461
562
  'N/A'
462
563
  end
463
564
 
464
- # Calculate 3-state S3 sync status
465
- s3_sync = calculate_project_s3_sync_status(brand_arg, brand_info, project)
565
+ # Calculate 3-state S3 sync status - only if requested (performance optimization)
566
+ s3_sync = s3 ? calculate_project_s3_sync_status(brand_arg, brand_info, project) : 'N/A'
466
567
 
467
568
  result = {
468
569
  name: project,
469
570
  path: project_path,
470
571
  size: size,
471
572
  modified: modified,
472
- age: format_age(modified),
573
+ age: FileHelper.format_age(modified),
473
574
  stale: stale?(modified),
474
575
  git_status: git_status,
475
576
  s3_sync: s3_sync
@@ -500,16 +601,23 @@ module Appydave
500
601
  File.exist?(ssd_project_path) ? shorten_path(ssd_project_path) : nil
501
602
  end
502
603
 
503
- # S3 timestamps (last upload/download)
504
- s3_timestamps = calculate_s3_timestamps(brand_arg, brand_info, project)
505
-
506
- result.merge!(
507
- heavy_files: "#{heavy_count} (#{format_size(heavy_size)})",
508
- light_files: "#{light_count} (#{format_size(light_size)})",
509
- ssd_backup: ssd_path,
510
- s3_last_upload: s3_timestamps[:last_upload],
511
- s3_last_download: s3_timestamps[:last_download]
512
- )
604
+ # S3 timestamps (last upload/download) - only if requested (performance optimization)
605
+ if s3
606
+ s3_timestamps = calculate_s3_timestamps(brand_arg, brand_info, project)
607
+ result.merge!(
608
+ heavy_files: "#{heavy_count} (#{format_size(heavy_size)})",
609
+ light_files: "#{light_count} (#{format_size(light_size)})",
610
+ ssd_backup: ssd_path,
611
+ s3_last_upload: s3_timestamps[:last_upload],
612
+ s3_last_download: s3_timestamps[:last_download]
613
+ )
614
+ else
615
+ result.merge!(
616
+ heavy_files: "#{heavy_count} (#{format_size(heavy_size)})",
617
+ light_files: "#{light_count} (#{format_size(light_size)})",
618
+ ssd_backup: ssd_path
619
+ )
620
+ end
513
621
  end
514
622
 
515
623
  result
@@ -544,32 +652,6 @@ module Appydave
544
652
  time.strftime('%Y-%m-%d %H:%M')
545
653
  end
546
654
 
547
- # Format age as relative time (e.g., "3 days", "2 weeks")
548
- def self.format_age(time)
549
- return 'N/A' if time.nil?
550
-
551
- seconds = Time.now - time
552
- return 'just now' if seconds < 60
553
-
554
- minutes = seconds / 60
555
- return "#{minutes.round}m" if minutes < 60
556
-
557
- hours = minutes / 60
558
- return "#{hours.round}h" if hours < 24
559
-
560
- days = hours / 24
561
- return "#{days.round}d" if days < 7
562
-
563
- weeks = days / 7
564
- return "#{weeks.round}w" if weeks < 4
565
-
566
- months = days / 30
567
- return "#{months.round}mo" if months < 12
568
-
569
- years = days / 365
570
- "#{years.round}y"
571
- end
572
-
573
655
  # Check if project is stale (>90 days old)
574
656
  def self.stale?(time)
575
657
  return false if time.nil?