kankaku-tui 0.3.0 → 0.3.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 +37 -21
- package/dist/adapters/project-discovery.js +78 -21
- package/dist/cli.js +1 -1
- package/dist/domain/setup-wizard.js +23 -12
- package/dist/ui/setup/wizard-screen.js +63 -66
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,24 +16,27 @@ kankaku setup
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
On a real terminal, `kankaku setup` opens as a full-screen wizard — one
|
|
19
|
-
step at a time, in the same
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
step at a time, in the same header/panel/footer look as the rest of the
|
|
20
|
+
app (there's no sidebar in the wizard itself: the panel's own title
|
|
21
|
+
tracks progress instead, e.g. `Setup · Agents 1/5`, numbering only the
|
|
22
|
+
steps this run will actually show). `kankaku` with no arguments does the
|
|
23
|
+
same the very first time (no `~/.kankaku/tui.json` yet); after that first
|
|
24
|
+
run it opens straight into the Dashboard as usual.
|
|
23
25
|
|
|
24
26
|
The wizard's steps, `enter` to advance and `esc` to go back throughout
|
|
25
|
-
(`esc` at the first step quits):
|
|
27
|
+
(`esc` at the first step, Agents, quits):
|
|
26
28
|
|
|
27
|
-
1. **
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
1. **Agents** — a checklist (`space` toggles) that also carries detection
|
|
30
|
+
for every agent kankaku knows about: pi, gentle-shell and Claude Code
|
|
31
|
+
each show `configured (<path, shortened with ~>)` or `not configured`;
|
|
32
|
+
pre-checked means already configured, and unchecking a configured
|
|
31
33
|
agent schedules removing kankaku from it, not just skipping it. Codex
|
|
32
|
-
and OpenCode are listed but disabled
|
|
33
|
-
|
|
34
|
+
and OpenCode are listed but disabled, showing `no adapter yet` (found,
|
|
35
|
+
but kankaku can't write its config) or `not installed` (not found).
|
|
36
|
+
2. **Claude Code** — the `kankaku-claude` checkout path (only shown when
|
|
34
37
|
Claude Code is checked and not already configured), guessed from any
|
|
35
38
|
existing `statusLine`.
|
|
36
|
-
|
|
39
|
+
3. **Hub** — `use an existing hub` (URL, email, masked password, a `c`
|
|
37
40
|
inline health check, reusing the current credentials as the default),
|
|
38
41
|
`install locally`, or `skip`. Installing locally looks for a
|
|
39
42
|
`kankaku-hub` checkout first; if found, its path is prefilled and
|
|
@@ -45,14 +48,14 @@ The wizard's steps, `enter` to advance and `esc` to go back throughout
|
|
|
45
48
|
wizard shows the exact commands to run in another terminal and a `c`
|
|
46
49
|
"check again" action, plus `m` to acknowledge you'll install it
|
|
47
50
|
manually.
|
|
48
|
-
|
|
51
|
+
4. **Roots** — the comma-separated project roots, defaulting to the
|
|
49
52
|
current `tui.json` (or the parent of the current directory the first
|
|
50
|
-
time).
|
|
51
|
-
|
|
53
|
+
time). See "Configuration" below for how deep each root is searched.
|
|
54
|
+
5. **Review** — the plan: one line per change, with the exact file it
|
|
52
55
|
touches. `enter` applies it.
|
|
53
|
-
|
|
56
|
+
6. **Apply** — runs each change and shows its result
|
|
54
57
|
(`wrote`/`unchanged`/`removed`/`started`/`error: …`) as it happens.
|
|
55
|
-
|
|
58
|
+
7. **Done** — a summary, then `enter` opens the Dashboard in place — no
|
|
56
59
|
restart.
|
|
57
60
|
|
|
58
61
|
`kankaku setup --yes` and `kankaku setup --dry-run` stay exactly as
|
|
@@ -90,10 +93,23 @@ this repo with `npm run dev`.
|
|
|
90
93
|
}
|
|
91
94
|
```
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
96
|
+
### Projects across roots
|
|
97
|
+
|
|
98
|
+
Each root is searched recursively for projects, up to 5 directory levels
|
|
99
|
+
below it by default: a directory is a project once it has its own
|
|
100
|
+
`.kankaku/worklog.jsonl` — including the root itself — and the search
|
|
101
|
+
still continues below it, so a stray worklog in a parent directory (a pi
|
|
102
|
+
session run once in `~/desarrollo`) never hides the projects beneath;
|
|
103
|
+
every directory is listed at most once. Subdirectories are searched one
|
|
104
|
+
level deeper, skipping `node_modules`, `.git` and any hidden
|
|
105
|
+
(dot-prefixed) directory. This lets one root cover a whole workspace, e.g.
|
|
106
|
+
`~/desarrollo` finding every project under `~/desarrollo/<client>/<project>`
|
|
107
|
+
without listing each one. Projects are deduped by real (symlink-resolved)
|
|
108
|
+
path and sorted by name — the directory's basename, or the last two path
|
|
109
|
+
segments joined with `/` when two discovered projects share a basename
|
|
110
|
+
(e.g. `clientA/shared` and `clientB/shared`). `~` expands to the home
|
|
111
|
+
directory. Missing or malformed config falls back to the current working
|
|
112
|
+
directory as the only root.
|
|
97
113
|
|
|
98
114
|
### Hub credentials (Catalog and Sync)
|
|
99
115
|
|
|
@@ -1,12 +1,20 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
-
import { basename, join } from "node:path";
|
|
1
|
+
import { existsSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join, sep } from "node:path";
|
|
3
|
+
/** How many directory levels below each root {@link discoverProjects} descends into by default. */
|
|
4
|
+
const DEFAULT_MAX_DEPTH = 5;
|
|
5
|
+
const SKIPPED_DIR_NAMES = new Set(["node_modules", ".git"]);
|
|
3
6
|
function hasWorklog(dir) {
|
|
4
7
|
return existsSync(join(dir, ".kankaku", "worklog.jsonl"));
|
|
5
8
|
}
|
|
6
|
-
|
|
9
|
+
/** `node_modules`, `.git`, and any dot-prefixed (hidden) directory are never searched. */
|
|
10
|
+
function isSkipped(name) {
|
|
11
|
+
return name.startsWith(".") || SKIPPED_DIR_NAMES.has(name);
|
|
12
|
+
}
|
|
13
|
+
function childDirs(dir) {
|
|
7
14
|
try {
|
|
8
|
-
return readdirSync(
|
|
9
|
-
.
|
|
15
|
+
return readdirSync(dir)
|
|
16
|
+
.filter((name) => !isSkipped(name))
|
|
17
|
+
.map((name) => join(dir, name))
|
|
10
18
|
.filter((entry) => {
|
|
11
19
|
try {
|
|
12
20
|
return statSync(entry).isDirectory();
|
|
@@ -21,23 +29,72 @@ function childDirs(root) {
|
|
|
21
29
|
}
|
|
22
30
|
}
|
|
23
31
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
32
|
+
* The real (symlink-resolved) path, used only as the dedup key — the
|
|
33
|
+
* `ProjectRef.dir` returned to callers always stays the path as
|
|
34
|
+
* discovered. Falls back to `dir` itself when it cannot be resolved (e.g.
|
|
35
|
+
* a permission error), so an unreadable directory never throws.
|
|
36
|
+
*/
|
|
37
|
+
function realDirPath(dir) {
|
|
38
|
+
try {
|
|
39
|
+
return realpathSync(dir);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Walk `dir`, `depth` levels below its root: a directory with
|
|
47
|
+
* `.kankaku/worklog.jsonl` is a project, and the walk still continues
|
|
48
|
+
* below it — a pi session run once in a parent directory (`~/desarrollo`)
|
|
49
|
+
* leaves a worklog there, and that stray file must never hide the real
|
|
50
|
+
* projects beneath (`~/desarrollo/<client>/<project>`). Subdirectories
|
|
51
|
+
* (skipping `node_modules`, `.git` and any hidden directory) are walked
|
|
52
|
+
* one level deeper, up to `maxDepth`.
|
|
28
53
|
*/
|
|
29
|
-
|
|
30
|
-
|
|
54
|
+
function walk(dir, depth, maxDepth, found) {
|
|
55
|
+
if (hasWorklog(dir))
|
|
56
|
+
found.set(realDirPath(dir), dir);
|
|
57
|
+
if (depth >= maxDepth)
|
|
58
|
+
return;
|
|
59
|
+
for (const child of childDirs(dir)) {
|
|
60
|
+
walk(child, depth + 1, maxDepth, found);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** `basename(dir)`, or the last two path segments joined with `/` when that basename is ambiguous (shared by another discovered project). */
|
|
64
|
+
function projectName(dir, duplicateBasenames) {
|
|
65
|
+
const base = basename(dir);
|
|
66
|
+
if (!duplicateBasenames.has(base))
|
|
67
|
+
return base;
|
|
68
|
+
const segments = dir.split(sep).filter((segment) => segment.length > 0);
|
|
69
|
+
return segments.slice(-2).join("/");
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Discover projects under `roots`, searching up to `options.maxDepth`
|
|
73
|
+
* directory levels below each root (default {@link DEFAULT_MAX_DEPTH}): a
|
|
74
|
+
* directory with `.kankaku/worklog.jsonl` is a project and is never
|
|
75
|
+
* descended into (so a project nested inside another project is never
|
|
76
|
+
* double-counted); otherwise its subdirectories are searched one level
|
|
77
|
+
* deeper, skipping `node_modules`, `.git` and any hidden (dot-prefixed)
|
|
78
|
+
* directory. Deduped by real (symlink-resolved) path; sorted by `name`,
|
|
79
|
+
* which is the directory basename, or the last two path segments joined
|
|
80
|
+
* with `/` when two discovered projects share a basename. Never throws on
|
|
81
|
+
* an unreadable or missing root or subdirectory — those are silently
|
|
82
|
+
* skipped.
|
|
83
|
+
*/
|
|
84
|
+
export function discoverProjects(roots, options = {}) {
|
|
85
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
86
|
+
const found = new Map();
|
|
31
87
|
for (const root of roots) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
}
|
|
88
|
+
walk(root, 0, maxDepth, found);
|
|
89
|
+
}
|
|
90
|
+
const dirs = Array.from(found.values());
|
|
91
|
+
const basenameCounts = new Map();
|
|
92
|
+
for (const dir of dirs) {
|
|
93
|
+
const base = basename(dir);
|
|
94
|
+
basenameCounts.set(base, (basenameCounts.get(base) ?? 0) + 1);
|
|
41
95
|
}
|
|
42
|
-
|
|
96
|
+
const duplicateBasenames = new Set(Array.from(basenameCounts.entries())
|
|
97
|
+
.filter(([, count]) => count > 1)
|
|
98
|
+
.map(([base]) => base));
|
|
99
|
+
return dirs.map((dir) => ({ name: projectName(dir, duplicateBasenames), dir })).sort((a, b) => a.name.localeCompare(b.name));
|
|
43
100
|
}
|
package/dist/cli.js
CHANGED
|
@@ -259,7 +259,7 @@ function gatherWizardFacts(deps) {
|
|
|
259
259
|
defaultRoots: [dirname(deps.cwd)],
|
|
260
260
|
path: tuiPath,
|
|
261
261
|
};
|
|
262
|
-
return { agentFacts, hub, roots };
|
|
262
|
+
return { agentFacts, hub, roots, homeDir: deps.homeDir };
|
|
263
263
|
}
|
|
264
264
|
/** Runs `adapters/setup/local-hub.ts#installLocalHub` for real, sharing one `ScriptRunner`/timer setup between the wizard's `installLocalHub` action and `apply`'s own `install-local-hub` handling. */
|
|
265
265
|
async function performLocalHubInstall(checkout, deps) {
|
|
@@ -14,6 +14,22 @@ export function guessClaudeCheckout(statusLineCommand) {
|
|
|
14
14
|
const match = CLAUDE_STATUS_LINE_RE.exec(statusLineCommand);
|
|
15
15
|
return match ? match[1] : "";
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Shorten `path` for display by replacing a leading `homeDir` with `~`.
|
|
19
|
+
* Only a real path-boundary match counts (`homeDir` itself, or `homeDir`
|
|
20
|
+
* followed by `/`) — a sibling directory that merely shares the prefix
|
|
21
|
+
* (e.g. `/homework` vs `/home`) is left unchanged. `path` is returned as-is
|
|
22
|
+
* when it doesn't start under `homeDir`.
|
|
23
|
+
*/
|
|
24
|
+
export function shortenHome(path, homeDir) {
|
|
25
|
+
if (homeDir === "")
|
|
26
|
+
return path;
|
|
27
|
+
if (path === homeDir)
|
|
28
|
+
return "~";
|
|
29
|
+
if (path.startsWith(`${homeDir}/`))
|
|
30
|
+
return `~${path.slice(homeDir.length)}`;
|
|
31
|
+
return path;
|
|
32
|
+
}
|
|
17
33
|
function clearError(errors, key) {
|
|
18
34
|
if (!(key in errors))
|
|
19
35
|
return errors;
|
|
@@ -30,7 +46,7 @@ export function createWizardState(facts) {
|
|
|
30
46
|
const agents = detectAgents(facts.agentFacts);
|
|
31
47
|
const selected = Object.fromEntries(agents.map((agent) => [agent.id, agent.configured]));
|
|
32
48
|
return {
|
|
33
|
-
step: "
|
|
49
|
+
step: "agents",
|
|
34
50
|
agents,
|
|
35
51
|
selected,
|
|
36
52
|
claudeCheckout: guessClaudeCheckout(facts.agentFacts.claudeCode?.statusLineCommand),
|
|
@@ -48,8 +64,8 @@ export function createWizardState(facts) {
|
|
|
48
64
|
results: [],
|
|
49
65
|
};
|
|
50
66
|
}
|
|
51
|
-
/** Whether the Claude Code step should be shown: Claude is selected and not already configured. */
|
|
52
|
-
function claudeStepNeeded(state) {
|
|
67
|
+
/** Whether the Claude Code step should be shown: Claude is selected and not already configured. Exported so `ui/setup/wizard-screen.tsx` can number only the steps that will actually be shown for this run. */
|
|
68
|
+
export function claudeStepNeeded(state) {
|
|
53
69
|
const claude = state.agents.find((agent) => agent.id === "claude-code");
|
|
54
70
|
return state.selected["claude-code"] === true && claude !== undefined && !claude.configured;
|
|
55
71
|
}
|
|
@@ -154,8 +170,6 @@ export function planFromWizard(state, facts) {
|
|
|
154
170
|
/** Advance from the current step: validates it, sets `errors` and stays when invalid, otherwise moves on. */
|
|
155
171
|
export function next(state, facts) {
|
|
156
172
|
switch (state.step) {
|
|
157
|
-
case "detect":
|
|
158
|
-
return { ...state, step: "agents", errors: {} };
|
|
159
173
|
case "agents":
|
|
160
174
|
return { ...state, step: claudeStepNeeded(state) ? "claude" : "hub", errors: {} };
|
|
161
175
|
case "claude": {
|
|
@@ -186,13 +200,11 @@ export function next(state, facts) {
|
|
|
186
200
|
return state;
|
|
187
201
|
}
|
|
188
202
|
}
|
|
189
|
-
/** Move back to the previous step, mirroring `next`'s claude-step skip. Terminal/first steps (`
|
|
203
|
+
/** Move back to the previous step, mirroring `next`'s claude-step skip. Terminal/first steps (`agents`, `apply`, `done`) are no-ops. */
|
|
190
204
|
export function back(state) {
|
|
191
205
|
switch (state.step) {
|
|
192
|
-
case "detect":
|
|
193
|
-
return state;
|
|
194
206
|
case "agents":
|
|
195
|
-
return
|
|
207
|
+
return state;
|
|
196
208
|
case "claude":
|
|
197
209
|
return { ...state, step: "agents", errors: {} };
|
|
198
210
|
case "hub":
|
|
@@ -214,10 +226,9 @@ export function applyResult(state, result) {
|
|
|
214
226
|
export function hintsForStep(step) {
|
|
215
227
|
const quit = { key: "q", label: "quit" };
|
|
216
228
|
switch (step) {
|
|
217
|
-
case "detect":
|
|
218
|
-
return [{ key: "enter", label: "next" }, quit];
|
|
219
229
|
case "agents":
|
|
220
|
-
|
|
230
|
+
// The first step: esc quits (handled by the caller), so it isn't hinted as "back" here.
|
|
231
|
+
return [{ key: "space", label: "toggle" }, { key: "↑↓", label: "move" }, { key: "enter", label: "next" }, quit];
|
|
221
232
|
case "claude":
|
|
222
233
|
return [{ key: "enter", label: "next" }, { key: "esc", label: "back" }, quit];
|
|
223
234
|
case "hub":
|
|
@@ -1,26 +1,44 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
|
-
import { Box, Text, useInput } from "ink";
|
|
4
|
-
import { applyResult, back, createWizardState, hintsForStep, next, planFromWizard, setClaudeCheckout, setHubField, setHubHealth, setHubLocalManual, setHubMode, setLocalCheckout, setRoots, toggleAgent, } from "../../domain/setup-wizard.js";
|
|
5
|
-
import {
|
|
3
|
+
import { Box, Text, useInput, useStdout } from "ink";
|
|
4
|
+
import { applyResult, back, claudeStepNeeded, createWizardState, hintsForStep, next, planFromWizard, setClaudeCheckout, setHubField, setHubHealth, setHubLocalManual, setHubMode, setLocalCheckout, setRoots, shortenHome, toggleAgent, } from "../../domain/setup-wizard.js";
|
|
5
|
+
import { HeaderBar } from "../components/header-bar.js";
|
|
6
|
+
import { KeyHints } from "../components/key-hints.js";
|
|
6
7
|
import { Panel } from "../components/panel.js";
|
|
7
8
|
import { Table } from "../components/table.js";
|
|
8
9
|
import { Checklist } from "../components/checklist.js";
|
|
9
10
|
import { Radio } from "../components/radio.js";
|
|
10
11
|
import { TextInput } from "../components/text-input.js";
|
|
11
12
|
import { useTheme } from "../theme.js";
|
|
12
|
-
/**
|
|
13
|
-
const
|
|
13
|
+
/** Fallback terminal height when neither `rows` nor `useStdout().rows` is available (matches `ui/layout.tsx`'s own fallback). */
|
|
14
|
+
const DEFAULT_ROWS = 24;
|
|
15
|
+
/** Rows used by the header line. */
|
|
16
|
+
const HEADER_ROWS = 1;
|
|
17
|
+
/** Rows used by the footer key-hints line. */
|
|
18
|
+
const FOOTER_ROWS = 1;
|
|
19
|
+
/** Panel titles for every step. Used verbatim for `apply`/`done` (never numbered); `agents`/`claude`/`hub`/`roots`/`review` get a `n/total` suffix from {@link panelTitle}. */
|
|
14
20
|
const STEP_TITLES = {
|
|
15
|
-
detect: "Detect",
|
|
16
21
|
agents: "Agents",
|
|
17
|
-
claude: "Claude",
|
|
22
|
+
claude: "Claude Code",
|
|
18
23
|
hub: "Hub",
|
|
19
24
|
roots: "Roots",
|
|
20
25
|
review: "Review",
|
|
21
26
|
apply: "Apply",
|
|
22
27
|
done: "Done",
|
|
23
28
|
};
|
|
29
|
+
/** The steps a run numbers in its panel title, in order; `claude` is dropped from this list when the run doesn't need it (see {@link numberedStepsForRun}). `apply` and `done` are never numbered — `apply` is Review's own execution and `done` is the terminal summary. */
|
|
30
|
+
const NUMBERED_STEPS = ["agents", "claude", "hub", "roots", "review"];
|
|
31
|
+
/** The steps that will actually be shown for this run, e.g. `["agents", "hub", "roots", "review"]` when Claude Code is skipped. */
|
|
32
|
+
function numberedStepsForRun(state) {
|
|
33
|
+
return NUMBERED_STEPS.filter((step) => step !== "claude" || claudeStepNeeded(state));
|
|
34
|
+
}
|
|
35
|
+
/** `[ Setup · Agents 1/5 ]`-style progress title: `n/total` over only the steps this run will show; `apply`/`done` render unnumbered. */
|
|
36
|
+
function panelTitle(state) {
|
|
37
|
+
const title = STEP_TITLES[state.step];
|
|
38
|
+
const steps = numberedStepsForRun(state);
|
|
39
|
+
const index = steps.indexOf(state.step);
|
|
40
|
+
return index === -1 ? `Setup · ${title}` : `Setup · ${title} ${index + 1}/${steps.length}`;
|
|
41
|
+
}
|
|
24
42
|
const AGENT_TITLES = {
|
|
25
43
|
pi: "pi",
|
|
26
44
|
"gentle-shell": "gentle-shell",
|
|
@@ -28,45 +46,11 @@ const AGENT_TITLES = {
|
|
|
28
46
|
codex: "Codex",
|
|
29
47
|
opencode: "OpenCode",
|
|
30
48
|
};
|
|
31
|
-
/** The
|
|
32
|
-
function
|
|
33
|
-
return step === "apply" ? "review" : step;
|
|
34
|
-
}
|
|
35
|
-
function wizardSidebarItems(step) {
|
|
36
|
-
const activeIndex = SIDEBAR_STEPS.indexOf(sidebarActiveId(step));
|
|
37
|
-
return SIDEBAR_STEPS.map((entry, index) => ({
|
|
38
|
-
id: entry,
|
|
39
|
-
key: String(index + 1),
|
|
40
|
-
label: index < activeIndex ? `✓ ${STEP_TITLES[entry]}` : STEP_TITLES[entry],
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
43
|
-
function agentStateLabel(agent) {
|
|
49
|
+
/** The Agents checklist row's note: presence/configured state for pi/gentle-shell/Claude Code (always checkable), or why an agent with no adapter yet (Codex/OpenCode, always disabled) can't be. */
|
|
50
|
+
function agentNote(agent, homeDir) {
|
|
44
51
|
if (!agent.adapterAvailable)
|
|
45
|
-
return "no adapter";
|
|
46
|
-
|
|
47
|
-
return "not found";
|
|
48
|
-
return agent.configured ? "configured" : "present";
|
|
49
|
-
}
|
|
50
|
-
const DETECT_FIXED_COLUMNS_WIDTH = 14 + 1 + 12 + 1 + 2 + 2;
|
|
51
|
-
function detectColumns(panelWidth) {
|
|
52
|
-
const detailWidth = Math.max(panelWidth - DETECT_FIXED_COLUMNS_WIDTH, 10);
|
|
53
|
-
return [
|
|
54
|
-
{ key: "agent", header: "agent", width: 14 },
|
|
55
|
-
{ key: "state", header: "state", width: 12 },
|
|
56
|
-
{ key: "detail", header: "detail", width: detailWidth },
|
|
57
|
-
];
|
|
58
|
-
}
|
|
59
|
-
function detectCell(row, key) {
|
|
60
|
-
switch (key) {
|
|
61
|
-
case "agent":
|
|
62
|
-
return AGENT_TITLES[row.id];
|
|
63
|
-
case "state":
|
|
64
|
-
return agentStateLabel(row);
|
|
65
|
-
case "detail":
|
|
66
|
-
return row.detail;
|
|
67
|
-
default:
|
|
68
|
-
return "";
|
|
69
|
-
}
|
|
52
|
+
return agent.present ? "no adapter yet" : "not installed";
|
|
53
|
+
return agent.configured ? `configured (${shortenHome(agent.detail, homeDir)})` : "not configured";
|
|
70
54
|
}
|
|
71
55
|
const REVIEW_FIXED_COLUMNS_WIDTH = 30 + 1 + 2 + 2;
|
|
72
56
|
function reviewColumns(panelWidth) {
|
|
@@ -85,17 +69,21 @@ function applyResultLine(result) {
|
|
|
85
69
|
const detail = result.detail ? ` (${result.detail})` : "";
|
|
86
70
|
return `${result.outcome} ${result.action.file}${detail}`;
|
|
87
71
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Detection is folded into the Agents checklist itself (R1): each row's
|
|
74
|
+
* label already carries the agent's presence/configured state, e.g.
|
|
75
|
+
* `pi — configured (~/.pi/agent/settings.json)` or `gentle-shell — not
|
|
76
|
+
* configured`; disabled rows (no adapter yet) read `Codex — no adapter
|
|
77
|
+
* yet` or `OpenCode — not installed`. `note` is left unset so `Checklist`
|
|
78
|
+
* never appends its own `(note)` suffix on top.
|
|
79
|
+
*/
|
|
80
|
+
function agentChecklistItems(agents, selected, homeDir) {
|
|
81
|
+
return agents.map((agent) => ({
|
|
82
|
+
id: agent.id,
|
|
83
|
+
label: `${AGENT_TITLES[agent.id]} — ${agentNote(agent, homeDir)}`,
|
|
84
|
+
checked: selected[agent.id] === true,
|
|
85
|
+
disabled: !agent.adapterAvailable,
|
|
86
|
+
}));
|
|
99
87
|
}
|
|
100
88
|
const HUB_MODE_OPTIONS = [
|
|
101
89
|
{ value: "existing", label: "use an existing hub" },
|
|
@@ -112,10 +100,15 @@ function healthLine(healthOk, checking) {
|
|
|
112
100
|
return healthOk ? "health ok" : "health failed";
|
|
113
101
|
}
|
|
114
102
|
/**
|
|
115
|
-
* `kankaku setup`'s interactive wizard: one full-screen step at a time
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
103
|
+
* `kankaku setup`'s interactive wizard: one full-screen step at a time,
|
|
104
|
+
* driven by `domain/setup-wizard.ts`'s pure reducers. Unlike every other
|
|
105
|
+
* screen it renders its own header/panel/footer frame directly instead of
|
|
106
|
+
* `ui/layout.tsx`'s `Layout` — there is no sidebar to show, since the
|
|
107
|
+
* wizard's progress lives in the panel's own title instead (`panelTitle`,
|
|
108
|
+
* e.g. `Setup · Agents 1/5`, numbering only the steps this run will
|
|
109
|
+
* actually show — Claude Code is dropped when it isn't needed). Enter
|
|
110
|
+
* advances (validating first), Esc goes back (quits from Agents, the
|
|
111
|
+
* first step, via `onQuit`); `q` quits from anywhere a text field isn't
|
|
119
112
|
* currently capturing keystrokes.
|
|
120
113
|
*/
|
|
121
114
|
export function SetupWizard({ facts, actions, onDone, onQuit, version, columns, rows }) {
|
|
@@ -162,7 +155,7 @@ export function SetupWizard({ facts, actions, onDone, onQuit, version, columns,
|
|
|
162
155
|
return;
|
|
163
156
|
}
|
|
164
157
|
if (key.escape) {
|
|
165
|
-
if (state.step === "
|
|
158
|
+
if (state.step === "agents") {
|
|
166
159
|
onQuit();
|
|
167
160
|
return;
|
|
168
161
|
}
|
|
@@ -189,10 +182,14 @@ export function SetupWizard({ facts, actions, onDone, onQuit, version, columns,
|
|
|
189
182
|
}
|
|
190
183
|
}
|
|
191
184
|
});
|
|
192
|
-
const stepTitle = STEP_TITLES[state.step];
|
|
193
185
|
const hints = hintsForStep(state.step);
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
186
|
+
const { stdout } = useStdout();
|
|
187
|
+
const width = columns ?? stdout?.columns ?? 80;
|
|
188
|
+
const height = rows ?? stdout?.rows ?? DEFAULT_ROWS;
|
|
189
|
+
const mainWidth = width;
|
|
190
|
+
const mainHeight = Math.max(height - HEADER_ROWS - FOOTER_ROWS, 0);
|
|
191
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, height: height, children: [_jsx(HeaderBar, { left: `>_ kankaku setup${version ? ` ${version}` : ""}`, width: width }), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, children: _jsxs(Panel, { title: panelTitle(state), width: mainWidth, height: mainHeight, active: true, children: [state.step === "agents" && (_jsx(Checklist, { items: agentChecklistItems(state.agents, state.selected, facts.homeDir), cursor: agentCursor, onToggle: (id) => setState((s) => toggleAgent(s, id)), onMove: (delta) => setAgentCursor((c) => Math.min(Math.max(c + delta, 0), state.agents.length - 1)), focused: true })), state.step === "claude" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "kankaku-claude checkout path:" }), _jsx(TextInput, { value: state.claudeCheckout, onChange: (value) => setState((s) => setClaudeCheckout(s, value)), focused: true }), state.errors.claudeCheckout && _jsx(Text, { color: theme.error, children: state.errors.claudeCheckout })] })), state.step === "hub" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Radio, { options: HUB_MODE_OPTIONS, value: state.hub.mode, onChange: (mode) => setState((s) => setHubMode(s, mode)), focused: hubFocus === 0 }), state.hub.mode === "existing" && (_jsxs(Box, { flexDirection: "column", children: [["url", "email", "password"].map((field, index) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: `${field}: ` }), _jsx(TextInput, { value: state.hub[field], onChange: (value) => setState((s) => setHubField(s, field, value)), masked: field === "password", focused: hubFocus === index + 1 })] }, field))), state.errors.url && _jsx(Text, { color: theme.error, children: state.errors.url }), state.errors.email && _jsx(Text, { color: theme.error, children: state.errors.email }), state.errors.password && _jsx(Text, { color: theme.error, children: state.errors.password }), _jsx(Text, { children: `[ check ] c ${healthLine(state.hub.healthOk, healthChecking)}` })] })), state.hub.mode === "local" && hasCheckoutField && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "local checkout path:" }), _jsx(TextInput, { value: checkoutValue, onChange: (value) => setState((s) => setLocalCheckout(s, value)), focused: hubFocus === 1 })] })), state.hub.mode === "local" && !hasCheckoutField && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "no kankaku-hub checkout found; run these commands, then check again:" }), actions.manualCommands(state.hub.localCheckout || undefined).map((line) => (_jsx(Text, { children: ` ${line}` }, line))), _jsx(Text, { children: `[ check again ] c ${healthLine(state.hub.healthOk, healthChecking)}` }), _jsx(Text, { children: `[ m ] acknowledge manual install${state.hub.localManual ? " (acknowledged)" : ""}` }), state.errors.localCheckout && _jsx(Text, { color: theme.error, children: state.errors.localCheckout })] }))] })), state.step === "roots" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: "project roots (comma-separated):" }), _jsx(TextInput, { value: rootsText, onChange: (value) => {
|
|
192
|
+
setRootsText(value);
|
|
193
|
+
setState((s) => setRoots(s, value));
|
|
194
|
+
}, focused: true }), state.errors.roots && _jsx(Text, { color: theme.error, children: state.errors.roots })] })), state.step === "review" && (_jsx(Table, { columns: reviewColumns(mainWidth), rows: state.plan, rowKey: (row) => `${row.kind}-${row.file}`, cell: reviewCell, emptyText: "nothing to change" })), state.step === "apply" && (_jsxs(Box, { flexDirection: "column", children: [state.results.map((result, index) => (_jsx(Text, { children: applyResultLine(result) }, `${result.action.kind}-${index}`))), state.results.length < state.plan.length && _jsx(Text, { dimColor: true, children: "applying\u2026" })] })), state.step === "done" && (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: `setup complete: ${state.results.length} change(s) applied` }), _jsx(Text, { dimColor: true, children: "enter open dashboard" })] }))] }) }), _jsx(KeyHints, { hints: hints })] }));
|
|
198
195
|
}
|