@chlrc/aiw 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -24
- package/README.zh-CN.md +70 -23
- package/package.json +2 -1
- package/skills/aiw-init/SKILL.md +109 -0
- package/skills/aiw-init/agents/openai.yaml +4 -0
- package/skills/aiw-reference/SKILL.md +197 -0
- package/skills/aiw-reference/agents/openai.yaml +4 -0
- package/src/cli.mjs +41 -16
- package/src/commit.mjs +39 -0
- package/src/deps.mjs +9 -10
- package/src/git.mjs +63 -19
- package/src/layout.mjs +1 -8
- package/src/workspace.mjs +368 -6
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: aiw-reference
|
|
3
|
+
description: Operate the AIW CLI directly for workspace automation, Git review, diff inspection, AI commits, cmux layout launching, Worktrunk worktree lifecycle, workspace garbage collection, and finishing feature branches. Use when the user asks an agent to run AIW commands, clean workspaces, merge back to a target branch, open or create AIW workspaces, automate AIW flows, or explain AIW command behavior.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# AIW Reference
|
|
7
|
+
|
|
8
|
+
## Purpose
|
|
9
|
+
|
|
10
|
+
Use AIW as the orchestration layer around existing terminal tools. AIW decides the workflow path, runs dependency gates, and delegates to Worktrunk, cmux, lazygit, delta, yazi, nvim, Git, and agent CLIs.
|
|
11
|
+
|
|
12
|
+
## Operating Rules
|
|
13
|
+
|
|
14
|
+
- Run real state checks before making lifecycle changes. Prefer `aiw doctor`, `git status --short`, and `aiw workspace list` as the first evidence.
|
|
15
|
+
- Treat these as high-impact operations: `aiw workspace done`, `aiw workspace remove`, `aiw workspace gc --apply`, `aiw workspace gc --yes`, and any `--force` use.
|
|
16
|
+
- Use dry-run or preview modes when available before applying changes. `gc --dry-run` is the default safe way to inspect cleanup.
|
|
17
|
+
- Do not run `aiw done` from the main checkout. It is only valid inside a feature worktree and refuses dirty worktrees.
|
|
18
|
+
- Do not silently stage files for `aiw commit`. AIW commit reads staged changes only; the user or agent must intentionally stage changes first.
|
|
19
|
+
- Keep personal AIW workflow files out of business repositories unless the user explicitly asks for project-local config.
|
|
20
|
+
|
|
21
|
+
## Resolve the AIW Command
|
|
22
|
+
|
|
23
|
+
Use the installed `aiw` binary for normal workspace operations:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
aiw --help
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use `npx @chlrc/aiw ...` for first-run or one-off package-based access:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npx @chlrc/aiw doctor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use a local checkout only when developing or deeply customizing AIW itself:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node bin/aiw --help
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Read-Only Orientation
|
|
42
|
+
|
|
43
|
+
Start with the smallest useful checks:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
aiw doctor
|
|
47
|
+
aiw doctor --gate workspace
|
|
48
|
+
git status --short
|
|
49
|
+
aiw workspace list
|
|
50
|
+
aiw ls
|
|
51
|
+
aiw als
|
|
52
|
+
aiw workspace list --json
|
|
53
|
+
aiw workspace states
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Use `--json` when automating decisions. The workspace table combines Worktrunk, Git, cmux, target-branch, merge-state, age, dirty, and GC signals.
|
|
57
|
+
|
|
58
|
+
## Open or Create Workspaces
|
|
59
|
+
|
|
60
|
+
Create or switch to a worktree and open the standard three-pane cmux layout:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
aiw cmux-new --agent codex
|
|
64
|
+
aiw new --agent codex
|
|
65
|
+
aiw cmux-new --pick-repo --agent codex
|
|
66
|
+
aiw cmux-new --repo ~/Code/my-repo --branch feat/foo --agent codex --dry-run
|
|
67
|
+
aiw cmux-new --repo ~/Code/my-repo --branch feat/foo --base main --agent codex --dry-run
|
|
68
|
+
aiw cmux-new --local --agent codex
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Behavior:
|
|
72
|
+
|
|
73
|
+
- New branch from current `HEAD`: `wt switch --create <branch> --base @ -x "aiw layout --agent <agent>"`.
|
|
74
|
+
- New branch from a base branch: pass `--base <branch>` or `--from <branch>`.
|
|
75
|
+
- Existing branch: `wt switch <branch> -x "aiw layout --agent <agent>"`.
|
|
76
|
+
- Current checkout without Worktrunk: `--local`.
|
|
77
|
+
|
|
78
|
+
Open an existing workspace or branch:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
aiw workspace open feat/foo --agent codex --dry-run
|
|
82
|
+
aiw workspace open /path/to/worktree --agent codex --dry-run
|
|
83
|
+
aiw open feat/foo --agent codex
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Review, Diff, and Commit
|
|
87
|
+
|
|
88
|
+
Use AIW surfaces without replacing native tool responsibilities:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
aiw git
|
|
92
|
+
aiw diff
|
|
93
|
+
aiw diff --watch
|
|
94
|
+
aiw diff --staged
|
|
95
|
+
aiw files
|
|
96
|
+
aiw edit src/file.ts:10
|
|
97
|
+
aiw grep keyword
|
|
98
|
+
aiw tree 3
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Commit flow:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
git status --short
|
|
105
|
+
git add <path>
|
|
106
|
+
aiw commit --agent codex --dry-run
|
|
107
|
+
aiw commit --agent codex
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`aiw git` opens lazygit with the AIW overlay. In that lazygit session, `Ctrl-A` triggers `aiw commit --prompt ...` for staged changes while leaving native lazygit commit behavior available.
|
|
111
|
+
|
|
112
|
+
## Finish a Feature Worktree
|
|
113
|
+
|
|
114
|
+
Use this flow when the user asks to merge a feature workspace back to a target branch:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
git status --short
|
|
118
|
+
aiw workspace list
|
|
119
|
+
aiw workspace done dev --no-close-cmux
|
|
120
|
+
aiw workspace done dev --agent codex --no-close-cmux
|
|
121
|
+
aiw workspace done dev --retries 3 --no-close-cmux
|
|
122
|
+
aiw workspace done dev --agent codex --retries 3 --no-close-cmux
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Rules:
|
|
126
|
+
|
|
127
|
+
- Run from the feature worktree, not the main workspace.
|
|
128
|
+
- Continue only when `git status --short` is empty.
|
|
129
|
+
- `done` defaults retries from `commit.retries` and restores the source worktree, target branch, and Worktrunk backup ref after failed merge attempts.
|
|
130
|
+
- Pass an explicit target such as `dev`, `main`, or `master` when the recorded AIW target is unclear.
|
|
131
|
+
- Use `--agent <name>` when the Worktrunk squash commit message should be generated by a specific AIW agent.
|
|
132
|
+
- If a Worktrunk squash commit fails commitlint with a fallback subject like `Squash commits from ...`, inspect `git status --short`; if the index now contains the squashed changes, create a valid Conventional Commit manually or restore from `refs/wt-backup/<branch>` before retrying.
|
|
133
|
+
- Omit `--no-close-cmux` when the user wants the matching cmux workspace closed after a successful Worktrunk merge.
|
|
134
|
+
|
|
135
|
+
Short alias:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
aiw done dev
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Garbage-Collect Workspaces
|
|
142
|
+
|
|
143
|
+
Preview first:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
aiw workspace gc --dry-run
|
|
147
|
+
aiw workspace gc --json
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
AIW GC separates signals:
|
|
151
|
+
|
|
152
|
+
- `dirty`: blocks automatic cleanup.
|
|
153
|
+
- `merged`: branch is integrated, same commit, empty, or known contained by a target.
|
|
154
|
+
- `stale`: last commit age exceeds `workspace.stale_seconds`; stale alone does not make a workspace removable.
|
|
155
|
+
|
|
156
|
+
Only clean, merged, non-current workspaces are removable. Stale dirty or unmerged workspaces are warnings only.
|
|
157
|
+
|
|
158
|
+
Apply only when the user wants cleanup:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
aiw workspace gc --apply
|
|
162
|
+
aiw workspace gc --yes
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Short aliases:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
aiw gc --dry-run
|
|
169
|
+
aiw clean --dry-run
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Remove a Specific Workspace
|
|
173
|
+
|
|
174
|
+
Use removal for a named target after checking state:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
aiw workspace list
|
|
178
|
+
aiw workspace remove feat/foo
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`remove` performs a dirty check before calling Worktrunk. Use `--force` only when the user explicitly intends to hand the decision to Worktrunk:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
aiw workspace remove feat/foo --force
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
## Automation Pattern
|
|
188
|
+
|
|
189
|
+
For agent-run automation, report the exact evidence and command sequence:
|
|
190
|
+
|
|
191
|
+
1. Show the current repo/worktree state.
|
|
192
|
+
2. Show the AIW preview or dry-run output when available.
|
|
193
|
+
3. State which operation is about to mutate worktrees, commits, or cmux.
|
|
194
|
+
4. Run the apply command only when the user has authorized that operation.
|
|
195
|
+
5. Verify with `aiw workspace list`, `git status --short`, or the relevant `doctor` gate.
|
|
196
|
+
|
|
197
|
+
Do not stop at a single local command if the user's request is an end-to-end AIW workflow. Carry the task through state inspection, action, verification, and a concise result.
|
package/src/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { expandHome, loadConfig, resolveAgent, aiwBinPath } from "./config.mjs";
|
|
5
|
-
import { runCommit } from "./commit.mjs";
|
|
5
|
+
import { runCommit, runCommitMessage } from "./commit.mjs";
|
|
6
6
|
import { assertGate, printDoctor } from "./deps.mjs";
|
|
7
7
|
import { assertGitRoot, gitRoot, isDirty, resolveRepo, selectBranch } from "./git.mjs";
|
|
8
8
|
import { runWorkspaceHook } from "./hooks.mjs";
|
|
@@ -12,7 +12,7 @@ import { commandExists, quoteShell, runInherit, sleep } from "./run.mjs";
|
|
|
12
12
|
import { commandWorkspace, recordWorkspaceTarget } from "./workspace.mjs";
|
|
13
13
|
|
|
14
14
|
export async function main(argv) {
|
|
15
|
-
const command = argv[0] || "help";
|
|
15
|
+
const command = normalizeCommand(argv[0] || "help");
|
|
16
16
|
const rest = argv.slice(1);
|
|
17
17
|
const config = loadConfig();
|
|
18
18
|
|
|
@@ -32,6 +32,12 @@ export async function main(argv) {
|
|
|
32
32
|
case "new":
|
|
33
33
|
await commandCmuxNew(config, rest, command);
|
|
34
34
|
return;
|
|
35
|
+
case "cmux":
|
|
36
|
+
if (normalizeCommand(rest[0] || "") === "new") {
|
|
37
|
+
await commandCmuxNew(config, rest.slice(1), "cmux-new");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
break;
|
|
35
41
|
case "layout":
|
|
36
42
|
await commandLayout(config, rest);
|
|
37
43
|
return;
|
|
@@ -46,6 +52,8 @@ export async function main(argv) {
|
|
|
46
52
|
await commandWorkspace(config, ["open", ...rest]);
|
|
47
53
|
return;
|
|
48
54
|
case "list":
|
|
55
|
+
case "ls":
|
|
56
|
+
case "als":
|
|
49
57
|
await commandWorkspace(config, ["list", ...rest]);
|
|
50
58
|
return;
|
|
51
59
|
case "done":
|
|
@@ -64,6 +72,9 @@ export async function main(argv) {
|
|
|
64
72
|
case "commit":
|
|
65
73
|
await commandCommit(config, rest);
|
|
66
74
|
return;
|
|
75
|
+
case "commit-message":
|
|
76
|
+
await commandCommitMessage(config, rest);
|
|
77
|
+
return;
|
|
67
78
|
case "git":
|
|
68
79
|
await commandGit(config, rest);
|
|
69
80
|
return;
|
|
@@ -83,12 +94,11 @@ export async function main(argv) {
|
|
|
83
94
|
case "tree":
|
|
84
95
|
await commandTree(config, rest);
|
|
85
96
|
return;
|
|
86
|
-
default: {
|
|
87
|
-
const error = new Error(`unknown command: ${command}`);
|
|
88
|
-
error.exitCode = 2;
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
97
|
}
|
|
98
|
+
|
|
99
|
+
const error = new Error(`unknown command: ${command}`);
|
|
100
|
+
error.exitCode = 2;
|
|
101
|
+
throw error;
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
async function commandDoctor(config, argv) {
|
|
@@ -114,11 +124,9 @@ async function commandCmuxNew(config, argv, commandName) {
|
|
|
114
124
|
const branchFromArgs = flags.positionals[0] && !isKnownAgent(config, flags.positionals[0])
|
|
115
125
|
? flags.positionals[0]
|
|
116
126
|
: "";
|
|
117
|
-
const agentFromArgs =
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (flags.local && (flags.branch || branchFromArgs || flags.create)) {
|
|
121
|
-
const error = new Error("--local cannot be combined with --branch, a branch argument, or --create");
|
|
127
|
+
const agentFromArgs = flags.positionals.find((item) => isKnownAgent(config, item));
|
|
128
|
+
if (flags.local && (flags.branch || branchFromArgs || flags.create || flags.base)) {
|
|
129
|
+
const error = new Error("--local cannot be combined with --branch, a branch argument, --create, or --base");
|
|
122
130
|
error.exitCode = 2;
|
|
123
131
|
throw error;
|
|
124
132
|
}
|
|
@@ -129,7 +137,8 @@ async function commandCmuxNew(config, argv, commandName) {
|
|
|
129
137
|
const branchSelection = flags.local
|
|
130
138
|
? { local: true }
|
|
131
139
|
: await selectBranch(repo, flags.branch || branchFromArgs, {
|
|
132
|
-
forceCreate: flags.create
|
|
140
|
+
forceCreate: flags.create,
|
|
141
|
+
baseBranch: flags.base
|
|
133
142
|
});
|
|
134
143
|
const agent = await selectAgent(config, flags.agent || agentFromArgs);
|
|
135
144
|
|
|
@@ -152,7 +161,7 @@ async function commandCmuxNew(config, argv, commandName) {
|
|
|
152
161
|
|
|
153
162
|
const layoutCommand = `${quoteShell(aiwBinPath())} layout --agent ${quoteShell(agent.name)}`;
|
|
154
163
|
const wtArgs = branchSelection.create
|
|
155
|
-
? ["switch", "--create", branchSelection.branch, "--base", "@", "-x", layoutCommand]
|
|
164
|
+
? ["switch", "--create", branchSelection.branch, "--base", branchSelection.baseBranch || "@", "-x", layoutCommand]
|
|
156
165
|
: ["switch", branchSelection.branch, "-x", layoutCommand];
|
|
157
166
|
if (flags.dryRun) {
|
|
158
167
|
console.log(`cd ${quoteShell(repo)} && wt ${wtArgs.map(quoteShell).join(" ")}`);
|
|
@@ -240,6 +249,13 @@ async function commandCommit(config, argv) {
|
|
|
240
249
|
await runCommit(config, flags);
|
|
241
250
|
}
|
|
242
251
|
|
|
252
|
+
async function commandCommitMessage(config, argv) {
|
|
253
|
+
const flags = parseFlags(argv);
|
|
254
|
+
const agent = resolveAgent(config, flags.agent || config.commit.agent || config.defaults.agent);
|
|
255
|
+
assertGate("commit", config, agent);
|
|
256
|
+
runCommitMessage(config, flags);
|
|
257
|
+
}
|
|
258
|
+
|
|
243
259
|
function resolveConfigFile(config, value) {
|
|
244
260
|
if (!value) {
|
|
245
261
|
return "";
|
|
@@ -325,6 +341,10 @@ function parseFlags(argv) {
|
|
|
325
341
|
case "--branch":
|
|
326
342
|
flags.branch = argv[++index];
|
|
327
343
|
break;
|
|
344
|
+
case "--base":
|
|
345
|
+
case "--from":
|
|
346
|
+
flags.base = argv[++index];
|
|
347
|
+
break;
|
|
328
348
|
case "--repo":
|
|
329
349
|
flags.repo = argv[++index];
|
|
330
350
|
break;
|
|
@@ -410,13 +430,18 @@ function printHelp() {
|
|
|
410
430
|
Commands:
|
|
411
431
|
init [--cmux-scope <home|code|none>] [--code-root <path>] [--config-dir <path>] [--dry-run]
|
|
412
432
|
doctor [--json] [--gate <p0|init|layout|cmux-new|workspace|worktrunk|diff|commit>] [--agent <name>]
|
|
413
|
-
cmux-new [--branch <branch>] [--agent <name>] [--repo <path>] [--pick-repo] [--create] [--local] [--dry-run]
|
|
433
|
+
cmux-new|new [--branch <branch>] [--base <branch>] [--agent <name>] [--repo <path>] [--pick-repo] [--create] [--local] [--dry-run]
|
|
414
434
|
layout [--agent <name>] [--print-json] [--dry-run]
|
|
415
435
|
workspace|ws <list|open|done|remove|gc> [options]
|
|
416
436
|
commit [--agent <name>] [--prompt <text>] [--prompt-file <path>] [--retries <n>] [--dry-run] [--print-prompt]
|
|
417
|
-
|
|
437
|
+
commit-message [--agent <name>] [--prompt <text>]
|
|
438
|
+
open | switch | list | ls | als | done | remove | gc | clean
|
|
418
439
|
diff [--watch] [--staged] [--all]
|
|
419
440
|
git | files [path] | edit <file[:line]> | grep <query> | pick | tree [depth]
|
|
420
441
|
|
|
421
442
|
Main workflow commands run dependency gates before creating worktrees or opening cmux layouts.`);
|
|
422
443
|
}
|
|
444
|
+
|
|
445
|
+
function normalizeCommand(command) {
|
|
446
|
+
return String(command || "").toLowerCase();
|
|
447
|
+
}
|
package/src/commit.mjs
CHANGED
|
@@ -83,6 +83,37 @@ export async function runCommit(config, flags) {
|
|
|
83
83
|
throw withExit(`git commit failed after ${retries} attempts`, 8);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
export function runCommitMessage(config, flags) {
|
|
87
|
+
const prompt = flags.prompt || readStdin();
|
|
88
|
+
if (!prompt.trim()) {
|
|
89
|
+
throw withExit("commit-message prompt is required on stdin or via --prompt", 2);
|
|
90
|
+
}
|
|
91
|
+
const repo = capture("git", ["rev-parse", "--show-toplevel"], { cwd: process.cwd() });
|
|
92
|
+
const message = generateCommitMessage(config, flags, prompt, repo);
|
|
93
|
+
console.log(message);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function generateCommitMessage(config, flags, prompt, repo) {
|
|
97
|
+
const agent = resolveAgent(config, flags.agent || config.commit.agent || config.defaults.agent);
|
|
98
|
+
const agentResult = runAgentForText(agent, prompt, { cwd: repo });
|
|
99
|
+
if (!agentResult.ok) {
|
|
100
|
+
throw withExit(
|
|
101
|
+
[
|
|
102
|
+
`agent '${agent.name}' failed while generating commit message`,
|
|
103
|
+
agentResult.stderr.trim(),
|
|
104
|
+
agentResult.stdout.trim()
|
|
105
|
+
].filter(Boolean).join("\n"),
|
|
106
|
+
agentResult.status || 1
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const message = cleanAgentText(agentResult.stdout);
|
|
111
|
+
if (!message) {
|
|
112
|
+
throw withExit(`agent '${agent.name}' returned an empty commit message`, 7);
|
|
113
|
+
}
|
|
114
|
+
return message;
|
|
115
|
+
}
|
|
116
|
+
|
|
86
117
|
function readStagedDiff(config, repo) {
|
|
87
118
|
const diff = capture("git", ["diff", "--cached", "--no-ext-diff"], { cwd: repo });
|
|
88
119
|
const statResult = tryCapture("git", ["diff", "--cached", "--stat"], { cwd: repo });
|
|
@@ -97,6 +128,14 @@ function readStagedDiff(config, repo) {
|
|
|
97
128
|
};
|
|
98
129
|
}
|
|
99
130
|
|
|
131
|
+
function readStdin() {
|
|
132
|
+
try {
|
|
133
|
+
return fs.readFileSync(0, "utf8");
|
|
134
|
+
} catch {
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
100
139
|
function buildCommitPrompt({ basePrompt, staged, attempt, retries, lastFailure }) {
|
|
101
140
|
const sections = [
|
|
102
141
|
basePrompt,
|
package/src/deps.mjs
CHANGED
|
@@ -126,29 +126,24 @@ export function printDoctor(config, options = {}) {
|
|
|
126
126
|
|
|
127
127
|
function requirementsFor(profile, config, agent) {
|
|
128
128
|
const agentCmd = agent?.cmd;
|
|
129
|
+
const gitDeps = [config.defaults.git || "lazygit", ...lazygitOverlayDeps(config)];
|
|
129
130
|
switch (profile) {
|
|
130
131
|
case "base":
|
|
131
132
|
return req(["git"]);
|
|
132
133
|
case "init":
|
|
133
|
-
return req(["sh", "git", "cmux", "wt", "yazi", "nvim",
|
|
134
|
-
["cmux-git-diff", "delta"]
|
|
135
|
-
]);
|
|
134
|
+
return req(["sh", "git", "cmux", "wt", "yazi", "nvim", ...gitDeps, "rg", "fzf", "bat", agentCmd].filter(Boolean));
|
|
136
135
|
case "layout":
|
|
137
|
-
return req(["git", "cmux", "yazi", "
|
|
138
|
-
["cmux-git-diff", "delta"]
|
|
139
|
-
]);
|
|
136
|
+
return req(["git", "cmux", "yazi", "nvim", ...gitDeps, agentCmd].filter(Boolean));
|
|
140
137
|
case "cmux-new":
|
|
141
138
|
case "new":
|
|
142
|
-
return req(["git", "wt", "cmux", "yazi", "
|
|
143
|
-
["cmux-git-diff", "delta"]
|
|
144
|
-
]);
|
|
139
|
+
return req(["git", "wt", "cmux", "yazi", "nvim", ...gitDeps, agentCmd].filter(Boolean));
|
|
145
140
|
case "worktrunk":
|
|
146
141
|
case "workspace":
|
|
147
142
|
return req(["git", "wt"]);
|
|
148
143
|
case "files":
|
|
149
144
|
return req([config.defaults.files || "yazi"]);
|
|
150
145
|
case "git":
|
|
151
|
-
return req(
|
|
146
|
+
return req(gitDeps);
|
|
152
147
|
case "edit":
|
|
153
148
|
return req([config.defaults.editor || "nvim"]);
|
|
154
149
|
case "grep":
|
|
@@ -170,3 +165,7 @@ function requirementsFor(profile, config, agent) {
|
|
|
170
165
|
function req(commands, anyOf = []) {
|
|
171
166
|
return { commands, anyOf };
|
|
172
167
|
}
|
|
168
|
+
|
|
169
|
+
function lazygitOverlayDeps(config) {
|
|
170
|
+
return config.git.lazygit_config ? ["delta"] : [];
|
|
171
|
+
}
|
package/src/git.mjs
CHANGED
|
@@ -86,19 +86,7 @@ export function currentBranch(repo) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
export function branchExists(repo, branch) {
|
|
89
|
-
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
const local = tryCapture("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { cwd: repo });
|
|
93
|
-
if (local.ok) {
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
const remote = tryCapture("git", ["show-ref", "--verify", "--quiet", `refs/remotes/${branch}`], { cwd: repo });
|
|
97
|
-
if (remote.ok) {
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
const originRemote = tryCapture("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`], { cwd: repo });
|
|
101
|
-
return originRemote.ok;
|
|
89
|
+
return Boolean(resolveBranchRef(repo, branch));
|
|
102
90
|
}
|
|
103
91
|
|
|
104
92
|
export function listBranches(repo) {
|
|
@@ -115,19 +103,34 @@ export function listBranches(repo) {
|
|
|
115
103
|
|
|
116
104
|
export async function selectBranch(repo, requestedBranch, options = {}) {
|
|
117
105
|
const current = currentBranch(repo);
|
|
106
|
+
const requestedBase = options.baseBranch || "";
|
|
107
|
+
const baseBranch = requestedBase ? resolveBranchRef(repo, requestedBase) : "";
|
|
108
|
+
if (requestedBase && !baseBranch) {
|
|
109
|
+
throw withExit(`base branch not found: ${requestedBase}`, 4);
|
|
110
|
+
}
|
|
118
111
|
if (requestedBranch) {
|
|
119
|
-
const
|
|
112
|
+
const branchAlreadyExists = branchExists(repo, requestedBranch);
|
|
113
|
+
const create = options.forceCreate || !branchAlreadyExists;
|
|
114
|
+
if (requestedBase && !create) {
|
|
115
|
+
throw withExit(`--base can only be used when creating a new branch: ${requestedBranch} already exists`, 2);
|
|
116
|
+
}
|
|
120
117
|
return {
|
|
121
118
|
branch: requestedBranch,
|
|
122
119
|
create,
|
|
123
|
-
|
|
120
|
+
baseBranch: create ? baseBranch || "@" : "",
|
|
121
|
+
targetBranch: create ? requestedBase || current : ""
|
|
124
122
|
};
|
|
125
123
|
}
|
|
126
124
|
|
|
127
125
|
const branches = listBranches(repo).filter((branch) => branch !== current);
|
|
126
|
+
if (requestedBase) {
|
|
127
|
+
return createBranchSelection(repo, baseBranch, requestedBase);
|
|
128
|
+
}
|
|
129
|
+
|
|
128
130
|
const createChoice = "Create new branch from current HEAD...";
|
|
131
|
+
const createFromBranchChoice = "Create new branch from existing branch...";
|
|
129
132
|
const localChoice = "Open current checkout...";
|
|
130
|
-
const selected = await pickFromList("Select worktree", [createChoice, localChoice, ...branches], {
|
|
133
|
+
const selected = await pickFromList("Select worktree", [createChoice, createFromBranchChoice, localChoice, ...branches], {
|
|
131
134
|
defaultItem: createChoice
|
|
132
135
|
});
|
|
133
136
|
if (selected === localChoice) {
|
|
@@ -136,21 +139,46 @@ export async function selectBranch(repo, requestedBranch, options = {}) {
|
|
|
136
139
|
};
|
|
137
140
|
}
|
|
138
141
|
if (selected !== createChoice) {
|
|
142
|
+
if (selected === createFromBranchChoice) {
|
|
143
|
+
const base = await pickFromList("Base branch", listBranches(repo), {
|
|
144
|
+
defaultItem: current
|
|
145
|
+
});
|
|
146
|
+
return createBranchSelection(repo, resolveBranchRef(repo, base), base);
|
|
147
|
+
}
|
|
139
148
|
return {
|
|
140
149
|
branch: selected,
|
|
141
150
|
create: false
|
|
142
151
|
};
|
|
143
152
|
}
|
|
144
153
|
|
|
154
|
+
return {
|
|
155
|
+
...(await promptNewBranch()),
|
|
156
|
+
create: true,
|
|
157
|
+
baseBranch: "@",
|
|
158
|
+
targetBranch: current
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function createBranchSelection(repo, baseBranch, targetBranch) {
|
|
163
|
+
if (!baseBranch) {
|
|
164
|
+
throw withExit(`base branch not found: ${targetBranch}`, 4);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
...(await promptNewBranch()),
|
|
168
|
+
create: true,
|
|
169
|
+
baseBranch,
|
|
170
|
+
targetBranch
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function promptNewBranch() {
|
|
145
175
|
const { askInput } = await import("./prompt.mjs");
|
|
146
176
|
const branch = await askInput("New branch");
|
|
147
177
|
if (!branch) {
|
|
148
178
|
throw withExit("branch is required", 4);
|
|
149
179
|
}
|
|
150
180
|
return {
|
|
151
|
-
branch
|
|
152
|
-
create: true,
|
|
153
|
-
targetBranch: current
|
|
181
|
+
branch
|
|
154
182
|
};
|
|
155
183
|
}
|
|
156
184
|
|
|
@@ -198,6 +226,22 @@ function listRefs(repo, refPath) {
|
|
|
198
226
|
}
|
|
199
227
|
}
|
|
200
228
|
|
|
229
|
+
function resolveBranchRef(repo, branch) {
|
|
230
|
+
if (!branch) {
|
|
231
|
+
return "";
|
|
232
|
+
}
|
|
233
|
+
const local = tryCapture("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { cwd: repo });
|
|
234
|
+
if (local.ok) {
|
|
235
|
+
return branch;
|
|
236
|
+
}
|
|
237
|
+
const remote = tryCapture("git", ["show-ref", "--verify", "--quiet", `refs/remotes/${branch}`], { cwd: repo });
|
|
238
|
+
if (remote.ok) {
|
|
239
|
+
return branch;
|
|
240
|
+
}
|
|
241
|
+
const originRemote = tryCapture("git", ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${branch}`], { cwd: repo });
|
|
242
|
+
return originRemote.ok ? `origin/${branch}` : "";
|
|
243
|
+
}
|
|
244
|
+
|
|
201
245
|
function isInside(child, parent) {
|
|
202
246
|
const relative = path.relative(parent, child);
|
|
203
247
|
return relative === "" || (!!relative && !relative.startsWith("..") && !path.isAbsolute(relative));
|
package/src/layout.mjs
CHANGED
|
@@ -18,14 +18,7 @@ export function buildLayout(config, agentName) {
|
|
|
18
18
|
terminalPane(agentTitle(agent.name), agentCommand)
|
|
19
19
|
]
|
|
20
20
|
},
|
|
21
|
-
{
|
|
22
|
-
direction: "horizontal",
|
|
23
|
-
split: 0.5,
|
|
24
|
-
children: [
|
|
25
|
-
terminalPane("Git", `${aiw} git`),
|
|
26
|
-
terminalPane("Diff", `${aiw} diff --watch`)
|
|
27
|
-
]
|
|
28
|
-
}
|
|
21
|
+
terminalPane("Git", `${aiw} git`)
|
|
29
22
|
]
|
|
30
23
|
};
|
|
31
24
|
}
|