appydave-tools 0.18.5 → 0.20.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.
data/docs/dam/usage.md CHANGED
@@ -154,6 +154,143 @@ dam list appydave 'b6*'
154
154
  # Output: Lists b60, b61, b62...b69 projects
155
155
  ```
156
156
 
157
+ ### Status & Monitoring
158
+
159
+ #### `dam status [brand] [project]`
160
+ Show unified status for project or brand (local, S3, SSD, git).
161
+
162
+ **Brand status:**
163
+ ```bash
164
+ dam status appydave
165
+ ```
166
+
167
+ **Output:**
168
+ ```
169
+ 📊 Brand Status: v-appydave
170
+ 📡 Git Remote: git@github.com:klueless-io/v-appydave.git
171
+
172
+ 🌿 Branch: main
173
+ 📡 Remote: git@github.com:klueless-io/v-appydave.git
174
+ ✓ Working directory clean
175
+ ✓ Up to date with remote
176
+
177
+ 📋 Manifest Summary:
178
+ Total projects: 114
179
+ Local: 74
180
+ S3 staging: 12
181
+ SSD backup: 67
182
+
183
+ Storyline projects: 3
184
+ FliVideo projects: 111
185
+ ```
186
+
187
+ **Project status:**
188
+ ```bash
189
+ dam status appydave b65
190
+ ```
191
+
192
+ **Output:**
193
+ ```
194
+ 📊 Status: v-appydave/b65-guy-monroe-marketing-plan
195
+
196
+ Storage:
197
+ 📁 Local: ✓ exists (flat structure)
198
+ Heavy files: no
199
+ Light files: yes
200
+
201
+ ☁️ S3 Staging: ✓ exists
202
+ Local staging files: 3
203
+
204
+ 💾 SSD Backup: ✓ exists
205
+ Path: b65-guy-monroe-marketing-plan
206
+
207
+ Git:
208
+ 🌿 Branch: main
209
+ 📡 Remote: git@github.com:klueless-io/v-appydave.git
210
+ ↕️ Status: Clean working directory
211
+ 🔄 Sync: Up to date
212
+ ```
213
+
214
+ **Auto-detect from PWD:**
215
+ ```bash
216
+ cd ~/dev/video-projects/v-appydave/b65-project
217
+ dam status
218
+ ```
219
+
220
+ ### Git Repository Commands
221
+
222
+ #### `dam repo-status [brand] [--all]`
223
+ Check git status for brand repositories.
224
+
225
+ ```bash
226
+ # Single brand
227
+ dam repo-status appydave
228
+
229
+ # All brands
230
+ dam repo-status --all
231
+ ```
232
+
233
+ **Output:**
234
+ ```
235
+ 🔍 Git Status: v-appydave
236
+
237
+ 🌿 Branch: main
238
+ 📡 Remote: git@github.com:klueless-io/v-appydave.git
239
+ ✓ Working directory clean
240
+ ✓ Up to date with remote
241
+ ```
242
+
243
+ #### `dam repo-sync [brand] [--all]`
244
+ Pull updates for brand repositories.
245
+
246
+ ```bash
247
+ # Single brand
248
+ dam repo-sync appydave
249
+
250
+ # All brands
251
+ dam repo-sync --all
252
+ ```
253
+
254
+ **Output:**
255
+ ```
256
+ 🔄 Syncing: v-appydave
257
+
258
+ ✓ Already up to date
259
+ ```
260
+
261
+ **Safety features:**
262
+ - Skips brands with uncommitted changes (prevents conflicts)
263
+ - Shows summary when syncing multiple brands
264
+
265
+ #### `dam repo-push [brand] [project]`
266
+ Push changes for brand repository.
267
+
268
+ ```bash
269
+ # Push all changes
270
+ dam repo-push appydave
271
+
272
+ # Validate project exists before push
273
+ dam repo-push appydave b65
274
+ ```
275
+
276
+ **Output:**
277
+ ```
278
+ 📤 Pushing: v-appydave
279
+
280
+ ✓ Project validated: b65-guy-monroe-marketing-plan
281
+
282
+ 📤 Pushing 2 commit(s)...
283
+
284
+ ✓ Push successful
285
+
286
+ main -> main
287
+ ```
288
+
289
+ **Safety features:**
290
+ - Warns if uncommitted changes detected
291
+ - Optional project validation against manifest
292
+ - Shows push summary
293
+
157
294
  ### S3 Sync Commands
158
295
 
159
296
  #### `dam s3-up [brand] [project] [--dry-run]`
@@ -649,4 +786,4 @@ dam s3-up appydave b65
649
786
 
650
787
  ---
651
788
 
652
- **Last Updated:** 2025-11-08
789
+ **Last Updated:** 2025-11-10
@@ -117,6 +117,7 @@ module Appydave
117
117
  'type' => 'owned',
118
118
  'youtube_channels' => [],
119
119
  'team' => [],
120
+ 'git_remote' => nil,
120
121
  'locations' => {
121
122
  'video_projects' => '',
122
123
  'ssd_backup' => ''
@@ -144,7 +145,7 @@ module Appydave
144
145
 
145
146
  # Type-safe class to access brand properties
146
147
  class BrandInfo
147
- attr_accessor :key, :name, :shortcut, :type, :youtube_channels, :team, :locations, :aws, :settings
148
+ attr_accessor :key, :name, :shortcut, :type, :youtube_channels, :team, :git_remote, :locations, :aws, :settings
148
149
 
149
150
  def initialize(key, data)
150
151
  @key = key
@@ -153,6 +154,7 @@ module Appydave
153
154
  @type = data['type'] || 'owned'
154
155
  @youtube_channels = data['youtube_channels'] || []
155
156
  @team = data['team'] || []
157
+ @git_remote = data['git_remote']
156
158
  @locations = BrandLocation.new(data['locations'] || {})
157
159
  @aws = BrandAws.new(data['aws'] || {})
158
160
  @settings = BrandSettings.new(data['settings'] || {})
@@ -165,6 +167,7 @@ module Appydave
165
167
  'type' => @type,
166
168
  'youtube_channels' => @youtube_channels,
167
169
  'team' => @team,
170
+ 'git_remote' => @git_remote,
168
171
  'locations' => @locations.to_h,
169
172
  'aws' => @aws.to_h,
170
173
  'settings' => @settings.to_h
@@ -43,6 +43,31 @@ module Appydave
43
43
  path
44
44
  end
45
45
 
46
+ # Get git remote URL for a brand (with self-healing)
47
+ # @param brand_key [String] Brand key (e.g., 'appydave', 'voz')
48
+ # @return [String, nil] Git remote URL or nil if not a git repo
49
+ def git_remote(brand_key)
50
+ Appydave::Tools::Configuration::Config.configure
51
+ brands_config = Appydave::Tools::Configuration::Config.brands
52
+ brand_info = brands_config.get_brand(brand_key)
53
+
54
+ # 1. Check if git_remote is already configured
55
+ return brand_info.git_remote if brand_info.git_remote && !brand_info.git_remote.empty?
56
+
57
+ # 2. Try to infer from git command
58
+ brand_path_dir = brand_path(brand_key)
59
+ inferred_remote = infer_git_remote(brand_path_dir)
60
+
61
+ # 3. Auto-save if inferred successfully
62
+ if inferred_remote
63
+ brand_info.git_remote = inferred_remote
64
+ brands_config.set_brand(brand_info.key, brand_info)
65
+ brands_config.save
66
+ end
67
+
68
+ inferred_remote
69
+ end
70
+
46
71
  # Expand brand shortcut to full brand name
47
72
  # Reads from brands.json if available, falls back to hardcoded shortcuts
48
73
  # @param shortcut [String] Brand shortcut (e.g., 'appydave', 'ad', 'APPYDAVE')
@@ -165,6 +190,19 @@ module Appydave
165
190
  config.save
166
191
  ERROR
167
192
  end
193
+
194
+ # Infer git remote URL from git repository
195
+ # @param path [String] Path to git repository
196
+ # @return [String, nil] Remote URL or nil if not a git repo
197
+ def infer_git_remote(path)
198
+ return nil unless Dir.exist?(path)
199
+
200
+ # Try to get git remote URL
201
+ result = `git -C "#{path}" remote get-url origin 2>/dev/null`.strip
202
+ result.empty? ? nil : result
203
+ rescue StandardError
204
+ nil
205
+ end
168
206
  end
169
207
  end
170
208
  end
@@ -157,6 +157,14 @@ module Appydave
157
157
  'archived'
158
158
  end
159
159
 
160
+ # Check S3 staging (only if local exists)
161
+ s3_staging_path = File.join(local_path, 's3-staging')
162
+ s3_exists = local_exists && Dir.exist?(s3_staging_path)
163
+
164
+ # Check for storyline.json
165
+ storyline_json_path = File.join(local_path, 'data', 'storyline.json')
166
+ has_storyline_json = local_exists && File.exist?(storyline_json_path)
167
+
160
168
  # Check SSD (try both flat and range-based structures)
161
169
  ssd_exists = if ssd_available
162
170
  flat_ssd_path = File.join(ssd_backup, project_id)
@@ -168,11 +176,16 @@ module Appydave
168
176
 
169
177
  {
170
178
  id: project_id,
179
+ type: has_storyline_json ? 'storyline-app' : 'flivideo',
180
+ hasStorylineJson: has_storyline_json,
171
181
  storage: {
172
182
  ssd: {
173
183
  exists: ssd_exists,
174
184
  path: ssd_exists ? project_id : nil
175
185
  },
186
+ s3: {
187
+ exists: s3_exists
188
+ },
176
189
  local: {
177
190
  exists: local_exists,
178
191
  structure: structure,
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Dam
6
+ # Push brand repository changes
7
+ class RepoPush
8
+ attr_reader :brand, :project_id, :brand_info, :brand_path
9
+
10
+ def initialize(brand, project_id = nil)
11
+ @brand_info = load_brand_info(brand)
12
+ @brand = @brand_info.key
13
+ @brand_path = Config.brand_path(@brand)
14
+ @project_id = project_id
15
+ end
16
+
17
+ # Push changes
18
+ def push
19
+ puts "📤 Pushing: v-#{brand}"
20
+ puts ''
21
+
22
+ unless git_repo?
23
+ puts "❌ Not a git repository: #{brand_path}"
24
+ return
25
+ end
26
+
27
+ # If project specified, validate it exists in manifest
28
+ validate_project if project_id
29
+
30
+ perform_push
31
+ end
32
+
33
+ private
34
+
35
+ def load_brand_info(brand)
36
+ Appydave::Tools::Configuration::Config.configure
37
+ Appydave::Tools::Configuration::Config.brands.get_brand(brand)
38
+ end
39
+
40
+ def git_repo?
41
+ git_dir = File.join(brand_path, '.git')
42
+ Dir.exist?(git_dir)
43
+ end
44
+
45
+ def validate_project
46
+ manifest = load_manifest
47
+ unless manifest
48
+ puts '⚠️ Warning: Manifest not found'
49
+ puts ' Continuing with push (manifest is optional for validation)'
50
+ return
51
+ end
52
+
53
+ # Resolve short name if needed (b65 -> b65-full-name)
54
+ resolved = ProjectResolver.new.resolve(brand, project_id)
55
+
56
+ project_entry = manifest[:projects].find { |p| p[:id] == resolved }
57
+ if project_entry
58
+ puts "✓ Project validated: #{resolved}"
59
+ else
60
+ puts "⚠️ Warning: Project '#{resolved}' not found in manifest"
61
+ puts ' Continuing with push (manifest may be outdated)'
62
+ end
63
+ puts ''
64
+ end
65
+
66
+ def load_manifest
67
+ manifest_path = File.join(brand_path, 'projects.json')
68
+ return nil unless File.exist?(manifest_path)
69
+
70
+ JSON.parse(File.read(manifest_path), symbolize_names: true)
71
+ rescue JSON::ParserError
72
+ nil
73
+ end
74
+
75
+ def perform_push
76
+ # Check for uncommitted changes
77
+ if uncommitted_changes?
78
+ puts '⚠️ Warning: Uncommitted changes detected'
79
+ puts ''
80
+ end
81
+
82
+ # Check if ahead of remote
83
+ ahead = commits_ahead
84
+ if ahead.zero?
85
+ puts '✓ Nothing to push (up to date with remote)'
86
+ return
87
+ end
88
+
89
+ puts "📤 Pushing #{ahead} commit(s)..."
90
+ puts ''
91
+
92
+ # Perform git push
93
+ output = `git -C "#{brand_path}" push 2>&1`
94
+ exit_code = $CHILD_STATUS.exitstatus
95
+
96
+ if exit_code.zero?
97
+ puts '✓ Push successful'
98
+ puts ''
99
+ show_push_summary(output)
100
+ else
101
+ puts '❌ Push failed:'
102
+ puts output
103
+ exit 1
104
+ end
105
+ end
106
+
107
+ def uncommitted_changes?
108
+ output = `git -C "#{brand_path}" status --porcelain 2>/dev/null`.strip
109
+ !output.empty?
110
+ rescue StandardError
111
+ false
112
+ end
113
+
114
+ def commits_ahead
115
+ `git -C "#{brand_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
116
+ rescue StandardError
117
+ 0
118
+ end
119
+
120
+ def show_push_summary(output)
121
+ # Extract useful info from git push output
122
+ lines = output.lines.map(&:strip).reject(&:empty?)
123
+
124
+ lines.each do |line|
125
+ puts " #{line}" if line.include?('->') || line.include?('branch')
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,140 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Dam
6
+ # Show git status for brand repositories
7
+ class RepoStatus
8
+ attr_reader :brand, :brand_info, :brand_path
9
+
10
+ def initialize(brand = nil)
11
+ return unless brand
12
+
13
+ @brand_info = load_brand_info(brand)
14
+ @brand = @brand_info.key
15
+ @brand_path = Config.brand_path(@brand)
16
+ end
17
+
18
+ # Show status for single brand
19
+ def show
20
+ puts "🔍 Git Status: v-#{brand}"
21
+ puts ''
22
+
23
+ unless git_repo?
24
+ puts "❌ Not a git repository: #{brand_path}"
25
+ return
26
+ end
27
+
28
+ show_git_info
29
+ end
30
+
31
+ # Show status for all brands
32
+ def show_all
33
+ puts '🔍 Git Status - All Brands'
34
+ puts ''
35
+
36
+ Appydave::Tools::Configuration::Config.configure
37
+ brands_config = Appydave::Tools::Configuration::Config.brands
38
+
39
+ brands_config.brands.each do |brand_info|
40
+ @brand_info = brand_info
41
+ @brand = brand_info.key
42
+ @brand_path = Config.brand_path(@brand)
43
+
44
+ next unless git_repo?
45
+
46
+ puts "📦 v-#{brand}"
47
+ show_git_info(indent: ' ')
48
+ puts ''
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def load_brand_info(brand)
55
+ Appydave::Tools::Configuration::Config.configure
56
+ Appydave::Tools::Configuration::Config.brands.get_brand(brand)
57
+ end
58
+
59
+ def git_repo?
60
+ git_dir = File.join(brand_path, '.git')
61
+ Dir.exist?(git_dir)
62
+ end
63
+
64
+ def show_git_info(indent: '')
65
+ status = git_status_info
66
+
67
+ puts "#{indent}🌿 Branch: #{status[:branch]}"
68
+ puts "#{indent}📡 Remote: #{status[:remote]}" if status[:remote]
69
+
70
+ if status[:modified_count].positive? || status[:untracked_count].positive?
71
+ puts "#{indent}↕️ Changes: #{status[:modified_count]} modified, #{status[:untracked_count]} untracked"
72
+ else
73
+ puts "#{indent}✓ Working directory clean"
74
+ end
75
+
76
+ if status[:ahead].positive? || status[:behind].positive?
77
+ puts "#{indent}🔄 Sync: #{sync_status_text(status[:ahead], status[:behind])}"
78
+ else
79
+ puts "#{indent}✓ Up to date with remote"
80
+ end
81
+ end
82
+
83
+ def sync_status_text(ahead, behind)
84
+ parts = []
85
+ parts << "#{ahead} ahead" if ahead.positive?
86
+ parts << "#{behind} behind" if behind.positive?
87
+ parts.join(', ')
88
+ end
89
+
90
+ def git_status_info
91
+ {
92
+ branch: current_branch,
93
+ remote: remote_url,
94
+ modified_count: modified_files_count,
95
+ untracked_count: untracked_files_count,
96
+ ahead: commits_ahead,
97
+ behind: commits_behind
98
+ }
99
+ end
100
+
101
+ def current_branch
102
+ `git -C "#{brand_path}" rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
103
+ rescue StandardError
104
+ 'unknown'
105
+ end
106
+
107
+ def remote_url
108
+ result = `git -C "#{brand_path}" remote get-url origin 2>/dev/null`.strip
109
+ result.empty? ? nil : result
110
+ rescue StandardError
111
+ nil
112
+ end
113
+
114
+ def modified_files_count
115
+ `git -C "#{brand_path}" status --porcelain 2>/dev/null | grep -E "^.M|^M" | wc -l`.strip.to_i
116
+ rescue StandardError
117
+ 0
118
+ end
119
+
120
+ def untracked_files_count
121
+ `git -C "#{brand_path}" status --porcelain 2>/dev/null | grep -E "^\\?\\?" | wc -l`.strip.to_i
122
+ rescue StandardError
123
+ 0
124
+ end
125
+
126
+ def commits_ahead
127
+ `git -C "#{brand_path}" rev-list --count @{upstream}..HEAD 2>/dev/null`.strip.to_i
128
+ rescue StandardError
129
+ 0
130
+ end
131
+
132
+ def commits_behind
133
+ `git -C "#{brand_path}" rev-list --count HEAD..@{upstream} 2>/dev/null`.strip.to_i
134
+ rescue StandardError
135
+ 0
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appydave
4
+ module Tools
5
+ module Dam
6
+ # Sync (git pull) brand repositories
7
+ class RepoSync
8
+ attr_reader :brand, :brand_info, :brand_path
9
+
10
+ def initialize(brand = nil)
11
+ return unless brand
12
+
13
+ @brand_info = load_brand_info(brand)
14
+ @brand = @brand_info.key
15
+ @brand_path = Config.brand_path(@brand)
16
+ end
17
+
18
+ # Sync single brand
19
+ def sync
20
+ puts "🔄 Syncing: v-#{brand}"
21
+ puts ''
22
+
23
+ unless git_repo?
24
+ puts "❌ Not a git repository: #{brand_path}"
25
+ return
26
+ end
27
+
28
+ perform_sync
29
+ end
30
+
31
+ # Sync all brands
32
+ def sync_all
33
+ puts '🔄 Syncing All Brands'
34
+ puts ''
35
+
36
+ Appydave::Tools::Configuration::Config.configure
37
+ brands_config = Appydave::Tools::Configuration::Config.brands
38
+
39
+ results = []
40
+
41
+ brands_config.brands.each do |brand_info|
42
+ @brand_info = brand_info
43
+ @brand = brand_info.key
44
+ @brand_path = Config.brand_path(@brand)
45
+
46
+ next unless git_repo?
47
+
48
+ puts "📦 v-#{brand}"
49
+ result = perform_sync(indent: ' ')
50
+ results << result
51
+ puts ''
52
+ end
53
+
54
+ show_summary(results)
55
+ end
56
+
57
+ private
58
+
59
+ def load_brand_info(brand)
60
+ Appydave::Tools::Configuration::Config.configure
61
+ Appydave::Tools::Configuration::Config.brands.get_brand(brand)
62
+ end
63
+
64
+ def git_repo?
65
+ git_dir = File.join(brand_path, '.git')
66
+ Dir.exist?(git_dir)
67
+ end
68
+
69
+ def perform_sync(indent: '')
70
+ # Check for uncommitted changes
71
+ if uncommitted_changes?
72
+ puts "#{indent}⚠️ Uncommitted changes detected"
73
+ puts "#{indent} Skipping pull (would cause conflicts)"
74
+ return { brand: brand, status: 'skipped', reason: 'uncommitted changes' }
75
+ end
76
+
77
+ # Perform git pull
78
+ output = `git -C "#{brand_path}" pull 2>&1`
79
+ exit_code = $CHILD_STATUS.exitstatus
80
+
81
+ if exit_code.zero?
82
+ if output.include?('Already up to date')
83
+ puts "#{indent}✓ Already up to date"
84
+ { brand: brand, status: 'current' }
85
+ else
86
+ puts "#{indent}✓ Updated successfully"
87
+ puts "#{indent} #{output.lines.first.strip}" if output.lines.any?
88
+ { brand: brand, status: 'updated' }
89
+ end
90
+ else
91
+ puts "#{indent}❌ Pull failed: #{output.strip}"
92
+ { brand: brand, status: 'error', reason: output.strip }
93
+ end
94
+ end
95
+
96
+ def uncommitted_changes?
97
+ output = `git -C "#{brand_path}" status --porcelain 2>/dev/null`.strip
98
+ !output.empty?
99
+ rescue StandardError
100
+ false
101
+ end
102
+
103
+ def show_summary(results)
104
+ puts '=' * 60
105
+ puts '📊 Sync Summary:'
106
+ puts ''
107
+
108
+ updated = results.count { |r| r[:status] == 'updated' }
109
+ current = results.count { |r| r[:status] == 'current' }
110
+ skipped = results.count { |r| r[:status] == 'skipped' }
111
+ errors = results.count { |r| r[:status] == 'error' }
112
+
113
+ puts " Total repos checked: #{results.size}"
114
+ puts " Updated: #{updated}"
115
+ puts " Already current: #{current}"
116
+ puts " Skipped: #{skipped}" if skipped.positive?
117
+ puts " Errors: #{errors}" if errors.positive?
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end