@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.
- package/README.md +681 -0
- package/README.zh-CN.md +681 -0
- package/bin/aiw +8 -0
- package/config/agents.toml +24 -0
- package/config/aiw.toml +41 -0
- package/config/commit-prompt.md +8 -0
- package/config/lazygit-delta.yml +15 -0
- package/package.json +42 -0
- package/scripts/install-global.sh +16 -0
- package/src/agent.mjs +53 -0
- package/src/cli.mjs +422 -0
- package/src/commit.mjs +175 -0
- package/src/config.mjs +190 -0
- package/src/deps.mjs +172 -0
- package/src/git.mjs +210 -0
- package/src/hooks.mjs +252 -0
- package/src/init.mjs +719 -0
- package/src/layout.mjs +54 -0
- package/src/prompt.mjs +60 -0
- package/src/run.mjs +78 -0
- package/src/workspace.mjs +1422 -0
package/src/init.mjs
ADDED
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { expandHome, projectRoot, resolveAgent } from "./config.mjs";
|
|
5
|
+
import { gate } from "./deps.mjs";
|
|
6
|
+
import { pickFromList } from "./prompt.mjs";
|
|
7
|
+
import { commandPath, tryCapture } from "./run.mjs";
|
|
8
|
+
|
|
9
|
+
const AIW_CONFIG_FILES = [
|
|
10
|
+
"aiw.toml",
|
|
11
|
+
"agents.toml",
|
|
12
|
+
"commit-prompt.md",
|
|
13
|
+
"lazygit-delta.yml"
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const AIW_ACTION_IDS = [
|
|
17
|
+
"aiw-new-worktree",
|
|
18
|
+
"aiw-pick-directory",
|
|
19
|
+
"aiw-local-workspace"
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const DEFAULT_CONTEXT_MENU = [
|
|
23
|
+
{ action: "aiw-new-worktree", title: "AIW New Worktree" },
|
|
24
|
+
{ action: "aiw-pick-directory", title: "AIW Pick Directory" },
|
|
25
|
+
{ action: "aiw-local-workspace", title: "AIW Local Workspace" },
|
|
26
|
+
{ type: "separator" },
|
|
27
|
+
{ action: "cmux.newTerminal", title: "New Terminal" },
|
|
28
|
+
{ action: "cmux.newBrowser", title: "New Browser" }
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const INSTALL_HINTS = {
|
|
32
|
+
git: "macOS: xcode-select --install or brew install git; Linux: use your distro package manager.",
|
|
33
|
+
cmux: "Install cmux and make sure the cmux CLI is on PATH.",
|
|
34
|
+
wt: "Install Worktrunk and make sure the wt CLI is on PATH.",
|
|
35
|
+
yazi: "macOS: brew install yazi; Linux: use your distro package manager.",
|
|
36
|
+
nvim: "macOS: brew install neovim; Linux: use your distro package manager.",
|
|
37
|
+
lazygit: "macOS: brew install lazygit; Linux: use your distro package manager.",
|
|
38
|
+
rg: "macOS: brew install ripgrep; Linux: install ripgrep.",
|
|
39
|
+
fzf: "macOS: brew install fzf; Linux: install fzf.",
|
|
40
|
+
bat: "macOS: brew install bat; Linux: install bat.",
|
|
41
|
+
delta: "macOS: brew install git-delta; Linux: install git-delta.",
|
|
42
|
+
"cmux-git-diff": "Install cmux-git-diff or install delta as the supported fallback.",
|
|
43
|
+
node: "Install Node.js >= 18 and ensure node is on PATH.",
|
|
44
|
+
npx: "Install npm/npx with Node.js and ensure npx is on PATH."
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export async function commandInit(config, argv) {
|
|
48
|
+
const flags = parseInitFlags(argv);
|
|
49
|
+
if (flags.help) {
|
|
50
|
+
printInitHelp();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const targetConfigDir = resolveConfigDir(flags.configDir);
|
|
55
|
+
const codeRoot = path.resolve(expandHome(flags.codeRoot || path.join(os.homedir(), "Code")));
|
|
56
|
+
const worktreesRoot = path.resolve(expandHome(flags.worktreesRoot || path.join(os.homedir(), "worktrees")));
|
|
57
|
+
const cmuxScope = await selectCmuxScope(flags, codeRoot);
|
|
58
|
+
const launcher = flags.launcher || process.env.AIW_INIT_COMMAND || "npx aiw";
|
|
59
|
+
const plan = buildInitPlan({
|
|
60
|
+
config,
|
|
61
|
+
flags,
|
|
62
|
+
targetConfigDir,
|
|
63
|
+
codeRoot,
|
|
64
|
+
worktreesRoot,
|
|
65
|
+
cmuxScope,
|
|
66
|
+
launcher
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (flags.json) {
|
|
70
|
+
console.log(JSON.stringify(plan, null, 2));
|
|
71
|
+
} else {
|
|
72
|
+
printPreflight(plan);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!plan.preflight.ok) {
|
|
76
|
+
const error = new Error("aiw init preflight failed; install missing blocking dependencies before retrying");
|
|
77
|
+
error.exitCode = 10;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (flags.dryRun) {
|
|
82
|
+
if (!flags.json) {
|
|
83
|
+
printPlan(plan);
|
|
84
|
+
console.log("[dry-run] no files were written");
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
applyInitPlan(plan);
|
|
90
|
+
if (!flags.json) {
|
|
91
|
+
printPlan(plan);
|
|
92
|
+
console.log("[ok] aiw init completed");
|
|
93
|
+
if (plan.cmux.path) {
|
|
94
|
+
console.log(`[ok] cmux registration: ${plan.cmux.path}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (plan.cmux.path && flags.reload !== false) {
|
|
99
|
+
reloadCmux(flags.json);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function buildInitPlan({ config, flags, targetConfigDir, codeRoot, worktreesRoot, cmuxScope, launcher }) {
|
|
104
|
+
const sourceConfigDir = path.join(projectRoot(), "config");
|
|
105
|
+
const cmuxPath = resolveCmuxPath(cmuxScope, codeRoot);
|
|
106
|
+
const preflight = collectPreflight(config);
|
|
107
|
+
const backupStamp = timestamp();
|
|
108
|
+
const aiwFiles = AIW_CONFIG_FILES.map((name) => {
|
|
109
|
+
const source = path.join(sourceConfigDir, name);
|
|
110
|
+
const target = path.join(targetConfigDir, name);
|
|
111
|
+
return {
|
|
112
|
+
name,
|
|
113
|
+
source,
|
|
114
|
+
target,
|
|
115
|
+
exists: fs.existsSync(target),
|
|
116
|
+
action: fs.existsSync(target) && !flags.force ? "keep" : fs.existsSync(target) ? "overwrite" : "create",
|
|
117
|
+
backup: fs.existsSync(target) && flags.force ? `${target}.${backupStamp}.bak` : ""
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
preflight,
|
|
123
|
+
options: {
|
|
124
|
+
dryRun: Boolean(flags.dryRun),
|
|
125
|
+
force: Boolean(flags.force),
|
|
126
|
+
codeRoot,
|
|
127
|
+
worktreesRoot,
|
|
128
|
+
configDir: targetConfigDir,
|
|
129
|
+
cmuxScope,
|
|
130
|
+
launcher
|
|
131
|
+
},
|
|
132
|
+
directories: [
|
|
133
|
+
{ path: targetConfigDir, action: fs.existsSync(targetConfigDir) ? "keep" : "create" },
|
|
134
|
+
{ path: codeRoot, action: fs.existsSync(codeRoot) ? "keep" : "create" },
|
|
135
|
+
{ path: worktreesRoot, action: fs.existsSync(worktreesRoot) ? "keep" : "create" }
|
|
136
|
+
],
|
|
137
|
+
aiwFiles,
|
|
138
|
+
cmux: cmuxPath
|
|
139
|
+
? planCmuxConfig(cmuxPath, launcher, backupStamp)
|
|
140
|
+
: { path: "", action: "skip", backup: "", plusButton: "skip" }
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectPreflight(config) {
|
|
145
|
+
const platformOk = process.platform === "darwin" || process.platform === "linux";
|
|
146
|
+
const env = [
|
|
147
|
+
envCheck("HOME", process.env.HOME, "block"),
|
|
148
|
+
envCheck("PATH", process.env.PATH, "block"),
|
|
149
|
+
envCheck("SHELL", process.env.SHELL, "warn"),
|
|
150
|
+
envCheck("AIW_CONFIG_DIR", process.env.AIW_CONFIG_DIR, "info")
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const nodeChecks = ["node", "npx"].map((name) => ({
|
|
154
|
+
name,
|
|
155
|
+
ok: Boolean(commandPath(name)),
|
|
156
|
+
path: commandPath(name),
|
|
157
|
+
blocking: true
|
|
158
|
+
}));
|
|
159
|
+
const layoutAgent = resolveAgent(config, config.defaults.agent);
|
|
160
|
+
const commitAgent = resolveAgent(config, config.commit.agent || config.defaults.agent);
|
|
161
|
+
const blockingAgents = uniqueAgents([layoutAgent, commitAgent]);
|
|
162
|
+
const dependencyGate = gate("init", config, layoutAgent);
|
|
163
|
+
const agentChecks = blockingAgents.map((agent) => ({
|
|
164
|
+
name: agent.name,
|
|
165
|
+
command: agent.cmd,
|
|
166
|
+
ok: Boolean(commandPath(agent.cmd)),
|
|
167
|
+
path: commandPath(agent.cmd)
|
|
168
|
+
}));
|
|
169
|
+
const missing = [
|
|
170
|
+
...(platformOk ? [] : [`unsupported platform: ${process.platform}`]),
|
|
171
|
+
...env.filter((item) => item.blocking && !item.ok).map((item) => item.name),
|
|
172
|
+
...nodeChecks.filter((item) => item.blocking && !item.ok).map((item) => item.name),
|
|
173
|
+
...dependencyGate.missing.filter((item) => !agentChecks.some((agent) => agent.command === item)),
|
|
174
|
+
...agentChecks.filter((item) => !item.ok).map((item) => `agent:${item.name}`)
|
|
175
|
+
];
|
|
176
|
+
const uniqueMissing = [...new Set(missing)];
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
ok: uniqueMissing.length === 0,
|
|
180
|
+
platform: {
|
|
181
|
+
name: process.platform,
|
|
182
|
+
ok: platformOk,
|
|
183
|
+
supported: ["darwin", "linux"]
|
|
184
|
+
},
|
|
185
|
+
env,
|
|
186
|
+
node: nodeChecks,
|
|
187
|
+
agents: agentChecks,
|
|
188
|
+
gate: dependencyGate,
|
|
189
|
+
optional: collectOptionalDependencies(config, new Set(blockingAgents.map((agent) => agent.name))),
|
|
190
|
+
missing: uniqueMissing
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function collectOptionalDependencies(config, blockingAgentNames) {
|
|
195
|
+
const agentCommands = Object.entries(config.agents)
|
|
196
|
+
.filter(([name]) => !blockingAgentNames.has(name))
|
|
197
|
+
.map(([name, agent]) => ({
|
|
198
|
+
name: `agent:${name}`,
|
|
199
|
+
command: String(agent.cmd || ""),
|
|
200
|
+
ok: typeof agent.cmd === "string" && Boolean(commandPath(agent.cmd)),
|
|
201
|
+
path: typeof agent.cmd === "string" ? commandPath(agent.cmd) : ""
|
|
202
|
+
}));
|
|
203
|
+
return [
|
|
204
|
+
{ name: "fd", command: "fd", ok: Boolean(commandPath("fd")), path: commandPath("fd") },
|
|
205
|
+
{ name: "eza", command: "eza", ok: Boolean(commandPath("eza")), path: commandPath("eza") },
|
|
206
|
+
...agentCommands
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function envCheck(name, value, level) {
|
|
211
|
+
return {
|
|
212
|
+
name,
|
|
213
|
+
ok: typeof value === "string" && value.length > 0,
|
|
214
|
+
value: value || "",
|
|
215
|
+
blocking: level === "block",
|
|
216
|
+
level
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function resolveConfigDir(explicitConfigDir) {
|
|
221
|
+
return path.resolve(expandHome(explicitConfigDir || process.env.AIW_CONFIG_DIR || path.join(os.homedir(), ".config", "aiw")));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function selectCmuxScope(flags, codeRoot) {
|
|
225
|
+
if (flags.cmuxScope) {
|
|
226
|
+
return normalizeCmuxScope(flags.cmuxScope);
|
|
227
|
+
}
|
|
228
|
+
if (flags.yes || !process.stdin.isTTY) {
|
|
229
|
+
return "home";
|
|
230
|
+
}
|
|
231
|
+
const homeLabel = `home - ${path.join(os.homedir(), ".config", "cmux", "cmux.json")} (default)`;
|
|
232
|
+
const codeLabel = `code - ${path.join(codeRoot, ".cmux", "cmux.json")}`;
|
|
233
|
+
const noneLabel = "none - skip cmux registration";
|
|
234
|
+
const selected = await pickFromList("Register cmux config", [homeLabel, codeLabel, noneLabel], {
|
|
235
|
+
defaultItem: homeLabel,
|
|
236
|
+
force: true
|
|
237
|
+
});
|
|
238
|
+
if (selected === codeLabel) {
|
|
239
|
+
return "code";
|
|
240
|
+
}
|
|
241
|
+
if (selected === noneLabel) {
|
|
242
|
+
return "none";
|
|
243
|
+
}
|
|
244
|
+
return "home";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function normalizeCmuxScope(value) {
|
|
248
|
+
const scope = String(value || "").trim().toLowerCase();
|
|
249
|
+
if (scope === "home" || scope === "global") {
|
|
250
|
+
return "home";
|
|
251
|
+
}
|
|
252
|
+
if (scope === "code" || scope === "code-root" || scope === "project") {
|
|
253
|
+
return "code";
|
|
254
|
+
}
|
|
255
|
+
if (scope === "none" || scope === "skip") {
|
|
256
|
+
return "none";
|
|
257
|
+
}
|
|
258
|
+
const error = new Error("--cmux-scope must be one of: home, code, none");
|
|
259
|
+
error.exitCode = 2;
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function resolveCmuxPath(scope, codeRoot) {
|
|
264
|
+
if (scope === "home") {
|
|
265
|
+
return path.join(os.homedir(), ".config", "cmux", "cmux.json");
|
|
266
|
+
}
|
|
267
|
+
if (scope === "code") {
|
|
268
|
+
return path.join(codeRoot, ".cmux", "cmux.json");
|
|
269
|
+
}
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function planCmuxConfig(cmuxPath, launcher, backupStamp) {
|
|
274
|
+
const exists = fs.existsSync(cmuxPath);
|
|
275
|
+
if (!exists) {
|
|
276
|
+
return {
|
|
277
|
+
path: cmuxPath,
|
|
278
|
+
action: "create",
|
|
279
|
+
backup: "",
|
|
280
|
+
plusButton: "set"
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const current = readJson(cmuxPath);
|
|
284
|
+
const existingAction = current.ui?.newWorkspace?.action;
|
|
285
|
+
return {
|
|
286
|
+
path: cmuxPath,
|
|
287
|
+
action: "merge",
|
|
288
|
+
backup: `${cmuxPath}.${backupStamp}.bak`,
|
|
289
|
+
plusButton: existingAction && !AIW_ACTION_IDS.includes(existingAction) ? "preserve-existing" : "set",
|
|
290
|
+
existingAction: existingAction || "",
|
|
291
|
+
launcher
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function applyInitPlan(plan) {
|
|
296
|
+
for (const directory of plan.directories) {
|
|
297
|
+
fs.mkdirSync(directory.path, { recursive: true });
|
|
298
|
+
}
|
|
299
|
+
for (const file of plan.aiwFiles) {
|
|
300
|
+
if (file.action === "keep") {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
if (file.backup) {
|
|
304
|
+
fs.copyFileSync(file.target, file.backup);
|
|
305
|
+
}
|
|
306
|
+
const source = fs.readFileSync(file.source, "utf8");
|
|
307
|
+
const next = file.name === "aiw.toml"
|
|
308
|
+
? renderAiwToml(source, plan.options.codeRoot, plan.options.worktreesRoot, plan.options.configDir)
|
|
309
|
+
: source;
|
|
310
|
+
fs.writeFileSync(file.target, next);
|
|
311
|
+
}
|
|
312
|
+
if (plan.cmux.path) {
|
|
313
|
+
writeCmuxConfig(plan.cmux.path, plan.options.launcher, plan.cmux);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function renderAiwToml(source, codeRoot, worktreesRoot, configDir) {
|
|
318
|
+
return source
|
|
319
|
+
.replace(/^code_root\s*=\s*".*"$/m, `code_root = "${escapeToml(codeRoot)}"`)
|
|
320
|
+
.replace(/^worktrees\s*=\s*".*"$/m, `worktrees = "${escapeToml(worktreesRoot)}"`)
|
|
321
|
+
.replace(/^core_config\s*=\s*".*"$/m, `core_config = "${escapeToml(configDir)}"`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function writeCmuxConfig(cmuxPath, launcher, cmuxPlan) {
|
|
325
|
+
const existing = fs.existsSync(cmuxPath) ? readJson(cmuxPath) : {};
|
|
326
|
+
if (cmuxPlan.backup) {
|
|
327
|
+
fs.copyFileSync(cmuxPath, cmuxPlan.backup);
|
|
328
|
+
}
|
|
329
|
+
const next = mergeCmuxConfig(existing, launcher, cmuxPlan);
|
|
330
|
+
fs.mkdirSync(path.dirname(cmuxPath), { recursive: true });
|
|
331
|
+
fs.writeFileSync(cmuxPath, `${JSON.stringify(next, null, 2)}\n`);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function mergeCmuxConfig(existing, launcher, cmuxPlan) {
|
|
335
|
+
const next = isPlainObject(existing) ? { ...existing } : {};
|
|
336
|
+
const actions = isPlainObject(next.actions) ? { ...next.actions } : {};
|
|
337
|
+
actions["aiw-new-worktree"] = cmuxAction({
|
|
338
|
+
title: "AIW New Worktree",
|
|
339
|
+
subtitle: "Create a Worktrunk worktree from the current workspace",
|
|
340
|
+
command: `${launcher} cmux-new`,
|
|
341
|
+
icon: "folder.badge.plus"
|
|
342
|
+
});
|
|
343
|
+
actions["aiw-pick-directory"] = cmuxAction({
|
|
344
|
+
title: "AIW Pick Directory",
|
|
345
|
+
subtitle: "Choose a repository before running aiw cmux-new",
|
|
346
|
+
command: `${launcher} cmux-new --pick-repo`,
|
|
347
|
+
icon: "folder.badge.plus"
|
|
348
|
+
});
|
|
349
|
+
actions["aiw-local-workspace"] = cmuxAction({
|
|
350
|
+
title: "AIW Local Workspace",
|
|
351
|
+
subtitle: "Open the current checkout without creating a worktree",
|
|
352
|
+
command: `${launcher} cmux-new --local`,
|
|
353
|
+
icon: "terminal"
|
|
354
|
+
});
|
|
355
|
+
next.actions = actions;
|
|
356
|
+
|
|
357
|
+
const ui = isPlainObject(next.ui) ? { ...next.ui } : {};
|
|
358
|
+
const newWorkspace = isPlainObject(ui.newWorkspace) ? { ...ui.newWorkspace } : {};
|
|
359
|
+
if (cmuxPlan.plusButton !== "preserve-existing") {
|
|
360
|
+
newWorkspace.action = "aiw-new-worktree";
|
|
361
|
+
}
|
|
362
|
+
newWorkspace.contextMenu = mergeContextMenu(newWorkspace.contextMenu);
|
|
363
|
+
ui.newWorkspace = newWorkspace;
|
|
364
|
+
next.ui = ui;
|
|
365
|
+
return next;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function cmuxAction({ title, subtitle, command, icon }) {
|
|
369
|
+
return {
|
|
370
|
+
type: "command",
|
|
371
|
+
title,
|
|
372
|
+
subtitle,
|
|
373
|
+
command,
|
|
374
|
+
target: "newTabInCurrentPane",
|
|
375
|
+
icon: {
|
|
376
|
+
type: "symbol",
|
|
377
|
+
name: icon
|
|
378
|
+
}
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function mergeContextMenu(currentMenu) {
|
|
383
|
+
const existingItems = Array.isArray(currentMenu) ? currentMenu : [];
|
|
384
|
+
const nonAiwItems = existingItems.filter((item) => {
|
|
385
|
+
if (!isPlainObject(item) || typeof item.action !== "string") {
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
return !AIW_ACTION_IDS.includes(item.action);
|
|
389
|
+
});
|
|
390
|
+
if (nonAiwItems.length === 0) {
|
|
391
|
+
return DEFAULT_CONTEXT_MENU;
|
|
392
|
+
}
|
|
393
|
+
return [
|
|
394
|
+
DEFAULT_CONTEXT_MENU[0],
|
|
395
|
+
DEFAULT_CONTEXT_MENU[1],
|
|
396
|
+
DEFAULT_CONTEXT_MENU[2],
|
|
397
|
+
{ type: "separator" },
|
|
398
|
+
...trimLeadingSeparators(nonAiwItems)
|
|
399
|
+
];
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function trimLeadingSeparators(items) {
|
|
403
|
+
let start = 0;
|
|
404
|
+
while (start < items.length && isPlainObject(items[start]) && items[start].type === "separator") {
|
|
405
|
+
start += 1;
|
|
406
|
+
}
|
|
407
|
+
return items.slice(start);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function readJson(filePath) {
|
|
411
|
+
const source = fs.readFileSync(filePath, "utf8");
|
|
412
|
+
try {
|
|
413
|
+
return JSON.parse(source);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
try {
|
|
416
|
+
return JSON.parse(removeTrailingCommas(stripJsonComments(source)));
|
|
417
|
+
} catch {
|
|
418
|
+
const wrapped = new Error(`invalid JSON in ${filePath}: ${error.message}`);
|
|
419
|
+
wrapped.exitCode = 2;
|
|
420
|
+
throw wrapped;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function stripJsonComments(source) {
|
|
426
|
+
let result = "";
|
|
427
|
+
let inString = false;
|
|
428
|
+
let escaped = false;
|
|
429
|
+
let inLineComment = false;
|
|
430
|
+
let inBlockComment = false;
|
|
431
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
432
|
+
const char = source[index];
|
|
433
|
+
const next = source[index + 1];
|
|
434
|
+
if (inLineComment) {
|
|
435
|
+
if (char === "\n" || char === "\r") {
|
|
436
|
+
inLineComment = false;
|
|
437
|
+
result += char;
|
|
438
|
+
}
|
|
439
|
+
continue;
|
|
440
|
+
}
|
|
441
|
+
if (inBlockComment) {
|
|
442
|
+
if (char === "*" && next === "/") {
|
|
443
|
+
inBlockComment = false;
|
|
444
|
+
index += 1;
|
|
445
|
+
}
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (escaped) {
|
|
449
|
+
escaped = false;
|
|
450
|
+
result += char;
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (char === "\\") {
|
|
454
|
+
escaped = true;
|
|
455
|
+
result += char;
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (char === '"') {
|
|
459
|
+
inString = !inString;
|
|
460
|
+
result += char;
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (!inString && char === "/" && next === "/") {
|
|
464
|
+
inLineComment = true;
|
|
465
|
+
index += 1;
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (!inString && char === "/" && next === "*") {
|
|
469
|
+
inBlockComment = true;
|
|
470
|
+
index += 1;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
result += char;
|
|
474
|
+
}
|
|
475
|
+
return result;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function removeTrailingCommas(source) {
|
|
479
|
+
let result = "";
|
|
480
|
+
let inString = false;
|
|
481
|
+
let escaped = false;
|
|
482
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
483
|
+
const char = source[index];
|
|
484
|
+
if (escaped) {
|
|
485
|
+
escaped = false;
|
|
486
|
+
result += char;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (char === "\\") {
|
|
490
|
+
escaped = true;
|
|
491
|
+
result += char;
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
if (char === '"') {
|
|
495
|
+
inString = !inString;
|
|
496
|
+
result += char;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (!inString && char === ",") {
|
|
500
|
+
const nextIndex = nextNonWhitespaceIndex(source, index + 1);
|
|
501
|
+
if (source[nextIndex] === "}" || source[nextIndex] === "]") {
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
result += char;
|
|
506
|
+
}
|
|
507
|
+
return result;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function nextNonWhitespaceIndex(source, start) {
|
|
511
|
+
for (let index = start; index < source.length; index += 1) {
|
|
512
|
+
if (!/\s/.test(source[index])) {
|
|
513
|
+
return index;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return source.length;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function reloadCmux(json) {
|
|
520
|
+
const result = tryCapture("cmux", ["reload-config"]);
|
|
521
|
+
if (json) {
|
|
522
|
+
console.log(JSON.stringify({ cmuxReload: result }, null, 2));
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
if (result.ok) {
|
|
526
|
+
console.log("[ok] cmux config reloaded");
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
console.warn(`[warn] cmux reload-config failed: ${result.stderr || result.stdout || `exit ${result.status}`}`);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function printPreflight(plan) {
|
|
533
|
+
const { preflight } = plan;
|
|
534
|
+
console.log("AIW init preflight");
|
|
535
|
+
console.log(`${preflight.platform.ok ? "[ok]" : "[missing]"} platform ${preflight.platform.name} (supported: ${preflight.platform.supported.join(", ")})`);
|
|
536
|
+
for (const item of preflight.env) {
|
|
537
|
+
if (item.level === "info" && !item.value) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
const status = item.ok ? "[ok]" : item.blocking ? "[missing]" : "[warn]";
|
|
541
|
+
const suffix = item.value ? `=${formatEnvValue(item)}` : "";
|
|
542
|
+
console.log(`${status} env ${item.name}${suffix}`);
|
|
543
|
+
}
|
|
544
|
+
for (const item of preflight.node) {
|
|
545
|
+
console.log(`${item.ok ? "[ok]" : "[missing]"} ${item.name}${item.path ? ` ${item.path}` : ""}`);
|
|
546
|
+
}
|
|
547
|
+
for (const item of preflight.agents) {
|
|
548
|
+
console.log(`${item.ok ? "[ok]" : "[missing]"} agent:${item.name} (${item.command})${item.path ? ` ${item.path}` : ""}`);
|
|
549
|
+
}
|
|
550
|
+
for (const item of preflight.gate.satisfied) {
|
|
551
|
+
if (preflight.agents.some((agent) => agent.command === item)) {
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
console.log(`[ok] ${item}`);
|
|
555
|
+
}
|
|
556
|
+
for (const item of preflight.gate.missing) {
|
|
557
|
+
if (preflight.agents.some((agent) => agent.command === item)) {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
console.log(`[missing] ${item}`);
|
|
561
|
+
}
|
|
562
|
+
const optionalMissing = preflight.optional.filter((item) => !item.ok);
|
|
563
|
+
for (const item of optionalMissing) {
|
|
564
|
+
console.log(`[optional missing] ${item.name}${item.command ? ` (${item.command})` : ""}`);
|
|
565
|
+
}
|
|
566
|
+
if (preflight.missing.length > 0) {
|
|
567
|
+
console.log("");
|
|
568
|
+
console.log("Install missing blocking dependencies:");
|
|
569
|
+
for (const item of preflight.missing) {
|
|
570
|
+
console.log(`- ${item}: ${hintFor(item)}`);
|
|
571
|
+
}
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
console.log("[ok] blocking dependency gate passed");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function printPlan(plan) {
|
|
578
|
+
console.log("");
|
|
579
|
+
console.log("AIW init plan");
|
|
580
|
+
for (const directory of plan.directories) {
|
|
581
|
+
console.log(`[${directory.action}] dir ${directory.path}`);
|
|
582
|
+
}
|
|
583
|
+
for (const file of plan.aiwFiles) {
|
|
584
|
+
const backup = file.backup ? ` backup=${file.backup}` : "";
|
|
585
|
+
console.log(`[${file.action}] ${file.target}${backup}`);
|
|
586
|
+
}
|
|
587
|
+
if (plan.cmux.path) {
|
|
588
|
+
const backup = plan.cmux.backup ? ` backup=${plan.cmux.backup}` : "";
|
|
589
|
+
const plus = plan.cmux.plusButton === "preserve-existing"
|
|
590
|
+
? ` preserve plus-button action=${plan.cmux.existingAction}`
|
|
591
|
+
: " set plus-button action=aiw-new-worktree";
|
|
592
|
+
console.log(`[${plan.cmux.action}] ${plan.cmux.path}${backup}${plus}`);
|
|
593
|
+
} else {
|
|
594
|
+
console.log("[skip] cmux registration");
|
|
595
|
+
}
|
|
596
|
+
console.log(`[skip] skills initialization`);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function hintFor(item) {
|
|
600
|
+
if (item.includes(" or ")) {
|
|
601
|
+
return item.split(" or ").map((part) => hintFor(part)).join(" OR ");
|
|
602
|
+
}
|
|
603
|
+
if (item.startsWith("unsupported platform")) {
|
|
604
|
+
return "aiw init currently supports macOS and Linux only.";
|
|
605
|
+
}
|
|
606
|
+
if (item.startsWith("agent:")) {
|
|
607
|
+
return "Install the configured agent CLI or change ~/.config/aiw/agents.toml.";
|
|
608
|
+
}
|
|
609
|
+
return INSTALL_HINTS[item] || "Install it and ensure it is available on PATH.";
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function uniqueAgents(agents) {
|
|
613
|
+
const seen = new Set();
|
|
614
|
+
const result = [];
|
|
615
|
+
for (const agent of agents) {
|
|
616
|
+
const key = `${agent.name}:${agent.cmd}`;
|
|
617
|
+
if (seen.has(key)) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
seen.add(key);
|
|
621
|
+
result.push(agent);
|
|
622
|
+
}
|
|
623
|
+
return result;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function formatEnvValue(item) {
|
|
627
|
+
if (item.name === "PATH") {
|
|
628
|
+
return `set (${item.value.split(path.delimiter).filter(Boolean).length} entries)`;
|
|
629
|
+
}
|
|
630
|
+
if (item.value.length > 120) {
|
|
631
|
+
return `${item.value.slice(0, 117)}...`;
|
|
632
|
+
}
|
|
633
|
+
return item.value;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function parseInitFlags(argv) {
|
|
637
|
+
const flags = {
|
|
638
|
+
reload: true
|
|
639
|
+
};
|
|
640
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
641
|
+
const arg = argv[index];
|
|
642
|
+
switch (arg) {
|
|
643
|
+
case "--help":
|
|
644
|
+
case "-h":
|
|
645
|
+
flags.help = true;
|
|
646
|
+
break;
|
|
647
|
+
case "--cmux-scope":
|
|
648
|
+
case "--cmux":
|
|
649
|
+
flags.cmuxScope = argv[++index];
|
|
650
|
+
break;
|
|
651
|
+
case "--config-dir":
|
|
652
|
+
flags.configDir = argv[++index];
|
|
653
|
+
break;
|
|
654
|
+
case "--code-root":
|
|
655
|
+
flags.codeRoot = argv[++index];
|
|
656
|
+
break;
|
|
657
|
+
case "--worktrees-root":
|
|
658
|
+
flags.worktreesRoot = argv[++index];
|
|
659
|
+
break;
|
|
660
|
+
case "--launcher":
|
|
661
|
+
case "--command-prefix":
|
|
662
|
+
flags.launcher = argv[++index];
|
|
663
|
+
break;
|
|
664
|
+
case "--force":
|
|
665
|
+
case "-f":
|
|
666
|
+
flags.force = true;
|
|
667
|
+
break;
|
|
668
|
+
case "--yes":
|
|
669
|
+
case "-y":
|
|
670
|
+
flags.yes = true;
|
|
671
|
+
break;
|
|
672
|
+
case "--dry-run":
|
|
673
|
+
flags.dryRun = true;
|
|
674
|
+
break;
|
|
675
|
+
case "--json":
|
|
676
|
+
flags.json = true;
|
|
677
|
+
break;
|
|
678
|
+
case "--no-reload":
|
|
679
|
+
flags.reload = false;
|
|
680
|
+
break;
|
|
681
|
+
default: {
|
|
682
|
+
const error = new Error(`unknown init option: ${arg}`);
|
|
683
|
+
error.exitCode = 2;
|
|
684
|
+
throw error;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return flags;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function printInitHelp() {
|
|
692
|
+
console.log(`Usage: aiw init [options]
|
|
693
|
+
|
|
694
|
+
Initialize AIW on macOS/Linux through the npx-friendly entrypoint.
|
|
695
|
+
|
|
696
|
+
Options:
|
|
697
|
+
--cmux-scope <home|code|none> Register cmux in ~/.config/cmux, <code-root>/.cmux, or skip
|
|
698
|
+
--config-dir <path> AIW config directory; defaults to AIW_CONFIG_DIR or ~/.config/aiw
|
|
699
|
+
--code-root <path> Code root written to aiw.toml; defaults to ~/Code
|
|
700
|
+
--worktrees-root <path> Worktree root written to aiw.toml; defaults to ~/worktrees
|
|
701
|
+
--launcher <command> Command prefix stored in cmux actions; defaults to "npx aiw"
|
|
702
|
+
--force Overwrite existing AIW config files after creating backups
|
|
703
|
+
--yes Use defaults without prompts
|
|
704
|
+
--dry-run Print the plan without writing files
|
|
705
|
+
--no-reload Do not run cmux reload-config after writing
|
|
706
|
+
--json Print structured preflight and plan data`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function escapeToml(value) {
|
|
710
|
+
return String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function timestamp() {
|
|
714
|
+
return new Date().toISOString().replace(/[-:]/g, "").replace(/\..+$/, "");
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function isPlainObject(value) {
|
|
718
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
719
|
+
}
|