inquirex-tools 0.9.4 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a86b68b3b9abc13c3046754f8b5f43a69f84360e3b581e5760ab9145049da76a
4
- data.tar.gz: 5001830e113f7c7b63547f9f49ca10cb290b946c90a0916313e83dee0e1f479a
3
+ metadata.gz: c0382f5f2ef1fed61354873914a92e24e3b25dbead0959aac9de8f48702bdfdf
4
+ data.tar.gz: 25c18df8bb5cb9a7bcad98668431ff7587df5444b7797917f8c3201308168170
5
5
  SHA512:
6
- metadata.gz: ab2d90315649ab9d2465156964f6436b96a79a5fb0a32c4489ef2910d8a991166da17bcd1731bae08d830097bb2249b91ca9c7ad339e171d48004b812363c8f7
7
- data.tar.gz: f7258bdb64b70a46a21b09ccb0f58111077e326b290fdaa3fc72e5cfea1c661abe546e005bce892ec69662fed81c938cbf1645a9cad4eb8e61f0061b56ff721f
6
+ metadata.gz: 0dc205efda474da503f17a2ac1259b12193996621bba9041ff696e67563bde5b050dd0ca52bb5149b261b694fdb941b29755dd05a9fb71a81c335c65ce903e8d
7
+ data.tar.gz: ddd1e89483f67689fc64b4d4618d19b0a667dfd91da9778b26ee56d7ff36c54c657540bf15441c7fb53273d15b4403d5d4705f1f61292e8772142fab65d6535c
data/.envrc CHANGED
@@ -1 +1,3 @@
1
1
  PATH_add bin
2
+
3
+ [[ -f ../.envrc ]] && source ../.envrc
@@ -0,0 +1,21 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg xmlns="http://www.w3.org/2000/svg" width="99" height="20">
3
+ <linearGradient id="b" x2="0" y2="100%">
4
+ <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
5
+ <stop offset="1" stop-opacity=".1"/>
6
+ </linearGradient>
7
+ <mask id="a">
8
+ <rect width="99" height="20" rx="3" fill="#fff"/>
9
+ </mask>
10
+ <g mask="url(#a)">
11
+ <path fill="#555" d="M0 0h63v20H0z"/>
12
+ <path fill="#4c1" d="M63 0h36v20H63z"/>
13
+ <path fill="url(#b)" d="M0 0h99v20H0z"/>
14
+ </g>
15
+ <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
16
+ <text x="31.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
17
+ <text x="31.5" y="14">coverage</text>
18
+ <text x="80" y="15" fill="#010101" fill-opacity=".3">97%</text>
19
+ <text x="80" y="14">97%</text>
20
+ </g>
21
+ </svg>
@@ -0,0 +1,29 @@
1
+ # Many Repos Setup
2
+
3
+ ## Problem
4
+
5
+ Inquirex Organization houses several interlinked public repositories under https://github.com/inquirex that are often worked on in tandem, and checked out into the same folder.
6
+
7
+ For instance, seeting up the entire tree requires the following:
8
+
9
+ ```bash
10
+ cd ~/
11
+ mkdir -p ~/github/inquirex
12
+ cd ~/github/inquirex
13
+ declare -a repos=(
14
+ inquirex
15
+ inquirex-llm
16
+ inquirex-tty
17
+ inquirex-webui
18
+ inquirex-widget
19
+ inquirex-tools
20
+ inquirex-claude
21
+ )
22
+ for repo in ${repos[@]}; do
23
+ git clone git@github.com:inquirex/${repo}
24
+ done
25
+
26
+ cd inquirex-tools && bin/setup
27
+
28
+ ```
29
+
@@ -0,0 +1,163 @@
1
+ # Monorepo Migration Plan
2
+
3
+ Status: **proposed**, not started. Execute only after the open PRs listed under [Sequencing](#sequencing) have merged.
4
+
5
+ ## Why
6
+
7
+ The Inquirex ecosystem implements the same semantics in two languages and enforces agreement by convention. Three independent implementations of "canonicalize an LLM answer against a question's allowed values" exist today:
8
+
9
+ | Implementation | Repo | Exact | Case-insensitive value | Label fallback | Strips whitespace |
10
+ | ------------------------- | --------------- | ----- | ---------------------- | -------------- | ----------------- |
11
+ | `Node#resolve_option` | inquirex | yes | yes | yes | no |
12
+ | `Adapter#canonical_value` | inquirex-llm | yes | yes | **no** | **yes** |
13
+ | `canonicalOption` | inquirex-widget | yes | yes | yes | **no** |
14
+
15
+ They disagree on real inputs. `"US citizen or permanent resident"` resolves through the gem and the widget but returns `nil` through the LLM adapter. `" us_person "` resolves only through the LLM adapter. Nothing in CI can observe this, because no CI job sees more than one of them.
16
+
17
+ This is the failure mode the family already has scar tissue for. The gem shipped `required false` in 0.7.0 and no consumer knew, because a verb present in one package and missing in another does not fail loudly — it silently drops data. The lockstep version rule exists to make "which combination has this bug?" a lookup rather than an investigation.
18
+
19
+ The rule is not actually enforced. Every inter-family gemspec declares `~> 0.6`, which admits any 0.x from 0.6 upward. `inquirex-llm`'s checked-in `Gemfile.lock` currently resolves `inquirex-llm (0.9.2)` against `inquirex (0.8.0)`.
20
+
21
+ A monorepo does not fix divergence by itself. It makes the fix — one shared fixture set, executed by every implementation in one CI run — cheap enough to actually build.
22
+
23
+ ## Scope
24
+
25
+ **In**, as one public repository:
26
+
27
+ - `inquirex` — the DSL, engine, and rule AST
28
+ - `inquirex-llm` — LLM verbs
29
+ - `inquirex-widget` — the embeddable TypeScript widget
30
+ - `inquirex-tty` — terminal adapter
31
+ - `inquirex-tools` — release and version tooling (largely absorbed; see below)
32
+
33
+ **Out**, staying as they are:
34
+
35
+ - `qualified-at` — the commercial Rails application. Private, its own release cadence, and it consumes the libraries as published packages. Folding a revenue product into the open-source repository forces a visibility decision on the whole tree and buys nothing.
36
+ - `inquirex-webui` — private, React, a distinct product (visual builder). Revisit once the library tree has settled.
37
+ - `inquirex-presentation`, `inquirex-slides`, `claude-setup` — unrelated artifacts.
38
+
39
+ The in-scope set is exactly the packages bound by the lockstep rule, minus `inquirex-webui`, plus `inquirex-tty` (which PR #2 in this repo brings into lockstep).
40
+
41
+ ## Target layout
42
+
43
+ ```
44
+ inquirex/
45
+ packages/
46
+ inquirex/ # gem: DSL, engine, rules
47
+ inquirex-llm/ # gem: LLM verbs
48
+ inquirex-tty/ # gem: terminal adapter
49
+ inquirex-widget/ # npm: TypeScript widget
50
+ conformance/
51
+ fixtures/ # language-neutral JSON cases
52
+ ruby/ # runner: executes fixtures against the gems
53
+ typescript/ # runner: executes fixtures against the widget
54
+ tools/ # release tooling (from inquirex-tools)
55
+ justfile # top-level recipes; per-package justfiles remain
56
+ .github/workflows/
57
+ ```
58
+
59
+ Each package keeps its own gemspec / package.json, test suite, and justfile. The monorepo adds a layer above them, it does not merge their build systems.
60
+
61
+ ## Migration mechanics
62
+
63
+ History is preserved. Do not squash the repositories into a single import commit.
64
+
65
+ For each repository, in the order listed under Sequencing:
66
+
67
+ ```bash
68
+ # 1. Clone a scratch copy — never run filter-repo against your working checkout
69
+ git clone git@github.com:inquirex/<pkg>.git /tmp/mono/<pkg>
70
+ cd /tmp/mono/<pkg>
71
+
72
+ # 2. Rewrite every path to sit under packages/<pkg>/
73
+ git filter-repo --to-subdirectory-filter packages/<pkg>
74
+
75
+ # 3. Graft it into the monorepo as an unrelated history
76
+ cd /path/to/inquirex-monorepo
77
+ git remote add <pkg> /tmp/mono/<pkg>
78
+ git fetch <pkg>
79
+ git merge --allow-unrelated-histories <pkg>/main -m "Import <pkg> under packages/"
80
+ git remote remove <pkg>
81
+ ```
82
+
83
+ `git filter-repo` requires a fresh clone and refuses to run on a repo with an origin unless forced — that is a guardrail, not an obstacle. Install with `brew install git-filter-repo`.
84
+
85
+ Path rewriting means `git log --follow packages/inquirex/lib/inquirex/node.rb` walks the full pre-migration history. Blame survives. Existing tags survive but become ambiguous across packages — retag as `<pkg>/vX.Y.Z` after the import, or accept that pre-migration tags refer to a single package's history.
86
+
87
+ ### What happens to the old repositories
88
+
89
+ Archive, do not delete. Each keeps its issues, its pull requests, and its clone URLs. GitHub archived repos stay readable and redirect nothing, so add a line to each README pointing at the monorepo path. RubyGems and npm metadata should be updated at the next release to point `source_code_uri` / `repository.url` at `inquirex/inquirex` with the appropriate `directory`.
90
+
91
+ Both npm and RubyGems support a subdirectory hint:
92
+
93
+ ```jsonc
94
+ // packages/inquirex-widget/package.json
95
+ "repository": { "type": "git", "url": "git+https://github.com/inquirex/inquirex.git", "directory": "packages/inquirex-widget" }
96
+ ```
97
+
98
+ ```ruby
99
+ # packages/inquirex/inquirex.gemspec
100
+ spec.metadata["source_code_uri"] = "https://github.com/inquirex/inquirex/tree/main/packages/inquirex"
101
+ ```
102
+
103
+ ## Versioning and release
104
+
105
+ One version number for the tree, which is what the lockstep rule always wanted. A single `VERSION` file at the root; each gemspec and `package.json` reads from it at build time.
106
+
107
+ This deletes most of `inquirex-tools`: cross-repo version checking and bumping becomes `just version <increment>` operating on one file. Keep the release recipes (tagging, GitHub release, publishing); drop the multi-repo coordination.
108
+
109
+ Fix the constraint hole at the same time. Inter-package dependencies move from `~> 0.6` to `= <VERSION>` or `~> <MAJOR>.<MINOR>`, so bundler cannot resolve a mismatched pair. Do this **in the release that first ships from the monorepo**, not before — tightening `inquirex-llm` to `~> 0.9` while `inquirex` 0.9.x is unpublished breaks `bundle install`.
110
+
111
+ ## CI
112
+
113
+ One workflow with path filters, so a widget-only change does not run the Ruby matrix:
114
+
115
+ - `ruby.yml` — triggers on `packages/inquirex*/**` and `conformance/**`; runs each gem's suite
116
+ - `typescript.yml` — triggers on `packages/inquirex-widget/**` and `conformance/**`; runs `bun test`, typecheck, lint, build
117
+ - `conformance.yml` — triggers on **everything**; runs the shared fixtures against both language runners
118
+
119
+ The third workflow is the point of the exercise. It must not be path-filtered.
120
+
121
+ ## The conformance suite
122
+
123
+ A fixture is a JSON document describing a flow, a sequence of inputs, and the expected resulting state:
124
+
125
+ ```jsonc
126
+ {
127
+ "name": "extraction resolves a label to its form value",
128
+ "flow": { "...": "flow definition" },
129
+ "steps": [
130
+ { "extract": { "residency_status": "US citizen or permanent resident" } }
131
+ ],
132
+ "expect": {
133
+ "answers": { "residency_status": "us_person" },
134
+ "current_step": "income_types",
135
+ "suggestions": {}
136
+ }
137
+ }
138
+ ```
139
+
140
+ Each language ships a thin runner that loads a fixture, drives its own engine, and asserts the expected state. Adding a case means adding one JSON file; both languages are then obliged to agree or fail.
141
+
142
+ Seed it with the divergence table above — those are three cases that fail today. Then extend to transition resolution, `skip_if`, accumulators, and multi-select suggestion handling.
143
+
144
+ This is the deliverable that justifies the migration. Everything else is filing.
145
+
146
+ ## Sequencing
147
+
148
+ 1. Merge the open work first. Migrating on top of open pull requests strands them: `inquirex-widget` #6-#9, `inquirex` #5/#6, `inquirex-llm` #3, `inquirex-tools` #1/#2, `inquirex-tty` #10. Close or rebase the stale ones (`inquirex-tty` #6 is conflicting and failing; `inquirex` #5 and `inquirex-tools` #1 appear superseded).
149
+ 1. Publish 0.9.2 from the existing repositories, so a known-good release exists on both registries before anything moves.
150
+ 1. Create the monorepo and import packages in dependency order: `inquirex`, `inquirex-llm`, `inquirex-tty`, `inquirex-widget`, `inquirex-tools`.
151
+ 1. Wire up the top-level justfile and per-language CI. Prove every existing suite still passes unchanged.
152
+ 1. Build the conformance harness with the three known-divergent cases. Watch them fail.
153
+ 1. Reconcile the three implementations. Decide the canonical rules — label fallback yes or no, whitespace stripping yes or no — and make all three obey.
154
+ 1. Move to a single root `VERSION`, tighten the inter-package constraints, release 0.10.0 from the monorepo.
155
+ 1. Archive the old repositories with a pointer in each README.
156
+
157
+ Steps 1-2 are prerequisites. Steps 3-4 are reversible: if the imported tree is unsatisfactory, delete it — the original repositories are untouched until step 8.
158
+
159
+ ## Open decisions
160
+
161
+ - **`inquirex-webui`** — in or out. It is bound by the lockstep rule but is private and a different product. Leaving it out means the rule stays partly manual.
162
+ - **Visibility** — the monorepo must be public for the gems to remain credible open source. That is only tenable if nothing private is imported, which the scope above respects.
163
+ - **Canonical matching rules** — the reconciliation in step 6 is a semantics decision, not a mechanical one. Whitespace stripping is almost certainly correct everywhere. The label fallback is the real question: it is most needed in `inquirex-llm`, which is the one implementation lacking it.
@@ -18,11 +18,34 @@ module Inquirex
18
18
  # Never overwritten by generation, in any repo.
19
19
  PROTECTED = %w[INQUIREX_VERSION_LOG.md].freeze
20
20
 
21
+ # Reads a GitHub token from the `gh` session rather than a stored secret,
22
+ # so nothing here handles a credential in plain text.
23
+ DEFAULT_TOKEN_READER = -> { `gh auth token 2>/dev/null`.strip }
24
+
25
+ # Runs the generator in one repository, discarding its very chatty
26
+ # output, with this gem's bundle scrubbed — see
27
+ # {Inquirex::Tools.unbundled}. The generator is a gem of its own and must
28
+ # resolve against the installation that provides it, not against ours.
29
+ DEFAULT_RUNNER = lambda { |dir, env|
30
+ Inquirex::Tools.unbundled do
31
+ system(env, "github_changelog_generator", chdir: dir, out: File::NULL, err: File::NULL)
32
+ end
33
+ }
34
+
21
35
  # @param workspace [Workspace]
22
36
  # @param out [IO]
23
- def initialize(workspace: Workspace.new, out: $stdout)
37
+ # @param token_reader [#call] returns a GitHub token, or "" when there is
38
+ # no session. Injected so a spec never needs one.
39
+ # @param runner [#call] invoked with (dir, env) to run the generator.
40
+ # Injected so a spec never shells out to it.
41
+ def initialize(workspace: Workspace.new,
42
+ out: $stdout,
43
+ token_reader: DEFAULT_TOKEN_READER,
44
+ runner: DEFAULT_RUNNER)
24
45
  @workspace = workspace
25
46
  @out = out
47
+ @token_reader = token_reader
48
+ @runner = runner
26
49
  end
27
50
 
28
51
  # @return [Array<String>] every repo carrying a generator config
@@ -41,7 +64,7 @@ module Inquirex
41
64
  # @param names [Array<String>] repositories, or all configured when empty
42
65
  # @return [Boolean] true when every repository succeeded
43
66
  def generate(*names)
44
- token = `gh auth token 2>/dev/null`.strip
67
+ token = @token_reader.call.to_s.strip
45
68
  if token.empty?
46
69
  @out.puts "ERROR: `gh auth token` returned nothing — run `gh auth login`"
47
70
  return false
@@ -69,7 +92,7 @@ module Inquirex
69
92
 
70
93
  @out.print format(" %<name>-16s ", name: name)
71
94
  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)
95
+ if @runner.call(dir, env)
73
96
  @out.puts "ok (#{line_count(File.join(dir, "CHANGELOG.md"))} lines)"
74
97
  true
75
98
  else
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Inquirex
4
+ module Tools
5
+ module Commands
6
+ # `inquirex create-release` — tags and publishes a GitHub release for
7
+ # every package in the family.
8
+ #
9
+ # The orchestrator runs each package's own `just release` in that
10
+ # package's directory, so how a repository tags and writes its notes
11
+ # stays that repository's business. What this adds is the order, refusing
12
+ # to tag a dirty tree or a feature branch, and one command instead of six.
13
+ #
14
+ # Separate from `publish` on purpose. Publishing is irreversible and
15
+ # authenticated; releasing is neither, and the two fail for entirely
16
+ # different reasons — a release that cannot find `gh` should not leave a
17
+ # half-published family behind it.
18
+ class CreateRelease < Dry::CLI::Command
19
+ desc "Tag and create a GitHub release for every package in the family"
20
+
21
+ option :dry_run,
22
+ type: :boolean,
23
+ default: false,
24
+ desc: "Print what would run; tag nothing"
25
+
26
+ option :force,
27
+ type: :boolean,
28
+ default: false,
29
+ desc: "Release even from a dirty tree or a branch other than main"
30
+
31
+ example [
32
+ " # release every package at its current version",
33
+ "--dry-run # print what would happen, tag nothing",
34
+ "--force # skip the clean-tree and main-branch checks"
35
+ ]
36
+
37
+ # Safe to re-run: each package's recipe force-moves its tag and
38
+ # recreates its GitHub release, so a run that stopped halfway can be
39
+ # fixed and reissued rather than unpicked.
40
+ #
41
+ # @param options [Hash] parsed CLI options
42
+ # @return [void]
43
+ def call(**options)
44
+ ok = Inquirex::Tools::Releaser.new.call(
45
+ dry_run: options.fetch(:dry_run, false),
46
+ force: options.fetch(:force, false)
47
+ )
48
+ exit(1) unless ok
49
+ rescue Inquirex::Tools::Error => e
50
+ warn "ERROR: #{e.message}"
51
+ exit 1
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -17,15 +17,29 @@ module Inquirex
17
17
  default: false,
18
18
  desc: "Print the commands that would run; publish nothing"
19
19
 
20
+ option :force,
21
+ type: :boolean,
22
+ default: false,
23
+ desc: "Attempt every package, even ones already on their registry"
24
+
20
25
  example [
21
- " # publish the whole family",
22
- "--dry-run # print what would happen, touch no registry"
26
+ " # publish whatever is not out yet",
27
+ "--dry-run # print what would happen, touch no registry",
28
+ "--force # skip the already-published check"
23
29
  ]
24
30
 
31
+ # Idempotent by default: a package whose version is already on its
32
+ # registry is skipped, so re-running after a mid-release failure
33
+ # resumes rather than throwing rejections at RubyGems and npm.
34
+ #
25
35
  # @param options [Hash] parsed CLI options
26
36
  # @return [void]
27
37
  def call(**options)
28
- exit(1) unless Inquirex::Tools::Publisher.new.call(dry_run: options.fetch(:dry_run, false))
38
+ ok = Inquirex::Tools::Publisher.new.call(
39
+ dry_run: options.fetch(:dry_run, false),
40
+ force: options.fetch(:force, false)
41
+ )
42
+ exit(1) unless ok
29
43
  rescue Inquirex::Tools::Error => e
30
44
  warn "ERROR: #{e.message}"
31
45
  exit 1
@@ -6,6 +6,7 @@ require_relative "commands/version"
6
6
  require_relative "commands/versions_bump"
7
7
  require_relative "commands/versions_check"
8
8
  require_relative "commands/publish"
9
+ require_relative "commands/create_release"
9
10
  require_relative "commands/changelogs"
10
11
 
11
12
  module Inquirex
@@ -21,6 +22,9 @@ module Inquirex
21
22
  register "versions-check", VersionsCheck
22
23
  register "versions-bump", VersionsBump
23
24
  register "publish", Publish
25
+ # `publish-all` reads naturally next to the justfile recipe of that name.
26
+ register "publish-all", Publish
27
+ register "create-release", CreateRelease
24
28
  register "changelogs", Changelogs
25
29
  register "version", Version
26
30
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "workspace"
4
+
5
+ module Inquirex
6
+ module Tools
7
+ # Raises the "newest published gem" pin that a package's CI installs from
8
+ # rubygems.org.
9
+ #
10
+ # `inquirex-webui` reparses its printer's output with the real gem, which
11
+ # CI installs from the registry rather than from a checkout. That pin
12
+ # therefore cannot name the version in the source tree — it does not exist
13
+ # yet — so it trails by one release and has to be raised by hand afterwards.
14
+ #
15
+ # Nobody remembers. The pin sat at 0.9.4 while the printer started emitting
16
+ # `min`, `max` and `step_size`, added to the gem in 0.9.5, and the
17
+ # round-trip failed with "`min` is not allowed inside a step block":
18
+ # SafeSource is default-deny, so a step setter it has never heard of is
19
+ # rejected outright. The failure names the printer, not the pin.
20
+ #
21
+ # Running immediately after a successful publish is what makes this safe.
22
+ # At that moment the version is on the registry by construction, so the
23
+ # pin's one rule — never run ahead of rubygems.org — cannot be violated.
24
+ #
25
+ # @example Raise the pin to the version just published
26
+ # Inquirex::Tools::PinUpdater.new.call("0.9.6") # => ["inquirex-webui/.github/workflows/ci.yml"]
27
+ class PinUpdater
28
+ # A pinned family version in a workflow's `env:` block. Anchored to the
29
+ # whole line so a value mentioned in a comment is left alone.
30
+ PIN = /^(?<lead>\s*INQUIREX_GEM_VERSION:\s*)(?<quote>["']?)\d+\.\d+\.\d+\k<quote>\s*$/
31
+
32
+ # @param workspace [Workspace] the ecosystem checkout to rewrite
33
+ # @param out [IO] where report output goes
34
+ def initialize(workspace: Workspace.new, out: $stdout)
35
+ @workspace = workspace
36
+ @out = out
37
+ end
38
+
39
+ # Rewrites every pin to `version`.
40
+ #
41
+ # @param version [String] the version that was just published
42
+ # @param dry_run [Boolean] report what would change and write nothing
43
+ # @return [Array<String>] workspace-relative paths that changed
44
+ def call(version, dry_run: false)
45
+ changed = workflows.select { |path| rewrite(path, version, dry_run:) }
46
+ report(changed, version, dry_run:)
47
+ changed.map { |path| path.delete_prefix("#{workspace.root}/") }
48
+ end
49
+
50
+ private
51
+
52
+ # @return [Workspace] the ecosystem checkout being rewritten
53
+ attr_reader :workspace
54
+
55
+ # @return [IO] where report output goes
56
+ attr_reader :out
57
+
58
+ # @return [Array<String>] every workflow file in every lockstep package
59
+ def workflows
60
+ Workspace::LOCKSTEP.keys.flat_map do |name|
61
+ Dir.glob(File.join(workspace.root, name, ".github", "workflows", "*.y{a,}ml"))
62
+ end.sort
63
+ end
64
+
65
+ # @return [Boolean] whether the file needed rewriting
66
+ def rewrite(path, version, dry_run:)
67
+ source = File.read(path)
68
+ updated = source.gsub(PIN) { "#{::Regexp.last_match(:lead)}\"#{version}\"" }
69
+ return false if updated == source
70
+
71
+ File.write(path, updated) unless dry_run
72
+ true
73
+ end
74
+
75
+ # @return [void]
76
+ def report(changed, version, dry_run:)
77
+ return if changed.empty?
78
+
79
+ verb = dry_run ? "Would raise" : "Raised"
80
+ out.puts
81
+ out.puts "#{verb} the published-gem pin to #{version}:"
82
+ changed.each { |path| out.puts " #{path.delete_prefix("#{workspace.root}/")}" }
83
+ # The publisher does not commit — it has no business deciding what a
84
+ # release commit looks like — so say plainly that this is not done yet.
85
+ out.puts "Commit that; CI installs the pinned version from the registry.".dark unless dry_run
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "colored2"
4
+ require "net/http"
5
+ require "uri"
4
6
  require_relative "workspace"
5
7
 
6
8
  module Inquirex
@@ -38,9 +40,11 @@ module Inquirex
38
40
  ROTATE_INTERVAL = 2
39
41
 
40
42
  # Shells out with the child's stdout and stderr left attached, so a
41
- # failing package's own output is what the operator sees.
43
+ # failing package's own output is what the operator sees, and with this
44
+ # gem's bundle scrubbed so the recipe resolves its own — see
45
+ # {Inquirex::Tools.unbundled}.
42
46
  DEFAULT_RUNNER = lambda { |dir, argv|
43
- Dir.chdir(dir) { system("just", *argv) }
47
+ Inquirex::Tools.unbundled { Dir.chdir(dir) { system("just", *argv) } }
44
48
  }
45
49
 
46
50
  # Reads the current RubyGems TOTP. `op` resolves the account from
@@ -53,39 +57,92 @@ module Inquirex
53
57
  code.empty? ? nil : code
54
58
  }
55
59
 
60
+ # Asks the registry whether a version is already public.
61
+ #
62
+ # Returns nil rather than false when the answer cannot be obtained — no
63
+ # network, a 500, a timeout. The caller treats nil as "attempt it": the
64
+ # registry itself refuses a duplicate, so guessing "not published" and
65
+ # trying is safe, whereas guessing "published" would silently skip a
66
+ # package that never shipped.
67
+ DEFAULT_PUBLISHED_CHECKER = lambda { |name, kind, version|
68
+ url = case kind
69
+ when :gem then "https://rubygems.org/api/v2/rubygems/#{name}/versions/#{version}.json"
70
+ when :npm then "https://registry.npmjs.org/#{name}/#{version}"
71
+ end
72
+ begin
73
+ uri = URI.parse(url)
74
+ response = Net::HTTP.start(uri.host,
75
+ uri.port,
76
+ use_ssl: true,
77
+ open_timeout: 5,
78
+ read_timeout: 5) do |http|
79
+ http.head(uri.request_uri)
80
+ end
81
+ case response
82
+ when Net::HTTPSuccess then true
83
+ when Net::HTTPNotFound then false
84
+ end
85
+ rescue StandardError
86
+ nil
87
+ end
88
+ }
89
+
56
90
  # @param workspace [Workspace] the ecosystem checkout to publish from
57
91
  # @param out [IO] where report output goes
58
92
  # @param runner [#call] invoked with (dir, argv) to run a command; the
59
93
  # default shells out. Injected so specs can assert what would run
60
94
  # without publishing anything.
61
95
  # @param otp_reader [#call] returns the current 2FA code, or nil
96
+ # @param published_checker [#call] (name, kind, version) => true/false/nil
97
+ # @param pin_updater [#call] (version, dry_run:) raises the published-gem
98
+ # pin once the version is certainly on the registry
62
99
  def initialize(workspace: Workspace.new,
63
100
  out: $stdout,
64
101
  runner: DEFAULT_RUNNER,
65
- otp_reader: DEFAULT_OTP_READER)
102
+ otp_reader: DEFAULT_OTP_READER,
103
+ published_checker: DEFAULT_PUBLISHED_CHECKER,
104
+ pin_updater: nil)
66
105
  @workspace = workspace
67
106
  @out = out
68
107
  @runner = runner
69
108
  @otp_reader = otp_reader
109
+ @published_checker = published_checker
110
+ @pin_updater = pin_updater || PinUpdater.new(workspace:, out:)
70
111
  end
71
112
 
72
113
  # Publishes every lockstep package, or prints what it would publish.
73
114
  #
115
+ # Idempotent: a package whose current version is already on its registry
116
+ # is skipped, so re-running after a mid-release failure picks up exactly
117
+ # where it stopped instead of throwing six rejections at two registries.
118
+ #
74
119
  # @param dry_run [Boolean] print the commands and run nothing
75
120
  # @param args [Array<String>] extra arguments forwarded verbatim to each
76
121
  # package's `just publish`
77
- # @return [Boolean] true when every package published (or was printed)
78
- def call(dry_run: false, args: [])
122
+ # @param force [Boolean] attempt every package even when it looks published
123
+ # @return [Boolean] true when every package published, skipped or printed
124
+ def call(dry_run: false, args: [], force: false)
79
125
  out.puts(dry_run ? "DRY RUN — these commands would run, in this order:" : "Publishing the family:")
80
126
 
81
127
  last_otp = nil
128
+ published = 0
129
+ skipped = 0
130
+
82
131
  Workspace::LOCKSTEP.each do |name, kind|
83
132
  dir = File.join(workspace.root, name)
84
133
  raise Error, "no checkout for #{name} at #{dir}" unless Dir.exist?(dir)
85
134
 
135
+ version = workspace.version_of(name)
136
+ if !force && version && published_checker.call(name, kind, version)
137
+ row(name, kind, "already #{version} on #{registry(kind)} — skipping".dark)
138
+ skipped += 1
139
+ next
140
+ end
141
+
86
142
  argv = ["publish", *args]
87
143
  if dry_run
88
- row(name, kind, "just #{argv.join(" ")}")
144
+ row(name, kind, "just #{argv.join(" ")}#{" (#{version})" if version}")
145
+ published += 1
89
146
  next
90
147
  end
91
148
 
@@ -98,10 +155,15 @@ module Inquirex
98
155
  out.puts
99
156
  out.puts "━━ #{name} (#{kind}) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
100
157
  return false unless run(name, dir, argv)
158
+
159
+ published += 1
101
160
  end
102
161
 
103
162
  out.puts
104
- out.puts(dry_run ? "Nothing was published." : "Published every package.".green)
163
+ out.puts summary(dry_run:, published:, skipped:)
164
+ # Only now is the version certainly on the registry, which is the one
165
+ # rule the pin has to obey.
166
+ pin_updater.call(workspace.version_of("inquirex"), dry_run:)
105
167
  true
106
168
  end
107
169
 
@@ -119,6 +181,12 @@ module Inquirex
119
181
  # @return [#call] source of 2FA codes
120
182
  attr_reader :otp_reader
121
183
 
184
+ # @return [#call] asks a registry whether a version is already public
185
+ attr_reader :published_checker
186
+
187
+ # @return [#call] raises the published-gem pin after a release
188
+ attr_reader :pin_updater
189
+
122
190
  # @return [Boolean] whether the package published cleanly
123
191
  def run(name, dir, argv)
124
192
  return true if runner.call(dir, argv)
@@ -155,6 +223,19 @@ module Inquirex
155
223
  def row(name, kind, detail)
156
224
  out.puts " #{name.ljust(18).yellow} #{kind.to_s.ljust(4).cyan} #{detail}"
157
225
  end
226
+
227
+ # @return [String] human-readable registry name for a package kind
228
+ def registry(kind) = kind == :gem ? "RubyGems" : "npm"
229
+
230
+ # @return [String] the closing line, which has to distinguish "nothing to
231
+ # do because everything is already out" from "nothing happened"
232
+ def summary(dry_run:, published:, skipped:)
233
+ return "Everything is already published — nothing to do.".green if published.zero? && skipped.positive?
234
+
235
+ counted = "#{published} package#{"s" unless published == 1}"
236
+ note = skipped.positive? ? ", #{skipped} already published" : ""
237
+ dry_run ? "Would publish #{counted}#{note}. Nothing was published." : "Published #{counted}#{note}.".green
238
+ end
158
239
  end
159
240
  end
160
241
  end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "colored2"
4
+ require_relative "workspace"
5
+
6
+ module Inquirex
7
+ module Tools
8
+ # Cuts a GitHub release for every package in the family by running each
9
+ # one's own `just release` in its own directory.
10
+ #
11
+ # The counterpart to publishing: that puts packages on RubyGems and npm,
12
+ # this puts a `vX.Y.Z` tag and a GitHub release against the commit that
13
+ # shipped. Every package already owns a `release` recipe — it tags,
14
+ # force-pushes the tag, and recreates the GitHub release with generated
15
+ # notes — so how a repository releases stays that repository's business.
16
+ # What this adds is the order, the guards, and one command instead of six.
17
+ #
18
+ # Unlike publishing, a release is **reversible**: a tag can be moved and a
19
+ # GitHub release deleted, which is exactly what each recipe does on every
20
+ # run. That is why this is safe to re-run, and why a mid-run failure can be
21
+ # fixed and the command simply issued again.
22
+ #
23
+ # The guards exist because the recipe tags `HEAD`. Releasing from a dirty
24
+ # tree tags a commit that does not match what was tested; releasing from a
25
+ # feature branch tags work that was never merged. Both produce a `v0.9.5`
26
+ # pointing somewhere nobody can reproduce, and neither is fixed by
27
+ # re-running — so they are refused rather than warned about.
28
+ #
29
+ # @example Preview a release without tagging anything
30
+ # Inquirex::Tools::Releaser.new.call(dry_run: true)
31
+ class Releaser
32
+ # The only branch a release may be cut from. A tag on anything else
33
+ # points at work that never went through review.
34
+ RELEASE_BRANCH = "main"
35
+
36
+ # Shells out with the child's stdout and stderr left attached, so a
37
+ # failing package's own `gh` output is what the operator sees, and with
38
+ # this gem's bundle scrubbed so the recipe resolves its own — see
39
+ # {Inquirex::Tools.unbundled}.
40
+ DEFAULT_RUNNER = lambda { |dir, argv|
41
+ Inquirex::Tools.unbundled { Dir.chdir(dir) { system("just", *argv) } }
42
+ }
43
+
44
+ # @param workspace [Workspace] the ecosystem checkout to release from
45
+ # @param out [IO] where report output goes
46
+ # @param runner [#call] invoked with (dir, argv) to run a command; the
47
+ # default shells out. Injected so specs can assert what would run
48
+ # without creating a tag or a GitHub release.
49
+ def initialize(workspace: Workspace.new, out: $stdout, runner: DEFAULT_RUNNER)
50
+ @workspace = workspace
51
+ @out = out
52
+ @runner = runner
53
+ end
54
+
55
+ # Creates a GitHub release for every lockstep package.
56
+ #
57
+ # Stops at the first package that fails or is refused, and says so. The
58
+ # packages after it are left alone rather than half-tagged.
59
+ #
60
+ # @param dry_run [Boolean] print what would run; tag nothing
61
+ # @param force [Boolean] release despite a dirty tree or a non-main branch
62
+ # @param args [Array<String>] extra arguments forwarded to `just release`
63
+ # @return [Boolean] true when every package released or was printed
64
+ def call(dry_run: false, force: false, args: [])
65
+ out.puts(dry_run ? "DRY RUN — these releases would be created:" : "Creating GitHub releases for the family:")
66
+
67
+ released = 0
68
+
69
+ Workspace::LOCKSTEP.each_key do |name|
70
+ dir = File.join(workspace.root, name)
71
+ raise Error, "no checkout for #{name} at #{dir}" unless Dir.exist?(dir)
72
+
73
+ version = workspace.version_of(name)
74
+
75
+ blocker = force ? nil : blocker_for(name)
76
+ if blocker
77
+ row(name, version, blocker.red)
78
+ return refused(name)
79
+ end
80
+
81
+ argv = ["release", *args]
82
+ if dry_run
83
+ row(name, version, "just #{argv.join(" ")}")
84
+ released += 1
85
+ next
86
+ end
87
+
88
+ out.puts
89
+ out.puts "━━ #{name} v#{version} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
90
+ return false unless run(name, dir, argv)
91
+
92
+ released += 1
93
+ end
94
+
95
+ out.puts
96
+ out.puts summary(dry_run:, released:)
97
+ true
98
+ end
99
+
100
+ private
101
+
102
+ # @return [Workspace] the ecosystem checkout being released
103
+ attr_reader :workspace
104
+
105
+ # @return [IO] where report output goes
106
+ attr_reader :out
107
+
108
+ # @return [#call] command runner
109
+ attr_reader :runner
110
+
111
+ # Why this package must not be released, or nil when it is fine.
112
+ #
113
+ # A `clean` of nil means the directory is not a git checkout at all. That
114
+ # is left for the recipe to fail on rather than pre-judged here: a state
115
+ # that cannot be read should never silently skip a package, because
116
+ # "unknown" and "not releasable" are different answers.
117
+ #
118
+ # @param name [String] package name
119
+ # @return [String, nil] human-readable blocker
120
+ def blocker_for(name)
121
+ state = workspace.git_state(name)
122
+ return "working tree is dirty — the tag would not match what was tested" if state[:clean] == false
123
+
124
+ branch = state[:branch]
125
+ return nil if branch.nil? || branch.empty? || branch == RELEASE_BRANCH
126
+
127
+ "on #{branch}, not #{RELEASE_BRANCH} — the tag would point at unmerged work"
128
+ end
129
+
130
+ # @param name [String] the package that was refused
131
+ # @return [Boolean] always false, so the caller can `return refused(name)`
132
+ def refused(name)
133
+ out.puts
134
+ out.puts "#{"ERROR".bold.red}: #{name} is not releasable — stopping here."
135
+ out.puts "Nothing was tagged. Fix the checkout, or re-run with --force to override."
136
+ false
137
+ end
138
+
139
+ # @return [Boolean] whether the package released cleanly
140
+ def run(name, dir, argv)
141
+ return true if runner.call(dir, argv)
142
+
143
+ out.puts
144
+ out.puts "#{"ERROR".bold.red}: #{name} failed `just #{argv.first}` — stopping here."
145
+ out.puts "A release is re-runnable, so fix the cause and issue the command again."
146
+ false
147
+ end
148
+
149
+ # Pads before colorizing: an ANSI escape is several characters wide to
150
+ # `String#ljust` and zero columns wide on screen, so colorizing first left
151
+ # the table ragged by exactly the length of the escape codes.
152
+ #
153
+ # @return [void]
154
+ def row(name, version, detail)
155
+ out.puts " #{name.ljust(18).yellow} #{(version || "?").ljust(8).cyan} #{detail}"
156
+ end
157
+
158
+ # @return [String] the closing line
159
+ def summary(dry_run:, released:)
160
+ counted = "#{released} release#{"s" unless released == 1}"
161
+ dry_run ? "Would create #{counted}. Nothing was tagged." : "Created #{counted}.".green
162
+ end
163
+ end
164
+ end
165
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Inquirex
4
4
  module Tools
5
- VERSION = "0.9.4"
5
+ VERSION = "0.10.0"
6
6
  end
7
7
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "colored2"
4
+ require "open3"
4
5
  require "rubygems/version"
5
6
  require_relative "workspace"
6
7
 
@@ -37,11 +38,47 @@ module Inquirex
37
38
  # Rule across the report, sized to the rows.
38
39
  RULE = ("—" * 52).freeze
39
40
 
41
+ # Bringing a checkout level with origin/main before its suite runs, so a
42
+ # bump is verified against what is actually on main rather than whatever
43
+ # the local clone last saw.
44
+ SYNC_STEPS = [
45
+ %w[fetch --tags origin],
46
+ %w[rebase origin/main]
47
+ ].freeze
48
+
49
+ # Runs one git command, capturing stdout *and* stderr.
50
+ #
51
+ # Capturing is the point. Left attached, a `git` run against a directory
52
+ # that is not a checkout writes "fatal: not a git repository" straight to
53
+ # the terminal — thirty such lines during the specs, which is noise the
54
+ # operator has to learn to ignore. The caller decides what is worth
55
+ # printing.
56
+ #
57
+ # @return [Array(Boolean, String)] success, and the combined output
58
+ DEFAULT_GIT = lambda { |dir, argv|
59
+ output, status = Open3.capture2e("git", "-C", dir, *argv)
60
+ [status.success?, output]
61
+ }
62
+
63
+ # Runs a package's own checks in its own directory, with this gem's
64
+ # bundle scrubbed so those checks resolve the package's own Gemfile
65
+ # rather than inheriting ours — see {Inquirex::Tools.unbundled}.
66
+ DEFAULT_RUNNER = lambda { |dir, argv|
67
+ Inquirex::Tools.unbundled { Dir.chdir(dir) { system(*argv) } }
68
+ }
69
+
40
70
  # @param workspace [Workspace] the ecosystem checkout to rewrite
41
71
  # @param out [IO] where report output goes
42
- def initialize(workspace: Workspace.new, out: $stdout)
72
+ # @param git [#call] invoked with (dir, argv); returns [ok, output].
73
+ # Injected so specs can assert what would run without touching a repo.
74
+ # @param runner [#call] invoked with (dir, argv) to run a package's
75
+ # checks. Injected for the same reason: a unit test must not shell out
76
+ # to six real suites.
77
+ def initialize(workspace: Workspace.new, out: $stdout, git: DEFAULT_GIT, runner: DEFAULT_RUNNER)
43
78
  @workspace = workspace
44
79
  @out = out
80
+ @git = git
81
+ @runner = runner
45
82
  end
46
83
 
47
84
  # Rewrites every lockstep package to the target version, then runs each
@@ -74,6 +111,12 @@ module Inquirex
74
111
  # @return [IO] where report output goes
75
112
  attr_reader :out
76
113
 
114
+ # @return [#call] runs one git command, returning [ok, output]
115
+ attr_reader :git
116
+
117
+ # @return [#call] runs a package's checks, returning truthy on success
118
+ attr_reader :runner
119
+
77
120
  # The one place the package list is walked, so adding a package changes
78
121
  # nothing here.
79
122
  #
@@ -100,25 +143,47 @@ module Inquirex
100
143
  # @return [Boolean, nil] pass/fail, or nil when skipped
101
144
  def check(name)
102
145
  dir = File.join(workspace.root, name)
103
- passed = Dir.chdir(dir) do
104
- row(name, "running #{CHECK.join(" ")} in ".yellow, dir)
105
- out.puts RULE
106
- system("git fetch")
107
- system("test -n $(git status --porcelain) && { git stash -u; echo 'Stashing locally modified files...'; }")
108
- system("git pull origin main")
109
- system("git fetch --tags")
110
- system("git rebase origin/main")
111
- unless File.exist?(File.join(dir, "justfile"))
112
- row(name, "skip".yellow, "no justfile".yellow)
113
- return nil
114
- end
115
- row(name, "Pulled in #{name}, current version is #{system("just version")}", dir)
116
- system(*CHECK)
146
+ unless File.exist?(File.join(dir, "justfile"))
147
+ row(name, "skip".yellow, "no justfile".yellow)
148
+ return nil
117
149
  end
118
- row(name, passed ? "[ OK ]".on.green : "[ 🅧 ]".on_red, dir)
150
+
151
+ sync(name, dir)
152
+ passed = runner.call(dir, CHECK)
153
+ row(name, passed ? "ok".green : "FAIL".red, dir)
119
154
  passed
120
155
  end
121
156
 
157
+ # Brings one package level with origin/main before its suite runs.
158
+ #
159
+ # Two states are reported and left alone rather than acted on:
160
+ #
161
+ # - **not a git checkout** — nothing to sync, and running git here is what
162
+ # printed "fatal: not a git repository" once per command per package.
163
+ # - **uncommitted changes** — the tree is *not* stashed. A version bump
164
+ # that silently runs `git stash -u` can bury work the operator never
165
+ # agreed to move, and the stash stack is shared across worktrees, so it
166
+ # is not even reliably theirs to pop back. Rebasing on top of local
167
+ # edits is equally unwelcome. Skipping is the only non-destructive
168
+ # answer; the checks still run against what is on disk.
169
+ #
170
+ # @param name [String] package name
171
+ # @param dir [String] absolute path to the package
172
+ # @return [void]
173
+ def sync(name, dir)
174
+ state = workspace.git_state(name)
175
+ return row(name, "sync".yellow, "not a git checkout".yellow) if state[:clean].nil?
176
+ return row(name, "sync".yellow, "uncommitted changes — left alone".yellow) unless state[:clean]
177
+
178
+ SYNC_STEPS.each do |argv|
179
+ ok, output = git.call(dir, argv)
180
+ next if ok
181
+
182
+ row(name, "sync".red, "git #{argv.first} failed: #{output.to_s.lines.first&.strip}".red)
183
+ break
184
+ end
185
+ end
186
+
122
187
  # The single formatting site for every line of the report.
123
188
  #
124
189
  # `format`, never `printf`: printf writes straight to $stdout and returns
@@ -42,7 +42,10 @@ module Inquirex
42
42
 
43
43
  Workspace::LOCKSTEP.each_key do |name|
44
44
  problems = readiness_problems(name, target)
45
- out.puts format(" %<name>-16s %<state>s", name: name, state: problems.empty? ? "ready".bright_green : problems.join(", ").red)
45
+ # `.green`, not `.bright_green`: colored2 defines no such method, so
46
+ # the ready branch raised NoMethodError — and only the ready branch,
47
+ # which is why a preflight that found problems looked fine.
48
+ out.puts format(" %<name>-16s %<state>s", name: name, state: problems.empty? ? "ready".green : problems.join(", ").red)
46
49
  ok &&= problems.empty?
47
50
  end
48
51
 
@@ -4,7 +4,9 @@ require_relative "tools/version"
4
4
  require_relative "tools/workspace"
5
5
  require_relative "tools/version_checker"
6
6
  require_relative "tools/version_bumper"
7
+ require_relative "tools/pin_updater"
7
8
  require_relative "tools/publisher"
9
+ require_relative "tools/releaser"
8
10
  require_relative "tools/changelogs"
9
11
 
10
12
  module Inquirex
@@ -24,6 +26,7 @@ module Inquirex
24
26
  # - `versions-check` — version parity across the lockstep packages
25
27
  # - `versions-bump` — move every lockstep package to one version
26
28
  # - `publish` — publish every package to RubyGems and npm
29
+ # - `create-release` — tag and create a GitHub release for every package
27
30
  # - `changelogs` — regenerate CHANGELOG.md from merged PRs
28
31
  # - `version` — this gem's own version
29
32
  #
@@ -31,5 +34,34 @@ module Inquirex
31
34
  # Inquirex::Tools::VersionChecker.new.preflight # => true when every package is ready
32
35
  module Tools
33
36
  class Error < StandardError; end
37
+
38
+ # Runs a block with Bundler's environment removed.
39
+ #
40
+ # Every orchestrator here shells into a *different* package and runs
41
+ # `bundle exec` there. `bundle exec inquirex versions-bump` exports
42
+ # BUNDLE_GEMFILE, RUBYOPT and friends, and a child process inherits them —
43
+ # so without this, each sibling resolves against **this gem's** Gemfile
44
+ # instead of its own.
45
+ #
46
+ # That failure is loud but misleading. `versions-bump` reported five of six
47
+ # packages failing `just check-all`: `inquirex` and `inquirex-llm` raised
48
+ # `NoMethodError: undefined method 'skip' for module SimpleCov`, because
49
+ # this gem pins simplecov ~> 0.22 while theirs resolve 1.0.3 where
50
+ # `SimpleCov.skip` exists; `inquirex-tty` raised `LoadError: cannot load
51
+ # such file -- inquirex`, because this gem deliberately does not depend on
52
+ # the gems it manages. Every one of those reads as a defect in the package
53
+ # being checked. None of them was.
54
+ #
55
+ # Bundler is not declared as a dependency — it is present whenever this
56
+ # runs under `bundle exec`, and when it is absent there is no environment
57
+ # to scrub, so the block simply runs.
58
+ #
59
+ # @yield with a clean environment
60
+ # @return [Object] whatever the block returns
61
+ def self.unbundled(&)
62
+ return yield unless defined?(::Bundler) && ::Bundler.respond_to?(:with_unbundled_env)
63
+
64
+ ::Bundler.with_unbundled_env(&)
65
+ end
34
66
  end
35
67
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inquirex-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.4
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Gredeskoul
@@ -50,7 +50,6 @@ executables:
50
50
  extensions: []
51
51
  extra_rdoc_files: []
52
52
  files:
53
- - ".DS_Store"
54
53
  - ".env.encrypted"
55
54
  - ".env.example"
56
55
  - ".envrc"
@@ -63,6 +62,9 @@ files:
63
62
  - LICENSE.txt
64
63
  - README.md
65
64
  - Rakefile
65
+ - docs/badges/coverage_badge.svg
66
+ - docs/plans/001-⚪️-many-repo-setup/spec.md
67
+ - docs/plans/002-⚪️-monorepo-migration/spec.md
66
68
  - exe/inquirex
67
69
  - exe/inquirexor
68
70
  - justfile
@@ -70,11 +72,14 @@ files:
70
72
  - lib/inquirex/tools/changelogs.rb
71
73
  - lib/inquirex/tools/commands.rb
72
74
  - lib/inquirex/tools/commands/changelogs.rb
75
+ - lib/inquirex/tools/commands/create_release.rb
73
76
  - lib/inquirex/tools/commands/publish.rb
74
77
  - lib/inquirex/tools/commands/version.rb
75
78
  - lib/inquirex/tools/commands/versions_bump.rb
76
79
  - lib/inquirex/tools/commands/versions_check.rb
80
+ - lib/inquirex/tools/pin_updater.rb
77
81
  - lib/inquirex/tools/publisher.rb
82
+ - lib/inquirex/tools/releaser.rb
78
83
  - lib/inquirex/tools/version.rb
79
84
  - lib/inquirex/tools/version_bumper.rb
80
85
  - lib/inquirex/tools/version_checker.rb
@@ -102,7 +107,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
102
107
  - !ruby/object:Gem::Version
103
108
  version: '0'
104
109
  requirements: []
105
- rubygems_version: 4.0.17
110
+ rubygems_version: 4.0.18
106
111
  specification_version: 4
107
112
  summary: Release tooling for the Inquirex package family
108
113
  test_files: []
data/.DS_Store DELETED
Binary file