feat(install-dynamic-plugins): cli-module variant (alternative to #3246)#3254
feat(install-dynamic-plugins): cli-module variant (alternative to #3246)#3254gustavolira wants to merge 7 commits into
Conversation
Migrates scripts/install-dynamic-plugins/ from redhat-developer/rhdh#4574 into this repo as @red-hat-developer-hub/install-dynamic-plugins so it can be published to npm and consumed by the RHDH init-container without curl-by-SHA. Runtime contract (CLI args, env vars, plugin-hash format, on-disk layout, tar/OCI security guards) preserved verbatim. Build remains a single self-contained .cjs via esbuild. Tests migrated from vitest to jest to align with the repo's backstage-cli pipeline (14 suites / 166 tests pass). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add bin/install-dynamic-plugins shim and have package.json bin point at it (matches the convention used by extensions-cli, translations-cli, and rhdh-repo-tools). Split src/cli.ts as the esbuild entry so the bundle no longer needs the require.main guard or a shebang banner. - Stop committing dist/install-dynamic-plugins.cjs; the release pipeline rebuilds via the customBuild path, and a new prepack script makes yarn npm publish self-healing for local runs. - Drop the .js suffix from relative imports across src/ so the package matches the rest of the repo and the jest moduleNameMapper workaround is no longer needed. - Consolidate the tsconfigs: the inner package extends the workspace tsconfig and only declares what differs. - Add why-it's-intentional comments to the two eslint-disable lines (PullPolicy const+type pair, tar.x filter inside a sequential loop). - README now leads with the npm/npx usage path; the RHDH init-container section is below. tar/yaml stay in dependencies (not devDependencies as the review suggested) — @backstage/no-undeclared-imports flagged the source imports, and the repo convention treats bundling as opaque. 166/166 tests pass, tsc/lint/prettier clean, bin shim and bundle both exit 0 on the empty-config smoke run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The CI step "check api reports and generate API reference" runs
`backstage-repo-tools api-reports --ci` before the build, and that tool
requires the bin file to introspect the CLI. The previous shim did a
plain `require('../dist/install-dynamic-plugins.cjs')`, which failed
under CI because dist/ is no longer committed and the build hasn't run
yet.
- Switch the bin shim to the local-vs-installed pattern used by every
other CLI in the repo (extensions-cli, translations-cli,
rhdh-repo-tools): when `src/` exists (monorepo), load TS directly via
`@backstage/cli/config/nodeTransform`; otherwise require the built
bundle (npm-installed scenario).
- Add `--help` / `-h` handling to main() so the api-reports tool can
introspect the CLI usage without creating a stray `--help/` directory.
- Commit the generated `cli-report.md`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Use String.raw for strings containing backslash literals so the source
reads with one '\' instead of '\\\\' (catalog-index.ts log message,
extra-catalog-index.test.ts subdirectory fixtures, skopeo.test.ts shell
escape).
- Switch the OCI regex builder to a joined string array — eliminates the
nested template literals SonarCloud was flagging on oci-key.ts (and
reads much better).
- Object.prototype.hasOwnProperty.call -> Object.hasOwn in
merger.test.ts (ES2022, available since Node 16.9).
- String#replace(/'/g, ...) -> String#replaceAll("'", ...) in
skopeo.test.ts (ES2021).
- Hoist test helpers (stageLayer, fakeImageCache) out of their describe
blocks so they aren't re-defined on every test.
- Drop the redundant parseMaxEntrySize(undefined) call in types.test.ts —
the parameter already defaults to process.env.MAX_ENTRY_SIZE.
166/166 tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switches the hand-rolled `process.argv` + USAGE-string handling in main() to `cleye` — the same parser every `@backstage/cli-module-*` package uses (already in our transitive deps). Aligns with the Backstage CLI convention requested during PR review. Existing surface preserved: - positional `<dynamic-plugins-root>` (required, exit 1 if absent) - `--help` / `-h` prints usage and exits 0 - normal run still exits with the installer's status code Bundle grew from 226 kB -> 267 kB (cleye + type-flag minified). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Alternative to redhat-developer#3246 — packages the installer as a Backstage CLI module per review feedback, so it integrates natively with backstage-cli. - Rename to @red-hat-developer-hub/cli-module-install-dynamic-plugins, backstage.role: cli-module. - src/module.ts exports createCliModule registering an `install` command; src/command.ts loads it; src/cli.ts (esbuild entry) runs it via runCliModule. main() now takes explicit argv from the command context. - @backstage/cli-node added as a dependency; cleye is still the argv parser inside the command (same as @backstage/cli-module-lint). Tradeoffs vs redhat-developer#3246: - CLI surface gains an `install` subcommand: `install-dynamic-plugins install <root>` (wrapper + README updated). - Bundle grows 267 KB -> 1.45 MB (~5.4x) from inlining cli-node + deps. - @backstage/cli-node pulls in keytar (native .node). esbuild can't bundle native binaries, so keytar is aliased to no-ops at build time (esbuild-keytar-stub.cjs). Safe today since install never touches credentials, but it's load-bearing on cli-node not lazy-loading keytar elsewhere. 166/166 tests pass; tsc, lint, prettier, api-reports clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Review by Qodo
1. Arch forced to amd64
|
|
This pull request adds a new top-level directory under |
|
Important This PR includes changes that affect public-facing API. Please ensure you are adding/updating documentation for new features or behavior. Changed Packages
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3254 +/- ##
==========================================
+ Coverage 53.23% 53.37% +0.13%
==========================================
Files 2413 2436 +23
Lines 86358 87399 +1041
Branches 23897 24154 +257
==========================================
+ Hits 45975 46647 +672
- Misses 40049 40418 +369
Partials 334 334
*This pull request uses carry forward flags. Click here to find out more. Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Review Summary by QodoImplement install-dynamic-plugins as Backstage CLI module with OCI/NPM plugin support and security hardening
WalkthroughsDescription• Implements @red-hat-developer-hub/cli-module-install-dynamic-plugins as a Backstage CLI module (backstage.role: cli-module) for native integration with backstage-cli discovery • Restructures the installer from a standalone CLI to use @backstage/cli-node's createCliModule pattern, registering an install subcommand • Core installation orchestration: loads and merges dynamic plugin configurations from YAML, categorizes plugins (OCI, NPM, skipped), and installs concurrently with worker pooling • Security hardening: implements path-traversal validation, zip-bomb detection, symlink containment, prototype-pollution protection, and SRI integrity verification for NPM packages • OCI plugin support: extracts plugins from OCI images with hash-based change detection, implements pull policies (IfNotPresent, Always), and registry fallback logic (registry.access.redhat.com → quay.io) • NPM plugin support: orchestrates npm pack with integrity verification and secure tarball extraction • Concurrency control: implements Semaphore class and mapConcurrent() for bounded parallel execution with adaptive worker counts • Exclusive locking: prevents concurrent installs via lock file with polling and timeout (default 10 min) • Comprehensive test coverage: 166 tests validating security checks, configuration merging, OCI/NPM parsing, hash computation, and error handling • Trade-off vs alternative #3246: ~5.4x larger bundle (1.45 MB vs 267 KB) due to @backstage/cli-node dependency, adds keytar native dependency (stubbed at build time), but gains native CLI discovery and follows Backstage CLI module convention Diagramflowchart LR
A["CLI Entry<br/>cli.ts"] -->|runCliModule| B["Module Registration<br/>module.ts"]
B -->|createCliModule| C["Command Handler<br/>command.ts"]
C -->|main argv| D["Config Loading<br/>index.ts"]
D -->|merge configs| E["Plugin Merger<br/>merger.ts"]
E -->|categorize| F["OCI Installer<br/>installer-oci.ts"]
E -->|categorize| G["NPM Installer<br/>installer-npm.ts"]
F -->|extract| H["Tar Extraction<br/>tar-extract.ts"]
G -->|verify| I["Integrity Check<br/>integrity.ts"]
H -->|security| J["Path Validation<br/>util.ts"]
I -->|security| J
F -->|image ops| K["Skopeo Wrapper<br/>skopeo.ts"]
G -->|package ops| L["NPM Pack<br/>installer-npm.ts"]
D -->|finalize| M["Lock & Cleanup<br/>lock-file.ts"]
File Changes1. workspaces/install-dynamic-plugins/packages/install-dynamic-plugins/src/index.ts
|
- bin: switch to object form so the installed command stays `install-dynamic-plugins`. With the string form, npm derived the command name from the renamed scoped package (cli-module-install-dynamic-plugins), contradicting the README, runCliModule name, and the shell wrapper. - keytar stub now throws on every method instead of silently returning null. Install never touches credentials, so a throw only fires if a future @backstage/cli-node release reaches keytar in our path — better a loud failure in CI than a silent wrong credential in an init-container. - Thread the command's `info.usage` into the inner cleye parser so `install-dynamic-plugins install --help` prints the real invocation (`install-dynamic-plugins install <dynamic-plugins-root>`) instead of dropping the subcommand. - main() now requires explicit args (the process.argv default was unreachable in the cli-module path). 166/166 tests pass; tsc, lint, prettier, api-reports clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Replaces the COPY of scripts/install-dynamic-plugins/{install-dynamic-plugins.cjs,
install-dynamic-plugins.sh} with an `npm install` of
@red-hat-developer-hub/cli-module-install-dynamic-plugins (built and published
out of redhat-developer/rhdh-plugins).
This unblocks the cli-module structure on the rhdh-plugins side — it lets
that package use the standard `backstage-cli package build` (unbundled,
multi-file dist) instead of a custom esbuild bundle with a keytar stub. See
the conversation context: redhat-developer/rhdh-plugins#3254
Backward compatibility is preserved by writing a tiny
`/opt/app-root/src/install-dynamic-plugins.sh` shim that delegates to the
npm-installed bin, so the Helm chart and Operator init-container spec
continue to invoke `./install-dynamic-plugins.sh /dynamic-plugins-root`
unchanged.
DRAFT — DO NOT MERGE: blocked on
redhat-developer/rhdh-plugins#3254 (or the unbundled successor) being
merged and published to npm. Opened for review of the consumption pattern
and to back the cold-start benchmark posted in Slack.
Trade-off summary (cold-start benchmark on empty config):
- Current (bundled .cjs, 231 KB single file): ~89 ms warm cache (median)
- Proposed (npm install, 25 MB node_modules): ~180 ms warm cache (median)
The ~90 ms gap is the module-resolution overhead of unbundled Node — paid
once per pod start. Image build time also gets +`npm install` of ~25 MB
(one extra layer), offset by deleting ~7000 lines of vendored installer
script from this repo in a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…arn workspace (#4908) * [DRAFT] chore(install-dynamic-plugins): consume installer from npm Replaces the COPY of scripts/install-dynamic-plugins/{install-dynamic-plugins.cjs, install-dynamic-plugins.sh} with an `npm install` of @red-hat-developer-hub/cli-module-install-dynamic-plugins (built and published out of redhat-developer/rhdh-plugins). This unblocks the cli-module structure on the rhdh-plugins side — it lets that package use the standard `backstage-cli package build` (unbundled, multi-file dist) instead of a custom esbuild bundle with a keytar stub. See the conversation context: redhat-developer/rhdh-plugins#3254 Backward compatibility is preserved by writing a tiny `/opt/app-root/src/install-dynamic-plugins.sh` shim that delegates to the npm-installed bin, so the Helm chart and Operator init-container spec continue to invoke `./install-dynamic-plugins.sh /dynamic-plugins-root` unchanged. DRAFT — DO NOT MERGE: blocked on redhat-developer/rhdh-plugins#3254 (or the unbundled successor) being merged and published to npm. Opened for review of the consumption pattern and to back the cold-start benchmark posted in Slack. Trade-off summary (cold-start benchmark on empty config): - Current (bundled .cjs, 231 KB single file): ~89 ms warm cache (median) - Proposed (npm install, 25 MB node_modules): ~180 ms warm cache (median) The ~90 ms gap is the module-resolution overhead of unbundled Node — paid once per pod start. Image build time also gets +`npm install` of ~25 MB (one extra layer), offset by deleting ~7000 lines of vendored installer script from this repo in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fixup: address PR review on the install-dynamic-plugins Containerfile step - Install into /opt/dynamic-plugins-installer (its own dir, no package.json) instead of /opt/app-root/src so npm cannot honor the yarn workspace and perturb the production tree that `yarn workspaces focus` built at line 208. - Delegate the shim to `node_modules/.bin/install-dynamic-plugins` (the symlink npm creates from the package's bin field) instead of reaching into the package's internal layout. - Add `--no-save --omit=dev` so npm doesn't write a package-lock.json into the installer dir and doesn't fetch devDependencies. - Pin the installer to an exact version (0.1.0) so image builds are reproducible. - Add a build-time smoke check (`install-dynamic-plugins --help`) so a missing or renamed CLI entrypoint fails the image build instead of the init container at pod start. The hermetic-build concern (npm reaching the public registry when this Containerfile runs under Konflux with networking disabled) is acknowledged separately in the PR description — it's the real gating work and is not addressed by this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fixup: drop synthetic `install` subcommand from the shim The unbundled cli-module variant ships a fast-path bin that calls the installer directly (bypassing @backstage/cli-node's runCliModule dispatch), so the published binary takes the dynamic-plugins-root as a positional without a subcommand prefix — matching the original CLI surface. Verified locally: $ /opt/dynamic-plugins-installer/node_modules/.bin/install-dynamic-plugins /dynamic-plugins-root exits 0 on an empty config. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(install-dynamic-plugins): consume installer through dynamic-plugins yarn workspace Pivots #4908 from `RUN npm install …` to a plain yarn dependency in dynamic-plugins/package.json. The existing `yarn install --immutable` at line 151 of this Containerfile pulls the installer as part of the same yarn run that already brings in the rest of the dynamic-plugins deps, so the bin lands at /opt/app-root/src/dynamic-plugins/node_modules/.bin/install-dynamic-plugins and the final stage just writes a shim pointing there. Why yarn and not npm: scripts/local-hermeto-build.sh:213 already prefetches yarn deps for ./dynamic-plugins via cachi2/hermeto. No infra change is needed in this repo or in the midstream Konflux pipeline — the hermetic build "just works". The earlier npm-install approach required wiring npm into hermeto's fetch-deps, which is real work spanning two repos. Validated locally: yarn install of the unbundled cli-module variant (via a tarball of the fast-path build) produces .bin/install-dynamic-plugins and the smoke invocation on an empty dynamic-plugins.yaml exits 0. dynamic-plugins/yarn.lock is intentionally NOT regenerated in this commit — that has to happen against the published @red-hat-developer-hub/cli-module-install-dynamic-plugins@0.1.0 once redhat-developer/rhdh-plugins ships it. Marking the PR as Draft until then. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fixup: shim uses install subcommand + forwards all positional args The rhdh-plugins side reverted the bin shim to the standard `runCliModule` dispatch (redhat-developer/rhdh-plugins#3246 review), so the installed bin now expects an `install` subcommand. Updating the wrapper to match and to forward `"$@"` instead of `"$1"` so any extra positional argument the Helm chart or Operator passes is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: pin cli-module-install-dynamic-plugins to 0.2.0 and regen yarn.lock redhat-developer/rhdh-plugins#3246 merged and the changesets release published @red-hat-developer-hub/cli-module-install-dynamic-plugins as 0.2.0 (the changesets minor bump rolled past 0.1.0 because of the existing `0.0.0` workspace version). Updates the dynamic-plugins/package.json pin to 0.2.0 and regenerates dynamic-plugins/yarn.lock so `yarn install --immutable` (line 151 of the Containerfile) and the hermeto yarn prefetch at scripts/local-hermeto-build.sh:213 both resolve the package. The Build Image GitHub Actions job should now go green. Verified locally: yarn install resolves the package, the bin symlink lands at dynamic-plugins/node_modules/.bin/install-dynamic-plugins, and invoking it with `install <dir>` exits 0 on an empty config (going through the standard @backstage/cli-node runCliModule dispatch like the final Containerfile shim does). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: drop vendored scripts/install-dynamic-plugins/ now that it's on npm The dynamic-plugins installer is consumed from @red-hat-developer-hub/cli-module-install-dynamic-plugins via the yarn dependency declared in dynamic-plugins/package.json, so the vendored copy under scripts/install-dynamic-plugins/ is no longer used by the Containerfile and can be removed. Deletes: - scripts/install-dynamic-plugins/ (46 files, ~7000 lines) Cleans up the references that pointed at it: - .gitattributes: drop linguist-generated marker for the bundled .cjs - .dockerignore: drop the dist/ exceptions that kept the bundle in the build context - codecov.yml: drop the install-dynamic-plugins flag (used to track Python coverage from the pre-TS era) - .github/workflows/pr.yaml: drop the vitest + bundle-up-to-date checks - .github/workflows/coverage-baseline.yml: drop the pytest baseline - docs/dynamic-plugins/installing-plugins.md: link to the rhdh-plugins source instead of the vendored path - docs/coverage/e2e-rhdh.md: drop the vitest row from the coverage table - .rulesync/rules/ci-e2e-testing.md + the derived files under .claude/, .cursor/, .opencode/: update the installer link Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: move installer dep to root package.json so the bin survives stage 3 The Containerfile's build stage at line 175 wipes everything under dynamic-plugins/ except dist/, which deleted dynamic-plugins/node_modules/.bin/ along with everything else. The final stage then failed at runtime with exit code 127 because the bin wasn't there: /bin/sh: line 1: /opt/app-root/src/dynamic-plugins/node_modules/.bin/install-dynamic-plugins: No such file or directory Moves @red-hat-developer-hub/cli-module-install-dynamic-plugins from dynamic-plugins/package.json to the repo root package.json. The bin then survives the `yarn workspaces focus --all --production` at line 208 and lands at /opt/app-root/src/node_modules/.bin/ where the final stage's shim can reach it. Hermeto already prefetches yarn deps for the repo root (scripts/local-hermeto-build.sh:213, `{"type": "yarn", "path": "."}`), so no infra change. Also corrects the shim path: the published package uses the string form of `bin` ("bin/install-dynamic-plugins"), and yarn names the symlink after the package's base name (after the scope), so the actual bin lands at node_modules/.bin/cli-module-install-dynamic-plugins, not node_modules/.bin/install-dynamic-plugins. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: drop pytest residue left by the Python install-dynamic-plugins era The dynamic-plugins installer was migrated to TypeScript in #4574 and the vendored TypeScript copy was removed earlier in this PR, so the pytest test infrastructure no longer has anything live to test. - Deletes python/requirements-dev.in and python/requirements-dev.txt (pytest, pytest-cov, pytest-mock — and their transitive deps). - Drops the .github/workflows/pr.yaml "Install Python dependencies" step that installed all three requirements files; no subsequent step in the PR workflow uses Python. - Cleans the stale comment in codecov.yml referencing the removed install-dynamic-plugins pytest flag. python/requirements.txt and python/requirements-build.txt stay — they are consumed by the TechDocs venv build in the Containerfile (line 237) for mkdocs / mkdocs-techdocs-core / plantuml-markdown, which is unrelated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: stop tracking and ignore the Claude Code session lock file `.claude/scheduled_tasks.lock` is a Claude Code runtime artifact (per-session pid lock); it got accidentally included by `git add -A` in the previous commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address PR review — smoke check `install`, stop pointing at line numbers, drop orphaned comments - Build-time smoke check now invokes `install --help`, not just `--help`. A top-level `--help` only proves the bin resolves; the shim hardcodes the `install` subcommand, so if a future release of the package renames/removes it, the failure should land at image build time, not at pod start. cleye prints the subcommand help and exits 0 before validating the required `<dynamic-plugins-root>` positional, so this is a strict upgrade with no behavioural change for the current published package. - Replace hardcoded line-number references in the Containerfile comment (already drifted: line 175 → actual 173) with the step banners `=== YARN WORKSPACES FOCUS ===` and `=== DELETE DYNAMIC PLUGINS/* ===`, which survive edits to the surrounding RUN steps. Same treatment for scripts/local-hermeto-build.sh: point at the `{"type": "yarn", "path": "."}` fetch-deps entry instead of a line number. - Drop the orphaned `.dockerignore` comment that explained the now-removed `!scripts/install-dynamic-plugins/dist` re-include lines and was reading as if it described `**/node_modules`. - Drop the orphaned `.gitattributes` section header `# Generated bundles — collapsed in GitHub diffs, …` left behind when the `dist/install-dynamic-plugins.cjs linguist-generated=true` entry was removed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: move installer dep to packages/backend to satisfy monorepo:check The sherif rule in `yarn run monorepo:check` rejects `dependencies` on the private root package.json — the rationale being that dependencies vs devDependencies is a no-op for a private package and creates confusion. CI fails on that check today. Moves `@red-hat-developer-hub/cli-module-install-dynamic-plugins` from the root package.json to `packages/backend/package.json`, keeping the load-bearing behaviour: backend is `private: true` and gets included by `yarn workspaces focus --all --production`, so the bin still hoists to `/opt/app-root/src/node_modules/.bin/cli-module-install-dynamic-plugins` where the Containerfile shim picks it up. Semantically this is the right home anyway — the backend is the runtime that the init-container runs alongside. Containerfile comment updated to point at the new declaration site. Verified locally: - yarn run monorepo:check → No issues found - node_modules/.bin/cli-module-install-dynamic-plugins install --help exits 0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>



Summary
Alternative to #3246, opened so the trade-offs are reviewable side by side (cc @schultzp2020).
#3246 ships the installer as a standalone CLI (
backstage.role: cli) that goes through backstage-cli'scustomBuildescape hatch. This PR restructures it as a Backstage CLI module (backstage.role: cli-module,@backstage/cli-node'screateCliModule), per review feedback, so it integrates natively withbackstage-clidiscovery.Both keep cleye as the argv parser and both produce a single bundled
.cjsfor the RHDH init-container.What changed vs #3246
@red-hat-developer-hub/cli-module-install-dynamic-plugins,backstage.role: cli-module.src/module.ts→createCliModuleregistering aninstallcommand;src/command.tsloads it;src/cli.ts(esbuild entry) runs it viarunCliModule.main()now takes explicit argv from the command context.@backstage/cli-nodeadded as a dependency. cleye stays as the in-command parser (same as@backstage/cli-module-lint).Trade-offs to weigh
role: cli)role: cli-module)install-dynamic-plugins <root>install-dynamic-plugins install <root>(adds subcommand)@backstage/cli-nodepulls in keytar (native.node)The keytar caveat
@backstage/cli-nodedepends onkeytar(a native credential store, for@backstage/cli-module-auth). esbuild can't bundle.nodebinaries into a single file, so this PR aliases keytar to no-ops at build time (esbuild-keytar-stub.cjs). The install command never touches credentials, so it's safe today — but it's load-bearing on cli-node never lazy-loading keytar in a path our command hits. If a future cli-node release changes that, the init-container would break.Test plan
yarn tsc/lint/prettier:check/build:api-reports:only --cicleaninstall-dynamic-plugins install <dir>exits 0 on an empty configRecommendation
Both work. #3246 is leaner and keeps the init-container artifact small with no native-dep gymnastics; this PR aligns with the cli-module convention at the cost of ~5x bundle size and a keytar stub. Posting both so the maintainers can pick the direction.
🤖 Generated with Claude Code