ace-demo 0.18.1 → 0.23.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52bcbe78edb6ed9c85b9aa166c0aeb6eddcc7f2c3abf2fc308c6948d11b10e2a
4
- data.tar.gz: 0ffc6151eabe9b4a7cf758c5e16befd95795e9fccf798ba6314e4db502033ddb
3
+ metadata.gz: 5cc7670ccb96a23d9ca6e12d3e4678f9a6bbc91d18ff9ac88254365e68304547
4
+ data.tar.gz: ad71cad734a5660dfe3a1c8a888f10914eddd2c49c427628680aa5ee5d755d58
5
5
  SHA512:
6
- metadata.gz: 3fbf0d84492ae0e797bfc18fafac44e0c3346c7ed68f73113452aa58cd388de3d0c1079df772bae8172fd81c980fce3a07ce7b03cc1e4fbbfb46742995cbeea3
7
- data.tar.gz: 9bbebcb303c7fe7a5f46f41dc07c9c624b4461e320e56c67c91999355ceb0d32f29797fb9ccccae2bd2db6d8be1ed82471c46f972f80977b349f19f13d8422f2
6
+ metadata.gz: 6aae377964291267ddcee9d8b809306bd3a9cd7c46bb6c87660757f46554f00e1ce9ee9aef8619e998f68a8dc89ab2152c22951d9d19b7f3c15d88e9dd7ff79d
7
+ data.tar.gz: 3b9daed37bff26a8bb486baf96d59a839dcfdaaa0b4d6175a7044ffb476c0b98ddd21ff7c4abbbafc942fc09657d2de220da5c9d73ac88698aa7d6ce1c5b7b70
@@ -1,3 +1,8 @@
1
1
  vhs_bin: vhs
2
+ asciinema_bin: asciinema
3
+ agg_bin: agg
4
+ agg_font_family: Hack Nerd Font Mono
2
5
  output_dir: .ace-local/demo
3
6
  sandbox_dir: .ace-local/demo/sandbox
7
+ record:
8
+ backend: asciinema
data/CHANGELOG.md CHANGED
@@ -7,6 +7,160 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.23.0] - 2026-03-28
11
+
12
+ ### Changed
13
+ - Switched YAML asciinema tape recording to an interactive shell session so demo commands are recorded visibly in casts by default instead of only being inferred from compiled script execution.
14
+
15
+ ### Fixed
16
+ - Wait for the initial shell prompt before sending the first tape command, preventing the first command from being dropped in interactive asciinema recordings.
17
+ - Normalized invalid `0x0` cast terminal headers before AGG conversion so interactive asciinema recordings convert cleanly to GIF output.
18
+ - Updated cast verification to recognize echoed interactive shell commands from the new default recording path.
19
+
20
+ ### Technical
21
+ - Reworked asciinema execution to drive the recorded PTY directly, preserving command ordering and shell session state across YAML tape scenes.
22
+ - Expanded recorder, executor, verifier, and command-builder coverage for the interactive YAML asciinema path.
23
+
24
+ ## [0.22.4] - 2026-03-27
25
+
26
+ ### Fixed
27
+ - Corrected default `agg_font_family` from `CaskaydiaMono Nerd Font` to `Hack Nerd Font Mono` to match spike-validated font available in the environment.
28
+
29
+ ## [0.22.3] - 2026-03-27
30
+
31
+ ### Fixed
32
+ - Validated asciinema tape `sleep` durations before script compilation to block malformed/injected values.
33
+
34
+ ### Changed
35
+ - Added `agg_font_family` default/config plumbing through YAML parsing, recorder conversion, and cast attachment conversion.
36
+
37
+ ### Technical
38
+ - Expanded parser/recorder/attacher/asciinema compiler test coverage for font-family wiring and sleep validation behavior.
39
+
40
+ ## [0.22.2] - 2026-03-27
41
+
42
+ ### Fixed
43
+ - Cleaned up temporary GIF conversion artifacts generated from `.cast` attachments after upload/comment orchestration completes.
44
+
45
+ ### Changed
46
+ - Centralized recording backend/format validation in `RecordOptionValidator` and reused it across planner, recorder, and CLI flows.
47
+
48
+ ### Technical
49
+ - Added validator-focused atom tests and updated attacher tests to assert temporary artifact lifecycle behavior.
50
+
51
+ ## [0.22.1] - 2026-03-27
52
+
53
+ ### Fixed
54
+ - Escaped asciinema `--command` script paths to handle spaces and shell metacharacters safely.
55
+ - Updated `CastVerifier` to validate command-mode asciinema recordings by inspecting script commands from cast header metadata.
56
+ - Prevented `.cast` conversion during `attach --dry-run` while preserving planned GIF naming in preview output.
57
+ - Improved missing-binary errors for asciinema and agg executors to report configured executable names.
58
+
59
+ ### Changed
60
+ - Simplified YAML record planner backend/format validation by removing a redundant unreachable guard.
61
+ - Updated dry-run upload planning to allow planned artifact paths without requiring generated files.
62
+
63
+ ### Technical
64
+ - Expanded atom/molecule/organism test coverage for escaped command generation, command-mode cast verification, dry-run cast attachment behavior, dry-run uploader planning, and configured binary error reporting.
65
+
66
+ ## [0.22.0] - 2026-03-27
67
+
68
+ ### Added
69
+ - Added cast verification primitives (`CastFileParser`, `CastVerifier`, and cast/verification models) for asciinema recording validation.
70
+ - Added `.cast` attachment support that converts recordings to GIF before PR upload/comment posting.
71
+
72
+ ### Changed
73
+ - Updated asciinema record flow to run non-blocking command-presence verification and expose verification status in CLI output.
74
+ - Updated `attach` command help and error handling to include `.cast` source files and agg conversion failures.
75
+ - Updated usage documentation for automatic cast verification and `.cast -> .gif` attachment behavior.
76
+
77
+ ### Technical
78
+ - Expanded atom, molecule, organism, and model test coverage for cast parsing, verification outcomes, recorder integration, and cast attachment conversion.
79
+
80
+ ## [0.21.0] - 2026-03-27
81
+
82
+ ### Added
83
+ - Added backend selection support for `ace-demo record` with `--backend` and YAML `settings.backend`.
84
+ - Added `RecordingResult` model to return structured recording output metadata (backend, visual path, optional cast path).
85
+
86
+ ### Changed
87
+ - Switched YAML tape recording default backend to asciinema with cast capture and AGG conversion to GIF.
88
+ - Updated YAML planning and CLI validation so backend/format constraints are enforced with actionable errors.
89
+ - Updated usage documentation for backend defaults, cast output visibility, and compatibility formatting rules.
90
+
91
+ ### Technical
92
+ - Expanded parser, recorder, and CLI test coverage for backend routing, format guards, and structured recorder output.
93
+
94
+ ## [0.20.0] - 2026-03-27
95
+
96
+ ### Added
97
+ - Added asciinema-native recording support with command-building and execution pipeline components.
98
+ - Added AGG post-processing support to compile asciinema recordings into final demo artifacts.
99
+
100
+ ### Changed
101
+ - Updated demo recording orchestration to support multi-backend execution and asciinema-to-AGG compatibility flows.
102
+ - Updated demo defaults/config to enable asciinema adapter behavior for task-driven recording workflows.
103
+
104
+ ### Technical
105
+ - Added atom and molecule test coverage for asciinema/AGG command builders and executors.
106
+ - Expanded smoke and pipeline tests for getting-started tape behavior under the new recording backend flow.
107
+
108
+ ## [0.19.4] - 2026-03-26
109
+
110
+ ### Added
111
+ - Added `ace-demo-tapeyml-recording-options.tape.yml` demo tape with fixtures showing `playback_speed` and `output` settings in action.
112
+ - Added `fixtures/sample-project/demo.tape.yml` fixture for demo sandbox recording.
113
+
114
+ ### Changed
115
+ - Updated `ace-demo-getting-started.tape.yml` with `playback_speed` and `output` settings example.
116
+
117
+ ## [0.19.3] - 2026-03-26
118
+
119
+ ### Fixed
120
+ - Unified YAML recording planning across dry-run preview and execution so unsupported `settings.format` values fail consistently.
121
+
122
+ ### Changed
123
+ - Centralized YAML format/speed/output-path planning into a shared helper reused by both CLI context building and `DemoRecorder`.
124
+
125
+ ### Technical
126
+ - Added dry-run regression coverage to assert YAML tapes with unsupported `settings.format` are rejected.
127
+
128
+ ## [0.19.2] - 2026-03-26
129
+
130
+ ### Fixed
131
+ - Reject blank or whitespace-only YAML `settings.output` values during parsing to prevent unusable expanded output paths.
132
+
133
+ ### Changed
134
+ - Updated tape record context resolution so live runs surface unresolved tape refs immediately while dry-run keeps preview fallback behavior.
135
+
136
+ ### Technical
137
+ - Removed the obsolete `dry_run_preview_for_tape` path and expanded parser/CLI regression coverage for output validation and YAML tape routing.
138
+
139
+ ## [0.19.1] - 2026-03-26
140
+
141
+ ### Fixed
142
+ - Honor CLI `--output` as the retime target when YAML defines `settings.playback_speed` but omits `settings.output`.
143
+
144
+ ### Changed
145
+ - Align tape-mode precedence handoff by passing parsed YAML spec and resolved context from CLI to `DemoRecorder`.
146
+
147
+ ### Technical
148
+ - Added regression coverage for YAML speed + CLI output precedence and pre-parsed YAML spec reuse in recorder tests.
149
+
150
+ ## [0.19.0] - 2026-03-26
151
+
152
+ ### Added
153
+ - Added support for `settings.playback_speed` and `settings.output` in `.tape.yml` for self-contained `ace-demo record` execution.
154
+ - Added parser contract tests for new YAML settings and invalid speed handling.
155
+
156
+ ### Changed
157
+ - Updated YAML tape recording flow to support retime-only output mode when both speed and output are set (raw stays in `.ace-local/demo`, retimed output writes to the exact configured path).
158
+ - Updated CLI tape-mode precedence so `--playback-speed` and `--output` override tape settings while preserving YAML defaults when flags are omitted.
159
+ - Updated getting-started and usage documentation plus example demo tape to reflect tape-defined speed/output behavior.
160
+
161
+ ### Technical
162
+ - Expanded recorder and CLI test coverage for speed-only, output-only, combined mode, dry-run preview messaging, and override precedence.
163
+
10
164
  ## [0.18.1] - 2026-03-23
11
165
 
12
166
  ### Fixed
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  Record terminal sessions as proof-of-work evidence for pull requests.
5
5
 
6
- <img src="https://raw.githubusercontent.com/cs3b/ace/main/docs/brand/AgenticCodingEnvironment.Logo.XS.jpg" alt="ACE Logo" width="480">
6
+ <img src="../docs/brand/AgenticCodingEnvironment.Logo.XS.jpg" alt="ACE Logo" width="480">
7
7
  <br><br>
8
8
 
9
9
  <a href="https://rubygems.org/gems/ace-demo"><img alt="Gem Version" src="https://img.shields.io/gem/v/ace-demo.svg" /></a>
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Demo
5
+ module Atoms
6
+ module AggCommandBuilder
7
+ module_function
8
+
9
+ def build(input_path:, output_path:, font_size: nil, theme: nil, font_family: nil, agg_bin: "agg")
10
+ cmd = [agg_bin]
11
+ cmd.concat(["--font-size", font_size.to_s]) unless font_size.nil?
12
+ cmd.concat(["--theme", theme.to_s]) if present?(theme)
13
+ cmd.concat(["--font-family", font_family.to_s]) if present?(font_family)
14
+ cmd.concat([input_path, output_path])
15
+ cmd
16
+ end
17
+
18
+ def present?(value)
19
+ !value.nil? && !value.to_s.strip.empty?
20
+ end
21
+ private_class_method :present?
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module Ace
6
+ module Demo
7
+ module Atoms
8
+ module AsciinemaCommandBuilder
9
+ module_function
10
+
11
+ # cast_compatibility is accepted to keep the interface aligned with task
12
+ # requirements; v3 passthrough is currently the proven production path.
13
+ def build(output_path:, script_path: nil, shell_command: nil, tty_size: "80x24", cast_compatibility: :v2, asciinema_bin: "asciinema")
14
+ validate_cast_compatibility!(cast_compatibility)
15
+
16
+ command = shell_command.to_s.strip
17
+ if command.empty?
18
+ escaped_script_path = Shellwords.escape(script_path.to_s)
19
+ command = "bash #{escaped_script_path}"
20
+ end
21
+
22
+ cmd = [asciinema_bin, "rec", "--overwrite", "--command", command]
23
+ cmd.concat(tty_size_flags(tty_size))
24
+ cmd << output_path
25
+ cmd
26
+ end
27
+
28
+ def tty_size_flags(tty_size)
29
+ return [] if tty_size.nil? || tty_size.to_s.strip.empty?
30
+
31
+ cols, rows = tty_size.to_s.split("x", 2)
32
+ if cols.to_s.empty? || rows.to_s.empty? || cols !~ /\A\d+\z/ || rows !~ /\A\d+\z/
33
+ raise ArgumentError, "tty_size must be formatted as <cols>x<rows> (e.g. 80x24)"
34
+ end
35
+
36
+ ["--cols", cols, "--rows", rows]
37
+ end
38
+ private_class_method :tty_size_flags
39
+
40
+ def validate_cast_compatibility!(cast_compatibility)
41
+ return if %i[v2 v3 auto].include?(cast_compatibility)
42
+
43
+ raise ArgumentError, "cast_compatibility must be one of: :v2, :v3, :auto"
44
+ end
45
+ private_class_method :validate_cast_compatibility!
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module Ace
6
+ module Demo
7
+ module Atoms
8
+ module AsciinemaTapeCompiler
9
+ module_function
10
+
11
+ def compile(spec:, default_timeout: "2s")
12
+ settings = spec["settings"] || {}
13
+ lines = [
14
+ "#!/usr/bin/env bash",
15
+ "set -euo pipefail",
16
+ ""
17
+ ]
18
+
19
+ env = settings["env"] || {}
20
+ env.each do |key, value|
21
+ lines << "export #{key}=#{Shellwords.escape(value.to_s)}"
22
+ end
23
+ lines << "" unless env.empty?
24
+
25
+ spec.fetch("scenes", []).each do |scene|
26
+ scene_name = scene["name"]
27
+ lines << "# Scene: #{scene_name}" unless scene_name.to_s.strip.empty?
28
+
29
+ scene.fetch("commands", []).each do |command|
30
+ lines << command.fetch("type")
31
+ sleep_value = validate_sleep!(command["sleep"] || default_timeout)
32
+ lines << "sleep #{sleep_value}"
33
+ lines << ""
34
+ end
35
+ end
36
+
37
+ lines.join("\n").rstrip + "\n"
38
+ end
39
+
40
+ def validate_sleep!(value)
41
+ sleep_value = value.to_s.strip
42
+ pattern = /\A\d+(?:\.\d+)?(?:ms|s|m|h)?\z/
43
+ return sleep_value if sleep_value.match?(pattern)
44
+
45
+ raise ArgumentError, "sleep must be a numeric duration (e.g. 0.5s, 250ms, 2)"
46
+ end
47
+ private_class_method :validate_sleep!
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Ace
6
+ module Demo
7
+ module Atoms
8
+ module CastFileParser
9
+ module_function
10
+
11
+ def parse(path)
12
+ raise CastParseError, "Cast file not found: #{path}" unless File.exist?(path)
13
+
14
+ header = nil
15
+ events = []
16
+
17
+ File.foreach(path).with_index(1) do |line, line_number|
18
+ stripped = line.strip
19
+ next if stripped.empty?
20
+
21
+ json = JSON.parse(stripped)
22
+ if header.nil?
23
+ header = parse_header(json, path: path)
24
+ else
25
+ events << parse_event(json, path: path, line_number: line_number)
26
+ end
27
+ rescue JSON::ParserError => e
28
+ raise CastParseError, "Invalid JSON in #{path}:#{line_number}: #{e.message}"
29
+ end
30
+
31
+ raise CastParseError, "Missing cast header in #{path}" if header.nil?
32
+
33
+ Models::CastRecording.new(header: header, events: events)
34
+ end
35
+
36
+ def parse_header(json, path:)
37
+ unless json.is_a?(Hash)
38
+ raise CastParseError, "Invalid cast header in #{path}: expected JSON object"
39
+ end
40
+
41
+ json
42
+ end
43
+ private_class_method :parse_header
44
+
45
+ def parse_event(json, path:, line_number:)
46
+ unless json.is_a?(Array) && json.length == 3
47
+ raise CastParseError,
48
+ "Invalid cast event in #{path}:#{line_number}: expected [timestamp, type, data]"
49
+ end
50
+
51
+ time, type, data = json
52
+ unless type.is_a?(String) && !type.empty?
53
+ raise CastParseError, "Invalid cast event type in #{path}:#{line_number}"
54
+ end
55
+
56
+ Models::CastEvent.new(time: time, type: type, data: data)
57
+ end
58
+ private_class_method :parse_event
59
+ end
60
+ end
61
+ end
62
+ end
@@ -66,11 +66,42 @@ module Ace
66
66
  normalized["width"] = integer_or_nil(settings["width"], "settings.width", source_path)
67
67
  normalized["height"] = integer_or_nil(settings["height"], "settings.height", source_path)
68
68
  normalized["format"] = settings["format"]&.to_s
69
+ normalized["agg_font_family"] = settings["agg_font_family"]&.to_s if settings.key?("agg_font_family")
70
+ normalized["backend"] = normalize_backend(settings["backend"], source_path) if settings.key?("backend")
71
+ normalized["playback_speed"] = normalize_playback_speed(settings["playback_speed"], source_path) if settings.key?("playback_speed")
72
+ normalized["output"] = normalize_output_path(settings["output"], source_path) if settings.key?("output")
69
73
  normalized["env"] = normalize_env(settings["env"], source_path: source_path) if settings.key?("env")
70
74
  normalized
71
75
  end
72
76
  private_class_method :normalize_settings
73
77
 
78
+ def normalize_backend(value, source_path)
79
+ backend = value.to_s.strip.downcase
80
+ allowed = %w[asciinema vhs]
81
+ return backend if allowed.include?(backend)
82
+
83
+ raise DemoYamlParseError, "Unknown backend '#{backend}'. Valid: asciinema, vhs (#{source_path})"
84
+ end
85
+ private_class_method :normalize_backend
86
+
87
+ def normalize_playback_speed(value, source_path)
88
+ parsed = Atoms::PlaybackSpeedParser.parse(value)
89
+ parsed && parsed[:label]
90
+ rescue ArgumentError => e
91
+ raise DemoYamlParseError, "#{e.message} (#{source_path})"
92
+ end
93
+ private_class_method :normalize_playback_speed
94
+
95
+ def normalize_output_path(value, source_path)
96
+ normalized = value&.to_s
97
+ if normalized.nil? || normalized.strip.empty?
98
+ raise DemoYamlParseError, "settings.output must be a non-empty path in #{source_path}"
99
+ end
100
+
101
+ normalized
102
+ end
103
+ private_class_method :normalize_output_path
104
+
74
105
  def normalize_env(env, source_path:)
75
106
  return {} if env.nil?
76
107
  raise DemoYamlParseError, "settings.env must be a map in #{source_path}" unless env.is_a?(Hash)
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Demo
5
+ module Atoms
6
+ module RecordOptionValidator
7
+ module_function
8
+
9
+ MP4_UNSUPPORTED_ERROR =
10
+ "Unsupported format: mp4. Use gif, or use --backend vhs --format webm for compatibility output."
11
+
12
+ def normalize_backend(value)
13
+ return nil if value.nil?
14
+
15
+ backend = value.to_s.strip.downcase
16
+ return backend if %w[asciinema vhs].include?(backend)
17
+
18
+ raise ArgumentError, "Unknown backend '#{backend}'. Valid: asciinema, vhs"
19
+ end
20
+
21
+ def normalize_format(value, supported_formats:, allow_nil: true)
22
+ return nil if value.nil? && allow_nil
23
+
24
+ format = value.to_s.downcase
25
+ raise ArgumentError, MP4_UNSUPPORTED_ERROR if format == "mp4"
26
+ raise ArgumentError, "Unsupported format: #{format}" unless supported_formats.include?(format)
27
+
28
+ format
29
+ end
30
+
31
+ def validate_yaml_backend_format!(backend:, format:)
32
+ return unless format == "webm" && backend != "vhs"
33
+
34
+ raise ArgumentError, "Format 'webm' requires --backend vhs when recording YAML tapes"
35
+ end
36
+
37
+ def validate_raw_tape_backend!(backend:)
38
+ return if backend.nil? || backend == "vhs"
39
+
40
+ raise ArgumentError, "Raw .tape recordings support backend 'vhs' only"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -26,8 +26,12 @@ module Ace
26
26
  lines << "# Scene: #{scene_name}" unless scene_name.to_s.strip.empty?
27
27
 
28
28
  scene.fetch("commands", []).each do |command|
29
- escaped = command.fetch("type").gsub("\\", "\\\\\\\\").gsub('"', '\\"')
30
- lines << "Type \"#{escaped}\""
29
+ type_text = command.fetch("type")
30
+ if type_text.include?('"') || type_text.include?("$") || type_text.include?("\\")
31
+ lines << "Type `#{type_text}`"
32
+ else
33
+ lines << "Type \"#{type_text}\""
34
+ end
31
35
  lines << "Enter"
32
36
  lines << "Sleep #{command["sleep"] || default_timeout}"
33
37
  lines << ""
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Demo
5
+ module Atoms
6
+ module YamlRecordPlanner
7
+ module_function
8
+
9
+ def plan(
10
+ tape_path:,
11
+ output:,
12
+ format:,
13
+ playback_speed:,
14
+ retime_output:,
15
+ yaml_spec:,
16
+ yaml_parser:,
17
+ supported_formats:,
18
+ default_output_path_builder:,
19
+ backend: nil,
20
+ default_backend: "asciinema"
21
+ )
22
+ spec = yaml_spec || yaml_parser.parse_file(tape_path)
23
+ settings = spec["settings"] || {}
24
+ selected_backend = RecordOptionValidator.normalize_backend(backend || settings["backend"] || default_backend)
25
+ selected_format = RecordOptionValidator.normalize_format(
26
+ format || settings["format"] || "gif",
27
+ supported_formats: supported_formats,
28
+ allow_nil: false
29
+ )
30
+ RecordOptionValidator.validate_yaml_backend_format!(backend: selected_backend, format: selected_format)
31
+
32
+ selected_speed = playback_speed.nil? ? settings["playback_speed"] : playback_speed
33
+ selected_speed = Atoms::PlaybackSpeedParser.parse(selected_speed)
34
+ selected_output = output.nil? ? settings["output"] : output
35
+ selected_retime_output = retime_output || selected_output
36
+
37
+ default_output_path = File.expand_path(default_output_path_builder.call(selected_format), Dir.pwd)
38
+ raw_output_path, retime_output_path = resolve_output_paths(
39
+ default_output_path: default_output_path,
40
+ output: selected_output,
41
+ speed: selected_speed,
42
+ retime_output: selected_retime_output
43
+ )
44
+
45
+ {
46
+ spec: spec,
47
+ backend: selected_backend,
48
+ format: selected_format,
49
+ speed: selected_speed,
50
+ selected_output: selected_output,
51
+ raw_output_path: raw_output_path,
52
+ retime_output_path: retime_output_path
53
+ }
54
+ end
55
+
56
+ def resolve_output_paths(default_output_path:, output:, speed:, retime_output:)
57
+ return [default_output_path, nil] if output.nil? && speed.nil?
58
+ return [File.expand_path(output, Dir.pwd), nil] if output && speed.nil?
59
+ return [default_output_path, File.expand_path(retime_output, Dir.pwd)] if retime_output
60
+ return [default_output_path, nil] if output.nil?
61
+
62
+ [default_output_path, File.expand_path(output, Dir.pwd)]
63
+ end
64
+ private_class_method :resolve_output_paths
65
+ end
66
+ end
67
+ end
68
+ end
@@ -12,7 +12,7 @@ module Ace
12
12
 
13
13
  desc "Attach an existing demo recording to a PR"
14
14
 
15
- argument :file, required: true, desc: "Recording file path (GIF, MP4, or WebM)"
15
+ argument :file, required: true, desc: "Recording file path (GIF, MP4, WebM, or .cast)"
16
16
  option :pr, type: :string, desc: "PR number"
17
17
  option :dry_run, type: :boolean, aliases: ["-n"], default: false, desc: "Preview only"
18
18
 
@@ -23,7 +23,8 @@ module Ace
23
23
  attacher = Organisms::DemoAttacher.new
24
24
  result = attacher.attach(file: file, pr: pr, dry_run: options[:dry_run])
25
25
  Atoms::AttachOutputPrinter.print(result)
26
- rescue ArgumentError, PrNotFoundError, GhAuthenticationError, GhUploadError, GhCommentError, GhCommandError => e
26
+ rescue ArgumentError, PrNotFoundError, GhAuthenticationError, GhUploadError, GhCommentError, GhCommandError,
27
+ AggNotFoundError, AggExecutionError => e
27
28
  raise Ace::Support::Cli::Error, e.message
28
29
  end
29
30
  end