inquirex-tools 0.8.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2027915b1b69d07af80a30a63207731809243ae607da736ea2d558c2748c6c1f
4
+ data.tar.gz: 6c35d199ad702ffd061d90bf5fa5ebe17da1fc8b1f0d7722e3f8bf37676b3e4b
5
+ SHA512:
6
+ metadata.gz: a97efe3bc05fc24445e9b40920ec073055305e26c92764a7977cea0152cd33a25c28ccca97cf3e129ced24150b5957f0a137c57bc10172c1054cea7a5300ddfb
7
+ data.tar.gz: 0a975ec1e72ad597bf19827d9fe5f5afe68523bfea5809729a23ae184ab61a380b7aeff9128df1e984042c307a9de6c4ea464c660bf668474e731aa78779664e
data/.DS_Store ADDED
Binary file
@@ -0,0 +1,5 @@
1
+ user=inquirex
2
+ project=inquirex-tools
3
+ exclude-labels=duplicate,question,invalid,wontfix
4
+ issues=false
5
+ add-sections={"security":{"prefix":"### Security","labels":["security"]}}
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2026-07-31
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Konstantin Gredeskoul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # Inquirex::Tools
2
+
3
+ TODO: Delete this and the text below, and describe your gem
4
+
5
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/inquirex/tools`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+
7
+ ## Installation
8
+
9
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ ```bash
14
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
15
+ ```
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ ```bash
20
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/kigster/inquirex-tools.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "timeout"
6
+ require "yard"
7
+
8
+ def shell(*args)
9
+ puts "running: #{args.join(" ")}"
10
+ system(args.join(" "))
11
+ end
12
+
13
+ task :clean do
14
+ shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
15
+ end
16
+
17
+ task gem: [:build] do
18
+ shell("gem install pkg/*")
19
+ end
20
+
21
+ task permissions: [:clean] do
22
+ # One traversal replaces a six-level glob chain that printed "No such file
23
+ # or directory" for every level this project does not have, skipped dotfiles
24
+ # entirely, and silently stopped at depth six. .git is pruned — its objects
25
+ # have no business being group-readable.
26
+ shell("find . -path ./.git -prune -o -type d -exec chmod o+rx,g+rx {} + -o -type f -exec chmod o+r,g+r {} +")
27
+ end
28
+
29
+ task build: :permissions
30
+
31
+ YARD::Rake::YardocTask.new(:doc) do |t|
32
+ t.files = %w[lib/**/*.rb exe/*.rb - README.md LICENSE.txt CHANGELOG.md]
33
+ t.options.unshift("--title", '"FlowEngine — DSL + AST for buildiong complex flows in Ruby."')
34
+ t.after = -> { exec("open doc/index.html") } if RUBY_PLATFORM =~ /darwin/
35
+ end
36
+
37
+ RSpec::Core::RakeTask.new(:spec)
38
+
39
+ task default: :spec
data/exe/inquirex ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # The `inquirex` umbrella CLI. `inquirex-tty` ships its own `inquirex-tty`
5
+ # binary; this gem owns the bare name so cross-family tooling has one entry
6
+ # point and no two gems install a binary called `inquirex`.
7
+ #
8
+ # inquirex versions # lockstep parity across the family
9
+ # inquirex versions --set 0.8.0
10
+ # inquirex changelogs
11
+ #
12
+ # Inside a checkout, prefer the repo's own Gemfile over whatever rubygems has
13
+ # installed globally — the same bootstrap inquirex-tty uses.
14
+ gemfile = File.expand_path("../Gemfile", __dir__)
15
+ if File.file?(gemfile)
16
+ ENV["BUNDLE_GEMFILE"] = gemfile
17
+ begin
18
+ require "bundler/setup"
19
+ rescue LoadError
20
+ # Bundler unavailable — fall through to priming $LOAD_PATH directly.
21
+ end
22
+ end
23
+
24
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__)) if File.directory?("#{__dir__}/../lib")
25
+
26
+ require "inquirex/tools"
27
+ require "inquirex/tools/commands"
28
+
29
+ Dry::CLI.new(Inquirex::Tools::Commands).call
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "workspace"
4
+
5
+ module Inquirex
6
+ module Tools
7
+ # Regenerates CHANGELOG.md across the ecosystem from merged GitHub PRs.
8
+ #
9
+ # The generated files are disposable. `inquirex/INQUIREX_VERSION_LOG.md` is
10
+ # not, and is never touched here — a PR title records what merged, not what
11
+ # it means or what a consumer must do about it.
12
+ class Changelogs
13
+ # The gem is not installed for every Ruby the repos pin (inquirex pins
14
+ # 4.0.5), so the generator runs under one that has it rather than being
15
+ # installed into each.
16
+ DEFAULT_RUBY = "4.0.6"
17
+
18
+ # Never overwritten by generation, in any repo.
19
+ PROTECTED = %w[INQUIREX_VERSION_LOG.md].freeze
20
+
21
+ # @param workspace [Workspace]
22
+ # @param out [IO]
23
+ def initialize(workspace: Workspace.new, out: $stdout)
24
+ @workspace = workspace
25
+ @out = out
26
+ end
27
+
28
+ # @return [Array<String>] every repo carrying a generator config
29
+ def repositories
30
+ (Workspace::LOCKSTEP.keys + Workspace::CHANGELOG_ONLY).uniq.select do |name|
31
+ File.exist?(File.join(@workspace.root, name, ".github_changelog_generator"))
32
+ end
33
+ end
34
+
35
+ # Runs the generator in each repository.
36
+ #
37
+ # The token comes from `gh auth token` rather than a stored secret, so
38
+ # nothing here handles a credential in plain text and there is none to
39
+ # leak from the source.
40
+ #
41
+ # @param names [Array<String>] repositories, or all configured when empty
42
+ # @return [Boolean] true when every repository succeeded
43
+ def generate(*names)
44
+ token = `gh auth token 2>/dev/null`.strip
45
+ if token.empty?
46
+ @out.puts "ERROR: `gh auth token` returned nothing — run `gh auth login`"
47
+ return false
48
+ end
49
+
50
+ targets = names.flatten.compact
51
+ targets = repositories if targets.empty?
52
+ failed = targets.reject { |name| generate_one(name, token) }
53
+
54
+ report_failures(failed)
55
+ failed.empty?
56
+ end
57
+
58
+ private
59
+
60
+ # @param name [String] repository directory name
61
+ # @param token [String] GitHub token for the generator
62
+ # @return [Boolean] true on success or a deliberate skip
63
+ def generate_one(name, token)
64
+ dir = File.join(@workspace.root, name)
65
+ unless File.exist?(File.join(dir, ".github_changelog_generator"))
66
+ @out.puts format(" %<name>-16s SKIP (no .github_changelog_generator)", name: name)
67
+ return true
68
+ end
69
+
70
+ @out.print format(" %<name>-16s ", name: name)
71
+ env = { "CHANGELOG_GITHUB_TOKEN" => token, "RBENV_VERSION" => ENV.fetch("RBENV_VERSION", DEFAULT_RUBY) }
72
+ if system(env, "github_changelog_generator", chdir: dir, out: File::NULL, err: File::NULL)
73
+ @out.puts "ok (#{line_count(File.join(dir, "CHANGELOG.md"))} lines)"
74
+ true
75
+ else
76
+ @out.puts "FAILED"
77
+ false
78
+ end
79
+ end
80
+
81
+ # @param path [String]
82
+ # @return [Integer]
83
+ def line_count(path) = File.exist?(path) ? File.readlines(path).size : 0
84
+
85
+ # @param failed [Array<String>]
86
+ # @return [void]
87
+ def report_failures(failed)
88
+ return if failed.empty?
89
+
90
+ @out.puts
91
+ @out.puts "Failed: #{failed.join(", ")}"
92
+ @out.puts "Re-run one with output: cd <repo> && " \
93
+ "CHANGELOG_GITHUB_TOKEN=$(gh auth token) github_changelog_generator"
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module Tools
5
+ module Commands
6
+ # Regenerates CHANGELOG.md across the ecosystem from merged GitHub PRs.
7
+ class Changelogs < Dry::CLI::Command
8
+ desc "Regenerate CHANGELOG.md from merged PRs"
9
+
10
+ argument :repos, type: :array, required: false,
11
+ desc: "Repositories to regenerate (default: all configured)"
12
+
13
+ example [
14
+ " # every repo with a .github_changelog_generator",
15
+ "inquirex-js # just one"
16
+ ]
17
+
18
+ # The GitHub token comes from `gh auth token`, so no secret is stored
19
+ # or passed on the command line. inquirex/INQUIREX_VERSION_LOG.md is
20
+ # hand-written and never touched.
21
+ #
22
+ # @param repos [Array<String>, nil]
23
+ # @return [void]
24
+ def call(repos: [], **)
25
+ exit(1) unless Inquirex::Tools::Changelogs.new.generate(*Array(repos))
26
+ rescue Inquirex::Tools::Error => e
27
+ warn "ERROR: #{e.message}"
28
+ exit 1
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module Tools
5
+ module Commands
6
+ # Prints this tool's own version. See `versions` (plural) for the
7
+ # family's lockstep version.
8
+ class Version < Dry::CLI::Command
9
+ desc "Print inquirex-tools version"
10
+
11
+ # @return [void]
12
+ def call(**) = puts("inquirex-tools #{Inquirex::Tools::VERSION}")
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module Tools
5
+ module Commands
6
+ # Reports and enforces version parity across the lockstep packages.
7
+ #
8
+ # Plural on purpose: `inquirex version` prints this tool's own version,
9
+ # `inquirex versions` manages the family's.
10
+ class Versions < Dry::CLI::Command
11
+ desc "Check, set, or preflight the family's lockstep version"
12
+
13
+ option :set, type: :string, desc: "Move every lockstep package to this version"
14
+ option :preflight, type: :boolean, default: false,
15
+ desc: "Also check clean trees, main branch, and tags"
16
+
17
+ example [
18
+ " # report parity, exit 1 on drift",
19
+ "--set 0.8.0 # move all five packages",
20
+ "--preflight # release readiness"
21
+ ]
22
+
23
+ # @param set [String, nil] target version
24
+ # @param preflight [Boolean]
25
+ # @return [void]
26
+ def call(set: nil, preflight: false, **)
27
+ versions = Inquirex::Tools::Versions.new
28
+ ok =
29
+ if set then versions.set(set)
30
+ elsif preflight then versions.preflight
31
+ else versions.check
32
+ end
33
+ exit(1) unless ok
34
+ rescue Inquirex::Tools::Error => e
35
+ warn "ERROR: #{e.message}"
36
+ exit 1
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+
5
+ require_relative "commands/version"
6
+ require_relative "commands/versions"
7
+ require_relative "commands/changelogs"
8
+
9
+ module Inquirex
10
+ module Tools
11
+ # dry-cli registry for the `inquirex` executable.
12
+ #
13
+ # `inquirex-tty` owns `inquirex-tty`; this gem owns the bare `inquirex`
14
+ # command, so cross-family tooling has one entry point and the two gems no
15
+ # longer install binaries with the same name.
16
+ module Commands
17
+ extend Dry::CLI::Registry
18
+
19
+ register "versions", Versions
20
+ register "changelogs", Changelogs
21
+ register "version", Version
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module Tools
5
+ VERSION = "0.8.0"
6
+ end
7
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems/version"
4
+ require_relative "workspace"
5
+
6
+ module Inquirex
7
+ module Tools
8
+ # Reports and enforces version parity across the lockstep packages.
9
+ #
10
+ # @example Check for drift
11
+ # Inquirex::Tools::Versions.new.check # => false when versions differ
12
+ class Versions
13
+ # @param workspace [Workspace]
14
+ # @param out [IO] where report output goes
15
+ def initialize(workspace: Workspace.new, out: $stdout)
16
+ @workspace = workspace
17
+ @out = out
18
+ end
19
+
20
+ # Prints the current state of every lockstep package.
21
+ #
22
+ # @return [Boolean] true when all packages agree on one version
23
+ def check
24
+ versions = @workspace.versions
25
+ print_table(versions)
26
+ verdict(versions)
27
+ end
28
+
29
+ # Moves every lockstep package to one version.
30
+ #
31
+ # Refuses to move backwards: a version already published cannot be
32
+ # reissued, so the target must exceed every current value.
33
+ #
34
+ # @param version [String] target, MAJOR.MINOR.PATCH
35
+ # @return [Boolean] true when written
36
+ def set(version)
37
+ unless /\A\d+\.\d+\.\d+\z/.match?(version.to_s)
38
+ @out.puts "ERROR: #{version.inspect} is not MAJOR.MINOR.PATCH"
39
+ return false
40
+ end
41
+
42
+ current = @workspace.versions.values.compact
43
+ highest = current.max_by { Gem::Version.new(_1) }
44
+ if highest && Gem::Version.new(highest) > Gem::Version.new(version)
45
+ @out.puts "ERROR: #{version} is below the highest existing version (#{highest}) — versions only go forward"
46
+ return false
47
+ end
48
+
49
+ @out.puts "Setting all lockstep packages to #{version}:"
50
+ @workspace.versions.each do |name, was|
51
+ @workspace.write_version(name, version)
52
+ @out.puts format(" %-16s %s -> %s", name, was || "?", version)
53
+ end
54
+ @out.puts
55
+ @out.puts "Unchanged (excluded): #{Workspace::EXCLUDED.join(", ")}"
56
+ true
57
+ end
58
+
59
+ # Parity plus git readiness: clean tree, on main, tagged at the version.
60
+ #
61
+ # @return [Boolean] true when every package could be published now
62
+ def preflight
63
+ ok = check
64
+ target = @workspace.versions.values.compact.uniq.first
65
+ @out.puts
66
+ @out.puts "Release readiness:"
67
+
68
+ Workspace::LOCKSTEP.each_key do |name|
69
+ problems = readiness_problems(name, target)
70
+ @out.puts format(" %<name>-16s %<state>s", name: name, state: problems.empty? ? "ready" : problems.join(", "))
71
+ ok &&= problems.empty?
72
+ end
73
+
74
+ @out.puts
75
+ @out.puts ok ? "ALL READY — publish with `gem push` / `npm publish`" : "NOT READY — resolve the above"
76
+ ok
77
+ end
78
+
79
+ private
80
+
81
+ # @param versions [Hash{String => String, nil}]
82
+ # @return [void]
83
+ def print_table(versions)
84
+ @out.puts "Lockstep packages (#{Workspace::EXCLUDED.join(", ")} excluded by design):"
85
+ versions.each do |name, version|
86
+ @out.puts format(" %<name>-16s %<kind>-4s %<version>s",
87
+ name: name, kind: Workspace::LOCKSTEP[name], version: version || "MISSING")
88
+ end
89
+ @out.puts
90
+ end
91
+
92
+ # @param versions [Hash{String => String, nil}]
93
+ # @return [Boolean] true when every package agrees on one version
94
+ def verdict(versions)
95
+ distinct = versions.values.compact.uniq
96
+ if versions.value?(nil)
97
+ @out.puts "DRIFT: a version could not be read (MISSING above)"
98
+ false
99
+ elsif distinct.one?
100
+ @out.puts "OK: all #{versions.size} at #{distinct.first}"
101
+ true
102
+ else
103
+ @out.puts "DRIFT: #{distinct.sort.join(", ")} — run `inquirex versions --set <version>`"
104
+ false
105
+ end
106
+ end
107
+
108
+ # Why a package is not publishable right now.
109
+ #
110
+ # @param name [String] package name
111
+ # @param target [String, nil] the version its tag must match
112
+ # @return [Array<String>] empty when ready
113
+ def readiness_problems(name, target)
114
+ state = @workspace.git_state(name)
115
+ return [ "no git checkout" ] if state[:clean].nil?
116
+
117
+ problems = []
118
+ problems << "DIRTY" unless state[:clean]
119
+ problems << "on #{state[:branch]}" unless state[:branch] == "main"
120
+ problems << "untagged" unless state[:tags].any? { |tag| tag.delete_prefix("v") == target }
121
+ problems
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Inquirex
6
+ module Tools
7
+ # Locates the Inquirex ecosystem checkout and reads each package's version.
8
+ #
9
+ # As gem executables these tools run from an rbenv shim, so `__dir__` says
10
+ # nothing about where the repositories live. The root is discovered by
11
+ # walking up from the working directory looking for the sibling checkouts,
12
+ # or taken from INQUIREX_ROOT.
13
+ class Workspace
14
+ # Packages that must always carry the same version. They share one
15
+ # serialization format — the gem defines the DSL and emits step JSON,
16
+ # inquirex-js renders it, inquirex-webui prints it back to DSL,
17
+ # inquirex-llm extends the vocabulary, and inquirex-tools is the tooling
18
+ # that ships alongside them. A verb present in one and absent in another
19
+ # does not fail loudly; it drops data.
20
+ #
21
+ # `inquirex-tty` is deliberately absent: a developer tool outside the
22
+ # wire contract, versioned on its own cadence.
23
+ LOCKSTEP = {
24
+ "inquirex" => :gem,
25
+ "inquirex-llm" => :gem,
26
+ "inquirex-tools" => :gem,
27
+ "inquirex-js" => :npm,
28
+ "inquirex-webui" => :npm
29
+ }.freeze
30
+
31
+ # Excluded from lockstep, documented so the omission reads as deliberate.
32
+ EXCLUDED = %w[inquirex-tty].freeze
33
+
34
+ # Repositories that carry a generated CHANGELOG.md but are not versioned
35
+ # in lockstep.
36
+ CHANGELOG_ONLY = %w[inquirex-tty qualified-at].freeze
37
+
38
+ GEM_VERSION_RE = /VERSION\s*=\s*"([^"]+)"/
39
+
40
+ # Directories whose presence identifies the ecosystem root.
41
+ MARKERS = %w[inquirex inquirex-js].freeze
42
+
43
+ # @return [String] absolute path to the ecosystem root
44
+ attr_reader :root
45
+
46
+ # @param root [String, nil] explicit root; discovered when nil
47
+ # @raise [Error] when no root can be found
48
+ def initialize(root: nil)
49
+ @root = root || ENV["INQUIREX_ROOT"] || self.class.discover
50
+ raise Error, "cannot find the Inquirex workspace — run inside it or set INQUIREX_ROOT" if @root.nil?
51
+ end
52
+
53
+ # Walks up from the working directory looking for a directory that holds
54
+ # the marker checkouts.
55
+ #
56
+ # @return [String, nil]
57
+ def self.discover
58
+ dir = Dir.pwd
59
+ loop do
60
+ return dir if MARKERS.all? { |m| Dir.exist?(File.join(dir, m)) }
61
+
62
+ parent = File.dirname(dir)
63
+ return nil if parent == dir
64
+
65
+ dir = parent
66
+ end
67
+ end
68
+
69
+ # @param name [String] package name
70
+ # @return [String, nil] path to the file carrying its version
71
+ def version_file(name)
72
+ case LOCKSTEP[name]
73
+ when :npm then existing(File.join(name, "package.json"))
74
+ when :gem then gem_version_file(name)
75
+ end
76
+ end
77
+
78
+ # @param name [String] package name
79
+ # @return [String, nil] the current version, or nil when unreadable
80
+ def version_of(name)
81
+ file = version_file(name)
82
+ return nil unless file && File.exist?(file)
83
+
84
+ LOCKSTEP[name] == :gem ? File.read(file)[GEM_VERSION_RE, 1] : JSON.parse(File.read(file))["version"]
85
+ end
86
+
87
+ # @return [Hash{String => String, nil}] every lockstep package's version
88
+ def versions = LOCKSTEP.keys.to_h { |n| [n, version_of(n)] }
89
+
90
+ # Rewrites one package's version. Edits the single version line rather
91
+ # than re-emitting the file, so formatting and comments survive.
92
+ #
93
+ # @param name [String] package name
94
+ # @param version [String] new version
95
+ # @return [void]
96
+ def write_version(name, version)
97
+ file = version_file(name)
98
+ raise Error, "no version file for #{name}" unless file
99
+
100
+ src = File.read(file)
101
+ updated =
102
+ if LOCKSTEP[name] == :gem
103
+ src.sub(GEM_VERSION_RE, %(VERSION = "#{version}"))
104
+ else
105
+ src.sub(/"version":\s*"[^"]*"/, %("version": "#{version}"))
106
+ end
107
+ File.write(file, updated)
108
+ end
109
+
110
+ # @param name [String] repository directory name
111
+ # @return [Hash] :clean, :branch, :tags — all nil when not a checkout
112
+ def git_state(name)
113
+ dir = File.join(@root, name)
114
+ return { clean: nil, branch: nil, tags: [] } unless File.exist?(File.join(dir, ".git"))
115
+
116
+ {
117
+ clean: `git -C #{dir} status --porcelain 2>/dev/null`.strip.empty?,
118
+ branch: `git -C #{dir} rev-parse --abbrev-ref HEAD 2>/dev/null`.strip,
119
+ tags: `git -C #{dir} tag --points-at HEAD 2>/dev/null`.split("\n").map(&:strip).reject(&:empty?)
120
+ }
121
+ end
122
+
123
+ private
124
+
125
+ # A gem's version.rb, chosen by content rather than by name.
126
+ #
127
+ # `lib/**/version.rb` can match more than one file — this gem has both
128
+ # `lib/inquirex/tools/version.rb` and the CLI's
129
+ # `lib/inquirex/tools/commands/version.rb`, and the latter sorts first
130
+ # alphabetically. Requiring the VERSION constant excludes impostors;
131
+ # preferring the shallowest path picks the canonical one when several
132
+ # legitimately define it.
133
+ #
134
+ # @param name [String] package name
135
+ # @return [String, nil]
136
+ def gem_version_file(name)
137
+ Dir.glob(File.join(@root, name, "lib/**/version.rb"))
138
+ .select { |file| File.read(file).match?(GEM_VERSION_RE) }
139
+ .min_by { |file| [file.count("/"), file] }
140
+ end
141
+
142
+ # @param relative [String]
143
+ # @return [String, nil] absolute path when it exists
144
+ def existing(relative)
145
+ path = File.join(@root, relative)
146
+ File.exist?(path) ? path : nil
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tools/version"
4
+ require_relative "tools/workspace"
5
+ require_relative "tools/versions"
6
+ require_relative "tools/changelogs"
7
+
8
+ module Inquirex
9
+ # Shared tooling for the Inquirex family.
10
+ #
11
+ # This gem is the home for anything several packages need but none of them
12
+ # owns — release plumbing today, and whatever else turns out to be duplicated
13
+ # across the gems tomorrow.
14
+ #
15
+ # It ships developer executables, so it may take dependencies the runtime
16
+ # gems cannot. That is the point of separating it: `inquirex` itself is
17
+ # required to have zero runtime dependencies, and nothing added here changes
18
+ # that, because nothing here is loaded by the runtime gems.
19
+ #
20
+ # Executables:
21
+ #
22
+ # - `inquirex-version` — version parity across the lockstep packages
23
+ # - `inquirex-changelogs` — regenerate CHANGELOG.md from merged PRs
24
+ #
25
+ # @example Check whether the family is releasable
26
+ # Inquirex::Tools::Versions.new.preflight # => true when every package is ready
27
+ module Tools
28
+ class Error < StandardError; end
29
+ end
30
+ end
@@ -0,0 +1,6 @@
1
+ module Inquirex
2
+ module Tools
3
+ VERSION: String
4
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
5
+ end
6
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inquirex-tools
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Konstantin Gredeskoul
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: dry-cli
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.0'
26
+ description: |
27
+ Ships the `inquirex` command: lockstep version management across the
28
+ Inquirex gems and npm packages, changelog regeneration, and release
29
+ preflight checks. A developer tool — it never depends on the gems it
30
+ manages, so `inquirex` itself stays dependency-free.
31
+ email:
32
+ - kigster@gmail.com
33
+ executables:
34
+ - inquirex
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - ".DS_Store"
39
+ - ".github_changelog_generator"
40
+ - CHANGELOG.md
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - exe/inquirex
45
+ - lib/inquirex/tools.rb
46
+ - lib/inquirex/tools/changelogs.rb
47
+ - lib/inquirex/tools/commands.rb
48
+ - lib/inquirex/tools/commands/changelogs.rb
49
+ - lib/inquirex/tools/commands/version.rb
50
+ - lib/inquirex/tools/commands/versions.rb
51
+ - lib/inquirex/tools/version.rb
52
+ - lib/inquirex/tools/versions.rb
53
+ - lib/inquirex/tools/workspace.rb
54
+ - sig/inquirex/tools.rbs
55
+ homepage: https://github.com/kigster/inquirex-tools
56
+ licenses:
57
+ - MIT
58
+ metadata:
59
+ allowed_push_host: https://rubygems.org
60
+ homepage_uri: https://github.com/kigster/inquirex-tools
61
+ source_code_uri: https://github.com/kigster/inquirex-tools
62
+ changelog_uri: https://github.com/kigster/inquirex-tools/blob/main/CHANGELOG.md
63
+ rdoc_options: []
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 3.2.0
71
+ required_rubygems_version: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ requirements: []
77
+ rubygems_version: 4.0.17
78
+ specification_version: 4
79
+ summary: Release tooling for the Inquirex package family
80
+ test_files: []