sevgi 0.95.0 → 0.98.2

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: 47a098e5eabd1f185ef25e4d5797d347db21ccf9bddcb0dea480f921115cc129
4
- data.tar.gz: 1bc7cac24e7ab2771d670edaaf4e4323a454c7a331a2663e812df3d9a1868a82
3
+ metadata.gz: 637d6a0c7a281ad8f1800c9fcf08968266c0e6c6a73a0ad6206a7b41e5c57fc5
4
+ data.tar.gz: b01598c764d715a4b381a90a3a6d5de203695459147142575cbd326ce176a07b
5
5
  SHA512:
6
- metadata.gz: 6e9b877a8a87ab2493b64bd3a4e3bba4b718697e47fe860b3c98bc40db29271d9b1700113657e627cdb6e0810efc109f73b8e23ef1eb582d8976f7ab2ec8889f
7
- data.tar.gz: 05d88614f5030aafcf8ce3f8e05f738decf86614ade78eae319c348d9402c4db1d7657a2743cf4f2f84f2b31f21fdf39f7bf9fef6e4078257d90aa24c9b86140
6
+ metadata.gz: e0c7c633c134254b8eedd6f9d8da45530f4485b0e0b3508ce1e4b76522eca83d3de456a53e956cdba3c38f48fd38e811db70d25b3fb8ed44f4c6c0261319b500
7
+ data.tar.gz: b9182c6a0a4d250d0f1947bcf3b8c9e7b3bcebe9268e581d59cb2db4230315eadcf511e6880bd388a74847f441bbf5b0fcf4e37889ddfbc8c7ee7094e124a971
data/CHANGELOG.md CHANGED
@@ -5,6 +5,131 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.98.2 - 2026-07-25
9
+
10
+ ### Fixed
11
+
12
+ - Restored the `sevgi` CLI's main-object scope so bare toolkit calls remain available inside script helper classes.
13
+
14
+ ## 0.98.1 - 2026-07-20
15
+
16
+ ### Changed
17
+
18
+ - Changed the `sevgi` CLI to use the executor's isolated scope by default.
19
+
20
+ ### Removed
21
+
22
+ - Removed the redundant `-n` and `--nomain` CLI options; library consumers can still select `main: true` through
23
+ `Sevgi.execute` and `Sevgi.execute_file` when needed.
24
+
25
+ ## 0.98.0 - 2026-07-20
26
+
27
+ ### Added
28
+
29
+ - Added standard-input support to `sevgi`, `igves`, and `igsev`; `sevgi --as NAME` and
30
+ `Sevgi.execute_file(..., as: NAME)` supply the logical source basename used by implicit SVG, PDF, and PNG
31
+ destinations without changing physical load identity.
32
+
33
+ ## 0.97.0 - 2026-07-19
34
+
35
+ ### Added
36
+
37
+ - Added `sevgi-appendix`, containing the Sevgi agent skill and a RuboCop plugin for preserving readable `.sevgi` DSL
38
+ source alongside rubyfmt-formatted Ruby; the umbrella `sevgi` gem installs the matching Appendix version.
39
+ - Added `sevgi --skill` to report the validated path of the matching packaged agent skill for product-neutral setup.
40
+ - Added `igsev` to the umbrella gem for normalizing an SVG file through a complete SVG-to-Sevgi-to-SVG round trip.
41
+
42
+ ### Changed
43
+
44
+ - Made Homebrew the recommended complete CLI installation while retaining Bundler and focused component gems for Ruby
45
+ application dependencies.
46
+
47
+ ## 0.96.0 - 2026-07-18
48
+
49
+ ### Added
50
+
51
+ - Added exact, subtree-wide attribute omission to Derender content/file conversion, evaluation, and inclusion APIs;
52
+ `igves --omit` exposes the same behavior from the command line.
53
+ - Added `Sevgi.SVG` as the explicit namespaced form of the top-level SVG document entrypoint.
54
+ - Added the opt-in recursive `SVG::Modules` contract for callable drawing namespaces.
55
+ - Added callable drawing-module `base` blocks, document and paper registry introspection, Canvas structural equality,
56
+ renderer options on `RenderChildren`, axis translation helpers, and a CC BY RDF helper.
57
+ - Formalized non-rendering `-` metadata and `+` attribute updates as `Attributes::META_PREFIX` and
58
+ `Attributes::UPDATE_SUFFIX`; repeated Array updates now concatenate into a stable flat value.
59
+
60
+ ### Changed
61
+
62
+ - Breaking: replaced the `SVG = Sevgi::Graphics` alias with an independent SVG facade. Facade operations now use
63
+ capitalized names such as `SVG.Canvas` and `SVG.Document`; lowercase component helpers remain on
64
+ `Sevgi::Graphics`, and the stuttering `SVG.SVG` form was removed.
65
+ - Breaking: made low-level `Executor.execute` and `execute_file` runners private; consumers use
66
+ `Sevgi.execute` and `Sevgi.execute_file` while retaining the public result and error types.
67
+ - Breaking: removed the abstract `Document::Base` layer from the selectable document-profile registry; `Minimal` and
68
+ `Default` are now sibling concrete profiles, and custom profile hierarchies should derive from `Base`.
69
+ - Breaking: renamed Parallelogram segment and constraint arguments by geometric role: `base`, `side`, and
70
+ `constraint` replace axis-implying constructor names.
71
+ - Made degree-based sine and cosine exact at integer quarter turns, eliminating cardinal Geometry residue.
72
+ - Documented complete file-system failure families for file comparison, output, touch, and upward location helpers.
73
+ - Formalized generated Tile ids, positional CSS classes, template placement, and per-use callback signatures.
74
+ - Formalized String and Symbol ids across Derender selection APIs and documented file-read failures consistently.
75
+ - Breaking: made top-level `Decompile`, `Derender`, `Evaluate`, and `EvaluateChildren` consume inline SVG/XML;
76
+ file inputs now use the corresponding `File`-suffixed entrypoints.
77
+ - Breaking: replaced callable drawing module `call` block registration with argument-independent `base` blocks;
78
+ inherited bases run parent-first, and modules with one drawing method conventionally name it `call`.
79
+ - Breaking: made bracket and call notation the canonical Geometry constructors. Among Data value types, bracket notation
80
+ is public only for Point, Segment, LengthAngle, Margin, and Paper; use `.new` for Location and result carriers, and
81
+ `Stay` for traversal stop tokens.
82
+ - Breaking: aligned Point and Segment comparison with Ruby `Comparable`; malformed or unrelated comparison operands now
83
+ return nil from `<=>`.
84
+ - Breaking: executor entrypoints now return immutable `Executor::Result` values and expose only `execute` and
85
+ `execute_file`; inspect `result.error`, `result.stack`, and `result.value` instead of executor scopes or lifecycle state.
86
+ - Breaking: wrapper attributes and callable arguments use distinct channels, `With` rejects parentless receivers, and
87
+ unsupported direct constructors are private; use the documented factories for Content, concrete Geometry lined
88
+ elements, and Grid query values.
89
+ - Breaking: Canvas uses `.new` for explicit fields, `.from_paper(paper, **overrides)` for paper conversion, and
90
+ `.call`/`Graphics.canvas` for dispatch; the misleading keyword-only `.from_paper(width:, height:)` form was removed.
91
+ - Breaking: `Save`, `Write`, `PNG`, `PDF`, and Sundries native export normalize successful destinations to expanded
92
+ String paths and create missing parent directories; change-aware SVG writes still return nil when unchanged.
93
+ - Breaking: renamed `Derender.evaluate_file_children` to `evaluate_children_file`; decompiled nodes now own immutable
94
+ public state while parser, strategy, and construction plumbing remain private.
95
+ - Expanded runtime/YARD parity checks and exact contracts for inherited, extended, generated, and dynamic API surfaces,
96
+ forwarded options, error channels, path ownership, whitespace, namespaces, nil behavior, and return values.
97
+
98
+ ### Fixed
99
+
100
+ - Rejected directories from default file discovery while preserving custom locator matchers.
101
+ - Kept `Ancestral` context in non-rendering `-context` metadata instead of leaking it into SVG attributes.
102
+ - Preserved source Canvas units and names when deriving fitted Grid canvases.
103
+ - Prevented stale documentation assets from mixing old layouts with new HTML, and moved sidebar navigation into the
104
+ tablet menu at 1024px and below.
105
+ - Normalized numeric slots owned by path, shape, transform, tile, and Inkscape page helpers to finite SVG number spelling;
106
+ arbitrary user-supplied attributes remain untouched.
107
+ - Made named callable wrapper and symbol ids stable while omitting defaults for anonymous modules, and indexed every
108
+ rendered id value, including false and numeric values, through its serialized string.
109
+ - Preserved signed Geometry constraint directions, rejected invalid sweep/export channels through Sevgi error families,
110
+ and validated raw output paths before expansion or rendering.
111
+ - Made callable module configuration copy-owned and freeze-aware, document subclasses inherit their nearest profile, and
112
+ false executor boot receivers remain explicit rather than defaulting to an internal scope.
113
+ - Made element trees, attributes, identifiers, locator results, document profiles, Derender nodes, shell results, and
114
+ executor results retain owned immutable snapshots where their public contracts promise value semantics.
115
+ - Corrected Shell combined-output separators, nil export density errors, document render-option routing, executor source
116
+ snapshots, selected-node namespace/whitespace documentation, and Standard character-data validation.
117
+
118
+ ### Removed
119
+
120
+ - Removed public access to pluralization tables and the internal SVG save extension; pluralization rules are now deeply
121
+ immutable.
122
+ - Removed public documentation and constant access for command-line implementation modules; the `sevgi` and `igves`
123
+ executables remain unchanged.
124
+ - Removed eager loading and public documentation of private Showcase build/test support; explicit support entrypoints
125
+ now keep the harness under the Showcase namespace.
126
+ - Removed public access to document profile name normalizers; registry operations retain them as private plumbing.
127
+ - Removed the redundant `Margin.margin` constructor; use canonical bracket notation.
128
+ - Removed public executor orchestration, obsolete callable-module hooks, the old public attribute syntax constants, and
129
+ direct construction of abstract or internally wired Content, Element, and Grid query types.
130
+ - Removed accidental public access to abstract Lined factories, internal element-name/export maps, result/location
131
+ bracket constructors, and direct Stop construction.
132
+
8
133
  ## 0.95.0 - 2026-07-11
9
134
 
10
135
  ### Security
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Sevgi
2
2
 
3
- Top-level API and `.sevgi` script runner.
3
+ The `sevgi` gem provides the top-level API and the `.sevgi` script runner.
4
4
 
5
5
  ## Install
6
6
 
@@ -17,20 +17,38 @@ require "sevgi"
17
17
  ## Example
18
18
 
19
19
  ```ruby
20
- SVG(:minimal) do
21
- rect(width: 3, height: 5)
20
+ SVG :minimal do
21
+ rect width: 3, height: 5
22
22
  end.call
23
23
  ```
24
24
 
25
- ## Executable
25
+ ## Executables
26
26
 
27
27
  ```sh
28
28
  sevgi drawing.sevgi
29
+ igsev drawing.svg
29
30
  ```
30
31
 
32
+ Both commands read standard input when the file is omitted or `-`. For `sevgi`, implicit `Save`, `PDF`, and `PNG`
33
+ destinations use `output` as the input name; use `--as NAME` to choose another basename:
34
+
35
+ ```sh
36
+ sevgi --as badge < drawing.sevgi
37
+ igsev < drawing.svg > normalized.svg
38
+ ```
39
+
40
+ `NAME` cannot include a directory. An explicit output path or `default:` argument in the script remains authoritative.
41
+ With a file operand, `--as` keeps the file's source directory so relative `Load` calls continue to resolve there.
42
+
43
+ `sevgi --skill` prints the validated path of the matching packaged agent skill. See the
44
+ [Appendix documentation](https://github.com/roktas/sevgi/tree/main/appendix) for installation guidance.
45
+
46
+ `igsev` converts an SVG file to Sevgi source, evaluates it with the complete DSL, and prints the resulting normalized
47
+ SVG. Use `igves` from `sevgi-derender` when the generated Sevgi source itself is the desired output.
48
+
31
49
  ## Ruby compatibility
32
50
 
33
- Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4.0 and the current development Ruby from `.ruby-version`.
51
+ Requires Ruby 3.4.0 or newer. CI verifies the current Ruby 3.4 release and the development Ruby from `.ruby-version`.
34
52
 
35
53
  ## Native prerequisites
36
54
 
@@ -55,7 +73,7 @@ gem install cairo rsvg2 hexapdf
55
73
 
56
74
  ## Links
57
75
 
58
- - Documentation: https://sevgi.roktas.dev
59
- - API documentation: https://www.rubydoc.info/gems/sevgi
60
- - Source: https://github.com/roktas/sevgi/tree/main/toplevel
61
- - Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
76
+ - Documentation: <https://sevgi.roktas.dev>
77
+ - API documentation: <https://www.rubydoc.info/gems/sevgi>
78
+ - Source: <https://github.com/roktas/sevgi/tree/main/toplevel>
79
+ - Changelog: <https://github.com/roktas/sevgi/blob/main/CHANGELOG.md>
data/bin/igsev ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "sevgi/binaries/igsev"
5
+
6
+ Sevgi.const_get(:Binaries, false)::Igsev.(ARGV)
data/bin/sevgi CHANGED
@@ -3,4 +3,4 @@
3
3
 
4
4
  require "sevgi/binaries/sevgi"
5
5
 
6
- Sevgi::Binaries::Sevgi.(ARGV)
6
+ Sevgi.const_get(:Binaries, false)::Sevgi.(ARGV)
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sevgi"
4
+
5
+ module Sevgi
6
+ # Command-line entrypoint implementations shipped with Sevgi.
7
+ # @api private
8
+ module Binaries
9
+ # Implements the `igsev` executable that round-trips SVG files through Sevgi.
10
+ # @api private
11
+ module Igsev
12
+ extend self
13
+
14
+ # Executable name used in help output.
15
+ PROGNAME = "igsev"
16
+
17
+ # Error raised for invalid command-line usage.
18
+ Error = Class.new(::Sevgi::Error)
19
+
20
+ FLAGS = {
21
+ "--exception" => :vomit,
22
+ "--help" => :help,
23
+ "--version" => :version,
24
+ "-h" => :help,
25
+ "-v" => :version,
26
+ "-x" => :vomit
27
+ }.freeze
28
+ private_constant :FLAGS
29
+
30
+ # Parsed command-line options for the `igsev` executable.
31
+ # @api private
32
+ Options = Struct.new(:require, :vomit, :help, :version, :omit) do
33
+ # Parses command-line options and removes them from the argv array.
34
+ # @param argv [Array<String>] mutable command-line argument array
35
+ # @return [Sevgi::Binaries::Igsev::Options] parsed options
36
+ # @raise [Sevgi::Binaries::Igsev::Error] when an option is not recognized or a required value is missing
37
+ def self.parse(argv)
38
+ new.tap do |options|
39
+ until argv.empty? || argv.first == "-" || !argv.first.start_with?("-")
40
+ if argv.first == "--"
41
+ argv.shift
42
+ break
43
+ end
44
+
45
+ option(argv, options)
46
+ end
47
+ end
48
+ end
49
+
50
+ class << self
51
+ private
52
+
53
+ def option(argv, options)
54
+ arg = argv.shift
55
+ return options[FLAGS[arg]] = true if FLAGS.key?(arg)
56
+ if ["-r", "--require"].include?(arg)
57
+ return options.require = argv.shift || Error.("Option requires a library: #{arg}")
58
+ end
59
+
60
+ return (options.omit ||= []) << (argv.shift || Error.("No attribute given for --omit")) if arg == "--omit"
61
+
62
+ Error.("Not a valid option: #{arg}")
63
+ end
64
+ end
65
+ end
66
+
67
+ private_constant :Options
68
+
69
+ # Runs the `igsev` command-line interface.
70
+ # @param argv [Array<String>, String, nil] command-line arguments
71
+ # @return [nil]
72
+ # @raise [StandardError] when `--exception` or `SEVGI_VOMIT` requests raw errors
73
+ # @raise [SystemExit] when argv does not match `[options...] [--] [file|-]` or conversion aborts
74
+ def call(argv)
75
+ dispatch(Array(argv))
76
+ rescue Binaries::Igsev::Error => e
77
+ abort("#{e.message}\n\n#{help}")
78
+ end
79
+
80
+ private
81
+
82
+ def backtrace(error)
83
+ return error.load_backtrace if error.respond_to?(:load_backtrace)
84
+
85
+ error.backtrace || []
86
+ end
87
+
88
+ def die(error)
89
+ warn(error.message)
90
+ warn("")
91
+ backtrace(error).each { warn(" #{it}") }
92
+
93
+ exit(1)
94
+ end
95
+
96
+ def dispatch(argv)
97
+ options = Options.parse(argv)
98
+ return puts(help) if options.help
99
+ return puts(::Sevgi::VERSION) if options.version
100
+
101
+ emit(operand(argv), options)
102
+ rescue Binaries::Igsev::Error
103
+ raise
104
+ rescue ::StandardError => e
105
+ raise if raw_error?(options)
106
+
107
+ die(e)
108
+ end
109
+
110
+ def emit(file, options)
111
+ result = run(file, options)
112
+ unless result.success?
113
+ raise result.error if raw_error?(options)
114
+
115
+ die(result.error)
116
+ end
117
+
118
+ result.value.Out()
119
+ end
120
+
121
+ def help
122
+ <<~HELP
123
+ Usage: #{PROGNAME} [options...] [--] [SVG file|-]
124
+
125
+ See documentation for detailed help.
126
+
127
+ Options:
128
+
129
+ --omit ATTRIBUTE Omit an attribute (repeatable)
130
+ -r, --require LIB Require Ruby LIB while evaluating generated source
131
+ -x, --exception Raise exception instead of abort
132
+ -- Stop option parsing
133
+
134
+ -h, --help Show this help
135
+ -v, --version Display version
136
+ HELP
137
+ end
138
+
139
+ def operand(argv)
140
+ file = argv.shift
141
+ Error.("Unexpected argument: #{argv.first}") unless argv.empty?
142
+
143
+ file unless file == "-"
144
+ end
145
+
146
+ def raw_error?(options) = options&.vomit || ENV.fetch(ENVVOMIT, nil)
147
+
148
+ def run(file, options)
149
+ source = if file
150
+ ::Sevgi::Derender.derender_file(file, omit: options.omit)
151
+ else
152
+ ::Sevgi::Derender.derender($stdin.read, omit: options.omit)
153
+ end
154
+
155
+ ::Sevgi.execute(source, require: options.require)
156
+ end
157
+ end
158
+ end
159
+
160
+ private_constant :Binaries
161
+ end
@@ -14,11 +14,11 @@ module FileUtils
14
14
  # @param file [String] Sevgi script file, with or without `.sevgi` extension
15
15
  # @param args [Array] positional arguments exposed to the script as `ARGA`
16
16
  # @param kwargs [Hash] keyword arguments exposed to the script as `ARGH`
17
- # @return [Sevgi::Executor::Scope, nil] execution scope, or nil for an empty file
17
+ # @return [Sevgi::Executor::Result] immutable execution result
18
18
  # @raise [Sevgi::ArgumentError] when the script file cannot be found
19
- # @see Sevgi::Executor.execute_file
19
+ # @see Sevgi.execute_file
20
20
  def sevgi(file, *args, **kwargs)
21
- Sevgi::Executor.execute_file(Sevgi::F.existing!(file, [Sevgi::EXTENSION])) do
21
+ Sevgi::Executor.__send__(:execute_file, Sevgi::F.existing!(file, [Sevgi::EXTENSION])) do
22
22
  extend(Sevgi)
23
23
 
24
24
  const_set(:ARGA, args).freeze
@@ -1,30 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "sevgi"
4
+ require "sevgi/skill"
4
5
 
5
6
  module Sevgi
6
7
  # Command-line entrypoint implementations shipped with Sevgi.
8
+ # @api private
7
9
  module Binaries
8
10
  # Implements the `sevgi` executable.
11
+ # @api private
9
12
  module Sevgi
10
13
  extend self
11
14
 
12
15
  # Executable name used in help output.
13
16
  PROGNAME = "sevgi"
14
17
 
18
+ # Logical source name used for standard input.
19
+ STDIN_NAME = "output.sevgi"
20
+
15
21
  # Error raised for invalid command-line usage.
16
22
  Error = Class.new(::Sevgi::Error)
17
23
 
24
+ FLAGS = {
25
+ "-x" => :vomit,
26
+ "--exception" => :vomit,
27
+ "-h" => :help,
28
+ "--help" => :help,
29
+ "--skill" => :skill,
30
+ "-v" => :version,
31
+ "--version" => :version
32
+ }.freeze
33
+
18
34
  # Parsed command-line options for the `sevgi` executable.
19
35
  # @api private
20
- Options = Struct.new(:require, :nomain, :vomit, :help, :version) do
36
+ Options = Struct.new(:require, :vomit, :help, :skill, :version, :as) do
21
37
  # Parses command-line options and removes them from the argv array.
22
38
  # @param argv [Array<String>] mutable command-line argument array
23
39
  # @return [Sevgi::Binaries::Sevgi::Options] parsed options
24
- # @raise [Sevgi::Binaries::Sevgi::Error] when an option is not recognized
40
+ # @raise [Sevgi::Binaries::Sevgi::Error] when an option is not recognized or a required value is missing
25
41
  def self.parse(argv)
26
42
  new.tap do |options|
27
- argv.first.start_with?("-") ? option(argv, options) : break until argv.empty?
43
+ until argv.empty? || argv.first == "-" || !argv.first.start_with?("-")
44
+ if argv.first == "--"
45
+ argv.shift
46
+ break
47
+ end
48
+
49
+ option(argv, options)
50
+ end
28
51
  end
29
52
  end
30
53
 
@@ -32,18 +55,14 @@ module Sevgi
32
55
  private
33
56
 
34
57
  def option(argv, options)
35
- case (arg = argv.shift)
36
- when "-r", "--require"
37
- options.require = argv.shift
38
- when "-n", "--nomain"
39
- options.nomain = true
40
- when "-x", "--exception"
41
- options.vomit = true
42
-
43
- when "-h", "--help"
44
- options.help = true
45
- when "-v", "--version"
46
- options.version = true
58
+ arg = argv.shift
59
+ if (flag = FLAGS[arg])
60
+ options[flag] = true
61
+ elsif ["-r", "--require"].include?(arg)
62
+ options.require = argv.shift || Error.("Option requires a library: #{arg}")
63
+ elsif arg == "--as"
64
+ options.as = argv.shift
65
+ Error.("Option requires a name: --as") if options.as.nil? || options.as.empty?
47
66
  else
48
67
  Error.("Not a valid option: #{arg}")
49
68
  end
@@ -51,31 +70,31 @@ module Sevgi
51
70
  end
52
71
  end
53
72
 
54
- private_constant :Options
73
+ private_constant :FLAGS, :Options, :STDIN_NAME
55
74
 
56
75
  # Runs the `sevgi` command-line interface.
57
76
  # @param argv [Array<String>, String, nil] command-line arguments
58
77
  # @return [nil]
59
78
  # @raise [Sevgi::Executor::Error] when `--exception` or `SEVGI_VOMIT` requests raw executor errors
60
- # @raise [SystemExit] when command-line usage or script execution aborts
79
+ # @raise [SystemExit] when argv does not match `[options...] [--] [file|-]` or script execution aborts
61
80
  def call(argv)
62
81
  return puts(help) if (options = Options.parse(argv = Array(argv))).help
82
+ return puts(Skill.path) if options.skill
63
83
  return puts(::Sevgi::VERSION) if options.version
64
84
 
65
- file = argv.shift
85
+ file = operand(argv)
66
86
  handle(run(file, options), file, options)
67
87
 
68
- rescue Binaries::Sevgi::Error => e
88
+ rescue Skill::Error => e
69
89
  abort(e.message)
90
+ rescue Binaries::Sevgi::Error => e
91
+ abort("#{e.message}\n\n#{help}")
70
92
  end
71
93
 
72
94
  private
73
95
 
74
96
  def die(error, _file)
75
- warn(error.message)
76
- warn("")
77
- error.backtrace!.each { warn(" #{it}") }
78
-
97
+ warn(error.message, "", *error.load_backtrace.map { " #{it}" })
79
98
  exit(1)
80
99
  end
81
100
 
@@ -89,26 +108,49 @@ module Sevgi
89
108
 
90
109
  def help
91
110
  <<~HELP
92
- Usage: #{PROGNAME} [options...] <Sevgi file>
111
+ Usage: #{PROGNAME} [options...] [--] [Sevgi file|-]
93
112
 
94
113
  See documentation for detailed help.
95
114
 
96
115
  Options:
97
116
 
98
- -n, --nomain Do not modify main object
117
+ --as NAME Evaluate input as NAME for implicit output names
99
118
  -r, --require LIB Require Ruby LIB
119
+ --skill Display the packaged agent skill path
100
120
  -x, --exception Raise exception instead of abort
121
+ -- Stop option parsing
101
122
 
102
123
  -h, --help Show this help
103
124
  -v, --version Display version
104
125
  HELP
105
126
  end
106
127
 
128
+ def operand(argv)
129
+ file = argv.shift
130
+ Error.("Unexpected argument: #{argv.first}") unless argv.empty?
131
+
132
+ file unless file == "-"
133
+ end
134
+
135
+ def execute_file(file, options)
136
+ name = source_name(options.as) if options.as
137
+ ::Sevgi.execute_file(file, as: name, require: options.require, main: true)
138
+ end
139
+
107
140
  def run(file, options)
108
- Error.("No sevgi file given.") unless file
141
+ return execute_file(file, options) if file
109
142
 
110
- ::Sevgi.execute_file(file, require: options.require, receiver: options.nomain ? nil : TOPLEVEL_BINDING.receiver)
143
+ ::Sevgi.execute($stdin.read, file: source_name(options.as), require: options.require, main: true)
144
+ end
145
+
146
+ def source_name(name)
147
+ return STDIN_NAME unless name
148
+
149
+ Error.("Option requires a name, not a path: --as") unless ::File.basename(name) == name
150
+ F.subext(".sevgi", name)
111
151
  end
112
152
  end
113
153
  end
154
+
155
+ private_constant :Binaries
114
156
  end
@@ -3,48 +3,40 @@
3
3
  module Sevgi
4
4
  class Executor
5
5
  # Raised when a source attempts to load another source already active in the same scope.
6
+ # @see https://sevgi.roktas.dev/execution/ Execution guide
6
7
  class CycleError < ::Sevgi::Error
7
8
  end
8
9
 
9
- # Wraps an exception raised while executing Sevgi script source.
10
+ # Wraps an exception raised while executing Sevgi script source. Its visited source snapshot records every source in
11
+ # load order; it is not the active load stack at the instant of failure.
12
+ # @see https://sevgi.roktas.dev/execution/ Execution guide
10
13
  class Error < ::Sevgi::Error
11
- # Returns the original script exception.
12
- # @return [Exception]
13
- attr_reader :error
14
-
15
- # Returns the executor scope active when the error was captured.
16
- # @return [Sevgi::Executor::Scope]
17
- attr_reader :scope
18
-
19
14
  # Builds an executor error wrapper.
20
15
  # @param error [Exception] original exception
21
- # @param scope [Sevgi::Executor::Scope] executor scope active at failure time
16
+ # @param stack [Array<String>] source file keys visited in load order; the Array and its String entries are copied
17
+ # and frozen
22
18
  # @return [void]
23
- def initialize(error, scope)
24
- @error = error
25
- @scope = scope
19
+ def initialize(error, stack)
20
+ @cause = error
21
+ @stack = stack.map { it.dup.freeze }.freeze
26
22
 
27
23
  super(error.message)
28
24
  end
29
25
 
30
- # Returns backtrace entries that belong to the Sevgi load stack.
31
- # @return [Array<String>] filtered backtrace lines relative to the current directory
32
- def backtrace!
33
- sources = stack.map { ::File.expand_path(it) }
26
+ # Returns backtrace entries that belong to the visited Sevgi source set.
27
+ # @return [Array<String>] filtered backtrace lines relative to the current directory, or an empty Array when the
28
+ # original exception has no backtrace
29
+ def load_backtrace
30
+ sources = @stack.map { ::File.expand_path(it) }
34
31
 
35
- error
36
- .backtrace
32
+ Array(cause.backtrace)
37
33
  .select { sources.include?(::File.expand_path(it.split(":", 2).first)) }
38
34
  .map { |line| line.delete_prefix("#{::Dir.pwd}/") }
39
35
  end
40
36
 
41
37
  # Returns the original exception as the wrapped cause.
42
38
  # @return [Exception]
43
- def cause = error
44
-
45
- # Returns the script load stack active at failure time.
46
- # @return [Array<String>] script file names in load order
47
- def stack = scope.stack
39
+ attr_reader :cause
48
40
  end
49
41
  end
50
42
  end