@kal-elsam/kairo-runtime 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,6 +12,10 @@ The npm package (`@kal-elsam/kairo-runtime`) is how Kairo Runtime is distributed
12
12
  product identity is the local control plane: setup, status, sync, doctor (with `update`
13
13
  as a technical alias).
14
14
 
15
+ `@kal-elsam/harness` remains available as a **compatibility bridge** that delegates to
16
+ Kairo Runtime and prints a migration warning. Prefer `@kal-elsam/kairo-runtime` and the
17
+ `kairo` CLI for new installs. See [`packages/harness-bridge/README.md`](packages/harness-bridge/README.md).
18
+
15
19
  Terminal UX aims for Pi-like clarity (clear modes, non-interactive flags, extensible
16
20
  commands) without depending on Pi as a runtime or adding a Pi adapter.
17
21
 
@@ -47,14 +51,14 @@ curl -fsSL https://raw.githubusercontent.com/Kal-elSam/harness/main/scripts/inst
47
51
  The bootstrap installer:
48
52
 
49
53
  - requires Node.js 20.12+ and npm
50
- - runs `@kal-elsam/kairo-runtime` via `npx` (or `npm exec`)
51
- - ends with `kairo setup --dry-run` by default (no agent configs, no `~/.harness` writes)
54
+ - installs `@kal-elsam/kairo-runtime` globally (`kairo` CLI)
55
+ - runs `kairo setup --dry-run` by default (no agent configs, no `~/.harness` writes)
52
56
  - never uses `sudo`, never modifies shell profiles, and never installs AI apps
53
57
 
54
58
  Apply the plan when you are ready:
55
59
 
56
60
  ```bash
57
- npx @kal-elsam/kairo-runtime --yes
61
+ kairo setup --yes
58
62
  # or
59
63
  curl -fsSL https://raw.githubusercontent.com/Kal-elSam/harness/main/scripts/install.sh | sh -s -- --yes
60
64
  ```
@@ -62,8 +66,8 @@ curl -fsSL https://raw.githubusercontent.com/Kal-elSam/harness/main/scripts/inst
62
66
  CI, scripts, and advanced non-interactive configure:
63
67
 
64
68
  ```bash
65
- npx @kal-elsam/kairo-runtime install --agents cursor,codex --yes
66
- npx @kal-elsam/kairo-runtime setup --yes --agents all
69
+ kairo install --agents cursor,codex --yes
70
+ kairo setup --yes --agents all
67
71
  ```
68
72
 
69
73
  Passthrough examples:
@@ -79,11 +83,11 @@ Control plane:
79
83
  kairo status
80
84
  kairo sync
81
85
  kairo upgrade --dry-run
82
- npx @kal-elsam/kairo-runtime@latest setup --yes
86
+ kairo setup --yes
83
87
  ```
84
88
 
85
89
  After `install.sh --yes`, verify health with `kairo status`, repair drift with `kairo sync`,
86
- and move to the latest package with `npx @kal-elsam/kairo-runtime@latest setup --yes`.
90
+ and preview upgrades with `kairo upgrade --dry-run`.
87
91
 
88
92
  ### Version and updates
89
93
 
@@ -649,7 +653,23 @@ Install the harness in enterprise mode because this project will have AI, API, D
649
653
 
650
654
  ## Publishing
651
655
 
652
- Published on npm as `@kal-elsam/kairo-runtime`. Releases use **npm Trusted Publishing/OIDC** from GitHub Actions — no `NPM_TOKEN`.
656
+ Published on npm as `@kal-elsam/kairo-runtime`. Releases use **npm Trusted Publishing/OIDC**
657
+ from GitHub Actions — no `NPM_TOKEN`.
658
+
659
+ `@kal-elsam/harness` is published separately as a compatibility bridge from
660
+ `packages/harness-bridge/` (see bridge README).
661
+
662
+ ### Git tags (package-aware)
663
+
664
+ | Tag pattern | Package | Publish root |
665
+ |---|---|---|
666
+ | `kairo-runtime-v*` | `@kal-elsam/kairo-runtime` | repo root |
667
+ | `harness-bridge-v*` | `@kal-elsam/harness` | `packages/harness-bridge/` |
668
+ | `v*` | legacy / historical root releases | repo root |
669
+
670
+ Kairo Runtime releases should use `kairo-runtime-vX.Y.Z` going forward. The first
671
+ bootstrap used `v0.1.0`; later releases use package-aware tags such as
672
+ `kairo-runtime-v0.1.1`.
653
673
 
654
674
  Before tagging a new version:
655
675
 
@@ -684,37 +704,74 @@ to end:
684
704
  rollback apply (with safety backup), `uninstall`.
685
705
  - **workspace:** `install --scope=workspace`, `doctor`, `update --dry-run`.
686
706
 
687
- Release flow:
707
+ Release flow (Kairo Runtime):
688
708
 
689
709
  ```bash
690
710
  # bump version in package.json and package-lock.json
691
711
  git add .
692
- git commit -m "chore: release 0.5.0"
712
+ git commit -m "chore: release kairo-runtime 0.1.1"
693
713
  npm run release:check
694
- git tag v0.5.0
714
+ git tag kairo-runtime-v0.1.1
695
715
  git push origin main
696
- git push origin v0.5.0
716
+ git push origin kairo-runtime-v0.1.1
697
717
  ```
698
718
 
719
+ Bridge release flow:
720
+
721
+ ```bash
722
+ # bump packages/harness-bridge/package.json (+ lockfile)
723
+ git add .
724
+ git commit -m "chore: release harness bridge 0.30.0"
725
+ npm run release:check
726
+ git tag harness-bridge-v0.30.0
727
+ git push origin main
728
+ git push origin harness-bridge-v0.30.0
729
+ ```
730
+
731
+ Legacy `v*` tags still publish from the repo root for historical continuity.
732
+
699
733
  After npm publishes the tag, verify published provenance against git and the registry:
700
734
 
701
735
  ```bash
702
736
  git fetch --tags origin
703
737
  git fetch origin main
704
- npm run release:published -- --version 0.1.0
705
- npm run smoke:registry -- --version 0.1.0
706
- npm run smoke:installer -- --version 0.1.0
738
+ npm run release:published -- --version 0.1.1 --tag kairo-runtime-v0.1.1
739
+ npm run smoke:registry -- --version 0.1.1
740
+ npm run smoke:installer -- --version 0.1.1 --tag kairo-runtime-v0.1.1
741
+ npm run smoke:bridge
742
+ ```
743
+
744
+ `release:published` checks npm `version`, npm `gitHead`, the release git tag on
745
+ `origin`, and `origin/main`. Override the package and tag when needed:
746
+
747
+ ```bash
748
+ npm run release:published -- \
749
+ --package @kal-elsam/kairo-runtime \
750
+ --tag kairo-runtime-v0.1.1 \
751
+ --version 0.1.1
752
+
753
+ npm run release:published -- \
754
+ --package @kal-elsam/harness \
755
+ --tag harness-bridge-v0.30.0 \
756
+ --version 0.30.0
707
757
  ```
708
758
 
709
- `release:published` checks npm `version`, npm `gitHead`, local tag `v*`, remote tag on `origin`, and `origin/main`. Override the package with `--package @kal-elsam/kairo-runtime` when needed.
759
+ Without `--tag`, provenance checks fall back to `v${version}` (legacy tags).
710
760
 
711
761
  `smoke:registry` installs `@kal-elsam/kairo-runtime` from the npm registry (not the local tarball) into a throwaway workspace with a fake `HARNESS_HOME` and npm cache, then runs the recommended flow via `kairo`: `setup --dry-run`, `setup --yes`, `status`, drift simulation, `sync`, `status --json` (expects `overall=ok`), and `uninstall`. Use `latest` by default, pin with `--version x.y.z`, or override with `--package`.
712
762
 
713
- `smoke:installer` validates the public one-liner path: `curl .../install.sh | sh` against GitHub `raw` and the npm registry with isolated `HARNESS_HOME`. Preview must not write `~/.harness`; `--yes --agents all` must reach `kairo status --json` with `overall=ok`, then `kairo uninstall` must remove managed sections. Pin with `--version x.y.z` after publish.
763
+ `smoke:installer` validates the public one-liner path: `curl .../install.sh | sh` against GitHub `raw` and the npm registry with isolated `HARNESS_HOME`. Preview must not write `~/.harness`; `--yes --agents all` must reach `kairo status --json` with `overall=ok`, then `kairo uninstall` must remove managed sections. Pin with `--version x.y.z` after publish. For package-aware Kairo Runtime tags, pass the git tag explicitly:
764
+
765
+ ```bash
766
+ npm run smoke:installer -- --version 0.1.1 --tag kairo-runtime-v0.1.1
767
+ ```
768
+
769
+ Without `--tag`, the install script resolves from legacy `v${version}` tags.
714
770
 
715
- Suggested first publish tag: `kairo-runtime-v0.1.0` (or `v0.30.0` if you prefer repo version continuity).
771
+ Suggested first Kairo Runtime tag after bootstrap: `kairo-runtime-v0.1.1`.
716
772
 
717
- The `publish.yml` workflow runs on `v*` tags and publishes to npm using the `npm-publish` environment.
773
+ The `publish.yml` workflow runs on `v*`, `kairo-runtime-v*`, and `harness-bridge-v*`
774
+ tags and publishes to npm using the `npm-publish` environment.
718
775
  It runs `npm run release:check` on `HEAD` immediately before `npm publish`.
719
776
 
720
777
  See the full policy in `SECURITY.md`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kal-elsam/kairo-runtime",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Kairo Runtime — local agent operating system for Codex, Cursor, Claude, Gemini, Copilot, Engram, and Graphify.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/Kal-elSam/harness#readme",
@@ -35,6 +35,7 @@
35
35
  "ux:smoke": "bash scripts/ux-smoke-test.sh",
36
36
  "smoke:registry": "bash scripts/registry-smoke-test.sh",
37
37
  "smoke:installer": "bash scripts/installer-smoke-test.sh",
38
+ "smoke:bridge": "bash scripts/bridge-smoke-test.sh",
38
39
  "release:check": "node scripts/check-release-commit.mjs",
39
40
  "release:published": "node scripts/check-published-release.mjs"
40
41
  },
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT="$(cd "$(dirname "$0")/.." && pwd)"
5
+ BRIDGE_DIR="$ROOT/packages/harness-bridge"
6
+ WORKDIR="$(mktemp -d)"
7
+ FAKE_HOME="$(mktemp -d)"
8
+ NPM_CACHE="$(mktemp -d)"
9
+
10
+ cleanup() {
11
+ rm -rf "$WORKDIR" "$FAKE_HOME" "$NPM_CACHE"
12
+ }
13
+ trap cleanup EXIT
14
+
15
+ export HARNESS_HOME="$FAKE_HOME"
16
+ export npm_config_cache="$NPM_CACHE"
17
+
18
+ mkdir -p \
19
+ "$FAKE_HOME/.cursor" \
20
+ "$FAKE_HOME/.codex" \
21
+ "$FAKE_HOME/.config/opencode" \
22
+ "$FAKE_HOME/.claude"
23
+
24
+ echo "Packing @kal-elsam/kairo-runtime from $ROOT"
25
+ RUNTIME_TGZ="$(cd "$ROOT" && npm pack --silent --pack-destination "$WORKDIR")"
26
+ RUNTIME_TGZ_PATH="$WORKDIR/$RUNTIME_TGZ"
27
+
28
+ echo "Packing @kal-elsam/harness bridge"
29
+ BRIDGE_TGZ="$(cd "$BRIDGE_DIR" && npm pack --silent --pack-destination "$WORKDIR")"
30
+ BRIDGE_TGZ_PATH="$WORKDIR/$BRIDGE_TGZ"
31
+
32
+ INSTALL_DIR="$WORKDIR/project"
33
+ mkdir -p "$INSTALL_DIR"
34
+ (
35
+ cd "$INSTALL_DIR"
36
+ npm init -y >/dev/null
37
+ npm install "$RUNTIME_TGZ_PATH" "$BRIDGE_TGZ_PATH" --ignore-scripts
38
+ )
39
+
40
+ HARNESS_BIN="$INSTALL_DIR/node_modules/@kal-elsam/harness/bin/harness.js"
41
+
42
+ echo
43
+ echo "== harness --version (warning on stderr, version on stdout) =="
44
+ VERSION_OUTPUT="$(node "$HARNESS_BIN" --version 2>"$WORKDIR/version.stderr" || true)"
45
+ VERSION_STDERR="$(cat "$WORKDIR/version.stderr")"
46
+
47
+ if ! grep -q "@kal-elsam/harness has moved to @kal-elsam/kairo-runtime" <<<"$VERSION_STDERR"; then
48
+ echo "Expected migration warning on stderr" >&2
49
+ echo "stderr: $VERSION_STDERR" >&2
50
+ exit 1
51
+ fi
52
+
53
+ if ! grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' <<<"$VERSION_OUTPUT"; then
54
+ echo "Expected semver on stdout, got: $VERSION_OUTPUT" >&2
55
+ exit 1
56
+ fi
57
+
58
+ if grep -q "@kal-elsam/harness has moved" <<<"$VERSION_OUTPUT"; then
59
+ echo "Migration warning must not appear on stdout" >&2
60
+ exit 1
61
+ fi
62
+
63
+ echo "stdout version: $VERSION_OUTPUT"
64
+
65
+ echo
66
+ echo "== harness status --json (JSON on stdout only) =="
67
+ set +e
68
+ STATUS_OUTPUT="$(node "$HARNESS_BIN" status --json 2>"$WORKDIR/status.stderr")"
69
+ STATUS_EXIT=$?
70
+ set -e
71
+ STATUS_STDERR="$(cat "$WORKDIR/status.stderr")"
72
+
73
+ if [ "$STATUS_EXIT" -ne 0 ] && [ "$STATUS_EXIT" -ne 1 ]; then
74
+ echo "Unexpected exit code from harness status --json: $STATUS_EXIT" >&2
75
+ exit 1
76
+ fi
77
+
78
+ if ! grep -q "@kal-elsam/harness has moved to @kal-elsam/kairo-runtime" <<<"$STATUS_STDERR"; then
79
+ echo "Expected migration warning on stderr for status --json" >&2
80
+ exit 1
81
+ fi
82
+
83
+ node -e "JSON.parse(process.argv[1])" "$STATUS_OUTPUT"
84
+
85
+ if grep -q "@kal-elsam/harness has moved" <<<"$STATUS_OUTPUT"; then
86
+ echo "Migration warning must not appear in JSON stdout" >&2
87
+ exit 1
88
+ fi
89
+
90
+ echo
91
+ echo "Bridge smoke test passed."
@@ -15,10 +15,19 @@ async function defaultFetchJson(url) {
15
15
  return response.json();
16
16
  }
17
17
 
18
+ export function resolveReleaseTag({ version, tag = null }) {
19
+ if (tag) {
20
+ return tag;
21
+ }
22
+
23
+ return `v${version}`;
24
+ }
25
+
18
26
  export function parsePublishedReleaseArgs(argv) {
19
27
  const args = [...argv];
20
28
  let version = null;
21
29
  let packageName = "@kal-elsam/kairo-runtime";
30
+ let tag = null;
22
31
 
23
32
  for (let index = 2; index < args.length; index += 1) {
24
33
  const arg = args[index];
@@ -43,6 +52,16 @@ export function parsePublishedReleaseArgs(argv) {
43
52
  continue;
44
53
  }
45
54
 
55
+ if (arg === "--tag") {
56
+ tag = args[++index];
57
+ continue;
58
+ }
59
+
60
+ if (arg.startsWith("--tag=")) {
61
+ tag = arg.slice("--tag=".length);
62
+ continue;
63
+ }
64
+
46
65
  throw new Error(`Unknown option "${arg}".`);
47
66
  }
48
67
 
@@ -50,16 +69,17 @@ export function parsePublishedReleaseArgs(argv) {
50
69
  throw new Error("Missing required --version <x.y.z>.");
51
70
  }
52
71
 
53
- return { version, packageName };
72
+ return { version, packageName, tag };
54
73
  }
55
74
 
56
75
  export async function verifyPublishedRelease({
57
76
  version,
58
77
  packageName = "@kal-elsam/kairo-runtime",
78
+ tag = null,
59
79
  runGit = defaultRunGit,
60
80
  fetchJson = defaultFetchJson
61
81
  }) {
62
- const tag = `v${version}`;
82
+ const releaseTag = resolveReleaseTag({ version, tag });
63
83
  const encodedPackage = encodeURIComponent(packageName);
64
84
  const meta = await fetchJson(`https://registry.npmjs.org/${encodedPackage}/${version}`);
65
85
 
@@ -72,10 +92,12 @@ export async function verifyPublishedRelease({
72
92
  }
73
93
 
74
94
  const gitHead = meta.gitHead.trim();
75
- const tagSha = runGit(`git rev-parse ${tag}^{commit}`).trim();
95
+ const tagSha = runGit(`git rev-parse ${releaseTag}^{commit}`).trim();
76
96
 
77
97
  if (tagSha !== gitHead) {
78
- throw new Error(`Local tag ${tag} (${tagSha}) does not match npm gitHead (${gitHead})`);
98
+ throw new Error(
99
+ `Local tag ${releaseTag} (${tagSha}) does not match npm gitHead (${gitHead})`
100
+ );
79
101
  }
80
102
 
81
103
  const mainSha = runGit("git rev-parse origin/main").trim();
@@ -90,17 +112,19 @@ export async function verifyPublishedRelease({
90
112
  }
91
113
  }
92
114
 
93
- const remoteTagLine = runGit(`git ls-remote --tags origin refs/tags/${tag}`).trim();
115
+ const remoteTagLine = runGit(`git ls-remote --tags origin refs/tags/${releaseTag}`).trim();
94
116
 
95
117
  if (!remoteTagLine.startsWith(gitHead)) {
96
- throw new Error(`Remote tag ${tag} on origin does not point to npm gitHead (${gitHead})`);
118
+ throw new Error(
119
+ `Remote tag ${releaseTag} on origin does not point to npm gitHead (${gitHead})`
120
+ );
97
121
  }
98
122
 
99
123
  return {
100
124
  version,
101
125
  packageName,
102
126
  gitHead,
103
- tag,
127
+ tag: releaseTag,
104
128
  mainSha,
105
129
  tagSha,
106
130
  mainAhead: mainSha !== gitHead
@@ -114,8 +138,8 @@ function isMainModule() {
114
138
  }
115
139
 
116
140
  if (isMainModule()) {
117
- const { version, packageName } = parsePublishedReleaseArgs(process.argv);
118
- const result = await verifyPublishedRelease({ version, packageName });
141
+ const { version, packageName, tag } = parsePublishedReleaseArgs(process.argv);
142
+ const result = await verifyPublishedRelease({ version, packageName, tag });
119
143
 
120
144
  console.log("Published release provenance OK");
121
145
  console.log(`Package: ${result.packageName}@${result.version}`);
@@ -43,8 +43,9 @@ usage() {
43
43
  "" \
44
44
  "What it does:" \
45
45
  " 1. Checks for Node.js and npm." \
46
- " 2. Runs the package via npx (or npm exec) with setup --dry-run (default) or setup --yes (--yes)." \
47
- " 3. Prints next steps." \
46
+ " 2. Installs ${PACKAGE} globally (adds kairo to npm global bin)." \
47
+ " 3. Runs kairo setup --dry-run (default) or kairo setup --yes (--yes)." \
48
+ " 4. Prints next steps." \
48
49
  "" \
49
50
  "Security:" \
50
51
  " - Never uses sudo." \
@@ -62,6 +63,51 @@ append_setup_arg() {
62
63
  SETUP_EXTRA="${SETUP_EXTRA}${SETUP_EXTRA:+ }$1"
63
64
  }
64
65
 
66
+ resolve_global_spec() {
67
+ if [ "$VERSION" = "latest" ]; then
68
+ printf '%s@latest' "$PACKAGE"
69
+ else
70
+ printf '%s@%s' "$PACKAGE" "$VERSION"
71
+ fi
72
+ }
73
+
74
+ resolve_npm_global_bin() {
75
+ npm_prefix="$(npm prefix -g 2>/dev/null || true)"
76
+ [ -n "$npm_prefix" ] || return 1
77
+ printf '%s/bin' "$(printf '%s' "$npm_prefix" | sed 's#/*$##')"
78
+ }
79
+
80
+ resolve_kairo_bin() {
81
+ if command -v "$PREFERRED_CLI" >/dev/null 2>&1; then
82
+ command -v "$PREFERRED_CLI"
83
+ return 0
84
+ fi
85
+
86
+ global_bin="$(resolve_npm_global_bin || true)"
87
+ if [ -n "$global_bin" ] && [ -x "${global_bin}/${PREFERRED_CLI}" ]; then
88
+ printf '%s\n' "${global_bin}/${PREFERRED_CLI}"
89
+ return 0
90
+ fi
91
+
92
+ return 1
93
+ }
94
+
95
+ print_path_hint_if_needed() {
96
+ if command -v "$PREFERRED_CLI" >/dev/null 2>&1; then
97
+ return 0
98
+ fi
99
+
100
+ global_bin="$(resolve_npm_global_bin || true)"
101
+ if [ -n "$global_bin" ]; then
102
+ printf '%s\n' \
103
+ "" \
104
+ "PATH note:" \
105
+ " ${PREFERRED_CLI} was installed to ${global_bin}." \
106
+ " Add it to PATH if the command is not found:" \
107
+ " export PATH=\"${global_bin}:\$PATH\""
108
+ fi
109
+ }
110
+
65
111
  while [ "$#" -gt 0 ]; do
66
112
  case "$1" in
67
113
  --dry-run)
@@ -137,14 +183,9 @@ require_cmd npm
137
183
 
138
184
  NODE_VERSION="$(node --version 2>/dev/null || true)"
139
185
  NPM_VERSION="$(npm --version 2>/dev/null || true)"
140
-
141
- if command -v npx >/dev/null 2>&1; then
142
- RUNNER="npx"
143
- RUN_CMD="npx --yes ${PACKAGE}@${VERSION} setup ${SETUP_MODE}${SETUP_EXTRA:+ ${SETUP_EXTRA}}"
144
- else
145
- RUNNER="npm-exec"
146
- RUN_CMD="npm exec --yes --package=${PACKAGE}@${VERSION} -- ${PREFERRED_CLI} setup ${SETUP_MODE}${SETUP_EXTRA:+ ${SETUP_EXTRA}}"
147
- fi
186
+ GLOBAL_SPEC="$(resolve_global_spec)"
187
+ GLOBAL_INSTALL_CMD="npm install -g ${GLOBAL_SPEC}"
188
+ SETUP_CMD="${PREFERRED_CLI} setup ${SETUP_MODE}${SETUP_EXTRA:+ ${SETUP_EXTRA}}"
148
189
 
149
190
  printf '%s\n' \
150
191
  "Kairo Runtime bootstrap installer" \
@@ -153,13 +194,13 @@ printf '%s\n' \
153
194
  "Prerequisites:" \
154
195
  " node ${NODE_VERSION:-unknown}" \
155
196
  " npm ${NPM_VERSION:-unknown}" \
156
- " runner ${RUNNER}" \
157
197
  "" \
158
198
  "Will run:" \
159
- " ${RUN_CMD}" \
199
+ " ${GLOBAL_INSTALL_CMD}" \
200
+ " ${SETUP_CMD}" \
160
201
  "" \
161
202
  "Effects:" \
162
- " - Downloads/runs ${PACKAGE}@${VERSION} via npm (no global install required)." \
203
+ " - Installs ${GLOBAL_SPEC} globally (kairo CLI in npm global bin)." \
163
204
  " - Configures managed sections only (never installs AI apps)." \
164
205
  " - Never uses sudo or modifies shell profiles." \
165
206
  ""
@@ -181,19 +222,25 @@ if [ "$INSTALLER_DRY_RUN" -eq 1 ]; then
181
222
  exit 0
182
223
  fi
183
224
 
225
+ printf '%s\n' "Installing global CLI..." ""
226
+ # shellcheck disable=SC2086
227
+ npm install -g ${GLOBAL_SPEC}
228
+
229
+ KAIRO_BIN="$(resolve_kairo_bin || true)"
230
+ if [ -z "$KAIRO_BIN" ]; then
231
+ die "Installed ${GLOBAL_SPEC}, but ${PREFERRED_CLI} is not available.
232
+ Check npm global bin: $(resolve_npm_global_bin || echo unknown)
233
+ Add it to PATH, then re-run: ${SETUP_CMD}"
234
+ fi
235
+
184
236
  if [ "$APPLY" -eq 1 ]; then
185
237
  printf '%s\n' "Applying setup..." ""
186
238
  else
187
239
  printf '%s\n' "Running preview..." ""
188
240
  fi
189
241
 
190
- if [ "$RUNNER" = "npx" ]; then
191
- # shellcheck disable=SC2086
192
- npx --yes "${PACKAGE}@${VERSION}" setup ${SETUP_MODE} ${SETUP_EXTRA}
193
- else
194
- # shellcheck disable=SC2086
195
- npm exec --yes --package="${PACKAGE}@${VERSION}" -- ${PREFERRED_CLI} setup ${SETUP_MODE} ${SETUP_EXTRA}
196
- fi
242
+ # shellcheck disable=SC2086
243
+ "$KAIRO_BIN" setup ${SETUP_MODE} ${SETUP_EXTRA}
197
244
 
198
245
  if [ "$APPLY" -eq 1 ]; then
199
246
  printf '%s\n' \
@@ -202,24 +249,25 @@ if [ "$APPLY" -eq 1 ]; then
202
249
  "Next steps:" \
203
250
  " 1. Check health: ${PREFERRED_CLI} status" \
204
251
  " 2. Repair drift: ${PREFERRED_CLI} sync" \
205
- " 3. Upgrade latest: npx ${PACKAGE}@latest setup --yes" \
252
+ " 3. Upgrade latest: ${PREFERRED_CLI} upgrade --dry-run" \
206
253
  "" \
207
254
  "Version:" \
208
- " Installed CLI: npx ${PACKAGE} --version" \
209
- " Published package: npm view ${PACKAGE} version" \
210
- " Preview upgrade: ${PREFERRED_CLI} upgrade --dry-run"
255
+ " Installed CLI: ${PREFERRED_CLI} --version" \
256
+ " Published package: npm view ${PACKAGE} version"
257
+ print_path_hint_if_needed
211
258
  else
212
259
  printf '%s\n' \
213
260
  "" \
214
261
  "Bootstrap complete." \
215
262
  "Next steps:" \
216
- " 1. Apply the plan: npx ${PACKAGE}@${VERSION} setup --yes" \
263
+ " 1. Apply the plan: ${PREFERRED_CLI} setup --yes" \
217
264
  " (or re-run installer: curl ... | sh -s -- --yes)" \
218
- " 2. Check health: npx ${PACKAGE}@${VERSION} status" \
219
- " 3. Repair drift: npx ${PACKAGE}@${VERSION} sync" \
265
+ " 2. Check health: ${PREFERRED_CLI} status" \
266
+ " 3. Repair drift: ${PREFERRED_CLI} sync" \
220
267
  "" \
221
268
  "Version:" \
222
- " Installed CLI: npx ${PACKAGE} --version" \
269
+ " Installed CLI: ${PREFERRED_CLI} --version" \
223
270
  " Published package: npm view ${PACKAGE} version" \
224
- " Update / converge: npx ${PACKAGE}@latest sync"
271
+ " Update / converge: ${PREFERRED_CLI} sync"
272
+ print_path_hint_if_needed
225
273
  fi
@@ -4,7 +4,9 @@ set -euo pipefail
4
4
  PACKAGE="@kal-elsam/kairo-runtime"
5
5
  PREFERRED_CLI="kairo"
6
6
  VERSION="latest"
7
+ GIT_TAG=""
7
8
  REPO="Kal-elSam/harness"
9
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
10
 
9
11
  while [ "$#" -gt 0 ]; do
10
12
  case "$1" in
@@ -24,6 +26,14 @@ while [ "$#" -gt 0 ]; do
24
26
  VERSION="${1#*=}"
25
27
  shift
26
28
  ;;
29
+ --tag)
30
+ GIT_TAG="$2"
31
+ shift 2
32
+ ;;
33
+ --tag=*)
34
+ GIT_TAG="${1#*=}"
35
+ shift
36
+ ;;
27
37
  *)
28
38
  echo "Unknown option: $1" >&2
29
39
  exit 1
@@ -53,12 +63,13 @@ assert_harness_home_isolated() {
53
63
  }
54
64
 
55
65
  resolve_install_script_url() {
56
- if [ "$VERSION" = "latest" ]; then
57
- printf '%s\n' "https://raw.githubusercontent.com/${REPO}/main/scripts/install.sh"
58
- return
66
+ local args=(--version "$VERSION" --repo "$REPO")
67
+
68
+ if [ -n "$GIT_TAG" ]; then
69
+ args+=(--tag "$GIT_TAG")
59
70
  fi
60
71
 
61
- printf '%s\n' "https://raw.githubusercontent.com/${REPO}/v${VERSION}/scripts/install.sh"
72
+ node "${SCRIPT_DIR}/lib/install-script-url.mjs" "${args[@]}"
62
73
  }
63
74
 
64
75
  assert_no_harness_state() {
@@ -117,6 +128,9 @@ cd "$WORKDIR"
117
128
 
118
129
  INSTALL_SCRIPT_URL="$(resolve_install_script_url)"
119
130
  echo "Installer smoke for ${PACKAGE}@${VERSION}"
131
+ if [ -n "$GIT_TAG" ]; then
132
+ echo "Git tag: ${GIT_TAG}"
133
+ fi
120
134
  echo "Install script: ${INSTALL_SCRIPT_URL}"
121
135
  echo "Harness home: ${HARNESS_HOME}"
122
136
 
@@ -140,15 +154,36 @@ if [ ! -d "$FAKE_HOME/.harness" ]; then
140
154
  exit 1
141
155
  fi
142
156
 
157
+ NPM_GLOBAL_BIN="$(npm prefix -g 2>/dev/null)/bin"
158
+ export PATH="${NPM_GLOBAL_BIN}:${PATH}"
159
+
160
+ resolve_kairo_bin() {
161
+ if command -v "$PREFERRED_CLI" >/dev/null 2>&1; then
162
+ command -v "$PREFERRED_CLI"
163
+ return 0
164
+ fi
165
+ if [ -x "${NPM_GLOBAL_BIN}/${PREFERRED_CLI}" ]; then
166
+ printf '%s\n' "${NPM_GLOBAL_BIN}/${PREFERRED_CLI}"
167
+ return 0
168
+ fi
169
+ return 1
170
+ }
171
+
172
+ KAIRO_BIN="$(resolve_kairo_bin || true)"
173
+ if [ -z "$KAIRO_BIN" ]; then
174
+ echo "Expected ${PREFERRED_CLI} after install.sh global install" >&2
175
+ exit 1
176
+ fi
177
+
143
178
  echo
144
179
  echo "== ${PREFERRED_CLI} status --json =="
145
- STATUS_JSON="$(npx --yes "${PACKAGE}@${VERSION}" status --json)"
180
+ STATUS_JSON="$("$KAIRO_BIN" status --json)"
146
181
  echo "$STATUS_JSON"
147
182
  assert_status_ok "$STATUS_JSON"
148
183
 
149
184
  echo
150
185
  echo "== ${PREFERRED_CLI} uninstall =="
151
- npx --yes "${PACKAGE}@${VERSION}" uninstall
186
+ "$KAIRO_BIN" uninstall
152
187
  assert_managed_configs_removed
153
188
 
154
189
  echo
@@ -0,0 +1,85 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import path from "node:path";
3
+
4
+ const DEFAULT_REPO = "Kal-elSam/harness";
5
+
6
+ export function resolveInstallScriptRef({ version, tag = null }) {
7
+ if (version === "latest") {
8
+ return "main";
9
+ }
10
+
11
+ if (tag) {
12
+ return tag;
13
+ }
14
+
15
+ return `v${version}`;
16
+ }
17
+
18
+ export function resolveInstallScriptUrl({
19
+ repo = DEFAULT_REPO,
20
+ version,
21
+ tag = null
22
+ }) {
23
+ const ref = resolveInstallScriptRef({ version, tag });
24
+ return `https://raw.githubusercontent.com/${repo}/${ref}/scripts/install.sh`;
25
+ }
26
+
27
+ export function parseInstallScriptUrlArgs(argv) {
28
+ const args = [...argv];
29
+ let version = "latest";
30
+ let tag = null;
31
+ let repo = DEFAULT_REPO;
32
+
33
+ for (let index = 2; index < args.length; index += 1) {
34
+ const arg = args[index];
35
+
36
+ if (arg === "--version") {
37
+ version = args[++index];
38
+ continue;
39
+ }
40
+
41
+ if (arg.startsWith("--version=")) {
42
+ version = arg.slice("--version=".length);
43
+ continue;
44
+ }
45
+
46
+ if (arg === "--tag") {
47
+ tag = args[++index];
48
+ continue;
49
+ }
50
+
51
+ if (arg.startsWith("--tag=")) {
52
+ tag = arg.slice("--tag=".length);
53
+ continue;
54
+ }
55
+
56
+ if (arg === "--repo") {
57
+ repo = args[++index];
58
+ continue;
59
+ }
60
+
61
+ if (arg.startsWith("--repo=")) {
62
+ repo = arg.slice("--repo=".length);
63
+ continue;
64
+ }
65
+
66
+ throw new Error(`Unknown option "${arg}".`);
67
+ }
68
+
69
+ return { version, tag, repo };
70
+ }
71
+
72
+ function isMainModule() {
73
+ const entry = process.argv[1];
74
+
75
+ if (!entry) {
76
+ return false;
77
+ }
78
+
79
+ return path.resolve(fileURLToPath(import.meta.url)) === path.resolve(entry);
80
+ }
81
+
82
+ if (isMainModule()) {
83
+ const { version, tag, repo } = parseInstallScriptUrlArgs(process.argv);
84
+ console.log(resolveInstallScriptUrl({ repo, version, tag }));
85
+ }
package/src/cli.js CHANGED
@@ -31,51 +31,21 @@ import { applyPolicyToOptions, loadPolicyFile } from "./global/policy.js";
31
31
  import { resolveHomeDir } from "./global/paths.js";
32
32
  import { runWorkspaceDetect, runWorkspaceDoctor, runWorkspaceInit, runWorkspaceUpdate } from "./workspace-cli.js";
33
33
  import {
34
- ALL_CLI_NAMES,
35
34
  LEGACY_PACKAGE_NAME,
36
35
  PACKAGE_NAME,
37
36
  PREFERRED_CLI,
38
37
  formatCliCommand,
39
- maybeWarnLegacyCli
38
+ maybeWarnLegacyCli,
39
+ resolveSuggestedInvocation
40
40
  } from "./global/brand/cli.js";
41
41
  import { BRAND } from "./global/brand/index.js";
42
42
 
43
+ export { resolveSuggestedInvocation };
44
+
43
45
  const __dirname = dirname(fileURLToPath(import.meta.url));
44
46
  const packageRoot = resolve(__dirname, "..");
45
47
  const SCOPES = new Set(["agent-global", "workspace"]);
46
48
 
47
- export function resolveSuggestedInvocation(packageName, argv = process.argv) {
48
- const invokedPath = argv[1] ?? PREFERRED_CLI;
49
- const invokedBase = basename(invokedPath);
50
-
51
- if (!invokedBase.endsWith(".js") && ALL_CLI_NAMES.has(invokedBase)) {
52
- return invokedBase;
53
- }
54
-
55
- const packageManager = detectInvocationPackageManager();
56
-
57
- switch (packageManager) {
58
- case "pnpm":
59
- return `pnpm dlx ${packageName}`;
60
- case "yarn":
61
- return `yarn dlx ${packageName}`;
62
- case "bun":
63
- return `bunx ${packageName}`;
64
- default:
65
- return `npx ${packageName}`;
66
- }
67
- }
68
-
69
- function detectInvocationPackageManager() {
70
- const execPath = process.env.npm_execpath ?? "";
71
- const userAgent = process.env.npm_config_user_agent ?? "";
72
-
73
- if (execPath.includes("pnpm") || userAgent.startsWith("pnpm/")) return "pnpm";
74
- if (execPath.includes("yarn") || userAgent.startsWith("yarn/")) return "yarn";
75
- if (execPath.includes("bun") || userAgent.startsWith("bun/")) return "bun";
76
- return "npm";
77
- }
78
-
79
49
  export async function runCli(argv) {
80
50
  const { command, options } = parseArgs(argv);
81
51
  maybeWarnLegacyCli(process.argv, { json: options.json });
@@ -50,3 +50,44 @@ export function formatCliCommand(subcommand, cliName = PREFERRED_CLI) {
50
50
  const trimmed = subcommand.trim();
51
51
  return trimmed ? `${cliName} ${trimmed}` : cliName;
52
52
  }
53
+
54
+ function detectInvocationPackageManager() {
55
+ const execPath = process.env.npm_execpath ?? "";
56
+ const userAgent = process.env.npm_config_user_agent ?? "";
57
+
58
+ if (execPath.includes("pnpm") || userAgent.startsWith("pnpm/")) return "pnpm";
59
+ if (execPath.includes("yarn") || userAgent.startsWith("yarn/")) return "yarn";
60
+ if (execPath.includes("bun") || userAgent.startsWith("bun/")) return "bun";
61
+ return "npm";
62
+ }
63
+
64
+ export function resolveSuggestedInvocation(packageName = PACKAGE_NAME, argv = process.argv) {
65
+ const invokedPath = argv[1] ?? PREFERRED_CLI;
66
+ const invokedBase = basename(invokedPath);
67
+
68
+ if (!invokedBase.endsWith(".js") && ALL_CLI_NAMES.has(invokedBase)) {
69
+ return invokedBase;
70
+ }
71
+
72
+ const packageManager = detectInvocationPackageManager();
73
+
74
+ switch (packageManager) {
75
+ case "pnpm":
76
+ return `pnpm dlx ${packageName}`;
77
+ case "yarn":
78
+ return `yarn dlx ${packageName}`;
79
+ case "bun":
80
+ return `bunx ${packageName}`;
81
+ default:
82
+ return `npx ${packageName}`;
83
+ }
84
+ }
85
+
86
+ export function formatSuggestedCliCommand(
87
+ subcommand,
88
+ { packageName = PACKAGE_NAME, argv = process.argv, suggestedInvocation } = {}
89
+ ) {
90
+ const invoke = suggestedInvocation ?? resolveSuggestedInvocation(packageName, argv);
91
+ const trimmed = subcommand.trim();
92
+ return trimmed ? `${invoke} ${trimmed}` : invoke;
93
+ }
@@ -82,4 +82,10 @@ export function commandHeader(label) {
82
82
  return `${BRAND.displayName} ${label}`;
83
83
  }
84
84
 
85
- export { formatCliCommand, PREFERRED_CLI } from "./cli.js";
85
+ export {
86
+ formatCliCommand,
87
+ formatSuggestedCliCommand,
88
+ PACKAGE_NAME,
89
+ PREFERRED_CLI,
90
+ resolveSuggestedInvocation
91
+ } from "./cli.js";
@@ -1,4 +1,4 @@
1
- import { AGENT_HINTS, BRAND, PREFERRED_CLI, formatCliCommand, getAgentLabel, WIZARD_COPY } from "../brand/index.js";
1
+ import { AGENT_HINTS, BRAND, formatCliCommand, getAgentLabel, PREFERRED_CLI, WIZARD_COPY } from "../brand/index.js";
2
2
  import { formatAgentMultiselectHint } from "../clack/theme.js";
3
3
 
4
4
  export const SETUP_STEPS = {