rl-workspaces 0.0.0-stage → 0.6.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.
- package/LICENSE +21 -0
- package/README.md +735 -2
- package/bin/rl +54 -0
- package/config.example.zsh +9 -0
- package/dist/access.js +40 -0
- package/dist/activity.js +90 -0
- package/dist/agent-claude.js +16 -0
- package/dist/agent-codex.js +22 -0
- package/dist/agent-launch.js +74 -0
- package/dist/agent-presets.js +70 -0
- package/dist/agent-registry.js +8 -0
- package/dist/agent-resume.js +96 -0
- package/dist/agent-runner.js +386 -0
- package/dist/agent-workflows.js +275 -0
- package/dist/agents.js +73 -0
- package/dist/automation-config.js +381 -0
- package/dist/base.js +66 -0
- package/dist/cleanup-path.js +33 -0
- package/dist/cli.js +57 -0
- package/dist/codex-sessions.js +188 -0
- package/dist/command-context.js +67 -0
- package/dist/common.js +262 -0
- package/dist/delete.js +113 -0
- package/dist/display.js +134 -0
- package/dist/doctor.js +145 -0
- package/dist/git-safety.js +24 -0
- package/dist/git-workflow.js +89 -0
- package/dist/help.js +726 -0
- package/dist/hooks.js +159 -0
- package/dist/json.js +34 -0
- package/dist/mcp.js +336 -0
- package/dist/picker.js +182 -0
- package/dist/pr-discovery.js +223 -0
- package/dist/pr-update.js +419 -0
- package/dist/pr-workflow.js +161 -0
- package/dist/repos.js +537 -0
- package/dist/review-policy.js +150 -0
- package/dist/session-workflows.js +650 -0
- package/dist/session.js +12 -0
- package/dist/state.js +250 -0
- package/dist/storage.js +366 -0
- package/dist/store.js +379 -0
- package/dist/summary.js +164 -0
- package/dist/task-state.js +65 -0
- package/dist/task.js +192 -0
- package/dist/templates.js +4 -0
- package/dist/workflow-collaboration.js +531 -0
- package/dist/workflow-config.js +177 -0
- package/dist/workflow.js +211 -0
- package/lib/rl.zsh +329 -0
- package/node_modules/graceful-fs/LICENSE +15 -0
- package/node_modules/graceful-fs/README.md +143 -0
- package/node_modules/graceful-fs/clone.js +23 -0
- package/node_modules/graceful-fs/graceful-fs.js +448 -0
- package/node_modules/graceful-fs/legacy-streams.js +118 -0
- package/node_modules/graceful-fs/package.json +53 -0
- package/node_modules/graceful-fs/polyfills.js +355 -0
- package/node_modules/proper-lockfile/LICENSE +21 -0
- package/node_modules/proper-lockfile/README.md +183 -0
- package/node_modules/proper-lockfile/index.js +40 -0
- package/node_modules/proper-lockfile/lib/adapter.js +85 -0
- package/node_modules/proper-lockfile/lib/lockfile.js +342 -0
- package/node_modules/proper-lockfile/lib/mtime-precision.js +55 -0
- package/node_modules/proper-lockfile/package.json +71 -0
- package/node_modules/retry/.npmignore +3 -0
- package/node_modules/retry/.travis.yml +15 -0
- package/node_modules/retry/License +21 -0
- package/node_modules/retry/Makefile +18 -0
- package/node_modules/retry/README.md +227 -0
- package/node_modules/retry/equation.gif +0 -0
- package/node_modules/retry/example/dns.js +31 -0
- package/node_modules/retry/example/stop.js +40 -0
- package/node_modules/retry/index.js +1 -0
- package/node_modules/retry/lib/retry.js +100 -0
- package/node_modules/retry/lib/retry_operation.js +158 -0
- package/node_modules/retry/package.json +32 -0
- package/node_modules/retry/test/common.js +10 -0
- package/node_modules/retry/test/integration/test-forever.js +24 -0
- package/node_modules/retry/test/integration/test-retry-operation.js +258 -0
- package/node_modules/retry/test/integration/test-retry-wrap.js +101 -0
- package/node_modules/retry/test/integration/test-timeouts.js +69 -0
- package/node_modules/signal-exit/LICENSE.txt +16 -0
- package/node_modules/signal-exit/README.md +39 -0
- package/node_modules/signal-exit/index.js +202 -0
- package/node_modules/signal-exit/package.json +38 -0
- package/node_modules/signal-exit/signals.js +53 -0
- package/package.json +57 -4
- package/shell/rl.zsh +11 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { execute, git, gitEnv, inside } from "./common.js";
|
|
5
|
+
import { isJsonObject, readJsonObject } from "./json.js";
|
|
6
|
+
import { validateCleanupPath } from "./cleanup-path.js";
|
|
7
|
+
export const WORKFLOW_FILE = ".rl/workflows.json";
|
|
8
|
+
const RESERVED_DIRECTORIES = new Set([".git", ".rl", ".codex", ".claude"]);
|
|
9
|
+
const ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
10
|
+
const ENV_PREFIX = /^(APP_|PUBLIC_|VITE_|NEXT_PUBLIC_|REACT_APP_)/;
|
|
11
|
+
const ENV_NAMES = new Set(["PORT", "NODE_ENV", "DATABASE_URL", "REDIS_URL"]);
|
|
12
|
+
const PORT_START = 10000;
|
|
13
|
+
const PORT_COUNT = 40000;
|
|
14
|
+
const CONFIG_KEYS = new Set(["copy", "clean", "env", "onCreate", "onOpen"]);
|
|
15
|
+
export const EMPTY_WORKFLOW = {
|
|
16
|
+
copy: [],
|
|
17
|
+
clean: [],
|
|
18
|
+
env: {},
|
|
19
|
+
onCreate: [],
|
|
20
|
+
onOpen: [],
|
|
21
|
+
};
|
|
22
|
+
export function readWorkflowConfig(repo) {
|
|
23
|
+
const file = path.join(repo, WORKFLOW_FILE);
|
|
24
|
+
if (fs
|
|
25
|
+
.lstatSync(path.dirname(file), { throwIfNoEntry: false })
|
|
26
|
+
?.isSymbolicLink() ||
|
|
27
|
+
fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
28
|
+
throw new Error("Workflow configuration must not be a symlink");
|
|
29
|
+
if (!fs.existsSync(file))
|
|
30
|
+
return { ...EMPTY_WORKFLOW };
|
|
31
|
+
const data = readJsonObject(file);
|
|
32
|
+
for (const key of Object.keys(data))
|
|
33
|
+
if (!CONFIG_KEYS.has(key))
|
|
34
|
+
throw new Error(`Unknown workflow setting: ${key}`);
|
|
35
|
+
const copy = data.copy ?? [];
|
|
36
|
+
if (!Array.isArray(copy) ||
|
|
37
|
+
!copy.every((item) => typeof item === "string" && item.length > 0))
|
|
38
|
+
throw new Error("Workflow copy must be an array of relative paths");
|
|
39
|
+
const env = {};
|
|
40
|
+
if (data.env !== undefined) {
|
|
41
|
+
if (!isJsonObject(data.env))
|
|
42
|
+
throw new Error("Workflow env must be an object of strings");
|
|
43
|
+
for (const [key, value] of Object.entries(data.env)) {
|
|
44
|
+
if (!ENV_NAME.test(key) ||
|
|
45
|
+
(!ENV_PREFIX.test(key) && !ENV_NAMES.has(key)) ||
|
|
46
|
+
typeof value !== "string" ||
|
|
47
|
+
value.includes("\0"))
|
|
48
|
+
throw new Error(`Unsupported workflow environment variable: ${key}. Use APP_, PUBLIC_, VITE_, NEXT_PUBLIC_, REACT_APP_ prefixes or PORT, NODE_ENV, DATABASE_URL, REDIS_URL`);
|
|
49
|
+
env[key] = value;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const cleanup = data.clean ?? [];
|
|
53
|
+
if (!Array.isArray(cleanup))
|
|
54
|
+
throw new Error("Workflow clean must be an array of relative directory paths");
|
|
55
|
+
const clean = cleanup.map((item) => {
|
|
56
|
+
validateCleanupPath(item);
|
|
57
|
+
return item;
|
|
58
|
+
});
|
|
59
|
+
const result = { copy, clean, env, onCreate: [], onOpen: [] };
|
|
60
|
+
for (const event of ["onCreate", "onOpen"]) {
|
|
61
|
+
const commands = data[event] ?? [];
|
|
62
|
+
if (!Array.isArray(commands))
|
|
63
|
+
throw new Error(`Workflow ${event} must be an array of argv arrays`);
|
|
64
|
+
for (const command of commands) {
|
|
65
|
+
if (!Array.isArray(command) ||
|
|
66
|
+
!command.length ||
|
|
67
|
+
!command.every((argument) => typeof argument === "string" && !argument.includes("\0")) ||
|
|
68
|
+
!command[0])
|
|
69
|
+
throw new Error(`Workflow ${event} commands must be nonempty argv arrays`);
|
|
70
|
+
result[event].push(command);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
export function workflowEnvironment(repo, workspace, config) {
|
|
76
|
+
const hash = createHash("sha256")
|
|
77
|
+
.update(repo)
|
|
78
|
+
.update("\0")
|
|
79
|
+
.update(workspace)
|
|
80
|
+
.digest();
|
|
81
|
+
const inherited = gitEnv();
|
|
82
|
+
delete inherited.RL_SHELL_OUTPUT;
|
|
83
|
+
delete inherited.RL_INTERNAL_SHELL;
|
|
84
|
+
return {
|
|
85
|
+
...inherited,
|
|
86
|
+
...config.env,
|
|
87
|
+
RL_REPO: "",
|
|
88
|
+
RL_REPO_ROOT: repo,
|
|
89
|
+
RL_WORKSPACE: workspace,
|
|
90
|
+
RL_PORT: String(PORT_START + (hash.readUInt32BE(0) % PORT_COUNT)),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export function containedPath(root, relative) {
|
|
94
|
+
const components = relative.split(/[\\/]/);
|
|
95
|
+
if (path.isAbsolute(relative) ||
|
|
96
|
+
components.some((part) => !part ||
|
|
97
|
+
part === "." ||
|
|
98
|
+
part === ".." ||
|
|
99
|
+
RESERVED_DIRECTORIES.has(part.toLowerCase())) ||
|
|
100
|
+
relative.includes("\0"))
|
|
101
|
+
throw new Error(`Unsafe workflow copy path: ${relative}`);
|
|
102
|
+
const target = path.resolve(root, relative);
|
|
103
|
+
if (!inside(target, root))
|
|
104
|
+
throw new Error(`Copy path escapes workspace: ${relative}`);
|
|
105
|
+
let current = root;
|
|
106
|
+
for (const component of components) {
|
|
107
|
+
current = path.join(current, component);
|
|
108
|
+
if (fs.lstatSync(current, { throwIfNoEntry: false })?.isSymbolicLink())
|
|
109
|
+
throw new Error(`Workflow copy does not support symlinks: ${current}`);
|
|
110
|
+
}
|
|
111
|
+
return target;
|
|
112
|
+
}
|
|
113
|
+
function collectCopies(repo, workspace, relative, files, stateDir) {
|
|
114
|
+
const source = containedPath(repo, relative);
|
|
115
|
+
const destination = containedPath(workspace, relative);
|
|
116
|
+
if (stateDir &&
|
|
117
|
+
[source, destination].some((file) => file === stateDir || inside(file, stateDir)))
|
|
118
|
+
throw new Error(`Workflow copy cannot include RL state: ${relative}`);
|
|
119
|
+
const stat = fs.lstatSync(source);
|
|
120
|
+
if (stat.isDirectory()) {
|
|
121
|
+
if (fs.existsSync(destination) && !fs.lstatSync(destination).isDirectory())
|
|
122
|
+
throw new Error(`Copy destination is not a directory: ${destination}`);
|
|
123
|
+
for (const child of fs.readdirSync(source).sort())
|
|
124
|
+
collectCopies(repo, workspace, path.join(relative, child), files, stateDir);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (!stat.isFile())
|
|
128
|
+
throw new Error(`Copy supports only regular files and directories: ${relative}`);
|
|
129
|
+
for (const root of [repo, workspace]) {
|
|
130
|
+
if (git(root, "--literal-pathspecs", "ls-files", "--", relative))
|
|
131
|
+
throw new Error(`Workflow copy path is tracked: ${relative}`);
|
|
132
|
+
try {
|
|
133
|
+
git(root, "check-ignore", "--no-index", "--", relative);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw new Error(`Workflow copy path must be ignored in source and destination: ${relative}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (fs.existsSync(destination)) {
|
|
140
|
+
if (!fs.lstatSync(destination).isFile())
|
|
141
|
+
throw new Error(`Copy destination is not a regular file: ${destination}`);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!files.some((file) => file.destination === destination))
|
|
145
|
+
files.push({ source, destination, relative });
|
|
146
|
+
}
|
|
147
|
+
export async function setupWorkspace(repo, workspace, options = {}) {
|
|
148
|
+
const config = readWorkflowConfig(repo);
|
|
149
|
+
const files = [];
|
|
150
|
+
for (const relative of new Set(config.copy))
|
|
151
|
+
collectCopies(repo, workspace, relative, files, options.stateDir);
|
|
152
|
+
const commands = [
|
|
153
|
+
...(options.create ? config.onCreate : []),
|
|
154
|
+
...(options.open ? config.onOpen : []),
|
|
155
|
+
];
|
|
156
|
+
for (const file of files)
|
|
157
|
+
console.error(`${options.dryRun ? "Would copy" : "Copy"} ${file.relative}`);
|
|
158
|
+
for (const command of commands)
|
|
159
|
+
console.error(`${options.dryRun ? "Would run" : "Run"} ${JSON.stringify(command)}`);
|
|
160
|
+
if (options.dryRun)
|
|
161
|
+
return 0;
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
containedPath(repo, file.relative);
|
|
164
|
+
containedPath(workspace, file.relative);
|
|
165
|
+
fs.mkdirSync(path.dirname(file.destination), { recursive: true });
|
|
166
|
+
fs.copyFileSync(file.source, file.destination, fs.constants.COPYFILE_EXCL | fs.constants.COPYFILE_FICLONE);
|
|
167
|
+
}
|
|
168
|
+
for (const command of commands) {
|
|
169
|
+
const result = await execute(command, workspace, {
|
|
170
|
+
interactive: true,
|
|
171
|
+
env: workflowEnvironment(repo, workspace, config),
|
|
172
|
+
});
|
|
173
|
+
if (result.code !== 0)
|
|
174
|
+
return result.code;
|
|
175
|
+
}
|
|
176
|
+
return 0;
|
|
177
|
+
}
|
package/dist/workflow.js
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { args, arity, canonical, execute, git, inside, json, locked, } from "./common.js";
|
|
4
|
+
import { commandContext, contextOptions, resolveWorkspace, writeWorkspaceOutput, } from "./command-context.js";
|
|
5
|
+
import { resolveBase } from "./base.js";
|
|
6
|
+
import { worktrees } from "./repos.js";
|
|
7
|
+
import { launchAgent } from "./agent-launch.js";
|
|
8
|
+
import { EMPTY_WORKFLOW, WORKFLOW_FILE, readWorkflowConfig, setupWorkspace, workflowEnvironment, } from "./workflow-config.js";
|
|
9
|
+
const CREATE_LOCK = ".workspace-create";
|
|
10
|
+
const SETUP_LOCK = ".workspace-setup";
|
|
11
|
+
const LOCK_FILE = ".lock";
|
|
12
|
+
const SUPPORTED_AGENTS = new Set(["codex", "claude"]);
|
|
13
|
+
const COMMAND_OPTIONS = {
|
|
14
|
+
path: [],
|
|
15
|
+
config: [],
|
|
16
|
+
exec: ["workspace"],
|
|
17
|
+
setup: ["workspace", "dry-run"],
|
|
18
|
+
switch: ["create", "base", "no-fetch", "setup", "agent", "output-path"],
|
|
19
|
+
new: ["base", "no-fetch", "setup", "output-path"],
|
|
20
|
+
open: ["branch", "base", "no-fetch", "setup", "output-path"],
|
|
21
|
+
};
|
|
22
|
+
async function createWorkspace(context, name, base, noFetch, options = {}) {
|
|
23
|
+
if (name.startsWith("-") ||
|
|
24
|
+
name.startsWith("@") ||
|
|
25
|
+
name
|
|
26
|
+
.split(/[\\/]/)
|
|
27
|
+
.some((part) => !part || part === "." || part === "..") ||
|
|
28
|
+
path.isAbsolute(name))
|
|
29
|
+
throw new Error(`Invalid workspace name: ${name}`);
|
|
30
|
+
git(context.repo, "check-ref-format", "--branch", name);
|
|
31
|
+
return locked(path.join(context.store.root, CREATE_LOCK), LOCK_FILE, async () => {
|
|
32
|
+
const directoryName = options.directoryName ?? name;
|
|
33
|
+
const destination = path.resolve(context.managedDir, directoryName);
|
|
34
|
+
if (!inside(destination, context.managedDir) ||
|
|
35
|
+
canonical(destination) !== destination)
|
|
36
|
+
throw new Error("Workspace path must stay inside the managed directory without symlinks");
|
|
37
|
+
const registered = worktrees(context.repo).find((item) => item.branch === `refs/heads/${name}`);
|
|
38
|
+
if (registered) {
|
|
39
|
+
if (!options.reuseExisting)
|
|
40
|
+
throw new Error(`Branch already checked out at ${registered.worktree}; use rl switch without --create`);
|
|
41
|
+
const [root, branch] = context.store.worktree(registered.worktree);
|
|
42
|
+
if (branch !== name)
|
|
43
|
+
throw new Error("Workspace branch changed during selection; retry");
|
|
44
|
+
return { path: root, name: directoryName, branch, created: false };
|
|
45
|
+
}
|
|
46
|
+
if (fs.existsSync(destination))
|
|
47
|
+
throw new Error(`Workspace path already exists: ${destination}`);
|
|
48
|
+
if (!noFetch) {
|
|
49
|
+
const remotes = git(context.repo, "remote").split("\n");
|
|
50
|
+
if (remotes.includes("origin")) {
|
|
51
|
+
const result = await execute(["git", "fetch", "origin"], context.repo, { terminalError: true });
|
|
52
|
+
if (result.code !== 0)
|
|
53
|
+
throw new Error("Fetch failed; retry or use --no-fetch");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const exists = git(context.repo, "for-each-ref", "--format=%(refname)", `refs/heads/${name}`)
|
|
57
|
+
.split("\n")
|
|
58
|
+
.includes(`refs/heads/${name}`);
|
|
59
|
+
const remoteBranch = `refs/remotes/origin/${name}`;
|
|
60
|
+
const remoteExists = options.requireBranch &&
|
|
61
|
+
git(context.repo, "for-each-ref", "--format=%(refname)", remoteBranch)
|
|
62
|
+
.split("\n")
|
|
63
|
+
.includes(remoteBranch);
|
|
64
|
+
if (options.requireBranch && !exists && !remoteExists)
|
|
65
|
+
throw new Error(`Branch does not exist locally or on origin: ${name}`);
|
|
66
|
+
const resolved = exists
|
|
67
|
+
? null
|
|
68
|
+
: resolveBase(context.repo, options.requireBranch ? `origin/${name}` : base);
|
|
69
|
+
if (!exists && !resolved)
|
|
70
|
+
throw new Error(`Base branch does not exist: ${base}`);
|
|
71
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
72
|
+
const creationArgs = exists
|
|
73
|
+
? [destination, name]
|
|
74
|
+
: resolved
|
|
75
|
+
? [
|
|
76
|
+
...(options.requireBranch ? ["--track"] : []),
|
|
77
|
+
"-b",
|
|
78
|
+
name,
|
|
79
|
+
destination,
|
|
80
|
+
options.requireBranch ? `origin/${name}` : resolved.oid,
|
|
81
|
+
]
|
|
82
|
+
: [];
|
|
83
|
+
git(context.repo, "worktree", "add", ...creationArgs);
|
|
84
|
+
return {
|
|
85
|
+
path: destination,
|
|
86
|
+
name: directoryName,
|
|
87
|
+
branch: name,
|
|
88
|
+
created: true,
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
export async function main(argv) {
|
|
93
|
+
const separator = argv.indexOf("--");
|
|
94
|
+
const forwarded = separator < 0 ? [] : argv.slice(separator + 1);
|
|
95
|
+
const { values, positionals } = args(separator < 0 ? argv : argv.slice(0, separator), [...contextOptions, "base", "branch", "workspace", "agent", "output-path"], ["create", "no-fetch", "setup", "dry-run"], { create: "c", base: "b" });
|
|
96
|
+
const context = commandContext(values);
|
|
97
|
+
const [command, ...names] = positionals;
|
|
98
|
+
const allowedOptions = COMMAND_OPTIONS[command];
|
|
99
|
+
if (!allowedOptions)
|
|
100
|
+
throw new Error("Unknown workspace workflow command");
|
|
101
|
+
for (const option of Object.keys(values))
|
|
102
|
+
if (!contextOptions.includes(option) && !allowedOptions.includes(option))
|
|
103
|
+
throw new Error(`--${option} is not supported by rl ${command}`);
|
|
104
|
+
if (command === "switch" &&
|
|
105
|
+
!values.create &&
|
|
106
|
+
(values.base || values["no-fetch"]))
|
|
107
|
+
throw new Error("--base and --no-fetch require --create with rl switch");
|
|
108
|
+
const selector = typeof values.workspace === "string" ? values.workspace : undefined;
|
|
109
|
+
if (forwarded.length &&
|
|
110
|
+
command !== "exec" &&
|
|
111
|
+
!(command === "switch" && values.agent))
|
|
112
|
+
throw new Error("Arguments after -- require rl exec or rl switch --agent");
|
|
113
|
+
if (command === "path") {
|
|
114
|
+
arity(names, 0, 1);
|
|
115
|
+
console.log(resolveWorkspace(context, names[0]).path);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
if (command === "config") {
|
|
119
|
+
arity(names, 1);
|
|
120
|
+
if (names[0] === "show")
|
|
121
|
+
json(readWorkflowConfig(context.repo));
|
|
122
|
+
else if (names[0] === "init") {
|
|
123
|
+
const file = path.join(context.repo, WORKFLOW_FILE);
|
|
124
|
+
if (fs
|
|
125
|
+
.lstatSync(path.dirname(file), { throwIfNoEntry: false })
|
|
126
|
+
?.isSymbolicLink())
|
|
127
|
+
throw new Error("Workflow config directory must not be a symlink");
|
|
128
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
129
|
+
fs.writeFileSync(file, JSON.stringify(EMPTY_WORKFLOW, null, 2) + "\n", {
|
|
130
|
+
flag: "wx",
|
|
131
|
+
});
|
|
132
|
+
console.log(file);
|
|
133
|
+
}
|
|
134
|
+
else
|
|
135
|
+
throw new Error("Usage: rl config show|init");
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
if (command === "exec") {
|
|
139
|
+
arity(names, 0);
|
|
140
|
+
if (!forwarded.length)
|
|
141
|
+
throw new Error("Usage: rl exec [--workspace name] -- <command> [args...]");
|
|
142
|
+
const target = resolveWorkspace(context, selector);
|
|
143
|
+
return (await execute(forwarded, target.path, {
|
|
144
|
+
interactive: true,
|
|
145
|
+
env: workflowEnvironment(context.repo, target.path, readWorkflowConfig(context.repo)),
|
|
146
|
+
})).code;
|
|
147
|
+
}
|
|
148
|
+
if (command === "setup") {
|
|
149
|
+
arity(names, 0);
|
|
150
|
+
const target = resolveWorkspace(context, selector);
|
|
151
|
+
return locked(path.join(context.store.root, SETUP_LOCK), LOCK_FILE, () => {
|
|
152
|
+
if (context.store.worktree(target.path)[1] !== target.branch)
|
|
153
|
+
throw new Error("Workspace branch changed before setup; retry");
|
|
154
|
+
return setupWorkspace(context.repo, target.path, {
|
|
155
|
+
dryRun: !!values["dry-run"],
|
|
156
|
+
create: true,
|
|
157
|
+
open: true,
|
|
158
|
+
stateDir: context.stateDir,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (!["switch", "new", "open"].includes(command))
|
|
163
|
+
throw new Error("Unknown workspace workflow command");
|
|
164
|
+
arity(names, command === "open" ? 0 : 1);
|
|
165
|
+
const selectedBranch = values.branch ?? (command === "open" ? values.base : undefined);
|
|
166
|
+
if (command === "open" && typeof selectedBranch !== "string")
|
|
167
|
+
throw new Error("Usage: rl open --branch <branch>");
|
|
168
|
+
const agent = typeof values.agent === "string" ? values.agent : undefined;
|
|
169
|
+
if (agent && !SUPPORTED_AGENTS.has(agent))
|
|
170
|
+
throw new Error("Agent must be codex or claude");
|
|
171
|
+
const base = command !== "open" && typeof values.base === "string"
|
|
172
|
+
? values.base
|
|
173
|
+
: context.defaultBase;
|
|
174
|
+
const creating = !!values.create || command === "new" || command === "open";
|
|
175
|
+
const branch = typeof selectedBranch === "string"
|
|
176
|
+
? selectedBranch.replace(/^origin\//, "")
|
|
177
|
+
: names[0];
|
|
178
|
+
const target = creating
|
|
179
|
+
? await createWorkspace(context, branch, base, !!values["no-fetch"], command === "switch"
|
|
180
|
+
? {}
|
|
181
|
+
: {
|
|
182
|
+
reuseExisting: true,
|
|
183
|
+
...(command === "open"
|
|
184
|
+
? {
|
|
185
|
+
directoryName: branch.replaceAll("/", "-"),
|
|
186
|
+
requireBranch: true,
|
|
187
|
+
}
|
|
188
|
+
: {}),
|
|
189
|
+
})
|
|
190
|
+
: resolveWorkspace(context, names[0]);
|
|
191
|
+
await context.store.adopt(target.path, base);
|
|
192
|
+
if (values.setup) {
|
|
193
|
+
const code = await locked(path.join(context.store.root, SETUP_LOCK), LOCK_FILE, () => {
|
|
194
|
+
if (context.store.worktree(target.path)[1] !== target.branch)
|
|
195
|
+
throw new Error("Workspace branch changed before setup; retry");
|
|
196
|
+
return setupWorkspace(context.repo, target.path, {
|
|
197
|
+
create: "created" in target && target.created === true,
|
|
198
|
+
open: true,
|
|
199
|
+
stateDir: context.stateDir,
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
if (code !== 0)
|
|
203
|
+
return code;
|
|
204
|
+
}
|
|
205
|
+
writeWorkspaceOutput(target.path, typeof values["output-path"] === "string"
|
|
206
|
+
? values["output-path"]
|
|
207
|
+
: undefined);
|
|
208
|
+
return agent
|
|
209
|
+
? launchAgent(context.store, agent, forwarded, { cwd: target.path })
|
|
210
|
+
: 0;
|
|
211
|
+
}
|