Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 47 additions & 110 deletions .development/index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import chalk from "chalk";
import { createInterface } from "readline";
import * as prompts from "@clack/prompts";
import { executeCommands } from "../src/executor.js";
import { printCommand, printError, printFinished, sep } from "../src/ui.js";

// ── action definitions ────────────────────────────────────────────────
// Each entry mirrors the shape of an AI response so the real executor
// path is exercised end-to-end with no mocking.
import { printCommand, printError, printFinished } from "../src/ui.js";

// Each entry mirrors the shape of an AI response so the real executor path is
// exercised end-to-end with no mocking.
const ACTIONS = [
{
id: 1,
Expand Down Expand Up @@ -81,119 +79,58 @@ const ACTIONS = [
},
];

// ── helpers ───────────────────────────────────────────────────────────

const preview = (cmds) => {
const joined = cmds.join(" → ");
return joined.length > 52 ? joined.slice(0, 49) + "…" : joined;
const preview = (commands) => {
const joined = commands.join(" -> ");
return joined.length > 52 ? `${joined.slice(0, 49)}...` : joined;
};

const pad = (str, n) => str + " ".repeat(Math.max(0, n - str.length));

// ── menu renderer ─────────────────────────────────────────────────────

function printMenu() {
const labelWidth = Math.max(...ACTIONS.map((a) => a.label.length)) + 2;

console.log();
sep();
console.log(
` ${chalk.white(">")} ${chalk.bold.white("NOVA")} ${chalk.gray("— development mode")}`,
);
sep();
console.log();

for (const a of ACTIONS) {
const num = chalk.cyan(`[${a.id}]`);
const label = chalk.white(pad(a.label, labelWidth));
const hint = chalk.gray(preview(a.commands));
console.log(` ${num} ${label}${hint}`);
}

console.log();
}

// ── run an action ─────────────────────────────────────────────────────

async function runAction(action) {
console.log();
sep();
console.log(` ${chalk.cyan("→")} ${chalk.gray(action.description)}`);
console.log(` ${chalk.gray("category:")} ${chalk.white(action.category)}`);
prompts.log.info(action.description);
prompts.log.message(
`${chalk.dim("Category:")} ${chalk.white(action.category)}`,
);

const start = Date.now();

try {
await executeCommands(action.commands, (cmd) => printCommand(cmd));

const secs = ((Date.now() - start) / 1000).toFixed(1);
printFinished(secs);
} catch (err) {
console.log();
printError(err.message);
sep();
await executeCommands(action.commands, (command) => printCommand(command));
const seconds = ((Date.now() - start) / 1000).toFixed(1);
printFinished(seconds);
} catch (error) {
printError(error.message);
}
}

// ── prompt loop ───────────────────────────────────────────────────────

export async function devMode() {
const rl = createInterface({ input: process.stdin, output: process.stdout });

rl.on("SIGINT", () => {
console.log(
`\n\n ${chalk.gray(">")} ${chalk.gray("Exiting dev mode.")}\n`,
);
process.exit(0);
});

const ask = () => {
printMenu();

rl.question(
` ${chalk.white(">")} ${chalk.gray("pick action [1-9] or exit: ")}`,
async (raw) => {
const input = raw.trim().toLowerCase();

if (!input) {
ask();
return;
}

if (input === "exit" || input === "quit") {
console.log(
`\n ${chalk.gray(">")} ${chalk.gray("Exiting dev mode.")}\n`,
);
rl.close();
process.exit(0);
}

const num = parseInt(input, 10);
const action = ACTIONS.find((a) => a.id === num);

if (!action) {
console.log(
`\n ${chalk.red("✗")} ${chalk.red(`"${input}" is not valid — enter 1–9 or exit.`)}`,
);
ask();
return;
}

// ── hand stdin back to the child process ──────────────────────
// readline holds process.stdin and keeps it in line-buffered mode.
// Interactive tools like create-next-app need raw TTY access for
// arrow keys and keyboard navigation. pause() releases that hold
// so the spawned process gets full control of the terminal.
rl.pause();

await runAction(action);

// ── reclaim stdin for the next menu prompt ────────────────────
rl.resume();
ask();
},
);
};

ask();
prompts.intro(`${chalk.bold.white("NOVA")} ${chalk.dim("Development mode")}`);

while (true) {
const selected = await prompts.select({
message: "Choose a safe development action",
options: [
...ACTIONS.map((action) => ({
value: action.id,
label: action.label,
hint: preview(action.commands),
})),
{ value: "exit", label: "Exit development mode" },
],
maxItems: 10,
});

if (prompts.isCancel(selected) || selected === "exit") {
prompts.outro("Exiting development mode.");
return;
}

const action = ACTIONS.find(({ id }) => id === selected);
if (!action) {
printError("The selected development action is unavailable.");
continue;
}

// The select prompt has completed, so the child receives the terminal
// directly through the executor's inherited stdio.
await runAction(action);
}
}
43 changes: 19 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,30 +38,24 @@ On first run, NOVA will ask for your **Anthropic/GenAI API key** and save it to
```
$ nova

-
> Hello, vikash. I'm 'NOVA'
-

> describe your task! create a new react app called my-portfolio using vite

-
◕ thinking... ( 1.8 seconds )
→ Scaffolding a Vite React project called my-portfolio and installing dependencies

$ npx create-vite@latest my-portfolio --template react
[npx output...]

$ cd /Users/vikash/my-portfolio

$ npm install
[npm output...]

-
◕ generating... ( 18.4 seconds )
◓ finished.
-

> describe your task!
┌ NOVA Local terminal assistant for vikash
│
◆ What would you like Nova to do?
│ create a new react app called my-portfolio using vite
│
◇ Thinking complete (1.8s).
│
● Scaffolding a Vite React project called my-portfolio
│
◇ $ npx create-vite@latest my-portfolio --template react
[npx output...]
│
◇ $ npm install
[npm output...]
│
◆ Finished in 18.4 seconds.
│
◇ What would you like Nova to do?
```

---
Expand Down Expand Up @@ -115,6 +109,7 @@ nova --dev
## Tech

- Node.js 18+ (ESM)
- [Clack](https://github.com/bombshell-dev/clack) — prompts, spinners, and terminal UI
- [Anthropic SDK](https://github.com/anthropics/anthropic-sdk-node) — task evaluation
- [Google GenAI](https://ai.google.dev/gemini-api/docs/get-started) — task evaluation
- [chalk](https://github.com/chalk/chalk) — terminal styling
Expand Down
53 changes: 53 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
"bin": {
"nova": "./bin/nova.js"
},
"scripts": {
"test": "node --test"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.39.0",
"@clack/prompts": "1.2.0",
"@google/genai": "^2.9.0",
"chalk": "^5.3.0"
},
Expand Down
Loading
Loading