rl-workspaces 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +62 -53
  2. package/bin/rl +2 -54
  3. package/dist/agent-launch.js +1 -1
  4. package/dist/agent-resume.js +2 -2
  5. package/dist/bootstrap.js +74 -0
  6. package/dist/cli.js +2 -0
  7. package/dist/command-context.js +1 -1
  8. package/dist/common.js +3 -2
  9. package/dist/dependencies.js +197 -0
  10. package/dist/doctor.js +1 -1
  11. package/dist/help.js +81 -84
  12. package/dist/hooks.js +1 -1
  13. package/dist/install.js +143 -0
  14. package/dist/mcp-setup.js +171 -0
  15. package/dist/mcp.js +13 -16
  16. package/dist/picker.js +24 -7
  17. package/dist/pr-workflow.js +7 -28
  18. package/dist/repos.js +5 -1
  19. package/dist/session-workflows.js +71 -1
  20. package/dist/session.js +1 -1
  21. package/dist/state.js +4 -14
  22. package/dist/storage.js +2 -2
  23. package/dist/store.js +2 -2
  24. package/dist/summary.js +1 -1
  25. package/dist/workflow.js +24 -38
  26. package/lib/entry.zsh +54 -0
  27. package/lib/rl.zsh +35 -6
  28. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  29. package/node_modules/smol-toml/LICENSE +24 -0
  30. package/node_modules/smol-toml/README.md +605 -0
  31. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  32. package/node_modules/smol-toml/dist/date.js +156 -0
  33. package/node_modules/smol-toml/dist/error.d.ts +37 -0
  34. package/node_modules/smol-toml/dist/error.js +67 -0
  35. package/node_modules/smol-toml/dist/extract.js +293 -0
  36. package/node_modules/smol-toml/dist/index.cjs +771 -0
  37. package/node_modules/smol-toml/dist/index.d.ts +44 -0
  38. package/node_modules/smol-toml/dist/index.js +34 -0
  39. package/node_modules/smol-toml/dist/parse.d.ts +73 -0
  40. package/node_modules/smol-toml/dist/parse.js +164 -0
  41. package/node_modules/smol-toml/dist/primitive.js +151 -0
  42. package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
  43. package/node_modules/smol-toml/dist/stringify.js +219 -0
  44. package/node_modules/smol-toml/dist/struct.js +156 -0
  45. package/node_modules/smol-toml/dist/util.d.ts +43 -0
  46. package/node_modules/smol-toml/dist/util.js +57 -0
  47. package/node_modules/smol-toml/package.json +68 -0
  48. package/package.json +6 -3
  49. package/shell/rl.zsh +1 -1
package/README.md CHANGED
@@ -6,31 +6,41 @@ Create a workspace, start an agent, and keep task notes and sessions together.
6
6
  Switch projects, prepare local dependencies, check PRs, and merge finished work
7
7
  without losing your place.
8
8
 
9
- Requires macOS or Linux, Git, zsh, and Node.js 22+. Bash users can use the Bash
10
- integration. Install `fzf` for pickers, `gh` for PR commands, and whichever agent
11
- you use.
9
+ Requires macOS or Linux and Node.js 22+. Global npm installation sets up Git,
10
+ zsh, fzf, GitHub CLI and integration for your zsh or bash terminal.
12
11
 
13
12
  ## Install
14
13
 
15
14
  Install from npm:
16
15
 
17
16
  ```sh
18
- npm install --global rl-workspaces
17
+ npm install --global rl-workspaces --foreground-scripts
19
18
  ```
20
19
 
21
20
  See Development below to build and try unpublished changes.
22
21
 
23
- Add one line to your shell configuration, then open a new terminal:
22
+ The installer installs missing tools through your package manager, preserves
23
+ your shell settings, and registers one RL integration block. Open a new terminal
24
+ after installation; bare `rl` can then select a workspace and change directory.
25
+ For the terminal that ran the install, use the activation command printed by the
26
+ installer, or:
24
27
 
25
28
  ```sh
26
- eval "$(command rl setup shell zsh)" # ~/.zshrc
29
+ eval "$(command rl setup shell zsh)"
27
30
  # or
28
- eval "$(command rl setup shell bash)" # ~/.bashrc
31
+ eval "$(command rl setup shell bash)"
29
32
  ```
30
33
 
31
- Shell integration lets `rl` change your terminal's directory. Direct executable
32
- calls print the selected path. The integration loads the current runtime on
33
- each call, so an npm upgrade does not leave you using old commands.
34
+ This activates the setup already registered for future terminals. Shell
35
+ integration is what lets `rl` change the calling terminal's directory; an
36
+ executable by itself can only print the selected path. The integration loads
37
+ the current runtime on each call, including after npm upgrades.
38
+
39
+ Tool installation needs Homebrew on macOS, or Homebrew/APT/DNF/Yum/Pacman/APK
40
+ on Linux, with permission to install packages. A failed setup reports the command
41
+ to finish in your terminal. If lifecycle scripts were disabled, run
42
+ `rl setup install` to complete everything. `rl setup dependencies --check`
43
+ checks tools without changing them. GitHub PR commands require `gh auth login`.
34
44
 
35
45
  ## Get started
36
46
 
@@ -81,13 +91,17 @@ under `rl agent`, cleanup under `rl storage`, and project/shell setup under
81
91
  | `rl delete` | Remove workspaces |
82
92
  | `rl storage` | Inspect disk usage and clean generated files |
83
93
 
84
- `rl help switch` also documents the specialized `new`, `open`, and `path`
85
- helpers. `rl help exec` describes running an arbitrary command in a workspace.
94
+ `rl switch <branch>` opens an existing checkout or creates a managed workspace
95
+ for an existing local or origin branch. Remote branches are tracked automatically.
96
+ `rl switch -c <branch>` creates a feature workspace from the configured base and
97
+ refuses an already checked-out branch. Both use nested branch names as directory
98
+ paths. Switching installs hooks and preserves existing notes. A checkout that RL
99
+ adopts outside its usual worktree folder also appears in the open picker and can
100
+ be selected by directory name or branch. Unadopted outside checkouts stay hidden;
101
+ the delete picker remains restricted to the managed worktree folder.
86
102
 
87
- Switching installs hooks and preserves existing notes. Creation validates names
88
- and refuses path escapes or an already checked-out branch. `switch -c` requires a
89
- new checkout; `new` and `open` can reopen an existing checkout. `open` flattens
90
- slashes in directory names; `new` and `switch -c` retain nested names.
103
+ `rl help path` documents printing workspace paths, and `rl help exec` describes
104
+ running a command with a workspace's configured environment.
91
105
 
92
106
  Pickers show cached PR state, commits ahead/behind the base, and recent use.
93
107
  The `COMMITS + / −` column counts commits ahead/behind, rather than changed lines.
@@ -210,7 +224,6 @@ rl pr # open this branch's PR in your browser
210
224
  rl pr sync # save this branch's PR state
211
225
  rl pr show # readable saved snapshot with the full PR link
212
226
  rl pr show --json # saved metadata for scripts
213
- rl pr checks [--json] # query checks and preserve gh's exit status
214
227
  rl pr checkout 123 # new managed pr-123 workspace
215
228
  rl pr checkout 123 --name review/login
216
229
  rl pr update --base test # select your RL session PRs behind test, then update them
@@ -224,7 +237,8 @@ refresh output. `rl pr show` works offline. `rl pr` uses the saved URL when
224
237
  available; if the browser cannot open, that link remains in its output. With no
225
238
  saved PR, it looks up the current branch through `gh pr view --web`.
226
239
 
227
- Opening, refreshing, checks, and checkout require authenticated `gh`. Checkout verifies the origin repository,
240
+ Opening, refreshing, and checkout require authenticated `gh`. Use `gh pr checks`
241
+ to inspect CI results. Checkout verifies the origin repository,
228
242
  PR identity, and fetched commit before creating a workspace. Fork PR checkout
229
243
  is currently rejected. It does not change your current branch, publish, or merge
230
244
  a PR. Failed refreshes retain the previous snapshot.
@@ -336,7 +350,7 @@ used matching session directly.
336
350
  `rl continue` still resumes the exact session saved in its task handoff.
337
351
 
338
352
  Run Codex normally in an adopted RL workspace, including in the Codex app.
339
- `rl status`, `rl list`, `rl instances`, `rl agent sessions`, and `rl resume` / `rl resume codex`
353
+ `rl status`, `rl list`, `rl agent sessions`, and `rl resume` / `rl resume codex`
340
354
  automatically discover and save matching sessions through Codex's
341
355
  [`thread/list` API](https://learn.chatgpt.com/docs/app-server#list-threads-with-pagination--filters).
342
356
  Discovery filters by the exact workspace directory, excludes subagents and
@@ -387,6 +401,14 @@ creates and controls its native children. RL supplies their editable roles,
387
401
  handoff rules, review policy and durable run records. RL never launches a
388
402
  provider CLI, model API or check process for a workflow.
389
403
 
404
+ Use your normal agent session in the workspace. RL's hooks advertise discovery
405
+ automatically; a special launcher or pasted workflow prompt is unnecessary.
406
+ An agent can run `rl workflow context --json` through its ordinary shell tool.
407
+ The response includes active definitions plus `protocol.tools`: each operation's
408
+ input schema, CLI command, positional/flag mappings and JSON stdin requirements.
409
+ The same schemas are advertised through MCP, so an agent can use either connection.
410
+ Native subagent support still comes from the current session's host.
411
+
390
412
  The default team starts together and stays available for the task:
391
413
 
392
414
  - **Writer** implements scoped changes. Before adding a function, it asks DRY
@@ -600,27 +622,30 @@ the server does not inherit it. Claude requires an explicit UUID.
600
622
  Append tools take `{"text":"..."}`. They share the same locked document updates
601
623
  as the CLI and expose no arbitrary command, deletion, or filesystem tool.
602
624
 
603
- In the adopted workspace, add an entry to its trusted `.codex/config.toml`:
604
-
605
- ```toml
606
- [mcp_servers.rl]
607
- command = "rl"
608
- args = ["agent", "mcp"]
609
- ```
625
+ From an adopted RL workspace, connect your current agent with:
610
626
 
611
- For Claude, merge this entry into the workspace's `.mcp.json`:
612
-
613
- ```json
614
- {
615
- "mcpServers": {
616
- "rl": { "type": "stdio", "command": "rl", "args": ["agent", "mcp"] }
617
- }
618
- }
627
+ ```sh
628
+ rl setup mcp
619
629
  ```
620
630
 
621
- The client must launch the server in this workspace with `rl` on PATH.
622
- With custom state settings, pass the same `RL_STATE_DIR` in the server environment.
623
- Review project trust/server approval normally. Configuration follows the official
631
+ This configures both `.codex/config.toml` and `.mcp.json` in the current checkout.
632
+ Pass `codex` or `claude` to select one client, or `--dry-run` to preview changes.
633
+ Setup preserves other servers and settings, including Codex comments. It updates
634
+ RL-managed entries on later runs and leaves a conflicting manually configured
635
+ `rl` entry for you to resolve. Both files are validated before writing either.
636
+
637
+ The client starts the installed Node and RL entry point directly. The server is
638
+ bound to this checkout, repository and RL state directory, even when the client
639
+ starts it elsewhere or has no `rl` executable on PATH. Entries contain local
640
+ installation/workspace paths: run setup in each checkout and rerun after moving
641
+ Node or RL. Setup changes only this workspace's configuration.
642
+
643
+ Reload the client and review its normal project trust/MCP approval. The server
644
+ supplies discovery, consultations and rated reviews to your existing session;
645
+ the host supplies native agent creation and messaging. CLI discovery remains
646
+ available without an MCP connection. The stdio server can also be launched with
647
+ `rl agent mcp`, or `rl agent mcp --workspace <absolute-path>` when the repository
648
+ has been selected explicitly. Configuration follows the official
624
649
  [Codex MCP guide](https://learn.chatgpt.com/docs/extend/mcp?surface=cli) and
625
650
  [Claude MCP guide](https://code.claude.com/docs/en/mcp).
626
651
  RL does not modify global client settings or relax agent sandbox permissions.
@@ -697,22 +722,6 @@ Environment variables can also come from trusted zsh configuration selected by
697
722
  [config.example.zsh](config.example.zsh). Registration supplies repository/base
698
723
  settings; `rl help` and individual `--help` show command options.
699
724
 
700
- ## Compatibility aliases
701
-
702
- Existing commands keep their behavior. They are omitted from the main help to
703
- keep one primary spelling for each operation:
704
-
705
- | Previous command | Primary command |
706
- | --- | --- |
707
- | `rl merge` | `rl git merge` |
708
- | `rl context/progress show/set/append/path` | `rl notes show/set/append/path context/progress` |
709
- | `rl summary` | `rl notes summary` |
710
- | `rl session list/save/add` | `rl agent sessions/save` |
711
- | `rl handoff`, `rl mcp` | `rl agent handoff`, `rl agent mcp` |
712
- | `rl clean` | `rl storage clean` |
713
- | `rl config init/show`, `rl adopt`, `rl init` | `rl setup init/show`, `rl setup adopt`, `rl setup shell` |
714
- | `rl status --list`, `rl status --all` | `rl list`, `rl list --all` |
715
-
716
725
  ## Development
717
726
 
718
727
  ```sh
package/bin/rl CHANGED
@@ -1,54 +1,2 @@
1
- #!/bin/zsh -f
2
- emulate -L zsh
3
- readonly rl_home="${${(%):-%x}:A:h:h}"
4
- if [[ "$1" == __hook ]]; then
5
- # Hooks resolve the installed CLI on PATH, not a version-specific package path.
6
- # Their explicit repository/state arguments must not be replaced by user config.
7
- shift
8
- exec node "$rl_home/dist/cli.js" state "$@"
9
- fi
10
- if [[ "$1" == --version || "$1" == -V ]]; then
11
- exec node -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8")).version' "$rl_home/package.json"
12
- fi
13
- if [[ "$1" == init ]]; then
14
- if (( $# == 1 )); then
15
- exec node "$rl_home/dist/help.js" init
16
- fi
17
- for argument in "$@"; do
18
- [[ "$argument" == -- ]] && break
19
- if [[ "$argument" == --help || "$argument" == -h ]]; then
20
- exec node "$rl_home/dist/help.js" "$@"
21
- fi
22
- done
23
- if [[ "$2" != zsh && "$2" != bash || $# != 2 ]]; then
24
- print -u2 'Usage: rl setup shell <zsh|bash>'
25
- exit 1
26
- fi
27
- if [[ "$2" == zsh ]]; then
28
- print -r -- "source ${(q)rl_home}/shell/rl.zsh"
29
- else
30
- print -r -- "_rl_executable=${(qq)rl_home}/bin/rl"
31
- cat <<'BASH'
32
- rl() {
33
- local destination_file destination result previous="$PWD"
34
- local -a arguments=("$@")
35
- if [[ "$1" == switch && "${2:-}" == - ]]; then
36
- [[ -n "${RL_PREVIOUS_WORKSPACE:-}" ]] || { printf '%s\n' 'No previous workspace in this shell' >&2; return 1; }
37
- arguments[1]="$RL_PREVIOUS_WORKSPACE"
38
- fi
39
- destination_file=$(mktemp "${TMPDIR:-/tmp}/rl-shell.XXXXXXXX") || return 1
40
- if RL_SHELL_OUTPUT="$destination_file" command "$_rl_executable" "${arguments[@]}"; then result=0; else result=$?; fi
41
- destination=$(cat -- "$destination_file")
42
- rm -f -- "$destination_file"
43
- if [[ -n "$destination" ]]; then
44
- builtin cd -- "$destination" || return 1
45
- RL_PREVIOUS_WORKSPACE="$previous"
46
- fi
47
- return "$result"
48
- }
49
- BASH
50
- fi
51
- exit 0
52
- fi
53
- source "$rl_home/lib/rl.zsh" || exit 1
54
- _rl_main "$@"
1
+ #!/usr/bin/env node
2
+ await import('../dist/bootstrap.js');
@@ -53,7 +53,7 @@ export async function main(argv) {
53
53
  if (extra.length ||
54
54
  values.prompt !== undefined ||
55
55
  (values.stdout && values.output !== undefined))
56
- throw new Error("Usage: rl handoff [agent] [--stdout | --output path]");
56
+ throw new Error("Usage: rl agent handoff [agent] [--stdout | --output path]");
57
57
  const text = await handoff(context.store, name, target.path);
58
58
  if (typeof values.output === "string")
59
59
  fs.writeFileSync(values.output, text, { flag: "wx", mode: 0o600 });
@@ -19,7 +19,7 @@ export async function resumeSessions(store, kind, options = {}) {
19
19
  : a.agent.localeCompare(b.agent) || a.id.localeCompare(b.id);
20
20
  });
21
21
  if (!choices.length)
22
- throw new Error(`No ${kind ? `${kind} ` : ""}session recorded for ${data.id}. Use 'rl session save' to save a session first`);
22
+ throw new Error(`No ${kind ? `${kind} ` : ""}session recorded for ${data.id}. Use 'rl agent save' to save a session first`);
23
23
  let selected = choices[0];
24
24
  if (choices.length > 1 && !options.latest) {
25
25
  const visible = (value) => stripVTControlCharacters(value).replace(/[\x00-\x1f\x7f-\x9f]/g, " ");
@@ -70,7 +70,7 @@ export async function resumeAgent(store, kind, options = {}) {
70
70
  ? sessions.find((item) => item.id === sessionId(options.sessionId))
71
71
  : sessions.reduce((a, b) => !a || timestamp(b.lastUsedAt) > timestamp(a.lastUsedAt) ? b : a, undefined);
72
72
  if (!item)
73
- throw new Error(`No ${kind} session recorded for ${data.id}. Use 'rl session save ${kind}${kind === "claude" ? " <UUID>" : ""}'`);
73
+ throw new Error(`No ${kind} session recorded for ${data.id}. Use 'rl agent save ${kind}${kind === "claude" ? " <UUID>" : ""}'`);
74
74
  return [file, data, sessionId(item.id)];
75
75
  });
76
76
  const stamp = now();
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ import { fileURLToPath } from "node:url";
5
+ import { findExecutable, setupDependencies } from "./dependencies.js";
6
+ import { installRl } from "./install.js";
7
+ import { explicitHelp } from "./help.js";
8
+ const argv = process.argv.slice(2);
9
+ const selected = argv[0] === "--repo"
10
+ ? argv.slice(2)
11
+ : argv[0]?.startsWith("--repo=")
12
+ ? argv.slice(1)
13
+ : argv;
14
+ if (selected[0] === "setup" &&
15
+ ["dependencies", "install"].includes(selected[1])) {
16
+ try {
17
+ if (selected.includes("--help") || selected.includes("-h"))
18
+ console.log(explicitHelp(selected));
19
+ else
20
+ process.exitCode =
21
+ selected[1] === "install"
22
+ ? await installRl(selected.slice(2))
23
+ : await setupDependencies(selected.slice(2));
24
+ }
25
+ catch (error) {
26
+ console.error(`rl: ${error instanceof Error ? error.message : String(error)}`);
27
+ process.exitCode = 1;
28
+ }
29
+ }
30
+ else if (argv[0] === "--version" || argv[0] === "-V") {
31
+ console.log(JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version);
32
+ }
33
+ else {
34
+ const zsh = findExecutable("zsh");
35
+ if (!zsh) {
36
+ console.error("rl: zsh is required. Run 'rl setup dependencies' to install the RL toolchain.");
37
+ process.exitCode = 1;
38
+ }
39
+ else {
40
+ const child = spawn(zsh, [
41
+ "-f",
42
+ fileURLToPath(new URL("../lib/entry.zsh", import.meta.url)),
43
+ ...argv,
44
+ ], {
45
+ stdio: "inherit",
46
+ env: {
47
+ ...process.env,
48
+ PATH: `${process.env.PATH ? process.env.PATH + path.delimiter : ""}${path.dirname(process.execPath)}`,
49
+ },
50
+ });
51
+ const interrupt = () => child.kill("SIGINT");
52
+ const terminate = () => child.kill("SIGTERM");
53
+ process.on("SIGINT", interrupt);
54
+ process.on("SIGTERM", terminate);
55
+ await new Promise((resolve) => {
56
+ const cleanup = () => {
57
+ process.off("SIGINT", interrupt);
58
+ process.off("SIGTERM", terminate);
59
+ };
60
+ child.on("error", (error) => {
61
+ cleanup();
62
+ console.error(`rl: ${error.message}`);
63
+ process.exitCode = 1;
64
+ resolve();
65
+ });
66
+ child.on("close", (code, signal) => {
67
+ cleanup();
68
+ process.exitCode =
69
+ code ?? (signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 1);
70
+ resolve();
71
+ });
72
+ });
73
+ }
74
+ }
package/dist/cli.js CHANGED
@@ -10,6 +10,7 @@ import { main as git } from "./git-workflow.js";
10
10
  import { main as pr } from "./pr-workflow.js";
11
11
  import { main as agent } from "./agent-launch.js";
12
12
  import { main as mcp } from "./mcp.js";
13
+ import { main as mcpSetup } from "./mcp-setup.js";
13
14
  import { main as task } from "./task.js";
14
15
  import { main as storage } from "./storage.js";
15
16
  import { main as prUpdate } from "./pr-update.js";
@@ -25,6 +26,7 @@ const commands = {
25
26
  pr,
26
27
  agent,
27
28
  mcp,
29
+ "mcp-setup": mcpSetup,
28
30
  task,
29
31
  storage,
30
32
  "pr-update": prUpdate,
@@ -49,7 +49,7 @@ export function resolveWorkspace(context, selector) {
49
49
  const [root, branch] = context.store.worktree(context.repo);
50
50
  return { path: root, name: "@", branch };
51
51
  }
52
- const choices = candidates(context.repo, context.managedDir);
52
+ const choices = candidates(context.repo, context.managedDir, context.store);
53
53
  const matches = choices.filter((item) => item.name === selector ||
54
54
  item.branch === selector ||
55
55
  (path.isAbsolute(selector) && item.path === canonical(selector)));
package/dist/common.js CHANGED
@@ -6,6 +6,7 @@ import { spawnSync, spawn } from "node:child_process";
6
6
  import { parseArgs } from "node:util";
7
7
  import lockfile from "proper-lockfile";
8
8
  import { readJsonObject } from "./json.js";
9
+ import { executableError } from "./dependencies.js";
9
10
  export const now = () => new Date().toISOString();
10
11
  export const message = (error) => error instanceof Error ? error.message : String(error);
11
12
  export const json = (data) => console.log(JSON.stringify(data, null, 2));
@@ -54,7 +55,7 @@ export function run(argv, cwd, raw = false) {
54
55
  maxBuffer: 32 * 1024 * 1024,
55
56
  });
56
57
  if (result.error)
57
- throw result.error;
58
+ throw executableError(result.error, argv[0], cwd);
58
59
  if (result.status !== 0)
59
60
  throw new Error(result.stderr.trim() || `Command failed: ${argv.join(" ")}`);
60
61
  return raw ? result.stdout : result.stdout.trim();
@@ -130,7 +131,7 @@ export async function execute(argv, cwd, options = {}) {
130
131
  };
131
132
  child.on("error", (error) => {
132
133
  cleanup();
133
- reject(error);
134
+ reject(executableError(error, argv[0], cwd));
134
135
  });
135
136
  child.on("close", (code, signal) => {
136
137
  cleanup();
@@ -0,0 +1,197 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ import { parseArgs } from "node:util";
5
+ import { fileURLToPath } from "node:url";
6
+ export const TOOL_NAMES = ["git", "zsh", "fzf", "gh"];
7
+ export function findExecutable(name, env = process.env) {
8
+ for (const directory of (env.PATH ?? "").split(path.delimiter)) {
9
+ if (!directory)
10
+ continue;
11
+ const file = path.resolve(directory, name);
12
+ try {
13
+ fs.accessSync(file, fs.constants.X_OK);
14
+ if (fs.statSync(file).isFile())
15
+ return file;
16
+ }
17
+ catch {
18
+ /* Try the next PATH entry. */
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ export function inspectDependencies(env = process.env) {
24
+ return TOOL_NAMES.map((name) => {
25
+ const executable = findExecutable(name, env);
26
+ const probe = executable
27
+ ? spawnSync(executable, ["--version"], {
28
+ env,
29
+ encoding: "utf8",
30
+ timeout: 5000,
31
+ })
32
+ : undefined;
33
+ return {
34
+ name,
35
+ path: executable,
36
+ available: probe?.status === 0,
37
+ version: probe?.status === 0
38
+ ? (probe.stdout.trim().split(/\r?\n/)[0]?.slice(0, 200) ?? null)
39
+ : null,
40
+ };
41
+ });
42
+ }
43
+ function installPlan(missing, host, interactive) {
44
+ const env = host.env ?? process.env;
45
+ const platform = host.platform ?? process.platform;
46
+ const brew = findExecutable("brew", env);
47
+ if (brew)
48
+ return [{ executable: brew, args: ["install", ...missing] }];
49
+ if (platform === "darwin")
50
+ throw new Error("Homebrew is required to install missing RL tools on macOS. Install it from https://brew.sh, then run 'rl setup dependencies'.");
51
+ if (platform !== "linux")
52
+ throw new Error("RL supports macOS and Linux");
53
+ const managers = [
54
+ { name: "apt-get", args: ["install", "-y"], packages: missing },
55
+ { name: "dnf", args: ["install", "-y"], packages: missing },
56
+ { name: "yum", args: ["install", "-y"], packages: missing },
57
+ {
58
+ name: "pacman",
59
+ args: ["-S", "--needed", "--noconfirm"],
60
+ packages: missing.map((name) => (name === "gh" ? "github-cli" : name)),
61
+ },
62
+ {
63
+ name: "apk",
64
+ args: ["add"],
65
+ packages: missing.map((name) => (name === "gh" ? "github-cli" : name)),
66
+ },
67
+ ];
68
+ for (const manager of managers) {
69
+ const executable = findExecutable(manager.name, env);
70
+ if (!executable)
71
+ continue;
72
+ const commands = manager.name === "apt-get"
73
+ ? [
74
+ { executable, args: ["update"] },
75
+ { executable, args: [...manager.args, ...manager.packages] },
76
+ ]
77
+ : [{ executable, args: [...manager.args, ...manager.packages] }];
78
+ const uid = host.uid ?? process.getuid?.();
79
+ if (uid === 0)
80
+ return commands;
81
+ const sudo = findExecutable("sudo", env);
82
+ if (!sudo)
83
+ throw new Error(`Installing missing RL tools requires administrator access. Run: sudo ${manager.name} ${[...manager.args, ...manager.packages].join(" ")}`);
84
+ return commands.map((command) => ({
85
+ executable: sudo,
86
+ args: [
87
+ ...(interactive ? [] : ["-n"]),
88
+ command.executable,
89
+ ...command.args,
90
+ ],
91
+ }));
92
+ }
93
+ throw new Error("No supported package manager found. Install Git, zsh, fzf and gh, then run 'rl setup dependencies --check'.");
94
+ }
95
+ export async function setupDependencies(argv, host = {}) {
96
+ const { values, positionals } = parseArgs({
97
+ args: argv,
98
+ allowPositionals: true,
99
+ options: {
100
+ install: { type: "boolean" },
101
+ check: { type: "boolean" },
102
+ "dry-run": { type: "boolean" },
103
+ json: { type: "boolean" },
104
+ },
105
+ });
106
+ if (positionals.length ||
107
+ (values.check && (values.install || values["dry-run"])))
108
+ throw new Error("Usage: rl setup dependencies [--check | --dry-run] [--json]");
109
+ const env = host.env ?? process.env;
110
+ let tools = inspectDependencies(env);
111
+ const missing = tools
112
+ .filter((tool) => !tool.available)
113
+ .map((tool) => tool.name);
114
+ let commands = [];
115
+ let error;
116
+ const interactive = !!process.stdin.isTTY && !!process.stdout.isTTY && !values.json;
117
+ if (missing.length && !values.check) {
118
+ try {
119
+ commands = installPlan(missing, host, interactive);
120
+ if (!values["dry-run"]) {
121
+ console.error(`RL: installing missing tools: ${missing.join(", ")}`);
122
+ for (const command of commands) {
123
+ const result = spawnSync(command.executable, command.args, {
124
+ env: {
125
+ ...env,
126
+ HOMEBREW_NO_AUTO_UPDATE: "1",
127
+ ...(interactive ? {} : { NONINTERACTIVE: "1" }),
128
+ },
129
+ stdio: interactive ? "inherit" : ["ignore", "pipe", "pipe"],
130
+ encoding: "utf8",
131
+ maxBuffer: 8 * 1024 * 1024,
132
+ });
133
+ if (!interactive) {
134
+ if (result.stdout)
135
+ process.stderr.write(result.stdout);
136
+ if (result.stderr)
137
+ process.stderr.write(result.stderr);
138
+ }
139
+ if (result.error || result.status !== 0)
140
+ throw new Error(`Could not install RL tools. Run '${command.executable} ${command.args.filter((arg) => arg !== "-n").join(" ")}' in your terminal, then rerun 'rl setup dependencies'.`);
141
+ }
142
+ tools = inspectDependencies(env);
143
+ const remaining = tools
144
+ .filter((tool) => !tool.available)
145
+ .map((tool) => tool.name);
146
+ if (remaining.length)
147
+ throw new Error(`Tools still unavailable on PATH: ${remaining.join(", ")}. Add the package manager's bin directory to PATH, then rerun 'rl setup dependencies'.`);
148
+ }
149
+ }
150
+ catch (failure) {
151
+ error = failure instanceof Error ? failure.message : String(failure);
152
+ }
153
+ }
154
+ const ready = tools.every((tool) => tool.available);
155
+ const report = {
156
+ ready,
157
+ dryRun: !!values["dry-run"],
158
+ tools,
159
+ commands,
160
+ ...(error ? { error } : {}),
161
+ };
162
+ if (values.json)
163
+ console.log(JSON.stringify(report, null, 2));
164
+ else {
165
+ for (const tool of tools)
166
+ console.log(`${tool.name}: ${tool.available ? (tool.version ?? "installed") : "missing or not working"}`);
167
+ if (values["dry-run"])
168
+ for (const command of commands)
169
+ console.log(`Would run: ${command.executable} ${command.args.join(" ")}`);
170
+ if (error)
171
+ console.error(`RL: ${error}`);
172
+ else if (!ready && values.check)
173
+ console.error("Run 'rl setup dependencies' to install missing tools.");
174
+ }
175
+ return error ? 1 : values["dry-run"] || ready ? 0 : 1;
176
+ }
177
+ export function executableError(error, executable, cwd) {
178
+ if (error.code !== "ENOENT")
179
+ return error;
180
+ if (!fs.existsSync(cwd))
181
+ return new Error(`Working directory does not exist: ${cwd}`, {
182
+ cause: error,
183
+ });
184
+ if (TOOL_NAMES.includes(path.basename(executable)))
185
+ return new Error(`Missing required tool ${path.basename(executable)}. Run 'rl setup dependencies' to install Git, zsh, fzf and gh.`, { cause: error });
186
+ return error;
187
+ }
188
+ if (process.argv[1] &&
189
+ path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
190
+ try {
191
+ process.exitCode = await setupDependencies(process.argv.slice(2));
192
+ }
193
+ catch (error) {
194
+ console.error(`RL: ${error instanceof Error ? error.message : String(error)}`);
195
+ process.exitCode = 1;
196
+ }
197
+ }
package/dist/doctor.js CHANGED
@@ -126,7 +126,7 @@ export async function doctor(store, selected, asJson = false) {
126
126
  console.log(` CLI: ${item.executablePath ?? "not on PATH"}; launch flags: ${item.launchVerified ? "verified in installed help" : "unverified"}; hook trust: unknown`);
127
127
  console.log(` Last received event: ${item.lastSuccess ? `${item.lastSuccess.event} at ${item.lastSuccess.at}` : "none"}`);
128
128
  if (item.missingEvents.length)
129
- console.log(` Missing hooks: ${item.missingEvents.join(", ")}. Run rl adopt.`);
129
+ console.log(` Missing hooks: ${item.missingEvents.join(", ")}. Run rl setup adopt.`);
130
130
  if (item.configError)
131
131
  console.log(` Configuration error: ${item.configError}`);
132
132
  if (item.cliError)