feat: brew install + npm sibling channels via OIDC trusted publishing (#806)
* chore: ignore local secret file
* feat(cli): replace self-update with package-manager-owned runtime resolution
The CLI no longer downloads runtime bundles from GitHub on first run. Instead:
- A new resolveRuntime() in apps/cli/src/runtime.ts resolves the bundle in
three steps: KANDEV_BUNDLE_DIR env var (Homebrew wrapper, tests) → installed
@kdlbs/runtime-{platform} npm package → cached download (only when
--runtime-version is given explicitly).
- apps/cli/src/run.ts is split: launch logic separated from download logic.
The download path stays for the explicit --runtime-version flag only.
- The --version flag now prints CLI semver (was: runtime tag selector). The
old behavior moves to --runtime-version, documented as advanced/debug.
- The npm self-update prompt (apps/cli/src/update.ts) is removed entirely.
- apps/cli/package.json declares optionalDependencies for five
@kdlbs/runtime-{platform} packages and engines.npm: ">=7" (required for
optional-dep resolution during npx).
- Adds esbuild + a "bundle" script that produces dist/cli.bundle.js — a
single-file CJS bundle with all Node deps inlined. The Homebrew formula
uses this; the npm package keeps the unbundled dist/cli.js.
Tests: 130/130 passing. Bundle smoke-tested in a directory with no
node_modules to confirm tree-kill is correctly inlined.
* feat(release): unified vX.Y.Z release flow with brew + npm sibling channels
GitHub release artifacts become the canonical build output. npm and Homebrew
are independent sibling channels — both consume the same GitHub release
tarballs; neither depends on the other.
Workflow changes (.github/workflows/release.yml):
- Trigger is now workflow_dispatch only (maintainer-only, gated by repo write
access). Inputs: bump dropdown (patch/minor/major) + dry_run checkbox.
- Single job graph orchestrates everything in one run:
prepare (bump, PR, merge, tag) → build-web/build-cli → build-bundles (5
platforms) → docker → publish-release → publish-npm + update-homebrew-tap.
- The prepare job inlines version bump, optionalDependencies sync, CHANGELOG
generation, PR create + squash-merge, and tag push. Replaces what
release-pr.sh used to do locally.
- Tag-push trigger removed; everything runs from the dispatch flow.
New scripts (called by the workflow):
- package-cli.sh: builds + esbuild-bundles the CLI for inclusion in release
bundles (cli/bin/cli.js + cli/dist/cli.bundle.js + package.json).
- package-npm-runtime.sh: generates per-platform @kdlbs/runtime-* npm
packages from release bundles. Sets npm os/cpu fields so npm 7+ skips
non-matching platforms during npx kandev install.
- publish-npm.sh: verifies GitHub assets exist, downloads them, repackages,
publishes all 5 @kdlbs/runtime-* packages + main kandev@X.Y.Z.
- update-homebrew-tap.sh: clones kdlbs/homebrew-kandev via SSH using the
HOMEBREW_TAP_DEPLOY_KEY repo secret, generates Formula/kandev.rb pointing
at the GitHub release assets, pushes directly to main.
Removed scripts:
- release.sh and release-pr.sh — both are redundant with the new workflow.
Required CI secrets:
- NPM_TOKEN: granular access token with publish access to kandev + @kdlbs/*
- HOMEBREW_TAP_DEPLOY_KEY: SSH private key for kdlbs/homebrew-kandev
* docs: brew install, runtime resolution, and GHA-driven release flow
- README.md: add Homebrew quick start (brew install kdlbs/kandev/kandev),
update NPX docs to require npm 7+, document the three update paths.
- docs/cli.md: rewrite architecture diagram to show three-step runtime
resolution. Replace --version <tag> docs with --version (CLI semver) +
--runtime-version <tag> (advanced/debug). Remove KANDEV_NO_UPDATE_PROMPT.
- apps/cli/README.md: add Homebrew install path; document
package-manager-owned updates.
- apps/cli/README_internal.md: rewrite to describe the npm package vs
GitHub bundle artifact-shape distinction and the three-step resolver.
Document the GHA-driven release UI (Run workflow + bump dropdown).
- AGENTS.md: add Release & Versioning Convention section describing the
unified single-SemVer flow, the sibling channel model, and the GHA flow.
* fix(release): release-bundle bin/cli.js must require the esbuild bundle
The previous package-cli.sh copied apps/cli/bin/cli.js verbatim into the
release bundle. That file requires `../dist/cli.js` (unbundled), which
only works when node_modules is present alongside it.
In the GitHub release bundle (used by Homebrew and manual installs),
there is no node_modules — `require('tree-kill')` and `require('tar')`
inside dist/cli.js would crash at startup.
Now package-cli.sh writes a release-specific bin/cli.js that requires
`../dist/cli.bundle.js` instead. The bundled file has all Node deps
inlined via esbuild, so it runs standalone in the Cellar.
The npm package keeps its own bin/cli.js (in apps/cli/bin/) requiring
the unbundled dist/cli.js, since npm installs tree-kill etc. normally.
Caught while smoke-testing the release bundle locally with
KANDEV_BUNDLE_DIR — backend + web boot succeeded after this fix.
* feat(release): switch npm publishing to Trusted Publishers (OIDC)
The previous flow required an NPM_TOKEN secret. Local testing with that
token failed with EOTP because the granular access token still required a
2FA OTP on publish despite the account-level setting change. npm now
recommends Trusted Publishers (OIDC) over long-lived tokens.
Workflow change (release.yml):
- publish-npm job declares `permissions: id-token: write` so GitHub Actions
can mint OIDC tokens.
- Removes NODE_AUTH_TOKEN env var. The npm CLI auto-detects the OIDC
environment and exchanges the GitHub OIDC token for a short-lived
npmjs.com publish credential.
- Comment block updated to reflect the new auth model. NPM_TOKEN is no
longer listed in required secrets (already deleted from repo secrets).
Script change (publish-npm.sh):
- Updates `npm publish` calls to include `--provenance` so each published
package ships with a signed attestation linking it back to this commit
and workflow run.
- Header comment explains the OIDC auth model.
Setup required on npmjs.com (per package, before first OIDC publish):
For each of the 6 packages — kandev plus @kdlbs/runtime-{linux-x64,
linux-arm64,darwin-x64,darwin-arm64,win32-x64} — go to the package's
"Settings → Publishing access" and add a Trusted Publisher pointing at
this repo (kdlbs/kandev), workflow filename release.yml, and (optionally)
an environment.
* fix(release): pin optionalDependencies to bootstrap version in committed source
CI was failing because apps/cli/package.json declared optionalDependencies
at 0.16.0 but only 0.0.0-bootstrap exists on npm for those packages. The
lockfile couldn't resolve, and `pnpm install --frozen-lockfile` rejected
the working tree.
Pin optionalDependencies to 0.0.0-bootstrap in the committed source so the
lockfile resolves cleanly. The release workflow now bumps them to the real
version inside publish-npm.sh, just before publishing main kandev — at
which point the matching @kdlbs/runtime-*@VERSION tarballs have already
been published earlier in the same step. This avoids the chicken-and-egg
where the prepare job tried to bump optional deps to a version that didn't
yet exist on npm.
Workflow changes:
- prepare job: only bumps the main package version, not optional deps.
- publish-npm.sh: rewrites optionalDependencies to $VERSION right before
the main `npm publish`, after the 5 runtime packages have been published.
The published kandev@X.Y.Z still has its optionalDependencies pinned to
matching @kdlbs/runtime-*@X.Y.Z. Only the local source uses the bootstrap
pin for development convenience.
* fix: address PR review comments on release flow + runtime resolution
PR reviewers (Greptile, Claude, CodeRabbit) flagged six concrete issues.
Each is addressed below.
Critical:
- publish-npm.sh: previously exited 0 even when runtime packages failed to
publish, which would leave kandev@VERSION on the registry pointing at
@kdlbs/runtime-*@VERSION that never got published. Now: capture full npm
output on failure, distinguish "already published" (idempotent skip)
from real failures, abort before publishing main kandev if any runtime
fails.
- release.yml: build-bundles referenced needs.prepare.outputs.tag without
declaring prepare in its needs array. Static analysis flagged this.
Added prepare to the needs list.
Major:
- update-homebrew-tap.sh: the formula's write_env_script only injected
KANDEV_BUNDLE_DIR. run.ts:174 reads KANDEV_VERSION to display the
running version in startup logs; without it Homebrew users would see
"release: (env)" instead of "release: 0.17.0". Pass KANDEV_VERSION:
version.to_s through the wrapper.
- runtime.ts: the catch {} block silently swallowed validateBundle errors,
hiding "package installed but corrupt" cases behind the generic
"no runtime found" message. Split the lookup so MODULE_NOT_FOUND falls
through, but validation errors propagate.
- run.ts: the --runtime-version code path skipped validateBundle, missing
the file-existence checks that the npm/env paths get. Added it.
Minor:
- release.yml: workflow-level permissions defaulted to write for everyone.
Tightened to read-only at the workflow level; jobs that need writes
(prepare, docker, publish-release) declare them explicitly.
- args.test.ts: added missing tests for --runtime-version with no value
and --runtime-version followed by another flag, parallel to the
existing --port tests.
Tests: 132 passing (was 130; +2 new).
* fix: address second-round PR review (cleanup traps, host-key pinning, supply chain)
Latest review (Claude + Greptile) flagged three security/cleanup items
on top of the previous fix commit. All three addressed below.
publish-npm.sh:
- Add `trap 'rm -rf "$WORK_DIR"' EXIT` immediately after `mktemp -d` so
the tmpdir is cleaned up even when `pnpm build` or `npm publish` fails
partway through (set -e would otherwise abort before the explicit
rm at the end).
- Remove redundant `pnpm build` before `npm publish` — the `prepublishOnly`
script in package.json already runs the build.
update-homebrew-tap.sh:
- Combine `cleanup_ssh` + WORK_DIR cleanup into one EXIT trap so `git clone`
/ `git push` failures don't leak the tmpdir.
- Pin GitHub's published SSH host keys (ed25519, ECDSA, RSA) into a temp
`known_hosts` file instead of using `accept-new` + `/dev/null`. This
closes the "trust any host key on first connect" gap. The pinned keys
come from https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints.
release.yml:
- Replace the curl|tar git-cliff install with `orhun/git-cliff-action@v4`
(already used in publish-release). The action pins to a digest, so we
no longer need to verify a tarball checksum manually. Removes a
supply-chain attack surface where a tampered git-cliff binary could run
during `prepare` (which has contents:write + pull-requests:write).
* revert: keep curl install for git-cliff, add sha512 verification instead
The previous commit (18b2a5c) replaced our `curl | tar` install with
orhun/git-cliff-action@v4 under the banner "supply chain". On reflection
that swap doesn't actually improve anything:
- @v4 is a moving tag, not a digest pin — same trust as a tagged curl URL.
- Both depend on trusting the same maintainer (orhun).
- The action adds a third-party step we don't audit.
The legitimate concern from the review was "no integrity verification on
the downloaded tarball." Address that directly: pin the asset version,
embed its published sha512, and abort if the download doesn't match.
The hash for v2.6.1's x86_64-linux-gnu tarball came from
https://github.com/orhun/git-cliff/releases/download/v2.6.1/<asset>.sha512.
This restores the original setup with a real integrity check and one
fewer external dependency.
* fix: address remaining PR nits (env restore, sha256 download dir)
runtime.test.ts: drop the redundant `delete process.env.KANDEV_BUNDLE_DIR`
in afterEach — `process.env = { ...origEnv }` already restores the
original environment. The redundant delete would silently un-set the var
if a developer had it in their shell, leaking test side effects across
suites.
update-homebrew-tap.sh: fetch_sha256 now downloads into $WORK_DIR instead
of /tmp/. The EXIT trap covers WORK_DIR, so the .sha256 files are cleaned
up automatically. Avoids leaking 5 tmpfiles per release run on long-lived
runners.
* fix: address third-round PR comments
- update-homebrew-tap.sh: Homebrew formula was calling
`bin.write_env_script` on the bin directory with target `cli/bin/cli.js`,
which would create the wrapper at `$HOMEBREW_PREFIX/bin/cli.js` (named
after the target's basename) instead of `$HOMEBREW_PREFIX/bin/kandev`.
Result: `brew install kandev` would have produced `command not found`.
Use `(bin/"kandev").write_env_script(...)` to name the wrapper
explicitly. Caught by Claude as a P0 blocker.
- publish-npm.sh: main `kandev` publish lacked the EPUBLISHCONFLICT
idempotency handling that the runtime-package loop has. If publish-npm
was re-run after a partial failure (e.g. the runtime packages succeeded
but the main package failed and was then manually published), the
re-run would fail on the main package even though the version was
already on the registry. Now: capture output, treat
"already published" as success, fail loudly with full output on real
errors. Caught by Greptile as P1.
* refactor(release): direct push to main instead of PR-and-merge dance
The prepare job was opening a release branch, creating a PR, and
immediately squash-merging it via gh CLI. That pattern was carried over
from the old release-pr.sh, but in this workflow it earns nothing:
- main has no branch protection, so direct push is allowed.
- The PR was force-merged in the same step (no `--auto`, no waiting for
required checks), so it provided no pre-merge validation.
- Discoverability is fine without a PR — git tag, GitHub release, workflow
run, and main commit are all visible.
The PR-and-merge sequence had ~5 GitHub API roundtrips, each a potential
failure point, plus added 30+ lines of YAML and required pull-requests:
write on the job. Replaced with a straight-line commit + tag + push:
git fetch origin main && git checkout main && git reset --hard origin/main
... bump pkg.json, regen CHANGELOG, npm install ...
git add ... && git commit && git tag -a && git push origin main "$TAG"
Job permissions tightened to contents: write only.
Race condition note: if main advances between fetch and push, the push
fails non-fast-forward — same outcome as the old PR pattern, where the
auto-merge would have failed. For workflow_dispatch releases with the
maintainer actively driving, the race is essentially never going to
happen, and the recovery (re-run the workflow) is the same.
The dry-run path still bumps + regenerates CHANGELOG inside the runner
so the maintainer can verify the computed values look right, just
without committing or pushing anything.
* chore(release): drop verbose comment about direct-push rationale
* docs: drop 'no first-run download' note from user-facing docs
The phrasing only makes sense to readers who knew the previous model
required a first-run download. New users have no baseline to compare
against, so it adds noise. Removed from docs/cli.md and apps/cli/README.md
(which renders on npmjs.com). Kept in apps/cli/README_internal.md as a
design note for contributors.
* Revert "chore: ignore local secret file"
The /secret entry was added defensively after the bootstrap test, but it
clutters .gitignore with a project-specific path that nobody else needs.
Drop it; secrets stay out of the repo by other means (don't commit them).
* fix: address third-pass review (stale workflow comments + missing test)
release.yml:
- The header comment block and dry_run input description still referenced
the PR-and-merge step that was removed in 53c2e45. Updated both to
describe the direct-commit flow.
runtime.test.ts:
- The runtime resolver's npm-package branch (step 2 — the primary path
after npx kandev@latest or npm install -g) had no positive coverage. In
the test env, require.resolve for @kdlbs/runtime-* throws
MODULE_NOT_FOUND, so the existing tests only exercised step 1 (env var)
and step 3 (cache via --runtime-version), plus the no-runtime-found
fallthrough.
Added two tests using vi.spyOn on Module._resolveFilename to point at a
fake bundle inside tmpDir:
- happy path: installed npm package resolves to source=npm with the
right bundleDir
- corrupt-bundle path: validateBundle errors propagate (rather than
being swallowed and falling through to "no runtime found")
These cover the real-world scenarios users will hit on either install
channel.
Tests: 132 → 134.
* fix(release): push commit + tag atomically
Without --atomic, git push can succeed for the tag ref and fail for the
main ref on a non-fast-forward, leaving an orphaned tag at the pre-bump
commit. The orphan then trips the 'Tag already exists on origin' guard
on the next re-run, requiring manual tag deletion to unblock.
--atomic makes the multi-ref push succeed-or-fail as a unit.
Caught by Greptile as P1.
* refactor(cli): drop runtime.ts step 3 — cache fallback was unreachable
resolveRuntime's runtimeVersion parameter and cache-fallback step were
dead code from production's perspective. The --runtime-version path is
handled entirely in run.ts (findCachedRelease + downloadRuntimeVersion +
findBundleRoot + validateBundle), and run.ts only ever called
resolveRuntime() with no argument. The parameter and step 3 only existed
because tests exercised them.
Two parallel "compute cache dir" implementations (runtime.ts:65 and
run.ts:157) were a drift hazard if the cache layout ever changes.
Changes:
- Drop the runtimeVersion parameter from resolveRuntime.
- Drop the cache step from runtime.ts entirely; remove the unused
CACHE_DIR + findBundleRoot imports.
- Narrow RuntimeSource from "env" | "npm" | "cache" to "env" | "npm",
and drop the now-unused tag field from ResolvedRuntime.
- Remove the three "cache fallback" tests from runtime.test.ts. The
--runtime-version cache lookup is still covered by run.test.ts via
findCachedRelease tests.
Tests: 134 → 131 (three deleted tests are obsolete, the production
behavior they were verifying is now covered by run.test.ts).
* chore(test): drop dead mock setup left over from runtime.ts step 3 removal
After feb7c5a removed the cache fallback from runtime.ts, the test file
had two dangling artifacts:
- The vi.mock("./constants", ...) block + the mockCacheDir holder + the
mockCacheDir.value = tmpDir assignment in beforeEach. runtime.ts no
longer imports anything from ./constants, so the mock was a no-op
that would mislead readers tracing why constants are mocked here.
- The PLATFORM = "macos-arm64" constant was unused — platform
determinism is already enforced by the vi.mock("./platform", ...)
block one section above.
Caught by Claude and the github-code-quality bot.
This commit is contained in:
+310
-19
@@ -1,20 +1,213 @@
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
workflow_dispatch:
|
||||
# Maintainer-only release workflow.
|
||||
# Trigger: GitHub Actions UI → "Run workflow" (workflow_dispatch).
|
||||
#
|
||||
# Flow (one click → everything below):
|
||||
# 1. prepare — bump version, regen CHANGELOG, commit to main, push tag vX.Y.Z
|
||||
# 2. build-web — build Next.js standalone bundle
|
||||
# 3. build-cli — build + esbuild-bundle CLI
|
||||
# 4. build-bundles — assemble bin/ + web/ + cli/ tarballs for 5 platforms
|
||||
# 5. docker — build & push multi-arch container image to GHCR
|
||||
# 6. publish-release — create GH release with .tar.gz + .sha256 assets + auto-generated notes
|
||||
# 7. publish-npm — publish kandev@X.Y.Z + @kdlbs/runtime-*@X.Y.Z to npmjs
|
||||
# 8. update-homebrew-tap — push updated Formula/kandev.rb to kdlbs/homebrew-kandev
|
||||
#
|
||||
# npm publishing uses Trusted Publishers (OIDC) — no NPM_TOKEN required.
|
||||
# Each package (kandev + 5 @kdlbs/runtime-* packages) must have this workflow
|
||||
# configured as its trusted publisher on npmjs.com (Settings → Publishing access).
|
||||
#
|
||||
# Required secrets:
|
||||
# HOMEBREW_TAP_DEPLOY_KEY — SSH private key for kdlbs/homebrew-kandev deploy key
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
bump:
|
||||
description: "Version bump"
|
||||
required: true
|
||||
type: choice
|
||||
default: minor
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
dry_run:
|
||||
description: "Dry run (no commit, tag, npm publish, or tap update)"
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
# Default to read-only at the workflow level. Each job opts into the writes
|
||||
# it actually needs.
|
||||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
name: Prepare release (bump version, commit, tag)
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # commit + push to main, push tag
|
||||
outputs:
|
||||
version: ${{ steps.compute.outputs.version }}
|
||||
tag: ${{ steps.compute.outputs.tag }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: "9"
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
|
||||
- name: Install git-cliff
|
||||
env:
|
||||
# Pinned to v2.6.1. To bump, also update the sha512 below — fetch the
|
||||
# new one from:
|
||||
# https://github.com/orhun/git-cliff/releases/download/<tag>/<asset>.sha512
|
||||
CLIFF_VERSION: "2.6.1"
|
||||
CLIFF_ASSET: "git-cliff-2.6.1-x86_64-unknown-linux-gnu.tar.gz"
|
||||
CLIFF_SHA512: "b1e7a6b70fc794accdccd9717eff5a8bf8b507f0308da44d837d935be31b18b19db3758d1a9bb4975fbca7d01ddaae251229a753ae2cd2ecc24845cfc50d067f"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
curl -sSL "https://github.com/orhun/git-cliff/releases/download/v${CLIFF_VERSION}/${CLIFF_ASSET}" -o /tmp/git-cliff.tar.gz
|
||||
echo "${CLIFF_SHA512} /tmp/git-cliff.tar.gz" | sha512sum -c -
|
||||
tar -xz -C /tmp -f /tmp/git-cliff.tar.gz
|
||||
sudo mv "/tmp/git-cliff-${CLIFF_VERSION}/git-cliff" /usr/local/bin/
|
||||
rm -rf /tmp/git-cliff.tar.gz "/tmp/git-cliff-${CLIFF_VERSION}"
|
||||
git-cliff --version
|
||||
|
||||
- name: Configure git identity
|
||||
run: |
|
||||
git config user.email "release-bot@kandev"
|
||||
git config user.name "kandev release bot"
|
||||
|
||||
- name: Compute next version
|
||||
id: compute
|
||||
env:
|
||||
BUMP: ${{ inputs.bump }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
PKG_VERSION="$(node -p "require('./apps/cli/package.json').version")"
|
||||
|
||||
# Find latest git tag, normalizing legacy two-part tags (vM.m → M.m.0)
|
||||
LATEST_TAG="$(git tag --list 'v*' | python3 -c "
|
||||
import sys, re
|
||||
best = (0, 0, 0)
|
||||
for t in (l.strip() for l in sys.stdin if l.strip()):
|
||||
s = t.lstrip('v')
|
||||
m3 = re.match(r'^(\d+)\.(\d+)\.(\d+)$', s)
|
||||
m2 = re.match(r'^(\d+)\.(\d+)$', s)
|
||||
if m3:
|
||||
v = tuple(int(x) for x in m3.groups())
|
||||
elif m2:
|
||||
v = (int(m2.group(1)), int(m2.group(2)), 0)
|
||||
else:
|
||||
continue
|
||||
if v > best:
|
||||
best = v
|
||||
print(f'{best[0]}.{best[1]}.{best[2]}')
|
||||
")"
|
||||
|
||||
# Use the higher of package.json version and the latest tag as baseline
|
||||
BASELINE="$(python3 -c "
|
||||
a = '$PKG_VERSION'.split('.')
|
||||
b = '$LATEST_TAG'.split('.')
|
||||
ai = tuple(int(x) for x in a)
|
||||
bi = tuple(int(x) for x in b)
|
||||
print('$PKG_VERSION' if ai >= bi else '$LATEST_TAG')
|
||||
")"
|
||||
|
||||
IFS=. read -r MAJ MIN PAT <<<"$BASELINE"
|
||||
case "$BUMP" in
|
||||
patch) NEXT="${MAJ}.${MIN}.$((PAT + 1))" ;;
|
||||
minor) NEXT="${MAJ}.$((MIN + 1)).0" ;;
|
||||
major) NEXT="$((MAJ + 1)).0.0" ;;
|
||||
*) echo "Invalid bump '$BUMP'" >&2; exit 1 ;;
|
||||
esac
|
||||
|
||||
TAG="v${NEXT}"
|
||||
echo "Current package.json version: $PKG_VERSION"
|
||||
echo "Latest git tag (normalized): $LATEST_TAG"
|
||||
echo "Baseline: $BASELINE"
|
||||
echo "Bump: $BUMP"
|
||||
echo "Next version: $NEXT"
|
||||
echo "Next tag: $TAG"
|
||||
echo "version=$NEXT" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Sanity: tag must not already exist
|
||||
if git rev-parse "$TAG" >/dev/null 2>&1; then
|
||||
echo "Tag $TAG already exists locally" >&2
|
||||
exit 1
|
||||
fi
|
||||
if git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; then
|
||||
echo "Tag $TAG already exists on origin" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Bump version, generate CHANGELOG, commit, tag, push
|
||||
if: ${{ !inputs.dry_run }}
|
||||
env:
|
||||
NEXT: ${{ steps.compute.outputs.version }}
|
||||
TAG: ${{ steps.compute.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git fetch origin main
|
||||
git checkout main
|
||||
git reset --hard origin/main
|
||||
|
||||
# Bump only the main package version. optionalDependencies stay
|
||||
# pinned to 0.0.0-bootstrap in committed source — publish-npm.sh
|
||||
# rewrites them to the real version in-flight before publishing,
|
||||
# once the matching @kdlbs/runtime-* tarballs exist on npm.
|
||||
(cd apps/cli && npm version --no-git-tag-version "$NEXT")
|
||||
pnpm -C apps install --no-frozen-lockfile
|
||||
|
||||
git-cliff --tag "$TAG" -o CHANGELOG.md
|
||||
|
||||
git add apps/cli/package.json CHANGELOG.md apps/pnpm-lock.yaml
|
||||
git commit -m "release: $NEXT"
|
||||
git tag -a "$TAG" -m "release: $NEXT"
|
||||
|
||||
# Push commit and tag atomically — without --atomic, Git could
|
||||
# accept the tag push but reject the main update on a non-fast-forward,
|
||||
# leaving an orphaned tag at the pre-bump commit (and tripping the
|
||||
# "tag already exists" guard on the next re-run). --atomic makes the
|
||||
# whole multi-ref push succeed or fail as a unit.
|
||||
git push --atomic origin main "$TAG"
|
||||
|
||||
- name: Dry-run summary
|
||||
if: ${{ inputs.dry_run }}
|
||||
env:
|
||||
NEXT: ${{ steps.compute.outputs.version }}
|
||||
TAG: ${{ steps.compute.outputs.tag }}
|
||||
run: |
|
||||
# In dry-run we still bump + run git-cliff locally so the maintainer
|
||||
# can verify the computed version + changelog look right, but nothing
|
||||
# is committed or pushed.
|
||||
(cd apps/cli && npm version --no-git-tag-version "$NEXT")
|
||||
pnpm -C apps install --no-frozen-lockfile
|
||||
git-cliff --tag "$TAG" -o CHANGELOG.md
|
||||
echo
|
||||
echo "DRY RUN: Would have committed + tagged $TAG and pushed to origin/main."
|
||||
echo "DRY RUN: apps/cli/package.json bumped to $NEXT (in-runner only)."
|
||||
echo "DRY RUN: CHANGELOG.md regenerated (in-runner only)."
|
||||
|
||||
build-web:
|
||||
name: Build web standalone
|
||||
needs: prepare
|
||||
if: ${{ !inputs.dry_run }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: "9"
|
||||
@@ -25,7 +218,7 @@ jobs:
|
||||
run: pnpm -C apps install --frozen-lockfile
|
||||
- name: Build web (standalone)
|
||||
env:
|
||||
KANDEV_VERSION: ${{ github.ref_name }}
|
||||
KANDEV_VERSION: ${{ needs.prepare.outputs.tag }}
|
||||
run: pnpm -C apps --filter @kandev/web build
|
||||
- name: Package web bundle
|
||||
run: bash scripts/release/package-web.sh
|
||||
@@ -35,8 +228,34 @@ jobs:
|
||||
path: dist/web
|
||||
include-hidden-files: true
|
||||
|
||||
build-cli:
|
||||
name: Build & bundle CLI
|
||||
needs: prepare
|
||||
if: ${{ !inputs.dry_run }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: "9"
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
- name: Install dependencies
|
||||
run: pnpm -C apps install --frozen-lockfile
|
||||
- name: Build and bundle CLI
|
||||
run: bash scripts/release/package-cli.sh
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cli-bundle
|
||||
path: dist/kandev/cli
|
||||
|
||||
build-bundles:
|
||||
needs: build-web
|
||||
name: Build platform bundle (${{ matrix.platform }})
|
||||
needs: [prepare, build-web, build-cli]
|
||||
if: ${{ !inputs.dry_run }}
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
@@ -69,6 +288,8 @@ jobs:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
- uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: apps/backend/go.mod
|
||||
@@ -77,6 +298,10 @@ jobs:
|
||||
with:
|
||||
name: web-bundle
|
||||
path: dist/web
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: cli-bundle
|
||||
path: dist/kandev/cli
|
||||
|
||||
- name: Install cross-compilation tools
|
||||
if: matrix.cc
|
||||
@@ -96,19 +321,19 @@ jobs:
|
||||
|
||||
- name: Package bundle
|
||||
run: |
|
||||
rm -rf dist/kandev
|
||||
rm -rf dist/kandev/bin dist/kandev/web
|
||||
mkdir -p dist/kandev/bin
|
||||
EXT=${{ matrix.goos == 'windows' && '.exe' || '' }}
|
||||
cp apps/backend/bin/kandev${EXT} dist/kandev/bin/
|
||||
cp apps/backend/bin/agentctl${EXT} dist/kandev/bin/
|
||||
mkdir -p dist/kandev/web
|
||||
cp -R dist/web/. dist/kandev/web/
|
||||
# cli/ was already placed by the build-cli job (downloaded above)
|
||||
cd dist
|
||||
tar -czf "kandev-${{ matrix.platform }}.tar.gz" kandev
|
||||
shasum -a 256 "kandev-${{ matrix.platform }}.tar.gz" > "kandev-${{ matrix.platform }}.tar.gz.sha256"
|
||||
|
||||
- name: Upload bundle artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bundle-${{ matrix.platform }}
|
||||
path: |
|
||||
@@ -116,9 +341,17 @@ jobs:
|
||||
dist/kandev-${{ matrix.platform }}.tar.gz.sha256
|
||||
|
||||
docker:
|
||||
name: Build & push container image
|
||||
needs: prepare
|
||||
if: ${{ !inputs.dry_run }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write # push to GHCR
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
@@ -126,10 +359,10 @@ jobs:
|
||||
with:
|
||||
images: ghcr.io/kdlbs/kandev
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=${{ needs.prepare.outputs.version }}
|
||||
type=raw,value=${{ needs.prepare.outputs.tag }}
|
||||
type=sha
|
||||
type=raw,value=latest,enable=${{ github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}
|
||||
type=raw,value=latest
|
||||
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
|
||||
@@ -152,11 +385,16 @@ jobs:
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
publish-release:
|
||||
needs: build-bundles
|
||||
name: Publish GitHub release
|
||||
needs: [prepare, build-bundles]
|
||||
if: ${{ !inputs.dry_run }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # softprops/action-gh-release creates the release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate release notes
|
||||
@@ -167,8 +405,7 @@ jobs:
|
||||
env:
|
||||
OUTPUT: RELEASE_NOTES.md
|
||||
|
||||
- name: Download bundle artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: bundle-*
|
||||
path: dist/release-assets
|
||||
@@ -192,10 +429,64 @@ jobs:
|
||||
done
|
||||
ls -lh dist/release-assets
|
||||
|
||||
- name: Publish release assets
|
||||
- name: Publish release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ needs.prepare.outputs.tag }}
|
||||
name: ${{ needs.prepare.outputs.tag }}
|
||||
body_path: RELEASE_NOTES.md
|
||||
files: |
|
||||
dist/release-assets/kandev-*.tar.gz
|
||||
dist/release-assets/kandev-*.tar.gz.sha256
|
||||
|
||||
publish-npm:
|
||||
name: Publish npm packages
|
||||
needs: [prepare, publish-release]
|
||||
if: ${{ !inputs.dry_run }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write # Required for OIDC trusted publishing
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: "9"
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm -C apps install --frozen-lockfile
|
||||
|
||||
- name: Publish npm packages
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
bash scripts/release/publish-npm.sh \
|
||||
"${{ needs.prepare.outputs.version }}" \
|
||||
"${{ needs.prepare.outputs.tag }}"
|
||||
|
||||
update-homebrew-tap:
|
||||
name: Update Homebrew tap
|
||||
needs: [prepare, publish-release]
|
||||
if: ${{ !inputs.dry_run }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ needs.prepare.outputs.tag }}
|
||||
|
||||
- name: Update Homebrew tap formula
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
HOMEBREW_TAP_DEPLOY_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
|
||||
run: |
|
||||
bash scripts/release/update-homebrew-tap.sh \
|
||||
"${{ needs.prepare.outputs.version }}" \
|
||||
"${{ needs.prepare.outputs.tag }}"
|
||||
|
||||
@@ -274,6 +274,36 @@ lib/api/domains/ # API clients
|
||||
|
||||
Commits to `main` **must** follow [Conventional Commits](https://www.conventionalcommits.org/) (`type: description`). PRs are squash-merged — the PR title becomes the commit, validated by CI. Changelog is auto-generated from these via git-cliff (`cliff.toml`). See `.agents/skills/commit/SKILL.md` for allowed types and examples.
|
||||
|
||||
### Release & Versioning Convention
|
||||
|
||||
Kandev uses a **single SemVer** `X.Y.Z` shared across all distribution channels:
|
||||
|
||||
- `apps/cli/package.json` version → `X.Y.Z`
|
||||
- npm main package: `kandev@X.Y.Z`
|
||||
- npm runtime packages: `@kdlbs/runtime-{platform}@X.Y.Z` (5 platforms; declared as `optionalDependencies` in main package)
|
||||
- Git tag: `vX.Y.Z` (three-part; legacy `vM.m` tags normalize to `M.m.0`)
|
||||
- Homebrew formula: `kdlbs/homebrew-kandev` `Formula/kandev.rb` `version "X.Y.Z"`
|
||||
- GitHub release: `vX.Y.Z` with platform tarballs `kandev-{platform}.tar.gz` + `.sha256`
|
||||
|
||||
**npm and Homebrew are sibling channels**, not chained. Both consume the same GitHub release artifacts; neither depends on the other.
|
||||
|
||||
**Release flow** (entirely in CI via `.github/workflows/release.yml`, triggered by maintainer from GitHub Actions UI):
|
||||
|
||||
1. Maintainer clicks "Run workflow" → picks `bump` (patch/minor/major) → optional `dry_run`.
|
||||
2. `prepare` job bumps version + regenerates CHANGELOG, opens release PR, squash-merges, tags `vX.Y.Z`.
|
||||
3. `build-web` + `build-cli` + `build-bundles` (5 platforms) build the release artifacts.
|
||||
4. `publish-release` creates the GitHub release with platform tarballs + sha256 + auto-generated notes.
|
||||
5. `publish-npm` publishes 5 `@kdlbs/runtime-*` packages + main `kandev` package to npmjs.
|
||||
6. `update-homebrew-tap` pushes updated `Formula/kandev.rb` to `kdlbs/homebrew-kandev` via SSH deploy key.
|
||||
|
||||
There is no local release script — the entire flow runs in GHA.
|
||||
|
||||
**Runtime resolution** (in `apps/cli/src/runtime.ts`):
|
||||
|
||||
1. `KANDEV_BUNDLE_DIR` env var (set by Homebrew wrapper, used by tests).
|
||||
2. Installed `@kdlbs/runtime-{platform}` npm package via `require.resolve()`.
|
||||
3. `--runtime-version <tag>` cache fallback (debug only — downloads from GitHub).
|
||||
|
||||
### Code Quality (enforced by linters)
|
||||
|
||||
Static analysis runs in CI and pre-commit. New code **must** stay within these limits:
|
||||
|
||||
@@ -92,13 +92,35 @@ Each executor uses git worktrees for workspace isolation, preventing concurrent
|
||||
|
||||
## Quick Start
|
||||
|
||||
### NPX (recommended)
|
||||
### Homebrew (macOS, Linux)
|
||||
|
||||
```bash
|
||||
npx kandev
|
||||
brew install kdlbs/kandev/kandev
|
||||
kandev
|
||||
```
|
||||
|
||||
This downloads pre-built backend + frontend bundles and starts them locally. The worktrees and sqlite db will be created in `~/.kandev` by default. Should work on macOS, Linux, and Windows (WSL or native).
|
||||
### NPX
|
||||
|
||||
```bash
|
||||
npx kandev@latest
|
||||
```
|
||||
|
||||
Requires npm 7+. Installs the CLI plus a platform-matched runtime bundle in one step.
|
||||
|
||||
### NPM (global)
|
||||
|
||||
```bash
|
||||
npm install -g kandev@latest
|
||||
kandev
|
||||
```
|
||||
|
||||
### Updates
|
||||
|
||||
- `brew upgrade kandev`
|
||||
- `npx kandev@latest` (always uses the latest published version)
|
||||
- `npm install -g kandev@latest`
|
||||
|
||||
The worktrees and sqlite db live in `~/.kandev` by default. Works on macOS, Linux, and Windows (WSL or native).
|
||||
|
||||
### From Source
|
||||
|
||||
|
||||
+24
-6
@@ -4,18 +4,36 @@ Manage tasks. Orchestrate agents. Review changes. Ship value.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Homebrew
|
||||
|
||||
```bash
|
||||
npx kandev
|
||||
brew install kdlbs/kandev/kandev
|
||||
kandev
|
||||
```
|
||||
|
||||
Downloads the latest release, starts the backend + web app, and opens your browser. Data (worktrees, SQLite DB) is stored in `~/.kandev` by default.
|
||||
### NPX (requires npm 7+)
|
||||
|
||||
```bash
|
||||
npx kandev@latest
|
||||
```
|
||||
|
||||
Either install path resolves a platform-matched runtime (Go backend, agentctl, Next.js standalone web), launches the backend + web, and opens your browser. Data (worktrees, SQLite DB) is stored in `~/.kandev` by default.
|
||||
|
||||
## Version and Updates
|
||||
|
||||
- `npx kandev` (run mode) uses the latest GitHub release bundle by default.
|
||||
- At startup, the launcher prints the resolved release tag (for example: `[kandev] release: v0.2.3 (github latest)`).
|
||||
- Use `--version <tag>` to pin a specific runtime bundle release.
|
||||
- CLI package updates are checked from npm and shown as a prompt. They are not forced.
|
||||
The package manager owns the runtime version. `kandev@X.Y.Z` ships with the matching runtime.
|
||||
|
||||
- **Update via Homebrew**: `brew upgrade kandev`
|
||||
- **Update via npm/npx**: `npx kandev@latest` or `npm install -g kandev@latest`
|
||||
- **Print CLI version**: `kandev --version`
|
||||
|
||||
### Advanced: pin a specific runtime tag
|
||||
|
||||
`--runtime-version <tag>` downloads a specific GitHub release runtime instead of using the installed one. For debugging compatibility issues only:
|
||||
|
||||
```bash
|
||||
kandev --runtime-version v0.16.0
|
||||
```
|
||||
|
||||
## What You Get
|
||||
|
||||
|
||||
+124
-112
@@ -1,158 +1,170 @@
|
||||
# Kandev Launcher (npx)
|
||||
# Kandev CLI (internal docs)
|
||||
|
||||
## Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["npx kandev"] --> B["npm downloads kandev package"]
|
||||
B --> C["CLI parses arguments"]
|
||||
C --> D{"Command?"}
|
||||
A["kandev"] --> B["CLI parses arguments"]
|
||||
B --> C{"Command?"}
|
||||
|
||||
D -->|run| E["Fetch latest GitHub release"]
|
||||
D -->|dev| F["Use local repo"]
|
||||
D -->|start| G["Use local build"]
|
||||
C -->|run / default| D["resolveRuntime()"]
|
||||
C -->|dev| F["Use local repo"]
|
||||
C -->|start| G["Use local build"]
|
||||
|
||||
E --> E1["Download platform zip"]
|
||||
E1 --> E2["Extract to ~/.kandev/bin/"]
|
||||
E2 --> E3["Start backend + Next.js standalone"]
|
||||
D --> D1{"KANDEV_BUNDLE_DIR set?"}
|
||||
D1 -->|yes| D2["Use env bundle (Homebrew)"]
|
||||
D1 -->|no| D3["require.resolve('@kdlbs/runtime-{platform}')"]
|
||||
D3 -->|hit| D4["Use installed npm bundle"]
|
||||
D3 -->|miss| D5{"--runtime-version given?"}
|
||||
D5 -->|yes| D6["Download from GitHub Releases (cache)"]
|
||||
D5 -->|no| D7["Throw: 'No runtime found'"]
|
||||
|
||||
D2 --> E["Start backend + Next.js standalone"]
|
||||
D4 --> E
|
||||
D6 --> E
|
||||
|
||||
F --> F1["make dev + next dev"]
|
||||
|
||||
G --> G1["binary + next start"]
|
||||
```
|
||||
|
||||
## Overview
|
||||
|
||||
This package powers `npx kandev` by downloading prebuilt release bundles from GitHub Releases and running them locally. It:
|
||||
This package provides the `kandev` CLI launcher. The runtime bundle (Go backend, agentctl, Next.js standalone web) is **installed by the package manager** — there is no first-run download.
|
||||
|
||||
- Detects OS/arch and fetches the matching bundle ZIP.
|
||||
- Verifies the SHA256 checksum when available.
|
||||
- Extracts the bundle into `~/.kandev/bin/<version>/<platform>/`.
|
||||
- Starts the backend binary and waits for the `/health` endpoint.
|
||||
- Starts the Next.js standalone server with runtime `KANDEV_API_BASE_URL`.
|
||||
- Uses the latest GitHub Release by default, so runtime bundles update automatically.
|
||||
Two install channels share the same release artifacts:
|
||||
|
||||
It also supports local dev and production runs from a repo checkout.
|
||||
- **npm/npx**: `kandev@X.Y.Z` declares `optionalDependencies` for `@kdlbs/runtime-{platform}@X.Y.Z`. npm 7+ filters by `os`/`cpu` and installs only the matching one.
|
||||
- **Homebrew**: `kdlbs/homebrew-kandev` formula downloads the GitHub release tarball into the Cellar and sets `KANDEV_BUNDLE_DIR`.
|
||||
|
||||
Both channels resolve to the same runtime bundle layout (`bin/kandev`, `bin/agentctl`, `web/server.js`). The CLI launcher (`runtime.ts`) abstracts over them.
|
||||
|
||||
## Artifact shapes
|
||||
|
||||
The GitHub release bundle and the npm runtime package are **different shapes** because they serve different consumers:
|
||||
|
||||
```
|
||||
# GitHub release bundle (used by Homebrew + manual installs)
|
||||
kandev/
|
||||
├── bin/{kandev,agentctl}
|
||||
├── web/server.js
|
||||
└── cli/
|
||||
├── bin/cli.js # #!/usr/bin/env node + chmod +x
|
||||
└── dist/cli.bundle.js # esbuild single-file bundle (deps inlined)
|
||||
|
||||
# npm runtime package (@kdlbs/runtime-{platform})
|
||||
@kdlbs/runtime-{platform}/
|
||||
├── bin/{kandev,agentctl}
|
||||
└── web/server.js
|
||||
```
|
||||
|
||||
The `cli/` directory is only in the GitHub bundle (Homebrew needs the launcher). For npm installs, the main `kandev` package already provides the CLI; the runtime package only ships the binaries + web bundle.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
| ------------------------ | -------------------------------------------------------- |
|
||||
| `kandev` or `kandev run` | Download and run release bundles (default) |
|
||||
| `kandev` or `kandev run` | Run from installed runtime bundle (default) |
|
||||
| `kandev dev` | Run local repo with hot-reload (requires repo checkout) |
|
||||
| `kandev start` | Run local production build (requires `make build` first) |
|
||||
|
||||
## Updates
|
||||
|
||||
On `run`, the launcher checks npm for the latest `kandev` CLI version and prompts:
|
||||
|
||||
```
|
||||
Update available: <current> -> <latest>. Update now? [y/N]
|
||||
```
|
||||
|
||||
If you accept, it re-runs `npx kandev@latest` with the same arguments.
|
||||
This check is skipped in `dev` and `start` modes.
|
||||
|
||||
Note: the runtime bundles are pulled from the latest GitHub Release by default, even if the CLI version is unchanged. So:
|
||||
|
||||
- **New runtime release without CLI publish**: users get new runtime automatically, but no update prompt.
|
||||
- **New CLI publish**: users get an update prompt and then re-run with the new CLI.
|
||||
- On startup, `run` also prints the resolved runtime release tag.
|
||||
- Runtime assets are published atomically in CI (single final release upload job), so `/releases/latest`
|
||||
does not surface partially uploaded platform bundles.
|
||||
|
||||
You can disable the prompt with:
|
||||
|
||||
```bash
|
||||
KANDEV_NO_UPDATE_PROMPT=1 npx kandev
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Run the latest release bundle
|
||||
npx kandev
|
||||
|
||||
# Run a specific release tag
|
||||
npx kandev run --version v0.1.0
|
||||
|
||||
# Override ports
|
||||
npx kandev --backend-port 18080 --web-port 13000
|
||||
|
||||
# Local dev with hot-reload (from repo)
|
||||
npx kandev dev
|
||||
|
||||
# Local production build (from repo, after `make build`)
|
||||
npx kandev start
|
||||
|
||||
# Local test the built CLI (from repo root)
|
||||
pnpm -C apps/cli build
|
||||
pnpm -C apps/cli start
|
||||
```
|
||||
|
||||
## CLI Options
|
||||
|
||||
| Option | Description |
|
||||
| ----------------------- | ---------------------------------------- |
|
||||
| `run` | Use release bundles (default) |
|
||||
| `dev` | Use local repo with hot-reload |
|
||||
| `start` | Use local production build |
|
||||
| `--version <tag>` | Release tag to install (default: latest) |
|
||||
| `--backend-port <port>` | Override backend port |
|
||||
| `--web-port <port>` | Override web port |
|
||||
| `--help`, `-h` | Show help |
|
||||
| Option | Description |
|
||||
| -------------------------- | ---------------------------------------------------------- |
|
||||
| `--version`, `-V` | Print CLI version and exit |
|
||||
| `--port`, `--backend-port` | Backend port |
|
||||
| `--web-internal-port` | Override internal Next.js port |
|
||||
| `--verbose`, `-v` | Show info logs |
|
||||
| `--debug` | Show debug logs + agent message dumps |
|
||||
| `--runtime-version <tag>` | **Advanced/debug**: download a specific GitHub runtime tag |
|
||||
| `--help`, `-h` | Show help |
|
||||
|
||||
## Updates (package-manager owned)
|
||||
|
||||
The CLI no longer self-updates. Updates flow through the install channel:
|
||||
|
||||
```bash
|
||||
brew upgrade kandev # Homebrew
|
||||
npm install -g kandev@latest # npm global
|
||||
npx kandev@latest # always pulls latest
|
||||
```
|
||||
|
||||
There is no `KANDEV_NO_UPDATE_PROMPT` or `KANDEV_SKIP_UPDATE` env var anymore — they're gone with the self-updater.
|
||||
|
||||
## Bundling for Homebrew
|
||||
|
||||
The Homebrew bundle uses `dist/cli.bundle.js`, an esbuild single-file output that inlines `tree-kill`, `tar`, and other Node deps. Required because the Cellar has no `node_modules`. The npm package uses the unbundled `dist/cli.js` and resolves deps via npm.
|
||||
|
||||
```bash
|
||||
pnpm -C apps/cli build # produces dist/cli.js
|
||||
pnpm -C apps/cli bundle # produces dist/cli.bundle.js (~212KB)
|
||||
```
|
||||
|
||||
Smoke test the bundle in a clean directory:
|
||||
|
||||
```bash
|
||||
mkdir /tmp/smoke && cd /tmp/smoke
|
||||
node /path/to/apps/cli/dist/cli.bundle.js --help
|
||||
```
|
||||
|
||||
If `Cannot find module 'tree-kill'` appears, the bundle is broken.
|
||||
|
||||
## Local Development
|
||||
|
||||
```bash
|
||||
# Run CLI in dev mode
|
||||
# Run CLI in dev mode (uses tsx)
|
||||
pnpm -C apps/cli dev
|
||||
|
||||
# Run with arguments
|
||||
pnpm -C apps/cli dev -- start
|
||||
pnpm -C apps/cli dev -- --backend-port 9000
|
||||
pnpm -C apps/cli dev -- --port 9000
|
||||
```
|
||||
|
||||
## Build / Publish
|
||||
|
||||
```bash
|
||||
pnpm -C apps/cli build
|
||||
npm publish --access public
|
||||
```
|
||||
|
||||
The published package name is `kandev`, with a bin entry `kandev`.
|
||||
|
||||
## Release
|
||||
|
||||
```bash
|
||||
scripts/release/release.sh
|
||||
```
|
||||
Releases run entirely in GitHub Actions. From the GHA UI:
|
||||
|
||||
The unified release script supports `cli`, `app`, or `both` in one flow.
|
||||
Versioning strategy:
|
||||
1. Open the **Release** workflow.
|
||||
2. Click **Run workflow**.
|
||||
3. Choose `bump` (patch / minor / major) from the dropdown.
|
||||
4. Optionally tick `dry_run` to validate without publishing.
|
||||
5. Click **Run workflow**.
|
||||
|
||||
- App release tags: `vM.m`
|
||||
- CLI npm version: `M.m.0`
|
||||
- Default bump mode: `minor`
|
||||
The workflow does everything: version bump, CHANGELOG, PR, merge, tag, build, npm publish, Homebrew tap update. See [/.github/workflows/release.yml](../../.github/workflows/release.yml).
|
||||
|
||||
Single SemVer flow: `apps/cli/package.json` version, git tag, npm packages, and Homebrew formula are all bumped to the same `X.Y.Z`.
|
||||
|
||||
Versioning:
|
||||
|
||||
- One SemVer `X.Y.Z` for everything (npm, GitHub tag, Homebrew formula)
|
||||
- Git tag format: `vX.Y.Z`
|
||||
- Legacy `vM.m` tags normalized to `M.m.0` during migration; new releases always use `vX.Y.Z`
|
||||
|
||||
## Environment Overrides
|
||||
|
||||
| Variable | Description |
|
||||
| --------------------------- | ---------------------------------------------- |
|
||||
| `KANDEV_GITHUB_OWNER` | Override GitHub repo owner (default: `kandev`) |
|
||||
| `KANDEV_GITHUB_REPO` | Override GitHub repo name (default: `kandev`) |
|
||||
| `KANDEV_GITHUB_TOKEN` | Optional token for GitHub API rate limits |
|
||||
| `KANDEV_NO_UPDATE_PROMPT=1` | Disable the CLI update prompt |
|
||||
| `KANDEV_SKIP_UPDATE=1` | Internal guard to avoid update loops |
|
||||
| `KANDEV_HEALTH_TIMEOUT_MS` | Override health check timeout in milliseconds |
|
||||
| Variable | Description |
|
||||
| ------------------------------------- | --------------------------------------------------------------------- |
|
||||
| `KANDEV_BUNDLE_DIR` | Force runtime bundle location (set by Homebrew wrapper, tests) |
|
||||
| `KANDEV_PORT` / `KANDEV_BACKEND_PORT` | Backend port |
|
||||
| `KANDEV_WEB_PORT` | Internal Next.js port |
|
||||
| `KANDEV_HEALTH_TIMEOUT_MS` | Override health check timeout (ms) |
|
||||
| `KANDEV_GITHUB_OWNER` | Override GitHub repo owner (default: `kdlbs`) for `--runtime-version` |
|
||||
| `KANDEV_GITHUB_REPO` | Override GitHub repo name (default: `kandev`) for `--runtime-version` |
|
||||
| `KANDEV_GITHUB_TOKEN` | Optional GitHub token for rate limits |
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
| Platform | Asset Name |
|
||||
| --------------------- | ------------------------------------------- |
|
||||
| macOS (Apple Silicon) | `kandev-macos-arm64.zip` |
|
||||
| macOS (Intel) | `kandev-macos-x64.zip` |
|
||||
| Linux (x64) | `kandev-linux-x64.zip` |
|
||||
| Linux (ARM64) | `kandev-linux-arm64.zip` |
|
||||
| Windows (x64) | `kandev-windows-x64.zip` |
|
||||
| Windows (ARM64) | Falls back to `windows-x64` (x64 emulation) |
|
||||
| Platform | npm runtime package | GitHub asset name |
|
||||
| --------------------- | ------------------------------------------- | --------------------------- |
|
||||
| macOS (Apple Silicon) | `@kdlbs/runtime-darwin-arm64` | `kandev-macos-arm64.tar.gz` |
|
||||
| macOS (Intel) | `@kdlbs/runtime-darwin-x64` | `kandev-macos-x64.tar.gz` |
|
||||
| Linux (x64) | `@kdlbs/runtime-linux-x64` | `kandev-linux-x64.tar.gz` |
|
||||
| Linux (ARM64) | `@kdlbs/runtime-linux-arm64` | `kandev-linux-arm64.tar.gz` |
|
||||
| Windows (x64) | `@kdlbs/runtime-win32-x64` | `kandev-windows-x64.tar.gz` |
|
||||
| Windows (ARM64) | Falls back to `windows-x64` (x64 emulation) | |
|
||||
|
||||
Note: `platform.ts` uses internal naming (`macos`, `windows`); npm `os` field uses npm conventions (`darwin`, `win32`). `runtime.ts` maps between the two.
|
||||
|
||||
## npm requirements
|
||||
|
||||
The optional-dependency runtime resolution requires **npm 7 or newer**. npm 6 silently skips optional deps during `npx`, which would leave users with no runtime. `package.json` declares `engines.npm: ">=7"` to surface this at install time as a clear error.
|
||||
|
||||
+13
-1
@@ -2,7 +2,7 @@
|
||||
"name": "kandev",
|
||||
"version": "0.17.0",
|
||||
"private": false,
|
||||
"description": "NPX launcher for Kandev",
|
||||
"description": "Launcher for Kandev — manage tasks, orchestrate agents, review changes, and ship value",
|
||||
"license": "AGPL-3.0-only",
|
||||
"type": "commonjs",
|
||||
"bin": {
|
||||
@@ -13,12 +13,23 @@
|
||||
"bin",
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"npm": ">=7"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@kdlbs/runtime-linux-x64": "0.0.0-bootstrap",
|
||||
"@kdlbs/runtime-linux-arm64": "0.0.0-bootstrap",
|
||||
"@kdlbs/runtime-darwin-x64": "0.0.0-bootstrap",
|
||||
"@kdlbs/runtime-darwin-arm64": "0.0.0-bootstrap",
|
||||
"@kdlbs/runtime-win32-x64": "0.0.0-bootstrap"
|
||||
},
|
||||
"dependencies": {
|
||||
"tar": "^7.5.11",
|
||||
"tree-kill": "^1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"esbuild": "^0.24.0",
|
||||
"tsx": "^4.15.7",
|
||||
"typescript": "^5",
|
||||
"vitest": "^1.6.0"
|
||||
@@ -26,6 +37,7 @@
|
||||
"scripts": {
|
||||
"dev": "unset npm_config_prefix && tsx src/cli.ts",
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"bundle": "esbuild dist/cli.js --bundle --platform=node --format=cjs --outfile=dist/cli.bundle.js",
|
||||
"start": "node dist/cli.js",
|
||||
"test": "vitest run",
|
||||
"prepublishOnly": "pnpm build"
|
||||
|
||||
@@ -66,8 +66,29 @@ describe("parseArgs", () => {
|
||||
expect(() => parseArgs(["--port="])).toThrow(/--port value must be an integer/);
|
||||
});
|
||||
|
||||
it("throws ParseError on empty --version=", () => {
|
||||
expect(() => parseArgs(["--version="])).toThrow(/--version requires a value/);
|
||||
it("sets showVersion for --version and -V flags", () => {
|
||||
expect(parseArgs(["--version"]).options.showVersion).toBe(true);
|
||||
expect(parseArgs(["-V"]).options.showVersion).toBe(true);
|
||||
});
|
||||
|
||||
it("parses --runtime-version as runtimeVersion", () => {
|
||||
expect(parseArgs(["--runtime-version", "v0.16.0"]).options.runtimeVersion).toBe("v0.16.0");
|
||||
expect(parseArgs(["--runtime-version=v0.16.0"]).options.runtimeVersion).toBe("v0.16.0");
|
||||
});
|
||||
|
||||
it("throws ParseError on empty --runtime-version=", () => {
|
||||
expect(() => parseArgs(["--runtime-version="])).toThrow(/--runtime-version requires a value/);
|
||||
});
|
||||
|
||||
it("throws ParseError when --runtime-version has no value", () => {
|
||||
expect(() => parseArgs(["--runtime-version"])).toThrow(ParseError);
|
||||
expect(() => parseArgs(["--runtime-version"])).toThrow(/--runtime-version requires a value/);
|
||||
});
|
||||
|
||||
it("throws ParseError when the next token after --runtime-version is another flag", () => {
|
||||
expect(() => parseArgs(["--runtime-version", "--debug"])).toThrow(
|
||||
/--runtime-version requires a value/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+12
-7
@@ -2,11 +2,12 @@ export type Command = "run" | "dev" | "start";
|
||||
|
||||
export type CliOptions = {
|
||||
command: Command;
|
||||
version?: string;
|
||||
runtimeVersion?: string;
|
||||
backendPort?: number;
|
||||
webPort?: number;
|
||||
verbose?: boolean;
|
||||
debug?: boolean;
|
||||
showVersion?: boolean;
|
||||
};
|
||||
|
||||
export type ParseResult = {
|
||||
@@ -31,19 +32,23 @@ export function parseArgs(argv: string[]): ParseResult {
|
||||
showHelp = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--version" || arg === "-V") {
|
||||
opts.showVersion = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "dev" || arg === "run" || arg === "start") {
|
||||
opts.command = arg;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--version") {
|
||||
opts.version = takeValue(argv, i, "--version");
|
||||
if (arg === "--runtime-version") {
|
||||
opts.runtimeVersion = takeValue(argv, i, "--runtime-version");
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg.startsWith("--version=")) {
|
||||
const value = arg.slice("--version=".length);
|
||||
if (value.length === 0) throw new ParseError("--version requires a value");
|
||||
opts.version = value;
|
||||
if (arg.startsWith("--runtime-version=")) {
|
||||
const value = arg.slice("--runtime-version=".length);
|
||||
if (value.length === 0) throw new ParseError("--runtime-version requires a value");
|
||||
opts.runtimeVersion = value;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--dev") {
|
||||
|
||||
+20
-13
@@ -7,16 +7,15 @@ import { runDev } from "./dev";
|
||||
import { runRelease } from "./run";
|
||||
import { runStart } from "./start";
|
||||
import { ensureValidPort } from "./ports";
|
||||
import { maybePromptForUpdate } from "./update";
|
||||
|
||||
function printHelp() {
|
||||
console.log(`kandev launcher
|
||||
|
||||
Usage:
|
||||
kandev run [--version <tag>] [--port <port>] [--verbose] [--debug]
|
||||
kandev run [--port <port>] [--verbose] [--debug]
|
||||
kandev dev [--port <port>]
|
||||
kandev start [--port <port>] [--verbose] [--debug]
|
||||
kandev [--version <tag>] [--port <port>] [--verbose] [--debug]
|
||||
kandev [--port <port>] [--verbose] [--debug]
|
||||
kandev --dev [--port <port>]
|
||||
|
||||
Examples:
|
||||
@@ -25,16 +24,16 @@ Examples:
|
||||
kandev --dev
|
||||
kandev dev
|
||||
kandev start
|
||||
kandev --version v0.1.0
|
||||
kandev --version
|
||||
kandev --port 3000
|
||||
kandev --debug
|
||||
|
||||
Options:
|
||||
dev Use local repo for dev (make dev + next dev) if available.
|
||||
start Use local production build (make build + next start).
|
||||
run Use release bundles (default).
|
||||
run Use installed runtime bundle (default).
|
||||
--dev Alias for "dev".
|
||||
--version Release tag to install (default: latest).
|
||||
--version, -V Print CLI version and exit.
|
||||
--port Port for the Go backend (the URL kandev opens on in
|
||||
start/run). Alias for --backend-port. Also reads
|
||||
KANDEV_PORT or KANDEV_BACKEND_PORT.
|
||||
@@ -43,11 +42,14 @@ Options:
|
||||
--help, -h Show help.
|
||||
|
||||
Advanced:
|
||||
--backend-port Same as --port.
|
||||
--web-internal-port Override the internal Next.js port. The Go backend
|
||||
reverse-proxies to it; users hit the backend port.
|
||||
Also reads KANDEV_WEB_PORT.
|
||||
--web-port Deprecated alias for --web-internal-port.
|
||||
--backend-port Same as --port.
|
||||
--web-internal-port Override the internal Next.js port. The Go backend
|
||||
reverse-proxies to it; users hit the backend port.
|
||||
Also reads KANDEV_WEB_PORT.
|
||||
--web-port Deprecated alias for --web-internal-port.
|
||||
--runtime-version <tag> Download and use a specific release tag instead of
|
||||
the installed runtime. For debugging only.
|
||||
Example: kandev --runtime-version v0.16.0
|
||||
`);
|
||||
}
|
||||
|
||||
@@ -76,6 +78,12 @@ function findRepoRoot(startDir: string): string | null {
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const { options, showHelp, deprecatedFlags } = parseArgs(process.argv.slice(2));
|
||||
|
||||
if (options.showVersion) {
|
||||
console.log(pkg.version);
|
||||
return;
|
||||
}
|
||||
|
||||
if (showHelp) {
|
||||
printHelp();
|
||||
return;
|
||||
@@ -113,9 +121,8 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
await maybePromptForUpdate(pkg.version, process.argv.slice(2));
|
||||
await runRelease({
|
||||
version: options.version,
|
||||
runtimeVersion: options.runtimeVersion,
|
||||
backendPort,
|
||||
webPort,
|
||||
verbose: options.verbose,
|
||||
|
||||
+76
-71
@@ -17,11 +17,12 @@ import { getBinaryName, getPlatformDir } from "./platform";
|
||||
import { sortVersionsDesc } from "./version";
|
||||
import { pickAvailablePort } from "./ports";
|
||||
import { createProcessSupervisor } from "./process";
|
||||
import { resolveRuntime, validateBundle } from "./runtime";
|
||||
import { attachBackendExitHandler, logStartupInfo } from "./shared";
|
||||
import { launchWebApp, openBrowser } from "./web";
|
||||
|
||||
export type RunOptions = {
|
||||
version?: string;
|
||||
runtimeVersion?: string;
|
||||
backendPort?: number;
|
||||
webPort?: number;
|
||||
/** Show info logs from backend + web */
|
||||
@@ -30,14 +31,13 @@ export type RunOptions = {
|
||||
debug?: boolean;
|
||||
};
|
||||
|
||||
type PreparedRelease = {
|
||||
type PreparedBundle = {
|
||||
bundleDir: string;
|
||||
backendBin: string;
|
||||
backendUrl: string;
|
||||
backendEnv: NodeJS.ProcessEnv;
|
||||
webEnv: NodeJS.ProcessEnv;
|
||||
releaseTag: string;
|
||||
requestedVersion?: string;
|
||||
webPort: number;
|
||||
agentctlPort: number;
|
||||
dbPath: string;
|
||||
@@ -109,60 +109,71 @@ export function cleanOldReleases(currentTag: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function prepareReleaseBundle({
|
||||
version,
|
||||
/**
|
||||
* Download a specific release version into the local cache.
|
||||
* Only used when --runtime-version is given explicitly.
|
||||
*/
|
||||
async function downloadRuntimeVersion(runtimeVersion: string): Promise<string> {
|
||||
const platformDir = getPlatformDir();
|
||||
const release = await getRelease(runtimeVersion);
|
||||
const tag = release.tag_name;
|
||||
const assetName = `kandev-${platformDir}.tar.gz`;
|
||||
const cacheDir = path.join(CACHE_DIR, tag, platformDir);
|
||||
|
||||
const archivePath = await ensureAsset(tag, assetName, cacheDir, (downloaded, total) => {
|
||||
const percent = total ? Math.round((downloaded / total) * 100) : 0;
|
||||
const mb = (downloaded / (1024 * 1024)).toFixed(1);
|
||||
const totalMb = total ? (total / (1024 * 1024)).toFixed(1) : "?";
|
||||
process.stderr.write(`\r Downloading: ${mb}MB / ${totalMb}MB (${percent}%)`);
|
||||
});
|
||||
process.stderr.write("\n");
|
||||
|
||||
ensureExtracted(archivePath, cacheDir);
|
||||
cleanOldReleases(tag);
|
||||
return tag;
|
||||
}
|
||||
|
||||
async function prepareBundleForLaunch({
|
||||
runtimeVersion,
|
||||
backendPort,
|
||||
webPort,
|
||||
verbose = false,
|
||||
debug = false,
|
||||
}: RunOptions): Promise<PreparedRelease> {
|
||||
const platformDir = getPlatformDir();
|
||||
let tag: string;
|
||||
let cacheDir: string;
|
||||
}: RunOptions): Promise<PreparedBundle> {
|
||||
let bundleDir: string;
|
||||
let releaseTag: string;
|
||||
|
||||
try {
|
||||
const release = await getRelease(version);
|
||||
tag = release.tag_name;
|
||||
const assetName = `kandev-${platformDir}.tar.gz`;
|
||||
cacheDir = path.join(CACHE_DIR, tag, platformDir);
|
||||
|
||||
const archivePath = await ensureAsset(tag, assetName, cacheDir, (downloaded, total) => {
|
||||
const percent = total ? Math.round((downloaded / total) * 100) : 0;
|
||||
const mb = (downloaded / (1024 * 1024)).toFixed(1);
|
||||
const totalMb = total ? (total / (1024 * 1024)).toFixed(1) : "?";
|
||||
process.stderr.write(`\r Downloading: ${mb}MB / ${totalMb}MB (${percent}%)`);
|
||||
});
|
||||
process.stderr.write("\n");
|
||||
|
||||
ensureExtracted(archivePath, cacheDir);
|
||||
cleanOldReleases(tag);
|
||||
} catch (err) {
|
||||
// GitHub unreachable — try to launch from cache.
|
||||
const cached = findCachedRelease(platformDir, version);
|
||||
if (!cached) {
|
||||
const target = version ? `version ${version}` : "latest version";
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(
|
||||
`Failed to fetch ${target} and no cached release found.\n` +
|
||||
` Reason: ${reason}\n` +
|
||||
` Run kandev once while online to cache a release for offline use.`,
|
||||
);
|
||||
if (runtimeVersion) {
|
||||
// Explicit version: ensure it is in the cache (downloading if needed), then resolve.
|
||||
const platformDir = getPlatformDir();
|
||||
const cached = findCachedRelease(platformDir, runtimeVersion);
|
||||
let tag: string;
|
||||
if (cached) {
|
||||
tag = cached.tag;
|
||||
bundleDir = findBundleRoot(cached.cacheDir);
|
||||
} else {
|
||||
try {
|
||||
tag = await downloadRuntimeVersion(runtimeVersion);
|
||||
const cacheDir = path.join(CACHE_DIR, tag, platformDir);
|
||||
bundleDir = findBundleRoot(cacheDir);
|
||||
} catch (err) {
|
||||
const reason = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(
|
||||
`Failed to fetch runtime version ${runtimeVersion}.\n` +
|
||||
` Reason: ${reason}\n` +
|
||||
` Run kandev once while online to cache a release for offline use.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
tag = cached.tag;
|
||||
cacheDir = cached.cacheDir;
|
||||
process.stderr.write(`[kandev] GitHub unreachable — using cached release ${tag}\n`);
|
||||
}
|
||||
|
||||
const bundleDir = findBundleRoot(cacheDir);
|
||||
|
||||
const backendBin = path.join(bundleDir, "bin", getBinaryName("kandev"));
|
||||
if (!fs.existsSync(backendBin)) {
|
||||
throw new Error(`Backend binary not found at ${backendBin}`);
|
||||
}
|
||||
|
||||
const agentctlBin = path.join(bundleDir, "bin", getBinaryName("agentctl"));
|
||||
if (!fs.existsSync(agentctlBin)) {
|
||||
throw new Error(`agentctl binary not found at ${agentctlBin}`);
|
||||
// Validate the resolved bundle has all required binaries before launching.
|
||||
validateBundle(bundleDir);
|
||||
releaseTag = tag;
|
||||
} else {
|
||||
// Default path: resolve from KANDEV_BUNDLE_DIR or installed npm runtime package.
|
||||
const resolved = resolveRuntime();
|
||||
bundleDir = resolved.bundleDir;
|
||||
// Use KANDEV_VERSION if set (e.g. by Homebrew wrapper), otherwise show source.
|
||||
releaseTag = process.env.KANDEV_VERSION ?? `(${resolved.source})`;
|
||||
}
|
||||
|
||||
const actualBackendPort = backendPort ?? (await pickAvailablePort(DEFAULT_BACKEND_PORT));
|
||||
@@ -176,10 +187,8 @@ async function prepareReleaseBundle({
|
||||
fs.mkdirSync(DATA_DIR, { recursive: true });
|
||||
const dbPath = path.join(DATA_DIR, "kandev.db");
|
||||
|
||||
// Note: Release mode doesn't configure MCP server ports as it uses
|
||||
// the bundled configuration. Only backend and agentctl ports are set.
|
||||
// Log level defaults to warn for clean output and can be overridden
|
||||
// via KANDEV_LOG_LEVEL or --verbose/--debug flags.
|
||||
const backendBin = path.join(bundleDir, "bin", getBinaryName("kandev"));
|
||||
|
||||
const backendEnv: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
KANDEV_SERVER_PORT: String(actualBackendPort),
|
||||
@@ -194,8 +203,6 @@ async function prepareReleaseBundle({
|
||||
...process.env,
|
||||
KANDEV_API_BASE_URL: backendUrl,
|
||||
PORT: String(actualWebPort),
|
||||
// Ensure Next.js standalone server binds to 127.0.0.1 so localhost health checks work.
|
||||
// Without this, HOSTNAME from the host environment can cause binding issues.
|
||||
HOSTNAME: "127.0.0.1",
|
||||
};
|
||||
(webEnv as Record<string, string>).NODE_ENV = "production";
|
||||
@@ -206,8 +213,7 @@ async function prepareReleaseBundle({
|
||||
backendUrl,
|
||||
backendEnv,
|
||||
webEnv,
|
||||
releaseTag: tag,
|
||||
requestedVersion: version,
|
||||
releaseTag,
|
||||
webPort: actualWebPort,
|
||||
agentctlPort,
|
||||
dbPath,
|
||||
@@ -235,17 +241,14 @@ export function attachRingBuffer(
|
||||
return () => buf;
|
||||
}
|
||||
|
||||
function launchReleaseApps(prepared: PreparedRelease): {
|
||||
function launchBundle(prepared: PreparedBundle): {
|
||||
supervisor: ReturnType<typeof createProcessSupervisor>;
|
||||
backendProc: ReturnType<typeof spawn>;
|
||||
webServerPath: string;
|
||||
dumpBackendLogs: () => void;
|
||||
} {
|
||||
const releaseSource = prepared.requestedVersion
|
||||
? `(requested: ${prepared.requestedVersion})`
|
||||
: "(github latest)";
|
||||
logStartupInfo({
|
||||
header: `release: ${prepared.releaseTag} ${releaseSource}`,
|
||||
header: `release: ${prepared.releaseTag}`,
|
||||
ports: {
|
||||
backendPort: Number(prepared.backendEnv.KANDEV_SERVER_PORT),
|
||||
webPort: prepared.webPort,
|
||||
@@ -259,10 +262,6 @@ function launchReleaseApps(prepared: PreparedRelease): {
|
||||
const supervisor = createProcessSupervisor();
|
||||
supervisor.attachSignalHandlers();
|
||||
|
||||
// Start backend: ignore stdin, always show stderr immediately.
|
||||
// In verbose/debug mode stream stdout live; otherwise capture it into a ring
|
||||
// buffer so we can dump the last few KB if the healthcheck fails (users were
|
||||
// previously seeing opaque "timed out" errors with no backend context).
|
||||
const backendProc = spawn(prepared.backendBin, [], {
|
||||
cwd: path.dirname(prepared.backendBin),
|
||||
env: prepared.backendEnv,
|
||||
@@ -293,14 +292,20 @@ function launchReleaseApps(prepared: PreparedRelease): {
|
||||
}
|
||||
|
||||
export async function runRelease({
|
||||
version,
|
||||
runtimeVersion,
|
||||
backendPort,
|
||||
webPort,
|
||||
verbose = false,
|
||||
debug = false,
|
||||
}: RunOptions): Promise<void> {
|
||||
const prepared = await prepareReleaseBundle({ version, backendPort, webPort, verbose, debug });
|
||||
const { supervisor, backendProc, webServerPath, dumpBackendLogs } = launchReleaseApps(prepared);
|
||||
const prepared = await prepareBundleForLaunch({
|
||||
runtimeVersion,
|
||||
backendPort,
|
||||
webPort,
|
||||
verbose,
|
||||
debug,
|
||||
});
|
||||
const { supervisor, backendProc, webServerPath, dumpBackendLogs } = launchBundle(prepared);
|
||||
const healthTimeoutMs = resolveHealthTimeoutMs(HEALTH_TIMEOUT_MS_RELEASE);
|
||||
console.log("[kandev] starting backend...");
|
||||
await waitForHealth(prepared.backendUrl, backendProc, healthTimeoutMs, dumpBackendLogs);
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import fs from "node:fs";
|
||||
import Module from "node:module";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { resolveRuntime, validateBundle } from "./runtime";
|
||||
|
||||
// Keep getPlatformDir deterministic across test environments.
|
||||
vi.mock("./platform", () => ({
|
||||
getPlatformDir: () => "macos-arm64" as const,
|
||||
getBinaryName: (base: string) => base,
|
||||
}));
|
||||
|
||||
function createFakeBundle(dir: string) {
|
||||
fs.mkdirSync(path.join(dir, "bin"), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "bin", "kandev"), "fake");
|
||||
fs.writeFileSync(path.join(dir, "bin", "agentctl"), "fake");
|
||||
fs.mkdirSync(path.join(dir, "web"), { recursive: true });
|
||||
fs.writeFileSync(path.join(dir, "web", "server.js"), "fake");
|
||||
}
|
||||
|
||||
describe("validateBundle", () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kandev-validate-"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("passes for a complete bundle", () => {
|
||||
createFakeBundle(tmpDir);
|
||||
expect(() => validateBundle(tmpDir)).not.toThrow();
|
||||
});
|
||||
|
||||
it("throws when backend binary is missing", () => {
|
||||
fs.mkdirSync(path.join(tmpDir, "bin"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, "bin", "agentctl"), "fake");
|
||||
fs.mkdirSync(path.join(tmpDir, "web"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, "web", "server.js"), "fake");
|
||||
expect(() => validateBundle(tmpDir)).toThrow(/Backend binary not found/);
|
||||
});
|
||||
|
||||
it("throws when agentctl is missing", () => {
|
||||
fs.mkdirSync(path.join(tmpDir, "bin"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, "bin", "kandev"), "fake");
|
||||
fs.mkdirSync(path.join(tmpDir, "web"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, "web", "server.js"), "fake");
|
||||
expect(() => validateBundle(tmpDir)).toThrow(/agentctl binary not found/);
|
||||
});
|
||||
|
||||
it("throws when web server.js is missing", () => {
|
||||
fs.mkdirSync(path.join(tmpDir, "bin"), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, "bin", "kandev"), "fake");
|
||||
fs.writeFileSync(path.join(tmpDir, "bin", "agentctl"), "fake");
|
||||
expect(() => validateBundle(tmpDir)).toThrow(/Web server.*not found/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRuntime", () => {
|
||||
let tmpDir: string;
|
||||
const origEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "kandev-runtime-"));
|
||||
// Clear relevant env vars before each test
|
||||
delete process.env.KANDEV_BUNDLE_DIR;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
// Restore the original env exactly. Don't `delete process.env.KANDEV_BUNDLE_DIR`
|
||||
// afterwards — if a developer had that env var set when the suite started,
|
||||
// it's already in origEnv and should remain.
|
||||
process.env = { ...origEnv };
|
||||
});
|
||||
|
||||
describe("KANDEV_BUNDLE_DIR (step 1)", () => {
|
||||
it("resolves from KANDEV_BUNDLE_DIR when set and valid", () => {
|
||||
const bundleDir = path.join(tmpDir, "bundle");
|
||||
createFakeBundle(bundleDir);
|
||||
process.env.KANDEV_BUNDLE_DIR = bundleDir;
|
||||
|
||||
const result = resolveRuntime();
|
||||
expect(result.source).toBe("env");
|
||||
expect(result.bundleDir).toBe(bundleDir);
|
||||
});
|
||||
|
||||
it("throws when KANDEV_BUNDLE_DIR points to an invalid bundle", () => {
|
||||
const bundleDir = path.join(tmpDir, "empty-bundle");
|
||||
fs.mkdirSync(bundleDir);
|
||||
process.env.KANDEV_BUNDLE_DIR = bundleDir;
|
||||
|
||||
expect(() => resolveRuntime()).toThrow(/Backend binary not found/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("installed npm runtime package (step 2)", () => {
|
||||
// Step 2 normally relies on require.resolve('@kdlbs/runtime-darwin-arm64')
|
||||
// succeeding because the npm package is installed alongside the kandev
|
||||
// package. In this test environment that package isn't present, so we
|
||||
// intercept Node's module resolver to point at a fake bundle inside tmpDir.
|
||||
function mockResolve(target: string): void {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ModuleAny = Module as unknown as { _resolveFilename: any };
|
||||
vi.spyOn(ModuleAny, "_resolveFilename").mockImplementationOnce(() => target);
|
||||
}
|
||||
|
||||
it("resolves from the installed npm runtime package", () => {
|
||||
const pkgRoot = path.join(tmpDir, "npm-runtime");
|
||||
createFakeBundle(pkgRoot);
|
||||
// require.resolve('<pkg>/package.json') normally returns the path to the
|
||||
// package.json inside node_modules. Point it at our fake bundle instead.
|
||||
const pkgJsonPath = path.join(pkgRoot, "package.json");
|
||||
fs.writeFileSync(pkgJsonPath, "{}");
|
||||
mockResolve(pkgJsonPath);
|
||||
|
||||
const result = resolveRuntime();
|
||||
expect(result.source).toBe("npm");
|
||||
expect(result.bundleDir).toBe(pkgRoot);
|
||||
});
|
||||
|
||||
it("surfaces validateBundle errors when the npm package is installed but corrupt", () => {
|
||||
const pkgRoot = path.join(tmpDir, "broken-npm-runtime");
|
||||
fs.mkdirSync(pkgRoot);
|
||||
// package.json present (so require.resolve succeeds), but no bin/ or web/
|
||||
const pkgJsonPath = path.join(pkgRoot, "package.json");
|
||||
fs.writeFileSync(pkgJsonPath, "{}");
|
||||
mockResolve(pkgJsonPath);
|
||||
|
||||
expect(() => resolveRuntime()).toThrow(/Backend binary not found/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("no runtime found", () => {
|
||||
it("throws an actionable error message mentioning install paths", () => {
|
||||
let error: Error | null = null;
|
||||
try {
|
||||
resolveRuntime();
|
||||
} catch (e) {
|
||||
error = e as Error;
|
||||
}
|
||||
expect(error).not.toBeNull();
|
||||
expect(error!.message).toMatch(/npx kandev@latest/);
|
||||
expect(error!.message).toMatch(/brew install/);
|
||||
expect(error!.message).toMatch(/--runtime-version/);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import path from "node:path";
|
||||
import fs from "node:fs";
|
||||
|
||||
import { resolveWebServerPath } from "./bundle";
|
||||
import { getBinaryName, getPlatformDir, type PlatformDir } from "./platform";
|
||||
|
||||
const PLATFORM_TO_NPM_PACKAGE: Record<PlatformDir, string> = {
|
||||
"linux-x64": "@kdlbs/runtime-linux-x64",
|
||||
"linux-arm64": "@kdlbs/runtime-linux-arm64",
|
||||
"macos-x64": "@kdlbs/runtime-darwin-x64",
|
||||
"macos-arm64": "@kdlbs/runtime-darwin-arm64",
|
||||
"windows-x64": "@kdlbs/runtime-win32-x64",
|
||||
};
|
||||
|
||||
export type RuntimeSource = "env" | "npm";
|
||||
|
||||
export type ResolvedRuntime = {
|
||||
bundleDir: string;
|
||||
source: RuntimeSource;
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve the runtime bundle directory using a two-step priority chain:
|
||||
*
|
||||
* 1. KANDEV_BUNDLE_DIR env var — set by the Homebrew formula wrapper and
|
||||
* useful for local testing. Skips all other resolution.
|
||||
* 2. Installed npm runtime package — looks for @kdlbs/runtime-{platform}
|
||||
* in node_modules via Node module resolution. Works after
|
||||
* `npx kandev@latest` or `npm install -g kandev` (requires npm 7+).
|
||||
*
|
||||
* The explicit `--runtime-version <tag>` download path is handled directly
|
||||
* in run.ts (which manages the GitHub download + cache itself); it does
|
||||
* not flow through this function.
|
||||
*
|
||||
* Throws with an actionable error message if no runtime is found.
|
||||
*/
|
||||
export function resolveRuntime(): ResolvedRuntime {
|
||||
const envBundleDir = process.env.KANDEV_BUNDLE_DIR;
|
||||
if (envBundleDir) {
|
||||
validateBundle(envBundleDir);
|
||||
return { bundleDir: envBundleDir, source: "env" };
|
||||
}
|
||||
|
||||
const platformDir = getPlatformDir();
|
||||
const packageName = PLATFORM_TO_NPM_PACKAGE[platformDir];
|
||||
let pkgJsonPath: string | null = null;
|
||||
try {
|
||||
pkgJsonPath = require.resolve(`${packageName}/package.json`);
|
||||
} catch {
|
||||
// MODULE_NOT_FOUND — npm runtime package is not installed. Fall through
|
||||
// to the actionable error below.
|
||||
}
|
||||
if (pkgJsonPath) {
|
||||
// The package IS installed. If validateBundle throws here, the bundle is
|
||||
// present but corrupt — surface the error rather than the generic
|
||||
// "no runtime found" message below.
|
||||
const packageRoot = path.dirname(pkgJsonPath);
|
||||
validateBundle(packageRoot);
|
||||
return { bundleDir: packageRoot, source: "npm" };
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`No Kandev runtime found for ${platformDir}.\n` +
|
||||
` Install via npm (requires npm 7+): npx kandev@latest\n` +
|
||||
` Install via Homebrew: brew install kdlbs/kandev/kandev\n` +
|
||||
` Download a specific version (debug): kandev --runtime-version <tag>`,
|
||||
);
|
||||
}
|
||||
|
||||
export function validateBundle(bundleDir: string): void {
|
||||
const backendBin = path.join(bundleDir, "bin", getBinaryName("kandev"));
|
||||
if (!fs.existsSync(backendBin)) {
|
||||
throw new Error(`Backend binary not found in bundle at ${bundleDir}`);
|
||||
}
|
||||
const agentctlBin = path.join(bundleDir, "bin", getBinaryName("agentctl"));
|
||||
if (!fs.existsSync(agentctlBin)) {
|
||||
throw new Error(`agentctl binary not found in bundle at ${bundleDir}`);
|
||||
}
|
||||
const webServerPath = resolveWebServerPath(bundleDir);
|
||||
if (!webServerPath) {
|
||||
throw new Error(`Web server (server.js) not found in bundle at ${bundleDir}`);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import https from "node:https";
|
||||
import readline from "node:readline";
|
||||
|
||||
import { compareVersions } from "./version";
|
||||
|
||||
function requestJson<T>(url: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.get(url, { headers: { "User-Agent": "kandev-npx" } }, (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
return reject(new Error(`HTTP ${res.statusCode} fetching ${url}`));
|
||||
}
|
||||
let body = "";
|
||||
res.on("data", (chunk) => (body += chunk));
|
||||
res.on("end", () => {
|
||||
try {
|
||||
resolve(JSON.parse(body) as T);
|
||||
} catch {
|
||||
reject(new Error(`Failed to parse JSON from ${url}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.setTimeout(5000, () => {
|
||||
req.destroy(new Error(`Request timed out fetching ${url}`));
|
||||
});
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function getLatestNpmVersion(): Promise<string | undefined> {
|
||||
const data = await requestJson<{ "dist-tags"?: { latest?: string } }>(
|
||||
"https://registry.npmjs.org/kandev",
|
||||
);
|
||||
return data?.["dist-tags"]?.latest;
|
||||
}
|
||||
|
||||
function promptYesNo(question: string, defaultYes = false): Promise<boolean> {
|
||||
return new Promise<boolean>((resolve) => {
|
||||
if (!process.stdin.isTTY) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
const suffix = defaultYes ? "[Y/n]" : "[y/N]";
|
||||
rl.question(`${question} ${suffix} `, (answer: string) => {
|
||||
rl.close();
|
||||
const normalized = String(answer || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!normalized) {
|
||||
resolve(Boolean(defaultYes));
|
||||
return;
|
||||
}
|
||||
resolve(normalized === "y" || normalized === "yes");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function maybePromptForUpdate(currentVersion: string, args: string[]) {
|
||||
// Allow disabling update checks for CI or automation.
|
||||
if (process.env.KANDEV_SKIP_UPDATE === "1" || process.env.KANDEV_NO_UPDATE_PROMPT === "1") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const latest = await getLatestNpmVersion();
|
||||
if (!latest) return;
|
||||
if (compareVersions(latest, currentVersion) <= 0) return;
|
||||
const wantsUpdate = await promptYesNo(
|
||||
`Update available: ${currentVersion} -> ${latest}. Update now?`,
|
||||
false,
|
||||
);
|
||||
if (!wantsUpdate) return;
|
||||
const env = { ...process.env, KANDEV_SKIP_UPDATE: "1" };
|
||||
const child = spawn("npx", ["kandev@latest", ...args], {
|
||||
stdio: "inherit",
|
||||
env,
|
||||
});
|
||||
child.on("exit", (code) => process.exit(code || 0));
|
||||
await new Promise(() => {});
|
||||
} catch {
|
||||
// ignore update errors
|
||||
}
|
||||
}
|
||||
Generated
+334
-19
@@ -32,7 +32,7 @@ importers:
|
||||
dependencies:
|
||||
tar:
|
||||
specifier: ^7.5.11
|
||||
version: 7.5.11
|
||||
version: 7.5.13
|
||||
tree-kill:
|
||||
specifier: ^1.2.2
|
||||
version: 1.2.2
|
||||
@@ -40,6 +40,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.19.28
|
||||
esbuild:
|
||||
specifier: ^0.24.0
|
||||
version: 0.24.2
|
||||
tsx:
|
||||
specifier: ^4.15.7
|
||||
version: 4.21.0
|
||||
@@ -49,6 +52,22 @@ importers:
|
||||
vitest:
|
||||
specifier: ^1.6.0
|
||||
version: 1.6.1(@types/node@20.19.28)(happy-dom@20.8.9)(lightningcss@1.30.2)
|
||||
optionalDependencies:
|
||||
'@kdlbs/runtime-darwin-arm64':
|
||||
specifier: 0.0.0-bootstrap
|
||||
version: 0.0.0-bootstrap
|
||||
'@kdlbs/runtime-darwin-x64':
|
||||
specifier: 0.0.0-bootstrap
|
||||
version: 0.0.0-bootstrap
|
||||
'@kdlbs/runtime-linux-arm64':
|
||||
specifier: 0.0.0-bootstrap
|
||||
version: 0.0.0-bootstrap
|
||||
'@kdlbs/runtime-linux-x64':
|
||||
specifier: 0.0.0-bootstrap
|
||||
version: 0.0.0-bootstrap
|
||||
'@kdlbs/runtime-win32-x64':
|
||||
specifier: 0.0.0-bootstrap
|
||||
version: 0.0.0-bootstrap
|
||||
|
||||
packages/theme:
|
||||
dependencies:
|
||||
@@ -400,7 +419,7 @@ importers:
|
||||
version: 0.55.1
|
||||
next:
|
||||
specifier: 16.1.7
|
||||
version: 16.1.7(@babel/core@7.28.6)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
version: 16.1.7(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
@@ -454,7 +473,7 @@ importers:
|
||||
version: 2.0.7(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
styled-jsx:
|
||||
specifier: 5.1.6
|
||||
version: 5.1.6(@babel/core@7.28.6)(react@19.2.3)
|
||||
version: 5.1.6(react@19.2.3)
|
||||
tailwind-merge:
|
||||
specifier: ^3.4.0
|
||||
version: 3.4.0
|
||||
@@ -890,6 +909,12 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -902,6 +927,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -914,6 +945,12 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.24.2':
|
||||
resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -926,6 +963,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.24.2':
|
||||
resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -938,6 +981,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -950,6 +999,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.24.2':
|
||||
resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -962,6 +1017,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -974,6 +1035,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -986,6 +1053,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -998,6 +1071,12 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.24.2':
|
||||
resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1010,6 +1089,12 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.24.2':
|
||||
resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1022,6 +1107,12 @@ packages:
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.24.2':
|
||||
resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1034,6 +1125,12 @@ packages:
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.24.2':
|
||||
resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1046,6 +1143,12 @@ packages:
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.24.2':
|
||||
resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1058,6 +1161,12 @@ packages:
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.24.2':
|
||||
resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1070,6 +1179,12 @@ packages:
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.24.2':
|
||||
resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1082,12 +1197,24 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.24.2':
|
||||
resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1100,12 +1227,24 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1118,6 +1257,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.24.2':
|
||||
resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1136,6 +1281,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.24.2':
|
||||
resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1148,6 +1299,12 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.24.2':
|
||||
resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1160,6 +1317,12 @@ packages:
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.24.2':
|
||||
resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1172,6 +1335,12 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -1483,6 +1652,31 @@ packages:
|
||||
'@jridgewell/trace-mapping@0.3.31':
|
||||
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||
|
||||
'@kdlbs/runtime-darwin-arm64@0.0.0-bootstrap':
|
||||
resolution: {integrity: sha512-i3H7KOcw5m1KEhXhTSa9+jNBlAh0Vc8NVkiGL9BpKB5n/COrhBol7YcN1WNkxs/6WdpD9LFo2My+51wODrC9tQ==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@kdlbs/runtime-darwin-x64@0.0.0-bootstrap':
|
||||
resolution: {integrity: sha512-ia8qU58B1pWr2AM9/qsPP514L6SftSYYmbFG49kcWNZMpRhosdbKNm1GfDce4yE+MKeOpmLmjPu/pR7ZnyMJpg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@kdlbs/runtime-linux-arm64@0.0.0-bootstrap':
|
||||
resolution: {integrity: sha512-m3UUB0GYOKNVwgW8wwdsjV6dFFmTYxI9vmLMtvgWz01GW1y+Ms5YSRhb0RT5NRMvYpn7xDntlJCjtPATE+RZ7A==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@kdlbs/runtime-linux-x64@0.0.0-bootstrap':
|
||||
resolution: {integrity: sha512-XpcVCGngqq9wsZR2OASQwCiylTFUVD7iDoqeeWUuzZZ1wRxdGqYNV31xrEciq+96O/pI+dn5zWgbMYQEXc309w==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@kdlbs/runtime-win32-x64@0.0.0-bootstrap':
|
||||
resolution: {integrity: sha512-x/KRLCugV2eOLCJQWTUmBGU08rz7ep83GdTNZLeyQ3UmOKErVARk5eC9lWNO8LnlQr8LbahK9KdRW6Jhieee2A==}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@lezer/common@1.5.0':
|
||||
resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==}
|
||||
|
||||
@@ -4219,6 +4413,11 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.24.2:
|
||||
resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
esbuild@0.27.2:
|
||||
resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -6411,8 +6610,8 @@ packages:
|
||||
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tar@7.5.11:
|
||||
resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==}
|
||||
tar@7.5.13:
|
||||
resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
tiny-invariant@1.3.3:
|
||||
@@ -7474,120 +7673,183 @@ snapshots:
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
@@ -7597,24 +7859,36 @@ snapshots:
|
||||
'@esbuild/sunos-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.27.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.21.5':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.24.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.27.2':
|
||||
optional: true
|
||||
|
||||
@@ -7866,6 +8140,21 @@ snapshots:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
'@kdlbs/runtime-darwin-arm64@0.0.0-bootstrap':
|
||||
optional: true
|
||||
|
||||
'@kdlbs/runtime-darwin-x64@0.0.0-bootstrap':
|
||||
optional: true
|
||||
|
||||
'@kdlbs/runtime-linux-arm64@0.0.0-bootstrap':
|
||||
optional: true
|
||||
|
||||
'@kdlbs/runtime-linux-x64@0.0.0-bootstrap':
|
||||
optional: true
|
||||
|
||||
'@kdlbs/runtime-win32-x64@0.0.0-bootstrap':
|
||||
optional: true
|
||||
|
||||
'@lezer/common@1.5.0': {}
|
||||
|
||||
'@lezer/common@1.5.1': {}
|
||||
@@ -10724,6 +11013,34 @@ snapshots:
|
||||
'@esbuild/win32-ia32': 0.21.5
|
||||
'@esbuild/win32-x64': 0.21.5
|
||||
|
||||
esbuild@0.24.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.24.2
|
||||
'@esbuild/android-arm': 0.24.2
|
||||
'@esbuild/android-arm64': 0.24.2
|
||||
'@esbuild/android-x64': 0.24.2
|
||||
'@esbuild/darwin-arm64': 0.24.2
|
||||
'@esbuild/darwin-x64': 0.24.2
|
||||
'@esbuild/freebsd-arm64': 0.24.2
|
||||
'@esbuild/freebsd-x64': 0.24.2
|
||||
'@esbuild/linux-arm': 0.24.2
|
||||
'@esbuild/linux-arm64': 0.24.2
|
||||
'@esbuild/linux-ia32': 0.24.2
|
||||
'@esbuild/linux-loong64': 0.24.2
|
||||
'@esbuild/linux-mips64el': 0.24.2
|
||||
'@esbuild/linux-ppc64': 0.24.2
|
||||
'@esbuild/linux-riscv64': 0.24.2
|
||||
'@esbuild/linux-s390x': 0.24.2
|
||||
'@esbuild/linux-x64': 0.24.2
|
||||
'@esbuild/netbsd-arm64': 0.24.2
|
||||
'@esbuild/netbsd-x64': 0.24.2
|
||||
'@esbuild/openbsd-arm64': 0.24.2
|
||||
'@esbuild/openbsd-x64': 0.24.2
|
||||
'@esbuild/sunos-x64': 0.24.2
|
||||
'@esbuild/win32-arm64': 0.24.2
|
||||
'@esbuild/win32-ia32': 0.24.2
|
||||
'@esbuild/win32-x64': 0.24.2
|
||||
|
||||
esbuild@0.27.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.27.2
|
||||
@@ -10766,8 +11083,8 @@ snapshots:
|
||||
'@next/eslint-plugin-next': 16.1.1
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-react-hooks: 7.0.1(eslint@9.39.2(jiti@2.6.1))
|
||||
@@ -10789,7 +11106,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
@@ -10800,22 +11117,22 @@ snapshots:
|
||||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
@@ -10826,7 +11143,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.53.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -12382,7 +12699,7 @@ snapshots:
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
next@16.1.7(@babel/core@7.28.6)(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
next@16.1.7(@playwright/test@1.58.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
|
||||
dependencies:
|
||||
'@next/env': 16.1.7
|
||||
'@swc/helpers': 0.5.15
|
||||
@@ -12391,7 +12708,7 @@ snapshots:
|
||||
postcss: 8.4.31
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
styled-jsx: 5.1.6(@babel/core@7.28.6)(react@19.2.3)
|
||||
styled-jsx: 5.1.6(react@19.2.3)
|
||||
optionalDependencies:
|
||||
'@next/swc-darwin-arm64': 16.1.7
|
||||
'@next/swc-darwin-x64': 16.1.7
|
||||
@@ -13534,12 +13851,10 @@ snapshots:
|
||||
dependencies:
|
||||
inline-style-parser: 0.2.7
|
||||
|
||||
styled-jsx@5.1.6(@babel/core@7.28.6)(react@19.2.3):
|
||||
styled-jsx@5.1.6(react@19.2.3):
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 19.2.3
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.28.6
|
||||
|
||||
stylis@4.3.6: {}
|
||||
|
||||
@@ -13561,7 +13876,7 @@ snapshots:
|
||||
|
||||
tapable@2.3.0: {}
|
||||
|
||||
tar@7.5.11:
|
||||
tar@7.5.13:
|
||||
dependencies:
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
|
||||
+95
-76
@@ -4,17 +4,23 @@
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph CLI["npx kandev"]
|
||||
subgraph CLI["kandev"]
|
||||
run["run"]
|
||||
dev["dev"]
|
||||
start["start"]
|
||||
end
|
||||
|
||||
run --> gh["GitHub Release"]
|
||||
run --> resolve["Resolve installed runtime"]
|
||||
dev --> makedev["make dev"]
|
||||
start --> binary["Binary + next start"]
|
||||
start --> binary["Local binary + next start"]
|
||||
|
||||
gh --> supervisor
|
||||
resolve --> envvar["KANDEV_BUNDLE_DIR<br/>(Homebrew, tests)"]
|
||||
resolve --> npmpkg["@kdlbs/runtime-{platform}<br/>(npm/npx)"]
|
||||
resolve --> cache["~/.kandev/bin cache<br/>(--runtime-version only)"]
|
||||
|
||||
envvar --> supervisor
|
||||
npmpkg --> supervisor
|
||||
cache --> supervisor
|
||||
makedev --> supervisor
|
||||
binary --> supervisor
|
||||
|
||||
@@ -28,51 +34,66 @@ flowchart TB
|
||||
|
||||
## Overview
|
||||
|
||||
The Kandev CLI (`kandev`) is the primary way to run the Kandev application. It handles downloading release bundles, managing processes, and providing a unified interface for both end users and developers.
|
||||
The Kandev CLI (`kandev`) is the primary way to run the Kandev application. It launches the backend and web processes from an installed runtime bundle and provides a unified interface for end users and developers.
|
||||
|
||||
## Installation
|
||||
|
||||
No installation required. Use npx:
|
||||
### Homebrew (macOS, Linux)
|
||||
|
||||
```bash
|
||||
npx kandev
|
||||
brew install kdlbs/kandev/kandev
|
||||
```
|
||||
|
||||
Or install globally:
|
||||
### NPX (requires npm 7+)
|
||||
|
||||
```bash
|
||||
npm install -g kandev
|
||||
npx kandev@latest
|
||||
```
|
||||
|
||||
`npx` installs the `kandev` CLI plus the matching `@kdlbs/runtime-{platform}` package via npm optional dependencies. Older npm versions silently skip optional deps and won't work — npm 7 is the floor.
|
||||
|
||||
### NPM (global)
|
||||
|
||||
```bash
|
||||
npm install -g kandev@latest
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Run the latest release (recommended for users)
|
||||
npx kandev
|
||||
# Run the installed runtime
|
||||
kandev
|
||||
|
||||
# Opens the app at http://localhost:37429 (or next available port)
|
||||
# Opens the app at http://localhost:38429 (or next available port)
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
The package manager controls the runtime version:
|
||||
|
||||
```bash
|
||||
brew upgrade kandev # Homebrew users
|
||||
npm install -g kandev@latest # global npm users
|
||||
npx kandev@latest # npx users (always latest)
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
### `kandev` / `kandev run`
|
||||
|
||||
Downloads and runs the latest release bundle from GitHub. This is the default command and recommended for end users.
|
||||
Runs the installed runtime bundle. This is the default command.
|
||||
|
||||
```bash
|
||||
npx kandev
|
||||
npx kandev run
|
||||
npx kandev run --version v0.1.0
|
||||
kandev
|
||||
kandev run
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Checks for CLI updates (prompts if newer version available)
|
||||
2. Downloads the platform-specific release bundle from GitHub
|
||||
3. Extracts to `~/.kandev/bin/<version>/<platform>/`
|
||||
4. Starts the backend server
|
||||
5. Waits for backend health check
|
||||
6. Starts the web app
|
||||
7. Prints URLs when ready
|
||||
1. Resolves the runtime bundle (KANDEV_BUNDLE_DIR → npm package → cache).
|
||||
2. Starts the backend server.
|
||||
3. Waits for backend health check.
|
||||
4. Starts the web app.
|
||||
5. Opens browser when ready.
|
||||
|
||||
### `kandev dev`
|
||||
|
||||
@@ -80,53 +101,49 @@ Runs the application in development mode with hot-reloading. Requires a local re
|
||||
|
||||
```bash
|
||||
# From the repo root or any subdirectory
|
||||
npx kandev dev
|
||||
kandev dev
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Locates the repo root (looks for `apps/backend` and `apps/web`)
|
||||
2. Runs `make dev` for the backend (Go with hot-reload)
|
||||
3. Runs `pnpm dev` for the web app (Next.js dev server)
|
||||
4. Both processes run with hot-reloading enabled
|
||||
1. Locates the repo root (looks for `apps/backend` and `apps/web`).
|
||||
2. Runs `make dev` for the backend (Go with hot-reload).
|
||||
3. Runs `pnpm dev` for the web app (Next.js dev server).
|
||||
|
||||
### `kandev start`
|
||||
|
||||
Runs the application using local production builds. Requires running `make build` first.
|
||||
|
||||
```bash
|
||||
# Build first
|
||||
make build
|
||||
|
||||
# Then start
|
||||
npx kandev start
|
||||
kandev start
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
1. Locates the repo root
|
||||
2. Runs the compiled backend binary (`apps/backend/bin/kandev`)
|
||||
3. Runs `pnpm start` for the web app (Next.js production server)
|
||||
4. Both run in production mode without hot-reloading
|
||||
|
||||
## Options
|
||||
|
||||
| Option | Description | Example |
|
||||
|--------|-------------|---------|
|
||||
| `--version <tag>` | Use a specific release version | `--version v0.1.0` |
|
||||
| `--backend-port <port>` | Override backend port | `--backend-port 18080` |
|
||||
| `--web-port <port>` | Override web port | `--web-port 13000` |
|
||||
| `--version`, `-V` | Print CLI version and exit | `kandev --version` |
|
||||
| `--port <port>` | Backend port (alias: `--backend-port`) | `--port 3000` |
|
||||
| `--web-internal-port <port>` | Override internal Next.js port | `--web-internal-port 13000` |
|
||||
| `--verbose`, `-v` | Show info logs from backend + web | `--verbose` |
|
||||
| `--debug` | Show debug logs + agent message dumps | `--debug` |
|
||||
| `--help`, `-h` | Show help | `--help` |
|
||||
| `--runtime-version <tag>` | **Advanced/debug only**: download a specific runtime tag from GitHub releases instead of using the installed runtime | `--runtime-version v0.16.0` |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Use specific release
|
||||
npx kandev --version v0.1.0
|
||||
# Print CLI version
|
||||
kandev --version
|
||||
|
||||
# Custom ports
|
||||
npx kandev --backend-port 18080 --web-port 13000
|
||||
kandev --port 18080 --web-internal-port 13000
|
||||
|
||||
# Dev mode with custom ports
|
||||
npx kandev dev --backend-port 18080 --web-port 13000
|
||||
# Dev mode
|
||||
kandev dev --port 18080
|
||||
|
||||
# Force a specific runtime version (debug)
|
||||
kandev --runtime-version v0.16.0
|
||||
```
|
||||
|
||||
## Port Selection
|
||||
@@ -135,10 +152,9 @@ By default, the CLI automatically finds available ports:
|
||||
|
||||
| Service | Default Port | Fallback |
|
||||
|---------|--------------|----------|
|
||||
| Backend | 38429 | Auto-selects from 10000-60000 |
|
||||
| Web | 37429 | Auto-selects from 10000-60000 |
|
||||
| AgentCtl | 39429 | Auto-selects from 10000-60000 |
|
||||
| MCP Server | 40429 | Auto-selects from 10000-60000 |
|
||||
| Backend | 38429 | Auto-selects from 10000–60000 |
|
||||
| Web | 37429 | Auto-selects from 10000–60000 |
|
||||
| AgentCtl | 39429 | Auto-selects from 10000–60000 |
|
||||
|
||||
If the default port is in use, the CLI finds the next available port automatically.
|
||||
|
||||
@@ -146,25 +162,21 @@ If the default port is in use, the CLI finds the next available port automatical
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `KANDEV_GITHUB_OWNER` | Override GitHub repo owner for releases |
|
||||
| `KANDEV_GITHUB_REPO` | Override GitHub repo name for releases |
|
||||
| `KANDEV_GITHUB_TOKEN` | GitHub token for API rate limits |
|
||||
| `KANDEV_NO_UPDATE_PROMPT=1` | Disable CLI update prompts |
|
||||
| `KANDEV_BUNDLE_DIR` | Force the runtime bundle location (set by Homebrew wrapper) |
|
||||
| `KANDEV_PORT` / `KANDEV_BACKEND_PORT` | Backend port (CLI flag wins) |
|
||||
| `KANDEV_WEB_PORT` | Internal Next.js port |
|
||||
| `KANDEV_HEALTH_TIMEOUT_MS` | Override health check timeout (ms) |
|
||||
| `KANDEV_GITHUB_OWNER` / `KANDEV_GITHUB_REPO` | Override GitHub repo for `--runtime-version` downloads |
|
||||
| `KANDEV_GITHUB_TOKEN` | GitHub token for `--runtime-version` API access |
|
||||
|
||||
## Makefile Integration
|
||||
|
||||
The repo includes a Makefile that wraps the CLI for common operations:
|
||||
|
||||
```bash
|
||||
# Install deps, build, and start in production mode
|
||||
make start
|
||||
|
||||
# Run in development mode
|
||||
make dev
|
||||
|
||||
# Build everything
|
||||
make build
|
||||
make start # Production mode (after make build)
|
||||
make dev # Development mode
|
||||
make build # Build everything
|
||||
```
|
||||
|
||||
See `make help` for all available commands.
|
||||
@@ -173,7 +185,7 @@ See `make help` for all available commands.
|
||||
|
||||
| Feature | `run` | `dev` | `start` |
|
||||
|---------|-------|-------|---------|
|
||||
| Source | GitHub releases | Local repo | Local build |
|
||||
| Source | Installed runtime | Local repo | Local build |
|
||||
| Hot-reload | No | Yes | No |
|
||||
| Requires repo | No | Yes | Yes |
|
||||
| Requires build | No | No | Yes |
|
||||
@@ -181,12 +193,25 @@ See `make help` for all available commands.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "No Kandev runtime found for {platform}"
|
||||
|
||||
The CLI couldn't find an installed runtime. Install one:
|
||||
|
||||
```bash
|
||||
# via npm (requires npm 7+ for optional dep resolution)
|
||||
npx kandev@latest
|
||||
# via Homebrew
|
||||
brew install kdlbs/kandev/kandev
|
||||
```
|
||||
|
||||
If you're on npm 6 or older, optional dependencies aren't installed by `npx`. Upgrade npm: `npm install -g npm@latest`.
|
||||
|
||||
### Port Already in Use
|
||||
|
||||
The CLI automatically finds available ports. If you need a specific port:
|
||||
|
||||
```bash
|
||||
npx kandev --backend-port 18080 --web-port 13000
|
||||
kandev --port 18080
|
||||
```
|
||||
|
||||
### Backend Takes Too Long to Start
|
||||
@@ -194,15 +219,7 @@ npx kandev --backend-port 18080 --web-port 13000
|
||||
Increase the health check timeout:
|
||||
|
||||
```bash
|
||||
KANDEV_HEALTH_TIMEOUT_MS=60000 npx kandev
|
||||
```
|
||||
|
||||
### GitHub Rate Limits
|
||||
|
||||
If you hit GitHub API rate limits, provide a token:
|
||||
|
||||
```bash
|
||||
KANDEV_GITHUB_TOKEN=ghp_xxx npx kandev
|
||||
KANDEV_HEALTH_TIMEOUT_MS=60000 kandev
|
||||
```
|
||||
|
||||
### Dev Mode: "Unable to locate repo root"
|
||||
@@ -211,7 +228,7 @@ Run from within the kandev repository:
|
||||
|
||||
```bash
|
||||
cd /path/to/kandev
|
||||
npx kandev dev
|
||||
kandev dev
|
||||
```
|
||||
|
||||
### Start Mode: "Backend binary not found"
|
||||
@@ -220,12 +237,14 @@ Build the project first:
|
||||
|
||||
```bash
|
||||
make build
|
||||
npx kandev start
|
||||
kandev start
|
||||
```
|
||||
|
||||
## Data Storage
|
||||
|
||||
| Path | Contents |
|
||||
|------|----------|
|
||||
| `~/.kandev/bin/` | Downloaded release bundles |
|
||||
| `~/.kandev/bin/` | Cached downloads from `--runtime-version` (debug only) |
|
||||
| `~/.kandev/data/` | SQLite database and app data |
|
||||
| Homebrew Cellar | Installed runtime (when installed via brew) |
|
||||
| `<npm cache>/node_modules/@kdlbs/runtime-{platform}/` | Installed runtime (when installed via npm/npx) |
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the CLI and copy artifacts into dist/kandev/cli/ for inclusion in release bundles.
|
||||
#
|
||||
# The CLI bundle (dist/cli.bundle.js) is a single-file esbuild output with all Node
|
||||
# dependencies inlined. This is required for Homebrew installs where there is no
|
||||
# node_modules directory in the Cellar.
|
||||
#
|
||||
# Output layout inside dist/kandev/cli/:
|
||||
# bin/cli.js - Node entrypoint (requires #!/usr/bin/env node shebang + chmod +x)
|
||||
# dist/cli.bundle.js - Self-contained bundle with inlined deps (used by Homebrew)
|
||||
# package.json - Package metadata (used to read version at runtime)
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
CLI_DIR="$ROOT_DIR/apps/cli"
|
||||
OUT_DIR="$ROOT_DIR/dist/kandev/cli"
|
||||
|
||||
echo "Building CLI for release bundle..."
|
||||
|
||||
# Build TypeScript
|
||||
pnpm -C "$ROOT_DIR/apps" --filter kandev build
|
||||
echo " TypeScript build complete"
|
||||
|
||||
# Bundle with esbuild (inlines tree-kill and all other deps)
|
||||
pnpm -C "$ROOT_DIR/apps/cli" bundle
|
||||
echo " esbuild bundle complete"
|
||||
|
||||
# Copy artifacts into dist/kandev/cli/
|
||||
rm -rf "$OUT_DIR"
|
||||
mkdir -p "$OUT_DIR/bin" "$OUT_DIR/dist"
|
||||
|
||||
# Write a bin/cli.js that points at the bundled dist (for Homebrew/manual installs
|
||||
# where there is no node_modules to resolve the unbundled dist/cli.js's deps).
|
||||
# The npm package's bin/cli.js is different — it requires the unbundled dist/cli.js
|
||||
# because tree-kill etc. are installed normally via npm.
|
||||
cat > "$OUT_DIR/bin/cli.js" <<'EOF'
|
||||
#!/usr/bin/env node
|
||||
|
||||
require("../dist/cli.bundle.js");
|
||||
EOF
|
||||
chmod +x "$OUT_DIR/bin/cli.js"
|
||||
|
||||
cp "$CLI_DIR/dist/cli.bundle.js" "$OUT_DIR/dist/cli.bundle.js"
|
||||
cp "$CLI_DIR/package.json" "$OUT_DIR/package.json"
|
||||
|
||||
echo "CLI artifacts packaged at $OUT_DIR"
|
||||
echo " bin/cli.js (entrypoint, requires dist/cli.bundle.js, chmod +x)"
|
||||
echo " dist/cli.bundle.js (self-contained bundle, deps inlined)"
|
||||
echo " package.json"
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# Generate per-platform npm runtime packages from built dist/release-assets.
|
||||
#
|
||||
# Each platform bundle (kandev-{platform}.tar.gz) is repackaged as an npm package
|
||||
# containing only bin/ and web/ (no cli/). These are the @kdlbs/runtime-* packages
|
||||
# that the main kandev npm package declares as optionalDependencies.
|
||||
#
|
||||
# Usage:
|
||||
# package-npm-runtime.sh <version> <release-assets-dir> <output-dir>
|
||||
#
|
||||
# Arguments:
|
||||
# version SemVer string (e.g. 0.17.0)
|
||||
# release-assets-dir Directory containing kandev-*.tar.gz files
|
||||
# output-dir Directory where per-platform npm packages are written
|
||||
#
|
||||
# Output (one directory per platform ready for npm publish):
|
||||
# <output-dir>/@kdlbs/runtime-linux-x64/
|
||||
# <output-dir>/@kdlbs/runtime-linux-arm64/
|
||||
# <output-dir>/@kdlbs/runtime-darwin-x64/
|
||||
# <output-dir>/@kdlbs/runtime-darwin-arm64/
|
||||
# <output-dir>/@kdlbs/runtime-win32-x64/
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:?Usage: $0 <version> <release-assets-dir> <output-dir>}"
|
||||
ASSETS_DIR="${2:?Usage: $0 <version> <release-assets-dir> <output-dir>}"
|
||||
OUT_DIR="${3:?Usage: $0 <version> <release-assets-dir> <output-dir>}"
|
||||
|
||||
# Maps platform dir name → npm package name + npm os/cpu fields
|
||||
declare -A PLATFORM_TO_PACKAGE=(
|
||||
["linux-x64"]="@kdlbs/runtime-linux-x64"
|
||||
["linux-arm64"]="@kdlbs/runtime-linux-arm64"
|
||||
["macos-x64"]="@kdlbs/runtime-darwin-x64"
|
||||
["macos-arm64"]="@kdlbs/runtime-darwin-arm64"
|
||||
["windows-x64"]="@kdlbs/runtime-win32-x64"
|
||||
)
|
||||
|
||||
declare -A PLATFORM_TO_OS=(
|
||||
["linux-x64"]='["linux"]'
|
||||
["linux-arm64"]='["linux"]'
|
||||
["macos-x64"]='["darwin"]'
|
||||
["macos-arm64"]='["darwin"]'
|
||||
["windows-x64"]='["win32"]'
|
||||
)
|
||||
|
||||
declare -A PLATFORM_TO_CPU=(
|
||||
["linux-x64"]='["x64"]'
|
||||
["linux-arm64"]='["arm64"]'
|
||||
["macos-x64"]='["x64"]'
|
||||
["macos-arm64"]='["arm64"]'
|
||||
["windows-x64"]='["x64"]'
|
||||
)
|
||||
|
||||
echo "Packaging npm runtime packages for version $VERSION..."
|
||||
echo " assets dir: $ASSETS_DIR"
|
||||
echo " output dir: $OUT_DIR"
|
||||
|
||||
for platform in linux-x64 linux-arm64 macos-x64 macos-arm64 windows-x64; do
|
||||
archive="$ASSETS_DIR/kandev-${platform}.tar.gz"
|
||||
if [[ ! -f "$archive" ]]; then
|
||||
echo "Error: missing archive $archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
package_name="${PLATFORM_TO_PACKAGE[$platform]}"
|
||||
# @kdlbs/runtime-linux-x64 → scope=@kdlbs, name=runtime-linux-x64
|
||||
scope_dir="${package_name%%/*}" # @kdlbs
|
||||
pkg_dir="${package_name##*/}" # runtime-linux-x64
|
||||
pkg_out="$OUT_DIR/${scope_dir}/${pkg_dir}"
|
||||
|
||||
rm -rf "$pkg_out"
|
||||
mkdir -p "$pkg_out"
|
||||
|
||||
# Extract only bin/ and web/ from the archive (skip cli/)
|
||||
local_tmp="$pkg_out/.extract_tmp"
|
||||
mkdir -p "$local_tmp"
|
||||
tar -xzf "$archive" -C "$local_tmp"
|
||||
|
||||
# Find the extracted bundle root (named "kandev" inside the archive)
|
||||
bundle_root="$local_tmp/kandev"
|
||||
if [[ ! -d "$bundle_root" ]]; then
|
||||
echo "Error: expected kandev/ directory in $archive" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cp -R "$bundle_root/bin" "$pkg_out/bin"
|
||||
cp -R "$bundle_root/web" "$pkg_out/web"
|
||||
rm -rf "$local_tmp"
|
||||
|
||||
os_field="${PLATFORM_TO_OS[$platform]}"
|
||||
cpu_field="${PLATFORM_TO_CPU[$platform]}"
|
||||
|
||||
cat > "$pkg_out/package.json" <<EOF
|
||||
{
|
||||
"name": "$package_name",
|
||||
"version": "$VERSION",
|
||||
"description": "Kandev runtime bundle for $platform",
|
||||
"license": "AGPL-3.0-only",
|
||||
"os": $os_field,
|
||||
"cpu": $cpu_field,
|
||||
"files": [
|
||||
"bin",
|
||||
"web"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
echo " packaged $package_name@$VERSION"
|
||||
done
|
||||
|
||||
echo "Done. Runtime packages written to $OUT_DIR"
|
||||
Executable
+181
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env bash
|
||||
# Publish the main kandev npm package + all @kdlbs/runtime-* optional packages.
|
||||
#
|
||||
# Authentication: Trusted Publishers (OIDC). Each of the 6 packages must have
|
||||
# this workflow configured as its trusted publisher on npmjs.com. The npm CLI
|
||||
# auto-detects OIDC credentials from GitHub Actions and exchanges them for a
|
||||
# short-lived publish token. No NPM_TOKEN secret is needed.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - GitHub release assets for <tag> must already exist (verified before publishing).
|
||||
# - Running inside GitHub Actions with `id-token: write` permission set on
|
||||
# the publish-npm job. (npm publish from a local shell will fall back to
|
||||
# classic auth — but tokens are not the recommended path going forward.)
|
||||
#
|
||||
# Usage:
|
||||
# publish-npm.sh <version> <tag>
|
||||
#
|
||||
# Arguments:
|
||||
# version SemVer string (e.g. 0.17.0)
|
||||
# tag Git tag (e.g. v0.17.0) — used to verify GitHub release assets exist
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
VERSION="${1:?Usage: $0 <version> <tag>}"
|
||||
TAG="${2:?Usage: $0 <version> <tag>}"
|
||||
|
||||
bold() { printf '\033[1m%s\033[0m' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m' "$*"; }
|
||||
red() { printf '\033[31m%s\033[0m' "$*"; }
|
||||
yellow(){ printf '\033[33m%s\033[0m' "$*"; }
|
||||
|
||||
log() { echo " >> $*"; }
|
||||
log_ok() { echo " $(green "ok") $*"; }
|
||||
|
||||
die() {
|
||||
echo "$(red "Error:") $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# -- Verify GitHub release assets exist before publishing ---------------------
|
||||
|
||||
REQUIRED_PLATFORMS=(linux-x64 linux-arm64 macos-x64 macos-arm64 windows-x64)
|
||||
|
||||
log "Verifying GitHub release assets exist for $TAG..."
|
||||
for platform in "${REQUIRED_PLATFORMS[@]}"; do
|
||||
asset="kandev-${platform}.tar.gz"
|
||||
if ! gh release view "$TAG" --json assets --jq ".assets[].name" 2>/dev/null | grep -q "^${asset}$"; then
|
||||
die "GitHub release asset missing: $asset in release $TAG. Run release workflow first."
|
||||
fi
|
||||
done
|
||||
log_ok "All 5 platform assets present in GitHub release $TAG"
|
||||
|
||||
# -- Download release assets for packaging ------------------------------------
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
ASSETS_DIR="$WORK_DIR/assets"
|
||||
mkdir -p "$ASSETS_DIR"
|
||||
|
||||
log "Downloading release assets for $TAG..."
|
||||
for platform in "${REQUIRED_PLATFORMS[@]}"; do
|
||||
asset="kandev-${platform}.tar.gz"
|
||||
log " downloading $asset..."
|
||||
gh release download "$TAG" --pattern "$asset" --dir "$ASSETS_DIR"
|
||||
done
|
||||
log_ok "Assets downloaded to $ASSETS_DIR"
|
||||
|
||||
# -- Generate npm runtime packages --------------------------------------------
|
||||
|
||||
NPM_PKG_DIR="$WORK_DIR/npm-packages"
|
||||
bash "$ROOT_DIR/scripts/release/package-npm-runtime.sh" "$VERSION" "$ASSETS_DIR" "$NPM_PKG_DIR"
|
||||
|
||||
# -- Publish @kdlbs/runtime-* packages first ---------------------------------
|
||||
|
||||
RUNTIME_PACKAGES=(
|
||||
"@kdlbs/runtime-linux-x64"
|
||||
"@kdlbs/runtime-linux-arm64"
|
||||
"@kdlbs/runtime-darwin-x64"
|
||||
"@kdlbs/runtime-darwin-arm64"
|
||||
"@kdlbs/runtime-win32-x64"
|
||||
)
|
||||
|
||||
echo
|
||||
echo "$(bold "Publishing @kdlbs/runtime-* packages...")"
|
||||
FAILED_PACKAGES=()
|
||||
ALREADY_PUBLISHED=()
|
||||
|
||||
for pkg in "${RUNTIME_PACKAGES[@]}"; do
|
||||
scope="${pkg%%/*}" # @kdlbs
|
||||
name="${pkg##*/}" # runtime-linux-x64
|
||||
pkg_dir="$NPM_PKG_DIR/${scope}/${name}"
|
||||
|
||||
if [[ ! -d "$pkg_dir" ]]; then
|
||||
echo " $(red "missing") $pkg_dir (package directory was not generated)" >&2
|
||||
FAILED_PACKAGES+=("$pkg")
|
||||
continue
|
||||
fi
|
||||
|
||||
log "Publishing $pkg@$VERSION..."
|
||||
# Capture full npm output so we can show the real error on failure rather
|
||||
# than just a generic warning. Distinguish "already published" (idempotent
|
||||
# case — fine) from real failures (must abort).
|
||||
if output="$(cd "$pkg_dir" && npm publish --access public --provenance 2>&1)"; then
|
||||
log_ok "$pkg@$VERSION published"
|
||||
elif echo "$output" | grep -qE "EPUBLISHCONFLICT|cannot publish over the previously published versions|You cannot publish over"; then
|
||||
echo " $(yellow "skip") $pkg@$VERSION already published (treated as idempotent success)" >&2
|
||||
ALREADY_PUBLISHED+=("$pkg")
|
||||
else
|
||||
echo " $(red "FAIL") Failed to publish $pkg@$VERSION:" >&2
|
||||
echo "$output" | sed 's/^/ /' >&2
|
||||
FAILED_PACKAGES+=("$pkg")
|
||||
fi
|
||||
done
|
||||
|
||||
# Abort before publishing main kandev if any runtime publish failed.
|
||||
# Otherwise users on those platforms would get "No Kandev runtime found"
|
||||
# after install, with kandev pointing at @kdlbs/runtime-*@VERSION that
|
||||
# doesn't exist on npm.
|
||||
if [[ "${#FAILED_PACKAGES[@]}" -gt 0 ]]; then
|
||||
echo
|
||||
echo "$(red "Error:") The following runtime packages failed to publish:" >&2
|
||||
for pkg in "${FAILED_PACKAGES[@]}"; do
|
||||
echo " - $pkg" >&2
|
||||
done
|
||||
echo >&2
|
||||
echo "Refusing to publish main kandev@$VERSION. Fix the runtime publish failures" >&2
|
||||
echo "and re-run this script (already-published runtime packages will be skipped)." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# -- Pin optionalDependencies before publishing main kandev ------------------
|
||||
#
|
||||
# In committed source, optionalDependencies reference 0.0.0-bootstrap so the
|
||||
# lockfile resolves during normal development. For the published kandev@VERSION
|
||||
# package, we want optionalDependencies to point at @kdlbs/runtime-*@VERSION
|
||||
# so users get matching runtime bundles. The runtime packages were just
|
||||
# published above, so this version exists on npm now.
|
||||
log "Pinning optionalDependencies to $VERSION before publishing main package..."
|
||||
node -e "
|
||||
const fs = require('fs');
|
||||
const path = '$ROOT_DIR/apps/cli/package.json';
|
||||
const pkg = JSON.parse(fs.readFileSync(path, 'utf8'));
|
||||
if (pkg.optionalDependencies) {
|
||||
for (const k of Object.keys(pkg.optionalDependencies)) {
|
||||
pkg.optionalDependencies[k] = '$VERSION';
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n');
|
||||
"
|
||||
log_ok "optionalDependencies pinned to $VERSION"
|
||||
|
||||
# -- Publish main kandev package ----------------------------------------------
|
||||
|
||||
echo
|
||||
echo "$(bold "Publishing kandev@$VERSION...")"
|
||||
# Same idempotency handling as the runtime packages: capture output, treat
|
||||
# "already published" as success so partial-failure re-runs converge.
|
||||
# `prepublishOnly` (in package.json) runs `pnpm build` automatically.
|
||||
if main_output="$(cd "$ROOT_DIR/apps/cli" && npm publish --access public --provenance 2>&1)"; then
|
||||
log_ok "kandev@$VERSION published"
|
||||
elif echo "$main_output" | grep -qE "EPUBLISHCONFLICT|cannot publish over the previously published versions|You cannot publish over"; then
|
||||
echo " $(yellow "skip") kandev@$VERSION already published (treated as idempotent success)" >&2
|
||||
ALREADY_PUBLISHED+=("kandev")
|
||||
else
|
||||
echo " $(red "FAIL") Failed to publish kandev@$VERSION:" >&2
|
||||
echo "$main_output" | sed 's/^/ /' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# -- Report -------------------------------------------------------------------
|
||||
# (WORK_DIR cleanup happens via the EXIT trap above.)
|
||||
|
||||
echo
|
||||
echo "$(green "$(bold "All npm packages published successfully!")")"
|
||||
if [[ "${#ALREADY_PUBLISHED[@]}" -gt 0 ]]; then
|
||||
echo " $(yellow "note") The following runtime packages were already published at $VERSION:"
|
||||
for pkg in "${ALREADY_PUBLISHED[@]}"; do
|
||||
echo " - $pkg"
|
||||
done
|
||||
fi
|
||||
@@ -1,812 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# release-pr.sh
|
||||
#
|
||||
# Unified release script for:
|
||||
# - CLI npm release (apps/cli)
|
||||
# - App runtime release (git tag that triggers GitHub Actions release workflow)
|
||||
# - Both in one run
|
||||
#
|
||||
# Versioning model:
|
||||
# - App tag format: vM.m
|
||||
# - CLI npm version format: M.m.0
|
||||
# - Default bump mode: minor (major bump is explicit)
|
||||
#
|
||||
# High-level flow:
|
||||
# 1) Validate preconditions (tools, clean tree, main branch, fetch tags)
|
||||
# 2) Select release target (cli | app | both)
|
||||
# 3) Auto-detect current versions/tags and compute next versions
|
||||
# 4) Show a release plan and require confirmation
|
||||
# 5) Execute release actions:
|
||||
# - CLI: bump package version, install deps, build
|
||||
# - App: generate CHANGELOG.md
|
||||
# - Create release branch, commit, open PR, squash-merge
|
||||
# - App: create/push release tag (after PR merge)
|
||||
# - CLI: npm publish (after PR merge)
|
||||
#
|
||||
# Safety:
|
||||
# - Use --dry-run to preview every action without mutating repo or publishing.
|
||||
# - Use --yes for non-interactive confirmation.
|
||||
# -----------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
CLI_PACKAGE_JSON="$ROOT_DIR/apps/cli/package.json"
|
||||
|
||||
TARGET=""
|
||||
DEFAULT_BUMP="minor"
|
||||
AUTO_YES=0
|
||||
DRY_RUN=0
|
||||
|
||||
CLI_SELECTED=0
|
||||
APP_SELECTED=0
|
||||
|
||||
CURRENT_BRANCH=""
|
||||
CURRENT_CLI_VERSION=""
|
||||
CURRENT_CLI_MAJOR=0
|
||||
CURRENT_CLI_MINOR=0
|
||||
CURRENT_CLI_PATCH=0
|
||||
CURRENT_APP_TAG=""
|
||||
CURRENT_APP_TAG_FOUND=0
|
||||
CURRENT_APP_MAJOR=0
|
||||
CURRENT_APP_MINOR=0
|
||||
|
||||
CLI_BUMP=""
|
||||
APP_BUMP=""
|
||||
NEXT_CLI_VERSION=""
|
||||
NEXT_APP_TAG=""
|
||||
RELEASE_BRANCH=""
|
||||
|
||||
CURRENT_STEP=0
|
||||
TOTAL_STEPS=0
|
||||
|
||||
# -- Formatting helpers -------------------------------------------------------
|
||||
|
||||
bold() { printf '\033[1m%s\033[0m' "$*"; }
|
||||
dim() { printf '\033[2m%s\033[0m' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m' "$*"; }
|
||||
yellow() { printf '\033[33m%s\033[0m' "$*"; }
|
||||
red() { printf '\033[31m%s\033[0m' "$*"; }
|
||||
cyan() { printf '\033[36m%s\033[0m' "$*"; }
|
||||
|
||||
log() { echo " $(dim ">>") $*"; }
|
||||
phase() { echo; echo "$(bold "$*")"; }
|
||||
log_ok() { echo " $(green "ok") $*"; }
|
||||
log_skip() { echo " $(dim "skip") $*"; }
|
||||
|
||||
step() {
|
||||
((CURRENT_STEP++))
|
||||
echo
|
||||
echo "$(bold "[$CURRENT_STEP/$TOTAL_STEPS]") $1"
|
||||
}
|
||||
|
||||
# -- Core helpers --------------------------------------------------------------
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/release/release-pr.sh [options]
|
||||
|
||||
Options:
|
||||
--target <cli|app|both> Release target. If omitted, prompt interactively.
|
||||
--bump <minor|major> Bump mode. Default: minor.
|
||||
--yes Skip confirmation prompts.
|
||||
--dry-run Print actions without making changes/publishes.
|
||||
--help, -h Show help.
|
||||
|
||||
Versioning:
|
||||
- App tags: vM.m
|
||||
- CLI package version: M.m.0
|
||||
|
||||
Prerequisites:
|
||||
- Run from a clean working tree on branch main.
|
||||
- Required tools: git, node, npm, pnpm, git-cliff, gh.
|
||||
|
||||
Examples:
|
||||
# Fully interactive release wizard
|
||||
scripts/release/release-pr.sh
|
||||
|
||||
# Non-interactive CLI minor release
|
||||
scripts/release/release-pr.sh --target cli --bump minor --yes
|
||||
|
||||
# Preview both releases with major bump
|
||||
scripts/release/release-pr.sh --target both --bump major --dry-run
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "$(red "Error:") $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
run_cmd() {
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") $*"
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
}
|
||||
|
||||
confirm() {
|
||||
local prompt="$1"
|
||||
if [[ "$AUTO_YES" -eq 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
echo
|
||||
local answer
|
||||
read -r -p "$(bold "$prompt") [y/N]: " answer
|
||||
answer="$(echo "${answer:-}" | tr '[:upper:]' '[:lower:]')"
|
||||
[[ "$answer" == "y" || "$answer" == "yes" ]]
|
||||
}
|
||||
|
||||
validate_bump() {
|
||||
case "$1" in
|
||||
minor|major) ;;
|
||||
*) die "Invalid bump '$1'. Use 'minor' or 'major'." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
compute_total_steps() {
|
||||
TOTAL_STEPS=1 # create release PR (always)
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && ((TOTAL_STEPS++)) # build CLI
|
||||
[[ "$APP_SELECTED" -eq 1 ]] && ((TOTAL_STEPS++)) # create app tag
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && ((TOTAL_STEPS++)) # publish CLI
|
||||
}
|
||||
|
||||
release_branch_name() {
|
||||
if [[ "$CLI_SELECTED" -eq 1 && "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo "release/app-${NEXT_APP_TAG}-cli-${NEXT_CLI_VERSION}"
|
||||
elif [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
echo "release/cli-${NEXT_CLI_VERSION}"
|
||||
else
|
||||
echo "release/app-${NEXT_APP_TAG}"
|
||||
fi
|
||||
}
|
||||
|
||||
# -- Target selection ----------------------------------------------------------
|
||||
|
||||
parse_target() {
|
||||
case "$1" in
|
||||
cli)
|
||||
CLI_SELECTED=1
|
||||
APP_SELECTED=0
|
||||
;;
|
||||
app)
|
||||
CLI_SELECTED=0
|
||||
APP_SELECTED=1
|
||||
;;
|
||||
both)
|
||||
CLI_SELECTED=1
|
||||
APP_SELECTED=1
|
||||
;;
|
||||
*)
|
||||
die "Invalid target '$1'. Use cli, app, or both."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
select_target_interactive() {
|
||||
echo
|
||||
echo "$(bold "What do you want to release?")"
|
||||
echo " $(cyan "1)") cli $(dim "- npm publish apps/cli")"
|
||||
echo " $(cyan "2)") app $(dim "- git tag for GitHub Actions release")"
|
||||
echo " $(cyan "3)") both $(dim "- cli + app together")"
|
||||
echo
|
||||
local choice
|
||||
read -r -p "$(bold "Choice") [1=cli / 2=app / 3=both]: " choice
|
||||
case "$choice" in
|
||||
1|cli) parse_target "cli" ;;
|
||||
2|app) parse_target "app" ;;
|
||||
3|both) parse_target "both" ;;
|
||||
*) die "Invalid choice '$choice'. Enter 1, 2, 3, cli, app, or both." ;;
|
||||
esac
|
||||
|
||||
local selected=""
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && selected+="cli "
|
||||
[[ "$APP_SELECTED" -eq 1 ]] && selected+="app "
|
||||
log "Selected target: $(bold "$selected")"
|
||||
}
|
||||
|
||||
# -- Version parsing -----------------------------------------------------------
|
||||
|
||||
parse_cli_version() {
|
||||
local version="$1"
|
||||
if [[ ! "$version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))?$ ]]; then
|
||||
die "CLI version '$version' is not valid. Expected M.m.0 format."
|
||||
fi
|
||||
CURRENT_CLI_MAJOR="${BASH_REMATCH[1]}"
|
||||
CURRENT_CLI_MINOR="${BASH_REMATCH[2]}"
|
||||
CURRENT_CLI_PATCH="${BASH_REMATCH[4]:-0}"
|
||||
}
|
||||
|
||||
detect_current_cli_version() {
|
||||
[[ -f "$CLI_PACKAGE_JSON" ]] || die "Missing $CLI_PACKAGE_JSON"
|
||||
CURRENT_CLI_VERSION="$(node -p "require('$CLI_PACKAGE_JSON').version")"
|
||||
parse_cli_version "$CURRENT_CLI_VERSION"
|
||||
log "Current CLI version: $(bold "$CURRENT_CLI_VERSION") $(dim "(from apps/cli/package.json)")"
|
||||
}
|
||||
|
||||
detect_current_app_version() {
|
||||
local tags
|
||||
tags="$(git -C "$ROOT_DIR" tag --list 'v*')"
|
||||
|
||||
CURRENT_APP_TAG_FOUND=0
|
||||
local found=0
|
||||
local best_major=0
|
||||
local best_minor=0
|
||||
local tag
|
||||
|
||||
while IFS= read -r tag; do
|
||||
[[ -z "$tag" ]] && continue
|
||||
if [[ "$tag" =~ ^v([0-9]+)\.([0-9]+)$ ]]; then
|
||||
local major="${BASH_REMATCH[1]}"
|
||||
local minor="${BASH_REMATCH[2]}"
|
||||
if [[ "$found" -eq 0 ]] || (( 10#$major > 10#$best_major )) || \
|
||||
(( 10#$major == 10#$best_major && 10#$minor > 10#$best_minor )); then
|
||||
found=1
|
||||
best_major="$major"
|
||||
best_minor="$minor"
|
||||
fi
|
||||
fi
|
||||
done <<<"$tags"
|
||||
|
||||
CURRENT_APP_MAJOR="$best_major"
|
||||
CURRENT_APP_MINOR="$best_minor"
|
||||
CURRENT_APP_TAG="v${CURRENT_APP_MAJOR}.${CURRENT_APP_MINOR}"
|
||||
CURRENT_APP_TAG_FOUND="$found"
|
||||
|
||||
if [[ "$found" -eq 1 ]]; then
|
||||
log "Current app tag: $(bold "$CURRENT_APP_TAG") $(dim "(latest git tag matching vM.m)")"
|
||||
else
|
||||
log "Current app tag: $(dim "none found") $(dim "(will start at v0.1)")"
|
||||
fi
|
||||
}
|
||||
|
||||
# -- Bump selection ------------------------------------------------------------
|
||||
|
||||
next_minor_pair() {
|
||||
local major="$1"
|
||||
local minor="$2"
|
||||
echo "${major}.$((10#$minor + 1))"
|
||||
}
|
||||
|
||||
next_major_pair() {
|
||||
local major="$1"
|
||||
echo "$((10#$major + 1)).0"
|
||||
}
|
||||
|
||||
choose_bump() {
|
||||
local subject="$1"
|
||||
local current="$2"
|
||||
local next_minor="$3"
|
||||
local next_major="$4"
|
||||
|
||||
if [[ -n "$DEFAULT_BUMP" ]]; then
|
||||
validate_bump "$DEFAULT_BUMP"
|
||||
fi
|
||||
|
||||
if [[ "$AUTO_YES" -eq 1 && -n "$DEFAULT_BUMP" ]]; then
|
||||
echo "$DEFAULT_BUMP"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "$DEFAULT_BUMP" && "$TARGET" != "" ]]; then
|
||||
echo "$DEFAULT_BUMP"
|
||||
return
|
||||
fi
|
||||
|
||||
echo >&2
|
||||
echo "$(bold "$subject version bump") $(dim "(current: $current)")" >&2
|
||||
echo " $(cyan "1)") minor -> $(bold "$next_minor") $(dim "(default)")" >&2
|
||||
echo " $(cyan "2)") major -> $(bold "$next_major")" >&2
|
||||
local choice
|
||||
read -r -p "$(bold "Bump") [1=minor / 2=major]: " choice
|
||||
case "${choice:-1}" in
|
||||
1|minor) echo "minor" ;;
|
||||
2|major) echo "major" ;;
|
||||
*) die "Invalid bump choice '$choice'. Enter 1, 2, minor, or major." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# -- Version computation -------------------------------------------------------
|
||||
|
||||
compute_next_versions() {
|
||||
if [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
local cli_minor_pair cli_major_pair
|
||||
cli_minor_pair="$(next_minor_pair "$CURRENT_CLI_MAJOR" "$CURRENT_CLI_MINOR")"
|
||||
cli_major_pair="$(next_major_pair "$CURRENT_CLI_MAJOR")"
|
||||
|
||||
CLI_BUMP="$(choose_bump "CLI" "${CURRENT_CLI_MAJOR}.${CURRENT_CLI_MINOR}.0" \
|
||||
"${cli_minor_pair}.0" "${cli_major_pair}.0")"
|
||||
validate_bump "$CLI_BUMP"
|
||||
|
||||
if [[ "$CLI_BUMP" == "minor" ]]; then
|
||||
NEXT_CLI_VERSION="${cli_minor_pair}.0"
|
||||
else
|
||||
NEXT_CLI_VERSION="${cli_major_pair}.0"
|
||||
fi
|
||||
log "CLI bump: $CLI_BUMP -> $(bold "$NEXT_CLI_VERSION")"
|
||||
fi
|
||||
|
||||
if [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
local app_minor_pair app_major_pair
|
||||
app_minor_pair="$(next_minor_pair "$CURRENT_APP_MAJOR" "$CURRENT_APP_MINOR")"
|
||||
app_major_pair="$(next_major_pair "$CURRENT_APP_MAJOR")"
|
||||
|
||||
APP_BUMP="$(choose_bump "App" "$CURRENT_APP_TAG" "v${app_minor_pair}" "v${app_major_pair}")"
|
||||
validate_bump "$APP_BUMP"
|
||||
|
||||
if [[ "$APP_BUMP" == "minor" ]]; then
|
||||
NEXT_APP_TAG="v${app_minor_pair}"
|
||||
else
|
||||
NEXT_APP_TAG="v${app_major_pair}"
|
||||
fi
|
||||
log "App bump: $APP_BUMP -> $(bold "$NEXT_APP_TAG")"
|
||||
fi
|
||||
}
|
||||
|
||||
# -- Preconditions -------------------------------------------------------------
|
||||
|
||||
ensure_prerequisites() {
|
||||
phase "Checking prerequisites"
|
||||
|
||||
for tool in git node npm pnpm git-cliff gh; do
|
||||
if command_exists "$tool"; then
|
||||
log_ok "$tool"
|
||||
else
|
||||
die "$tool is required but not found in PATH."
|
||||
fi
|
||||
done
|
||||
|
||||
CURRENT_BRANCH="$(git -C "$ROOT_DIR" rev-parse --abbrev-ref HEAD)"
|
||||
if [[ "$CURRENT_BRANCH" == "main" ]]; then
|
||||
log_ok "On branch $(bold "main")"
|
||||
else
|
||||
die "Release must run from main branch (current: $CURRENT_BRANCH)."
|
||||
fi
|
||||
|
||||
if [[ -n "$(git -C "$ROOT_DIR" status --porcelain)" ]]; then
|
||||
die "Working tree is not clean. Commit or stash changes before releasing."
|
||||
fi
|
||||
log_ok "Working tree is clean"
|
||||
|
||||
log "Fetching tags from origin..."
|
||||
run_cmd git -C "$ROOT_DIR" fetch --tags origin
|
||||
log_ok "Tags fetched"
|
||||
}
|
||||
|
||||
ensure_npm_auth() {
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] || return 0
|
||||
[[ "$DRY_RUN" -eq 1 ]] && return 0
|
||||
|
||||
log "Checking npm authentication..."
|
||||
local npm_user
|
||||
if npm_user="$(npm whoami 2>/dev/null)"; then
|
||||
log_ok "Logged in to npm as $(bold "$npm_user")"
|
||||
else
|
||||
echo
|
||||
echo " $(yellow "npm auth required") — you are not logged in to npm."
|
||||
echo " $(dim "This is needed to publish the CLI package.")"
|
||||
echo
|
||||
if [[ "$AUTO_YES" -eq 1 ]]; then
|
||||
die "npm login required but --yes was specified. Run 'npm login' first."
|
||||
fi
|
||||
local answer
|
||||
read -r -p "$(bold "Run npm login now?") [Y/n]: " answer
|
||||
answer="$(echo "${answer:-y}" | tr '[:upper:]' '[:lower:]')"
|
||||
if [[ "$answer" == "y" || "$answer" == "yes" || -z "$answer" ]]; then
|
||||
npm login
|
||||
# Verify login succeeded
|
||||
if npm_user="$(npm whoami 2>/dev/null)"; then
|
||||
log_ok "Logged in to npm as $(bold "$npm_user")"
|
||||
else
|
||||
die "npm login failed. Cannot publish CLI."
|
||||
fi
|
||||
else
|
||||
die "npm login required to publish CLI. Run 'npm login' and try again."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_app_tag_available() {
|
||||
[[ "$APP_SELECTED" -eq 1 ]] || return 0
|
||||
|
||||
log "Checking tag $(bold "$NEXT_APP_TAG") is available..."
|
||||
if git -C "$ROOT_DIR" rev-parse "$NEXT_APP_TAG" >/dev/null 2>&1; then
|
||||
die "Tag $NEXT_APP_TAG already exists locally."
|
||||
fi
|
||||
|
||||
if git -C "$ROOT_DIR" ls-remote --tags origin "refs/tags/$NEXT_APP_TAG" | grep -q .; then
|
||||
die "Tag $NEXT_APP_TAG already exists on origin."
|
||||
fi
|
||||
log_ok "Tag $(bold "$NEXT_APP_TAG") is available"
|
||||
}
|
||||
|
||||
latest_origin_app_tag() {
|
||||
local refs
|
||||
if ! refs="$(git -C "$ROOT_DIR" ls-remote --tags origin 'refs/tags/v*')"; then
|
||||
return 2
|
||||
fi
|
||||
|
||||
local found=0
|
||||
local best_major=0
|
||||
local best_minor=0
|
||||
local ref tag
|
||||
|
||||
while read -r _ ref; do
|
||||
[[ -z "${ref:-}" ]] && continue
|
||||
tag="${ref#refs/tags/}"
|
||||
tag="${tag%\^\{\}}"
|
||||
if [[ "$tag" =~ ^v([0-9]+)\.([0-9]+)$ ]]; then
|
||||
local major="${BASH_REMATCH[1]}"
|
||||
local minor="${BASH_REMATCH[2]}"
|
||||
if [[ "$found" -eq 0 ]] || (( 10#$major > 10#$best_major )) || \
|
||||
(( 10#$major == 10#$best_major && 10#$minor > 10#$best_minor )); then
|
||||
found=1
|
||||
best_major="$major"
|
||||
best_minor="$minor"
|
||||
fi
|
||||
fi
|
||||
done <<<"$refs"
|
||||
|
||||
[[ "$found" -eq 1 ]] || return 1
|
||||
echo "v${best_major}.${best_minor}"
|
||||
}
|
||||
|
||||
ensure_latest_app_tag_present() {
|
||||
[[ "$APP_SELECTED" -eq 1 ]] || return 0
|
||||
[[ "$DRY_RUN" -eq 1 ]] && return 0
|
||||
|
||||
log "Verifying latest app tag $(bold "$CURRENT_APP_TAG") is present locally..."
|
||||
if [[ "$CURRENT_APP_TAG_FOUND" -eq 0 ]]; then
|
||||
local origin_status
|
||||
if latest_origin_app_tag >/dev/null; then
|
||||
origin_status=0
|
||||
else
|
||||
origin_status="$?"
|
||||
fi
|
||||
if [[ "$origin_status" -eq 0 ]]; then
|
||||
die "No local app tag found, but origin has app tags. Fetch origin tags and retry."
|
||||
fi
|
||||
if [[ "$origin_status" -ne 1 ]]; then
|
||||
die "Could not reach origin to verify app tags. Check your connection and retry."
|
||||
fi
|
||||
log_ok "No prior app tags on origin or locally"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local origin_app_tag
|
||||
if ! origin_app_tag="$(latest_origin_app_tag)"; then
|
||||
die "Could not determine latest app tag from origin. Refusing to generate CHANGELOG.md."
|
||||
fi
|
||||
|
||||
if [[ "$CURRENT_APP_TAG" != "$origin_app_tag" ]]; then
|
||||
die "Latest local app tag is $CURRENT_APP_TAG, but origin latest is $origin_app_tag. Refusing to generate CHANGELOG.md."
|
||||
fi
|
||||
|
||||
if ! git -C "$ROOT_DIR" rev-parse --verify --quiet "refs/tags/$CURRENT_APP_TAG" >/dev/null; then
|
||||
die "Latest app tag $CURRENT_APP_TAG is missing locally after fetching origin tags. Refusing to generate CHANGELOG.md."
|
||||
fi
|
||||
|
||||
log_ok "Latest app tag $(bold "$CURRENT_APP_TAG") is present"
|
||||
}
|
||||
|
||||
# -- Release plan --------------------------------------------------------------
|
||||
|
||||
print_plan() {
|
||||
echo
|
||||
echo "$(bold "=== Release Plan ===")"
|
||||
echo " branch: $(bold "$CURRENT_BRANCH")"
|
||||
if [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
echo " cli: $(bold "$CURRENT_CLI_VERSION") -> $(green "$NEXT_CLI_VERSION") ($CLI_BUMP bump)"
|
||||
fi
|
||||
if [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo " app tag: $(bold "$CURRENT_APP_TAG") -> $(green "$NEXT_APP_TAG") ($APP_BUMP bump)"
|
||||
fi
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " mode: $(yellow "DRY RUN") $(dim "(no changes will be made)")"
|
||||
fi
|
||||
echo
|
||||
|
||||
local n=0
|
||||
if [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
echo " $(dim "Steps for CLI:")"
|
||||
echo " $(dim "1.") Bump version in apps/cli/package.json"
|
||||
echo " $(dim "2.") Install dependencies (pnpm install)"
|
||||
echo " $(dim "3.") Build CLI (pnpm build)"
|
||||
fi
|
||||
if [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo " $(dim "Steps for App:")"
|
||||
echo " $(dim "1.") Generate CHANGELOG.md (git-cliff)"
|
||||
fi
|
||||
echo " $(dim "Steps for Release:")"
|
||||
((n++)); echo " $(dim "$n.") Create release branch and open PR"
|
||||
((n++)); echo " $(dim "$n.") Squash-merge PR into main"
|
||||
if [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
((n++)); echo " $(dim "$n.") Create and push annotated git tag $NEXT_APP_TAG"
|
||||
fi
|
||||
if [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
((n++)); echo " $(dim "$n.") Publish CLI to npm"
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
# -- Release actions -----------------------------------------------------------
|
||||
|
||||
commit_message() {
|
||||
if [[ "$CLI_SELECTED" -eq 1 && "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo "release: app $NEXT_APP_TAG, cli $NEXT_CLI_VERSION"
|
||||
elif [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
echo "release: cli $NEXT_CLI_VERSION"
|
||||
elif [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo "release: app $NEXT_APP_TAG"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
apply_cli_release() {
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] || return 0
|
||||
|
||||
step "Building CLI release"
|
||||
|
||||
log "Bumping CLI version to $(bold "$NEXT_CLI_VERSION")..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") npm version --no-git-tag-version $NEXT_CLI_VERSION"
|
||||
else
|
||||
(cd "$ROOT_DIR/apps/cli" && npm version --no-git-tag-version "$NEXT_CLI_VERSION")
|
||||
fi
|
||||
log_ok "Version bumped in package.json"
|
||||
|
||||
log "Installing dependencies..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") pnpm install --frozen-lockfile"
|
||||
else
|
||||
pnpm -C "$ROOT_DIR/apps" install --frozen-lockfile
|
||||
fi
|
||||
log_ok "Dependencies installed"
|
||||
|
||||
log "Building CLI..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") pnpm -C apps/cli build"
|
||||
else
|
||||
pnpm -C "$ROOT_DIR/apps/cli" build
|
||||
fi
|
||||
log_ok "CLI built"
|
||||
}
|
||||
|
||||
generate_changelog() {
|
||||
[[ "$APP_SELECTED" -eq 1 ]] || return 0
|
||||
|
||||
log "Refreshing origin tags before changelog generation..."
|
||||
run_cmd git -C "$ROOT_DIR" fetch --tags origin
|
||||
ensure_latest_app_tag_present
|
||||
|
||||
log "Generating changelog for $(bold "$NEXT_APP_TAG")..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") git-cliff --tag $NEXT_APP_TAG -o CHANGELOG.md"
|
||||
return 0
|
||||
fi
|
||||
git-cliff --tag "$NEXT_APP_TAG" -o "$ROOT_DIR/CHANGELOG.md"
|
||||
log_ok "CHANGELOG.md updated"
|
||||
}
|
||||
|
||||
create_release_pr() {
|
||||
local msg
|
||||
msg="$(commit_message)"
|
||||
[[ -n "$msg" ]] || return 0
|
||||
|
||||
step "Creating release PR"
|
||||
|
||||
RELEASE_BRANCH="$(release_branch_name)"
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") git checkout -b $RELEASE_BRANCH"
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && echo " $(yellow "[dry-run]") git add apps/cli/package.json"
|
||||
[[ "$APP_SELECTED" -eq 1 ]] && echo " $(yellow "[dry-run]") git add CHANGELOG.md"
|
||||
echo " $(yellow "[dry-run]") git commit -m \"$msg\""
|
||||
echo " $(yellow "[dry-run]") git push -u origin $RELEASE_BRANCH"
|
||||
echo " $(yellow "[dry-run]") gh pr create --title \"$msg\""
|
||||
echo " $(yellow "[dry-run]") gh pr merge --squash --delete-branch"
|
||||
echo " $(yellow "[dry-run]") git checkout main && git pull origin main"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Creating release branch $(bold "$RELEASE_BRANCH")..."
|
||||
git -C "$ROOT_DIR" checkout -b "$RELEASE_BRANCH"
|
||||
log_ok "Branch created"
|
||||
|
||||
log "Staging release files..."
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && git -C "$ROOT_DIR" add apps/cli/package.json
|
||||
[[ "$APP_SELECTED" -eq 1 && -f "$ROOT_DIR/CHANGELOG.md" ]] && git -C "$ROOT_DIR" add CHANGELOG.md
|
||||
|
||||
if git -C "$ROOT_DIR" diff --cached --quiet; then
|
||||
log_skip "No staged changes to commit"
|
||||
git -C "$ROOT_DIR" checkout main
|
||||
git -C "$ROOT_DIR" branch -d "$RELEASE_BRANCH"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Committing: $(dim "$msg")"
|
||||
git -C "$ROOT_DIR" commit -m "$msg"
|
||||
log_ok "Committed"
|
||||
|
||||
log "Pushing release branch..."
|
||||
git -C "$ROOT_DIR" push -u origin "$RELEASE_BRANCH"
|
||||
log_ok "Branch pushed"
|
||||
|
||||
local pr_body
|
||||
pr_body="Automated release PR."$'\n\n'"**Changes:**"
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && pr_body+=$'\n'"- CLI: \`$CURRENT_CLI_VERSION\` → \`$NEXT_CLI_VERSION\`"
|
||||
[[ "$APP_SELECTED" -eq 1 ]] && pr_body+=$'\n'"- App: \`$CURRENT_APP_TAG\` → \`$NEXT_APP_TAG\`"
|
||||
|
||||
log "Creating pull request..."
|
||||
local pr_url
|
||||
pr_url="$(gh pr create \
|
||||
--title "$msg" \
|
||||
--body "$pr_body" \
|
||||
--head "$RELEASE_BRANCH" \
|
||||
--base main)"
|
||||
log_ok "PR created: $(bold "$pr_url")"
|
||||
|
||||
log "Merging PR (squash)..."
|
||||
if gh pr merge "$RELEASE_BRANCH" --squash --delete-branch --subject "$msg" --body ""; then
|
||||
log_ok "PR merged"
|
||||
else
|
||||
echo
|
||||
echo " $(yellow "Auto-merge failed.") Merge the PR manually:"
|
||||
echo " $(cyan "$pr_url")"
|
||||
echo
|
||||
if [[ "$AUTO_YES" -eq 1 ]]; then
|
||||
die "Cannot proceed without PR merge in --yes mode. Merge the PR manually and re-run."
|
||||
fi
|
||||
read -r -p "$(bold "Press Enter after merging the PR...")"
|
||||
# Verify PR was merged
|
||||
local pr_state
|
||||
pr_state="$(gh pr view "$RELEASE_BRANCH" --json state --jq '.state')"
|
||||
if [[ "$pr_state" != "MERGED" ]]; then
|
||||
die "PR is not merged (state: $pr_state). Cannot continue."
|
||||
fi
|
||||
log_ok "PR merged"
|
||||
fi
|
||||
|
||||
log "Switching to main and pulling..."
|
||||
git -C "$ROOT_DIR" checkout main
|
||||
git -C "$ROOT_DIR" pull origin main
|
||||
# Clean up local release branch if it still exists
|
||||
git -C "$ROOT_DIR" branch -d "$RELEASE_BRANCH" 2>/dev/null || true
|
||||
log_ok "main is up to date"
|
||||
}
|
||||
|
||||
create_and_push_app_tag() {
|
||||
[[ "$APP_SELECTED" -eq 1 ]] || return 0
|
||||
|
||||
step "Creating app tag"
|
||||
|
||||
log "Creating annotated tag $(bold "$NEXT_APP_TAG")..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") git tag -a $NEXT_APP_TAG -m \"release: $NEXT_APP_TAG\""
|
||||
echo " $(yellow "[dry-run]") git push origin $NEXT_APP_TAG"
|
||||
return 0
|
||||
fi
|
||||
|
||||
git -C "$ROOT_DIR" tag -a "$NEXT_APP_TAG" -m "release: $NEXT_APP_TAG"
|
||||
log_ok "Tag created"
|
||||
|
||||
log "Pushing tag to origin..."
|
||||
git -C "$ROOT_DIR" push origin "$NEXT_APP_TAG"
|
||||
log_ok "Tag pushed (GitHub Actions release workflow will trigger)"
|
||||
}
|
||||
|
||||
publish_cli() {
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] || return 0
|
||||
|
||||
step "Publishing CLI to npm"
|
||||
|
||||
log "Running npm publish --access public..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") npm publish --access public"
|
||||
return 0
|
||||
fi
|
||||
(cd "$ROOT_DIR/apps/cli" && npm publish --access public)
|
||||
log_ok "Published to npm"
|
||||
}
|
||||
|
||||
# -- Argument parsing ----------------------------------------------------------
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--target)
|
||||
[[ $# -ge 2 ]] || die "--target requires a value."
|
||||
TARGET="$2"
|
||||
shift 2
|
||||
;;
|
||||
--bump)
|
||||
[[ $# -ge 2 ]] || die "--bump requires a value."
|
||||
DEFAULT_BUMP="$2"
|
||||
shift 2
|
||||
;;
|
||||
--yes)
|
||||
AUTO_YES=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Unknown argument '$1'."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# -- Main ----------------------------------------------------------------------
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
validate_bump "$DEFAULT_BUMP"
|
||||
|
||||
echo
|
||||
echo "$(bold "Kandev Release") $(dim "(PR mode)")"
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo "$(yellow " DRY RUN MODE - no changes will be made")"
|
||||
fi
|
||||
|
||||
ensure_prerequisites
|
||||
|
||||
phase "Selecting target and version"
|
||||
|
||||
if [[ -n "$TARGET" ]]; then
|
||||
parse_target "$TARGET"
|
||||
local selected=""
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && selected+="cli "
|
||||
[[ "$APP_SELECTED" -eq 1 ]] && selected+="app "
|
||||
log "Target: $(bold "$selected") $(dim "(from --target flag)")"
|
||||
else
|
||||
# Interactive mode should prompt for target and bump choices.
|
||||
DEFAULT_BUMP=""
|
||||
select_target_interactive
|
||||
fi
|
||||
|
||||
ensure_npm_auth
|
||||
detect_current_cli_version
|
||||
detect_current_app_version
|
||||
compute_next_versions
|
||||
ensure_latest_app_tag_present
|
||||
ensure_app_tag_available
|
||||
compute_total_steps
|
||||
print_plan
|
||||
|
||||
if ! confirm "Proceed with release?"; then
|
||||
echo
|
||||
echo "Release cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
apply_cli_release
|
||||
generate_changelog
|
||||
create_release_pr
|
||||
create_and_push_app_tag
|
||||
publish_cli
|
||||
|
||||
echo
|
||||
echo "$(green "$(bold "Release complete!")")"
|
||||
if [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
echo " $(green "cli:") published $NEXT_CLI_VERSION to npm"
|
||||
fi
|
||||
if [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo " $(green "app:") pushed tag $NEXT_APP_TAG to origin"
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -1,648 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# -----------------------------------------------------------------------------
|
||||
# release.sh
|
||||
#
|
||||
# Unified release script for:
|
||||
# - CLI npm release (apps/cli)
|
||||
# - App runtime release (git tag that triggers GitHub Actions release workflow)
|
||||
# - Both in one run
|
||||
#
|
||||
# Versioning model:
|
||||
# - App tag format: vM.m
|
||||
# - CLI npm version format: M.m.0
|
||||
# - Default bump mode: minor (major bump is explicit)
|
||||
#
|
||||
# High-level flow:
|
||||
# 1) Validate preconditions (tools, clean tree, main branch, fetch tags)
|
||||
# 2) Select release target (cli | app | both)
|
||||
# 3) Auto-detect current versions/tags and compute next versions
|
||||
# 4) Show a release plan and require confirmation
|
||||
# 5) Execute release actions:
|
||||
# - CLI: bump package version, install deps, build, commit/push, npm publish
|
||||
# - App: create/push release tag
|
||||
#
|
||||
# Safety:
|
||||
# - Use --dry-run to preview every action without mutating repo or publishing.
|
||||
# - Use --yes for non-interactive confirmation.
|
||||
# -----------------------------------------------------------------------------
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
CLI_PACKAGE_JSON="$ROOT_DIR/apps/cli/package.json"
|
||||
|
||||
TARGET=""
|
||||
DEFAULT_BUMP="minor"
|
||||
AUTO_YES=0
|
||||
DRY_RUN=0
|
||||
|
||||
CLI_SELECTED=0
|
||||
APP_SELECTED=0
|
||||
|
||||
CURRENT_BRANCH=""
|
||||
CURRENT_CLI_VERSION=""
|
||||
CURRENT_CLI_MAJOR=0
|
||||
CURRENT_CLI_MINOR=0
|
||||
CURRENT_CLI_PATCH=0
|
||||
CURRENT_APP_TAG=""
|
||||
CURRENT_APP_MAJOR=0
|
||||
CURRENT_APP_MINOR=0
|
||||
|
||||
CLI_BUMP=""
|
||||
APP_BUMP=""
|
||||
NEXT_CLI_VERSION=""
|
||||
NEXT_APP_TAG=""
|
||||
|
||||
# -- Formatting helpers -------------------------------------------------------
|
||||
|
||||
bold() { printf '\033[1m%s\033[0m' "$*"; }
|
||||
dim() { printf '\033[2m%s\033[0m' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m' "$*"; }
|
||||
yellow() { printf '\033[33m%s\033[0m' "$*"; }
|
||||
red() { printf '\033[31m%s\033[0m' "$*"; }
|
||||
cyan() { printf '\033[36m%s\033[0m' "$*"; }
|
||||
|
||||
log() { echo " $(dim ">>") $*"; }
|
||||
log_step() { echo; echo "$(bold "[$1]") $2"; }
|
||||
log_ok() { echo " $(green "ok") $*"; }
|
||||
log_skip() { echo " $(dim "skip") $*"; }
|
||||
|
||||
# -- Core helpers --------------------------------------------------------------
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/release/release.sh [options]
|
||||
|
||||
Options:
|
||||
--target <cli|app|both> Release target. If omitted, prompt interactively.
|
||||
--bump <minor|major> Bump mode. Default: minor.
|
||||
--yes Skip confirmation prompts.
|
||||
--dry-run Print actions without making changes/publishes.
|
||||
--help, -h Show help.
|
||||
|
||||
Versioning:
|
||||
- App tags: vM.m
|
||||
- CLI package version: M.m.0
|
||||
|
||||
Prerequisites:
|
||||
- Run from a clean working tree on branch main.
|
||||
- Required tools: git, node, npm, pnpm, git-cliff.
|
||||
|
||||
Examples:
|
||||
# Fully interactive release wizard
|
||||
scripts/release/release.sh
|
||||
|
||||
# Non-interactive CLI minor release
|
||||
scripts/release/release.sh --target cli --bump minor --yes
|
||||
|
||||
# Preview both releases with major bump
|
||||
scripts/release/release.sh --target both --bump major --dry-run
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "$(red "Error:") $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
command_exists() {
|
||||
command -v "$1" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
run_cmd() {
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") $*"
|
||||
return 0
|
||||
fi
|
||||
"$@"
|
||||
}
|
||||
|
||||
confirm() {
|
||||
local prompt="$1"
|
||||
if [[ "$AUTO_YES" -eq 1 ]]; then
|
||||
return 0
|
||||
fi
|
||||
echo
|
||||
local answer
|
||||
read -r -p "$(bold "$prompt") [y/N]: " answer
|
||||
answer="$(echo "${answer:-}" | tr '[:upper:]' '[:lower:]')"
|
||||
[[ "$answer" == "y" || "$answer" == "yes" ]]
|
||||
}
|
||||
|
||||
validate_bump() {
|
||||
case "$1" in
|
||||
minor|major) ;;
|
||||
*) die "Invalid bump '$1'. Use 'minor' or 'major'." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# -- Target selection ----------------------------------------------------------
|
||||
|
||||
parse_target() {
|
||||
case "$1" in
|
||||
cli)
|
||||
CLI_SELECTED=1
|
||||
APP_SELECTED=0
|
||||
;;
|
||||
app)
|
||||
CLI_SELECTED=0
|
||||
APP_SELECTED=1
|
||||
;;
|
||||
both)
|
||||
CLI_SELECTED=1
|
||||
APP_SELECTED=1
|
||||
;;
|
||||
*)
|
||||
die "Invalid target '$1'. Use cli, app, or both."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
select_target_interactive() {
|
||||
echo
|
||||
echo "$(bold "What do you want to release?")"
|
||||
echo " $(cyan "1)") cli $(dim "- npm publish apps/cli")"
|
||||
echo " $(cyan "2)") app $(dim "- git tag for GitHub Actions release")"
|
||||
echo " $(cyan "3)") both $(dim "- cli + app together")"
|
||||
echo
|
||||
local choice
|
||||
read -r -p "$(bold "Choice") [1=cli / 2=app / 3=both]: " choice
|
||||
case "$choice" in
|
||||
1|cli) parse_target "cli" ;;
|
||||
2|app) parse_target "app" ;;
|
||||
3|both) parse_target "both" ;;
|
||||
*) die "Invalid choice '$choice'. Enter 1, 2, 3, cli, app, or both." ;;
|
||||
esac
|
||||
|
||||
local selected=""
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && selected+="cli "
|
||||
[[ "$APP_SELECTED" -eq 1 ]] && selected+="app "
|
||||
log "Selected target: $(bold "$selected")"
|
||||
}
|
||||
|
||||
# -- Version parsing -----------------------------------------------------------
|
||||
|
||||
parse_cli_version() {
|
||||
local version="$1"
|
||||
if [[ ! "$version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))?$ ]]; then
|
||||
die "CLI version '$version' is not valid. Expected M.m.0 format."
|
||||
fi
|
||||
CURRENT_CLI_MAJOR="${BASH_REMATCH[1]}"
|
||||
CURRENT_CLI_MINOR="${BASH_REMATCH[2]}"
|
||||
CURRENT_CLI_PATCH="${BASH_REMATCH[4]:-0}"
|
||||
}
|
||||
|
||||
detect_current_cli_version() {
|
||||
[[ -f "$CLI_PACKAGE_JSON" ]] || die "Missing $CLI_PACKAGE_JSON"
|
||||
CURRENT_CLI_VERSION="$(node -p "require('$CLI_PACKAGE_JSON').version")"
|
||||
parse_cli_version "$CURRENT_CLI_VERSION"
|
||||
log "Current CLI version: $(bold "$CURRENT_CLI_VERSION") $(dim "(from apps/cli/package.json)")"
|
||||
}
|
||||
|
||||
detect_current_app_version() {
|
||||
local tags
|
||||
tags="$(git -C "$ROOT_DIR" tag --list 'v*')"
|
||||
|
||||
local found=0
|
||||
local best_major=0
|
||||
local best_minor=0
|
||||
local tag
|
||||
|
||||
while IFS= read -r tag; do
|
||||
[[ -z "$tag" ]] && continue
|
||||
if [[ "$tag" =~ ^v([0-9]+)\.([0-9]+)(\.[0-9]+)?$ ]]; then
|
||||
local major="${BASH_REMATCH[1]}"
|
||||
local minor="${BASH_REMATCH[2]}"
|
||||
if [[ "$found" -eq 0 ]] || (( 10#$major > 10#$best_major )) || \
|
||||
(( 10#$major == 10#$best_major && 10#$minor > 10#$best_minor )); then
|
||||
found=1
|
||||
best_major="$major"
|
||||
best_minor="$minor"
|
||||
fi
|
||||
fi
|
||||
done <<<"$tags"
|
||||
|
||||
CURRENT_APP_MAJOR="$best_major"
|
||||
CURRENT_APP_MINOR="$best_minor"
|
||||
CURRENT_APP_TAG="v${CURRENT_APP_MAJOR}.${CURRENT_APP_MINOR}"
|
||||
|
||||
if [[ "$found" -eq 1 ]]; then
|
||||
log "Current app tag: $(bold "$CURRENT_APP_TAG") $(dim "(latest git tag matching vM.m)")"
|
||||
else
|
||||
log "Current app tag: $(dim "none found") $(dim "(will start at v0.1)")"
|
||||
fi
|
||||
}
|
||||
|
||||
# -- Bump selection ------------------------------------------------------------
|
||||
|
||||
next_minor_pair() {
|
||||
local major="$1"
|
||||
local minor="$2"
|
||||
echo "${major}.$((10#$minor + 1))"
|
||||
}
|
||||
|
||||
next_major_pair() {
|
||||
local major="$1"
|
||||
echo "$((10#$major + 1)).0"
|
||||
}
|
||||
|
||||
choose_bump() {
|
||||
local subject="$1"
|
||||
local current="$2"
|
||||
local next_minor="$3"
|
||||
local next_major="$4"
|
||||
|
||||
if [[ -n "$DEFAULT_BUMP" ]]; then
|
||||
validate_bump "$DEFAULT_BUMP"
|
||||
fi
|
||||
|
||||
if [[ "$AUTO_YES" -eq 1 && -n "$DEFAULT_BUMP" ]]; then
|
||||
echo "$DEFAULT_BUMP"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -n "$DEFAULT_BUMP" && "$TARGET" != "" ]]; then
|
||||
echo "$DEFAULT_BUMP"
|
||||
return
|
||||
fi
|
||||
|
||||
echo >&2
|
||||
echo "$(bold "$subject version bump") $(dim "(current: $current)")" >&2
|
||||
echo " $(cyan "1)") minor -> $(bold "$next_minor") $(dim "(default)")" >&2
|
||||
echo " $(cyan "2)") major -> $(bold "$next_major")" >&2
|
||||
local choice
|
||||
read -r -p "$(bold "Bump") [1=minor / 2=major]: " choice
|
||||
case "${choice:-1}" in
|
||||
1|minor) echo "minor" ;;
|
||||
2|major) echo "major" ;;
|
||||
*) die "Invalid bump choice '$choice'. Enter 1, 2, minor, or major." ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# -- Version computation -------------------------------------------------------
|
||||
|
||||
compute_next_versions() {
|
||||
if [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
local cli_minor_pair cli_major_pair
|
||||
cli_minor_pair="$(next_minor_pair "$CURRENT_CLI_MAJOR" "$CURRENT_CLI_MINOR")"
|
||||
cli_major_pair="$(next_major_pair "$CURRENT_CLI_MAJOR")"
|
||||
|
||||
CLI_BUMP="$(choose_bump "CLI" "${CURRENT_CLI_MAJOR}.${CURRENT_CLI_MINOR}.0" \
|
||||
"${cli_minor_pair}.0" "${cli_major_pair}.0")"
|
||||
validate_bump "$CLI_BUMP"
|
||||
|
||||
if [[ "$CLI_BUMP" == "minor" ]]; then
|
||||
NEXT_CLI_VERSION="${cli_minor_pair}.0"
|
||||
else
|
||||
NEXT_CLI_VERSION="${cli_major_pair}.0"
|
||||
fi
|
||||
log "CLI bump: $CLI_BUMP -> $(bold "$NEXT_CLI_VERSION")"
|
||||
fi
|
||||
|
||||
if [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
local app_minor_pair app_major_pair
|
||||
app_minor_pair="$(next_minor_pair "$CURRENT_APP_MAJOR" "$CURRENT_APP_MINOR")"
|
||||
app_major_pair="$(next_major_pair "$CURRENT_APP_MAJOR")"
|
||||
|
||||
APP_BUMP="$(choose_bump "App" "$CURRENT_APP_TAG" "v${app_minor_pair}" "v${app_major_pair}")"
|
||||
validate_bump "$APP_BUMP"
|
||||
|
||||
if [[ "$APP_BUMP" == "minor" ]]; then
|
||||
NEXT_APP_TAG="v${app_minor_pair}"
|
||||
else
|
||||
NEXT_APP_TAG="v${app_major_pair}"
|
||||
fi
|
||||
log "App bump: $APP_BUMP -> $(bold "$NEXT_APP_TAG")"
|
||||
fi
|
||||
}
|
||||
|
||||
# -- Preconditions -------------------------------------------------------------
|
||||
|
||||
ensure_prerequisites() {
|
||||
log_step "1/5" "Checking prerequisites"
|
||||
|
||||
for tool in git node npm pnpm git-cliff; do
|
||||
if command_exists "$tool"; then
|
||||
log_ok "$tool"
|
||||
else
|
||||
die "$tool is required but not found in PATH."
|
||||
fi
|
||||
done
|
||||
|
||||
CURRENT_BRANCH="$(git -C "$ROOT_DIR" rev-parse --abbrev-ref HEAD)"
|
||||
if [[ "$CURRENT_BRANCH" == "main" ]]; then
|
||||
log_ok "On branch $(bold "main")"
|
||||
else
|
||||
die "Release must run from main branch (current: $CURRENT_BRANCH)."
|
||||
fi
|
||||
|
||||
if [[ -n "$(git -C "$ROOT_DIR" status --porcelain)" ]]; then
|
||||
die "Working tree is not clean. Commit or stash changes before releasing."
|
||||
fi
|
||||
log_ok "Working tree is clean"
|
||||
|
||||
log "Fetching tags from origin..."
|
||||
run_cmd git -C "$ROOT_DIR" fetch --tags origin
|
||||
log_ok "Tags fetched"
|
||||
}
|
||||
|
||||
ensure_npm_auth() {
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] || return 0
|
||||
[[ "$DRY_RUN" -eq 1 ]] && return 0
|
||||
|
||||
log "Checking npm authentication..."
|
||||
local npm_user
|
||||
if npm_user="$(npm whoami 2>/dev/null)"; then
|
||||
log_ok "Logged in to npm as $(bold "$npm_user")"
|
||||
else
|
||||
echo
|
||||
echo " $(yellow "npm auth required") — you are not logged in to npm."
|
||||
echo " $(dim "This is needed to publish the CLI package.")"
|
||||
echo
|
||||
if [[ "$AUTO_YES" -eq 1 ]]; then
|
||||
die "npm login required but --yes was specified. Run 'npm login' first."
|
||||
fi
|
||||
local answer
|
||||
read -r -p "$(bold "Run npm login now?") [Y/n]: " answer
|
||||
answer="$(echo "${answer:-y}" | tr '[:upper:]' '[:lower:]')"
|
||||
if [[ "$answer" == "y" || "$answer" == "yes" || -z "$answer" ]]; then
|
||||
npm login
|
||||
# Verify login succeeded
|
||||
if npm_user="$(npm whoami 2>/dev/null)"; then
|
||||
log_ok "Logged in to npm as $(bold "$npm_user")"
|
||||
else
|
||||
die "npm login failed. Cannot publish CLI."
|
||||
fi
|
||||
else
|
||||
die "npm login required to publish CLI. Run 'npm login' and try again."
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
ensure_app_tag_available() {
|
||||
[[ "$APP_SELECTED" -eq 1 ]] || return 0
|
||||
|
||||
log "Checking tag $(bold "$NEXT_APP_TAG") is available..."
|
||||
if git -C "$ROOT_DIR" rev-parse "$NEXT_APP_TAG" >/dev/null 2>&1; then
|
||||
die "Tag $NEXT_APP_TAG already exists locally."
|
||||
fi
|
||||
|
||||
if git -C "$ROOT_DIR" ls-remote --tags origin "refs/tags/$NEXT_APP_TAG" | grep -q .; then
|
||||
die "Tag $NEXT_APP_TAG already exists on origin."
|
||||
fi
|
||||
log_ok "Tag $(bold "$NEXT_APP_TAG") is available"
|
||||
}
|
||||
|
||||
# -- Release plan --------------------------------------------------------------
|
||||
|
||||
print_plan() {
|
||||
echo
|
||||
echo "$(bold "=== Release Plan ===")"
|
||||
echo " branch: $(bold "$CURRENT_BRANCH")"
|
||||
if [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
echo " cli: $(bold "$CURRENT_CLI_VERSION") -> $(green "$NEXT_CLI_VERSION") ($CLI_BUMP bump)"
|
||||
fi
|
||||
if [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo " app tag: $(bold "$CURRENT_APP_TAG") -> $(green "$NEXT_APP_TAG") ($APP_BUMP bump)"
|
||||
fi
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " mode: $(yellow "DRY RUN") $(dim "(no changes will be made)")"
|
||||
fi
|
||||
echo
|
||||
|
||||
if [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
echo " $(dim "Steps for CLI:")"
|
||||
echo " $(dim "1.") Bump version in apps/cli/package.json"
|
||||
echo " $(dim "2.") Install dependencies (pnpm install)"
|
||||
echo " $(dim "3.") Build CLI (pnpm build)"
|
||||
echo " $(dim "4.") Commit and push to main"
|
||||
echo " $(dim "5.") Publish to npm"
|
||||
fi
|
||||
if [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo " $(dim "Steps for App:")"
|
||||
echo " $(dim "1.") Generate CHANGELOG.md (git-cliff)"
|
||||
echo " $(dim "2.") Commit and push changelog"
|
||||
echo " $(dim "3.") Create annotated git tag $NEXT_APP_TAG"
|
||||
echo " $(dim "4.") Push tag to origin (triggers GitHub Actions)"
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
# -- Release actions -----------------------------------------------------------
|
||||
|
||||
commit_message() {
|
||||
if [[ "$CLI_SELECTED" -eq 1 && "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo "release: app $NEXT_APP_TAG, cli $NEXT_CLI_VERSION"
|
||||
elif [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
echo "release: cli $NEXT_CLI_VERSION"
|
||||
elif [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo "release: app $NEXT_APP_TAG"
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
apply_cli_release() {
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] || return 0
|
||||
|
||||
log_step "3/5" "Building CLI release"
|
||||
|
||||
log "Bumping CLI version to $(bold "$NEXT_CLI_VERSION")..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") npm version --no-git-tag-version $NEXT_CLI_VERSION"
|
||||
else
|
||||
(cd "$ROOT_DIR/apps/cli" && npm version --no-git-tag-version "$NEXT_CLI_VERSION")
|
||||
fi
|
||||
log_ok "Version bumped in package.json"
|
||||
|
||||
log "Installing dependencies..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") pnpm install --frozen-lockfile"
|
||||
else
|
||||
pnpm -C "$ROOT_DIR/apps" install --frozen-lockfile
|
||||
fi
|
||||
log_ok "Dependencies installed"
|
||||
|
||||
log "Building CLI..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") pnpm -C apps/cli build"
|
||||
else
|
||||
pnpm -C "$ROOT_DIR/apps/cli" build
|
||||
fi
|
||||
log_ok "CLI built"
|
||||
}
|
||||
|
||||
generate_changelog() {
|
||||
[[ "$APP_SELECTED" -eq 1 ]] || return 0
|
||||
|
||||
log "Generating changelog for $(bold "$NEXT_APP_TAG")..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") git-cliff --tag $NEXT_APP_TAG -o CHANGELOG.md"
|
||||
return 0
|
||||
fi
|
||||
git-cliff --tag "$NEXT_APP_TAG" -o "$ROOT_DIR/CHANGELOG.md"
|
||||
log_ok "CHANGELOG.md updated"
|
||||
}
|
||||
|
||||
commit_and_push_if_needed() {
|
||||
local msg
|
||||
msg="$(commit_message)"
|
||||
[[ -n "$msg" ]] || return 0
|
||||
|
||||
log_step "4/5" "Committing and pushing"
|
||||
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && echo " $(yellow "[dry-run]") git add apps/cli/package.json"
|
||||
[[ "$APP_SELECTED" -eq 1 ]] && echo " $(yellow "[dry-run]") git add CHANGELOG.md"
|
||||
echo " $(yellow "[dry-run]") git commit -m \"$msg\""
|
||||
echo " $(yellow "[dry-run]") git push origin $CURRENT_BRANCH"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Staging release files..."
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && git -C "$ROOT_DIR" add apps/cli/package.json
|
||||
[[ "$APP_SELECTED" -eq 1 && -f "$ROOT_DIR/CHANGELOG.md" ]] && git -C "$ROOT_DIR" add CHANGELOG.md
|
||||
if git -C "$ROOT_DIR" diff --cached --quiet; then
|
||||
log_skip "No staged changes to commit"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "Committing: $(dim "$msg")"
|
||||
git -C "$ROOT_DIR" commit -m "$msg"
|
||||
log_ok "Committed"
|
||||
|
||||
log "Pushing to origin/$CURRENT_BRANCH..."
|
||||
git -C "$ROOT_DIR" push origin "$CURRENT_BRANCH"
|
||||
log_ok "Pushed"
|
||||
}
|
||||
|
||||
create_and_push_app_tag() {
|
||||
[[ "$APP_SELECTED" -eq 1 ]] || return 0
|
||||
|
||||
if [[ "$CLI_SELECTED" -eq 0 ]]; then
|
||||
log_step "3/5" "Creating app tag"
|
||||
else
|
||||
log_step "4/5" "Creating app tag"
|
||||
fi
|
||||
|
||||
log "Creating annotated tag $(bold "$NEXT_APP_TAG")..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") git tag -a $NEXT_APP_TAG -m \"release: $NEXT_APP_TAG\""
|
||||
echo " $(yellow "[dry-run]") git push origin $NEXT_APP_TAG"
|
||||
return 0
|
||||
fi
|
||||
|
||||
git -C "$ROOT_DIR" tag -a "$NEXT_APP_TAG" -m "release: $NEXT_APP_TAG"
|
||||
log_ok "Tag created"
|
||||
|
||||
log "Pushing tag to origin..."
|
||||
git -C "$ROOT_DIR" push origin "$NEXT_APP_TAG"
|
||||
log_ok "Tag pushed (GitHub Actions release workflow will trigger)"
|
||||
}
|
||||
|
||||
publish_cli() {
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] || return 0
|
||||
|
||||
log_step "5/5" "Publishing CLI to npm"
|
||||
|
||||
log "Running npm publish --access public..."
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo " $(yellow "[dry-run]") npm publish --access public"
|
||||
return 0
|
||||
fi
|
||||
(cd "$ROOT_DIR/apps/cli" && npm publish --access public)
|
||||
log_ok "Published to npm"
|
||||
}
|
||||
|
||||
# -- Argument parsing ----------------------------------------------------------
|
||||
|
||||
parse_args() {
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--target)
|
||||
[[ $# -ge 2 ]] || die "--target requires a value."
|
||||
TARGET="$2"
|
||||
shift 2
|
||||
;;
|
||||
--bump)
|
||||
[[ $# -ge 2 ]] || die "--bump requires a value."
|
||||
DEFAULT_BUMP="$2"
|
||||
shift 2
|
||||
;;
|
||||
--yes)
|
||||
AUTO_YES=1
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=1
|
||||
shift
|
||||
;;
|
||||
--help|-h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
die "Unknown argument '$1'."
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
# -- Main ----------------------------------------------------------------------
|
||||
|
||||
main() {
|
||||
parse_args "$@"
|
||||
validate_bump "$DEFAULT_BUMP"
|
||||
|
||||
echo
|
||||
echo "$(bold "Kandev Release")"
|
||||
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||
echo "$(yellow " DRY RUN MODE - no changes will be made")"
|
||||
fi
|
||||
|
||||
ensure_prerequisites
|
||||
|
||||
log_step "2/5" "Selecting target and version"
|
||||
|
||||
if [[ -n "$TARGET" ]]; then
|
||||
parse_target "$TARGET"
|
||||
local selected=""
|
||||
[[ "$CLI_SELECTED" -eq 1 ]] && selected+="cli "
|
||||
[[ "$APP_SELECTED" -eq 1 ]] && selected+="app "
|
||||
log "Target: $(bold "$selected") $(dim "(from --target flag)")"
|
||||
else
|
||||
# Interactive mode should prompt for target and bump choices.
|
||||
DEFAULT_BUMP=""
|
||||
select_target_interactive
|
||||
fi
|
||||
|
||||
ensure_npm_auth
|
||||
detect_current_cli_version
|
||||
detect_current_app_version
|
||||
compute_next_versions
|
||||
ensure_app_tag_available
|
||||
print_plan
|
||||
|
||||
if ! confirm "Proceed with release?"; then
|
||||
echo
|
||||
echo "Release cancelled."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
apply_cli_release
|
||||
generate_changelog
|
||||
commit_and_push_if_needed
|
||||
create_and_push_app_tag
|
||||
publish_cli
|
||||
|
||||
echo
|
||||
echo "$(green "$(bold "Release complete!")")"
|
||||
if [[ "$CLI_SELECTED" -eq 1 ]]; then
|
||||
echo " $(green "cli:") published $NEXT_CLI_VERSION to npm"
|
||||
fi
|
||||
if [[ "$APP_SELECTED" -eq 1 ]]; then
|
||||
echo " $(green "app:") pushed tag $NEXT_APP_TAG to origin"
|
||||
fi
|
||||
echo
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Executable
+203
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env bash
|
||||
# Update the kdlbs/homebrew-kandev tap with a new formula for the given release.
|
||||
#
|
||||
# Downloads .sha256 files from the GitHub release to populate the formula,
|
||||
# generates Formula/kandev.rb, and pushes (or opens a PR).
|
||||
#
|
||||
# Usage:
|
||||
# update-homebrew-tap.sh <version> <tag>
|
||||
#
|
||||
# Arguments:
|
||||
# version SemVer string (e.g. 0.17.0)
|
||||
# tag Git tag (e.g. v0.17.0)
|
||||
#
|
||||
# Environment:
|
||||
# HOMEBREW_TAP_REPO Override tap repo (default: kdlbs/homebrew-kandev).
|
||||
# HOMEBREW_TAP_DEPLOY_KEY Private SSH key (PEM) for the tap repo. When set,
|
||||
# clone via SSH and push directly to main. This is
|
||||
# the default CI path. Deploy keys cannot open PRs.
|
||||
# HOMEBREW_TAP_PUSH When no deploy key is present, set to "1" to push
|
||||
# directly via `gh`/HTTPS instead of opening a PR.
|
||||
set -euo pipefail
|
||||
|
||||
VERSION="${1:?Usage: $0 <version> <tag>}"
|
||||
TAG="${2:?Usage: $0 <version> <tag>}"
|
||||
TAP_REPO="${HOMEBREW_TAP_REPO:-kdlbs/homebrew-kandev}"
|
||||
PUSH_DIRECT="${HOMEBREW_TAP_PUSH:-0}"
|
||||
|
||||
bold() { printf '\033[1m%s\033[0m' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m' "$*"; }
|
||||
red() { printf '\033[31m%s\033[0m' "$*"; }
|
||||
|
||||
log() { echo " >> $*"; }
|
||||
log_ok() { echo " $(green "ok") $*"; }
|
||||
|
||||
die() {
|
||||
echo "$(red "Error:") $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# -- Determine clone strategy: deploy key (SSH) or gh (HTTPS) -----------------
|
||||
|
||||
CLONE_METHOD="gh"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
TAP_DIR="$WORK_DIR/tap"
|
||||
SSH_KEY_FILE=""
|
||||
KNOWN_HOSTS_FILE=""
|
||||
|
||||
cleanup() {
|
||||
[[ -n "${SSH_KEY_FILE:-}" && -f "$SSH_KEY_FILE" ]] && rm -f "$SSH_KEY_FILE"
|
||||
[[ -n "${KNOWN_HOSTS_FILE:-}" && -f "$KNOWN_HOSTS_FILE" ]] && rm -f "$KNOWN_HOSTS_FILE"
|
||||
[[ -n "${WORK_DIR:-}" && -d "$WORK_DIR" ]] && rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -n "${HOMEBREW_TAP_DEPLOY_KEY:-}" ]]; then
|
||||
CLONE_METHOD="ssh"
|
||||
# Materialize the deploy key + a custom GIT_SSH_COMMAND so it doesn't pollute ~/.ssh
|
||||
SSH_KEY_FILE="$(mktemp)"
|
||||
chmod 600 "$SSH_KEY_FILE"
|
||||
printf '%s\n' "$HOMEBREW_TAP_DEPLOY_KEY" > "$SSH_KEY_FILE"
|
||||
# Ensure trailing newline (some secret stores strip it; openssh requires it)
|
||||
if [[ "$(tail -c1 "$SSH_KEY_FILE")" != "" ]]; then
|
||||
echo "" >> "$SSH_KEY_FILE"
|
||||
fi
|
||||
|
||||
# Pin GitHub's published SSH host keys so we don't trust whatever key the
|
||||
# remote presents. Source: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
|
||||
KNOWN_HOSTS_FILE="$(mktemp)"
|
||||
chmod 600 "$KNOWN_HOSTS_FILE"
|
||||
cat > "$KNOWN_HOSTS_FILE" <<'EOF'
|
||||
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
|
||||
github.com ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEmKSENjQEezOmxkZMy7opKgwFB9nkt5YRrYMjNuG5N87uRgg6CLrbo5wAdT/y6v0mKV0U2w0WZ2YB/++Tpockg=
|
||||
github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
|
||||
EOF
|
||||
export GIT_SSH_COMMAND="ssh -i $SSH_KEY_FILE -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$KNOWN_HOSTS_FILE -o LogLevel=ERROR"
|
||||
log "Using SSH deploy key for tap auth (pinned github.com host keys)"
|
||||
fi
|
||||
|
||||
# -- Fetch sha256 values from GitHub release ----------------------------------
|
||||
|
||||
fetch_sha256() {
|
||||
local platform="$1"
|
||||
local sha_file="kandev-${platform}.tar.gz.sha256"
|
||||
local content
|
||||
# Download into WORK_DIR so the EXIT trap cleans it up automatically.
|
||||
content="$(gh release download "$TAG" --pattern "$sha_file" --dir "$WORK_DIR" 2>/dev/null && cat "$WORK_DIR/$sha_file")" || {
|
||||
die "Failed to download $sha_file from release $TAG."
|
||||
}
|
||||
# sha256 files contain: <hash> <filename> — extract just the hash
|
||||
echo "$content" | awk '{print $1}'
|
||||
}
|
||||
|
||||
log "Fetching sha256 values from GitHub release $TAG..."
|
||||
SHA_MACOS_ARM64="$(fetch_sha256 macos-arm64)"
|
||||
SHA_MACOS_X64="$(fetch_sha256 macos-x64)"
|
||||
SHA_LINUX_ARM64="$(fetch_sha256 linux-arm64)"
|
||||
SHA_LINUX_X64="$(fetch_sha256 linux-x64)"
|
||||
log_ok "sha256 values retrieved"
|
||||
|
||||
# -- Clone tap repo -----------------------------------------------------------
|
||||
# (WORK_DIR and TAP_DIR were created near the top so the EXIT trap covers them.)
|
||||
|
||||
log "Cloning $TAP_REPO..."
|
||||
if [[ "$CLONE_METHOD" == "ssh" ]]; then
|
||||
git clone "git@github.com:${TAP_REPO}.git" "$TAP_DIR"
|
||||
else
|
||||
gh repo clone "$TAP_REPO" "$TAP_DIR"
|
||||
fi
|
||||
log_ok "Tap cloned to $TAP_DIR"
|
||||
|
||||
# -- Generate formula ---------------------------------------------------------
|
||||
|
||||
FORMULA_PATH="$TAP_DIR/Formula/kandev.rb"
|
||||
mkdir -p "$(dirname "$FORMULA_PATH")"
|
||||
|
||||
GITHUB_BASE="https://github.com/kdlbs/kandev/releases/download/${TAG}"
|
||||
|
||||
cat > "$FORMULA_PATH" <<EOF
|
||||
class Kandev < Formula
|
||||
desc "Manage tasks, orchestrate agents, review changes, and ship value"
|
||||
homepage "https://github.com/kdlbs/kandev"
|
||||
license "AGPL-3.0-only"
|
||||
version "$VERSION"
|
||||
|
||||
# Node is required: the CLI launcher and Next.js standalone server both need it.
|
||||
depends_on "node"
|
||||
|
||||
on_macos do
|
||||
if Hardware::CPU.arm?
|
||||
url "${GITHUB_BASE}/kandev-macos-arm64.tar.gz"
|
||||
sha256 "$SHA_MACOS_ARM64"
|
||||
else
|
||||
url "${GITHUB_BASE}/kandev-macos-x64.tar.gz"
|
||||
sha256 "$SHA_MACOS_X64"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
if Hardware::CPU.arm?
|
||||
url "${GITHUB_BASE}/kandev-linux-arm64.tar.gz"
|
||||
sha256 "$SHA_LINUX_ARM64"
|
||||
else
|
||||
url "${GITHUB_BASE}/kandev-linux-x64.tar.gz"
|
||||
sha256 "$SHA_LINUX_X64"
|
||||
end
|
||||
end
|
||||
|
||||
def install
|
||||
libexec.install Dir["*"]
|
||||
# cli/bin/cli.js has #!/usr/bin/env node shebang. (bin/"kandev").write_env_script
|
||||
# creates a wrapper at \$HOMEBREW_PREFIX/bin/kandev that sets KANDEV_BUNDLE_DIR
|
||||
# (so the CLI launcher finds bin/ and web/ in the Cellar) and KANDEV_VERSION
|
||||
# (read by run.ts at startup so the launcher logs "release: X.Y.Z" instead of
|
||||
# "release: (env)"). Calling write_env_script on the bin directory itself would
|
||||
# name the wrapper after the target's basename (cli.js), giving the wrong name.
|
||||
(bin/"kandev").write_env_script libexec/"cli/bin/cli.js",
|
||||
KANDEV_BUNDLE_DIR: libexec.to_s,
|
||||
KANDEV_VERSION: version.to_s
|
||||
end
|
||||
|
||||
test do
|
||||
assert_match "kandev launcher", shell_output("#{bin}/kandev --help")
|
||||
end
|
||||
end
|
||||
EOF
|
||||
|
||||
log_ok "Formula written to Formula/kandev.rb"
|
||||
|
||||
# -- Commit and push (or open PR) ---------------------------------------------
|
||||
|
||||
cd "$TAP_DIR"
|
||||
git config user.email "release-bot@kandev"
|
||||
git config user.name "kandev release bot"
|
||||
|
||||
git add Formula/kandev.rb
|
||||
|
||||
if git diff --cached --quiet; then
|
||||
log "Formula unchanged — nothing to commit"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git commit -m "kandev $VERSION"
|
||||
|
||||
# Deploy keys can push but cannot open PRs (PR creation requires user/app auth).
|
||||
# So when using SSH (deploy key path) or HOMEBREW_TAP_PUSH=1, push directly.
|
||||
if [[ "$CLONE_METHOD" == "ssh" || "$PUSH_DIRECT" == "1" ]]; then
|
||||
git push origin HEAD:main
|
||||
log_ok "Pushed directly to main in $TAP_REPO"
|
||||
else
|
||||
BRANCH="update-kandev-$VERSION"
|
||||
git branch -m "$BRANCH"
|
||||
git push -u origin "$BRANCH"
|
||||
gh pr create \
|
||||
--repo "$TAP_REPO" \
|
||||
--title "kandev $VERSION" \
|
||||
--body "Updates formula to [kandev $VERSION](https://github.com/kdlbs/kandev/releases/tag/$TAG)." \
|
||||
--head "$BRANCH" \
|
||||
--base main
|
||||
log_ok "PR opened in $TAP_REPO"
|
||||
fi
|
||||
|
||||
# WORK_DIR cleanup happens via the EXIT trap.
|
||||
echo "$(green "$(bold "Homebrew tap updated to $VERSION!")")"
|
||||
Reference in New Issue
Block a user