markov-cli 1.0.10 → 1.0.12

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.
@@ -0,0 +1,72 @@
1
+ import chalk from 'chalk';
2
+ import { mkdirSync, writeFileSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { execCommand } from '../tools.js';
5
+
6
+ /**
7
+ * Run a list of setup steps (mkdir / cd / run / write) in sequence.
8
+ * Returns true if all steps succeeded, false if any failed.
9
+ */
10
+ export async function runSetupSteps(steps) {
11
+ for (const step of steps) {
12
+ if (step.type === 'mkdir') {
13
+ process.stdout.write(chalk.dim(` mkdir: ${step.path}\n`));
14
+ try {
15
+ mkdirSync(resolve(process.cwd(), step.path), { recursive: true });
16
+ console.log(chalk.green(` ✓ created ${step.path}\n`));
17
+ } catch (err) {
18
+ console.log(chalk.red(` ✗ ${err.message}\n`));
19
+ return false;
20
+ }
21
+ } else if (step.type === 'cd') {
22
+ try {
23
+ process.chdir(resolve(process.cwd(), step.path));
24
+ console.log(chalk.dim(` 📁 ${process.cwd()}\n`));
25
+ } catch (err) {
26
+ console.log(chalk.red(` ✗ no such directory: ${step.path}\n`));
27
+ return false;
28
+ }
29
+ } else if (step.type === 'run') {
30
+ process.stdout.write(chalk.dim(` running: ${step.cmd}\n`));
31
+ const { stdout, stderr, exitCode } = await execCommand(step.cmd);
32
+ const output = [stdout, stderr].filter(Boolean).join('\n').trim();
33
+ if (output) console.log(chalk.dim(output));
34
+ if (exitCode !== 0) {
35
+ console.log(chalk.red(` ✗ command failed (exit ${exitCode})\n`));
36
+ return false;
37
+ }
38
+ console.log(chalk.green(` ✓ done\n`));
39
+ } else if (step.type === 'write') {
40
+ process.stdout.write(chalk.dim(` write: ${step.path}\n`));
41
+ try {
42
+ const abs = resolve(process.cwd(), step.path);
43
+ const dir = abs.split('/').slice(0, -1).join('/');
44
+ mkdirSync(dir, { recursive: true });
45
+ writeFileSync(abs, step.content);
46
+ console.log(chalk.green(` ✓ wrote ${step.path}\n`));
47
+ } catch (err) {
48
+ console.log(chalk.red(` ✗ ${err.message}\n`));
49
+ return false;
50
+ }
51
+ }
52
+ }
53
+ return true;
54
+ }
55
+
56
+ /** Next.js setup steps. */
57
+ export const NEXTJS_STEPS = [
58
+ { type: 'run', cmd: 'npx create-next-app@latest . --typescript --tailwind --eslint --no-git' },
59
+ ];
60
+
61
+ /** TanStack Start setup steps (Vite + TanStack Router, type-safe full-stack). Uses @tanstack/cli (create-start is deprecated). */
62
+ export const TANSTACK_STEPS = [
63
+ {
64
+ type: 'run',
65
+ cmd: 'npx @tanstack/cli create . --package-manager npm --toolchain eslint',
66
+ },
67
+ ];
68
+
69
+ /** Laravel setup steps. */
70
+ export const LARAVEL_STEPS = [
71
+ { type: 'run', cmd: 'composer create-project laravel/laravel .' },
72
+ ];
@@ -0,0 +1,27 @@
1
+ import chalk from 'chalk';
2
+ import { parseEdits, renderDiff, applyEdit } from '../editor.js';
3
+ import { confirm } from '../ui/prompts.js';
4
+
5
+ /**
6
+ * Scan AI response text for fenced code blocks that reference files.
7
+ * Show a diff preview for each and apply on user confirmation.
8
+ * @param {string} responseText - The AI's response content
9
+ * @param {string[]} loadedFiles - Files attached via @ref
10
+ */
11
+ export async function applyCodeBlockEdits(responseText, loadedFiles = []) {
12
+ const edits = parseEdits(responseText, loadedFiles);
13
+ if (edits.length === 0) return;
14
+
15
+ console.log(chalk.dim(`\n📝 Detected ${edits.length} file edit(s) in response:\n`));
16
+
17
+ for (const edit of edits) {
18
+ renderDiff(edit.filepath, edit.content);
19
+ const ok = await confirm(chalk.bold(`Apply changes to ${chalk.cyan(edit.filepath)}? [y/N] `));
20
+ if (ok) {
21
+ applyEdit(edit.filepath, edit.content);
22
+ console.log(chalk.green(` ✓ ${edit.filepath} updated\n`));
23
+ } else {
24
+ console.log(chalk.dim(` ✗ skipped ${edit.filepath}\n`));
25
+ }
26
+ }
27
+ }
package/src/files.js CHANGED
@@ -76,7 +76,7 @@ export function resolveFileRefs(input, cwd = process.cwd()) {
76
76
 
77
77
  const contextBlock = blocks.length > 0
78
78
  ? `The user referenced the following file(s) as context:\n\n${blocks.join('\n\n')}\n\n` +
79
- `ATTACHED FILES RULE: The user attached the file(s) above. If they ask you to edit, add, fix, or create anything in these files, you MUST use the write_file or search_replace tool — do not output the new or modified file content in your reply (no code blocks with file content). Use the exact paths shown above (e.g. ${loaded[0] ?? 'path'}).\n\n`
79
+ `ATTACHED FILES RULE: The user attached the file(s) above. If they ask you to edit, add, fix, or create anything in these files, you MUST use run_terminal_command (e.g. echo, cat, tee, heredocs) or search_replace for small edits — do not output the new or modified file content in your reply (no code blocks with file content). Use the exact paths shown above (e.g. ${loaded[0] ?? 'path'}).\n\n`
80
80
  : '';
81
81
 
82
82
  return { content: contextBlock + input, loaded, failed };
package/src/input.js CHANGED
@@ -21,18 +21,25 @@ function statusBar() {
21
21
  return STATUS_LEFT + ' '.repeat(gap) + right;
22
22
  }
23
23
 
24
+ const INPUT_HISTORY_MAX = 100;
25
+
24
26
  /**
25
- * Show an interactive raw-mode prompt that supports @file autocomplete.
27
+ * Show an interactive raw-mode prompt that supports @file autocomplete and Up/Down input history.
26
28
  * Returns a Promise<string|null> — null means the prompt was cancelled (Ctrl+Q).
29
+ * @param {string} _promptStr
30
+ * @param {string[]} allFiles
31
+ * @param {string[]} inputHistory - Mutable array of previous inputs; newest last. Up/Down navigate this; Enter pushes current input.
27
32
  */
28
- export function chatPrompt(_promptStr, allFiles) {
33
+ export function chatPrompt(_promptStr, allFiles, inputHistory = []) {
29
34
  return new Promise((resolve) => {
30
35
  const stdin = process.stdin;
31
36
  const stdout = process.stdout;
32
37
 
33
38
  let buffer = '';
39
+ let cursorPos = 0; // 0..buffer.length; position of cursor in buffer
34
40
  let pickerFiles = [];
35
41
  let pickerIndex = 0;
42
+ let historyIndex = -1; // -1 = not navigating history
36
43
  let cursorLineOffset = 0; // lines from top of drawn block to where cursor sits
37
44
 
38
45
  const getAtPos = () => {
@@ -96,9 +103,12 @@ export function chatPrompt(_promptStr, allFiles) {
96
103
  const inputVisualLines = Math.max(1, Math.ceil(inputVisualLen / w));
97
104
  cursorLineOffset = inputVisualLines;
98
105
 
99
- // Position cursor at the correct column on the last visual line of the input.
100
- const totalInputLen = visibleLen(chalk.cyan(PREFIX)) + visibleLen(buffer);
101
- const col = (totalInputLen % w) + 1;
106
+ // Position cursor: beforeCursorLen is the character offset where the cursor sits.
107
+ const prefixLen = visibleLen(chalk.cyan(PREFIX));
108
+ const beforeCursorLen = prefixLen + visibleLen(buffer.slice(0, cursorPos));
109
+ const lineIdx = Math.floor(beforeCursorLen / w);
110
+ const col = (beforeCursorLen % w) + 1;
111
+ if (lineIdx > 0) stdout.write(`\x1b[${lineIdx}B`);
102
112
  stdout.write(`\x1b[${col}G`);
103
113
  };
104
114
 
@@ -147,12 +157,17 @@ export function chatPrompt(_promptStr, allFiles) {
147
157
  const atPos = getAtPos();
148
158
  if (atPos !== -1) {
149
159
  buffer = buffer.slice(0, atPos) + '@' + pickerFiles[pickerIndex] + ' ';
160
+ cursorPos = buffer.length;
150
161
  }
151
162
  pickerFiles = [];
152
163
  pickerIndex = 0;
153
164
  redraw();
154
165
  return;
155
166
  }
167
+ if (buffer.trim() && buffer !== inputHistory[inputHistory.length - 1]) {
168
+ inputHistory.push(buffer);
169
+ if (inputHistory.length > INPUT_HISTORY_MAX) inputHistory.shift();
170
+ }
156
171
  clearPanel();
157
172
  cleanup();
158
173
  stdout.write('\n');
@@ -167,28 +182,67 @@ export function chatPrompt(_promptStr, allFiles) {
167
182
  return;
168
183
  }
169
184
 
170
- // Arrow keys
185
+ // Arrow keys: Up/Down navigate picker when open, else input history (Up = older, Down = newer)
171
186
  if (key === '\x1b[A') {
172
- if (pickerFiles.length > 0) { pickerIndex = (pickerIndex - 1 + pickerFiles.length) % pickerFiles.length; redraw(); }
187
+ if (pickerFiles.length > 0) {
188
+ pickerIndex = (pickerIndex - 1 + pickerFiles.length) % pickerFiles.length;
189
+ redraw();
190
+ } else if (inputHistory.length > 0) {
191
+ historyIndex = historyIndex === -1 ? inputHistory.length - 1 : Math.max(0, historyIndex - 1);
192
+ buffer = inputHistory[historyIndex];
193
+ cursorPos = buffer.length;
194
+ updatePicker();
195
+ redraw();
196
+ }
173
197
  return;
174
198
  }
175
199
  if (key === '\x1b[B') {
176
- if (pickerFiles.length > 0) { pickerIndex = (pickerIndex + 1) % pickerFiles.length; redraw(); }
200
+ if (pickerFiles.length > 0) {
201
+ pickerIndex = (pickerIndex + 1) % pickerFiles.length;
202
+ redraw();
203
+ } else if (historyIndex >= 0) {
204
+ historyIndex++;
205
+ if (historyIndex >= inputHistory.length) {
206
+ historyIndex = -1;
207
+ buffer = '';
208
+ } else {
209
+ buffer = inputHistory[historyIndex];
210
+ }
211
+ cursorPos = buffer.length;
212
+ updatePicker();
213
+ redraw();
214
+ }
215
+ return;
216
+ }
217
+ // Left/Right: move cursor within the line
218
+ if (key === '\x1b[D') {
219
+ if (cursorPos > 0) { cursorPos--; redraw(); }
220
+ return;
221
+ }
222
+ if (key === '\x1b[C') {
223
+ if (cursorPos < buffer.length) { cursorPos++; redraw(); }
177
224
  return;
178
225
  }
179
- if (key === '\x1b[C' || key === '\x1b[D') return; // left/right — ignore
180
226
 
181
- // Backspace
227
+ // Backspace: delete character before cursor
182
228
  if (key === '\x7f' || key === '\b') {
183
- if (buffer.length > 0) { buffer = buffer.slice(0, -1); updatePicker(); redraw(); }
229
+ historyIndex = -1;
230
+ if (cursorPos > 0) {
231
+ buffer = buffer.slice(0, cursorPos - 1) + buffer.slice(cursorPos);
232
+ cursorPos--;
233
+ updatePicker();
234
+ redraw();
235
+ }
184
236
  return;
185
237
  }
186
238
 
187
239
  // Ignore other control chars
188
240
  if (key < ' ') return;
189
241
 
190
- // Printable
191
- buffer += key;
242
+ // Printable: insert at cursor
243
+ historyIndex = -1;
244
+ buffer = buffer.slice(0, cursorPos) + key + buffer.slice(cursorPos);
245
+ cursorPos += key.length;
192
246
  updatePicker();
193
247
  redraw();
194
248
  };