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 +4 -4
- data/.ace-defaults/demo/config.yml +5 -0
- data/CHANGELOG.md +154 -0
- data/README.md +1 -1
- data/lib/ace/demo/atoms/agg_command_builder.rb +25 -0
- data/lib/ace/demo/atoms/asciinema_command_builder.rb +49 -0
- data/lib/ace/demo/atoms/asciinema_tape_compiler.rb +51 -0
- data/lib/ace/demo/atoms/cast_file_parser.rb +62 -0
- data/lib/ace/demo/atoms/demo_yaml_parser.rb +31 -0
- data/lib/ace/demo/atoms/record_option_validator.rb +45 -0
- data/lib/ace/demo/atoms/vhs_tape_compiler.rb +6 -2
- data/lib/ace/demo/atoms/yaml_record_planner.rb +68 -0
- data/lib/ace/demo/cli/commands/attach.rb +3 -2
- data/lib/ace/demo/cli/commands/record.rb +146 -40
- data/lib/ace/demo/models/cast_event.rb +17 -0
- data/lib/ace/demo/models/cast_recording.rb +16 -0
- data/lib/ace/demo/models/recording_result.rb +18 -0
- data/lib/ace/demo/models/verification_result.rb +23 -0
- data/lib/ace/demo/molecules/agg_executor.rb +39 -0
- data/lib/ace/demo/molecules/asciinema_executor.rb +146 -0
- data/lib/ace/demo/molecules/cast_verifier.rb +119 -0
- data/lib/ace/demo/molecules/gh_asset_uploader.rb +1 -2
- data/lib/ace/demo/organisms/demo_attacher.rb +86 -16
- data/lib/ace/demo/organisms/demo_recorder.rb +185 -18
- data/lib/ace/demo/version.rb +1 -1
- data/lib/ace/demo.rb +18 -0
- metadata +14 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5cc7670ccb96a23d9ca6e12d3e4678f9a6bbc91d18ff9ac88254365e68304547
|
|
4
|
+
data.tar.gz: ad71cad734a5660dfe3a1c8a888f10914eddd2c49c427628680aa5ee5d755d58
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6aae377964291267ddcee9d8b809306bd3a9cd7c46bb6c87660757f46554f00e1ce9ee9aef8619e998f68a8dc89ab2152c22951d9d19b7f3c15d88e9dd7ff79d
|
|
7
|
+
data.tar.gz: 3b9daed37bff26a8bb486baf96d59a839dcfdaaa0b4d6175a7044ffb476c0b98ddd21ff7c4abbbafc942fc09657d2de220da5c9d73ac88698aa7d6ce1c5b7b70
|
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="
|
|
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
|
-
|
|
30
|
-
|
|
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
|
|
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
|
|
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
|