Skip to content

Commit 0a627de

Browse files
committed
feat: added flags --dry-run --yes and --help (same with no-args) + update catalog on git key
1 parent b184d26 commit 0a627de

File tree

4 files changed

+113
-36
lines changed

4 files changed

+113
-36
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ekaone/json-cli",
3-
"version": "0.1.2",
3+
"version": "0.1.3",
44
"description": "AI-powered CLI task runner with JSON command plans",
55
"keywords": ["ai", "agent", "cli", "task-runner", "llm"],
66
"author": {

src/catalog.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,24 @@ import { z } from "zod";
44
// Allowed commands per type — the whitelist that prevents hallucination
55
// ---------------------------------------------------------------------------
66
export const CATALOG = {
7-
npm: ["install", "run", "build", "test", "publish", "ci"],
8-
pnpm: ["install", "run", "build", "test", "publish", "add", "remove"],
9-
yarn: ["install", "run", "build", "test", "publish", "add", "remove"],
10-
bun: ["install", "run", "build", "test", "publish", "add", "remove"],
11-
git: ["init", "add", "commit", "push", "pull", "clone", "status", "log"],
7+
npm: ["install", "run", "build", "test", "publish", "ci"],
8+
pnpm: ["install", "run", "build", "test", "publish", "add", "remove"],
9+
yarn: ["install", "run", "build", "test", "publish", "add", "remove"],
10+
bun: ["install", "run", "build", "test", "publish", "add", "remove"],
11+
git: [
12+
"init",
13+
"add",
14+
"commit",
15+
"push",
16+
"pull",
17+
"clone",
18+
"status",
19+
"log",
20+
"branch",
21+
"checkout",
22+
"merge",
23+
"stash",
24+
],
1225
shell: ["any"], // escape hatch — always requires extra confirmation
1326
} as const;
1427

@@ -18,16 +31,16 @@ export type CommandType = keyof typeof CATALOG;
1831
// Zod schemas — Layer 2 defense against hallucinated output
1932
// ---------------------------------------------------------------------------
2033
export const StepSchema = z.object({
21-
id: z.number(),
22-
type: z.enum(["npm", "pnpm", "yarn", "bun", "git", "shell"]),
23-
command: z.string(),
24-
args: z.array(z.string()).default([]),
34+
id: z.number(),
35+
type: z.enum(["npm", "pnpm", "yarn", "bun", "git", "shell"]),
36+
command: z.string(),
37+
args: z.array(z.string()).default([]),
2538
description: z.string(),
26-
cwd: z.string().optional(), // optional working directory override
39+
cwd: z.string().optional(), // optional working directory override
2740
});
2841

2942
export const PlanSchema = z.object({
30-
goal: z.string(),
43+
goal: z.string(),
3144
steps: z.array(StepSchema).min(1).max(10),
3245
});
3346

@@ -60,7 +73,10 @@ export function validateStep(step: Step): { valid: boolean; reason?: string } {
6073
// ---------------------------------------------------------------------------
6174
export function buildCatalogPrompt(): string {
6275
const lines = Object.entries(CATALOG).map(([type, commands]) => {
63-
const list = commands[0] === "any" ? "any shell command (use sparingly)" : commands.join(", ");
76+
const list =
77+
commands[0] === "any"
78+
? "any shell command (use sparingly)"
79+
: commands.join(", ");
6480
return ` - ${type}: [${list}]`;
6581
});
6682

src/cli.ts

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,57 @@ import { generatePlan } from "./planner.js";
44
import { runPlan } from "./runner.js";
55
import type { Step } from "./catalog.js";
66

7+
// ---------------------------------------------------------------------------
8+
// Help
9+
// ---------------------------------------------------------------------------
10+
function showHelp(): void {
11+
p.intro("json-cli — AI-powered CLI task runner");
12+
p.log.message(`Usage\n json-cli "<your goal>" [options]\n`);
13+
p.log.message(
14+
`Options
15+
--provider <name> AI provider: claude | openai | ollama (default: claude)
16+
--yes Skip confirmation prompt
17+
--dry-run Show plan without executing
18+
--help Show this help message`,
19+
);
20+
p.log.message(
21+
`Examples
22+
json-cli "please run tests"
23+
json-cli "run tests and build"
24+
json-cli "run tests and build" --yes
25+
json-cli "git add, commit with message 'fix: bug', push"
26+
json-cli "clone https://github.com/user/repo, install deps, run dev"
27+
json-cli "run tests and publish" --provider openai
28+
json-cli "run tests" --dry-run`,
29+
);
30+
p.outro("Docs: https://github.com/ekaone/json-cli");
31+
}
32+
733
// ---------------------------------------------------------------------------
834
// Parse CLI args
9-
// e.g. json-cli "run tests" --provider claude
35+
// e.g. json-cli "run tests" --provider claude --yes --dry-run
1036
// ---------------------------------------------------------------------------
11-
function parseArgs(): { prompt: string; provider: ProviderName } {
37+
function parseArgs(): {
38+
prompt: string;
39+
provider: ProviderName;
40+
yes: boolean;
41+
dryRun: boolean;
42+
} {
1243
const args = process.argv.slice(2);
44+
45+
// show help if no args or --help flag
46+
if (args.length === 0 || args.includes("--help")) {
47+
showHelp();
48+
process.exit(0);
49+
}
50+
1351
const providerFlag = args.indexOf("--provider");
1452
const provider: ProviderName =
1553
providerFlag !== -1 ? (args[providerFlag + 1] as ProviderName) : "claude";
1654

55+
const yes = args.includes("--yes");
56+
const dryRun = args.includes("--dry-run");
57+
1758
const prompt = args
1859
.filter(
1960
(a, i) =>
@@ -22,13 +63,11 @@ function parseArgs(): { prompt: string; provider: ProviderName } {
2263
.join(" ");
2364

2465
if (!prompt) {
25-
console.error(
26-
'Usage: json-cli "<your goal>" [--provider claude|openai|ollama]',
27-
);
28-
process.exit(1);
66+
showHelp();
67+
process.exit(0);
2968
}
3069

31-
return { prompt, provider };
70+
return { prompt, provider, yes, dryRun };
3271
}
3372

3473
// ---------------------------------------------------------------------------
@@ -46,9 +85,11 @@ function formatStep(step: Step): string {
4685
// Main
4786
// ---------------------------------------------------------------------------
4887
async function main() {
49-
const { prompt, provider: providerName } = parseArgs();
88+
const { prompt, provider: providerName, yes, dryRun } = parseArgs();
5089

51-
p.intro(`json-cli — powered by ${providerName}`);
90+
p.intro(
91+
`json-cli — powered by ${providerName}${dryRun ? " (dry run)" : ""}`,
92+
);
5293

5394
// Step 1: Generate plan
5495
const spinner = p.spinner();
@@ -82,27 +123,37 @@ async function main() {
82123
);
83124
}
84125

85-
// Step 4: Confirm
86-
const confirmed = await p.confirm({ message: "Proceed?" });
87-
if (p.isCancel(confirmed) || !confirmed) {
88-
p.cancel("Aborted.");
89-
process.exit(0);
126+
// Step 4: Dry run — show plan and exit
127+
if (dryRun) {
128+
p.outro("Dry run complete — no commands were executed.");
129+
setTimeout(() => process.exit(0), 50);
90130
}
91131

92-
// Step 5: Execute
132+
// Step 5: Confirm — skip if --yes
133+
if (!yes) {
134+
const confirmed = await p.confirm({ message: "Proceed?" });
135+
if (p.isCancel(confirmed) || !confirmed) {
136+
p.cancel("Aborted.");
137+
setTimeout(() => process.exit(0), 50);
138+
}
139+
} else {
140+
p.log.info("Skipping confirmation (--yes)");
141+
}
142+
143+
// Step 6: Execute
93144
console.log("");
94145
const result = await runPlan(plan, (step, i, total) => {
95146
p.log.step(`Step ${i + 1}/${total}: ${formatStep(step)}`);
96147
});
97148

98-
// Step 6: Result
149+
// Step 7: Result
99150
if (result.success) {
100151
p.outro("✅ All steps completed successfully.");
101152
} else {
102153
p.log.error(
103154
`❌ Failed at step ${result.failedStep?.id}: ${result.failedStep?.description}\n${result.error ?? ""}`,
104155
);
105-
process.exit(1);
156+
setTimeout(() => process.exit(1), 50);
106157
}
107158
}
108159

src/runner.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,29 @@ function resolveCommand(step: Step): { bin: string; args: string[] } {
1717
// ---------------------------------------------------------------------------
1818
// Run a single step, streaming stdout/stderr live
1919
// ---------------------------------------------------------------------------
20-
export async function runStep(step: Step): Promise<{ success: boolean; error?: string }> {
20+
export async function runStep(
21+
step: Step,
22+
): Promise<{ success: boolean; error?: string }> {
2123
const { bin, args } = resolveCommand(step);
2224

2325
try {
2426
await execa(bin, args, {
25-
cwd: step.cwd ?? process.cwd(),
27+
cwd: step.cwd ?? process.cwd(),
2628
stdout: "inherit", // stream directly to terminal
2729
stderr: "inherit",
2830
});
2931
return { success: true };
30-
} catch (err) {
31-
const message = err instanceof Error ? err.message : String(err);
32-
return { success: false, error: message };
32+
} catch (err: any) {
33+
const parts = [
34+
`Command: ${bin} ${args.join(" ")}`,
35+
err?.exitCode ? `Exit code: ${err.exitCode}` : null,
36+
err?.stderr ? `Reason: ${err.stderr.trim()}` : null,
37+
!err?.stderr ? (err?.message ?? String(err)) : null,
38+
]
39+
.filter(Boolean)
40+
.join("\n ");
41+
42+
return { success: false, error: parts };
3343
}
3444
}
3545

@@ -38,7 +48,7 @@ export async function runStep(step: Step): Promise<{ success: boolean; error?: s
3848
// ---------------------------------------------------------------------------
3949
export async function runPlan(
4050
plan: Plan,
41-
onStep: (step: Step, index: number, total: number) => void
51+
onStep: (step: Step, index: number, total: number) => void,
4252
): Promise<{ success: boolean; failedStep?: Step; error?: string }> {
4353
const total = plan.steps.length;
4454

0 commit comments

Comments
 (0)