@codyswann/lisa 2.338.1 → 2.338.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.
Files changed (66) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +3 -2
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/package.json +1 -1
  5. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  6. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/skills/lisa-setup-remote-env/SKILL.md +1 -1
  8. package/plugins/lisa/.codex-plugin/skills/lisa-setup-workstation/scripts/workstation.mjs +84 -0
  9. package/plugins/lisa/skills/lisa-setup-remote-env/SKILL.md +1 -1
  10. package/plugins/lisa/skills/lisa-setup-workstation/scripts/workstation.mjs +84 -0
  11. package/plugins/lisa-agy/plugin.json +1 -1
  12. package/plugins/lisa-agy/skills/lisa-setup-remote-env/SKILL.md +1 -1
  13. package/plugins/lisa-agy/skills/lisa-setup-workstation/scripts/workstation.mjs +84 -0
  14. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  16. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  17. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  18. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  19. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-copilot/skills/lisa-setup-remote-env/SKILL.md +1 -1
  21. package/plugins/lisa-copilot/skills/lisa-setup-workstation/scripts/workstation.mjs +84 -0
  22. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  23. package/plugins/lisa-cursor/skills/lisa-setup-remote-env/SKILL.md +1 -1
  24. package/plugins/lisa-cursor/skills/lisa-setup-workstation/scripts/workstation.mjs +84 -0
  25. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  27. package/plugins/lisa-expo-agy/plugin.json +1 -1
  28. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  29. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  32. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  33. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  34. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  37. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  38. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  39. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  42. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  43. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  44. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  47. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  48. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  49. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  52. package/plugins/lisa-rails-agy/plugin.json +1 -1
  53. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  54. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  56. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  57. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  58. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  59. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  60. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  61. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  62. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  63. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  64. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  65. package/plugins/src/base/skills/lisa-setup-remote-env/SKILL.md +1 -1
  66. package/plugins/src/base/skills/lisa-setup-workstation/scripts/workstation.mjs +84 -0
package/package.json CHANGED
@@ -118,7 +118,7 @@
118
118
  }
119
119
  },
120
120
  "name": "@codyswann/lisa",
121
- "version": "2.338.1",
121
+ "version": "2.338.2",
122
122
  "description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
123
123
  "main": "dist/index.js",
124
124
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.338.1",
3
+ "version": "2.338.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.338.1",
3
+ "version": "2.338.2",
4
4
  "description": "Universal governance: agents, skills, commands, hooks, and rules for all projects.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -13,7 +13,7 @@ Prepare a remote surface so a host project can execute there. Today that means *
13
13
  The remote environment's own configuration fields stay **one line into the repository**. Nothing else is pasted into a vendor UI.
14
14
 
15
15
  ```text
16
- setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.338.0 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.local/share/sonarqube-cli/bin:$PATH"; npx -y @codyswann/lisa@2.338.0 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.338.0 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.338.0 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
16
+ setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.338.1 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.local/share/sonarqube-cli/bin:$PATH"; npx -y @codyswann/lisa@2.338.1 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.338.1 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.338.1 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
17
17
  maintenance: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] || { echo "lisa-remote-env entrypoint not found. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; }; exit "$rc"
18
18
  ```
19
19
 
@@ -34,6 +34,8 @@
34
34
  */
35
35
 
36
36
  import { execFileSync } from "node:child_process";
37
+ import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
38
+ import { join } from "node:path";
37
39
 
38
40
  import {
39
41
  AGENTS,
@@ -297,6 +299,84 @@ export function renderPlan(plan) {
297
299
  * @param {object} [deps] Injected runner, for tests.
298
300
  * @returns {object} Result row.
299
301
  */
302
+ /**
303
+ * Make a tool that installed elsewhere reachable from `~/.local/bin`.
304
+ *
305
+ * A vendor script installs where it likes — SonarQube's puts `sonar` under
306
+ * `~/.local/share/sonarqube-cli/bin` — and the catalogue records that in
307
+ * `binDir`. Until now the only thing that put those directories on PATH was an
308
+ * edit to the shell rc files, which a cloud container never reads: its tool
309
+ * shell is not a login shell. So the tool installed, and the session that asked
310
+ * for it could not find it. Observed on a live Claude Tag channel, where
311
+ * `~/.local/share/sonarqube-cli` existed and `command -v sonar` came back empty.
312
+ *
313
+ * Exporting PATH from the setup script does not fix that either — those exports
314
+ * die with the process that ran them. Only the filesystem survives into the
315
+ * session, so the fix has to be a file.
316
+ *
317
+ * A symlink into `~/.local/bin` is the shape the AWS installer already uses
318
+ * (`aws -> ~/.local/aws-cli/v2/current/bin/aws`), and that directory is the one
319
+ * place every surface agrees is on PATH.
320
+ *
321
+ * Deliberately never fatal. A tool that is already reachable does not need the
322
+ * link, and one that cannot be linked is caught a moment later by the probe —
323
+ * which reports the real problem instead of a symlink error standing in for it.
324
+ * @param {object} entry Catalogue entry just installed.
325
+ * @param {object} [deps] Injected seams, for tests.
326
+ */
327
+ export function linkIntoBinDir(entry, deps = {}) {
328
+ if (!entry.binDir) return;
329
+
330
+ const home = deps.home ?? process.env.HOME ?? "";
331
+ const link = deps.link ?? symlinkSync;
332
+ const exists = deps.exists ?? existsSync;
333
+ const remove = deps.remove ?? rmSync;
334
+ const makeDir = deps.mkdir ?? mkdirSync;
335
+ // Two different questions, and they need two different probes.
336
+ //
337
+ // The SOURCE must be followed: a link pointing at a binary that is not there
338
+ // means the install did not produce one, whatever the link says.
339
+ //
340
+ // The TARGET must NOT be followed. `existsSync` reports false for a dangling
341
+ // symlink, which is precisely the case this function claims to repair — the
342
+ // vendor moved and the old link now points at nothing. Following it would
343
+ // skip the removal, `symlinkSync` would throw EEXIST, the catch below would
344
+ // swallow it, and the broken link would survive the fix meant to replace it.
345
+ const present = deps.present ?? (path => lstatPresent(path));
346
+
347
+ const source = join(
348
+ entry.binDir.replace(/^~/, home),
349
+ entry.binary ?? entry.name
350
+ );
351
+ const target = join(home, ".local", "bin", entry.name);
352
+ if (source === target || !exists(source)) return;
353
+
354
+ try {
355
+ makeDir(join(home, ".local", "bin"), { recursive: true });
356
+ // Replaced rather than skipped-if-present: a stale link left by an earlier
357
+ // version pointing at a path the vendor has since moved is worse than none,
358
+ // because it resolves and then fails at the moment of use.
359
+ if (present(target)) remove(target, { force: true });
360
+ link(source, target);
361
+ } catch {
362
+ // See above: the probe is the authority on whether this worked.
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Whether a path exists WITHOUT following it, so a dangling link counts.
368
+ * @param {string} path Path to inspect.
369
+ * @returns {boolean} True when something occupies the path.
370
+ */
371
+ function lstatPresent(path) {
372
+ try {
373
+ lstatSync(path);
374
+ return true;
375
+ } catch {
376
+ return false;
377
+ }
378
+ }
379
+
300
380
  export function installEntry(entry, row, deps = {}) {
301
381
  const run =
302
382
  deps.run ??
@@ -398,6 +478,10 @@ export function installEntry(entry, row, deps = {}) {
398
478
  };
399
479
  }
400
480
 
481
+ // Link it into ~/.local/bin BEFORE probing, because that is what makes the
482
+ // probe meaningful for a tool the vendor put somewhere else.
483
+ linkIntoBinDir(entry, deps);
484
+
401
485
  // Verify by looking, not by exit status. Every installer above can exit 0
402
486
  // having installed nothing, so a claim of success is only as good as a fresh
403
487
  // probe — reporting a tool as installed when it is absent is worse than
@@ -13,7 +13,7 @@ Prepare a remote surface so a host project can execute there. Today that means *
13
13
  The remote environment's own configuration fields stay **one line into the repository**. Nothing else is pasted into a vendor UI.
14
14
 
15
15
  ```text
16
- setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.338.0 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.local/share/sonarqube-cli/bin:$PATH"; npx -y @codyswann/lisa@2.338.0 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.338.0 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.338.0 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
16
+ setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.338.1 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.local/share/sonarqube-cli/bin:$PATH"; npx -y @codyswann/lisa@2.338.1 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.338.1 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.338.1 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
17
17
  maintenance: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] || { echo "lisa-remote-env entrypoint not found. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; }; exit "$rc"
18
18
  ```
19
19
 
@@ -34,6 +34,8 @@
34
34
  */
35
35
 
36
36
  import { execFileSync } from "node:child_process";
37
+ import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
38
+ import { join } from "node:path";
37
39
 
38
40
  import {
39
41
  AGENTS,
@@ -297,6 +299,84 @@ export function renderPlan(plan) {
297
299
  * @param {object} [deps] Injected runner, for tests.
298
300
  * @returns {object} Result row.
299
301
  */
302
+ /**
303
+ * Make a tool that installed elsewhere reachable from `~/.local/bin`.
304
+ *
305
+ * A vendor script installs where it likes — SonarQube's puts `sonar` under
306
+ * `~/.local/share/sonarqube-cli/bin` — and the catalogue records that in
307
+ * `binDir`. Until now the only thing that put those directories on PATH was an
308
+ * edit to the shell rc files, which a cloud container never reads: its tool
309
+ * shell is not a login shell. So the tool installed, and the session that asked
310
+ * for it could not find it. Observed on a live Claude Tag channel, where
311
+ * `~/.local/share/sonarqube-cli` existed and `command -v sonar` came back empty.
312
+ *
313
+ * Exporting PATH from the setup script does not fix that either — those exports
314
+ * die with the process that ran them. Only the filesystem survives into the
315
+ * session, so the fix has to be a file.
316
+ *
317
+ * A symlink into `~/.local/bin` is the shape the AWS installer already uses
318
+ * (`aws -> ~/.local/aws-cli/v2/current/bin/aws`), and that directory is the one
319
+ * place every surface agrees is on PATH.
320
+ *
321
+ * Deliberately never fatal. A tool that is already reachable does not need the
322
+ * link, and one that cannot be linked is caught a moment later by the probe —
323
+ * which reports the real problem instead of a symlink error standing in for it.
324
+ * @param {object} entry Catalogue entry just installed.
325
+ * @param {object} [deps] Injected seams, for tests.
326
+ */
327
+ export function linkIntoBinDir(entry, deps = {}) {
328
+ if (!entry.binDir) return;
329
+
330
+ const home = deps.home ?? process.env.HOME ?? "";
331
+ const link = deps.link ?? symlinkSync;
332
+ const exists = deps.exists ?? existsSync;
333
+ const remove = deps.remove ?? rmSync;
334
+ const makeDir = deps.mkdir ?? mkdirSync;
335
+ // Two different questions, and they need two different probes.
336
+ //
337
+ // The SOURCE must be followed: a link pointing at a binary that is not there
338
+ // means the install did not produce one, whatever the link says.
339
+ //
340
+ // The TARGET must NOT be followed. `existsSync` reports false for a dangling
341
+ // symlink, which is precisely the case this function claims to repair — the
342
+ // vendor moved and the old link now points at nothing. Following it would
343
+ // skip the removal, `symlinkSync` would throw EEXIST, the catch below would
344
+ // swallow it, and the broken link would survive the fix meant to replace it.
345
+ const present = deps.present ?? (path => lstatPresent(path));
346
+
347
+ const source = join(
348
+ entry.binDir.replace(/^~/, home),
349
+ entry.binary ?? entry.name
350
+ );
351
+ const target = join(home, ".local", "bin", entry.name);
352
+ if (source === target || !exists(source)) return;
353
+
354
+ try {
355
+ makeDir(join(home, ".local", "bin"), { recursive: true });
356
+ // Replaced rather than skipped-if-present: a stale link left by an earlier
357
+ // version pointing at a path the vendor has since moved is worse than none,
358
+ // because it resolves and then fails at the moment of use.
359
+ if (present(target)) remove(target, { force: true });
360
+ link(source, target);
361
+ } catch {
362
+ // See above: the probe is the authority on whether this worked.
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Whether a path exists WITHOUT following it, so a dangling link counts.
368
+ * @param {string} path Path to inspect.
369
+ * @returns {boolean} True when something occupies the path.
370
+ */
371
+ function lstatPresent(path) {
372
+ try {
373
+ lstatSync(path);
374
+ return true;
375
+ } catch {
376
+ return false;
377
+ }
378
+ }
379
+
300
380
  export function installEntry(entry, row, deps = {}) {
301
381
  const run =
302
382
  deps.run ??
@@ -398,6 +478,10 @@ export function installEntry(entry, row, deps = {}) {
398
478
  };
399
479
  }
400
480
 
481
+ // Link it into ~/.local/bin BEFORE probing, because that is what makes the
482
+ // probe meaningful for a tool the vendor put somewhere else.
483
+ linkIntoBinDir(entry, deps);
484
+
401
485
  // Verify by looking, not by exit status. Every installer above can exit 0
402
486
  // having installed nothing, so a claim of success is only as good as a fresh
403
487
  // probe — reporting a tool as installed when it is absent is worse than
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.338.1",
3
+ "version": "2.338.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -13,7 +13,7 @@ Prepare a remote surface so a host project can execute there. Today that means *
13
13
  The remote environment's own configuration fields stay **one line into the repository**. Nothing else is pasted into a vendor UI.
14
14
 
15
15
  ```text
16
- setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.338.0 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.local/share/sonarqube-cli/bin:$PATH"; npx -y @codyswann/lisa@2.338.0 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.338.0 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.338.0 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
16
+ setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.338.1 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.local/share/sonarqube-cli/bin:$PATH"; npx -y @codyswann/lisa@2.338.1 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.338.1 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.338.1 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
17
17
  maintenance: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] || { echo "lisa-remote-env entrypoint not found. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; }; exit "$rc"
18
18
  ```
19
19
 
@@ -34,6 +34,8 @@
34
34
  */
35
35
 
36
36
  import { execFileSync } from "node:child_process";
37
+ import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
38
+ import { join } from "node:path";
37
39
 
38
40
  import {
39
41
  AGENTS,
@@ -297,6 +299,84 @@ export function renderPlan(plan) {
297
299
  * @param {object} [deps] Injected runner, for tests.
298
300
  * @returns {object} Result row.
299
301
  */
302
+ /**
303
+ * Make a tool that installed elsewhere reachable from `~/.local/bin`.
304
+ *
305
+ * A vendor script installs where it likes — SonarQube's puts `sonar` under
306
+ * `~/.local/share/sonarqube-cli/bin` — and the catalogue records that in
307
+ * `binDir`. Until now the only thing that put those directories on PATH was an
308
+ * edit to the shell rc files, which a cloud container never reads: its tool
309
+ * shell is not a login shell. So the tool installed, and the session that asked
310
+ * for it could not find it. Observed on a live Claude Tag channel, where
311
+ * `~/.local/share/sonarqube-cli` existed and `command -v sonar` came back empty.
312
+ *
313
+ * Exporting PATH from the setup script does not fix that either — those exports
314
+ * die with the process that ran them. Only the filesystem survives into the
315
+ * session, so the fix has to be a file.
316
+ *
317
+ * A symlink into `~/.local/bin` is the shape the AWS installer already uses
318
+ * (`aws -> ~/.local/aws-cli/v2/current/bin/aws`), and that directory is the one
319
+ * place every surface agrees is on PATH.
320
+ *
321
+ * Deliberately never fatal. A tool that is already reachable does not need the
322
+ * link, and one that cannot be linked is caught a moment later by the probe —
323
+ * which reports the real problem instead of a symlink error standing in for it.
324
+ * @param {object} entry Catalogue entry just installed.
325
+ * @param {object} [deps] Injected seams, for tests.
326
+ */
327
+ export function linkIntoBinDir(entry, deps = {}) {
328
+ if (!entry.binDir) return;
329
+
330
+ const home = deps.home ?? process.env.HOME ?? "";
331
+ const link = deps.link ?? symlinkSync;
332
+ const exists = deps.exists ?? existsSync;
333
+ const remove = deps.remove ?? rmSync;
334
+ const makeDir = deps.mkdir ?? mkdirSync;
335
+ // Two different questions, and they need two different probes.
336
+ //
337
+ // The SOURCE must be followed: a link pointing at a binary that is not there
338
+ // means the install did not produce one, whatever the link says.
339
+ //
340
+ // The TARGET must NOT be followed. `existsSync` reports false for a dangling
341
+ // symlink, which is precisely the case this function claims to repair — the
342
+ // vendor moved and the old link now points at nothing. Following it would
343
+ // skip the removal, `symlinkSync` would throw EEXIST, the catch below would
344
+ // swallow it, and the broken link would survive the fix meant to replace it.
345
+ const present = deps.present ?? (path => lstatPresent(path));
346
+
347
+ const source = join(
348
+ entry.binDir.replace(/^~/, home),
349
+ entry.binary ?? entry.name
350
+ );
351
+ const target = join(home, ".local", "bin", entry.name);
352
+ if (source === target || !exists(source)) return;
353
+
354
+ try {
355
+ makeDir(join(home, ".local", "bin"), { recursive: true });
356
+ // Replaced rather than skipped-if-present: a stale link left by an earlier
357
+ // version pointing at a path the vendor has since moved is worse than none,
358
+ // because it resolves and then fails at the moment of use.
359
+ if (present(target)) remove(target, { force: true });
360
+ link(source, target);
361
+ } catch {
362
+ // See above: the probe is the authority on whether this worked.
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Whether a path exists WITHOUT following it, so a dangling link counts.
368
+ * @param {string} path Path to inspect.
369
+ * @returns {boolean} True when something occupies the path.
370
+ */
371
+ function lstatPresent(path) {
372
+ try {
373
+ lstatSync(path);
374
+ return true;
375
+ } catch {
376
+ return false;
377
+ }
378
+ }
379
+
300
380
  export function installEntry(entry, row, deps = {}) {
301
381
  const run =
302
382
  deps.run ??
@@ -398,6 +478,10 @@ export function installEntry(entry, row, deps = {}) {
398
478
  };
399
479
  }
400
480
 
481
+ // Link it into ~/.local/bin BEFORE probing, because that is what makes the
482
+ // probe meaningful for a tool the vendor put somewhere else.
483
+ linkIntoBinDir(entry, deps);
484
+
401
485
  // Verify by looking, not by exit status. Every installer above can exit 0
402
486
  // having installed nothing, so a claim of success is only as good as a fresh
403
487
  // probe — reporting a tool as installed when it is absent is worse than
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.338.1",
3
+ "version": "2.338.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.338.1",
3
+ "version": "2.338.2",
4
4
  "description": "AWS CDK-specific Lisa plugin.",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.338.1",
3
+ "version": "2.338.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.338.1",
3
+ "version": "2.338.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa-cdk",
3
- "version": "2.338.1",
3
+ "version": "2.338.2",
4
4
  "description": "AWS CDK-specific plugin",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.338.1",
3
+ "version": "2.338.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -13,7 +13,7 @@ Prepare a remote surface so a host project can execute there. Today that means *
13
13
  The remote environment's own configuration fields stay **one line into the repository**. Nothing else is pasted into a vendor UI.
14
14
 
15
15
  ```text
16
- setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.338.0 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.local/share/sonarqube-cli/bin:$PATH"; npx -y @codyswann/lisa@2.338.0 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.338.0 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.338.0 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
16
+ setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.338.1 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.local/share/sonarqube-cli/bin:$PATH"; npx -y @codyswann/lisa@2.338.1 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.338.1 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.338.1 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
17
17
  maintenance: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] || { echo "lisa-remote-env entrypoint not found. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; }; exit "$rc"
18
18
  ```
19
19
 
@@ -34,6 +34,8 @@
34
34
  */
35
35
 
36
36
  import { execFileSync } from "node:child_process";
37
+ import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync } from "node:fs";
38
+ import { join } from "node:path";
37
39
 
38
40
  import {
39
41
  AGENTS,
@@ -297,6 +299,84 @@ export function renderPlan(plan) {
297
299
  * @param {object} [deps] Injected runner, for tests.
298
300
  * @returns {object} Result row.
299
301
  */
302
+ /**
303
+ * Make a tool that installed elsewhere reachable from `~/.local/bin`.
304
+ *
305
+ * A vendor script installs where it likes — SonarQube's puts `sonar` under
306
+ * `~/.local/share/sonarqube-cli/bin` — and the catalogue records that in
307
+ * `binDir`. Until now the only thing that put those directories on PATH was an
308
+ * edit to the shell rc files, which a cloud container never reads: its tool
309
+ * shell is not a login shell. So the tool installed, and the session that asked
310
+ * for it could not find it. Observed on a live Claude Tag channel, where
311
+ * `~/.local/share/sonarqube-cli` existed and `command -v sonar` came back empty.
312
+ *
313
+ * Exporting PATH from the setup script does not fix that either — those exports
314
+ * die with the process that ran them. Only the filesystem survives into the
315
+ * session, so the fix has to be a file.
316
+ *
317
+ * A symlink into `~/.local/bin` is the shape the AWS installer already uses
318
+ * (`aws -> ~/.local/aws-cli/v2/current/bin/aws`), and that directory is the one
319
+ * place every surface agrees is on PATH.
320
+ *
321
+ * Deliberately never fatal. A tool that is already reachable does not need the
322
+ * link, and one that cannot be linked is caught a moment later by the probe —
323
+ * which reports the real problem instead of a symlink error standing in for it.
324
+ * @param {object} entry Catalogue entry just installed.
325
+ * @param {object} [deps] Injected seams, for tests.
326
+ */
327
+ export function linkIntoBinDir(entry, deps = {}) {
328
+ if (!entry.binDir) return;
329
+
330
+ const home = deps.home ?? process.env.HOME ?? "";
331
+ const link = deps.link ?? symlinkSync;
332
+ const exists = deps.exists ?? existsSync;
333
+ const remove = deps.remove ?? rmSync;
334
+ const makeDir = deps.mkdir ?? mkdirSync;
335
+ // Two different questions, and they need two different probes.
336
+ //
337
+ // The SOURCE must be followed: a link pointing at a binary that is not there
338
+ // means the install did not produce one, whatever the link says.
339
+ //
340
+ // The TARGET must NOT be followed. `existsSync` reports false for a dangling
341
+ // symlink, which is precisely the case this function claims to repair — the
342
+ // vendor moved and the old link now points at nothing. Following it would
343
+ // skip the removal, `symlinkSync` would throw EEXIST, the catch below would
344
+ // swallow it, and the broken link would survive the fix meant to replace it.
345
+ const present = deps.present ?? (path => lstatPresent(path));
346
+
347
+ const source = join(
348
+ entry.binDir.replace(/^~/, home),
349
+ entry.binary ?? entry.name
350
+ );
351
+ const target = join(home, ".local", "bin", entry.name);
352
+ if (source === target || !exists(source)) return;
353
+
354
+ try {
355
+ makeDir(join(home, ".local", "bin"), { recursive: true });
356
+ // Replaced rather than skipped-if-present: a stale link left by an earlier
357
+ // version pointing at a path the vendor has since moved is worse than none,
358
+ // because it resolves and then fails at the moment of use.
359
+ if (present(target)) remove(target, { force: true });
360
+ link(source, target);
361
+ } catch {
362
+ // See above: the probe is the authority on whether this worked.
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Whether a path exists WITHOUT following it, so a dangling link counts.
368
+ * @param {string} path Path to inspect.
369
+ * @returns {boolean} True when something occupies the path.
370
+ */
371
+ function lstatPresent(path) {
372
+ try {
373
+ lstatSync(path);
374
+ return true;
375
+ } catch {
376
+ return false;
377
+ }
378
+ }
379
+
300
380
  export function installEntry(entry, row, deps = {}) {
301
381
  const run =
302
382
  deps.run ??
@@ -398,6 +478,10 @@ export function installEntry(entry, row, deps = {}) {
398
478
  };
399
479
  }
400
480
 
481
+ // Link it into ~/.local/bin BEFORE probing, because that is what makes the
482
+ // probe meaningful for a tool the vendor put somewhere else.
483
+ linkIntoBinDir(entry, deps);
484
+
401
485
  // Verify by looking, not by exit status. Every installer above can exit 0
402
486
  // having installed nothing, so a claim of success is only as good as a fresh
403
487
  // probe — reporting a tool as installed when it is absent is worse than
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lisa",
3
- "version": "2.338.1",
3
+ "version": "2.338.2",
4
4
  "description": "Universal governance — agents, skills, commands, hooks, and rules for all projects",
5
5
  "author": {
6
6
  "name": "Cody Swann"
@@ -13,7 +13,7 @@ Prepare a remote surface so a host project can execute there. Today that means *
13
13
  The remote environment's own configuration fields stay **one line into the repository**. Nothing else is pasted into a vendor UI.
14
14
 
15
15
  ```text
16
- setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.338.0 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.local/share/sonarqube-cli/bin:$PATH"; npx -y @codyswann/lisa@2.338.0 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.338.0 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.338.0 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
16
+ setup: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] && exit "$rc"; g=0; for c in ./.git ./*/.git "$HOME"/*/.git /workspace/*/.git; do [ -e "$c" ] && g=1; done; if [ "$g" -eq 1 ]; then echo "lisa-remote-env entrypoint not found, but a checkout is present. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; fi; ten="${LISA_TENANT:-${LISA_SECRETS_NAMESPACE:-}}"; if [ -n "$ten" ]; then echo "No checkout; preparing tools and credentials for $ten."; tw=0; tt=0; ts=0; tp=0; npx -y @codyswann/lisa@2.338.1 workstation --install --agents=none --tools=none --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tw=$?; export PATH="$HOME/.local/bin:$HOME/.opencode/bin:$HOME/.local/share/sonarqube-cli/bin:$PATH"; npx -y @codyswann/lisa@2.338.1 remote-env --phase=secrets || ts=$?; w=$(npx -y @codyswann/lisa@2.338.1 remote-env --print-tools 2>/dev/null) || tp=$?; w=$(printf "%s\n" "$w" | tail -1); [ "$tp" -eq 0 ] || echo "SETUP INCOMPLETE: could not read the tool list from the vault (exit $tp). Installing the full catalogue." >&2; npx -y @codyswann/lisa@2.338.1 workstation --install --agents=none --tools="$w" --provider="${LISA_PROVIDER:-${LISA_SECRETS_PROVIDER:-bitwarden}}" || tt=$?; [ "$tw" -eq 0 ] && [ "$tt" -eq 0 ] || echo "SETUP INCOMPLETE: tool install failed (exit $tw/$tt). The session will start WITHOUT the pinned tools." >&2; [ "$ts" -eq 0 ] || echo "SETUP INCOMPLETE: secrets did not materialize (exit $ts). The session will start WITHOUT credentials." >&2; else echo "No checkout and no tenant configured; nothing to prepare."; fi; exit 0
17
17
  maintenance: n=0; rc=0; seen=""; for f in scripts/lisa-remote-env/setup.sh */scripts/lisa-remote-env/setup.sh "$HOME"/scripts/lisa-remote-env/setup.sh "$HOME"/*/scripts/lisa-remote-env/setup.sh /workspace/scripts/lisa-remote-env/setup.sh /workspace/*/scripts/lisa-remote-env/setup.sh; do [ -f "$f" ] || continue; d=$(cd "$(dirname "$f")" && pwd -P); case " $seen " in *" $d "*) continue;; esac; seen="$seen $d"; n=$((n+1)); bash "$f" || rc=$?; done; [ "$n" -gt 0 ] || { echo "lisa-remote-env entrypoint not found. PWD=$PWD HOME=$HOME" >&2; ls -1 . "$HOME" /workspace 2>&1 | head -40 >&2; exit 1; }; exit "$rc"
18
18
  ```
19
19