@kata-sh/cli 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* General-purpose multi-page wizard UI.
|
|
3
|
+
*
|
|
4
|
+
* Supports declarative page definitions with select and text fields.
|
|
5
|
+
* Pages can conditionally route to different next pages based on answers.
|
|
6
|
+
*
|
|
7
|
+
* Navigation:
|
|
8
|
+
* ← go back one page (on page 1: triggers exit confirmation)
|
|
9
|
+
* → / Enter advance to next page (or submit on last page)
|
|
10
|
+
* Escape triggers exit confirmation overlay
|
|
11
|
+
*
|
|
12
|
+
* Exit confirmation (shown on Escape or ← from page 1):
|
|
13
|
+
* 1. Go back — dismiss and return to current page
|
|
14
|
+
* 2. Exit — cancel the wizard, returns null to caller
|
|
15
|
+
*
|
|
16
|
+
* Returns:
|
|
17
|
+
* Record<pageId, Record<fieldId, string | string[]>> on completion
|
|
18
|
+
* null on exit/cancel
|
|
19
|
+
*
|
|
20
|
+
* Example:
|
|
21
|
+
*
|
|
22
|
+
* const result = await showWizard(ctx, {
|
|
23
|
+
* title: "New Project",
|
|
24
|
+
* pages: [
|
|
25
|
+
* {
|
|
26
|
+
* id: "mode",
|
|
27
|
+
* fields: [
|
|
28
|
+
* {
|
|
29
|
+
* type: "select",
|
|
30
|
+
* id: "start_type",
|
|
31
|
+
* question: "How do you want to start?",
|
|
32
|
+
* options: [
|
|
33
|
+
* { label: "Describe it", description: "Type what you want to build." },
|
|
34
|
+
* { label: "Provide a file", description: "Point to an existing doc." },
|
|
35
|
+
* ],
|
|
36
|
+
* },
|
|
37
|
+
* ],
|
|
38
|
+
* next: (answers) =>
|
|
39
|
+
* answers["mode"]?.["start_type"] === "Provide a file" ? "file_path" : null,
|
|
40
|
+
* },
|
|
41
|
+
* {
|
|
42
|
+
* id: "file_path",
|
|
43
|
+
* fields: [
|
|
44
|
+
* { type: "text", id: "path", label: "File path", placeholder: "/path/to/doc.md" },
|
|
45
|
+
* ],
|
|
46
|
+
* next: () => null,
|
|
47
|
+
* },
|
|
48
|
+
* ],
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* if (!result) return; // user exited
|
|
52
|
+
* const startType = result["mode"]["start_type"]; // "Describe it" | "Provide a file"
|
|
53
|
+
* const filePath = result["file_path"]?.["path"];
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import type { ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
|
|
57
|
+
import { type Theme } from "@mariozechner/pi-coding-agent";
|
|
58
|
+
import {
|
|
59
|
+
Editor,
|
|
60
|
+
Key,
|
|
61
|
+
matchesKey,
|
|
62
|
+
truncateToWidth,
|
|
63
|
+
type TUI,
|
|
64
|
+
} from "@mariozechner/pi-tui";
|
|
65
|
+
import { makeUI } from "./ui.js";
|
|
66
|
+
|
|
67
|
+
// ─── Public types ─────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export interface WizardOption {
|
|
70
|
+
label: string;
|
|
71
|
+
description: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SelectField {
|
|
75
|
+
type: "select";
|
|
76
|
+
id: string;
|
|
77
|
+
question: string;
|
|
78
|
+
options: WizardOption[];
|
|
79
|
+
/** Allow multiple selections. Default: false. */
|
|
80
|
+
allowMultiple?: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface TextField {
|
|
84
|
+
type: "text";
|
|
85
|
+
id: string;
|
|
86
|
+
label: string;
|
|
87
|
+
placeholder?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type WizardField = SelectField | TextField;
|
|
91
|
+
|
|
92
|
+
/** Answers collected so far: pageId → fieldId → value */
|
|
93
|
+
export type WizardAnswers = Record<string, Record<string, string | string[]>>;
|
|
94
|
+
|
|
95
|
+
export interface WizardPage {
|
|
96
|
+
id: string;
|
|
97
|
+
/** Optional subtitle shown below the wizard title for this page. */
|
|
98
|
+
subtitle?: string;
|
|
99
|
+
fields: WizardField[];
|
|
100
|
+
/**
|
|
101
|
+
* Return the id of the next page, or null to end the wizard.
|
|
102
|
+
* Called with all answers collected so far when the user advances.
|
|
103
|
+
* If omitted, the wizard ends after this page.
|
|
104
|
+
*/
|
|
105
|
+
next?: (answers: WizardAnswers) => string | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface WizardOptions {
|
|
109
|
+
/** Title shown at the top of every page. */
|
|
110
|
+
title: string;
|
|
111
|
+
/** Ordered page definitions. Pages are navigated in order unless next() routes elsewhere. */
|
|
112
|
+
pages: WizardPage[];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Internal state ───────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
interface SelectState {
|
|
118
|
+
cursorIndex: number;
|
|
119
|
+
/** Single-select: committed option index, null if not yet chosen */
|
|
120
|
+
committedIndex: number | null;
|
|
121
|
+
/** Multi-select: which indices are checked */
|
|
122
|
+
checkedIndices: Set<number>;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface PageState {
|
|
126
|
+
selectStates: Map<string, SelectState>;
|
|
127
|
+
textValues: Map<string, string>;
|
|
128
|
+
/** Which field is focused (for text fields) */
|
|
129
|
+
focusedFieldId: string | null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Main export ──────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Show a multi-page wizard and return collected answers, or null if the user exits.
|
|
136
|
+
*/
|
|
137
|
+
export async function showWizard(
|
|
138
|
+
ctx: ExtensionCommandContext,
|
|
139
|
+
opts: WizardOptions,
|
|
140
|
+
): Promise<WizardAnswers | null> {
|
|
141
|
+
const pageMap = new Map<string, WizardPage>(opts.pages.map((p) => [p.id, p]));
|
|
142
|
+
|
|
143
|
+
return ctx.ui.custom<WizardAnswers | null>((tui: TUI, theme: Theme, _kb, done) => {
|
|
144
|
+
// ── State ──────────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
/** Stack of page ids visited — drives back navigation */
|
|
147
|
+
const pageStack: string[] = [opts.pages[0].id];
|
|
148
|
+
const pageStates = new Map<string, PageState>();
|
|
149
|
+
/** Collected answers across all pages */
|
|
150
|
+
const answers: WizardAnswers = {};
|
|
151
|
+
/** Whether the exit-confirmation overlay is showing */
|
|
152
|
+
let showingExitConfirm = false;
|
|
153
|
+
/** Cursor in the exit-confirm overlay: 0 = go back, 1 = exit */
|
|
154
|
+
let exitCursor = 0;
|
|
155
|
+
|
|
156
|
+
let cachedLines: string[] | undefined;
|
|
157
|
+
|
|
158
|
+
// Editors keyed by fieldId — one per text field
|
|
159
|
+
// editorTheme is derived from the design system at first render
|
|
160
|
+
const editors = new Map<string, Editor>();
|
|
161
|
+
let resolvedEditorTheme: import("@mariozechner/pi-tui").EditorTheme | null = null;
|
|
162
|
+
|
|
163
|
+
function getEditor(fieldId: string): Editor {
|
|
164
|
+
if (!resolvedEditorTheme) resolvedEditorTheme = makeUI(theme, 80).editorTheme;
|
|
165
|
+
if (!editors.has(fieldId)) editors.set(fieldId, new Editor(tui, resolvedEditorTheme));
|
|
166
|
+
return editors.get(fieldId)!;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── Page state helpers ─────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
function getPageState(pageId: string): PageState {
|
|
172
|
+
if (!pageStates.has(pageId)) {
|
|
173
|
+
pageStates.set(pageId, {
|
|
174
|
+
selectStates: new Map(),
|
|
175
|
+
textValues: new Map(),
|
|
176
|
+
focusedFieldId: null,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return pageStates.get(pageId)!;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getSelectState(pageId: string, fieldId: string, _optCount: number): SelectState {
|
|
183
|
+
const ps = getPageState(pageId);
|
|
184
|
+
if (!ps.selectStates.has(fieldId)) {
|
|
185
|
+
ps.selectStates.set(fieldId, {
|
|
186
|
+
cursorIndex: 0,
|
|
187
|
+
committedIndex: null, // nothing pre-committed — user must explicitly confirm
|
|
188
|
+
checkedIndices: new Set(),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return ps.selectStates.get(fieldId)!;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Current page ───────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
function currentPageId(): string {
|
|
197
|
+
return pageStack[pageStack.length - 1];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function currentPage(): WizardPage {
|
|
201
|
+
return pageMap.get(currentPageId())!;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function currentPageState(): PageState {
|
|
205
|
+
return getPageState(currentPageId());
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ── Validation ─────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
function isPageComplete(page: WizardPage, ps: PageState): boolean {
|
|
211
|
+
for (const field of page.fields) {
|
|
212
|
+
if (field.type === "select") {
|
|
213
|
+
const ss = ps.selectStates.get(field.id);
|
|
214
|
+
if (!ss) return false;
|
|
215
|
+
if (field.allowMultiple) {
|
|
216
|
+
if (ss.checkedIndices.size === 0) return false;
|
|
217
|
+
} else {
|
|
218
|
+
if (ss.committedIndex === null) return false;
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
const val = ps.textValues.get(field.id) ?? "";
|
|
222
|
+
if (!val.trim()) return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Collect answers for a page ─────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
function collectPageAnswers(page: WizardPage, ps: PageState): Record<string, string | string[]> {
|
|
231
|
+
const result: Record<string, string | string[]> = {};
|
|
232
|
+
for (const field of page.fields) {
|
|
233
|
+
if (field.type === "select") {
|
|
234
|
+
const ss = ps.selectStates.get(field.id);
|
|
235
|
+
if (!ss) continue;
|
|
236
|
+
if (field.allowMultiple) {
|
|
237
|
+
result[field.id] = Array.from(ss.checkedIndices)
|
|
238
|
+
.sort((a, b) => a - b)
|
|
239
|
+
.map((i) => field.options[i].label);
|
|
240
|
+
} else {
|
|
241
|
+
if (ss.committedIndex !== null && ss.committedIndex < field.options.length) {
|
|
242
|
+
result[field.id] = field.options[ss.committedIndex].label;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
result[field.id] = ps.textValues.get(field.id) ?? "";
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return result;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Auto-focus helper ──────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
/** If a page's first field is a text field, focus it immediately on arrival. */
|
|
255
|
+
function autoFocusPageIfText(pageId: string) {
|
|
256
|
+
const page = pageMap.get(pageId);
|
|
257
|
+
if (!page) return;
|
|
258
|
+
const firstField = page.fields[0];
|
|
259
|
+
if (firstField?.type === "text") {
|
|
260
|
+
const ps = getPageState(pageId);
|
|
261
|
+
ps.focusedFieldId = firstField.id;
|
|
262
|
+
const editor = getEditor(firstField.id);
|
|
263
|
+
editor.setText(ps.textValues.get(firstField.id) ?? "");
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Auto-focus the first page if it starts with a text field
|
|
268
|
+
autoFocusPageIfText(opts.pages[0].id);
|
|
269
|
+
|
|
270
|
+
// ── Navigation ─────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
function advance() {
|
|
273
|
+
const page = currentPage();
|
|
274
|
+
const ps = currentPageState();
|
|
275
|
+
if (!isPageComplete(page, ps)) {
|
|
276
|
+
refresh();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Save text field values from editors
|
|
281
|
+
for (const field of page.fields) {
|
|
282
|
+
if (field.type === "text") {
|
|
283
|
+
ps.textValues.set(field.id, getEditor(field.id).getText().trim());
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Collect answers for this page
|
|
288
|
+
answers[page.id] = collectPageAnswers(page, ps);
|
|
289
|
+
|
|
290
|
+
// Route to next page
|
|
291
|
+
const nextId = page.next ? page.next(answers) : null;
|
|
292
|
+
if (!nextId) {
|
|
293
|
+
// End of wizard
|
|
294
|
+
done(answers);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const nextPage = pageMap.get(nextId);
|
|
299
|
+
if (!nextPage) {
|
|
300
|
+
done(answers);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
pageStack.push(nextId);
|
|
305
|
+
autoFocusPageIfText(nextId);
|
|
306
|
+
refresh();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function goBack() {
|
|
310
|
+
if (pageStack.length <= 1) {
|
|
311
|
+
// Already at first page — Esc here means exit
|
|
312
|
+
showingExitConfirm = true;
|
|
313
|
+
exitCursor = 0;
|
|
314
|
+
refresh();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
pageStack.pop();
|
|
318
|
+
autoFocusPageIfText(currentPageId());
|
|
319
|
+
refresh();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function refresh() {
|
|
323
|
+
cachedLines = undefined;
|
|
324
|
+
tui.requestRender();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Input handler ──────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
function handleInput(data: string) {
|
|
330
|
+
// ── Exit confirm overlay ─────────────────────────────────────────
|
|
331
|
+
if (showingExitConfirm) {
|
|
332
|
+
if (matchesKey(data, Key.up)) { exitCursor = 0; refresh(); return; }
|
|
333
|
+
if (matchesKey(data, Key.down)) { exitCursor = 1; refresh(); return; }
|
|
334
|
+
if (data === "1") { showingExitConfirm = false; refresh(); return; }
|
|
335
|
+
if (data === "2") { done(null); return; }
|
|
336
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
|
|
337
|
+
if (exitCursor === 0) { showingExitConfirm = false; refresh(); }
|
|
338
|
+
else { done(null); }
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
// Esc on the confirm screen = go back (dismiss confirm)
|
|
342
|
+
if (matchesKey(data, Key.escape)) { showingExitConfirm = false; refresh(); return; }
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── Text field focus ─────────────────────────────────────────────
|
|
347
|
+
const ps = currentPageState();
|
|
348
|
+
if (ps.focusedFieldId) {
|
|
349
|
+
const editor = getEditor(ps.focusedFieldId);
|
|
350
|
+
if (matchesKey(data, Key.escape)) {
|
|
351
|
+
// First Esc: unfocus the text field
|
|
352
|
+
ps.textValues.set(ps.focusedFieldId, editor.getText().trim());
|
|
353
|
+
ps.focusedFieldId = null;
|
|
354
|
+
refresh();
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (matchesKey(data, Key.enter)) {
|
|
358
|
+
ps.textValues.set(ps.focusedFieldId, editor.getText().trim());
|
|
359
|
+
ps.focusedFieldId = null;
|
|
360
|
+
advance();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
editor.handleInput(data);
|
|
364
|
+
refresh();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// ── Esc with no text field focused: go back (or exit if on page 1) ──
|
|
369
|
+
if (matchesKey(data, Key.escape)) { goBack(); return; }
|
|
370
|
+
|
|
371
|
+
// ── Enter / → to advance ─────────────────────────────────────────
|
|
372
|
+
if (matchesKey(data, Key.enter) || matchesKey(data, Key.right)) {
|
|
373
|
+
// For single-select fields, commit cursor before advancing
|
|
374
|
+
const page = currentPage();
|
|
375
|
+
for (const field of page.fields) {
|
|
376
|
+
if (field.type === "select" && !field.allowMultiple) {
|
|
377
|
+
const ss = getSelectState(currentPageId(), field.id, field.options.length);
|
|
378
|
+
if (ss.committedIndex === null) ss.committedIndex = ss.cursorIndex;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
advance();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ── Select field interactions ────────────────────────────────────
|
|
386
|
+
const page = currentPage();
|
|
387
|
+
for (const field of page.fields) {
|
|
388
|
+
if (field.type !== "select") continue;
|
|
389
|
+
const ss = getSelectState(currentPageId(), field.id, field.options.length);
|
|
390
|
+
const totalOpts = field.options.length;
|
|
391
|
+
|
|
392
|
+
if (matchesKey(data, Key.up)) {
|
|
393
|
+
ss.cursorIndex = (ss.cursorIndex - 1 + totalOpts) % totalOpts;
|
|
394
|
+
refresh(); return;
|
|
395
|
+
}
|
|
396
|
+
if (matchesKey(data, Key.down)) {
|
|
397
|
+
ss.cursorIndex = (ss.cursorIndex + 1) % totalOpts;
|
|
398
|
+
refresh(); return;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (field.allowMultiple) {
|
|
402
|
+
if (matchesKey(data, Key.space)) {
|
|
403
|
+
if (ss.checkedIndices.has(ss.cursorIndex)) ss.checkedIndices.delete(ss.cursorIndex);
|
|
404
|
+
else ss.checkedIndices.add(ss.cursorIndex);
|
|
405
|
+
refresh(); return;
|
|
406
|
+
}
|
|
407
|
+
} else {
|
|
408
|
+
// Numeric shortcut: press the number to select and immediately advance
|
|
409
|
+
if (data.length === 1 && data >= "1" && data <= "9") {
|
|
410
|
+
const idx = parseInt(data, 10) - 1;
|
|
411
|
+
if (idx < totalOpts) {
|
|
412
|
+
ss.cursorIndex = idx;
|
|
413
|
+
ss.committedIndex = idx;
|
|
414
|
+
advance();
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Enter/Space commit cursor and advance (Enter handled above, Space here)
|
|
419
|
+
if (matchesKey(data, Key.space)) {
|
|
420
|
+
ss.committedIndex = ss.cursorIndex;
|
|
421
|
+
advance();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Only handle the first select field for nav
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// ── Render ─────────────────────────────────────────────────────────────
|
|
431
|
+
|
|
432
|
+
function renderExitConfirm(width: number): string[] {
|
|
433
|
+
const ui = makeUI(theme, width);
|
|
434
|
+
const lines: string[] = [];
|
|
435
|
+
const push = (...rows: string[][]) => { for (const r of rows) lines.push(...r); };
|
|
436
|
+
|
|
437
|
+
push(
|
|
438
|
+
ui.bar(), ui.blank(),
|
|
439
|
+
ui.header(" Exit wizard?"),
|
|
440
|
+
ui.blank(),
|
|
441
|
+
ui.subtitle(" Your progress will be lost."),
|
|
442
|
+
ui.blank(),
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
if (exitCursor === 0) push(ui.actionSelected(1, "Go back", "Return to where you were."));
|
|
446
|
+
else push(ui.actionUnselected(1, "Go back", "Return to where you were."));
|
|
447
|
+
push(ui.blank());
|
|
448
|
+
if (exitCursor === 1) push(ui.actionSelected(2, "Exit", "Cancel and discard all answers."));
|
|
449
|
+
else push(ui.actionUnselected(2, "Exit", "Cancel and discard all answers."));
|
|
450
|
+
push(
|
|
451
|
+
ui.blank(),
|
|
452
|
+
ui.hints(["↑/↓ to choose", "1/2 to quick-select", "enter to confirm"]),
|
|
453
|
+
ui.bar(),
|
|
454
|
+
);
|
|
455
|
+
return lines;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function renderSelectField(ui: ReturnType<typeof makeUI>, field: SelectField, lines: string[]) {
|
|
459
|
+
const push = (...rows: string[][]) => { for (const r of rows) lines.push(...r); };
|
|
460
|
+
const ss = getSelectState(currentPageId(), field.id, field.options.length);
|
|
461
|
+
const multi = !!field.allowMultiple;
|
|
462
|
+
|
|
463
|
+
push(ui.question(` ${field.question}`));
|
|
464
|
+
if (multi) push(ui.meta(" (select all that apply — space to toggle, enter to confirm)"));
|
|
465
|
+
push(ui.blank());
|
|
466
|
+
|
|
467
|
+
for (let i = 0; i < field.options.length; i++) {
|
|
468
|
+
const opt = field.options[i];
|
|
469
|
+
const isCursor = i === ss.cursorIndex;
|
|
470
|
+
const isCommitted = i === ss.committedIndex;
|
|
471
|
+
|
|
472
|
+
if (multi) {
|
|
473
|
+
const isChecked = ss.checkedIndices.has(i);
|
|
474
|
+
if (isCursor) push(ui.checkboxSelected(opt.label, opt.description, isChecked));
|
|
475
|
+
else push(ui.checkboxUnselected(opt.label, opt.description, isChecked));
|
|
476
|
+
} else {
|
|
477
|
+
if (isCursor) push(ui.optionSelected(i + 1, opt.label, opt.description, isCommitted));
|
|
478
|
+
else push(ui.optionUnselected(i + 1, opt.label, opt.description, { isCommitted }));
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function renderTextField(ui: ReturnType<typeof makeUI>, field: TextField, ps: PageState, lines: string[], width: number) {
|
|
484
|
+
const push = (...rows: string[][]) => { for (const r of rows) lines.push(...r); };
|
|
485
|
+
const isFocused = ps.focusedFieldId === field.id;
|
|
486
|
+
const value = isFocused ? getEditor(field.id).getText() : (ps.textValues.get(field.id) ?? "");
|
|
487
|
+
|
|
488
|
+
push(ui.question(` ${field.label}`), ui.blank());
|
|
489
|
+
|
|
490
|
+
if (isFocused) {
|
|
491
|
+
for (const line of getEditor(field.id).render(width - 2)) lines.push(truncateToWidth(` ${line}`, width));
|
|
492
|
+
} else if (value) {
|
|
493
|
+
push(ui.answer(` ${value}`));
|
|
494
|
+
} else if (field.placeholder) {
|
|
495
|
+
push(ui.meta(` ${field.placeholder}`));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function render(width: number): string[] {
|
|
500
|
+
if (cachedLines) return cachedLines;
|
|
501
|
+
if (showingExitConfirm) { cachedLines = renderExitConfirm(width); return cachedLines; }
|
|
502
|
+
|
|
503
|
+
const ui = makeUI(theme, width);
|
|
504
|
+
const lines: string[] = [];
|
|
505
|
+
const push = (...rows: string[][]) => { for (const r of rows) lines.push(...r); };
|
|
506
|
+
|
|
507
|
+
push(ui.bar(), ui.header(` ${opts.title}`));
|
|
508
|
+
|
|
509
|
+
// ── Page indicator ────────────────────────────────────────────────
|
|
510
|
+
if (opts.pages.length > 1) {
|
|
511
|
+
push(ui.pageDots(opts.pages.length, pageStack.length - 1));
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// ── Page content ──────────────────────────────────────────────────
|
|
515
|
+
const page = currentPage();
|
|
516
|
+
const ps = currentPageState();
|
|
517
|
+
|
|
518
|
+
if (page.subtitle) { push(ui.blank(), ui.subtitle(` ${page.subtitle}`)); }
|
|
519
|
+
push(ui.blank());
|
|
520
|
+
|
|
521
|
+
for (const field of page.fields) {
|
|
522
|
+
if (field.type === "select") renderSelectField(ui, field, lines);
|
|
523
|
+
else renderTextField(ui, field, ps, lines, width);
|
|
524
|
+
push(ui.blank());
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ── Footer hints ──────────────────────────────────────────────────
|
|
528
|
+
const isFirst = pageStack.length === 1;
|
|
529
|
+
const ps2 = currentPageState();
|
|
530
|
+
const hints: string[] = [];
|
|
531
|
+
if (ps2.focusedFieldId) {
|
|
532
|
+
hints.push("enter to continue");
|
|
533
|
+
hints.push("esc to unfocus");
|
|
534
|
+
} else {
|
|
535
|
+
hints.push("↑/↓ to move");
|
|
536
|
+
hints.push("enter to select");
|
|
537
|
+
hints.push(!isFirst ? "esc to go back" : "esc to exit");
|
|
538
|
+
}
|
|
539
|
+
push(ui.hints(hints), ui.bar());
|
|
540
|
+
|
|
541
|
+
cachedLines = lines;
|
|
542
|
+
return lines;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
render,
|
|
547
|
+
invalidate: () => { cachedLines = undefined; },
|
|
548
|
+
handleInput,
|
|
549
|
+
};
|
|
550
|
+
});
|
|
551
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionCommandContext,
|
|
4
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5
|
+
|
|
6
|
+
export default function auditCommand(pi: ExtensionAPI) {
|
|
7
|
+
pi.registerCommand("audit", {
|
|
8
|
+
description:
|
|
9
|
+
"Audit the current codebase against a specific goal and write a structured report to .kata/audits/",
|
|
10
|
+
async handler(args: string, ctx: ExtensionCommandContext) {
|
|
11
|
+
// ── Step 1: Get the audit goal ────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
let goal = (typeof args === "string" ? args : "").trim();
|
|
14
|
+
|
|
15
|
+
if (!goal) {
|
|
16
|
+
const input = await ctx.ui.input(
|
|
17
|
+
"What is the audit goal?",
|
|
18
|
+
"e.g. understand performance bottlenecks before planning a roadmap",
|
|
19
|
+
);
|
|
20
|
+
if (!input?.trim()) {
|
|
21
|
+
ctx.ui.notify("audit: No goal provided — cancelled.", "error");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
goal = input.trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Step 2: Build output path (.kata/audits/<timestamp>-<slug>.md) ────
|
|
28
|
+
|
|
29
|
+
const now = new Date();
|
|
30
|
+
const timestamp = now
|
|
31
|
+
.toISOString()
|
|
32
|
+
.replace(/T/, "-")
|
|
33
|
+
.replace(/:/g, "")
|
|
34
|
+
.replace(/\..+/, "");
|
|
35
|
+
|
|
36
|
+
const slug = goal
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
39
|
+
.replace(/^-+|-+$/g, "")
|
|
40
|
+
.slice(0, 40);
|
|
41
|
+
|
|
42
|
+
const outputPath = `.kata/audits/${timestamp}-${slug}.md`;
|
|
43
|
+
|
|
44
|
+
// ── Step 3: Ensure the output directory exists ───────────────────────
|
|
45
|
+
|
|
46
|
+
await pi.exec("mkdir", ["-p", ".kata/audits"]);
|
|
47
|
+
|
|
48
|
+
// ── Step 4: Send the audit prompt to the agent ───────────────────────
|
|
49
|
+
|
|
50
|
+
const prompt = `You are conducting a codebase audit. This is a **read-only recce** — you will explore the codebase deeply and produce a structured report. You must NOT edit any code or create any files other than the audit report itself.
|
|
51
|
+
|
|
52
|
+
## Audit Goal
|
|
53
|
+
${goal}
|
|
54
|
+
|
|
55
|
+
## Your Task
|
|
56
|
+
|
|
57
|
+
1. **Discover the codebase** — explore the project structure, key files, configuration, dependencies, and architecture. Use \`find\`, \`ls\`, \`cat\`, \`grep\`, and \`read\` freely. Read as deeply as needed to form a thorough opinion relative to the goal.
|
|
58
|
+
|
|
59
|
+
2. **Analyse against the goal** — evaluate the codebase specifically through the lens of the audit goal. What already exists? What works well? What's missing, weak, or risky?
|
|
60
|
+
|
|
61
|
+
3. **Write the audit report** to \`${outputPath}\` using exactly this markdown template:
|
|
62
|
+
|
|
63
|
+
\`\`\`markdown
|
|
64
|
+
# Audit: ${goal}
|
|
65
|
+
|
|
66
|
+
**Date:** ${now.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}
|
|
67
|
+
**Goal:** ${goal}
|
|
68
|
+
**Codebase:** [detected project name / root path]
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Strengths
|
|
73
|
+
<!-- What already exists that supports this goal? What's solid? -->
|
|
74
|
+
|
|
75
|
+
## Gaps
|
|
76
|
+
<!-- What's missing, incomplete, or problematic relative to this goal? Be specific: file paths, patterns, missing abstractions. -->
|
|
77
|
+
|
|
78
|
+
## Next Steps
|
|
79
|
+
<!-- Concrete, prioritised actions. These should be directly usable as input to /kata-roadmap. -->
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
*Generated by /audit — read-only recce, no code was modified.*
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
After writing the file, confirm with: "✅ Audit complete — report saved to \`${outputPath}\`"`;
|
|
87
|
+
|
|
88
|
+
ctx.ui.notify(`Starting audit: "${goal}"`, "info");
|
|
89
|
+
pi.sendUserMessage(prompt);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|