@chlrc/aiw 0.1.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.
@@ -0,0 +1,24 @@
1
+ [agents.codex]
2
+ cmd = "codex"
3
+ args = []
4
+ commit_args = ["exec", "--skip-git-repo-check", "--sandbox", "read-only", "--color", "never", "-"]
5
+
6
+ [agents.claude]
7
+ cmd = "claude"
8
+ args = []
9
+ commit_args = ["--print"]
10
+
11
+ [agents.opencode]
12
+ cmd = "opencode"
13
+ args = []
14
+ commit_args = ["run", "{{prompt}}"]
15
+
16
+ [agents.gemini]
17
+ cmd = "gemini"
18
+ args = []
19
+ commit_args = ["--prompt", "{{prompt}}"]
20
+
21
+ [agents.aider]
22
+ cmd = "aider"
23
+ args = []
24
+ commit_args = ["--message", "{{prompt}}"]
@@ -0,0 +1,41 @@
1
+ [defaults]
2
+ agent = "codex"
3
+ editor = "nvim"
4
+ files = "yazi"
5
+ git = "lazygit"
6
+ diff = "cmux-git-diff"
7
+ diff_fallback = "delta"
8
+ tree_depth = 3
9
+
10
+ [paths]
11
+ code_root = "~/Code"
12
+ worktrees = "~/worktrees"
13
+ core_config = "~/.config/aiw"
14
+
15
+ [behavior]
16
+ require_git_repo = true
17
+ warn_dirty_before_new = true
18
+ open_cmux_after_new = true
19
+ use_worktrunk = true
20
+ remove_worktree_after_merge = true
21
+
22
+ [commit]
23
+ agent = "codex"
24
+ prompt_file = "commit-prompt.md"
25
+ retries = 3
26
+ max_diff_chars = 120000
27
+
28
+ [git]
29
+ lazygit_config = "lazygit-delta.yml"
30
+
31
+ [workspace]
32
+ stale_seconds = 604800
33
+
34
+ [workspace.hooks]
35
+ pre_init = []
36
+ pre_remove = []
37
+
38
+ # Optional global per-project hooks:
39
+ # [workspace.hooks.projects.aiw]
40
+ # pre_init = []
41
+ # pre_remove = []
@@ -0,0 +1,8 @@
1
+ Generate a Git commit message for the staged diff.
2
+
3
+ Rules:
4
+ - Output only the commit message. Do not add Markdown fences or explanations.
5
+ - Prefer Conventional Commits when the change clearly fits one type.
6
+ - Keep the first line concise and specific.
7
+ - Use a body only when it clarifies non-obvious context or hook failures.
8
+ - Do not mention generated tooling unless it is part of the change.
@@ -0,0 +1,15 @@
1
+ git:
2
+ pagers:
3
+ - pager: delta --dark --paging=never --line-numbers
4
+ colorArg: always
5
+ customCommands:
6
+ - key: '<c-a>'
7
+ context: 'global'
8
+ description: 'AI commit staged changes'
9
+ prompts:
10
+ - type: 'input'
11
+ title: 'Additional AI commit prompt (optional)'
12
+ key: 'AIWCommitPrompt'
13
+ initialValue: ''
14
+ command: 'aiw commit --prompt {{.Form.AIWCommitPrompt | quote}}'
15
+ output: terminal
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@chlrc/aiw",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Personal AI workflow bridge for cmux, Worktrunk, and CLI agents.",
6
+ "keywords": [
7
+ "ai",
8
+ "cli",
9
+ "cmux",
10
+ "workflow",
11
+ "worktree"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/KiritoKing/aiw-cli.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/KiritoKing/aiw-cli/issues"
19
+ },
20
+ "homepage": "https://github.com/KiritoKing/aiw-cli#readme",
21
+ "bin": {
22
+ "aiw": "bin/aiw"
23
+ },
24
+ "files": [
25
+ "bin",
26
+ "src",
27
+ "config",
28
+ "scripts",
29
+ "README.zh-CN.md"
30
+ ],
31
+ "publishConfig": {
32
+ "access": "public",
33
+ "registry": "https://registry.npmjs.org/"
34
+ },
35
+ "scripts": {
36
+ "check": "node --check src/agent.mjs && node --check src/cli.mjs && node --check src/commit.mjs && node --check src/config.mjs && node --check src/deps.mjs && node --check src/git.mjs && node --check src/hooks.mjs && node --check src/init.mjs && node --check src/layout.mjs && node --check src/prompt.mjs && node --check src/run.mjs && node --check src/workspace.mjs",
37
+ "install:global": "scripts/install-global.sh"
38
+ },
39
+ "engines": {
40
+ "node": ">=18"
41
+ }
42
+ }
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+ target="${AIW_INSTALL_DIR:-$HOME/.local/bin}"
6
+
7
+ mkdir -p "$target"
8
+ chmod +x "$root/bin/aiw"
9
+ ln -sf "$root/bin/aiw" "$target/aiw"
10
+
11
+ echo "Installed aiw -> $target/aiw"
12
+ if command -v aiw >/dev/null 2>&1; then
13
+ echo "Resolved aiw: $(command -v aiw)"
14
+ else
15
+ echo "aiw is installed, but $target is not on PATH for this shell."
16
+ fi
package/src/agent.mjs ADDED
@@ -0,0 +1,53 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ export function runAgentForText(agent, prompt, options = {}) {
4
+ const commitArgs = agent.commitArgs && agent.commitArgs.length > 0 ? agent.commitArgs : agent.args;
5
+ const expandedArgs = [];
6
+ let promptInArgs = false;
7
+
8
+ for (const arg of commitArgs) {
9
+ if (arg.includes("{{prompt}}")) {
10
+ expandedArgs.push(arg.replaceAll("{{prompt}}", prompt));
11
+ promptInArgs = true;
12
+ } else {
13
+ expandedArgs.push(arg);
14
+ }
15
+ }
16
+
17
+ const result = spawnSync(agent.cmd, expandedArgs, {
18
+ cwd: options.cwd,
19
+ env: process.env,
20
+ encoding: "utf8",
21
+ input: promptInArgs ? undefined : prompt,
22
+ maxBuffer: 1024 * 1024 * 16,
23
+ stdio: ["pipe", "pipe", "pipe"]
24
+ });
25
+
26
+ return {
27
+ ok: result.status === 0,
28
+ status: result.status || 0,
29
+ stdout: result.stdout || "",
30
+ stderr: result.stderr || "",
31
+ command: [agent.cmd, ...expandedArgs]
32
+ };
33
+ }
34
+
35
+ export function cleanAgentText(text) {
36
+ let cleaned = stripAnsi(text).trim();
37
+ const fenced = cleaned.match(/```(?:[a-zA-Z0-9_-]+)?\s*([\s\S]*?)```/);
38
+ if (fenced) {
39
+ cleaned = fenced[1].trim();
40
+ }
41
+ cleaned = cleaned.replace(/^commit message:\s*/i, "").trim();
42
+ if (
43
+ (cleaned.startsWith('"') && cleaned.endsWith('"')) ||
44
+ (cleaned.startsWith("'") && cleaned.endsWith("'"))
45
+ ) {
46
+ cleaned = cleaned.slice(1, -1).trim();
47
+ }
48
+ return cleaned;
49
+ }
50
+
51
+ function stripAnsi(text) {
52
+ return text.replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, "");
53
+ }
package/src/cli.mjs ADDED
@@ -0,0 +1,422 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { expandHome, loadConfig, resolveAgent, aiwBinPath } from "./config.mjs";
5
+ import { runCommit } from "./commit.mjs";
6
+ import { assertGate, printDoctor } from "./deps.mjs";
7
+ import { assertGitRoot, gitRoot, isDirty, resolveRepo, selectBranch } from "./git.mjs";
8
+ import { runWorkspaceHook } from "./hooks.mjs";
9
+ import { commandInit } from "./init.mjs";
10
+ import { buildLayout, workspaceName } from "./layout.mjs";
11
+ import { commandExists, quoteShell, runInherit, sleep } from "./run.mjs";
12
+ import { commandWorkspace, recordWorkspaceTarget } from "./workspace.mjs";
13
+
14
+ export async function main(argv) {
15
+ const command = argv[0] || "help";
16
+ const rest = argv.slice(1);
17
+ const config = loadConfig();
18
+
19
+ switch (command) {
20
+ case "help":
21
+ case "-h":
22
+ case "--help":
23
+ printHelp();
24
+ return;
25
+ case "doctor":
26
+ await commandDoctor(config, rest);
27
+ return;
28
+ case "init":
29
+ await commandInit(config, rest);
30
+ return;
31
+ case "cmux-new":
32
+ case "new":
33
+ await commandCmuxNew(config, rest, command);
34
+ return;
35
+ case "layout":
36
+ await commandLayout(config, rest);
37
+ return;
38
+ case "workspace":
39
+ case "ws":
40
+ await commandWorkspace(config, rest);
41
+ return;
42
+ case "open":
43
+ await commandWorkspace(config, ["open", ...rest]);
44
+ return;
45
+ case "switch":
46
+ await commandWorkspace(config, ["open", ...rest]);
47
+ return;
48
+ case "list":
49
+ await commandWorkspace(config, ["list", ...rest]);
50
+ return;
51
+ case "done":
52
+ await commandWorkspace(config, ["done", ...rest]);
53
+ return;
54
+ case "remove":
55
+ await commandWorkspace(config, ["remove", ...rest]);
56
+ return;
57
+ case "gc":
58
+ case "clean":
59
+ await commandWorkspace(config, ["gc", ...rest]);
60
+ return;
61
+ case "diff":
62
+ await commandDiff(config, rest);
63
+ return;
64
+ case "commit":
65
+ await commandCommit(config, rest);
66
+ return;
67
+ case "git":
68
+ await commandGit(config, rest);
69
+ return;
70
+ case "files":
71
+ assertGate("files", config);
72
+ await runInherit(config.defaults.files || "yazi", [rest[0] || "."]);
73
+ return;
74
+ case "edit":
75
+ await commandEdit(config, rest);
76
+ return;
77
+ case "grep":
78
+ await commandGrep(config, rest);
79
+ return;
80
+ case "pick":
81
+ await commandPick(config, rest);
82
+ return;
83
+ case "tree":
84
+ await commandTree(config, rest);
85
+ return;
86
+ default: {
87
+ const error = new Error(`unknown command: ${command}`);
88
+ error.exitCode = 2;
89
+ throw error;
90
+ }
91
+ }
92
+ }
93
+
94
+ async function commandDoctor(config, argv) {
95
+ const flags = parseFlags(argv);
96
+ const agent = flags.agent ? resolveAgent(config, flags.agent) : undefined;
97
+ try {
98
+ printDoctor(config, {
99
+ json: flags.json,
100
+ gate: flags.gate,
101
+ agent
102
+ });
103
+ } catch (error) {
104
+ if (error.exitCode === 10) {
105
+ process.exitCode = 10;
106
+ return;
107
+ }
108
+ throw error;
109
+ }
110
+ }
111
+
112
+ async function commandCmuxNew(config, argv, commandName) {
113
+ const flags = parseFlags(argv);
114
+ const branchFromArgs = flags.positionals[0] && !isKnownAgent(config, flags.positionals[0])
115
+ ? flags.positionals[0]
116
+ : "";
117
+ const agentFromArgs = commandName === "new"
118
+ ? flags.positionals[1]
119
+ : flags.positionals.find((item) => isKnownAgent(config, item));
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");
122
+ error.exitCode = 2;
123
+ throw error;
124
+ }
125
+ const repo = await resolveRepo(process.cwd(), config.paths.code_root, flags.repo, {
126
+ pickRepo: flags.pickRepo,
127
+ interactive: !flags.repo
128
+ });
129
+ const branchSelection = flags.local
130
+ ? { local: true }
131
+ : await selectBranch(repo, flags.branch || branchFromArgs, {
132
+ forceCreate: flags.create
133
+ });
134
+ const agent = await selectAgent(config, flags.agent || agentFromArgs);
135
+
136
+ if (branchSelection.local) {
137
+ assertGate("layout", config, agent);
138
+ const layoutArgs = ["layout", "--agent", agent.name];
139
+ if (flags.dryRun) {
140
+ console.log(`cd ${quoteShell(repo)} && ${quoteShell(aiwBinPath())} ${layoutArgs.map(quoteShell).join(" ")}`);
141
+ return;
142
+ }
143
+ await runInherit(aiwBinPath(), layoutArgs, { cwd: repo });
144
+ return;
145
+ }
146
+
147
+ assertGate("cmux-new", config, agent);
148
+
149
+ if (config.behavior.warn_dirty_before_new !== false && isDirty(repo)) {
150
+ console.warn(`[warn] ${repo} has uncommitted changes; Worktrunk will continue with the selected branch flow`);
151
+ }
152
+
153
+ const layoutCommand = `${quoteShell(aiwBinPath())} layout --agent ${quoteShell(agent.name)}`;
154
+ const wtArgs = branchSelection.create
155
+ ? ["switch", "--create", branchSelection.branch, "--base", "@", "-x", layoutCommand]
156
+ : ["switch", branchSelection.branch, "-x", layoutCommand];
157
+ if (flags.dryRun) {
158
+ console.log(`cd ${quoteShell(repo)} && wt ${wtArgs.map(quoteShell).join(" ")}`);
159
+ return;
160
+ }
161
+ await runInherit("wt", wtArgs, { cwd: repo });
162
+ if (branchSelection.create && branchSelection.targetBranch) {
163
+ recordWorkspaceTarget(repo, branchSelection.branch, branchSelection.targetBranch);
164
+ }
165
+ }
166
+
167
+ async function commandLayout(config, argv) {
168
+ const flags = parseFlags(argv);
169
+ const agent = resolveAgent(config, flags.agent || flags.positionals[0]);
170
+ const cwd = process.cwd();
171
+ let repo = "";
172
+ if (!flags.printJson) {
173
+ assertGate("layout", config, agent);
174
+ }
175
+ if (config.behavior.require_git_repo !== false) {
176
+ if (!flags.printJson) {
177
+ repo = assertGitRoot(cwd);
178
+ }
179
+ } else {
180
+ repo = gitRoot(cwd) || cwd;
181
+ }
182
+ const layout = buildLayout(config, agent.name);
183
+ const layoutJson = JSON.stringify(layout);
184
+ if (flags.printJson) {
185
+ console.log(JSON.stringify(layout, null, 2));
186
+ return;
187
+ }
188
+ await runWorkspaceHook(config, "pre_init", {
189
+ repo: repo || cwd,
190
+ cwd: repo || cwd,
191
+ workspacePath: repo || cwd,
192
+ branch: "",
193
+ agent: agent.name,
194
+ dryRun: flags.dryRun
195
+ });
196
+ if (flags.dryRun) {
197
+ console.log(`cmux new-workspace --name ${quoteShell(workspaceName(cwd, agent.name))} --cwd ${quoteShell(cwd)} --focus true --layout ${quoteShell(layoutJson)}`);
198
+ return;
199
+ }
200
+ await runInherit("cmux", [
201
+ "new-workspace",
202
+ "--name",
203
+ workspaceName(cwd, agent.name),
204
+ "--cwd",
205
+ cwd,
206
+ "--focus",
207
+ "true",
208
+ "--layout",
209
+ layoutJson
210
+ ]);
211
+ }
212
+
213
+ async function commandDiff(config, argv) {
214
+ const flags = parseFlags(argv);
215
+ assertGate("diff", config);
216
+ const mode = flags.staged ? "--staged" : flags.all ? "--all" : "";
217
+ if (flags.watch) {
218
+ for (;;) {
219
+ process.stdout.write("\x1Bc");
220
+ console.log(`[aiw diff] ${new Date().toLocaleTimeString()} ${mode}`.trim());
221
+ await runDiffOnce(mode);
222
+ await sleep(2000);
223
+ }
224
+ }
225
+ await runDiffOnce(mode);
226
+ }
227
+
228
+ async function commandGit(config, argv) {
229
+ assertGate("git", config);
230
+ const lazygit = config.defaults.git || "lazygit";
231
+ const lazygitConfig = resolveConfigFile(config, config.git.lazygit_config);
232
+ const args = lazygitConfig ? ["--use-config-file", lazygitConfig, ...argv] : argv;
233
+ await runInherit(lazygit, args);
234
+ }
235
+
236
+ async function commandCommit(config, argv) {
237
+ const flags = parseFlags(argv);
238
+ const agent = resolveAgent(config, flags.agent || config.commit.agent || config.defaults.agent);
239
+ assertGate("commit", config, agent);
240
+ await runCommit(config, flags);
241
+ }
242
+
243
+ function resolveConfigFile(config, value) {
244
+ if (!value) {
245
+ return "";
246
+ }
247
+ const expanded = expandHome(value);
248
+ const resolved = path.isAbsolute(expanded) ? expanded : path.join(config.configDir, expanded);
249
+ return fs.existsSync(resolved) ? resolved : "";
250
+ }
251
+
252
+ async function runDiffOnce(mode) {
253
+ if (!mode && commandExists("cmux-git-diff")) {
254
+ await runInherit("cmux-git-diff");
255
+ return;
256
+ }
257
+ const args = mode === "--staged" ? ["diff", "--staged"] : mode === "--all" ? ["diff", "HEAD"] : ["diff"];
258
+ if (commandExists("delta")) {
259
+ await runInherit("sh", ["-lc", `git ${args.map(quoteShell).join(" ")} | delta`]);
260
+ return;
261
+ }
262
+ await runInherit("git", args);
263
+ }
264
+
265
+ async function commandEdit(config, argv) {
266
+ assertGate("edit", config);
267
+ const target = argv[0];
268
+ if (!target) {
269
+ const error = new Error("Usage: aiw edit <file[:line]>");
270
+ error.exitCode = 2;
271
+ throw error;
272
+ }
273
+ const editor = config.defaults.editor || process.env.EDITOR || "nvim";
274
+ const match = target.match(/^(.+):([0-9]+)$/);
275
+ if (match) {
276
+ await runInherit(editor, [`+${match[2]}`, match[1]]);
277
+ return;
278
+ }
279
+ await runInherit(editor, [target]);
280
+ }
281
+
282
+ async function commandGrep(config, argv) {
283
+ assertGate("grep", config);
284
+ const query = argv.join(" ");
285
+ if (!query) {
286
+ const error = new Error("Usage: aiw grep <query>");
287
+ error.exitCode = 2;
288
+ throw error;
289
+ }
290
+ const editor = config.defaults.editor || process.env.EDITOR || "nvim";
291
+ await runInherit("sh", [
292
+ "-lc",
293
+ `rg -n ${quoteShell(query)} | fzf --delimiter ':' --preview 'bat --style=numbers --color=always --highlight-line {2} {1}' --bind ${quoteShell(`enter:execute(${editor} +{2} {1})`)}`
294
+ ]);
295
+ }
296
+
297
+ async function commandPick(config) {
298
+ assertGate("pick", config);
299
+ const editor = config.defaults.editor || process.env.EDITOR || "nvim";
300
+ await runInherit("sh", [
301
+ "-lc",
302
+ `fd -t f | fzf --preview 'bat --style=numbers --color=always {}' --bind ${quoteShell(`enter:execute(${editor} {})`)}`
303
+ ]);
304
+ }
305
+
306
+ async function commandTree(config, argv) {
307
+ const depth = argv[0] || String(config.defaults.tree_depth || 3);
308
+ if (commandExists("eza")) {
309
+ await runInherit("eza", ["--tree", `--level=${depth}`, "--git-ignore"]);
310
+ return;
311
+ }
312
+ await runInherit("find", [".", "-maxdepth", depth, "-type", "f"]);
313
+ }
314
+
315
+ function parseFlags(argv) {
316
+ const flags = {
317
+ positionals: []
318
+ };
319
+ for (let index = 0; index < argv.length; index += 1) {
320
+ const arg = argv[index];
321
+ switch (arg) {
322
+ case "--agent":
323
+ flags.agent = argv[++index];
324
+ break;
325
+ case "--branch":
326
+ flags.branch = argv[++index];
327
+ break;
328
+ case "--repo":
329
+ flags.repo = argv[++index];
330
+ break;
331
+ case "--prompt":
332
+ flags.prompt = argv[++index];
333
+ break;
334
+ case "--prompt-file":
335
+ flags.promptFile = argv[++index];
336
+ break;
337
+ case "--retries":
338
+ flags.retries = argv[++index];
339
+ break;
340
+ case "--pick-repo":
341
+ case "--select-repo":
342
+ flags.pickRepo = true;
343
+ break;
344
+ case "--create":
345
+ flags.create = true;
346
+ break;
347
+ case "--local":
348
+ flags.local = true;
349
+ break;
350
+ case "--gate":
351
+ flags.gate = argv[++index];
352
+ break;
353
+ case "--json":
354
+ flags.json = true;
355
+ break;
356
+ case "--print-json":
357
+ flags.printJson = true;
358
+ break;
359
+ case "--print-prompt":
360
+ flags.printPrompt = true;
361
+ break;
362
+ case "--dry-run":
363
+ flags.dryRun = true;
364
+ break;
365
+ case "--watch":
366
+ flags.watch = true;
367
+ break;
368
+ case "--staged":
369
+ flags.staged = true;
370
+ break;
371
+ case "--all":
372
+ flags.all = true;
373
+ break;
374
+ case "--force":
375
+ case "-f":
376
+ flags.force = true;
377
+ break;
378
+ default:
379
+ flags.positionals.push(arg);
380
+ }
381
+ }
382
+ return flags;
383
+ }
384
+
385
+ function isKnownAgent(config, value) {
386
+ return Boolean(value && config.agents[value]);
387
+ }
388
+
389
+ async function selectAgent(config, requestedAgent) {
390
+ if (requestedAgent) {
391
+ return resolveAgent(config, requestedAgent);
392
+ }
393
+ if (!process.stdin.isTTY) {
394
+ return resolveAgent(config, config.defaults.agent);
395
+ }
396
+ const { pickFromList } = await import("./prompt.mjs");
397
+ const agents = Object.keys(config.agents);
398
+ const selected = await pickFromList("Select agent", agents, {
399
+ defaultItem: config.defaults.agent || "codex"
400
+ });
401
+ return resolveAgent(config, selected);
402
+ }
403
+
404
+ function printHelp() {
405
+ const executable = path.relative(process.cwd(), fileURLToPath(import.meta.url)).startsWith("..")
406
+ ? "aiw"
407
+ : "./bin/aiw";
408
+ console.log(`Usage: ${executable} <command> [options]
409
+
410
+ Commands:
411
+ init [--cmux-scope <home|code|none>] [--code-root <path>] [--config-dir <path>] [--dry-run]
412
+ 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]
414
+ layout [--agent <name>] [--print-json] [--dry-run]
415
+ workspace|ws <list|open|done|remove|gc> [options]
416
+ commit [--agent <name>] [--prompt <text>] [--prompt-file <path>] [--retries <n>] [--dry-run] [--print-prompt]
417
+ open | switch | list | done | remove | gc | clean
418
+ diff [--watch] [--staged] [--all]
419
+ git | files [path] | edit <file[:line]> | grep <query> | pick | tree [depth]
420
+
421
+ Main workflow commands run dependency gates before creating worktrees or opening cmux layouts.`);
422
+ }