markov-cli 1.0.4 → 1.0.6
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/package.json +1 -1
- package/src/interactive.js +87 -42
package/package.json
CHANGED
package/src/interactive.js
CHANGED
|
@@ -106,15 +106,17 @@ function parseWriteCommands(text) {
|
|
|
106
106
|
/**
|
|
107
107
|
* Parse and apply all file operations from a model reply.
|
|
108
108
|
* Shows diffs/confirmations for each op. Returns updated allFiles list.
|
|
109
|
+
* options.autoConfirm: if true, skip y/n prompts and apply all ops.
|
|
109
110
|
*/
|
|
110
|
-
async function handleFileOps(reply, loadedFiles) {
|
|
111
|
+
async function handleFileOps(reply, loadedFiles, options = {}) {
|
|
112
|
+
const autoConfirm = options.autoConfirm === true;
|
|
111
113
|
let allFiles = getFilesAndDirs();
|
|
112
114
|
|
|
113
115
|
// Create folders first (so !!run: cd <folder> can succeed later)
|
|
114
116
|
for (const folderPath of parseMkdirCommands(reply)) {
|
|
115
117
|
const path = toRelativePath(folderPath);
|
|
116
118
|
process.stdout.write(chalk.dim(` mkdir: ${path} — `));
|
|
117
|
-
const confirmed = await confirm(chalk.bold(`Create folder ${chalk.cyan(path)}? [y/N] `));
|
|
119
|
+
const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Create folder ${chalk.cyan(path)}? [y/N] `));
|
|
118
120
|
if (confirmed) {
|
|
119
121
|
try {
|
|
120
122
|
mkdirSync(resolve(process.cwd(), path), { recursive: true });
|
|
@@ -131,7 +133,7 @@ async function handleFileOps(reply, loadedFiles) {
|
|
|
131
133
|
// Create empty files
|
|
132
134
|
for (const filePath of parseTouchCommands(reply)) {
|
|
133
135
|
const path = toRelativePath(filePath);
|
|
134
|
-
const confirmed = await confirm(chalk.bold(`Create file ${chalk.cyan(path)}? [y/N] `));
|
|
136
|
+
const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Create file ${chalk.cyan(path)}? [y/N] `));
|
|
135
137
|
if (confirmed) {
|
|
136
138
|
try {
|
|
137
139
|
const abs = resolve(process.cwd(), path);
|
|
@@ -152,7 +154,7 @@ async function handleFileOps(reply, loadedFiles) {
|
|
|
152
154
|
for (const { filepath, content } of parseWriteCommands(reply)) {
|
|
153
155
|
const path = toRelativePath(filepath);
|
|
154
156
|
renderDiff(path, content);
|
|
155
|
-
const confirmed = await confirm(chalk.bold(`Write ${chalk.cyan(path)}? [y/N] `));
|
|
157
|
+
const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Write ${chalk.cyan(path)}? [y/N] `));
|
|
156
158
|
if (confirmed) {
|
|
157
159
|
try {
|
|
158
160
|
applyEdit(path, content);
|
|
@@ -171,7 +173,7 @@ async function handleFileOps(reply, loadedFiles) {
|
|
|
171
173
|
for (const { filepath, content } of edits) {
|
|
172
174
|
const path = toRelativePath(filepath);
|
|
173
175
|
renderDiff(path, content);
|
|
174
|
-
const confirmed = await confirm(chalk.bold(`Write ${chalk.cyan(path)}? [y/N] `));
|
|
176
|
+
const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Write ${chalk.cyan(path)}? [y/N] `));
|
|
175
177
|
if (confirmed) {
|
|
176
178
|
try {
|
|
177
179
|
applyEdit(path, content);
|
|
@@ -187,7 +189,7 @@ async function handleFileOps(reply, loadedFiles) {
|
|
|
187
189
|
|
|
188
190
|
// Run terminal commands (after folders/files exist, so cd works)
|
|
189
191
|
for (const cmd of parseRunCommands(reply)) {
|
|
190
|
-
const ok = await confirm(chalk.bold(`Run: ${chalk.cyan(cmd)}? [y/N] `));
|
|
192
|
+
const ok = autoConfirm ? true : await confirm(chalk.bold(`Run: ${chalk.cyan(cmd)}? [y/N] `));
|
|
191
193
|
if (ok) {
|
|
192
194
|
// cd must be handled in-process — child processes can't change the parent's cwd
|
|
193
195
|
const cdMatch = cmd.match(/^cd\s+(.+)$/);
|
|
@@ -215,7 +217,7 @@ async function handleFileOps(reply, loadedFiles) {
|
|
|
215
217
|
// Remove directories
|
|
216
218
|
for (const dirPath of parseRmdirCommands(reply)) {
|
|
217
219
|
const path = toRelativePath(dirPath);
|
|
218
|
-
const confirmed = await confirm(chalk.bold(`Remove directory ${chalk.cyan(path)}? [y/N] `));
|
|
220
|
+
const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Remove directory ${chalk.cyan(path)}? [y/N] `));
|
|
219
221
|
if (confirmed) {
|
|
220
222
|
const abs = resolve(process.cwd(), path);
|
|
221
223
|
if (existsSync(abs)) {
|
|
@@ -237,7 +239,7 @@ async function handleFileOps(reply, loadedFiles) {
|
|
|
237
239
|
// Delete files
|
|
238
240
|
for (const filePath of parseDeleteCommands(reply)) {
|
|
239
241
|
const path = toRelativePath(filePath);
|
|
240
|
-
const confirmed = await confirm(chalk.bold(`Delete ${chalk.cyan(path)}? [y/N] `));
|
|
242
|
+
const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Delete ${chalk.cyan(path)}? [y/N] `));
|
|
241
243
|
if (confirmed) {
|
|
242
244
|
const abs = resolve(process.cwd(), path);
|
|
243
245
|
if (existsSync(abs)) {
|
|
@@ -429,11 +431,7 @@ function buildSystemMessage() {
|
|
|
429
431
|
`\nSETUP NEXT.JS APP (or any new project in a subfolder):\n` +
|
|
430
432
|
`1. Create the folder first: !!mkdir: next-app (or the requested name).\n` +
|
|
431
433
|
`2. Change into it on its own line: !!run: cd next-app (nothing after the path).\n` +
|
|
432
|
-
`3. Run each following command on its own !!run: line: e.g. !!run: npx create-next-app@latest . --yes, then !!run: git init, !!run: git add ., !!run: git commit -m "Initial commit".\n
|
|
433
|
-
`\nSETUP LARAVEL API (output exactly this):\n` +
|
|
434
|
-
`!!mkdir: laravel-api then !!run: cd laravel-api then !!run: composer create-project --prefer-dist laravel/laravel . then !!run: php artisan serve. One per line; nothing after the dot in composer; no composer run dev or dev-server. No custom routes.\n` +
|
|
435
|
-
`\nSETUP ROUTES (API only — use when user asks to add routes, /health, /users, or API endpoints):\n` +
|
|
436
|
-
`Only after the Laravel app exists. Add in routes/api.php only (not web.php): GET /api/health returning response()->json(['status' => 'ok']), and /api/users as a resource (index, show, store, update, destroy) using User model and a controller. Register api in bootstrap/app.php if needed. Use !!write: for routes/api.php and the controller; prefix with laravel-api/ if cwd is parent. Do not use routes/web.php.\n`;
|
|
434
|
+
`3. Run each following command on its own !!run: line: e.g. !!run: npx create-next-app@latest . --yes, then !!run: git init, !!run: git add ., !!run: git commit -m "Initial commit".\n`;
|
|
437
435
|
return { role: 'system', content: `You are Markov, an AI coding assistant.\nWorking directory: ${process.cwd()}\n${fileList}${fileOpsInstructions}` };
|
|
438
436
|
}
|
|
439
437
|
|
|
@@ -522,11 +520,12 @@ const HELP_TEXT =
|
|
|
522
520
|
'\n' +
|
|
523
521
|
chalk.bold('Commands:\n') +
|
|
524
522
|
chalk.cyan(' /help') + chalk.dim(' show this help\n') +
|
|
525
|
-
chalk.cyan(' /plan <message>') + chalk.dim(' ask AI to create a plan (no files written)\n') +
|
|
526
523
|
chalk.cyan(' /build') + chalk.dim(' execute the stored plan\n') +
|
|
524
|
+
chalk.cyan(' /setup-nextjs') + chalk.dim(' scaffold a Next.js app\n') +
|
|
525
|
+
chalk.cyan(' /setup-laravel') + chalk.dim(' scaffold a Laravel API\n') +
|
|
527
526
|
chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
|
|
528
527
|
chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
|
|
529
|
-
chalk.dim('\
|
|
528
|
+
chalk.dim('\nNormal chat: generates a plan · /build to execute · ') + chalk.cyan('@filename') + chalk.dim(' to attach a file · ctrl+q to cancel\n');
|
|
530
529
|
|
|
531
530
|
export async function startInteractive() {
|
|
532
531
|
printLogo();
|
|
@@ -577,28 +576,18 @@ export async function startInteractive() {
|
|
|
577
576
|
continue;
|
|
578
577
|
}
|
|
579
578
|
|
|
580
|
-
// /
|
|
581
|
-
if (trimmed
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
`Create a detailed, numbered plan for the following task:\n\n${planRequest}\n\n` +
|
|
585
|
-
`For each step, specify exactly what will happen and which syntax will be used:\n` +
|
|
586
|
-
`- Writing or editing a file → !!write: path/to/file then fenced code block\n` +
|
|
587
|
-
`- Creating an empty file → !!touch: path/to/file\n` +
|
|
588
|
-
`- Creating a folder → !!mkdir: path/to/folder\n` +
|
|
589
|
-
`- Removing a folder → !!rmdir: path/to/folder\n` +
|
|
590
|
-
`- Deleting a file → !!delete: path/to/file\n\n` +
|
|
591
|
-
`Do NOT output any actual file contents or commands yet — only the plan.`;
|
|
592
|
-
chatMessages.push({ role: 'user', content: planPrompt });
|
|
579
|
+
// /setup-nextjs — scaffold a Next.js app
|
|
580
|
+
if (trimmed === '/setup-nextjs') {
|
|
581
|
+
const msg = 'Set up a new Next.js app in a subfolder. Follow the SETUP NEXT.JS APP instructions exactly.';
|
|
582
|
+
chatMessages.push({ role: 'user', content: msg });
|
|
593
583
|
const abortController = new AbortController();
|
|
594
584
|
try {
|
|
595
585
|
const reply = await streamWithViewport(chatMessages, abortController.signal);
|
|
596
|
-
if (reply === null) {
|
|
597
|
-
|
|
598
|
-
} else {
|
|
586
|
+
if (reply === null) { chatMessages.pop(); }
|
|
587
|
+
else {
|
|
599
588
|
chatMessages.push({ role: 'assistant', content: reply });
|
|
600
|
-
|
|
601
|
-
console.log(chalk.green('
|
|
589
|
+
allFiles = await handleFileOps(reply, [], { autoConfirm: true });
|
|
590
|
+
console.log(chalk.green('✓ Next.js app created.\n'));
|
|
602
591
|
}
|
|
603
592
|
} catch (err) {
|
|
604
593
|
if (!abortController.signal.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
@@ -606,13 +595,61 @@ export async function startInteractive() {
|
|
|
606
595
|
continue;
|
|
607
596
|
}
|
|
608
597
|
|
|
609
|
-
// /
|
|
598
|
+
// /setup-laravel — scaffold a Laravel API (hardcoded, no AI)
|
|
599
|
+
if (trimmed === '/setup-laravel') {
|
|
600
|
+
const steps = [
|
|
601
|
+
{ type: 'mkdir', path: 'laravel-api' },
|
|
602
|
+
{ type: 'cd', path: 'laravel-api' },
|
|
603
|
+
{ type: 'run', cmd: 'composer create-project --prefer-dist laravel/laravel .' },
|
|
604
|
+
{ type: 'run', cmd: 'php artisan serve' },
|
|
605
|
+
];
|
|
606
|
+
for (const step of steps) {
|
|
607
|
+
if (step.type === 'mkdir') {
|
|
608
|
+
process.stdout.write(chalk.dim(` mkdir: ${step.path}\n`));
|
|
609
|
+
try {
|
|
610
|
+
mkdirSync(resolve(process.cwd(), step.path), { recursive: true });
|
|
611
|
+
allFiles = getFilesAndDirs();
|
|
612
|
+
console.log(chalk.green(` ✓ created ${step.path}\n`));
|
|
613
|
+
} catch (err) {
|
|
614
|
+
console.log(chalk.red(` ✗ ${err.message}\n`));
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
} else if (step.type === 'cd') {
|
|
618
|
+
try {
|
|
619
|
+
process.chdir(resolve(process.cwd(), step.path));
|
|
620
|
+
allFiles = getFilesAndDirs();
|
|
621
|
+
console.log(chalk.dim(` 📁 ${process.cwd()}\n`));
|
|
622
|
+
} catch (err) {
|
|
623
|
+
console.log(chalk.red(` ✗ no such directory: ${step.path}\n`));
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
} else if (step.type === 'run') {
|
|
627
|
+
process.stdout.write(chalk.dim(` running: ${step.cmd}\n`));
|
|
628
|
+
const { stdout, stderr, exitCode } = await execCommand(step.cmd);
|
|
629
|
+
const output = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
630
|
+
if (output) console.log(chalk.dim(output));
|
|
631
|
+
if (exitCode !== 0) {
|
|
632
|
+
console.log(chalk.red(` ✗ command failed (exit ${exitCode})\n`));
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
console.log(chalk.green(` ✓ done\n`));
|
|
636
|
+
allFiles = getFilesAndDirs();
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
console.log(chalk.green('✓ Laravel API created.\n'));
|
|
640
|
+
continue;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
|
|
644
|
+
// /build — execute the stored plan with file ops
|
|
610
645
|
if (trimmed === '/build') {
|
|
611
646
|
if (!currentPlan) {
|
|
612
|
-
console.log(chalk.yellow('\n⚠ No plan stored.
|
|
647
|
+
console.log(chalk.yellow('\n⚠ No plan stored. Describe what you want first.\n'));
|
|
613
648
|
continue;
|
|
614
649
|
}
|
|
615
650
|
|
|
651
|
+
const autoConfirm = await confirm(chalk.bold('Auto-confirm all operations? [y/N] '));
|
|
652
|
+
|
|
616
653
|
const buildPrompt =
|
|
617
654
|
`Execute the following plan. Use !!write: path then a fenced code block for file writes, !!mkdir: for folders, !!delete: for deletions.\n\n` +
|
|
618
655
|
`Plan:\n${currentPlan.text}`;
|
|
@@ -625,7 +662,7 @@ export async function startInteractive() {
|
|
|
625
662
|
chatMessages.pop();
|
|
626
663
|
} else {
|
|
627
664
|
chatMessages.push({ role: 'assistant', content: reply });
|
|
628
|
-
allFiles = await handleFileOps(reply, []);
|
|
665
|
+
allFiles = await handleFileOps(reply, [], { autoConfirm });
|
|
629
666
|
currentPlan = null;
|
|
630
667
|
console.log(chalk.green('✓ Plan executed.\n'));
|
|
631
668
|
}
|
|
@@ -635,7 +672,7 @@ export async function startInteractive() {
|
|
|
635
672
|
continue;
|
|
636
673
|
}
|
|
637
674
|
|
|
638
|
-
// Normal chat —
|
|
675
|
+
// Normal chat — generate a plan, store it, require /build to execute
|
|
639
676
|
const { loaded, failed } = resolveFileRefs(trimmed);
|
|
640
677
|
|
|
641
678
|
if (loaded.length > 0) {
|
|
@@ -645,18 +682,26 @@ export async function startInteractive() {
|
|
|
645
682
|
console.log(chalk.yellow(`\n⚠ not found: ${failed.map(f => `@${f}`).join(', ')}`));
|
|
646
683
|
}
|
|
647
684
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
685
|
+
const planPrompt =
|
|
686
|
+
`Create a detailed, numbered plan for the following task:\n\n${trimmed}\n\n` +
|
|
687
|
+
`For each step, specify exactly what will happen and which syntax will be used:\n` +
|
|
688
|
+
`- Writing or editing a file → !!write: path/to/file then fenced code block\n` +
|
|
689
|
+
`- Creating an empty file → !!touch: path/to/file\n` +
|
|
690
|
+
`- Creating a folder → !!mkdir: path/to/folder\n` +
|
|
691
|
+
`- Removing a folder → !!rmdir: path/to/folder\n` +
|
|
692
|
+
`- Deleting a file → !!delete: path/to/file\n\n` +
|
|
693
|
+
`Do NOT output any actual file contents or commands yet — only the plan.`;
|
|
694
|
+
|
|
695
|
+
chatMessages.push({ role: 'user', content: planPrompt });
|
|
651
696
|
const abortController = new AbortController();
|
|
652
697
|
try {
|
|
653
698
|
const reply = await streamWithViewport(chatMessages, abortController.signal);
|
|
654
|
-
|
|
655
699
|
if (reply === null) {
|
|
656
700
|
chatMessages.pop();
|
|
657
701
|
} else {
|
|
658
702
|
chatMessages.push({ role: 'assistant', content: reply });
|
|
659
|
-
|
|
703
|
+
currentPlan = { text: reply };
|
|
704
|
+
console.log(chalk.green('\n✓ Plan stored. Use /build to execute it.\n'));
|
|
660
705
|
}
|
|
661
706
|
} catch (err) {
|
|
662
707
|
if (!abortController.signal.aborted) {
|