@compaction/cli 0.1.2 → 0.1.4

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 CHANGED
@@ -66,7 +66,7 @@ npx @compaction/cli --help # no install at all
66
66
  ```
67
67
 
68
68
  The installed binary is `compaction`. After a global install, sanity-check with
69
- `compaction --version` (→ `0.1.2`) and `compaction --help`.
69
+ `compaction --version` (→ `0.1.4`) and `compaction --help`.
70
70
 
71
71
  ### curl one-liner
72
72
 
@@ -2,6 +2,7 @@ import path from "node:path";
2
2
  import chalk from "chalk";
3
3
  import { Option } from "commander";
4
4
  import { writeJsonArtifact, writeTextArtifact } from "../../core/artifact-writer.js";
5
+ import { withTraceFingerprint } from "../../core/report-generator.js";
5
6
  import { describeTokenAccounting } from "../../core/token-accounting.js";
6
7
  import { parseTraceFile } from "../../core/trace-parser.js";
7
8
  import { buildRunLabelsFile, hasAnyLabel } from "../../core/run-labels.js";
@@ -123,6 +124,11 @@ export function registerCompactCommand(program) {
123
124
  // hand-assembled bundle — the eval consumes the identical PolicyMiddlewareResult.
124
125
  const policyResult = applyCompactionPolicy({ trace, compactSkillInjections: approveSkillInjectionPolicy });
125
126
  const artifacts = await writeCompactionArtifacts(trace, options.out, runId, policyResult, undefined, { reviewerType, reviewSummaryPresent, approveSkillInjectionPolicy });
127
+ // Attribution/integrity (V0.2 savings-evidence): re-persist report.json with the
128
+ // content-addressed trace fingerprint so `aggregate`/`summary` can attribute each saving to a
129
+ // verifiable run and de-duplicate the same captured run if it is rolled up twice. Additive,
130
+ // backward-compatible; does not change any existing report field/label.
131
+ await writeJsonArtifact(options.out, "report.json", withTraceFingerprint(artifacts.report, trace));
126
132
  console.log(chalk.cyan("compaction compact"));
127
133
  console.log(artifacts.consoleReport);
128
134
  // Operator-visible only when the approval gesture was given; default output is unchanged.
@@ -1,216 +1,243 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { readFile } from "node:fs/promises";
3
+ import { fileURLToPath } from "node:url";
4
+ import path from "node:path";
2
5
  import chalk from "chalk";
3
6
  import { defaultProjectsDir, discoverClaudeCodeSessions } from "../../core/adapters/claude-code-discovery.js";
7
+ import { BRAND_HEX, VALUE_PROMISE, STATUS_PILLS, PRIMARY, SECONDARY, FREE_FLOW, HONESTY_LINES, FOOTER_LINES, ALL_WORKFLOW_KEYS } from "../onboarding/model.js";
8
+ import { decideInteractiveTuiFromProcess } from "../onboarding/should-use-tui.js";
4
9
  /**
5
10
  * `compaction init` — first-run, value-led terminal onboarding.
6
11
  *
7
- * Converts "I installed a CLI" into "I am one command away from seeing my own
8
- * workflow's context waste." It is guidance plus ONE read-only local detection
9
- * (does a Claude Code projects directory exist, and how many sessions are in it).
12
+ * In a real interactive terminal this launches a graphical Ink TUI (a workflow
13
+ * chooser). Everywhere else pipes, CI, NO_COLOR, dumb terminals, the test
14
+ * runner, `--static`, or `--path <focus>` it prints the byte-stable static
15
+ * screen. Both surfaces share ONE model (`../onboarding/model`) so they always
16
+ * feature the same workflows, the same free follow-ups, and the same honest
17
+ * claim labels.
10
18
  *
11
- * Invariants (non-negotiable, hold on every path):
19
+ * Invariants (non-negotiable, hold on EVERY path — static and TUI):
12
20
  * - writes nothing
13
21
  * - makes no network call
14
22
  * - reads no provider credential
15
23
  * - needs no repo access
16
- * A provider credential is ONLY ever used if the operator explicitly runs
17
- * `capture provider-usage` (or enters operator-run measurement records).
24
+ * The TUI is a chooser, not an agent: no text input, no model call, no upload.
18
25
  *
19
- * Proprietary boundary (per mvp-plan.md §13): output names high-level waste
20
- * CATEGORIES and policy-FAMILY names plus before/after deltas, risk, safety,
21
- * approval requirement, artifacts, and the next action never the proprietary
22
- * scoring formulas, thresholds, hidden heuristics, future-policy internals, or
23
- * commercial / moat logic.
26
+ * Free-CLI scope: onboarding features ONLY the free, local-first commands
27
+ * (capture / import / analyze / spend / summary / feedback). The optimization &
28
+ * verification engine commands (recommend / compact / inspect / approve /
29
+ * apply-context) are the opt-in Compaction API NOT in the free CLI — and so
30
+ * are deliberately NOT featured here.
24
31
  *
25
- * Presentation note: this is a calm, structured, minimal first-run surface.
26
- * Hierarchy comes from spacing, dim secondary text, and a few restrained
27
- * symbols not from a wall of enumerated text.
32
+ * Honesty rails: token counts are provider-reported when usage metadata is
33
+ * present, otherwise a labeled local estimate (chars/4); cost is a price-table
34
+ * estimate; nothing is called a saving until it is measured. No billing-confirmed,
35
+ * no semantic-preservation, no hosted-API-live, no auto-apply, no demo-first.
28
36
  */
29
- // Clean one-line wordmark. The hand-drawn ASCII banner read busy at small
30
- // terminal sizes; a spaced, bold mark with a rule under it reads premium.
31
- const MARK = chalk.bold("compaction");
37
+ // Brand accent. chalk.hex degrades gracefully: on a non-color / non-truecolor
38
+ // terminal chalk down-samples or no-ops, so the text is never lost.
39
+ const ACCENT = chalk.hex(BRAND_HEX);
40
+ // Spaced wordmark reads as a premium brand mark at any terminal width.
41
+ const WORDMARK = ACCENT.bold("C O M P A C T I O N");
32
42
  const RULE = chalk.dim("─".repeat(58));
33
- const HEADLINE = "Context optimization for AI agents";
34
- const VALUE_PROP = chalk.dim("Reduce avoidable input/output tokens while preserving task-critical\n context. Local-first — nothing is applied without your approval.");
35
- // The value loop, compressed to one line per step. Each step earns its place.
36
- const LOOP = [
37
- ["capture", "a real trace from your own agent workflow"],
38
- ["analyze", "where token spend and avoidable context waste are"],
39
- ["recommend", "a candidate policy family + estimated before/after delta"],
40
- ["compact", "the before/after token delta, and which family produced it"],
41
- ["inspect", "safety evidence — source pointers, hashes, recoverable originals"],
42
- ["approve", "emit a compacted context for your next call. No auto-apply."]
43
- ];
44
- // Real-workflow paths only. compaction operates on the user's OWN agent
45
- // workflow; there is no demo / synthetic first-value path here. Demo fixtures
46
- // exist solely for tests, package smoke, and explicitly-labeled docs.
47
- const PRIMARY = {
48
- key: "claude-code",
49
- title: "Claude Code",
50
- command: "compaction capture claude-code --discover"
51
- };
52
- const SECONDARY = [
53
- {
54
- key: "codex",
55
- title: "Codex",
56
- command: "codex exec --json '<task>' > run.jsonl && compaction import run.jsonl --source codex-exec-jsonl --out ./my-trace"
57
- },
58
- {
59
- key: "openai-agents",
60
- title: "OpenAI Agents",
61
- command: "compaction capture openai-agents --out ./my-trace -- <your-command>"
62
- },
63
- {
64
- key: "import",
65
- title: "Local trace",
66
- command: "compaction import --list-sources # then: import <file> --source <source> --out ./my-trace"
43
+ async function readPackageVersion() {
44
+ try {
45
+ const here = path.dirname(fileURLToPath(import.meta.url));
46
+ // dist/cli/commands -> dist -> package root
47
+ const pkgPath = path.resolve(here, "..", "..", "..", "package.json");
48
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
49
+ return typeof pkg.version === "string" ? pkg.version : "unknown";
67
50
  }
68
- ];
69
- // The follow-up sequence is identical for every path once a trace exists.
70
- const FOLLOW_UP = [
71
- "compaction analyze ./my-trace/captured-trace.json",
72
- "compaction recommend ./my-trace/captured-trace.json",
73
- "compaction compact ./my-trace/captured-trace.json --out ./my-trace/compacted",
74
- "compaction apply-context ./my-trace/captured-trace.json --out ./my-trace/apply"
75
- ];
51
+ catch {
52
+ return "unknown";
53
+ }
54
+ }
55
+ async function detectClaudeCode(projectsDir) {
56
+ let claudeDetected = false;
57
+ let sessionCount = 0;
58
+ if (existsSync(projectsDir)) {
59
+ try {
60
+ const discovery = await discoverClaudeCodeSessions({ projectsDir });
61
+ sessionCount = discovery.sessions.length;
62
+ claudeDetected = sessionCount > 0;
63
+ }
64
+ catch {
65
+ claudeDetected = false; // detection is best-effort; onboarding never fails on it
66
+ }
67
+ }
68
+ return { claudeDetected, sessionCount, projectsDir };
69
+ }
70
+ // ---------------------------------------------------------------------------
71
+ // Static screen (the universal fallback). Output is intentionally byte-stable:
72
+ // scripts, snapshots, and smoke tests depend on it.
73
+ // ---------------------------------------------------------------------------
76
74
  function header(text) {
77
75
  return chalk.bold(text);
78
76
  }
79
- function loopBlock() {
80
- const width = Math.max(...LOOP.map(([verb]) => verb.length));
81
- return LOOP.map(([verb, desc]) => ` ${chalk.cyan(verb.padEnd(width))} ${chalk.dim(desc)}`);
82
- }
83
77
  function primaryBlock(sessionCount) {
84
- const tag = chalk.green(` ✓ ${sessionCount} session(s) found`);
78
+ const tag = chalk.green(` ✓ ${sessionCount} session(s) found`);
85
79
  return [
86
- `${chalk.bold("▸ " + PRIMARY.title)}${chalk.dim(" · recommended")}${tag}`,
87
- ` ${chalk.cyan(PRIMARY.command)}`
80
+ `${chalk.bold("▸ " + PRIMARY.title)}${chalk.dim(" · recommended first run")}${tag}`,
81
+ ` ${ACCENT(PRIMARY.command)}`
88
82
  ];
89
83
  }
90
- function secondaryBlock() {
91
- const lines = [chalk.dim("Other workflows")];
92
- for (const p of SECONDARY) {
93
- lines.push(` ${chalk.dim(p.title)}`);
94
- lines.push(` ${chalk.cyan(p.command)}`);
95
- }
96
- return lines;
97
- }
98
84
  function emptyStateBlock(projectsDir) {
99
- // Calm, premium no-sessions state: explain, show how to create one, and give
100
- // the exact rerun command. The secondary real paths follow below.
101
85
  return [
102
- `${chalk.bold("▸ " + PRIMARY.title)}${chalk.dim(" · recommended")}`,
86
+ `${chalk.bold("▸ " + PRIMARY.title)}${chalk.dim(" · recommended first run")}`,
103
87
  chalk.dim(` No sessions found under ${projectsDir}.`),
104
88
  chalk.dim(" Start or run a Claude Code session in any project, then re-run:"),
105
- ` ${chalk.cyan("compaction capture claude-code --discover")}`
89
+ ` ${ACCENT("compaction capture claude-code --discover")}`
106
90
  ];
107
91
  }
92
+ function supportedBlock() {
93
+ const summary = "Claude Code · OpenAI Agents SDK · Codex / local import · local trace import";
94
+ const lines = [header("Supported workflows"), ` ${chalk.dim(summary)}`, ""];
95
+ for (const p of SECONDARY) {
96
+ lines.push(` ${chalk.bold(p.title)}`);
97
+ lines.push(` ${ACCENT(p.command)}`);
98
+ }
99
+ return lines;
100
+ }
108
101
  function afterCaptureBlock() {
102
+ const width = Math.max(...FREE_FLOW.map((f) => f.command.length));
109
103
  return [
110
104
  header("Then, on the trace you captured"),
111
105
  "",
112
- ...FOLLOW_UP.map((c) => ` ${chalk.cyan(c)}`),
106
+ ...FREE_FLOW.map((f) => ` ${ACCENT(f.command.padEnd(width))} ${chalk.dim(f.desc)}`),
107
+ "",
108
+ chalk.dim(` ${HONESTY_LINES[0]}`),
109
+ chalk.dim(` ${HONESTY_LINES[1]}`),
110
+ chalk.dim(` ${HONESTY_LINES[2]}`),
113
111
  "",
114
- chalk.dim(" You'll see: waste pattern CATEGORY, high-level POLICY FAMILY,"),
115
- chalk.dim(" before/after input/output tokens, the TOKEN-COUNT SOURCE"),
116
- chalk.dim(" (provider-reported vs estimated — never an estimate as reported),"),
117
- chalk.dim(" RISK LEVEL, safety + source_recoverability, the APPROVAL REQUIREMENT"),
118
- chalk.dim(" (--approve-in-workflow-use; no auto-apply), and the next action.")
112
+ chalk.dim(` ${HONESTY_LINES[3]}`),
113
+ chalk.dim(` ${HONESTY_LINES[4]}`)
119
114
  ];
120
115
  }
121
116
  function footerBlock() {
122
- return [
123
- chalk.dim("Local-first: no network, no credentials, no repo access for any of the above."),
124
- chalk.dim("A provider credential is only ever used if YOU explicitly run"),
125
- chalk.dim("`compaction capture provider-usage` or enter operator-run records.")
117
+ return FOOTER_LINES.map((l) => chalk.dim(l));
118
+ }
119
+ function buildStaticScreen(version, focus, det) {
120
+ const lines = [];
121
+ // 1. Wordmark + status + value promise.
122
+ lines.push("");
123
+ lines.push(` ${WORDMARK}`);
124
+ lines.push("");
125
+ lines.push(` ${chalk.dim(`v${version} · ${STATUS_PILLS.join(" · ")}`)}`);
126
+ lines.push("");
127
+ lines.push(` ${VALUE_PROMISE}`);
128
+ lines.push("");
129
+ lines.push(RULE);
130
+ lines.push("");
131
+ // 2. Start here — primary path (Claude Code) or a focused secondary path.
132
+ if (!focus || focus === "claude-code") {
133
+ lines.push(header("Start here"));
134
+ lines.push("");
135
+ if (det.claudeDetected) {
136
+ lines.push(...primaryBlock(det.sessionCount));
137
+ }
138
+ else {
139
+ lines.push(...emptyStateBlock(det.projectsDir));
140
+ }
141
+ lines.push("");
142
+ if (!focus) {
143
+ lines.push(...supportedBlock());
144
+ lines.push("");
145
+ }
146
+ }
147
+ else {
148
+ const p = SECONDARY.find((s) => s.key === focus);
149
+ lines.push(header("Start here"));
150
+ lines.push("");
151
+ lines.push(` ${chalk.bold(p.title)}`);
152
+ lines.push(` ${ACCENT(p.command)}`);
153
+ lines.push("");
154
+ }
155
+ lines.push(RULE);
156
+ lines.push("");
157
+ // 3. The free follow-up sequence + honest labeling + opt-in-API positioning.
158
+ lines.push(...afterCaptureBlock());
159
+ lines.push("");
160
+ lines.push(RULE);
161
+ lines.push("");
162
+ // 4. Local-first footer.
163
+ lines.push(...footerBlock());
164
+ lines.push("");
165
+ return lines.join("\n");
166
+ }
167
+ // After the TUI exits with a selection, print a clean, copy-ready next-command
168
+ // block to the scrollback so the user can paste and run it.
169
+ function printSelection(card) {
170
+ const out = [
171
+ "",
172
+ `${chalk.bold("▸ " + card.title)}`,
173
+ chalk.dim(` ${card.blurb}`),
174
+ "",
175
+ chalk.dim(" Run this next:"),
176
+ ` ${ACCENT(card.command)}`,
177
+ "",
178
+ chalk.dim(` Then: ${FREE_FLOW.map((f) => f.command.replace("compaction ", "")).join(" · ")}`),
179
+ chalk.dim(" Local-first: no network, no credentials, no upload. Nothing is called a"),
180
+ chalk.dim(" saving until it is measured."),
181
+ ""
126
182
  ];
183
+ console.log(out.join("\n"));
127
184
  }
128
185
  export function registerInitCommand(program) {
129
186
  program
130
187
  .command("init")
131
- .description("First-run onboarding: the value loop, how to capture YOUR first real trace, and the exact next commands. " +
132
- "Guidance + one read-only local detection only writes nothing, no network, no credentials.")
133
- .option("--path <path>", "Focus one input path: claude-code | codex | openai-agents | import")
188
+ .description("First-run onboarding: an interactive workflow chooser in a real terminal (TUI), or the " +
189
+ "value promise + exact next commands as a static screen elsewhere. Guidance + one read-only " +
190
+ "local detection only writes nothing, no network, no credentials.")
191
+ .option("--path <path>", "Focus one input path: claude-code | openai-agents | codex | import")
134
192
  .option("--projects-dir <dir>", "Claude Code projects directory to detect against (default: the standard local location)")
193
+ .option("--interactive", "prefer the interactive TUI; if no TTY is available, show the static screen and why")
194
+ .option("--static", "always show the plain static screen (no TUI)")
135
195
  .action(async (options) => {
136
196
  // Validate --path early so the error stays a clean one-liner.
137
- const allKeys = [PRIMARY.key, ...SECONDARY.map((p) => p.key)];
138
- if (options.path && !allKeys.includes(options.path)) {
139
- console.error(`unknown --path '${options.path}'. Valid: ${allKeys.join(" | ")}`);
197
+ if (options.path && !ALL_WORKFLOW_KEYS.includes(options.path)) {
198
+ console.error(`unknown --path '${options.path}'. Valid: ${ALL_WORKFLOW_KEYS.join(" | ")}`);
140
199
  process.exitCode = 1;
141
200
  return;
142
201
  }
202
+ // Version read at runtime so it always matches the installed package.
203
+ const version = await readPackageVersion();
143
204
  // Read-only local detection (no writes, no network).
144
205
  const projectsDir = options.projectsDir ?? defaultProjectsDir();
145
- let claudeDetected = false;
146
- let sessionCount = 0;
147
- if (existsSync(projectsDir)) {
148
- try {
149
- const discovery = await discoverClaudeCodeSessions({ projectsDir });
150
- sessionCount = discovery.sessions.length;
151
- claudeDetected = sessionCount > 0;
152
- }
153
- catch {
154
- claudeDetected = false; // detection is best-effort; onboarding never fails on it
155
- }
156
- }
206
+ const det = await detectClaudeCode(projectsDir);
207
+ // Decide static vs interactive. A focused path (--path) or --static always
208
+ // serves static; --interactive forces the TUI iff a TTY is present;
209
+ // otherwise auto-detect.
210
+ // Note: --interactive cannot conjure a TTY (raw-mode keyboard nav needs
211
+ // one), so it does not override the TTY check; it only documents intent.
212
+ // When asked for but unavailable, we fall back to static with a reason.
157
213
  const focus = options.path;
158
- const lines = [];
159
- // 1. Mark + value prop.
160
- lines.push("");
161
- lines.push(`${MARK} ${chalk.dim("·")} ${HEADLINE}`);
162
- lines.push(` ${VALUE_PROP}`);
163
- lines.push("");
164
- lines.push(RULE);
165
- lines.push("");
166
- // 2. The value loop — what the tool actually does, one line per step.
167
- lines.push(header("How it works"));
168
- lines.push("");
169
- lines.push(...loopBlock());
170
- lines.push("");
171
- lines.push(chalk.dim(" Estimates are labeled as estimates; provider-reported as provider-reported."));
172
- lines.push(chalk.dim(" Nothing is called a saving until it is measured."));
173
- lines.push("");
174
- lines.push(RULE);
175
- lines.push("");
176
- // 3. Start here — primary path (Claude Code) or a focused secondary path.
177
- if (!focus || focus === "claude-code") {
178
- lines.push(header("Start here"));
179
- lines.push("");
180
- if (claudeDetected) {
181
- lines.push(...primaryBlock(sessionCount));
214
+ const ttyDecision = decideInteractiveTuiFromProcess();
215
+ const wantTui = !focus && !options.static && ttyDecision.useTui;
216
+ if (wantTui) {
217
+ // Dynamic import keeps Ink/React off every non-TTY code path.
218
+ const { runInitTui } = await import("../onboarding/InitTui.js");
219
+ const result = await runInitTui({
220
+ version,
221
+ detection: {
222
+ detected: det.claudeDetected,
223
+ sessionCount: det.sessionCount,
224
+ projectsDir: det.projectsDir
225
+ }
226
+ });
227
+ if (result.action === "select") {
228
+ printSelection(result.card);
182
229
  }
183
230
  else {
184
- lines.push(...emptyStateBlock(projectsDir));
185
- }
186
- lines.push("");
187
- // 4. Secondary real-workflow paths (compact, secondary styling).
188
- // Only on the default (unfocused) view.
189
- if (!focus) {
190
- lines.push(...secondaryBlock());
191
- lines.push("");
231
+ console.log(chalk.dim("\nNo workflow selected. Run `compaction init` anytime.\n"));
192
232
  }
233
+ return;
193
234
  }
194
- else {
195
- // Focused secondary path.
196
- const p = SECONDARY.find((s) => s.key === focus);
197
- lines.push(header("Start here"));
198
- lines.push("");
199
- lines.push(` ${chalk.bold(p.title)}`);
200
- lines.push(` ${chalk.cyan(p.command)}`);
201
- lines.push("");
235
+ // If the user explicitly asked for --interactive but we are not in a TTY,
236
+ // tell them why we fell back, then serve the static screen.
237
+ if (options.interactive && !ttyDecision.useTui) {
238
+ console.error(chalk.dim(`(interactive TUI unavailable here: ${ttyDecision.reason}; showing static screen)`));
202
239
  }
203
- lines.push(RULE);
204
- lines.push("");
205
- // 5. The follow-up sequence + what to expect.
206
- lines.push(...afterCaptureBlock());
207
- lines.push("");
208
- lines.push(RULE);
209
- lines.push("");
210
- // 6. Local-first footer.
211
- lines.push(...footerBlock());
212
- lines.push("");
213
- console.log(lines.join("\n"));
240
+ console.log(buildStaticScreen(version, focus, det));
214
241
  });
215
242
  }
216
243
  //# sourceMappingURL=init.js.map
@@ -1,6 +1,7 @@
1
1
  import chalk from "chalk";
2
2
  import { parseRunCommand, executeLocalCommand, persistLocalCommandRun } from "../../core/command-runner.js";
3
3
  import { writeJsonArtifact } from "../../core/artifact-writer.js";
4
+ import { withTraceFingerprint } from "../../core/report-generator.js";
4
5
  import { localCommandRunToAgentTrace } from "../../core/run-trace-converter.js";
5
6
  import { runEngineCommand } from "../engine-degrade.js";
6
7
  export function registerRunCommand(program) {
@@ -23,6 +24,10 @@ export function registerRunCommand(program) {
23
24
  const trace = localCommandRunToAgentTrace(run);
24
25
  const tracePath = await writeJsonArtifact(persistedRun.outputDirectory, "trace.json", trace);
25
26
  const artifacts = await writeCompactionArtifacts(trace, persistedRun.outputDirectory, run.runId);
27
+ // Attribution/integrity (V0.2 savings-evidence): re-persist report.json with the content-addressed
28
+ // trace fingerprint so `aggregate`/`summary` attribute each saving to a verifiable run and
29
+ // de-duplicate a re-counted run. Additive; no existing report field/label changes.
30
+ await writeJsonArtifact(persistedRun.outputDirectory, "report.json", withTraceFingerprint(artifacts.report, trace));
26
31
  console.log(`Exit code: ${run.exitCode ?? "none"}`);
27
32
  console.log(`Signal: ${run.signal ?? "none"}`);
28
33
  console.log(`Duration: ${run.durationMs} ms`);
package/dist/cli/index.js CHANGED
@@ -27,7 +27,7 @@ const program = new Command();
27
27
  program
28
28
  .name("compaction")
29
29
  .description("CLI-first trace compaction tools for AI agent workflows.")
30
- .version("0.1.2");
30
+ .version("0.1.4");
31
31
  registerAnalyzeCommand(program);
32
32
  registerAdapterCommand(program);
33
33
  registerAggregateCommand(program);
@@ -0,0 +1,52 @@
1
+ import React from "react";
2
+ import { type WorkflowCard } from "./model.js";
3
+ /**
4
+ * Interactive first-run TUI for `compaction init` (Ink/React) — a minimal,
5
+ * premium, left-aligned, dark-terminal-first workflow chooser.
6
+ *
7
+ * Progressive enhancement gated behind a real TTY (see `decideInteractiveTui`):
8
+ * it NEVER runs in pipes/CI/NO_COLOR/dumb terminals or the test runner — those
9
+ * get the static screen — so it may freely rely on color and Unicode.
10
+ *
11
+ * The big COMPACTION wordmark is the visual hero; everything below is quiet and
12
+ * compact. Palette is restrained Compaction-blue only (no teal/cyan). The full
13
+ * UI is short and rendered into the alternate screen buffer (see `runInitTui`)
14
+ * so resizing repaints in place and never duplicates the wordmark.
15
+ *
16
+ * Invariants (identical to the static path): writes nothing, no network call,
17
+ * no credential read, features ONLY the free local-first workflows. It is a
18
+ * chooser, not an agent: no text input, no model call, nothing uploaded.
19
+ */
20
+ export interface InitTuiDetection {
21
+ detected: boolean;
22
+ sessionCount: number;
23
+ projectsDir: string;
24
+ }
25
+ export type InitTuiResult = {
26
+ action: "select";
27
+ card: WorkflowCard;
28
+ } | {
29
+ action: "quit";
30
+ };
31
+ interface AppProps {
32
+ version: string;
33
+ detection: InitTuiDetection;
34
+ onDone: (result: InitTuiResult) => void;
35
+ }
36
+ export declare function App({ version, detection, onDone }: AppProps): React.ReactElement;
37
+ /**
38
+ * Render the TUI and resolve with the user's choice. Resolves to `{action:"quit"}`
39
+ * if the user exits without selecting. The caller prints the chosen command to
40
+ * the scrollback after this resolves.
41
+ *
42
+ * Renders into the terminal's ALTERNATE SCREEN BUFFER (like vim/less/fzf): no
43
+ * scrollback, so a reflow can never push a stale frame up into history. Resize
44
+ * redraw is handled INSIDE the component (reactive terminal size → re-render),
45
+ * so Ink owns the clear/repaint and the screen stays visible across resizes. On
46
+ * exit the screen is restored and the selected next-command prints normally.
47
+ */
48
+ export declare function runInitTui(props: {
49
+ version: string;
50
+ detection: InitTuiDetection;
51
+ }): Promise<InitTuiResult>;
52
+ export {};