rl-workspaces 0.5.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 (55) hide show
  1. package/README.md +234 -193
  2. package/bin/rl +2 -54
  3. package/dist/agent-launch.js +1 -1
  4. package/dist/agent-presets.js +42 -17
  5. package/dist/agent-resume.js +2 -2
  6. package/dist/agent-runner.js +20 -0
  7. package/dist/agent-workflows.js +60 -16
  8. package/dist/automation-config.js +144 -5
  9. package/dist/bootstrap.js +74 -0
  10. package/dist/cli.js +2 -0
  11. package/dist/command-context.js +1 -1
  12. package/dist/common.js +3 -2
  13. package/dist/dependencies.js +197 -0
  14. package/dist/doctor.js +1 -1
  15. package/dist/git-workflow.js +8 -102
  16. package/dist/help.js +113 -116
  17. package/dist/hooks.js +1 -1
  18. package/dist/install.js +143 -0
  19. package/dist/mcp-setup.js +171 -0
  20. package/dist/mcp.js +13 -16
  21. package/dist/picker.js +24 -7
  22. package/dist/pr-workflow.js +7 -28
  23. package/dist/repos.js +5 -1
  24. package/dist/session-workflows.js +122 -18
  25. package/dist/session.js +1 -1
  26. package/dist/state.js +4 -14
  27. package/dist/storage.js +2 -2
  28. package/dist/store.js +2 -2
  29. package/dist/summary.js +1 -1
  30. package/dist/workflow-collaboration.js +531 -0
  31. package/dist/workflow.js +24 -38
  32. package/lib/entry.zsh +54 -0
  33. package/lib/rl.zsh +37 -9
  34. package/node_modules/proper-lockfile/CHANGELOG.md +108 -0
  35. package/node_modules/smol-toml/LICENSE +24 -0
  36. package/node_modules/smol-toml/README.md +605 -0
  37. package/node_modules/smol-toml/dist/date.d.ts +41 -0
  38. package/node_modules/smol-toml/dist/date.js +156 -0
  39. package/node_modules/smol-toml/dist/error.d.ts +37 -0
  40. package/node_modules/smol-toml/dist/error.js +67 -0
  41. package/node_modules/smol-toml/dist/extract.js +293 -0
  42. package/node_modules/smol-toml/dist/index.cjs +771 -0
  43. package/node_modules/smol-toml/dist/index.d.ts +44 -0
  44. package/node_modules/smol-toml/dist/index.js +34 -0
  45. package/node_modules/smol-toml/dist/parse.d.ts +73 -0
  46. package/node_modules/smol-toml/dist/parse.js +164 -0
  47. package/node_modules/smol-toml/dist/primitive.js +151 -0
  48. package/node_modules/smol-toml/dist/stringify.d.ts +32 -0
  49. package/node_modules/smol-toml/dist/stringify.js +219 -0
  50. package/node_modules/smol-toml/dist/struct.js +156 -0
  51. package/node_modules/smol-toml/dist/util.d.ts +43 -0
  52. package/node_modules/smol-toml/dist/util.js +57 -0
  53. package/node_modules/smol-toml/package.json +68 -0
  54. package/package.json +6 -3
  55. package/shell/rl.zsh +1 -1
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)
@@ -1,116 +1,22 @@
1
1
  import path from "node:path";
2
- import { randomUUID } from "node:crypto";
3
- import { args, arity, canonical, execute, git, gitRaw, locked, required, } from "./common.js";
2
+ import { args, arity, canonical, execute, git, locked, required, } from "./common.js";
4
3
  import { commandContext, contextOptions, resolveWorkspace, } from "./command-context.js";
5
4
  import { worktrees } from "./repos.js";
6
5
  import { assertReady, GIT_WORKFLOW_DIRECTORY, GIT_WORKFLOW_LOCK, } from "./git-safety.js";
7
- const BACKUP_PREFIX = "refs/rl/backups/";
8
- const READ_OPTIONS = new Set([
9
- "--stat",
10
- "--name-only",
11
- "--name-status",
12
- "--numstat",
13
- "--patch",
14
- "--oneline",
15
- "--graph",
16
- "--decorate",
17
- "--all",
18
- ]);
19
6
  function revision(root, ref) {
20
7
  return git(root, "rev-parse", "--verify", "--end-of-options", `${ref}^{commit}`);
21
8
  }
22
9
  export async function main(argv) {
23
- const separator = argv.indexOf("--");
24
- const trailing = separator < 0 ? [] : argv.slice(separator + 1);
25
- const { values, positionals } = args(separator < 0 ? argv : argv.slice(0, separator), [...contextOptions, "base", "message"], ["all", "squash", "cleanup", "dry-run"], { message: "m" });
26
- const context = commandContext(values);
10
+ const { values, positionals } = args(argv, [...contextOptions, "message"], ["squash", "cleanup", "dry-run"], { message: "m" });
27
11
  const [command, ...rest] = positionals;
12
+ if (command !== "merge")
13
+ throw new Error("Unknown Git workflow command");
14
+ arity(rest, 0, 1);
15
+ if (values.message && !values.squash)
16
+ throw new Error("Invalid merge options");
17
+ const context = commandContext(values);
28
18
  const source = resolveWorkspace(context);
29
- if (command === "diff" || command === "log") {
30
- arity(rest, 0);
31
- if (values.all ||
32
- values.squash ||
33
- values.cleanup ||
34
- values.message ||
35
- values["dry-run"])
36
- throw new Error("Invalid display options");
37
- const pathSeparator = trailing.indexOf("--");
38
- const flags = pathSeparator < 0 ? trailing : trailing.slice(0, pathSeparator);
39
- const paths = pathSeparator < 0 ? [] : trailing.slice(pathSeparator + 1);
40
- for (const flag of flags)
41
- if (!READ_OPTIONS.has(flag) && !/^(-\d+|--max-count=\d+)$/.test(flag))
42
- throw new Error(`Unsupported display option: ${flag}`);
43
- const base = revision(source.path, String(values.base ?? context.defaultBase));
44
- const range = command === "diff"
45
- ? git(source.path, "merge-base", base, "HEAD")
46
- : `${base}..HEAD`;
47
- process.stdout.write(gitRaw(source.path, "--no-pager", command, "--no-ext-diff", "--no-textconv", ...(command === "log" ? ["--oneline"] : []), ...flags, range, "--", ...paths));
48
- return 0;
49
- }
50
- if (trailing.length)
51
- throw new Error("Extra arguments are only supported by diff and log");
52
19
  return locked(path.join(context.store.root, GIT_WORKFLOW_DIRECTORY), GIT_WORKFLOW_LOCK, async () => {
53
- if (command === "step") {
54
- arity(rest, 1);
55
- const [action] = rest;
56
- if (!["commit", "rebase", "squash"].includes(action))
57
- throw new Error("Usage: rl step commit|rebase|squash");
58
- if (values.cleanup ||
59
- values.squash ||
60
- (values.all && action !== "commit"))
61
- throw new Error("Invalid step options");
62
- assertReady(source.path, action !== "commit", source.branch);
63
- if (action === "commit") {
64
- if (values.base)
65
- throw new Error("commit does not accept --base");
66
- const commitMessage = required(values, "message");
67
- if (values["dry-run"]) {
68
- const changes = git(source.path, "diff", "--name-only", ...(values.all ? ["HEAD"] : ["--cached"]));
69
- if (!changes)
70
- throw new Error("No changes to commit");
71
- console.log(`Would commit ${values.all ? "tracked" : "staged"} changes in ${source.branch}: ${commitMessage}\n${changes}`);
72
- return 0;
73
- }
74
- console.log(git(source.path, "commit", ...(values.all ? ["--all"] : []), "-m", required(values, "message")));
75
- return 0;
76
- }
77
- if (action === "rebase" && values.message)
78
- throw new Error("rebase does not accept --message");
79
- const base = revision(source.path, String(values.base ?? context.defaultBase));
80
- if (action === "rebase") {
81
- if (values["dry-run"]) {
82
- console.log(`Would rebase ${source.branch} onto ${base}`);
83
- return 0;
84
- }
85
- const result = await execute(["git", "-C", source.path, "rebase", base], source.path);
86
- process.stdout.write(result.stdout);
87
- if (result.code)
88
- throw new Error(`${result.stderr.trim()}\nRebase did not complete. Resolve conflicts and run git rebase --continue, or git rebase --abort in ${source.path}`);
89
- return 0;
90
- }
91
- const message = required(values, "message");
92
- const head = revision(source.path, "HEAD");
93
- const baseCommit = git(source.path, "merge-base", base, head);
94
- if (baseCommit === head)
95
- throw new Error("No feature commits to squash");
96
- if (values["dry-run"]) {
97
- console.log(`Would squash commits after ${baseCommit} in ${source.branch}: ${message}`);
98
- return 0;
99
- }
100
- const tree = git(source.path, "rev-parse", `${head}^{tree}`);
101
- const backup = BACKUP_PREFIX + randomUUID();
102
- const squashed = git(source.path, "commit-tree", tree, "-p", baseCommit, "-m", message);
103
- git(source.path, "update-ref", backup, head);
104
- // CAS leaves concurrent branch changes untouched; the worktree and index never change.
105
- git(source.path, "update-ref", "-m", "rl step squash", `refs/heads/${source.branch}`, squashed, head);
106
- console.log(`Squashed feature commits. Original HEAD retained at ${backup}`);
107
- return 0;
108
- }
109
- if (command !== "merge")
110
- throw new Error("Unknown Git workflow command");
111
- arity(rest, 0, 1);
112
- if (values.all || values.base || (values.message && !values.squash))
113
- throw new Error("Invalid merge options");
114
20
  const selected = rest[0] ?? context.defaultBase.replace(/^origin\//, "");
115
21
  const targetRecord = worktrees(context.repo).find((record) => record.branch === `refs/heads/${selected}`);
116
22
  if (!targetRecord)