@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/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
+ }