ace-b36ts 0.13.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.
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module B36ts
5
+ module Atoms
6
+ # Format specifications for granular timestamp encoding.
7
+ #
8
+ # Defines 7 format types with varying precision and length:
9
+ # - 2sec (6 chars, ~1.85s precision) - default
10
+ # - month (2 chars, month precision)
11
+ # - week (3 chars, week precision)
12
+ # - day (3 chars, day precision)
13
+ # - 40min (4 chars, 40-minute block precision)
14
+ # - 50ms (7 chars, ~50ms precision)
15
+ # - ms (8 chars, ~1.4ms precision)
16
+ #
17
+ # Day/week disambiguation (3-char formats):
18
+ # - Day format: 3rd character in 0-30 range
19
+ # - Week format: 3rd character in 31-35 range
20
+ #
21
+ # @example Access format specifications
22
+ # FormatSpecs::FORMATS[:"2sec"] # => FormatSpec for 6-char IDs
23
+ # FormatSpecs::FORMATS[:month] # => FormatSpec for 2-char IDs
24
+ #
25
+ module FormatSpecs
26
+ # Immutable value object defining a timestamp format specification
27
+ FormatSpec = Data.define(:name, :length, :precision_desc, :pattern)
28
+
29
+ # All supported format specifications
30
+ FORMATS = {
31
+ "2sec": FormatSpec.new(
32
+ name: :"2sec",
33
+ length: 6,
34
+ precision_desc: "~1.85s",
35
+ pattern: /\A[0-9a-z]{6}\z/i
36
+ ),
37
+ month: FormatSpec.new(
38
+ name: :month,
39
+ length: 2,
40
+ precision_desc: "month",
41
+ pattern: /\A[0-9a-z]{2}\z/i
42
+ ),
43
+ week: FormatSpec.new(
44
+ name: :week,
45
+ length: 3,
46
+ precision_desc: "week",
47
+ pattern: /\A[0-9a-z]{3}\z/i
48
+ ),
49
+ day: FormatSpec.new(
50
+ name: :day,
51
+ length: 3,
52
+ precision_desc: "day",
53
+ pattern: /\A[0-9a-z]{3}\z/i
54
+ ),
55
+ "40min": FormatSpec.new(
56
+ name: :"40min",
57
+ length: 4,
58
+ precision_desc: "40min",
59
+ pattern: /\A[0-9a-z]{4}\z/i
60
+ ),
61
+ "50ms": FormatSpec.new(
62
+ name: :"50ms",
63
+ length: 7,
64
+ precision_desc: "~50ms",
65
+ pattern: /\A[0-9a-z]{7}\z/i
66
+ ),
67
+ ms: FormatSpec.new(
68
+ name: :ms,
69
+ length: 8,
70
+ precision_desc: "~1.4ms",
71
+ pattern: /\A[0-9a-z]{8}\z/i
72
+ )
73
+ }.freeze
74
+
75
+ # Day/week disambiguation for 3-char formats
76
+ # Day format: 3rd char (day value) is 0-30
77
+ # Week format: 3rd char (week value) is 31-35
78
+ DAY_FORMAT_MAX = 30
79
+ WEEK_FORMAT_MIN = 31
80
+ WEEK_FORMAT_MAX = 35
81
+ SPLIT_LEVELS = %i[month week day block].freeze
82
+
83
+ class << self
84
+ # Get format specification by name
85
+ #
86
+ # @param name [Symbol] Format name (:"2sec", :month, :week, :day, :"40min", :"50ms", :ms)
87
+ # @return [FormatSpec, nil] Format specification or nil if not found
88
+ def get(name)
89
+ FORMATS[name]
90
+ end
91
+
92
+ # Check if format name is valid
93
+ #
94
+ # @param name [Symbol] Format name to check
95
+ # @return [Boolean] True if format is supported
96
+ def valid_format?(name)
97
+ FORMATS.key?(name)
98
+ end
99
+
100
+ # Get all format names
101
+ #
102
+ # @return [Array<Symbol>] List of supported format names
103
+ def all_formats
104
+ FORMATS.keys
105
+ end
106
+
107
+ # Get all supported lengths
108
+ #
109
+ # @return [Array<Integer>] List of supported ID lengths
110
+ def all_lengths
111
+ FORMATS.values.map(&:length).uniq.sort
112
+ end
113
+
114
+ # Validate split level ordering and hierarchy
115
+ #
116
+ # @param levels [Array<Symbol>] Split levels to validate
117
+ # @return [Boolean] True if split levels are valid
118
+ def valid_split_levels?(levels)
119
+ return false unless levels.is_a?(Array)
120
+ return false if levels.empty?
121
+ return false unless levels.all? { |level| SPLIT_LEVELS.include?(level) }
122
+ return false unless levels.uniq.length == levels.length
123
+ return false unless levels.first == :month
124
+
125
+ indices = levels.map { |level| SPLIT_LEVELS.index(level) }
126
+ return false unless indices == indices.sort
127
+ return false if levels.include?(:block) && !levels.include?(:day)
128
+
129
+ true
130
+ end
131
+
132
+ # Detect format from ID string
133
+ # For 3-char IDs, uses the 3rd character value to distinguish day vs week
134
+ #
135
+ # @param encoded_id [String] The encoded ID string
136
+ # @param alphabet [String] Base36 alphabet
137
+ # @return [Symbol, nil] Detected format name or nil if invalid
138
+ def detect_from_id(encoded_id, alphabet: CompactIdEncoder::DEFAULT_ALPHABET)
139
+ return nil if encoded_id.nil? || encoded_id.empty?
140
+
141
+ # Validate all characters are in the alphabet
142
+ # Use Set for faster character validation (O(1) vs O(n))
143
+ alphabet_set = (alphabet == CompactIdEncoder::DEFAULT_ALPHABET) ? CompactIdEncoder::DEFAULT_ALPHABET_SET : alphabet.chars.to_set
144
+ return nil unless encoded_id.downcase.chars.all? { |c| alphabet_set.include?(c) }
145
+
146
+ length = encoded_id.length
147
+
148
+ case length
149
+ when 2
150
+ :month
151
+ when 3
152
+ # Disambiguate day vs week by 3rd character value
153
+ third_char_value = alphabet.index(encoded_id[2].downcase)
154
+ return nil if third_char_value.nil?
155
+
156
+ if third_char_value <= DAY_FORMAT_MAX
157
+ :day
158
+ elsif third_char_value <= WEEK_FORMAT_MAX
159
+ :week
160
+ end
161
+ # Note: With base36 (values 0-35), day covers 0-30 and week covers 31-35,
162
+ # so all valid values are handled. This branch implicitly returns nil
163
+ # only if alphabet validation fails (which is caught earlier).
164
+ when 4
165
+ :"40min"
166
+ when 6
167
+ :"2sec"
168
+ when 7
169
+ :"50ms"
170
+ when 8
171
+ :ms
172
+ end
173
+ end
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "format_specs"
4
+
5
+ module Ace
6
+ module B36ts
7
+ module Atoms
8
+ # Detects and validates timestamp format types.
9
+ #
10
+ # Supports multiple compact ID formats:
11
+ # - :"2sec" - 6-character Base36 compact ID (e.g., "i50jj3")
12
+ # - :month - 2-character Base36 month ID (e.g., "i5")
13
+ # - :week - 3-character Base36 week ID (e.g., "i5v")
14
+ # - :day - 3-character Base36 day ID (e.g., "i50")
15
+ # - :"40min" - 4-character Base36 40min block ID (e.g., "i50j")
16
+ # - :"50ms" - 7-character Base36 high-precision ID (e.g., "i50jj3z")
17
+ # - :ms - 8-character Base36 high-precision ID (e.g., "i50jj3zz")
18
+ # - :timestamp - 14-character timestamp format (e.g., "20250101-120000")
19
+ #
20
+ # @example Detect format
21
+ # Formats.detect("i50jj3") # => :"2sec"
22
+ # Formats.detect("i5") # => :month
23
+ # Formats.detect("i5v") # => :week (3rd char 31-35)
24
+ # Formats.detect("i50") # => :day (3rd char 0-30)
25
+ # Formats.detect("i50j") # => :"40min"
26
+ # Formats.detect("i50jj3z") # => :"50ms"
27
+ # Formats.detect("i50jj3zz") # => :ms
28
+ # Formats.detect("20250101-120000") # => :timestamp
29
+ # Formats.detect("invalid") # => nil
30
+ #
31
+ module Formats
32
+ # Regex patterns for format detection
33
+ MONTH_PATTERN = /\A[0-9a-z]{2}\z/i
34
+ DAY_WEEK_PATTERN = /\A[0-9a-z]{3}\z/i # Disambiguate by 3rd char value
35
+ HOUR_PATTERN = /\A[0-9a-z]{4}\z/i
36
+ COMPACT_PATTERN = /\A[0-9a-z]{6}\z/i
37
+ HIGH_7_PATTERN = /\A[0-9a-z]{7}\z/i
38
+ HIGH_8_PATTERN = /\A[0-9a-z]{8}\z/i
39
+ TIMESTAMP_PATTERN = /\A\d{8}-\d{6}\z/
40
+
41
+ class << self
42
+ # Detect the format type of a timestamp string
43
+ #
44
+ # For 3-character IDs, uses the 3rd character value to distinguish day vs week:
45
+ # - Day format: 3rd char in 0-30 range
46
+ # - Week format: 3rd char in 31-35 range
47
+ #
48
+ # @param value [String] The timestamp string to analyze
49
+ # @return [Symbol, nil] :"2sec", :month, :week, :day, :"40min", :"50ms", :ms, :timestamp, or nil if unrecognized
50
+ def detect(value)
51
+ return nil unless value.is_a?(String)
52
+
53
+ # For compact ID formats, delegate to FormatSpecs for proper detection
54
+ # including day/week disambiguation
55
+ if FormatSpecs::FORMATS.values.any? { |spec| spec.pattern.match?(value) }
56
+ FormatSpecs.detect_from_id(value)
57
+ elsif TIMESTAMP_PATTERN.match?(value)
58
+ :timestamp
59
+ end
60
+ end
61
+
62
+ # Check if value is a valid compact ID format
63
+ #
64
+ # @param value [String] The string to check
65
+ # @return [Boolean] true if valid compact format
66
+ def compact?(value)
67
+ detect(value) == :"2sec"
68
+ end
69
+
70
+ # Check if value is a valid timestamp format
71
+ #
72
+ # @param value [String] The string to check
73
+ # @return [Boolean] true if valid timestamp format
74
+ def timestamp?(value)
75
+ detect(value) == :timestamp
76
+ end
77
+
78
+ # Parse a timestamp format string to Time
79
+ #
80
+ # @param value [String] Timestamp in "YYYYMMDD-HHMMSS" format
81
+ # @return [Time] Parsed time in UTC
82
+ # @raise [ArgumentError] If format is invalid
83
+ def parse_timestamp(value)
84
+ unless timestamp?(value)
85
+ raise ArgumentError, "Invalid timestamp format: #{value} (expected YYYYMMDD-HHMMSS)"
86
+ end
87
+
88
+ year = value[0..3].to_i
89
+ month = value[4..5].to_i
90
+ day = value[6..7].to_i
91
+ hour = value[9..10].to_i
92
+ minute = value[11..12].to_i
93
+ second = value[13..14].to_i
94
+
95
+ Time.utc(year, month, day, hour, minute, second)
96
+ end
97
+
98
+ # Format a Time object to timestamp format
99
+ #
100
+ # @param time [Time] The time to format
101
+ # @return [String] Timestamp in "YYYYMMDD-HHMMSS" format
102
+ def format_timestamp(time)
103
+ time = time.utc if time.respond_to?(:utc)
104
+ time.strftime("%Y%m%d-%H%M%S")
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../commands/config_command"
4
+
5
+ module Ace
6
+ module B36ts
7
+ module CLI
8
+ module Commands
9
+ # Show current configuration
10
+ class Config < Ace::Support::Cli::Command
11
+ include Ace::Support::Cli::Base
12
+
13
+ desc "Show current configuration"
14
+
15
+ option :verbose, type: :boolean, aliases: ["-v"], desc: "Show verbose output"
16
+
17
+ example [
18
+ " # Show basic config",
19
+ "--verbose # Show full config with sources"
20
+ ]
21
+
22
+ def call(**options)
23
+ Ace::B36ts::Commands::ConfigCommand.execute(options)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../commands/decode_command"
4
+
5
+ module Ace
6
+ module B36ts
7
+ module CLI
8
+ module Commands
9
+ # Decode a compact ID to a timestamp
10
+ class Decode < Ace::Support::Cli::Command
11
+ include Ace::Support::Cli::Base
12
+
13
+ desc "Decode a compact ID (2-8 characters) to a timestamp"
14
+
15
+ argument :compact_id, required: true, desc: "2-8 character compact ID to decode (auto-detects format)"
16
+ option :year_zero, type: :integer, aliases: ["-y"], desc: "Base year for decoding (default: 2000)"
17
+ option :format, type: :string, aliases: ["-f"], desc: "Output format (readable, iso, timestamp)"
18
+ option :split, type: :boolean, desc: "Force hierarchical split decoding"
19
+ option :quiet, type: :boolean, aliases: ["-q"], desc: "Suppress non-essential output"
20
+ option :verbose, type: :boolean, aliases: ["-v"], desc: "Show verbose output"
21
+ option :debug, type: :boolean, aliases: ["-d"], desc: "Show debug output"
22
+
23
+ example [
24
+ "i5 # Decode 2-char month ID",
25
+ "i5v # Decode 3-char week ID (3rd char 31-35)",
26
+ "i50 # Decode 3-char day ID (3rd char 0-30)",
27
+ "i50j # Decode 4-char 40min ID",
28
+ "i50jj3 # Decode 6-char 2sec ID",
29
+ "i50jj3z # Decode 7-char 50ms ID",
30
+ "i50jj3zz # Decode 8-char ms ID",
31
+ "i50jj3 --format iso # Decode to ISO format",
32
+ "i50jj3 --format timestamp # Decode to YYYYMMDD-HHMMSS",
33
+ "i5/1/5/j/j3 # Decode split path (auto-detect separators)",
34
+ "i515jj3 --split # Decode split full string"
35
+ ]
36
+
37
+ def call(compact_id:, **options)
38
+ # Convert numeric options from strings to integers
39
+ coerce_types(options, year_zero: :integer)
40
+
41
+ Ace::B36ts::Commands::DecodeCommand.execute(compact_id, options)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../commands/encode_command"
4
+
5
+ module Ace
6
+ module B36ts
7
+ module CLI
8
+ module Commands
9
+ # Encode a timestamp to a 6-character compact ID
10
+ class Encode < Ace::Support::Cli::Command
11
+ include Ace::Support::Cli::Base
12
+
13
+ desc "Encode a timestamp to a compact ID (2-8 characters)"
14
+
15
+ argument :timestamp, required: false, desc: "Timestamp to encode (ISO, readable, 'now', or empty for current time)"
16
+ option :format, type: :string, aliases: ["-f"], desc: "Output format: 2sec (default), month, week, day, 40min, 50ms, ms"
17
+ option :count, type: :integer, aliases: ["-n"], desc: "Generate N sequential IDs starting from timestamp"
18
+ option :split, type: :string, desc: "Split levels for hierarchical output (month,week,day,block)"
19
+ option :path_only, type: :boolean, desc: "Output only the split path"
20
+ option :json, type: :boolean, desc: "Output split data as JSON (works with --split or --count)"
21
+ option :year_zero, type: :integer, aliases: ["-y"], desc: "Base year for encoding (default: 2000)"
22
+ option :quiet, type: :boolean, aliases: ["-q"], desc: "Suppress non-essential output"
23
+ option :verbose, type: :boolean, aliases: ["-v"], desc: "Show verbose output"
24
+ option :debug, type: :boolean, aliases: ["-d"], desc: "Show debug output"
25
+
26
+ example [
27
+ "'2025-01-06 12:30:00' # Encode readable timestamp",
28
+ "now # Encode current time",
29
+ "--format day '2025-01-06' # Encode to day format",
30
+ "--format month now # Encode to month format",
31
+ "--format 40min now # Encode to 40min format",
32
+ "--format 50ms now # Encode to 50ms format",
33
+ "--count 10 --format ms now # Generate 10 sequential ms-precision IDs",
34
+ "-n 5 --format day now # Generate 5 consecutive day IDs",
35
+ "--count 3 --format 2sec --json now # Generate 3 IDs as JSON array",
36
+ "--split month,week now # Encode to hierarchical split output",
37
+ "--split month,week now --path-only # Output only the path",
38
+ "--split month,day now --json # Output JSON split data",
39
+ "--year-zero 2025 '2025-01-06' # Encode with custom base year"
40
+ ]
41
+
42
+ def call(timestamp: nil, **options)
43
+ # Convert numeric options from strings to integers
44
+ coerce_types(options, year_zero: :integer, count: :integer)
45
+
46
+ Ace::B36ts::Commands::EncodeCommand.execute(timestamp, options)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ace/support/cli"
4
+ require "ace/core"
5
+ require_relative "cli/commands/encode"
6
+ require_relative "cli/commands/decode"
7
+ require_relative "cli/commands/config"
8
+ require_relative "version"
9
+
10
+ module Ace
11
+ module B36ts
12
+ # CLI interface for ace-b36ts using ace-support-cli
13
+ #
14
+ # This follows the Hanami pattern with all commands in CLI::Commands:: namespace.
15
+ #
16
+ # @example Encode a timestamp
17
+ # $ ace-b36ts encode "2025-01-06 12:30:00"
18
+ # i50jj3
19
+ #
20
+ # @example Decode a compact ID
21
+ # $ ace-b36ts decode i50jj3
22
+ # 2025-01-06 12:30:00 UTC
23
+ #
24
+ # @example Show configuration
25
+ # $ ace-b36ts config
26
+ # year_zero: 2000
27
+ # alphabet: 0123456789abcdefghijklmnopqrstuvwxyz
28
+ #
29
+ module CLI
30
+ extend Ace::Support::Cli::RegistryDsl
31
+
32
+ PROGRAM_NAME = "ace-b36ts"
33
+
34
+ REGISTERED_COMMANDS = [
35
+ ["encode", "Encode timestamp to compact ID"],
36
+ ["decode", "Decode compact ID to timestamp"],
37
+ ["config", "Show current configuration"]
38
+ ].freeze
39
+
40
+ HELP_EXAMPLES = [
41
+ "ace-b36ts encode # Generate ID from now",
42
+ "ace-b36ts encode 2024-01-15T10:30:00Z # Encode specific time",
43
+ "ace-b36ts decode abc123 # Decode ID to timestamp"
44
+ ].freeze
45
+
46
+ # Register commands (Hanami pattern: CLI::Commands::*)
47
+ register "encode", Commands::Encode
48
+ register "decode", Commands::Decode
49
+ register "config", Commands::Config
50
+
51
+ # Version command
52
+ version_cmd = Ace::Support::Cli::VersionCommand.build(
53
+ gem_name: "ace-b36ts",
54
+ version: Ace::B36ts::VERSION
55
+ )
56
+ register "version", version_cmd
57
+ register "--version", version_cmd
58
+
59
+ # Help command
60
+ help_cmd = Ace::Support::Cli::HelpCommand.build(
61
+ program_name: PROGRAM_NAME,
62
+ version: Ace::B36ts::VERSION,
63
+ commands: REGISTERED_COMMANDS,
64
+ examples: HELP_EXAMPLES
65
+ )
66
+ register "help", help_cmd
67
+ register "--help", help_cmd
68
+ register "-h", help_cmd
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module B36ts
5
+ module Commands
6
+ # Command to display current configuration
7
+ #
8
+ # @example Usage
9
+ # ConfigCommand.execute
10
+ # # Output:
11
+ # # year_zero: 2000
12
+ # # alphabet: 0123456789abcdefghijklmnopqrstuvwxyz
13
+ #
14
+ class ConfigCommand
15
+ class << self
16
+ # Execute the config command
17
+ #
18
+ # @param options [Hash] Command options
19
+ # @option options [Boolean] :verbose Show additional config details
20
+ # @return [Integer] Exit code (0 for success)
21
+ def execute(options = {})
22
+ config = Molecules::ConfigResolver.resolve
23
+
24
+ puts "Current ace-b36ts configuration:"
25
+ puts ""
26
+ puts " year_zero: #{config[:year_zero]}"
27
+ puts " alphabet: #{config[:alphabet]}"
28
+
29
+ if options[:verbose]
30
+ puts ""
31
+ puts "Configuration sources (in order of precedence):"
32
+ puts " 1. Runtime options (passed to commands)"
33
+ puts " 2. Project config: .ace/b36ts/config.yml"
34
+ puts " 3. User config: ~/.ace/b36ts/config.yml"
35
+ puts " 4. Gem defaults: .ace-defaults/b36ts/config.yml"
36
+ puts ""
37
+ puts "Year range: #{config[:year_zero]} to #{config[:year_zero] + 107}"
38
+ puts "ID length: 6 characters (Base36)"
39
+ puts "Time precision: ~1.85 seconds"
40
+ end
41
+
42
+ 0
43
+ rescue => e
44
+ warn "Error displaying config: #{e.message}"
45
+ warn e.backtrace.first(5).join("\n") if Ace::B36ts.debug?
46
+ raise
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module B36ts
5
+ module Commands
6
+ # Command to decode a compact ID to a timestamp
7
+ #
8
+ # @example Usage
9
+ # DecodeCommand.execute("i50jj3")
10
+ # # => "2025-01-06 12:30:00 UTC"
11
+ #
12
+ class DecodeCommand
13
+ class << self
14
+ # Execute the decode command
15
+ #
16
+ # @param compact_id [String] 2-8 character compact ID to decode
17
+ # @param options [Hash] Command options
18
+ # @option options [Integer] :year_zero Override year_zero config
19
+ # @option options [String] :format Output format (:iso, :timestamp, :readable)
20
+ # @option options [Boolean] :quiet Suppress config summary output
21
+ # @return [Integer] Exit code (0 for success, 1 for error)
22
+ def execute(compact_id, options = {})
23
+ config = Molecules::ConfigResolver.resolve(options)
24
+
25
+ display_config_summary("decode", config, options)
26
+
27
+ # Use split decoding for hierarchical paths, otherwise auto-detect
28
+ time = if options[:split] || contains_split_separator?(compact_id)
29
+ Atoms::CompactIdEncoder.decode_path(
30
+ compact_id,
31
+ year_zero: config[:year_zero],
32
+ alphabet: config[:alphabet]
33
+ )
34
+ else
35
+ Atoms::CompactIdEncoder.decode_auto(
36
+ compact_id,
37
+ year_zero: config[:year_zero],
38
+ alphabet: config[:alphabet]
39
+ )
40
+ end
41
+
42
+ output = format_output(time, options[:format])
43
+ puts output
44
+ 0
45
+ rescue ArgumentError => e
46
+ warn "Error: #{e.message}"
47
+ raise
48
+ rescue => e
49
+ warn "Error decoding compact ID: #{e.message}"
50
+ warn e.backtrace.first(5).join("\n") if Ace::B36ts.debug?
51
+ raise
52
+ end
53
+
54
+ private
55
+
56
+ # Format the time output based on requested format
57
+ #
58
+ # @param time [Time] Decoded time
59
+ # @param format [Symbol, String, nil] Output format
60
+ # @return [String] Formatted time string
61
+ def format_output(time, format)
62
+ case format&.to_sym
63
+ when :iso
64
+ time.iso8601
65
+ when :timestamp
66
+ Atoms::Formats.format_timestamp(time)
67
+ when :readable
68
+ time.strftime("%Y-%m-%d %H:%M:%S UTC")
69
+ else
70
+ # Default: readable format
71
+ time.strftime("%Y-%m-%d %H:%M:%S UTC")
72
+ end
73
+ end
74
+
75
+ # Display configuration summary (unless quiet mode)
76
+ #
77
+ # @param command [String] Command name
78
+ # @param config [Hash] Resolved configuration
79
+ # @param options [Hash] Command options
80
+ def display_config_summary(command, config, options)
81
+ return if options[:quiet]
82
+
83
+ require "ace/core"
84
+ Ace::Core::Atoms::ConfigSummary.display(
85
+ command: command,
86
+ config: config,
87
+ defaults: Molecules::ConfigResolver::FALLBACK_DEFAULTS,
88
+ options: options,
89
+ quiet: false
90
+ )
91
+ end
92
+
93
+ # Detect split separators in input
94
+ #
95
+ # @param value [String] Input to check
96
+ # @return [Boolean] True if split separators are present
97
+ def contains_split_separator?(value)
98
+ value.is_a?(String) && value.match?(/[\/\\:]/)
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end