Skip to content

Commit 9be6f77

Browse files
committed
feat: add new dry run functionality
1 parent bd7a6e7 commit 9be6f77

8 files changed

Lines changed: 233 additions & 2 deletions

File tree

src/Cli.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,6 +917,7 @@ describe('subcommands', () => {
917917
list
918918
919919
Global Options:
920+
--dry-run Preview parsed inputs without executing
920921
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
921922
--format <toon|json|yaml|md|jsonl> Output format
922923
--help Show help
@@ -1356,6 +1357,7 @@ describe('help', () => {
13561357
skills add Sync skill files to agents
13571358
13581359
Global Options:
1360+
--dry-run Preview parsed inputs without executing
13591361
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
13601362
--format <toon|json|yaml|md|jsonl> Output format
13611363
--help Show help
@@ -1394,6 +1396,7 @@ describe('help', () => {
13941396
skills add Sync skill files to agents
13951397
13961398
Global Options:
1399+
--dry-run Preview parsed inputs without executing
13971400
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
13981401
--format <toon|json|yaml|md|jsonl> Output format
13991402
--help Show help
@@ -1428,6 +1431,7 @@ describe('help', () => {
14281431
name Name
14291432
14301433
Global Options:
1434+
--dry-run Preview parsed inputs without executing
14311435
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
14321436
--format <toon|json|yaml|md|jsonl> Output format
14331437
--help Show help
@@ -1462,6 +1466,7 @@ describe('help', () => {
14621466
list List PRs
14631467
14641468
Global Options:
1469+
--dry-run Preview parsed inputs without executing
14651470
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
14661471
--format <toon|json|yaml|md|jsonl> Output format
14671472
--help Show help
@@ -1557,6 +1562,7 @@ describe('help', () => {
15571562
skills add Sync skill files to agents
15581563
15591564
Global Options:
1565+
--dry-run Preview parsed inputs without executing
15601566
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
15611567
--format <toon|json|yaml|md|jsonl> Output format
15621568
--help Show help
@@ -1589,6 +1595,7 @@ describe('help', () => {
15891595
Run "tool status" to check deployment progress.
15901596
15911597
Global Options:
1598+
--dry-run Preview parsed inputs without executing
15921599
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
15931600
--format <toon|json|yaml|md|jsonl> Output format
15941601
--help Show help
@@ -1684,6 +1691,7 @@ describe('env', () => {
16841691
Usage: test deploy
16851692
16861693
Global Options:
1694+
--dry-run Preview parsed inputs without executing
16871695
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
16881696
--format <toon|json|yaml|md|jsonl> Output format
16891697
--help Show help
@@ -1722,6 +1730,7 @@ describe('env', () => {
17221730
Usage: test deploy
17231731
17241732
Global Options:
1733+
--dry-run Preview parsed inputs without executing
17251734
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
17261735
--format <toon|json|yaml|md|jsonl> Output format
17271736
--help Show help

src/Cli.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,20 @@ export declare namespace create {
344344
usage?: Usage<args, options>[] | undefined
345345
/** Zod schema for middleware variables. Keys define variable names, schemas define types and defaults. */
346346
vars?: vars | undefined
347+
/**
348+
* Opts into receiving `--dry-run` invocations. When `true`, `run()` is called with `c.dryRun = true`
349+
* instead of the framework returning a schema-derived preview.
350+
*/
351+
dryRun?: true | undefined
347352
/** The root command handler. When provided, creates a leaf CLI with no subcommands. */
348353
run?:
349354
| ((context: {
350355
/** Whether the consumer is an agent (stdout is not a TTY). */
351356
agent: boolean
352357
/** Positional arguments. */
353358
args: InferOutput<args>
359+
/** Whether this is a dry-run invocation. Only `true` when the command sets `dryRun: true`. */
360+
dryRun: boolean
354361
/** Parsed environment variables. */
355362
env: InferOutput<env>
356363
/** Return an error result with optional CTAs. */
@@ -429,6 +436,7 @@ async function serveImpl(
429436

430437
const {
431438
verbose,
439+
dryRun,
432440
format: formatFlag,
433441
formatExplicit,
434442
filterOutput,
@@ -1142,6 +1150,7 @@ async function serveImpl(
11421150
const mwCtx: MiddlewareContext = {
11431151
agent: !human,
11441152
command: path,
1153+
dryRun,
11451154
env: cliEnv,
11461155
error: errorFn,
11471156
format,
@@ -1217,6 +1226,7 @@ async function serveImpl(
12171226
const result = await Command.execute(command, {
12181227
agent: !human,
12191228
argv: rest,
1229+
dryRun,
12201230
env: options.envSchema,
12211231
envSource: options.env,
12221232
format,
@@ -1258,6 +1268,7 @@ async function serveImpl(
12581268
meta: {
12591269
command: path,
12601270
duration,
1271+
...(result.dryRun ? { dryRun: true } : undefined),
12611272
...(cta ? { cta } : undefined),
12621273
},
12631274
})
@@ -1302,6 +1313,8 @@ async function serveImpl(
13021313
/** @internal Options for fetchImpl. */
13031314
declare namespace fetchImpl {
13041315
type Options = {
1316+
/** Whether this is a dry-run invocation. */
1317+
dryRun?: boolean | undefined
13051318
/** CLI-level env schema. */
13061319
envSchema?: z.ZodObject<any> | undefined
13071320
/** Group-level middleware collected during command resolution. */
@@ -1391,6 +1404,7 @@ async function fetchImpl(
13911404
options: fetchImpl.Options = {},
13921405
): Promise<Response> {
13931406
const start = performance.now()
1407+
if (req.headers.get('x-dry-run') === 'true') options = { ...options, dryRun: true }
13941408

13951409
const url = new URL(req.url)
13961410
const segments = url.pathname.split('/').filter(Boolean)
@@ -1542,6 +1556,7 @@ async function executeCommand(
15421556
const result = await Command.execute(command, {
15431557
agent: true,
15441558
argv: rest,
1559+
dryRun: options.dryRun,
15451560
env: options.envSchema,
15461561
format: 'json',
15471562
formatExplicit: true,
@@ -1627,6 +1642,7 @@ async function executeCommand(
16271642
meta: {
16281643
command: path,
16291644
duration,
1645+
...(result.dryRun ? { dryRun: true } : undefined),
16301646
...(cta ? { cta } : undefined),
16311647
},
16321648
},
@@ -1794,6 +1810,7 @@ declare namespace serveImpl {
17941810
/** @internal Extracts built-in flags (--verbose, --format, --json, --llms, --help, --version) from argv. */
17951811
function extractBuiltinFlags(argv: string[]) {
17961812
let verbose = false
1813+
let dryRun = false
17971814
let llms = false
17981815
let llmsFull = false
17991816
let mcp = false
@@ -1811,6 +1828,7 @@ function extractBuiltinFlags(argv: string[]) {
18111828
for (let i = 0; i < argv.length; i++) {
18121829
const token = argv[i]!
18131830
if (token === '--verbose') verbose = true
1831+
else if (token === '--dry-run') dryRun = true
18141832
else if (token === '--llms') llms = true
18151833
else if (token === '--llms-full') llmsFull = true
18161834
else if (token === '--mcp') mcp = true
@@ -1839,6 +1857,7 @@ function extractBuiltinFlags(argv: string[]) {
18391857

18401858
return {
18411859
verbose,
1860+
dryRun,
18421861
format,
18431862
formatExplicit,
18441863
filterOutput,
@@ -2517,6 +2536,14 @@ type CommandDefinition<
25172536
* @default 'all'
25182537
*/
25192538
outputPolicy?: OutputPolicy | undefined
2539+
/**
2540+
* Opts into receiving `--dry-run` invocations. When `true`, `run()` is called with `c.dryRun = true`
2541+
* instead of the framework returning a schema-derived preview. Use this for commands that need custom
2542+
* dry-run logic (e.g. showing which files would be deleted, which repos would be synced).
2543+
*
2544+
* When not set, `--dry-run` returns a preview of parsed inputs and the output schema without calling `run()`.
2545+
*/
2546+
dryRun?: true | undefined
25202547
/** Middleware that runs only for this command, after root and group middleware. */
25212548
middleware?: MiddlewareHandler<vars, cliEnv>[] | undefined
25222549
/** Alternative usage patterns shown in help output. */
@@ -2527,6 +2554,8 @@ type CommandDefinition<
25272554
agent: boolean
25282555
/** Positional arguments. */
25292556
args: InferOutput<args>
2557+
/** Whether this is a dry-run invocation. Only `true` when the command sets `dryRun: true`. */
2558+
dryRun: boolean
25302559
/** Parsed environment variables. */
25312560
env: InferOutput<env>
25322561
/** Return an error result with optional CTAs. */

src/Help.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ describe('formatCommand', () => {
2525
--limit <number> Max PRs to return (default: 30)
2626
2727
Global Options:
28+
--dry-run Preview parsed inputs without executing
2829
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
2930
--format <toon|json|yaml|md|jsonl> Output format
3031
--help Show help
@@ -47,6 +48,7 @@ describe('formatCommand', () => {
4748
Usage: tool ping
4849
4950
Global Options:
51+
--dry-run Preview parsed inputs without executing
5052
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
5153
--format <toon|json|yaml|md|jsonl> Output format
5254
--help Show help
@@ -76,6 +78,7 @@ describe('formatCommand', () => {
7678
title Title
7779
7880
Global Options:
81+
--dry-run Preview parsed inputs without executing
7982
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
8083
--format <toon|json|yaml|md|jsonl> Output format
8184
--help Show help
@@ -153,6 +156,7 @@ describe('formatRoot', () => {
153156
issue list List issues
154157
155158
Global Options:
159+
--dry-run Preview parsed inputs without executing
156160
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
157161
--format <toon|json|yaml|md|jsonl> Output format
158162
--help Show help
@@ -178,6 +182,7 @@ describe('formatRoot', () => {
178182
ping Health check
179183
180184
Global Options:
185+
--dry-run Preview parsed inputs without executing
181186
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
182187
--format <toon|json|yaml|md|jsonl> Output format
183188
--help Show help
@@ -207,6 +212,7 @@ describe('formatRoot', () => {
207212
fetch Fetch a URL
208213
209214
Global Options:
215+
--dry-run Preview parsed inputs without executing
210216
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
211217
--format <toon|json|yaml|md|jsonl> Output format
212218
--help Show help
@@ -236,6 +242,7 @@ describe('formatRoot', () => {
236242
url URL to fetch
237243
238244
Global Options:
245+
--dry-run Preview parsed inputs without executing
239246
--filter-output <keys> Filter output by key paths (e.g. foo,bar.baz,a[0,3])
240247
--format <toon|json|yaml|md|jsonl> Output format
241248
--help Show help

src/Help.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,7 @@ function globalOptionsLines(root = false): string[] {
344344
}
345345

346346
const flags = [
347+
{ flag: '--dry-run', desc: 'Preview parsed inputs without executing' },
347348
{
348349
flag: '--filter-output <keys>',
349350
desc: 'Filter output by key paths (e.g. foo,bar.baz,a[0,3])',

src/Mcp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,12 @@ export async function callTool(
9191
...((tool.command.middleware as MiddlewareHandler[] | undefined) ?? []),
9292
]
9393

94+
const dryRun = (options.extra?._meta as any)?.dryRun === true
95+
9496
const result = await Command.execute(tool.command, {
9597
agent: true,
9698
argv: [],
99+
dryRun: dryRun || undefined,
97100
env: options.env,
98101
format: 'json',
99102
formatExplicit: true,

0 commit comments

Comments
 (0)