@cruxy/cli 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +39 -16
  2. package/dist/agent/loop.d.ts +9 -5
  3. package/dist/agent/loop.js +53 -10
  4. package/dist/agent/prompts.d.ts +2 -0
  5. package/dist/agent/prompts.js +6 -0
  6. package/dist/agent/session.d.ts +29 -3
  7. package/dist/agent/session.js +37 -10
  8. package/dist/approval/prompt.d.ts +9 -0
  9. package/dist/approval/prompt.js +2 -77
  10. package/dist/cli/commands/init.d.ts +7 -0
  11. package/dist/cli/commands/init.js +40 -0
  12. package/dist/cli/commands/login.d.ts +8 -0
  13. package/dist/cli/commands/login.js +36 -0
  14. package/dist/cli/commands/run.js +46 -62
  15. package/dist/cli/onboard.d.ts +25 -0
  16. package/dist/cli/onboard.js +59 -0
  17. package/dist/cli/program.js +19 -1
  18. package/dist/cli/repl.d.ts +9 -4
  19. package/dist/cli/repl.js +32 -12
  20. package/dist/cli/session-factory.d.ts +13 -0
  21. package/dist/cli/session-factory.js +109 -0
  22. package/dist/config/credentials.d.ts +10 -0
  23. package/dist/config/credentials.js +69 -0
  24. package/dist/config/index.d.ts +1 -0
  25. package/dist/config/index.js +1 -0
  26. package/dist/config/manager.d.ts +6 -1
  27. package/dist/config/manager.js +11 -1
  28. package/dist/config/schema.d.ts +10 -0
  29. package/dist/config/schema.js +2 -0
  30. package/dist/constants.d.ts +6 -0
  31. package/dist/constants.js +6 -0
  32. package/dist/errors/constructors.d.ts +10 -0
  33. package/dist/errors/constructors.js +46 -2
  34. package/dist/errors/types.d.ts +3 -0
  35. package/dist/errors/types.js +6 -0
  36. package/dist/onboarding/detect.d.ts +26 -0
  37. package/dist/onboarding/detect.js +56 -0
  38. package/dist/onboarding/flow.d.ts +28 -0
  39. package/dist/onboarding/flow.js +100 -0
  40. package/dist/onboarding/index.d.ts +5 -0
  41. package/dist/onboarding/index.js +5 -0
  42. package/dist/onboarding/io.d.ts +8 -0
  43. package/dist/onboarding/io.js +133 -0
  44. package/dist/onboarding/steps.d.ts +17 -0
  45. package/dist/onboarding/steps.js +100 -0
  46. package/dist/onboarding/types.d.ts +81 -0
  47. package/dist/onboarding/types.js +6 -0
  48. package/dist/plan/approve.d.ts +16 -0
  49. package/dist/plan/approve.js +46 -0
  50. package/dist/plan/execute.d.ts +20 -0
  51. package/dist/plan/execute.js +31 -0
  52. package/dist/plan/index.d.ts +7 -0
  53. package/dist/plan/index.js +7 -0
  54. package/dist/plan/policy.d.ts +26 -0
  55. package/dist/plan/policy.js +45 -0
  56. package/dist/plan/render.d.ts +5 -0
  57. package/dist/plan/render.js +47 -0
  58. package/dist/plan/service.d.ts +40 -0
  59. package/dist/plan/service.js +118 -0
  60. package/dist/plan/submit-plan.d.ts +33 -0
  61. package/dist/plan/submit-plan.js +57 -0
  62. package/dist/plan/types.d.ts +60 -0
  63. package/dist/plan/types.js +6 -0
  64. package/dist/render/capabilities.d.ts +12 -0
  65. package/dist/render/capabilities.js +27 -0
  66. package/dist/render/diff.d.ts +19 -0
  67. package/dist/render/diff.js +80 -0
  68. package/dist/render/highlight.d.ts +47 -0
  69. package/dist/render/highlight.js +265 -0
  70. package/dist/render/index.d.ts +14 -0
  71. package/dist/render/index.js +20 -0
  72. package/dist/render/plain-renderer.d.ts +32 -0
  73. package/dist/render/plain-renderer.js +61 -0
  74. package/dist/render/tty-renderer.d.ts +47 -0
  75. package/dist/render/tty-renderer.js +149 -0
  76. package/dist/render/types.d.ts +76 -0
  77. package/dist/render/types.js +1 -0
  78. package/package.json +1 -1
@@ -0,0 +1,61 @@
1
+ import pc from "picocolors";
2
+ import { createStreamPrinter } from "../cli/stream-print.js";
3
+ import { renderActionPreview } from "./diff.js";
4
+ /**
5
+ * The append-only renderer for pipes, CI, and cursor-less terminals. Emits no
6
+ * cursor-control sequences ever, and no color unless the capabilities say so
7
+ * (FORCE_COLOR); with color off, not a single ANSI byte leaves this class.
8
+ *
9
+ * Assistant text goes to `out` verbatim (beyond the per-turn leading-newline
10
+ * trim) so piped stdout stays pure model output; chrome (`note`) goes to `err`,
11
+ * matching the logger's stdout/stderr split. Transient `status` has no meaning
12
+ * in an append-only medium and is dropped — the loop reports anything durable
13
+ * via `note`.
14
+ */
15
+ export class PlainRenderer {
16
+ caps;
17
+ out;
18
+ err;
19
+ colors;
20
+ /** Per-turn leading-newline trim; also tells endSegment whether to newline. */
21
+ print;
22
+ wroteInSegment = false;
23
+ constructor(caps, out, err) {
24
+ this.caps = caps;
25
+ this.out = out;
26
+ this.err = err;
27
+ this.colors = pc.createColors(caps.color);
28
+ this.print = this.newPrinter();
29
+ }
30
+ newPrinter() {
31
+ return createStreamPrinter((text) => {
32
+ this.wroteInSegment = true;
33
+ this.out.write(text);
34
+ });
35
+ }
36
+ beginTurn() {
37
+ this.print = this.newPrinter();
38
+ this.wroteInSegment = false;
39
+ }
40
+ write(delta) {
41
+ this.print(delta);
42
+ }
43
+ endSegment() {
44
+ if (this.wroteInSegment)
45
+ this.out.write("\n");
46
+ this.wroteInSegment = false;
47
+ }
48
+ note(text) {
49
+ this.err.write(this.colors.dim(text) + "\n");
50
+ }
51
+ preview(preview) {
52
+ const block = renderActionPreview(preview, this.colors);
53
+ if (block)
54
+ this.out.write(block + "\n");
55
+ }
56
+ status() {
57
+ // Append-only medium: transient state is dropped by design.
58
+ }
59
+ endTurn() { }
60
+ close() { }
61
+ }
@@ -0,0 +1,47 @@
1
+ import type { ActionPreview } from "../tools/types.js";
2
+ import type { RenderCapabilities, RenderStream, StreamRenderer } from "./types.js";
3
+ /**
4
+ * The interactive renderer: committed content is append-only; the one transient
5
+ * thing on screen is a single managed status line, redrawn in place.
6
+ *
7
+ * The no-flicker discipline, concretely:
8
+ * - Only the status line is ever rewritten, via `\r` + erase-line — never a
9
+ * screen clear, never a repaint of committed rows.
10
+ * - Every committed write first erases the status line, so the live region is
11
+ * always the last row and committed text can never interleave with it. A
12
+ * committed write *dismisses* the status (it does not redraw underneath), so
13
+ * nothing re-renders per delta while text streams.
14
+ * - The status text is hard-truncated to the terminal width: a soft-wrapped
15
+ * status would span two rows and erase-line could no longer clean it up.
16
+ *
17
+ * Fenced code blocks are highlighted incrementally (see highlight.ts): prose
18
+ * deltas pass straight through, code is styled line-by-line on arrival.
19
+ */
20
+ export declare class TtyRenderer implements StreamRenderer {
21
+ readonly caps: RenderCapabilities;
22
+ private readonly out;
23
+ private readonly colors;
24
+ private print;
25
+ private highlighter;
26
+ private wroteInSegment;
27
+ private statusText;
28
+ private timer;
29
+ private frame;
30
+ private closed;
31
+ constructor(caps: RenderCapabilities, out: RenderStream);
32
+ private newPrinter;
33
+ /** Append committed content, erasing the status line first if one is live. */
34
+ private commit;
35
+ /** Erase the live status line (if any) and stop the spinner. */
36
+ private dismissStatus;
37
+ private stopTimer;
38
+ private drawStatus;
39
+ beginTurn(): void;
40
+ write(delta: string): void;
41
+ endSegment(): void;
42
+ note(text: string): void;
43
+ preview(preview: ActionPreview): void;
44
+ status(text: string | null): void;
45
+ endTurn(): void;
46
+ close(): void;
47
+ }
@@ -0,0 +1,149 @@
1
+ import pc from "picocolors";
2
+ import { createStreamPrinter } from "../cli/stream-print.js";
3
+ import { renderActionPreview } from "./diff.js";
4
+ import { createStreamHighlighter, } from "./highlight.js";
5
+ /** Erase the current line and return the cursor to column 0. */
6
+ const CLEAR_LINE = "\r\x1b[2K";
7
+ /** Spinner frames (braille); a static glyph when animation is disabled. */
8
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
9
+ const STATIC_FRAME = "◐";
10
+ const SPINNER_INTERVAL_MS = 100;
11
+ /**
12
+ * The interactive renderer: committed content is append-only; the one transient
13
+ * thing on screen is a single managed status line, redrawn in place.
14
+ *
15
+ * The no-flicker discipline, concretely:
16
+ * - Only the status line is ever rewritten, via `\r` + erase-line — never a
17
+ * screen clear, never a repaint of committed rows.
18
+ * - Every committed write first erases the status line, so the live region is
19
+ * always the last row and committed text can never interleave with it. A
20
+ * committed write *dismisses* the status (it does not redraw underneath), so
21
+ * nothing re-renders per delta while text streams.
22
+ * - The status text is hard-truncated to the terminal width: a soft-wrapped
23
+ * status would span two rows and erase-line could no longer clean it up.
24
+ *
25
+ * Fenced code blocks are highlighted incrementally (see highlight.ts): prose
26
+ * deltas pass straight through, code is styled line-by-line on arrival.
27
+ */
28
+ export class TtyRenderer {
29
+ caps;
30
+ out;
31
+ colors;
32
+ print;
33
+ highlighter;
34
+ wroteInSegment = false;
35
+ statusText = null;
36
+ timer = null;
37
+ frame = 0;
38
+ closed = false;
39
+ constructor(caps, out) {
40
+ this.caps = caps;
41
+ this.out = out;
42
+ this.colors = pc.createColors(caps.color);
43
+ this.highlighter = createStreamHighlighter(this.colors);
44
+ this.print = this.newPrinter();
45
+ }
46
+ newPrinter() {
47
+ return createStreamPrinter((text) => {
48
+ this.commit(this.highlighter.push(text));
49
+ });
50
+ }
51
+ /** Append committed content, erasing the status line first if one is live. */
52
+ commit(text) {
53
+ if (text === "")
54
+ return;
55
+ this.dismissStatus();
56
+ this.wroteInSegment = true;
57
+ this.out.write(text);
58
+ }
59
+ /** Erase the live status line (if any) and stop the spinner. */
60
+ dismissStatus() {
61
+ if (this.statusText === null)
62
+ return;
63
+ this.statusText = null;
64
+ this.stopTimer();
65
+ this.out.write(CLEAR_LINE);
66
+ }
67
+ stopTimer() {
68
+ if (this.timer !== null) {
69
+ clearInterval(this.timer);
70
+ this.timer = null;
71
+ }
72
+ }
73
+ drawStatus() {
74
+ if (this.statusText === null)
75
+ return;
76
+ const glyph = this.caps.spinner
77
+ ? FRAMES[this.frame % FRAMES.length]
78
+ : STATIC_FRAME;
79
+ // Reserve glyph + space; truncate so the live line can never soft-wrap.
80
+ const room = Math.max(1, this.caps.width - 2);
81
+ const text = this.statusText.length > room
82
+ ? this.statusText.slice(0, Math.max(0, room - 1)) + "…"
83
+ : this.statusText;
84
+ this.out.write(`${CLEAR_LINE}${this.colors.cyan(glyph)} ${this.colors.dim(text)}`);
85
+ }
86
+ beginTurn() {
87
+ this.highlighter = createStreamHighlighter(this.colors);
88
+ this.print = this.newPrinter();
89
+ this.wroteInSegment = false;
90
+ }
91
+ write(delta) {
92
+ if (this.closed)
93
+ return;
94
+ this.print(delta);
95
+ }
96
+ endSegment() {
97
+ if (this.closed)
98
+ return;
99
+ this.commit(this.highlighter.flush());
100
+ if (this.wroteInSegment) {
101
+ this.commit("\n");
102
+ this.wroteInSegment = false;
103
+ }
104
+ }
105
+ note(text) {
106
+ if (this.closed)
107
+ return;
108
+ const room = Math.max(1, this.caps.width);
109
+ const line = text.length > room ? text.slice(0, room - 1) + "…" : text;
110
+ this.commit(this.colors.dim(line) + "\n");
111
+ }
112
+ preview(preview) {
113
+ if (this.closed)
114
+ return;
115
+ const block = renderActionPreview(preview, this.colors);
116
+ if (block)
117
+ this.commit(block + "\n");
118
+ }
119
+ status(text) {
120
+ if (this.closed)
121
+ return;
122
+ if (text === null) {
123
+ this.dismissStatus();
124
+ return;
125
+ }
126
+ this.statusText = text;
127
+ this.drawStatus();
128
+ if (this.caps.spinner && this.timer === null) {
129
+ this.timer = setInterval(() => {
130
+ this.frame++;
131
+ this.drawStatus();
132
+ }, SPINNER_INTERVAL_MS);
133
+ // Never hold the process open for a spinner.
134
+ this.timer.unref?.();
135
+ }
136
+ }
137
+ endTurn() {
138
+ if (this.closed)
139
+ return;
140
+ this.dismissStatus();
141
+ this.commit(this.highlighter.flush());
142
+ }
143
+ close() {
144
+ if (this.closed)
145
+ return;
146
+ this.endTurn();
147
+ this.closed = true;
148
+ }
149
+ }
@@ -0,0 +1,76 @@
1
+ import type { ActionPreview } from "../tools/types.js";
2
+ /**
3
+ * The streaming render seam (U.2): the agent loop talks to a
4
+ * {@link StreamRenderer}, never to raw stdout. Two implementations exist —
5
+ * `TtyRenderer` (managed one-line live region, append-only committed content)
6
+ * and `PlainRenderer` (append-only, zero ANSI) — picked by the factory from
7
+ * detected {@link RenderCapabilities}. Same swappable discipline as
8
+ * `Formatter`/`VectorStore`.
9
+ */
10
+ /**
11
+ * What the output medium supports, detected once at construction (see
12
+ * `detectCapabilities`). Renderers and the factory key off these flags; nothing
13
+ * downstream re-probes the environment.
14
+ */
15
+ export interface RenderCapabilities {
16
+ /** The primary output stream is a terminal. */
17
+ tty: boolean;
18
+ /** Emit ANSI color (NO_COLOR / FORCE_COLOR / TTY resolved; `TERM=dumb` → false). */
19
+ color: boolean;
20
+ /** Cursor-control sequences are safe (`tty` and not `TERM=dumb`). */
21
+ cursor: boolean;
22
+ /** Animation is welcome (`cursor` and CRUXY_NO_SPINNER unset). */
23
+ spinner: boolean;
24
+ /** Terminal columns; 80 when unknown (non-TTY). */
25
+ width: number;
26
+ }
27
+ /**
28
+ * How the agent loop paints a turn. The contract that keeps output flicker-free:
29
+ *
30
+ * - **Committed content is append-only.** `write`/`note`/`preview` output is
31
+ * never repainted, moved, or cleared.
32
+ * - **Transient state is one line.** `status` owns a single live line that is
33
+ * redrawn in place and erased before any committed write lands.
34
+ *
35
+ * Call shape per user turn: `beginTurn` → per model turn: `write`* +
36
+ * `endSegment`, with `status`/`note`/`preview` between segments → `endTurn`.
37
+ * All methods must be safe to call in any environment — degradation is the
38
+ * implementation's job, not the caller's.
39
+ */
40
+ export interface StreamRenderer {
41
+ readonly caps: RenderCapabilities;
42
+ /** Start a user turn: reset leading-newline trim and code-fence state. */
43
+ beginTurn(): void;
44
+ /**
45
+ * Append streamed assistant text. Must forward promptly — implementations may
46
+ * hold back at most the current line (for fence detection), never the block
47
+ * or the response. Dismisses any live status first.
48
+ */
49
+ write(delta: string): void;
50
+ /**
51
+ * Close one model-turn text segment: flush any held partial line / open
52
+ * fence and terminate with a single newline (only if the segment produced
53
+ * visible text), so whatever follows starts on its own line.
54
+ */
55
+ endSegment(): void;
56
+ /** A committed one-line chrome note (e.g. `✓ read_file src/x.ts`), dim when colored. */
57
+ note(text: string): void;
58
+ /** A committed diff/action preview, rendered by the shared diff renderer. */
59
+ preview(preview: ActionPreview): void;
60
+ /**
61
+ * Replace the transient status line ("thinking…", "running bash…"); `null`
62
+ * clears it. Where in-place updates are impossible this may drop the text —
63
+ * status is progress decor, never information of record (use `note` for that).
64
+ */
65
+ status(text: string | null): void;
66
+ /** End the user turn: clear any status, flush everything held. */
67
+ endTurn(): void;
68
+ /** Release resources (spinner timer). Further calls are no-ops. */
69
+ close(): void;
70
+ }
71
+ /** The minimal stream surface a renderer writes to. Injectable for tests. */
72
+ export interface RenderStream {
73
+ isTTY?: boolean;
74
+ columns?: number;
75
+ write(text: string): unknown;
76
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {