1pub mod commands;
2pub mod errors;
3pub mod output;
4
5use crate::model::FormulaParsePolicy;
6use anyhow::Result;
7use clap::{Args, Parser, Subcommand, ValueEnum};
8use serde_json::Value;
9use std::ffi::OsString;
10use std::path::PathBuf;
11
12#[derive(Debug, Clone, Copy, ValueEnum)]
13pub enum OutputFormat {
14 Json,
15 Csv,
16}
17
18#[derive(Debug, Clone, Copy, ValueEnum)]
19pub enum TableReadFormat {
20 Json,
21 Values,
22 Csv,
23}
24
25#[derive(Debug, Clone, Copy, ValueEnum)]
26pub enum RangeValuesFormatArg {
27 Json,
28 Values,
29 Csv,
30 Dense,
31 Rows,
32}
33
34#[derive(Debug, Clone, Copy, ValueEnum)]
35pub enum SheetPageFormatArg {
36 #[value(name = "full")]
37 Full,
38 #[value(name = "compact")]
39 Compact,
40 #[value(name = "values_only")]
41 ValuesOnly,
42}
43
44#[derive(Debug, Clone, Copy, ValueEnum)]
45pub enum TableSampleModeArg {
46 First,
47 Last,
48 Distributed,
49}
50
51#[derive(Debug, Clone, Copy, ValueEnum)]
52pub enum OutputShape {
53 Canonical,
54 Compact,
55}
56
57#[derive(Debug, Clone, Copy, ValueEnum)]
58pub enum FindValueMode {
59 Value,
60 Label,
61}
62
63#[derive(Debug, Clone, Copy, ValueEnum)]
64pub enum LabelDirectionArg {
65 Right,
66 Below,
67 Any,
68}
69
70#[derive(Debug, Clone, Copy, ValueEnum)]
71pub enum FormulaSort {
72 Complexity,
73 Count,
74}
75
76#[derive(Debug, Clone, Copy, ValueEnum)]
77pub enum TraceDirectionArg {
78 Precedents,
79 Dependents,
80}
81
82#[derive(Debug, Clone, Copy, ValueEnum)]
83pub enum LayoutModeArg {
84 Values,
85 Formulas,
86}
87
88#[derive(Debug, Clone, Copy, ValueEnum)]
89pub enum LayoutRenderArg {
90 Json,
91 Ascii,
92 Both,
93}
94
95#[derive(Debug, Clone, Copy, ValueEnum)]
96pub enum AppendRegionFooterPolicyArg {
97 Auto,
98 BeforeFooter,
99 AppendAtEnd,
100}
101
102#[derive(Debug, Clone, Copy, ValueEnum)]
103pub enum ClonePatchTargetsArg {
104 LikelyInputs,
105 AllNonFormula,
106 None,
107}
108
109#[derive(Debug, Clone, Copy, ValueEnum)]
110pub enum CloneMergePolicyArg {
111 Safe,
112 Strict,
113}
114
115#[derive(Debug, Subcommand)]
116pub enum SheetportManifestCommands {
117 #[command(
118 about = "Discover candidate SheetPort ports from workbook structure",
119 after_long_help = "Examples:\n agent-spreadsheet sheetport manifest candidates deal_model.xlsx\n agent-spreadsheet sheetport manifest candidates deal_model.xlsx --sheet-filter Assumptions"
120 )]
121 Candidates {
122 #[arg(value_name = "FILE", help = "Path to the workbook")]
123 file: PathBuf,
124 #[arg(long, value_name = "SHEET", help = "Optional sheet filter")]
125 sheet_filter: Option<String>,
126 },
127 #[command(about = "Print the canonical SheetPort JSON schema")]
128 Schema,
129 #[command(
130 about = "Validate a SheetPort manifest",
131 after_long_help = "Example:\n agent-spreadsheet sheetport manifest validate manifest.yaml"
132 )]
133 Validate {
134 #[arg(value_name = "MANIFEST", help = "Path to the YAML manifest")]
135 manifest: PathBuf,
136 },
137 #[command(
138 about = "Normalize a SheetPort manifest for deterministic diffs",
139 after_long_help = "Examples:\n agent-spreadsheet sheetport manifest normalize manifest.yaml\n agent-spreadsheet sheetport manifest normalize manifest.yaml --output manifest.normalized.yaml"
140 )]
141 Normalize {
142 #[arg(value_name = "MANIFEST", help = "Path to the YAML manifest")]
143 manifest: PathBuf,
144 #[arg(long, value_name = "PATH", help = "Write normalized YAML to this file")]
145 output: Option<PathBuf>,
146 },
147}
148
149#[derive(Debug, Subcommand)]
150pub enum SheetportCommands {
151 #[command(about = "Manifest lifecycle helpers", subcommand)]
152 Manifest(SheetportManifestCommands),
153 #[command(
154 about = "Bind-check a workbook against a SheetPort manifest",
155 after_long_help = "Example:\n agent-spreadsheet sheetport bind-check deal_model.xlsx manifest.yaml"
156 )]
157 BindCheck {
158 #[arg(value_name = "FILE", help = "Path to the workbook")]
159 file: PathBuf,
160 #[arg(value_name = "MANIFEST", help = "Path to the YAML manifest")]
161 manifest: PathBuf,
162 },
163 #[command(
164 about = "Execute a SheetPort manifest with JSON inputs",
165 after_long_help = "Examples:\n agent-spreadsheet sheetport run data.xlsx manifest.yaml --inputs '{\"loan\": 10000}'\n agent-spreadsheet sheetport run data.xlsx manifest.yaml"
166 )]
167 Run {
168 #[arg(value_name = "FILE", help = "Path to the workbook")]
169 file: PathBuf,
170 #[arg(value_name = "MANIFEST", help = "Path to the YAML manifest")]
171 manifest: PathBuf,
172 #[arg(long, help = "JSON string or @file containing input arguments")]
173 inputs: Option<String>,
174 #[arg(long, help = "Seed for deterministic RNG evaluation")]
175 rng_seed: Option<u64>,
176 #[arg(long, help = "Freeze volatile functions (e.g. NOW(), RAND())")]
177 freeze_volatile: bool,
178 },
179}
180
181#[derive(Debug, Subcommand)]
182pub enum SessionCommands {
183 #[command(about = "Start a new session tracking a base workbook file")]
184 Start {
185 #[arg(long, value_name = "FILE", help = "Path to the base workbook")]
186 base: PathBuf,
187 #[arg(long, value_name = "LABEL", help = "Human-readable session label")]
188 label: Option<String>,
189 #[arg(
190 long,
191 value_name = "PATH",
192 help = "Workspace root directory (default: cwd)"
193 )]
194 workspace: Option<PathBuf>,
195 },
196 #[command(about = "View the event timeline for a session")]
197 Log {
198 #[arg(long, value_name = "ID", help = "Session identifier")]
199 session: String,
200 #[arg(long, value_name = "OP_ID", help = "Show events since this op_id")]
201 since: Option<String>,
202 #[arg(
203 long,
204 value_name = "KIND",
205 help = "Filter by operation kind prefix (e.g. structure)"
206 )]
207 kind: Option<String>,
208 #[arg(long, value_name = "PATH", help = "Workspace root directory")]
209 workspace: Option<PathBuf>,
210 },
211 #[command(about = "List branches in a session")]
212 Branches {
213 #[arg(long, value_name = "ID", help = "Session identifier")]
214 session: String,
215 #[arg(long, value_name = "PATH", help = "Workspace root directory")]
216 workspace: Option<PathBuf>,
217 },
218 #[command(about = "Switch to a different branch")]
219 Switch {
220 #[arg(long, value_name = "ID", help = "Session identifier")]
221 session: String,
222 #[arg(long, value_name = "NAME", help = "Branch name to switch to")]
223 branch: String,
224 #[arg(long, value_name = "PATH", help = "Workspace root directory")]
225 workspace: Option<PathBuf>,
226 },
227 #[command(about = "Set HEAD to a specific event (time-travel)")]
228 Checkout {
229 #[arg(long, value_name = "ID", help = "Session identifier")]
230 session: String,
231 #[arg(value_name = "OP_ID", help = "Event identifier to checkout")]
232 op_id: String,
233 #[arg(long, value_name = "PATH", help = "Workspace root directory")]
234 workspace: Option<PathBuf>,
235 },
236 #[command(about = "Move HEAD back one event (branch-local undo)")]
237 Undo {
238 #[arg(long, value_name = "ID", help = "Session identifier")]
239 session: String,
240 #[arg(long, value_name = "PATH", help = "Workspace root directory")]
241 workspace: Option<PathBuf>,
242 },
243 #[command(about = "Move HEAD forward one event (branch-local redo)")]
244 Redo {
245 #[arg(long, value_name = "ID", help = "Session identifier")]
246 session: String,
247 #[arg(long, value_name = "PATH", help = "Workspace root directory")]
248 workspace: Option<PathBuf>,
249 },
250 #[command(about = "Create a new branch forking from a given event")]
251 Fork {
252 #[arg(long, value_name = "ID", help = "Session identifier")]
253 session: String,
254 #[arg(
255 long,
256 value_name = "OP_ID",
257 help = "Fork from this event (default: current HEAD)"
258 )]
259 from: Option<String>,
260 #[arg(long, value_name = "LABEL", help = "Human-readable branch label")]
261 label: Option<String>,
262 #[arg(value_name = "NAME", help = "New branch name")]
263 branch_name: String,
264 #[arg(long, value_name = "PATH", help = "Workspace root directory")]
265 workspace: Option<PathBuf>,
266 },
267 #[command(
268 about = "Stage an operation (compute dry-run impact without advancing HEAD)",
269 after_long_help = "Canonical session payload contract:\n • Every payload must include a top-level kind field.\n • transform.write_matrix is a flat object with sheet_name/anchor/rows.\n • Batch families use an ops array envelope.\n\nExamples:\n asp session op --session sess_abc123 --ops @write_matrix.json\n\n write_matrix.json\n {\n \"kind\": \"transform.write_matrix\",\n \"sheet_name\": \"Sheet1\",\n \"anchor\": \"B7\",\n \"rows\": [[\"Revenue\", 100]]\n }\n\n asp session op --session sess_abc123 --ops @structure_ops.json\n\n structure_ops.json\n {\n \"kind\": \"structure.insert_rows\",\n \"ops\": [{ \"sheet_name\": \"Sheet1\", \"at\": 12, \"count\": 2 }]\n }"
270 )]
271 Op {
272 #[arg(long, value_name = "ID", help = "Session identifier")]
273 session: String,
274 #[arg(
275 long,
276 value_name = "OPS_REF",
277 help = "Ops payload file reference (@path)"
278 )]
279 ops: String,
280 #[arg(long, value_name = "PATH", help = "Workspace root directory")]
281 workspace: Option<PathBuf>,
282 },
283 #[command(about = "Apply a staged operation (compare-and-swap against current HEAD)")]
284 Apply {
285 #[arg(long, value_name = "ID", help = "Session identifier")]
286 session: String,
287 #[arg(value_name = "STAGED_ID", help = "Staged operation identifier")]
288 staged_id: String,
289 #[arg(long, value_name = "PATH", help = "Workspace root directory")]
290 workspace: Option<PathBuf>,
291 },
292 #[command(about = "Compile the current HEAD into a standalone Excel file")]
293 Materialize {
294 #[arg(long, value_name = "ID", help = "Session identifier")]
295 session: String,
296 #[arg(long, value_name = "PATH", help = "Output file path")]
297 output: PathBuf,
298 #[arg(long, help = "Allow overwriting existing output file")]
299 force: bool,
300 #[arg(long, value_name = "PATH", help = "Workspace root directory")]
301 workspace: Option<PathBuf>,
302 },
303}
304
305#[derive(Debug, Subcommand)]
306pub enum DiscoverabilityCommands {
307 #[command(about = "Schema/example target for transform-batch payloads")]
308 TransformBatch,
309 #[command(about = "Schema/example target for style-batch payloads")]
310 StyleBatch,
311 #[command(about = "Schema/example target for apply-formula-pattern payloads")]
312 ApplyFormulaPattern,
313 #[command(about = "Schema/example target for structure-batch payloads")]
314 StructureBatch,
315 #[command(about = "Schema/example target for column-size-batch payloads")]
316 ColumnSizeBatch,
317 #[command(about = "Schema/example target for sheet-layout-batch payloads")]
318 SheetLayoutBatch,
319 #[command(about = "Schema/example target for rules-batch payloads")]
320 RulesBatch,
321 #[command(about = "Schema/example target for event-sourced session op payloads")]
322 SessionOp {
323 #[arg(
324 value_name = "KIND",
325 help = "Exact session op kind, e.g. transform.write_matrix"
326 )]
327 kind: String,
328 },
329}
330
331#[derive(Debug, Args, Clone)]
332struct SurfaceLeafArgs {
333 #[arg(
334 trailing_var_arg = true,
335 allow_hyphen_values = true,
336 num_args = 0..,
337 value_name = "ARGS"
338 )]
339 args: Vec<OsString>,
340}
341
342#[derive(Debug, Subcommand)]
343enum SurfaceReadCommands {
344 #[command(about = "List workbook sheets with basic summary metadata")]
345 Sheets(SurfaceLeafArgs),
346 #[command(about = "Inspect one sheet and detect structured regions")]
347 Overview(SurfaceLeafArgs),
348 #[command(about = "Read raw values for one or more A1 ranges")]
349 Values(SurfaceLeafArgs),
350 #[command(about = "Export a range to a specific format")]
351 Export(SurfaceLeafArgs),
352 #[command(about = "Inspect detail snapshots for targeted A1 cells/ranges")]
353 Cells(SurfaceLeafArgs),
354 #[command(about = "Read one sheet page with deterministic continuation")]
355 Page(SurfaceLeafArgs),
356 #[command(about = "Read a table-like region as json, values, or csv")]
357 Table(SurfaceLeafArgs),
358 #[command(about = "List workbook named ranges and table/formula named items")]
359 Names(SurfaceLeafArgs),
360 #[command(about = "Describe workbook-level metadata and sheet counts")]
361 Workbook(SurfaceLeafArgs),
362 #[command(about = "Render a range with layout metadata")]
363 Layout(SurfaceLeafArgs),
364}
365
366#[derive(Debug, Subcommand)]
367enum SurfaceAnalyzeCommands {
368 #[command(about = "Find cells matching a text query by value or label")]
369 FindValue(SurfaceLeafArgs),
370 #[command(about = "Find formulas containing a text query with pagination")]
371 FindFormula(SurfaceLeafArgs),
372 #[command(about = "Summarize formulas on a sheet by complexity or frequency")]
373 FormulaMap(SurfaceLeafArgs),
374 #[command(about = "Trace formula precedents or dependents from one origin cell")]
375 FormulaTrace(SurfaceLeafArgs),
376 #[command(about = "Scan workbook formulas for volatile functions")]
377 ScanVolatiles(SurfaceLeafArgs),
378 #[command(about = "Compute per-sheet statistics for density and column types")]
379 SheetStatistics(SurfaceLeafArgs),
380 #[command(about = "Profile table headers, types, and column distributions")]
381 TableProfile(SurfaceLeafArgs),
382 #[command(about = "Analyze structural operation impact without mutation")]
383 RefImpact(SurfaceLeafArgs),
384}
385
386#[derive(Debug, Subcommand)]
387enum SurfaceWriteFormulaCommands {
388 #[command(about = "Find and replace text in formula bodies (not values)")]
389 Replace(SurfaceLeafArgs),
390}
391
392#[derive(Debug, Subcommand)]
393enum SurfaceWriteNameCommands {
394 #[command(about = "Define a new named range in a workbook")]
395 Define(SurfaceLeafArgs),
396 #[command(about = "Update an existing named range")]
397 Update(SurfaceLeafArgs),
398 #[command(about = "Delete a named range from a workbook")]
399 Delete(SurfaceLeafArgs),
400}
401
402#[derive(Debug, Subcommand)]
403enum SurfaceWriteBatchCommands {
404 #[command(about = "Apply stateless transform operations from an @ops payload")]
405 Transform(SurfaceLeafArgs),
406 #[command(about = "Apply stateless style operations from an @ops payload")]
407 Style(SurfaceLeafArgs),
408 #[command(about = "Apply stateless formula pattern operations from an @ops payload")]
409 FormulaPattern(SurfaceLeafArgs),
410 #[command(about = "Apply stateless structure operations from an @ops payload")]
411 Structure(SurfaceLeafArgs),
412 #[command(about = "Apply stateless column sizing operations from an @ops payload")]
413 ColumnSize(SurfaceLeafArgs),
414 #[command(about = "Apply stateless sheet layout operations from an @ops payload")]
415 SheetLayout(SurfaceLeafArgs),
416 #[command(
417 about = "Apply stateless data validation and conditional format operations from an @ops payload"
418 )]
419 Rules(SurfaceLeafArgs),
420}
421
422#[derive(Debug, Subcommand)]
423enum SurfaceWriteCommands {
424 #[command(about = "Apply one or more shorthand cell edits to a sheet")]
425 Cells(SurfaceLeafArgs),
426 #[command(about = "Import range data from grid JSON or CSV")]
427 Import(SurfaceLeafArgs),
428 #[command(about = "Append rows into a detected region with footer-aware insertion")]
429 Append(SurfaceLeafArgs),
430 #[command(about = "Clone one template row into inserted rows with preview-first planning")]
431 CloneTemplateRow(SurfaceLeafArgs),
432 #[command(about = "Clone a contiguous template row band with preview-first planning")]
433 CloneRowBand(SurfaceLeafArgs),
434 #[command(subcommand, about = "Formula-only mutation helpers")]
435 Formulas(SurfaceWriteFormulaCommands),
436 #[command(subcommand, about = "Named range mutation helpers")]
437 Name(SurfaceWriteNameCommands),
438 #[command(subcommand, about = "Stateless batch mutation surfaces")]
439 Batch(SurfaceWriteBatchCommands),
440}
441
442#[derive(Debug, Subcommand)]
443enum SurfaceWorkbookCommands {
444 #[command(about = "Create a new workbook at a destination path")]
445 Create(SurfaceLeafArgs),
446 #[command(about = "Copy a workbook to a new path for safe edits")]
447 Copy(SurfaceLeafArgs),
448 #[command(about = "Recalculate workbook formulas")]
449 Recalculate(SurfaceLeafArgs),
450}
451
452#[derive(Debug, Subcommand)]
453enum SurfaceVerifyCommands {
454 #[command(about = "Compare two workbook states and verify target deltas plus error provenance")]
455 Proof(SurfaceLeafArgs),
456 #[command(about = "Diff two workbook versions with summary-first, paged details")]
457 Diff(SurfaceLeafArgs),
458}
459
460#[derive(Debug, Subcommand)]
461enum SurfaceDiscoverabilityBatchCommands {
462 #[command(about = "Schema/example target for transform batch payloads")]
463 Transform,
464 #[command(about = "Schema/example target for style batch payloads")]
465 Style,
466 #[command(about = "Schema/example target for formula pattern batch payloads")]
467 FormulaPattern,
468 #[command(about = "Schema/example target for structure batch payloads")]
469 Structure,
470 #[command(about = "Schema/example target for column size batch payloads")]
471 ColumnSize,
472 #[command(about = "Schema/example target for sheet layout batch payloads")]
473 SheetLayout,
474 #[command(about = "Schema/example target for rules batch payloads")]
475 Rules,
476}
477
478#[derive(Debug, Subcommand)]
479enum SurfaceDiscoverabilityWriteCommands {
480 #[command(subcommand, about = "Batch payload targets")]
481 Batch(SurfaceDiscoverabilityBatchCommands),
482}
483
484#[derive(Debug, Subcommand)]
485enum SurfaceDiscoverabilitySessionCommands {
486 #[command(about = "Schema/example target for event-sourced session op payloads")]
487 Op {
488 #[arg(
489 value_name = "KIND",
490 help = "Exact session op kind, e.g. transform.write_matrix"
491 )]
492 kind: String,
493 },
494}
495
496#[derive(Debug, Subcommand)]
497enum SurfaceDiscoverabilityCommands {
498 #[command(subcommand, about = "Discoverability targets for write payloads")]
499 Write(SurfaceDiscoverabilityWriteCommands),
500 #[command(subcommand, about = "Discoverability targets for session payloads")]
501 Session(SurfaceDiscoverabilitySessionCommands),
502}
503
504#[derive(Debug, Subcommand)]
505enum SurfaceCommands {
506 #[command(subcommand, about = "Read workbook data and structure")]
507 Read(SurfaceReadCommands),
508 #[command(subcommand, about = "Analyze workbook contents and formulas")]
509 Analyze(SurfaceAnalyzeCommands),
510 #[command(subcommand, about = "Write and mutate workbook contents")]
511 Write(SurfaceWriteCommands),
512 #[command(subcommand, about = "Workbook-level file operations")]
513 Workbook(SurfaceWorkbookCommands),
514 #[command(subcommand, about = "Verification and review workflows")]
515 Verify(SurfaceVerifyCommands),
516 #[command(about = "Print canonical JSON schema for a command or payload target")]
517 Schema {
518 #[command(subcommand)]
519 command: SurfaceDiscoverabilityCommands,
520 },
521 #[command(about = "Print a copy-pastable canonical example for a command or payload target")]
522 Example {
523 #[command(subcommand)]
524 command: SurfaceDiscoverabilityCommands,
525 },
526 #[command(about = "Event-sourced session management", subcommand, hide = false)]
527 Session(Box<SessionCommands>),
528 #[command(about = "SheetPort manifest lifecycle and execution commands")]
529 Sheetport {
530 #[command(subcommand)]
531 command: SheetportCommands,
532 },
533}
534
535#[derive(Debug, Parser)]
536#[command(
537 name = "asp",
538 version,
539 about = "Stateless spreadsheet CLI for reads, writes, and verification workflows",
540 long_about = "Stateless spreadsheet CLI for AI and automation workflows.\n\nPrimary command: asp\nCompatibility alias: agent-spreadsheet\n\nVerify install:\n asp --version\n asp --help\n\nPrimary groups:\n • read -> workbook extraction and inspection\n • analyze -> search, profiling, and diagnostics\n • write -> direct edits, workflow helpers, and batch mutations\n • workbook -> file-level create/copy/recalculate flows\n • verify -> proof and diff review surfaces\n • session -> event-sourced stateful editing\n • sheetport -> manifest lifecycle and execution\n\nDiscoverability:\n • asp schema write batch transform\n • asp example write batch transform\n • asp schema session op transform.write_matrix\n\nTip: global --output-format csv is currently unsupported and returns an error. Use --output-format json, or command-level CSV options such as asp read table --table-format csv."
541)]
542struct SurfaceCli {
543 #[arg(
544 long = "output-format",
545 value_enum,
546 default_value_t = OutputFormat::Json,
547 global = true,
548 help = "Output format (csv is currently unsupported globally; use json or command-specific CSV options like asp read table --table-format csv)"
549 )]
550 output_format: OutputFormat,
551
552 #[arg(
553 long,
554 value_enum,
555 default_value_t = OutputShape::Canonical,
556 global = true,
557 help = "Output shape (canonical keeps full schema; compact applies command-specific projections while preserving stable payload contracts for range-values/read-table/sheet-page; formula-trace compact omits per-layer highlights while preserving continuation fields)"
558 )]
559 shape: OutputShape,
560
561 #[arg(
562 long,
563 global = true,
564 help = "Emit compact JSON without pretty-printing (default behavior)"
565 )]
566 compact: bool,
567
568 #[arg(long, global = true, help = "Suppress non-fatal warnings")]
569 quiet: bool,
570
571 #[command(subcommand)]
572 command: SurfaceCommands,
573}
574
575#[derive(Debug, Parser)]
576#[command(
577 name = "asp",
578 version,
579 about = "Stateless spreadsheet CLI for reads, edits, and diffs",
580 long_about = "Stateless spreadsheet CLI for AI and automation workflows.\n\nPrimary command: asp\nCompatibility alias: agent-spreadsheet\n\nVerify install:\n asp --version\n asp --help\n\nCommon workflows:\n • Inspect a workbook: list-sheets → sheet-overview → table-profile\n • Deterministic pagination loops: sheet-page (--format + next_start_row) and read-table (--limit/--offset + next_offset)\n • Find labels or values: find-value --mode label|value\n • Discover payload contracts: schema <target> / example <target>\n • Stateless batch writes: transform/style/formula/structure/column/layout/rules via --ops @ops.json + one mode (--dry-run|--in-place|--output)\n • Copy → edit → recalculate → diff for safe what-if changes\n • SheetPort manifest loop: sheetport manifest candidates → draft/edit YAML → sheetport manifest validate → sheetport bind-check → sheetport run\n\nTip: global --output-format csv is currently unsupported and returns an error. Use --output-format json, or command-level CSV options such as read-table --table-format csv."
581)]
582pub struct Cli {
583 #[arg(
584 long = "output-format",
585 value_enum,
586 default_value_t = OutputFormat::Json,
587 global = true,
588 help = "Output format (csv is currently unsupported globally; use json or command-specific CSV options like read-table --table-format csv)"
589 )]
590 pub output_format: OutputFormat,
591
592 #[arg(
593 long,
594 value_enum,
595 default_value_t = OutputShape::Canonical,
596 global = true,
597 help = "Output shape (canonical keeps full schema; compact applies command-specific projections while preserving stable payload contracts for range-values/read-table/sheet-page; formula-trace compact omits per-layer highlights while preserving continuation fields)"
598 )]
599 pub shape: OutputShape,
600
601 #[arg(
602 long,
603 global = true,
604 help = "Emit compact JSON without pretty-printing (default behavior)"
605 )]
606 pub compact: bool,
607
608 #[arg(long, global = true, help = "Suppress non-fatal warnings")]
609 pub quiet: bool,
610
611 #[command(subcommand)]
612 pub command: Commands,
613}
614
615#[derive(Debug, Subcommand)]
616pub enum Commands {
617 #[command(about = "List workbook sheets with basic summary metadata")]
618 ListSheets {
619 #[arg(value_name = "FILE", help = "Path to the workbook (.xlsx/.xlsm)")]
620 file: PathBuf,
621 #[arg(
622 long,
623 value_name = "ID",
624 help = "Read from a session's materialized state instead of the file"
625 )]
626 session: Option<String>,
627 #[arg(
628 long = "session-workspace",
629 value_name = "PATH",
630 help = "Workspace root for session resolution"
631 )]
632 session_workspace: Option<PathBuf>,
633 },
634 #[command(about = "Inspect one sheet and detect structured regions")]
635 SheetOverview {
636 #[arg(value_name = "FILE", help = "Path to the workbook")]
637 file: PathBuf,
638 #[arg(
639 value_name = "SHEET",
640 help = "Exact sheet name (quote names with spaces)"
641 )]
642 sheet: String,
643 #[arg(
644 long,
645 value_name = "ID",
646 help = "Read from a session's materialized state instead of the file"
647 )]
648 session: Option<String>,
649 #[arg(
650 long = "session-workspace",
651 value_name = "PATH",
652 help = "Workspace root for session resolution"
653 )]
654 session_workspace: Option<PathBuf>,
655 },
656 #[command(
657 about = "Read raw values for one or more A1 ranges",
658 after_long_help = "Examples:\n agent-spreadsheet range-values data.xlsx Sheet1 A1:C20\n agent-spreadsheet range-values data.xlsx \"Q1 Actuals\" A1:B5 D10:E20\n agent-spreadsheet range-values data.xlsx Sheet1 A1:C20 --include-formulas\n\nDense default:\n range-values defaults to dense JSON encoding optimized for agent consumption:\n dictionary + row_runs + optional sparse formulas.\n\nFormula semantics:\n By default, range-values returns resolved values only.\n Use --include-formulas to include formulas in the response (sparse list in dense mode, matrix in json mode).\n\nShape behavior:\n range-values keeps a stable top-level shape in both canonical and compact modes (no single-range flattening).\n\nRelated:\n Use inspect-cells when you need formula + value + style metadata in one response."
659 )]
660 RangeValues {
661 #[arg(value_name = "FILE", help = "Path to the workbook")]
662 file: PathBuf,
663 #[arg(value_name = "SHEET", help = "Sheet name containing the ranges")]
664 sheet: String,
665 #[arg(
666 value_name = "RANGE",
667 help = "One or more A1 ranges (for example A1:C10)"
668 )]
669 ranges: Vec<String>,
670 #[arg(
671 long,
672 value_enum,
673 value_name = "FORMAT",
674 help = "Output payload format (dense default, or json/values/csv explicitly)"
675 )]
676 format: Option<RangeValuesFormatArg>,
677 #[arg(
678 long = "include-formulas",
679 value_name = "BOOL",
680 num_args = 0..=1,
681 default_missing_value = "true",
682 help = "Include formulas (sparse list in dense mode, matrix in json mode)"
683 )]
684 include_formulas: Option<bool>,
685 #[arg(
686 long,
687 value_name = "ID",
688 help = "Read from a session's materialized state instead of the file"
689 )]
690 session: Option<String>,
691 #[arg(
692 long = "session-workspace",
693 value_name = "PATH",
694 help = "Workspace root for session resolution"
695 )]
696 session_workspace: Option<PathBuf>,
697 },
698 #[command(
699 about = "Export a range to a specific format (e.g., csv, grid)",
700 after_long_help = "Examples:\n agent-spreadsheet range-export data.xlsx Sheet1 A1:C20 --format csv --output data.csv\n agent-spreadsheet range-export data.xlsx Sheet1 A1:C20 --format csv --output -"
701 )]
702 RangeExport {
703 #[arg(value_name = "FILE", help = "Path to the workbook")]
704 file: PathBuf,
705 #[arg(value_name = "SHEET", help = "Sheet name containing the range")]
706 sheet: String,
707 #[arg(value_name = "RANGE", help = "A1 range (for example A1:C10)")]
708 range: String,
709 #[arg(long, help = "Output format (e.g. csv, grid)", default_value = "json")]
710 format: String,
711 #[arg(long, help = "Output path or '-' for stdout")]
712 output: Option<String>,
713 #[arg(
714 long = "include-formulas",
715 value_name = "BOOL",
716 num_args = 0..=1,
717 default_missing_value = "true",
718 help = "Include parsed formulas in formula cells alongside evaluated values (JSON only)"
719 )]
720 include_formulas: Option<bool>,
721 #[arg(
722 long,
723 value_name = "ID",
724 help = "Read from a session's materialized state instead of the file"
725 )]
726 session: Option<String>,
727 #[arg(
728 long = "session-workspace",
729 value_name = "PATH",
730 help = "Workspace root for session resolution"
731 )]
732 session_workspace: Option<PathBuf>,
733 },
734 #[command(
735 about = "Import range data from grid JSON or CSV",
736 after_long_help = "Examples:\n agent-spreadsheet range-import data.xlsx Sheet1 --anchor B7 --from-grid region.json\n agent-spreadsheet range-import data.xlsx Sheet1 --anchor B7 --from-csv data.csv --in-place"
737 )]
738 RangeImport {
739 #[arg(value_name = "FILE", help = "Path to the workbook")]
740 file: PathBuf,
741 #[arg(value_name = "SHEET", help = "Sheet name to import into")]
742 sheet: String,
743 #[arg(long, help = "Anchor cell for import (e.g. B7)")]
744 anchor: String,
745 #[arg(long, help = "Path to the grid JSON file to import")]
746 from_grid: Option<String>,
747 #[arg(long, help = "Path to the CSV file to import")]
748 from_csv: Option<String>,
749 #[arg(long, help = "Skip first CSV row when importing --from-csv")]
750 header: bool,
751 #[arg(long, help = "Clear the target area before import")]
752 clear_target: bool,
753 #[arg(long, help = "Validate ops without mutating files")]
754 dry_run: bool,
755 #[arg(long, help = "Apply imports by atomically replacing the source file")]
756 in_place: bool,
757 #[arg(long, help = "Apply imports to this output path")]
758 output: Option<PathBuf>,
759 #[arg(long, help = "Allow overwriting --output when it already exists")]
760 force: bool,
761 },
762 #[command(
763 about = "Inspect detail snapshots for targeted A1 cells/ranges (detail view, default max 25 cells)",
764 after_long_help = "Examples:
765 agent-spreadsheet inspect-cells data.xlsx Sheet1 A1:C3
766 agent-spreadsheet inspect-cells data.xlsx \"Q1 Actuals\" D4 D7:F8
767 agent-spreadsheet inspect-cells data.xlsx Sheet1 B2,C4 --include-empty
768 agent-spreadsheet inspect-cells data.xlsx Sheet1 A1:J10 --budget 100
769
770inspect-cells is a detail view for formula/value/cached/style triage and enforces a small per-request cell budget.
771Use --budget to raise the limit for rect-style reads (up to 200).
772For broader discovery, use sheet-page, range-values, or layout-page."
773 )]
774 InspectCells {
775 #[arg(value_name = "FILE", help = "Path to the workbook")]
776 file: PathBuf,
777 #[arg(value_name = "SHEET", help = "Sheet name containing the targets")]
778 sheet: String,
779 #[arg(
780 value_name = "TARGET",
781 value_delimiter = ',',
782 num_args = 1..,
783 help = "One or more A1 cells/ranges (e.g. B2, A1:C3, D7:F8)"
784 )]
785 targets: Vec<String>,
786 #[arg(long, help = "Include empty cells in the response")]
787 include_empty: bool,
788 #[arg(
789 long,
790 value_name = "N",
791 help = "Override the per-request cell budget (default 25, max 200)"
792 )]
793 budget: Option<u32>,
794 #[arg(
795 long,
796 value_name = "ID",
797 help = "Read from a session's materialized state instead of the file"
798 )]
799 session: Option<String>,
800 #[arg(
801 long = "session-workspace",
802 value_name = "PATH",
803 help = "Workspace root for session resolution"
804 )]
805 session_workspace: Option<PathBuf>,
806 },
807 #[command(
808 about = "Read one sheet page with deterministic continuation",
809 after_long_help = "Examples:\n agent-spreadsheet sheet-page data.xlsx Sheet1 --format compact --page-size 200\n agent-spreadsheet sheet-page data.xlsx Sheet1 --format compact --page-size 200 --start-row 201\n agent-spreadsheet sheet-page data.xlsx Sheet1 --format full --columns A,C:E --include-styles\n\nMachine contract:\n - Inspect the top-level format field first.\n - format=full: consume top-level rows/header_row/next_start_row.\n - format=compact: consume compact.headers/compact.header_row/compact.rows plus next_start_row.\n - format=values_only: consume values_only.rows plus next_start_row.\n - Global --shape compact preserves the active sheet-page branch (no flattening).\n\nPagination loop:\n 1) Run without --start-row.\n 2) If next_start_row is present, pass it to --start-row for the next request.\n 3) Stop when next_start_row is omitted.\n\nMachine continuation example:\n Request page 1, read next_start_row, then request page 2 with --start-row <next_start_row>."
810 )]
811 SheetPage {
812 #[arg(value_name = "FILE", help = "Path to the workbook")]
813 file: PathBuf,
814 #[arg(value_name = "SHEET", help = "Sheet to page through")]
815 sheet: String,
816 #[arg(long, value_name = "ROW", help = "1-based starting row")]
817 start_row: Option<u32>,
818 #[arg(
819 long = "page-size",
820 value_name = "N",
821 help = "Rows per page (must be at least 1)"
822 )]
823 page_size: Option<u32>,
824 #[arg(
825 long,
826 value_name = "COLUMNS",
827 value_delimiter = ',',
828 help = "Column selectors by letter/range, e.g. A,C,E:G"
829 )]
830 columns: Option<Vec<String>>,
831 #[arg(
832 long = "columns-by-header",
833 value_name = "HEADERS",
834 value_delimiter = ',',
835 help = "Column selectors by header text (case-insensitive)"
836 )]
837 columns_by_header: Option<Vec<String>>,
838 #[arg(
839 long = "include-formulas",
840 value_name = "BOOL",
841 num_args = 0..=1,
842 default_missing_value = "true",
843 help = "Include formulas (default true)"
844 )]
845 include_formulas: Option<bool>,
846 #[arg(
847 long = "include-styles",
848 value_name = "BOOL",
849 num_args = 0..=1,
850 default_missing_value = "true",
851 help = "Include style metadata (default false)"
852 )]
853 include_styles: Option<bool>,
854 #[arg(
855 long = "include-header",
856 value_name = "BOOL",
857 num_args = 0..=1,
858 default_missing_value = "true",
859 help = "Include header row (default true)"
860 )]
861 include_header: Option<bool>,
862 #[arg(
863 long,
864 value_enum,
865 value_name = "FORMAT",
866 required = true,
867 help = "Page output format: full, compact, or values_only"
868 )]
869 format: SheetPageFormatArg,
870 #[arg(
871 long,
872 value_name = "ID",
873 help = "Read from a session's materialized state instead of the file"
874 )]
875 session: Option<String>,
876 #[arg(
877 long = "session-workspace",
878 value_name = "PATH",
879 help = "Workspace root for session resolution"
880 )]
881 session_workspace: Option<PathBuf>,
882 },
883 #[command(
884 about = "Read a table-like region as json, values, or csv",
885 after_long_help = "Examples:\n agent-spreadsheet read-table data.xlsx --sheet Sheet1 --table-format values\n agent-spreadsheet read-table data.xlsx --sheet Sheet1 --table-format csv --limit 50 --offset 0\n agent-spreadsheet read-table data.xlsx --table-name SalesTable --sample-mode distributed --limit 20\n\nPagination loop:\n Repeat with --offset set to next_offset until next_offset is omitted."
886 )]
887 ReadTable {
888 #[arg(value_name = "FILE", help = "Path to the workbook")]
889 file: PathBuf,
890 #[arg(long, value_name = "SHEET", help = "Restrict read to a specific sheet")]
891 sheet: Option<String>,
892 #[arg(long, value_name = "RANGE", help = "Optional A1 range override")]
893 range: Option<String>,
894 #[arg(long, value_name = "NAME", help = "Read from a named Excel table")]
895 table_name: Option<String>,
896 #[arg(long, value_name = "ID", help = "Read from a detected region id")]
897 region_id: Option<u32>,
898 #[arg(
899 long,
900 value_name = "LIMIT",
901 help = "Maximum rows to return (must be at least 1)"
902 )]
903 limit: Option<u32>,
904 #[arg(long, value_name = "OFFSET", help = "Row offset for pagination")]
905 offset: Option<u32>,
906 #[arg(
907 long = "sample-mode",
908 value_enum,
909 value_name = "MODE",
910 help = "Sampling mode: first, last, or distributed"
911 )]
912 sample_mode: Option<TableSampleModeArg>,
913 #[arg(
914 long = "filters-json",
915 value_name = "JSON",
916 help = "Inline JSON array of filters (mutually exclusive with --filters-file)"
917 )]
918 filters_json: Option<String>,
919 #[arg(
920 long = "filters-file",
921 value_name = "PATH",
922 help = "Path to JSON array of filters (mutually exclusive with --filters-json)"
923 )]
924 filters_file: Option<PathBuf>,
925 #[arg(
926 long = "table-format",
927 value_enum,
928 value_name = "FORMAT",
929 help = "Output format for this command"
930 )]
931 table_format: Option<TableReadFormat>,
932 #[arg(
933 long,
934 value_name = "ID",
935 help = "Read from a session's materialized state instead of the file"
936 )]
937 session: Option<String>,
938 #[arg(
939 long = "session-workspace",
940 value_name = "PATH",
941 help = "Workspace root for session resolution"
942 )]
943 session_workspace: Option<PathBuf>,
944 },
945 #[command(
946 about = "Find cells matching a text query by value or label",
947 after_long_help = "Examples:\n agent-spreadsheet find-value data.xlsx Revenue --mode value\n agent-spreadsheet find-value data.xlsx \"Net Income\" --sheet \"Q1 Actuals\" --mode label --label-direction below\n\nLabel mode behavior:\n - QUERY is matched against label cells.\n - Result value is taken from an adjacent cell, not from the label itself.\n - --label-direction any (default) checks right first, then below."
948 )]
949 FindValue {
950 #[arg(value_name = "FILE", help = "Path to the workbook")]
951 file: PathBuf,
952 #[arg(value_name = "QUERY", help = "Text to search for")]
953 query: String,
954 #[arg(long, value_name = "SHEET", help = "Limit search to one sheet")]
955 sheet: Option<String>,
956 #[arg(
957 long,
958 value_enum,
959 value_name = "MODE",
960 help = "Search mode: value or label"
961 )]
962 mode: Option<FindValueMode>,
963 #[arg(
964 long = "label-direction",
965 value_enum,
966 value_name = "DIR",
967 help = "For --mode label, read the value from right, below, or any (default: any)"
968 )]
969 label_direction: Option<LabelDirectionArg>,
970 #[arg(
971 long,
972 value_name = "ID",
973 help = "Read from a session's materialized state instead of the file"
974 )]
975 session: Option<String>,
976 #[arg(
977 long = "session-workspace",
978 value_name = "PATH",
979 help = "Workspace root for session resolution"
980 )]
981 session_workspace: Option<PathBuf>,
982 },
983 #[command(
984 about = "List workbook named ranges and table/formula named items",
985 after_long_help = "Examples:\n agent-spreadsheet named-ranges data.xlsx\n agent-spreadsheet named-ranges data.xlsx --sheet \"Q1 Actuals\" --name-prefix Sales"
986 )]
987 NamedRanges {
988 #[arg(value_name = "FILE", help = "Path to the workbook")]
989 file: PathBuf,
990 #[arg(long, value_name = "SHEET", help = "Optional sheet name filter")]
991 sheet: Option<String>,
992 #[arg(
993 long = "name-prefix",
994 value_name = "PREFIX",
995 help = "Optional case-insensitive prefix filter for item names"
996 )]
997 name_prefix: Option<String>,
998 #[arg(
999 long,
1000 value_name = "ID",
1001 help = "Read from a session's materialized state instead of the file"
1002 )]
1003 session: Option<String>,
1004 #[arg(
1005 long = "session-workspace",
1006 value_name = "PATH",
1007 help = "Workspace root for session resolution"
1008 )]
1009 session_workspace: Option<PathBuf>,
1010 },
1011 #[command(
1012 about = "Define a new named range in a workbook",
1013 after_long_help = "Examples:\n agent-spreadsheet define-name data.xlsx MyRange 'Sheet1!$A$1:$B$10'\n agent-spreadsheet define-name data.xlsx SheetLocal 'Sheet1!$A$1' --scope sheet --scope-sheet-name Sheet1 --in-place"
1014 )]
1015 DefineName {
1016 #[arg(value_name = "FILE", help = "Path to the workbook")]
1017 file: PathBuf,
1018 #[arg(value_name = "NAME", help = "Name to define")]
1019 name: String,
1020 #[arg(value_name = "REFERS_TO", help = "Range or formula the name refers to")]
1021 refers_to: String,
1022 #[arg(
1023 long,
1024 value_name = "SCOPE",
1025 help = "Scope: workbook (default) or sheet"
1026 )]
1027 scope: Option<String>,
1028 #[arg(
1029 long = "scope-sheet-name",
1030 value_name = "SHEET",
1031 help = "Sheet name when scope is 'sheet'"
1032 )]
1033 scope_sheet_name: Option<String>,
1034 #[arg(long, help = "Validate without mutating files")]
1035 dry_run: bool,
1036 #[arg(long, help = "Apply by atomically replacing the source file")]
1037 in_place: bool,
1038 #[arg(long, value_name = "PATH", help = "Apply to this output path")]
1039 output: Option<PathBuf>,
1040 #[arg(long, help = "Allow overwriting --output when it already exists")]
1041 force: bool,
1042 },
1043 #[command(
1044 about = "Update an existing named range",
1045 after_long_help = "Examples:\n agent-spreadsheet update-name data.xlsx MyRange 'Sheet1!$A$1:$C$20' --in-place\n agent-spreadsheet update-name data.xlsx SheetLocal --scope sheet --scope-sheet-name Sheet1 --in-place\n\nNote: REFERS_TO is optional. Omit it to update scope metadata only."
1046 )]
1047 UpdateName {
1048 #[arg(value_name = "FILE", help = "Path to the workbook")]
1049 file: PathBuf,
1050 #[arg(value_name = "NAME", help = "Name to update")]
1051 name: String,
1052 #[arg(
1053 value_name = "REFERS_TO",
1054 help = "Optional new range or formula the name refers to"
1055 )]
1056 refers_to: Option<String>,
1057 #[arg(long, value_name = "SCOPE", help = "Scope filter: workbook or sheet")]
1058 scope: Option<String>,
1059 #[arg(
1060 long = "scope-sheet-name",
1061 value_name = "SHEET",
1062 help = "Sheet name to disambiguate"
1063 )]
1064 scope_sheet_name: Option<String>,
1065 #[arg(long, help = "Validate without mutating files")]
1066 dry_run: bool,
1067 #[arg(long, help = "Apply by atomically replacing the source file")]
1068 in_place: bool,
1069 #[arg(long, value_name = "PATH", help = "Apply to this output path")]
1070 output: Option<PathBuf>,
1071 #[arg(long, help = "Allow overwriting --output when it already exists")]
1072 force: bool,
1073 },
1074 #[command(
1075 about = "Delete a named range from a workbook",
1076 after_long_help = "Examples:\n agent-spreadsheet delete-name data.xlsx MyRange --in-place\n agent-spreadsheet delete-name data.xlsx SheetLocal --scope sheet --scope-sheet-name Sheet1 --in-place"
1077 )]
1078 DeleteName {
1079 #[arg(value_name = "FILE", help = "Path to the workbook")]
1080 file: PathBuf,
1081 #[arg(value_name = "NAME", help = "Name to delete")]
1082 name: String,
1083 #[arg(long, value_name = "SCOPE", help = "Scope filter: workbook or sheet")]
1084 scope: Option<String>,
1085 #[arg(
1086 long = "scope-sheet-name",
1087 value_name = "SHEET",
1088 help = "Sheet name to disambiguate"
1089 )]
1090 scope_sheet_name: Option<String>,
1091 #[arg(long, help = "Validate without mutating files")]
1092 dry_run: bool,
1093 #[arg(long, help = "Apply by atomically replacing the source file")]
1094 in_place: bool,
1095 #[arg(long, value_name = "PATH", help = "Apply to this output path")]
1096 output: Option<PathBuf>,
1097 #[arg(long, help = "Allow overwriting --output when it already exists")]
1098 force: bool,
1099 },
1100 #[command(
1101 about = "Find formulas containing a text query with pagination",
1102 after_long_help = "Examples:\n agent-spreadsheet find-formula data.xlsx SUM(\n agent-spreadsheet find-formula data.xlsx VLOOKUP --sheet \"Q1 Actuals\" --limit 25 --offset 50\n\nRelated:\n Use inspect-cells for per-cell formula/value/cached/style snapshots in a target range."
1103 )]
1104 FindFormula {
1105 #[arg(value_name = "FILE", help = "Path to the workbook")]
1106 file: PathBuf,
1107 #[arg(value_name = "QUERY", help = "Text to search for within formulas")]
1108 query: String,
1109 #[arg(long, value_name = "SHEET", help = "Optional sheet name filter")]
1110 sheet: Option<String>,
1111 #[arg(
1112 long,
1113 value_name = "N",
1114 help = "Maximum matches to return (must be at least 1)"
1115 )]
1116 limit: Option<u32>,
1117 #[arg(long, value_name = "N", help = "Match offset for continuation")]
1118 offset: Option<u32>,
1119 },
1120 #[command(
1121 about = "Scan workbook formulas for volatile functions",
1122 after_long_help = "Examples:\n agent-spreadsheet scan-volatiles data.xlsx\n agent-spreadsheet scan-volatiles data.xlsx --sheet \"Q1 Actuals\" --limit 10 --offset 10"
1123 )]
1124 ScanVolatiles {
1125 #[arg(value_name = "FILE", help = "Path to the workbook")]
1126 file: PathBuf,
1127 #[arg(long, value_name = "SHEET", help = "Optional sheet name filter")]
1128 sheet: Option<String>,
1129 #[arg(
1130 long,
1131 value_name = "N",
1132 help = "Maximum entries to return (must be at least 1)"
1133 )]
1134 limit: Option<u32>,
1135 #[arg(long, value_name = "N", help = "Entry offset for continuation")]
1136 offset: Option<u32>,
1137 #[arg(
1138 long = "formula-parse-policy",
1139 value_enum,
1140 value_name = "POLICY",
1141 help = "Formula parse policy: fail, warn (default), or off"
1142 )]
1143 formula_parse_policy: Option<FormulaParsePolicy>,
1144 },
1145 #[command(
1146 about = "Compute per-sheet statistics for density and column types",
1147 after_long_help = "Examples:\n agent-spreadsheet sheet-statistics data.xlsx Sheet1\n agent-spreadsheet sheet-statistics data.xlsx \"Q1 Actuals\""
1148 )]
1149 SheetStatistics {
1150 #[arg(value_name = "FILE", help = "Path to the workbook")]
1151 file: PathBuf,
1152 #[arg(value_name = "SHEET", help = "Sheet to summarize")]
1153 sheet: String,
1154 },
1155 #[command(
1156 about = "Summarize formulas on a sheet by complexity or frequency",
1157 after_long_help = "Examples:\n agent-spreadsheet formula-map data.xlsx Sheet1\n agent-spreadsheet formula-map data.xlsx \"Q1 Actuals\" --sort-by count --limit 25"
1158 )]
1159 FormulaMap {
1160 #[arg(value_name = "FILE", help = "Path to the workbook")]
1161 file: PathBuf,
1162 #[arg(value_name = "SHEET", help = "Sheet to analyze")]
1163 sheet: String,
1164 #[arg(long, value_name = "LIMIT", help = "Maximum groups to return")]
1165 limit: Option<u32>,
1166 #[arg(
1167 long,
1168 value_enum,
1169 value_name = "ORDER",
1170 help = "Sort groups by complexity or count"
1171 )]
1172 sort_by: Option<FormulaSort>,
1173 #[arg(
1174 long = "formula-parse-policy",
1175 value_enum,
1176 value_name = "POLICY",
1177 help = "Formula parse policy: fail, warn (default), or off"
1178 )]
1179 formula_parse_policy: Option<FormulaParsePolicy>,
1180 },
1181 #[command(
1182 about = "Trace formula precedents or dependents from one origin cell",
1183 after_long_help = "Examples:\n agent-spreadsheet formula-trace data.xlsx Sheet1 C2 precedents --depth 2\n agent-spreadsheet formula-trace data.xlsx Sheet1 C2 dependents --page-size 25\n agent-spreadsheet formula-trace data.xlsx Sheet1 C2 precedents --cursor-depth 1 --cursor-offset 25\n\nContinuation:\n Reuse next_cursor.depth/next_cursor.offset as --cursor-depth/--cursor-offset to continue paged traces.\n\nRelated:\n Use inspect-cells for a local per-cell triage view that includes formula/value/cached/style metadata."
1184 )]
1185 FormulaTrace {
1186 #[arg(value_name = "FILE", help = "Path to the workbook")]
1187 file: PathBuf,
1188 #[arg(value_name = "SHEET", help = "Sheet containing the origin cell")]
1189 sheet: String,
1190 #[arg(value_name = "CELL", help = "Origin cell in A1 notation")]
1191 cell: String,
1192 #[arg(
1193 value_name = "DIRECTION",
1194 help = "Trace direction: precedents or dependents"
1195 )]
1196 direction: TraceDirectionArg,
1197 #[arg(
1198 long,
1199 value_name = "DEPTH",
1200 help = "Trace depth (must be between 1 and 5)"
1201 )]
1202 depth: Option<u32>,
1203 #[arg(
1204 long = "page-size",
1205 value_name = "N",
1206 help = "Page size for trace edges (must be between 5 and 200)"
1207 )]
1208 page_size: Option<usize>,
1209 #[arg(
1210 long = "cursor-depth",
1211 value_name = "DEPTH",
1212 help = "Continuation cursor depth (must be paired with --cursor-offset)"
1213 )]
1214 cursor_depth: Option<u32>,
1215 #[arg(
1216 long = "cursor-offset",
1217 value_name = "OFFSET",
1218 help = "Continuation cursor offset (must be paired with --cursor-depth)"
1219 )]
1220 cursor_offset: Option<usize>,
1221 #[arg(
1222 long = "formula-parse-policy",
1223 value_enum,
1224 value_name = "POLICY",
1225 help = "Formula parse policy: fail, warn (default), or off"
1226 )]
1227 formula_parse_policy: Option<FormulaParsePolicy>,
1228 #[arg(
1229 long,
1230 value_name = "ID",
1231 help = "Read from a session's materialized state instead of the file"
1232 )]
1233 session: Option<String>,
1234 #[arg(
1235 long = "session-workspace",
1236 value_name = "PATH",
1237 help = "Workspace root for session resolution"
1238 )]
1239 session_workspace: Option<PathBuf>,
1240 },
1241 #[command(about = "Describe workbook-level metadata and sheet counts")]
1242 Describe {
1243 #[arg(value_name = "FILE", help = "Path to the workbook")]
1244 file: PathBuf,
1245 #[arg(
1246 long,
1247 value_name = "ID",
1248 help = "Read from a session's materialized state instead of the file"
1249 )]
1250 session: Option<String>,
1251 #[arg(
1252 long = "session-workspace",
1253 value_name = "PATH",
1254 help = "Workspace root for session resolution"
1255 )]
1256 session_workspace: Option<PathBuf>,
1257 },
1258 #[command(
1259 about = "Profile table headers, types, and column distributions",
1260 after_long_help = "Examples:\n agent-spreadsheet table-profile data.xlsx\n agent-spreadsheet table-profile data.xlsx --sheet \"Q1 Actuals\""
1261 )]
1262 TableProfile {
1263 #[arg(value_name = "FILE", help = "Path to the workbook")]
1264 file: PathBuf,
1265 #[arg(long, value_name = "SHEET", help = "Optional sheet to profile")]
1266 sheet: Option<String>,
1267 #[arg(
1268 long,
1269 value_name = "ID",
1270 help = "Read from a session's materialized state instead of the file"
1271 )]
1272 session: Option<String>,
1273 #[arg(
1274 long = "session-workspace",
1275 value_name = "PATH",
1276 help = "Workspace root for session resolution"
1277 )]
1278 session_workspace: Option<PathBuf>,
1279 },
1280 #[command(
1281 about = "Render a range with layout: column widths, borders, bold/italic, alignment",
1282 after_long_help = "Examples:\n agent-spreadsheet layout-page data.xlsx Sheet1 --range A1:F30\n agent-spreadsheet layout-page data.xlsx Sheet1 --range A1:H40 --render both\n agent-spreadsheet layout-page data.xlsx Sheet1 --range B2:G20 --mode formulas\n agent-spreadsheet layout-page data.xlsx Sheet1 --range B2:G20 --render ascii\n\nThe JSON output (default) includes per-column widths, merged cell spans, and per-cell style metadata.\nThe ASCII render gives a proportional grid with box-drawing borders and bold/italic markers.\n\nCLI notes:\n --render ascii prints the grid directly (plain text) instead of JSON.\n Empty edge columns are trimmed by default; use --skip-empty-columns-trim to keep them.\n\nLimits: 80 rows × 25 columns. Ranges exceeding these are silently capped."
1283 )]
1284 LayoutPage {
1285 #[arg(value_name = "FILE", help = "Path to the workbook")]
1286 file: PathBuf,
1287 #[arg(value_name = "SHEET", help = "Sheet name")]
1288 sheet: String,
1289 #[arg(
1290 value_name = "RANGE",
1291 help = "A1 range to render (default: A1:T50); equivalent to --range"
1292 )]
1293 range_positional: Option<String>,
1294 #[arg(
1295 long,
1296 value_name = "RANGE",
1297 help = "A1 range to render (default: A1:T50)"
1298 )]
1299 range: Option<String>,
1300 #[arg(
1301 long,
1302 value_enum,
1303 value_name = "MODE",
1304 help = "Cell content: values (default) or formulas"
1305 )]
1306 mode: Option<LayoutModeArg>,
1307 #[arg(
1308 long = "max-col-width",
1309 value_name = "N",
1310 help = "Maximum column width in character units before truncating (default: 20)"
1311 )]
1312 max_col_width: Option<u32>,
1313 #[arg(
1314 long = "fit-columns",
1315 help = "Set each column width to the longest rendered cell so truncation is avoided (default off)"
1316 )]
1317 fit_columns: bool,
1318 #[arg(
1319 long = "skip-empty-columns-trim",
1320 help = "Disable default trimming of empty edge columns"
1321 )]
1322 skip_empty_columns_trim: bool,
1323 #[arg(
1324 long,
1325 value_enum,
1326 value_name = "RENDER",
1327 help = "Output format: json (default), ascii, or both"
1328 )]
1329 render: Option<LayoutRenderArg>,
1330 #[arg(
1331 long,
1332 value_name = "ID",
1333 help = "Read from a session's materialized state instead of the file"
1334 )]
1335 session: Option<String>,
1336 #[arg(
1337 long = "session-workspace",
1338 value_name = "PATH",
1339 help = "Workspace root for session resolution"
1340 )]
1341 session_workspace: Option<PathBuf>,
1342 },
1343 #[command(
1344 about = "Create a new workbook at a destination path",
1345 after_long_help = "Examples:
1346 agent-spreadsheet create-workbook new.xlsx
1347 agent-spreadsheet create-workbook model.xlsx --sheets Inputs,Calc,Output
1348 agent-spreadsheet create-workbook model.xlsx --overwrite"
1349 )]
1350 CreateWorkbook {
1351 #[arg(value_name = "PATH", help = "Destination workbook path")]
1352 path: PathBuf,
1353 #[arg(
1354 long,
1355 value_name = "SHEETS",
1356 value_delimiter = ',',
1357 help = "Comma-separated sheet names (default: Sheet1)"
1358 )]
1359 sheets: Option<Vec<String>>,
1360 #[arg(long, help = "Overwrite destination file when it exists")]
1361 overwrite: bool,
1362 },
1363 #[command(about = "Copy a workbook to a new path for safe edits")]
1364 Copy {
1365 #[arg(value_name = "SOURCE", help = "Original workbook path")]
1366 source: PathBuf,
1367 #[arg(value_name = "DEST", help = "Destination workbook path")]
1368 dest: PathBuf,
1369 },
1370 #[command(
1371 about = "Apply one or more shorthand cell edits to a sheet",
1372 after_long_help = r#"Examples:
1373 agent-spreadsheet edit workbook.xlsx Sheet1 A1=42 'B2==SUM(A1:A10)'
1374 agent-spreadsheet edit workbook.xlsx Sheet1 --dry-run A1=42 'B2==SUM(A1:A10)'
1375 agent-spreadsheet edit workbook.xlsx Sheet1 --output edited.xlsx --force A1=42 'B2==SUM(A1:A10)'
1376 agent-spreadsheet edit workbook.xlsx Sheet1 --edits-file edits.txt --output edited.xlsx --force
1377 printf '%s\n' 'B13==-MIN(B9,B12)' 'C13==-MIN(C9,C12)' | agent-spreadsheet edit workbook.xlsx Sheet1 --edits-file - --output edited.xlsx --force
1378
1379Mode selection:
1380 Default behavior (no mode flags): in-place edit of the source workbook.
1381 Optional explicit modes: --dry-run, --in-place, or --output <PATH>.
1382
1383Formula shorthand:
1384 Use double equals for formulas, e.g. C2==SUM(A1:A10).
1385 Single equals writes a literal value/text, e.g. C2=SUM(A1:A10).
1386
1387Shell quoting (positional edits):
1388 Single-quote every edit that contains parentheses, spaces, or $:
1389 unquoted ( breaks the shell, and double quotes let the shell expand
1390 $-style absolute references (e.g. "$A$1" becomes "1").
1391 Prefer --edits-file (one edit per line, '-' for stdin) for formula
1392 batches; file/stdin edits bypass shell quoting entirely.
1393
1394Cache note:
1395 Formula edits (values starting with =) clear cached results.
1396 Run recalculate to refresh computed values.
1397
1398Diagnostics note:
1399 Formula writes include write_path_provenance (written_via + formula_targets)."#
1400 )]
1401 Edit {
1402 #[arg(value_name = "FILE", help = "Workbook path to modify")]
1403 file: PathBuf,
1404 #[arg(value_name = "SHEET", help = "Target sheet name")]
1405 sheet: String,
1406 #[arg(long, help = "Validate edits without mutating any workbook")]
1407 dry_run: bool,
1408 #[arg(long, help = "Apply edits by atomically replacing the source file")]
1409 in_place: bool,
1410 #[arg(long, value_name = "PATH", help = "Apply edits to this output path")]
1411 output: Option<PathBuf>,
1412 #[arg(long, help = "Allow overwriting --output when it already exists")]
1413 force: bool,
1414 #[arg(
1415 value_name = "EDIT",
1416 help = "Edit operations like A1=42 or 'B2==SUM(A1:A10)' (single-quote formulas containing parentheses or $)"
1417 )]
1418 edits: Vec<String>,
1419 #[arg(
1420 long = "edits-file",
1421 value_name = "PATH",
1422 help = "Read edits from a file, one edit per line ('-' reads stdin). Blank lines and lines starting with # are ignored. Avoids shell quoting issues with $ and parentheses."
1423 )]
1424 edits_file: Option<PathBuf>,
1425 #[arg(
1426 long = "formula-parse-policy",
1427 value_enum,
1428 value_name = "POLICY",
1429 help = "Formula parse policy: fail (default for edit), warn, or off"
1430 )]
1431 formula_parse_policy: Option<FormulaParsePolicy>,
1432 },
1433 #[command(
1434 about = "Append rows into a detected region with footer-aware insertion",
1435 after_long_help = "Examples:\n asp append-region workbook.xlsx --sheet Sheet1 --region-id 0 --rows @rows.json --dry-run\n asp append-region workbook.xlsx --sheet Sheet1 --table-name SalesTable --from-csv rows.csv --header --footer-policy before-footer --output updated.xlsx --force\n\nTarget selection:\n Use exactly one of --region-id or --table-name.\n --region-id comes from `asp sheet-overview`.\n --table-name resolves an existing sheet table by name.\n\nInput payloads:\n Use exactly one of --rows or --from-csv.\n --rows accepts a top-level JSON array of rows, or an object with a rows array.\n Cells may be raw JSON scalars/null, {'v': ...} value cells, or {'f': 'FORMULA'} formula cells.\n --from-csv imports CSV rows and treats empty fields as blanks; use --header to skip the first CSV row.\n\nFooter policies:\n - auto (default): insert before a detected footer row when found, else append at the region end\n - before-footer: require a detected footer/subtotal row and fail when none is found\n - append-at-end: always append after the detected region end, even when a footer row is present\n\nBehavior:\n - resolves a detected region or table target\n - reports footer candidates, policy choice, and formula footer targets in dry-run output\n - writes the appended matrix into inserted rows\n - expands adjacent SUM footers below the insertion band when rows are inserted before them"
1436 )]
1437 AppendRegion {
1438 #[arg(value_name = "FILE", help = "Workbook path to update")]
1439 file: PathBuf,
1440 #[arg(
1441 long = "sheet",
1442 value_name = "SHEET",
1443 help = "Sheet containing the detected region or table"
1444 )]
1445 sheet_name: String,
1446 #[arg(
1447 long = "region-id",
1448 value_name = "ID",
1449 help = "Detected region id from `asp sheet-overview`"
1450 )]
1451 region_id: Option<u32>,
1452 #[arg(
1453 long = "table-name",
1454 value_name = "NAME",
1455 help = "Sheet table name to append into instead of a detected region id"
1456 )]
1457 table_name: Option<String>,
1458 #[arg(
1459 long,
1460 value_name = "ROWS_REF",
1461 help = "Rows payload as @file or inline JSON"
1462 )]
1463 rows: Option<String>,
1464 #[arg(
1465 long = "from-csv",
1466 value_name = "PATH",
1467 help = "CSV file to append as rows"
1468 )]
1469 from_csv: Option<String>,
1470 #[arg(long, help = "Skip first CSV row when importing --from-csv")]
1471 header: bool,
1472 #[arg(
1473 long = "footer-policy",
1474 value_enum,
1475 default_value = "auto",
1476 value_name = "POLICY",
1477 help = "Footer handling policy: auto, before-footer, or append-at-end"
1478 )]
1479 footer_policy: AppendRegionFooterPolicyArg,
1480 #[arg(long, help = "Preview insertion plan without mutating files")]
1481 dry_run: bool,
1482 #[arg(long, help = "Apply by atomically replacing the source file")]
1483 in_place: bool,
1484 #[arg(long, value_name = "PATH", help = "Apply append to this output path")]
1485 output: Option<PathBuf>,
1486 #[arg(long, help = "Allow overwriting --output when it already exists")]
1487 force: bool,
1488 },
1489 #[command(
1490 about = "Clone one template row into inserted rows with preview-first planning",
1491 after_long_help = "Examples:\n asp clone-template-row workbook.xlsx --sheet Sheet1 --source-row 12 --after 12 --count 2 --dry-run\n asp clone-template-row workbook.xlsx --sheet Sheet1 --source-row 8 --before 20 --patch-targets all-non-formula --output updated.xlsx --force\n\nAnchor selection:\n Use exactly one of --before, --after, or --insert-at.\n\nBehavior:\n - clones a single template row using the existing row-clone structure path\n - reports formula targets, patch targets, merge-boundary warnings, and confidence metadata in dry-run output\n - merge-policy safe warns on boundary-crossing merges; strict fails instead"
1492 )]
1493 CloneTemplateRow {
1494 #[arg(value_name = "FILE", help = "Workbook path to update")]
1495 file: PathBuf,
1496 #[arg(
1497 long = "sheet",
1498 value_name = "SHEET",
1499 help = "Sheet containing the template row"
1500 )]
1501 sheet_name: String,
1502 #[arg(long = "source-row", value_name = "ROW", help = "1-based row to clone")]
1503 source_row: u32,
1504 #[arg(long, value_name = "ROW", help = "Insert before this 1-based row")]
1505 before: Option<u32>,
1506 #[arg(long, value_name = "ROW", help = "Insert after this 1-based row")]
1507 after: Option<u32>,
1508 #[arg(
1509 long = "insert-at",
1510 value_name = "ROW",
1511 help = "Raw 1-based insertion row"
1512 )]
1513 insert_at: Option<u32>,
1514 #[arg(
1515 long,
1516 value_name = "N",
1517 default_value_t = 1,
1518 help = "Number of row copies to insert"
1519 )]
1520 count: u32,
1521 #[arg(
1522 long = "expand-adjacent-sums",
1523 help = "Expand adjacent SUM footer formulas below the inserted rows"
1524 )]
1525 expand_adjacent_sums: bool,
1526 #[arg(
1527 long = "patch-targets",
1528 value_enum,
1529 default_value = "likely-inputs",
1530 value_name = "MODE",
1531 help = "Patch target mode: likely-inputs, all-non-formula, or none"
1532 )]
1533 patch_targets: ClonePatchTargetsArg,
1534 #[arg(
1535 long = "merge-policy",
1536 value_enum,
1537 default_value = "safe",
1538 value_name = "POLICY",
1539 help = "Merge handling policy: safe warns, strict fails"
1540 )]
1541 merge_policy: CloneMergePolicyArg,
1542 #[arg(long, help = "Preview clone plan without mutating files")]
1543 dry_run: bool,
1544 #[arg(long, help = "Apply by atomically replacing the source file")]
1545 in_place: bool,
1546 #[arg(long, value_name = "PATH", help = "Apply clone to this output path")]
1547 output: Option<PathBuf>,
1548 #[arg(long, help = "Allow overwriting --output when it already exists")]
1549 force: bool,
1550 },
1551 #[command(
1552 about = "Clone a contiguous template row band with preview-first planning",
1553 after_long_help = "Examples:\n asp clone-row-band workbook.xlsx --sheet Sheet1 --source-rows 12:14 --after 14 --repeat 2 --dry-run\n asp clone-row-band workbook.xlsx --sheet Sheet1 --source-rows 20:22 --before 30 --patch-targets all-non-formula --output updated.xlsx --force\n\nAnchor selection:\n Use exactly one of --before, --after, or --insert-at.\n\nBehavior:\n - clones a contiguous source row band using row insertion plus stamped template cells\n - reports inserted blocks, formula targets, patch targets, merge-boundary warnings, and confidence metadata in dry-run output\n - merge-policy safe warns on boundary-crossing merges; strict fails instead"
1554 )]
1555 CloneRowBand {
1556 #[arg(value_name = "FILE", help = "Workbook path to update")]
1557 file: PathBuf,
1558 #[arg(
1559 long = "sheet",
1560 value_name = "SHEET",
1561 help = "Sheet containing the source row band"
1562 )]
1563 sheet_name: String,
1564 #[arg(
1565 long = "source-rows",
1566 value_name = "START:END",
1567 help = "Contiguous 1-based source row band"
1568 )]
1569 source_rows: String,
1570 #[arg(long, value_name = "ROW", help = "Insert before this 1-based row")]
1571 before: Option<u32>,
1572 #[arg(long, value_name = "ROW", help = "Insert after this 1-based row")]
1573 after: Option<u32>,
1574 #[arg(
1575 long = "insert-at",
1576 value_name = "ROW",
1577 help = "Raw 1-based insertion row"
1578 )]
1579 insert_at: Option<u32>,
1580 #[arg(
1581 long,
1582 value_name = "N",
1583 default_value_t = 1,
1584 help = "Number of times to repeat the row band"
1585 )]
1586 repeat: u32,
1587 #[arg(
1588 long = "expand-adjacent-sums",
1589 help = "Expand adjacent SUM footer formulas below the inserted rows"
1590 )]
1591 expand_adjacent_sums: bool,
1592 #[arg(
1593 long = "patch-targets",
1594 value_enum,
1595 default_value = "likely-inputs",
1596 value_name = "MODE",
1597 help = "Patch target mode: likely-inputs, all-non-formula, or none"
1598 )]
1599 patch_targets: ClonePatchTargetsArg,
1600 #[arg(
1601 long = "merge-policy",
1602 value_enum,
1603 default_value = "safe",
1604 value_name = "POLICY",
1605 help = "Merge handling policy: safe warns, strict fails"
1606 )]
1607 merge_policy: CloneMergePolicyArg,
1608 #[arg(long, help = "Preview clone plan without mutating files")]
1609 dry_run: bool,
1610 #[arg(long, help = "Apply by atomically replacing the source file")]
1611 in_place: bool,
1612 #[arg(long, value_name = "PATH", help = "Apply clone to this output path")]
1613 output: Option<PathBuf>,
1614 #[arg(long, help = "Allow overwriting --output when it already exists")]
1615 force: bool,
1616 },
1617 #[command(
1618 about = "Apply stateless transform operations from an @ops payload",
1619 after_long_help = r#"Examples:
1620 agent-spreadsheet transform-batch workbook.xlsx --ops @ops.json --dry-run
1621 agent-spreadsheet transform-batch workbook.xlsx --ops @ops.json --in-place
1622 agent-spreadsheet transform-batch workbook.xlsx --ops @ops.json --output transformed.xlsx --force
1623
1624Mode selection:
1625 Choose exactly one of --dry-run, --in-place, or --output <PATH>.
1626
1627Payload examples (`--ops @transform_ops.json`):
1628 Minimal:
1629 {"ops":[{"kind":"fill_range","sheet_name":"Sheet1","target":{"kind":"range","range":"B2:B4"},"value":"0"}]}
1630 Advanced:
1631 {"ops":[{"kind":"replace_in_range","sheet_name":"Sheet1","target":{"kind":"region","region_id":1},"find":"N/A","replace":"","match_mode":"contains","case_sensitive":false,"include_formulas":true}]}
1632
1633Required envelope:
1634 Top-level object with an `ops` array.
1635 Each op requires a `kind` discriminator and command-specific required fields.
1636
1637Cache note:
1638 Formula writes (FillRange with is_formula, ReplaceInRange with include_formulas) clear cached results.
1639 Run recalculate to refresh computed values.
1640
1641Diagnostics note:
1642 Formula writes include write_path_provenance (written_via + formula_targets)."#
1643 )]
1644 TransformBatch {
1645 #[arg(
1646 value_name = "FILE",
1647 help = "Workbook path to transform",
1648 required_unless_present = "print_schema"
1649 )]
1650 file: Option<PathBuf>,
1651 #[arg(
1652 long,
1653 value_name = "OPS_REF",
1654 help = "Ops payload file reference (@path)",
1655 required_unless_present = "print_schema"
1656 )]
1657 ops: Option<String>,
1658 #[arg(long, help = "Validate ops and report summary without mutating files")]
1659 dry_run: bool,
1660 #[arg(
1661 long,
1662 help = "Apply transforms by atomically replacing the source file"
1663 )]
1664 in_place: bool,
1665 #[arg(
1666 long,
1667 value_name = "PATH",
1668 help = "Apply transforms to this output path"
1669 )]
1670 output: Option<PathBuf>,
1671 #[arg(long, help = "Allow overwriting --output when it already exists")]
1672 force: bool,
1673 #[arg(
1674 long = "print-schema",
1675 hide = true,
1676 help = "Print the full JSON schema for the --ops payload and exit"
1677 )]
1678 print_schema: bool,
1679 #[arg(
1680 long = "formula-parse-policy",
1681 value_enum,
1682 value_name = "POLICY",
1683 help = "Formula parse policy: fail, warn (default for transform-batch), or off"
1684 )]
1685 formula_parse_policy: Option<FormulaParsePolicy>,
1686 },
1687 #[command(
1688 about = "Apply stateless style operations from an @ops payload",
1689 after_long_help = r#"Examples:
1690 agent-spreadsheet style-batch workbook.xlsx --ops @style_ops.json --dry-run
1691 agent-spreadsheet style-batch workbook.xlsx --ops @style_ops.json --output styled.xlsx --force
1692
1693Payload examples (`--ops @style_ops.json`):
1694 Minimal:
1695 {"ops":[{"sheet_name":"Sheet1","target":{"kind":"range","range":"B2:B2"},"patch":{"font":{"bold":true}}}]}
1696 Advanced:
1697 {"ops":[{"sheet_name":"Sheet1","target":{"kind":"cells","cells":["B2","B3"]},"patch":{"number_format":"$#,##0.00","alignment":{"horizontal":"right"}},"op_mode":"merge"}]}
1698
1699Required envelope:
1700 Top-level object with an `ops` array.
1701 Style ops require `sheet_name`, `target`, and `patch` (no top-level op `kind`)."#
1702 )]
1703 StyleBatch {
1704 #[arg(
1705 value_name = "FILE",
1706 help = "Workbook path to style",
1707 required_unless_present = "print_schema"
1708 )]
1709 file: Option<PathBuf>,
1710 #[arg(
1711 long,
1712 value_name = "OPS_REF",
1713 help = "Ops payload file reference (@path)",
1714 required_unless_present = "print_schema"
1715 )]
1716 ops: Option<String>,
1717 #[arg(long, help = "Validate ops and report summary without mutating files")]
1718 dry_run: bool,
1719 #[arg(long, help = "Apply style ops by atomically replacing the source file")]
1720 in_place: bool,
1721 #[arg(
1722 long,
1723 value_name = "PATH",
1724 help = "Apply style ops to this output path"
1725 )]
1726 output: Option<PathBuf>,
1727 #[arg(long, help = "Allow overwriting --output when it already exists")]
1728 force: bool,
1729 #[arg(
1730 long = "print-schema",
1731 hide = true,
1732 help = "Print the full JSON schema for the --ops payload and exit"
1733 )]
1734 print_schema: bool,
1735 },
1736 #[command(
1737 about = "Apply stateless formula pattern operations from an @ops payload",
1738 after_long_help = r#"Examples:
1739 agent-spreadsheet apply-formula-pattern workbook.xlsx --ops @formula_ops.json --in-place
1740 agent-spreadsheet apply-formula-pattern workbook.xlsx --ops @formula_ops.json --dry-run
1741
1742Payload examples (`--ops @formula_ops.json`):
1743 Minimal:
1744 {"ops":[{"sheet_name":"Sheet1","target_range":"C2:C4","anchor_cell":"C2","base_formula":"B2*2"}]}
1745 Advanced:
1746 {"ops":[{"sheet_name":"Sheet1","target_range":"C2:E4","anchor_cell":"C2","base_formula":"B2*2","fill_direction":"both","relative_mode":"excel"}]}
1747
1748Required envelope:
1749 Top-level object with an `ops` array.
1750 Each op requires `sheet_name`, `target_range`, `anchor_cell`, and `base_formula`.
1751 `relative_mode` valid values: excel|abs_cols|abs_rows.
1752
1753Cache note:
1754 Updated formula cells clear cached results. Run recalculate to refresh computed values.
1755
1756Diagnostics note:
1757 Formula writes include write_path_provenance (written_via + formula_targets)."#
1758 )]
1759 ApplyFormulaPattern {
1760 #[arg(
1761 value_name = "FILE",
1762 help = "Workbook path to update",
1763 required_unless_present = "print_schema"
1764 )]
1765 file: Option<PathBuf>,
1766 #[arg(
1767 long,
1768 value_name = "OPS_REF",
1769 help = "Ops payload file reference (@path)",
1770 required_unless_present = "print_schema"
1771 )]
1772 ops: Option<String>,
1773 #[arg(long, help = "Validate ops and report summary without mutating files")]
1774 dry_run: bool,
1775 #[arg(
1776 long,
1777 help = "Apply formula pattern ops by atomically replacing the source file"
1778 )]
1779 in_place: bool,
1780 #[arg(
1781 long,
1782 value_name = "PATH",
1783 help = "Apply formula pattern ops to this output path"
1784 )]
1785 output: Option<PathBuf>,
1786 #[arg(long, help = "Allow overwriting --output when it already exists")]
1787 force: bool,
1788 #[arg(
1789 long = "print-schema",
1790 hide = true,
1791 help = "Print the full JSON schema for the --ops payload and exit"
1792 )]
1793 print_schema: bool,
1794 },
1795 #[command(
1796 about = "Apply stateless structure operations from an @ops payload",
1797 after_long_help = r#"Examples:
1798 agent-spreadsheet structure-batch workbook.xlsx --ops @structure_ops.json --dry-run
1799 agent-spreadsheet structure-batch workbook.xlsx --ops @structure_ops.json --output structured.xlsx
1800
1801Payload examples (`--ops @structure_ops.json`):
1802 Minimal:
1803 {"ops":[{"kind":"rename_sheet","old_name":"Summary","new_name":"Dashboard"}]}
1804 Advanced:
1805 {"ops":[{"kind":"copy_range","sheet_name":"Sheet1","dest_sheet_name":"Summary","src_range":"A1:C4","dest_anchor":"A1","include_styles":true,"include_formulas":true}]}
1806
1807Required envelope:
1808 Top-level object with an `ops` array.
1809 Each op requires a `kind` discriminator and kind-specific required fields.
1810
1811Cache note:
1812 Structural operations that rewrite formula references (row/column insert/delete, sheet rename,
1813 copy/move) clear cached formula results. Run recalculate to refresh computed values."#
1814 )]
1815 StructureBatch {
1816 #[arg(
1817 value_name = "FILE",
1818 help = "Workbook path to update",
1819 required_unless_present = "print_schema"
1820 )]
1821 file: Option<PathBuf>,
1822 #[arg(
1823 long,
1824 value_name = "OPS_REF",
1825 help = "Ops payload file reference (@path)",
1826 required_unless_present = "print_schema"
1827 )]
1828 ops: Option<String>,
1829 #[arg(long, help = "Validate ops and report summary without mutating files")]
1830 dry_run: bool,
1831 #[arg(
1832 long,
1833 help = "Apply structure ops by atomically replacing the source file"
1834 )]
1835 in_place: bool,
1836 #[arg(
1837 long,
1838 value_name = "PATH",
1839 help = "Apply structure ops to this output path"
1840 )]
1841 output: Option<PathBuf>,
1842 #[arg(long, help = "Allow overwriting --output when it already exists")]
1843 force: bool,
1844 #[arg(
1845 long = "print-schema",
1846 hide = true,
1847 help = "Print the full JSON schema for the --ops payload and exit"
1848 )]
1849 print_schema: bool,
1850 #[arg(
1851 long = "formula-parse-policy",
1852 value_enum,
1853 value_name = "POLICY",
1854 help = "Formula parse policy: fail, warn (default for structure-batch), or off"
1855 )]
1856 formula_parse_policy: Option<FormulaParsePolicy>,
1857 #[arg(
1858 long = "impact-report",
1859 help = "Include a structural impact report (shifted spans, absolute-ref warnings). Requires --dry-run."
1860 )]
1861 impact_report: bool,
1862 #[arg(
1863 long = "show-formula-delta",
1864 help = "Include before/after formula delta preview samples. Requires --dry-run."
1865 )]
1866 show_formula_delta: bool,
1867 },
1868 #[command(
1869 about = "Analyze structural operation impact without mutation (preflight ref-risk check)",
1870 after_long_help = r#"Examples:
1871 agent-spreadsheet check-ref-impact workbook.xlsx --ops @structure_ops.json
1872 agent-spreadsheet check-ref-impact workbook.xlsx --ops @structure_ops.json --show-formula-delta
1873
1874Payload format is the same as structure-batch --ops.
1875This command is read-only: it never modifies the workbook.
1876
1877Output includes:
1878 - shifted_spans: which rows/cols shift and by how much
1879 - absolute_ref_warnings: $-anchored references that cross insertion/deletion boundaries
1880 - tokens_affected / tokens_unaffected counts
1881 - optional formula_delta_preview (before/after formula samples)"#
1882 )]
1883 CheckRefImpact {
1884 #[arg(value_name = "FILE", help = "Path to the workbook")]
1885 file: PathBuf,
1886 #[arg(
1887 long,
1888 value_name = "OPS_REF",
1889 help = "Ops payload file reference (@path) \u{2014} same format as structure-batch"
1890 )]
1891 ops: String,
1892 #[arg(
1893 long = "show-formula-delta",
1894 help = "Include before/after formula delta preview samples"
1895 )]
1896 show_formula_delta: bool,
1897 },
1898 #[command(
1899 about = "Apply stateless column sizing operations from an @ops payload",
1900 after_long_help = r#"Examples:
1901 agent-spreadsheet column-size-batch workbook.xlsx --ops @column_size_ops.json --in-place
1902 agent-spreadsheet column-size-batch workbook.xlsx --ops @column_size_ops.json --output columns.xlsx
1903
1904Payload examples (`--ops @column_size_ops.json`):
1905 Minimal:
1906 {"sheet_name":"Sheet1","ops":[{"range":"A:A","size":{"kind":"width","width_chars":12.0}}]}
1907 Advanced:
1908 {"sheet_name":"Sheet1","ops":[{"target":{"kind":"columns","range":"A:C"},"size":{"kind":"auto","min_width_chars":8.0,"max_width_chars":24.0}}]}
1909
1910Required envelope:
1911 Preferred: top-level object with `sheet_name` and `ops`.
1912 Also accepted: top-level `ops` where each op includes `sheet_name`.
1913 Each op requires `size.kind`; canonical form also includes `target.kind:"columns"`."#
1914 )]
1915 ColumnSizeBatch {
1916 #[arg(
1917 value_name = "FILE",
1918 help = "Workbook path to update",
1919 required_unless_present = "print_schema"
1920 )]
1921 file: Option<PathBuf>,
1922 #[arg(
1923 long,
1924 value_name = "OPS_REF",
1925 help = "Ops payload file reference (@path)",
1926 required_unless_present = "print_schema"
1927 )]
1928 ops: Option<String>,
1929 #[arg(long, help = "Validate ops and report summary without mutating files")]
1930 dry_run: bool,
1931 #[arg(
1932 long,
1933 help = "Apply column sizing ops by atomically replacing the source file"
1934 )]
1935 in_place: bool,
1936 #[arg(
1937 long,
1938 value_name = "PATH",
1939 help = "Apply column sizing ops to this output path"
1940 )]
1941 output: Option<PathBuf>,
1942 #[arg(long, help = "Allow overwriting --output when it already exists")]
1943 force: bool,
1944 #[arg(
1945 long = "print-schema",
1946 hide = true,
1947 help = "Print the full JSON schema for the --ops payload and exit"
1948 )]
1949 print_schema: bool,
1950 },
1951 #[command(
1952 about = "Apply stateless sheet layout operations from an @ops payload",
1953 after_long_help = r#"Examples:
1954 agent-spreadsheet sheet-layout-batch workbook.xlsx --ops @layout_ops.json --dry-run
1955 agent-spreadsheet sheet-layout-batch workbook.xlsx --ops @layout_ops.json --in-place
1956
1957Payload examples (`--ops @layout_ops.json`):
1958 Minimal:
1959 {"ops":[{"kind":"freeze_panes","sheet_name":"Sheet1","freeze_rows":1,"freeze_cols":1}]}
1960 Advanced:
1961 {"ops":[{"kind":"set_page_setup","sheet_name":"Sheet1","orientation":"landscape","fit_to_width":1,"fit_to_height":1}]}
1962
1963Required envelope:
1964 Top-level object with an `ops` array.
1965 Each op requires a `kind` discriminator plus kind-specific required fields."#
1966 )]
1967 SheetLayoutBatch {
1968 #[arg(
1969 value_name = "FILE",
1970 help = "Workbook path to update",
1971 required_unless_present = "print_schema"
1972 )]
1973 file: Option<PathBuf>,
1974 #[arg(
1975 long,
1976 value_name = "OPS_REF",
1977 help = "Ops payload file reference (@path)",
1978 required_unless_present = "print_schema"
1979 )]
1980 ops: Option<String>,
1981 #[arg(long, help = "Validate ops and report summary without mutating files")]
1982 dry_run: bool,
1983 #[arg(
1984 long,
1985 help = "Apply sheet layout ops by atomically replacing the source file"
1986 )]
1987 in_place: bool,
1988 #[arg(
1989 long,
1990 value_name = "PATH",
1991 help = "Apply sheet layout ops to this output path"
1992 )]
1993 output: Option<PathBuf>,
1994 #[arg(long, help = "Allow overwriting --output when it already exists")]
1995 force: bool,
1996 #[arg(
1997 long = "print-schema",
1998 hide = true,
1999 help = "Print the full JSON schema for the --ops payload and exit"
2000 )]
2001 print_schema: bool,
2002 },
2003 #[command(
2004 about = "Apply stateless data validation and conditional format operations from an @ops payload",
2005 after_long_help = r##"Examples:
2006 agent-spreadsheet rules-batch workbook.xlsx --ops @rules_ops.json --dry-run
2007 agent-spreadsheet rules-batch workbook.xlsx --ops @rules_ops.json --output ruled.xlsx --force
2008
2009Payload examples (`--ops @rules_ops.json`):
2010 Minimal:
2011 {"ops":[{"kind":"set_data_validation","sheet_name":"Sheet1","target_range":"B2:B4","validation":{"kind":"list","formula1":"\"A,B,C\""}}]}
2012 Advanced:
2013 {"ops":[{"kind":"set_conditional_format","sheet_name":"Sheet1","target_range":"C2:C10","rule":{"kind":"expression","formula":"C2>100"},"style":{"fill_color":"#FFF2CC","bold":true}}]}
2014
2015Required envelope:
2016 Top-level object with an `ops` array.
2017 Each op requires a `kind` discriminator and kind-specific required fields.
2018
2019Note:
2020 Data-validation and conditional-format formulas are rule-level (not cell-level) and do not affect
2021 cell formula caches. No recalculate is needed after rules-batch operations."##
2022 )]
2023 RulesBatch {
2024 #[arg(
2025 value_name = "FILE",
2026 help = "Workbook path to update",
2027 required_unless_present = "print_schema"
2028 )]
2029 file: Option<PathBuf>,
2030 #[arg(
2031 long,
2032 value_name = "OPS_REF",
2033 help = "Ops payload file reference (@path)",
2034 required_unless_present = "print_schema"
2035 )]
2036 ops: Option<String>,
2037 #[arg(long, help = "Validate ops and report summary without mutating files")]
2038 dry_run: bool,
2039 #[arg(long, help = "Apply rules ops by atomically replacing the source file")]
2040 in_place: bool,
2041 #[arg(
2042 long,
2043 value_name = "PATH",
2044 help = "Apply rules ops to this output path"
2045 )]
2046 output: Option<PathBuf>,
2047 #[arg(long, help = "Allow overwriting --output when it already exists")]
2048 force: bool,
2049 #[arg(
2050 long = "print-schema",
2051 hide = true,
2052 help = "Print the full JSON schema for the --ops payload and exit"
2053 )]
2054 print_schema: bool,
2055 #[arg(
2056 long = "formula-parse-policy",
2057 value_enum,
2058 value_name = "POLICY",
2059 help = "Formula parse policy: fail, warn (default for rules-batch), or off"
2060 )]
2061 formula_parse_policy: Option<FormulaParsePolicy>,
2062 },
2063 #[command(
2064 about = "SheetPort manifest lifecycle and execution commands",
2065 after_long_help = "Examples:\n agent-spreadsheet sheetport manifest candidates model.xlsx\n agent-spreadsheet sheetport manifest validate manifest.yaml\n agent-spreadsheet sheetport bind-check model.xlsx manifest.yaml\n agent-spreadsheet sheetport run model.xlsx manifest.yaml --inputs @inputs.json"
2066 )]
2067 Sheetport {
2068 #[command(subcommand)]
2069 command: SheetportCommands,
2070 },
2071 #[command(
2072 about = "Find and replace text in formula bodies (not values)",
2073 after_long_help = r#"Examples:
2074 agent-spreadsheet replace-in-formulas data.xlsx Sheet1 --find '$64' --replace '$65' --dry-run
2075 agent-spreadsheet replace-in-formulas data.xlsx Sheet1 --find 'SUM' --replace 'SUMIFS' --in-place
2076 agent-spreadsheet replace-in-formulas data.xlsx Sheet1 --find 'Sheet1!' --replace 'Sheet2!' --range A1:Z100 --output fixed.xlsx
2077 agent-spreadsheet replace-in-formulas data.xlsx Sheet1 --find '(?i)old_name' --replace 'new_name' --regex --in-place
2078
2079Mode selection:
2080 Choose exactly one of --dry-run, --in-place, or --output <PATH>.
2081
2082Behavior:
2083 Only formula-bearing cells are considered. Literal values are never touched.
2084 When --range is omitted, the used range of the sheet is scanned.
2085 Output includes a count of changed formulas and sample diffs (address, before, after).
2086
2087Regex mode:
2088 Use --regex for regular expression patterns. Capture groups are supported in --replace (e.g. $1).
2089
2090Formula parse policy:
2091 After replacement, each new formula is validated. Policy controls behavior on malformed results:
2092 warn (default) => report diagnostics and skip invalid replacements
2093 fail => reject and error
2094 off => skip validation"#
2095 )]
2096 ReplaceInFormulas {
2097 #[arg(value_name = "FILE", help = "Workbook path to update")]
2098 file: PathBuf,
2099 #[arg(
2100 value_name = "SHEET",
2101 help = "Sheet name containing formulas to update"
2102 )]
2103 sheet: String,
2104 #[arg(long, help = "Text or pattern to find in formula bodies")]
2105 find: String,
2106 #[arg(long, help = "Replacement text")]
2107 replace: String,
2108 #[arg(
2109 long,
2110 value_name = "RANGE",
2111 help = "Optional A1 range to scope replacement (default: used range)"
2112 )]
2113 range: Option<String>,
2114 #[arg(long, help = "Interpret --find as a regular expression")]
2115 regex: bool,
2116 #[arg(long, help = "Case-sensitive matching (default: true)")]
2117 case_sensitive: Option<bool>,
2118 #[arg(long, help = "Validate ops and report summary without mutating files")]
2119 dry_run: bool,
2120 #[arg(
2121 long,
2122 help = "Apply replacement by atomically replacing the source file"
2123 )]
2124 in_place: bool,
2125 #[arg(
2126 long,
2127 value_name = "PATH",
2128 help = "Apply replacement to this output path"
2129 )]
2130 output: Option<PathBuf>,
2131 #[arg(long, help = "Allow overwriting --output when it already exists")]
2132 force: bool,
2133 #[arg(
2134 long = "formula-parse-policy",
2135 value_enum,
2136 value_name = "POLICY",
2137 help = "Formula parse policy: warn (default), fail, or off"
2138 )]
2139 formula_parse_policy: Option<FormulaParsePolicy>,
2140 },
2141 #[command(
2142 about = "Recalculate workbook formulas",
2143 after_long_help = "Examples:\n asp recalculate data.xlsx\n asp recalculate data.xlsx --output /tmp/recalced.xlsx\n asp recalculate data.xlsx --output /tmp/recalced.xlsx --force\n\nDefault (no flags): recalculate the file in-place.\n--output <PATH>: copy source to output, recalculate the copy, leave source unchanged.\n--force: allow overwriting an existing --output file."
2144 )]
2145 Recalculate {
2146 #[arg(value_name = "FILE", help = "Workbook path to recalculate")]
2147 file: PathBuf,
2148 #[arg(
2149 long,
2150 value_name = "PATH",
2151 help = "Recalculate into this output path (source stays unchanged)"
2152 )]
2153 output: Option<PathBuf>,
2154 #[arg(long, help = "Allow overwriting --output when it already exists")]
2155 force: bool,
2156 #[arg(
2157 long = "ignore-sheets",
2158 value_name = "SHEETS",
2159 value_delimiter = ',',
2160 help = "Comma-separated sheet names to exclude from changed-cells summary"
2161 )]
2162 ignore_sheets: Option<Vec<String>>,
2163 #[arg(
2164 long = "changed-cells",
2165 help = "Include a summary of cells whose values changed after recalculation"
2166 )]
2167 changed_cells: bool,
2168 },
2169 #[command(
2170 about = "Compare two workbook states and verify target deltas plus error provenance",
2171 after_long_help = "Examples:\n asp verify baseline.xlsx candidate.xlsx --targets Summary!B2\n asp verify baseline.xlsx candidate.xlsx --targets Sheet1!C2,Summary!B2 --named-ranges\n asp verify baseline.xlsx candidate.xlsx --sheet Summary --errors-only\n asp verify baseline.xlsx candidate.xlsx --targets Sheet1!C2,Summary!B2 --targets-only\n\nBehavior:\n - target_deltas compares the exact Sheet!A1 cells you request\n - each target delta includes a classification such as unchanged, direct_edit, recalc_result, formula_shift, or new_error\n - new_errors reports error cells present only in the current workbook\n - resolved_errors reports baseline error cells that no longer error in the current workbook\n - preexisting_errors reports error cells that existed in both baseline and current\n - --sheet scopes error and named-range scans to one sheet; explicit --targets remain exact\n - --errors-only returns only error provenance output\n - --targets-only returns only target proof output\n - --named-ranges adds added/removed/changed named range deltas in default verify mode"
2172 )]
2173 Verify {
2174 #[arg(value_name = "BASELINE", help = "Baseline workbook path")]
2175 baseline: PathBuf,
2176 #[arg(value_name = "CURRENT", help = "Current workbook path")]
2177 current: PathBuf,
2178 #[arg(
2179 long = "targets",
2180 value_name = "SHEET!CELL",
2181 value_delimiter = ',',
2182 help = "One or more Sheet!A1 targets to compare (comma-separated)"
2183 )]
2184 targets: Option<Vec<String>>,
2185 #[arg(
2186 long = "sheet",
2187 value_name = "SHEET",
2188 help = "Limit error and named-range scanning to one sheet"
2189 )]
2190 sheet_name: Option<String>,
2191 #[arg(
2192 long = "named-ranges",
2193 help = "Include added/removed/changed named range deltas"
2194 )]
2195 named_ranges: bool,
2196 #[arg(
2197 long,
2198 help = "Return only error provenance output (no target or named-range deltas)"
2199 )]
2200 errors_only: bool,
2201 #[arg(long, help = "Return only target proof output (requires --targets)")]
2202 targets_only: bool,
2203 },
2204 #[command(
2205 about = "Diff two workbook versions with summary-first, paged details",
2206 after_long_help = "Examples:\n asp diff baseline.xlsx candidate.xlsx\n asp diff baseline.xlsx candidate.xlsx --details --limit 200 --offset 0\n asp diff baseline.xlsx candidate.xlsx --sheet \"GL Data\" --range A1:P200\n asp diff baseline.xlsx candidate.xlsx --exclude-recalc-result\n\nBehavior:\n - summary output now includes grouped change buckets and subtype counts\n - recalc_result changes are counted separately from direct edits\n - --exclude-recalc-result suppresses cached-value churn so direct edits are easier to review"
2207 )]
2208 Diff {
2209 #[arg(value_name = "ORIGINAL", help = "Baseline workbook path")]
2210 original: PathBuf,
2211 #[arg(value_name = "MODIFIED", help = "Modified workbook path")]
2212 modified: PathBuf,
2213 #[arg(long, help = "Limit diff to one sheet name")]
2214 sheet: Option<String>,
2215 #[arg(
2216 long,
2217 value_name = "SHEETS",
2218 value_delimiter = ',',
2219 help = "Limit diff to multiple sheet names (comma-separated)"
2220 )]
2221 sheets: Option<Vec<String>>,
2222 #[arg(
2223 long,
2224 value_name = "A1_RANGE",
2225 help = "Optional A1 range filter (e.g. A1:C100)"
2226 )]
2227 range: Option<String>,
2228 #[arg(
2229 long,
2230 help = "Include paged change items; default output is summary-only"
2231 )]
2232 details: bool,
2233 #[arg(
2234 long = "exclude-recalc-result",
2235 help = "Exclude recalc_result cell changes from summary and details"
2236 )]
2237 exclude_recalc_result: bool,
2238 #[arg(
2239 long,
2240 default_value_t = 200,
2241 help = "Page size for --details (1..2000)"
2242 )]
2243 limit: u32,
2244 #[arg(long, default_value_t = 0, help = "Offset for --details pagination")]
2245 offset: u32,
2246 },
2247 #[command(
2248 about = "Print canonical JSON schema for a command or payload target",
2249 after_long_help = "Examples:\n asp schema transform-batch\n asp schema structure-batch\n asp schema session-op transform.write_matrix"
2250 )]
2251 Schema {
2252 #[command(subcommand)]
2253 command: DiscoverabilityCommands,
2254 },
2255 #[command(
2256 about = "Print a copy-pastable canonical example for a command or payload target",
2257 after_long_help = "Examples:\n asp example transform-batch\n asp example rules-batch\n asp example session-op structure.clone_row"
2258 )]
2259 Example {
2260 #[command(subcommand)]
2261 command: DiscoverabilityCommands,
2262 },
2263 #[command(
2264 about = "Event-sourced session management (start, navigate, stage, apply, materialize)",
2265 subcommand,
2266 after_long_help = "Session commands provide event-sourced workbook editing with undo/redo, branching, staged apply, and payload discovery.\n\nWorkflow:\n 1. asp session start --base model.xlsx\n 2. asp example session-op transform.write_matrix\n 3. asp session op --session <id> --ops @edits.json\n 4. asp session apply --session <id> <staged_id>\n 5. asp session materialize --session <id> --output result.xlsx\n\nDiscoverability:\n • asp schema session-op transform.write_matrix\n • asp example session-op transform.write_matrix"
2267 )]
2268 Session(Box<SessionCommands>),
2269 #[command(
2270 about = "[Deprecated] Execute a SheetPort manifest with JSON inputs",
2271 after_long_help = "Use `agent-spreadsheet sheetport run ...` for new workflows.\n\nExamples:\n agent-spreadsheet run-manifest data.xlsx manifest.yaml --inputs '{\"loan\": 10000}'\n agent-spreadsheet sheetport run data.xlsx manifest.yaml --inputs @inputs.json"
2272 )]
2273 RunManifest {
2274 #[arg(value_name = "FILE", help = "Path to the workbook")]
2275 file: PathBuf,
2276 #[arg(value_name = "MANIFEST", help = "Path to the YAML manifest")]
2277 manifest: PathBuf,
2278 #[arg(long, help = "JSON string or @file containing input arguments")]
2279 inputs: Option<String>,
2280 #[arg(long, help = "Seed for deterministic RNG evaluation")]
2281 rng_seed: Option<u64>,
2282 #[arg(long, help = "Freeze volatile functions (e.g. NOW(), RAND())")]
2283 freeze_volatile: bool,
2284 },
2285}
2286
2287pub async fn run_command(command: Commands) -> Result<Value> {
2288 match command {
2289 Commands::ListSheets {
2290 file,
2291 session,
2292 session_workspace,
2293 } => {
2294 let (resolved, _guard) =
2295 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2296 commands::read::list_sheets(resolved).await
2297 }
2298 Commands::SheetOverview {
2299 file,
2300 sheet,
2301 session,
2302 session_workspace,
2303 } => {
2304 let (resolved, _guard) =
2305 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2306 commands::read::sheet_overview(resolved, sheet).await
2307 }
2308 Commands::RangeValues {
2309 file,
2310 sheet,
2311 ranges,
2312 format,
2313 include_formulas,
2314 session,
2315 session_workspace,
2316 } => {
2317 let (resolved, _guard) =
2318 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2319 commands::read::range_values(resolved, sheet, ranges, format, include_formulas).await
2320 }
2321 Commands::RangeExport {
2322 file,
2323 sheet,
2324 range,
2325 format,
2326 output,
2327 include_formulas,
2328 session,
2329 session_workspace,
2330 } => {
2331 let (resolved, _guard) =
2332 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2333 commands::read::range_export(resolved, sheet, range, format, output, include_formulas)
2334 .await
2335 }
2336 Commands::RangeImport {
2337 file,
2338 sheet,
2339 anchor,
2340 from_grid,
2341 from_csv,
2342 header,
2343 clear_target,
2344 dry_run,
2345 in_place,
2346 output,
2347 force,
2348 } => {
2349 commands::write::range_import(
2350 file,
2351 sheet,
2352 anchor,
2353 from_grid,
2354 from_csv,
2355 header,
2356 clear_target,
2357 dry_run,
2358 in_place,
2359 output,
2360 force,
2361 )
2362 .await
2363 }
2364 Commands::InspectCells {
2365 file,
2366 sheet,
2367 targets,
2368 include_empty,
2369 budget,
2370 session,
2371 session_workspace,
2372 } => {
2373 let (resolved, _guard) =
2374 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2375 commands::read::inspect_cells(resolved, sheet, targets, include_empty, budget).await
2376 }
2377 Commands::SheetPage {
2378 file,
2379 sheet,
2380 start_row,
2381 page_size,
2382 columns,
2383 columns_by_header,
2384 include_formulas,
2385 include_styles,
2386 include_header,
2387 format,
2388 session,
2389 session_workspace,
2390 } => {
2391 let (resolved, _guard) =
2392 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2393 commands::read::sheet_page(
2394 resolved,
2395 sheet,
2396 start_row,
2397 page_size,
2398 columns,
2399 columns_by_header,
2400 include_formulas,
2401 include_styles,
2402 include_header,
2403 format,
2404 )
2405 .await
2406 }
2407 Commands::ReadTable {
2408 file,
2409 sheet,
2410 range,
2411 table_name,
2412 region_id,
2413 limit,
2414 offset,
2415 sample_mode,
2416 filters_json,
2417 filters_file,
2418 table_format,
2419 session,
2420 session_workspace,
2421 } => {
2422 let (resolved, _guard) =
2423 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2424 commands::read::read_table(
2425 resolved,
2426 sheet,
2427 range,
2428 table_name,
2429 region_id,
2430 limit,
2431 offset,
2432 sample_mode,
2433 filters_json,
2434 filters_file,
2435 table_format,
2436 )
2437 .await
2438 }
2439 Commands::FindValue {
2440 file,
2441 query,
2442 sheet,
2443 mode,
2444 label_direction,
2445 session,
2446 session_workspace,
2447 } => {
2448 let (resolved, _guard) =
2449 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2450 commands::read::find_value(resolved, query, sheet, mode, label_direction).await
2451 }
2452 Commands::NamedRanges {
2453 file,
2454 sheet,
2455 name_prefix,
2456 session,
2457 session_workspace,
2458 } => {
2459 let (resolved, _guard) =
2460 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2461 commands::read::named_ranges(resolved, sheet, name_prefix).await
2462 }
2463 Commands::DefineName {
2464 file,
2465 name,
2466 refers_to,
2467 scope,
2468 scope_sheet_name,
2469 dry_run,
2470 in_place,
2471 output,
2472 force,
2473 } => {
2474 commands::write::define_name(
2475 file,
2476 name,
2477 refers_to,
2478 scope,
2479 scope_sheet_name,
2480 dry_run,
2481 in_place,
2482 output,
2483 force,
2484 )
2485 .await
2486 }
2487 Commands::UpdateName {
2488 file,
2489 name,
2490 refers_to,
2491 scope,
2492 scope_sheet_name,
2493 dry_run,
2494 in_place,
2495 output,
2496 force,
2497 } => {
2498 commands::write::update_name(
2499 file,
2500 name,
2501 refers_to,
2502 scope,
2503 scope_sheet_name,
2504 dry_run,
2505 in_place,
2506 output,
2507 force,
2508 )
2509 .await
2510 }
2511 Commands::DeleteName {
2512 file,
2513 name,
2514 scope,
2515 scope_sheet_name,
2516 dry_run,
2517 in_place,
2518 output,
2519 force,
2520 } => {
2521 commands::write::delete_name(
2522 file,
2523 name,
2524 scope,
2525 scope_sheet_name,
2526 dry_run,
2527 in_place,
2528 output,
2529 force,
2530 )
2531 .await
2532 }
2533 Commands::FindFormula {
2534 file,
2535 query,
2536 sheet,
2537 limit,
2538 offset,
2539 } => commands::read::find_formula(file, query, sheet, limit, offset).await,
2540 Commands::ScanVolatiles {
2541 file,
2542 sheet,
2543 limit,
2544 offset,
2545 formula_parse_policy,
2546 } => commands::read::scan_volatiles(file, sheet, limit, offset, formula_parse_policy).await,
2547 Commands::SheetStatistics { file, sheet } => {
2548 commands::read::sheet_statistics(file, sheet).await
2549 }
2550 Commands::FormulaMap {
2551 file,
2552 sheet,
2553 limit,
2554 sort_by,
2555 formula_parse_policy,
2556 } => commands::read::formula_map(file, sheet, limit, sort_by, formula_parse_policy).await,
2557 Commands::FormulaTrace {
2558 file,
2559 sheet,
2560 cell,
2561 direction,
2562 depth,
2563 page_size,
2564 cursor_depth,
2565 cursor_offset,
2566 formula_parse_policy,
2567 session,
2568 session_workspace,
2569 } => {
2570 let (resolved, _guard) =
2571 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2572 commands::read::formula_trace(
2573 resolved,
2574 sheet,
2575 cell,
2576 direction,
2577 depth,
2578 page_size,
2579 cursor_depth,
2580 cursor_offset,
2581 formula_parse_policy,
2582 )
2583 .await
2584 }
2585 Commands::Describe {
2586 file,
2587 session,
2588 session_workspace,
2589 } => {
2590 let (resolved, _guard) =
2591 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2592 commands::read::describe(resolved).await
2593 }
2594 Commands::TableProfile {
2595 file,
2596 sheet,
2597 session,
2598 session_workspace,
2599 } => {
2600 let (resolved, _guard) =
2601 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2602 commands::read::table_profile(resolved, sheet).await
2603 }
2604 Commands::LayoutPage {
2605 file,
2606 sheet,
2607 range_positional,
2608 range,
2609 mode,
2610 max_col_width,
2611 fit_columns,
2612 skip_empty_columns_trim,
2613 render,
2614 session,
2615 session_workspace,
2616 } => {
2617 let (resolved, _guard) =
2618 commands::read::resolve_file_or_session(file, session, session_workspace)?;
2619 commands::read::layout_page(
2620 resolved,
2621 sheet,
2622 range.or(range_positional),
2623 mode,
2624 max_col_width,
2625 fit_columns,
2626 skip_empty_columns_trim,
2627 render,
2628 )
2629 .await
2630 }
2631 Commands::CreateWorkbook {
2632 path,
2633 sheets,
2634 overwrite,
2635 } => commands::write::create_workbook(path, sheets, overwrite).await,
2636 Commands::Copy { source, dest } => commands::write::copy(source, dest).await,
2637 Commands::Edit {
2638 file,
2639 sheet,
2640 dry_run,
2641 in_place,
2642 output,
2643 force,
2644 edits,
2645 edits_file,
2646 formula_parse_policy,
2647 } => {
2648 commands::write::edit(
2649 file,
2650 sheet,
2651 edits,
2652 edits_file,
2653 dry_run,
2654 in_place,
2655 output,
2656 force,
2657 formula_parse_policy,
2658 )
2659 .await
2660 }
2661 Commands::AppendRegion {
2662 file,
2663 sheet_name,
2664 region_id,
2665 table_name,
2666 rows,
2667 from_csv,
2668 header,
2669 footer_policy,
2670 dry_run,
2671 in_place,
2672 output,
2673 force,
2674 } => {
2675 commands::write::append_region(
2676 file,
2677 sheet_name,
2678 region_id,
2679 table_name,
2680 rows,
2681 from_csv,
2682 header,
2683 footer_policy,
2684 dry_run,
2685 in_place,
2686 output,
2687 force,
2688 )
2689 .await
2690 }
2691 Commands::CloneTemplateRow {
2692 file,
2693 sheet_name,
2694 source_row,
2695 before,
2696 after,
2697 insert_at,
2698 count,
2699 expand_adjacent_sums,
2700 patch_targets,
2701 merge_policy,
2702 dry_run,
2703 in_place,
2704 output,
2705 force,
2706 } => {
2707 commands::write::clone_template_row(
2708 file,
2709 sheet_name,
2710 source_row,
2711 before,
2712 after,
2713 insert_at,
2714 count,
2715 expand_adjacent_sums,
2716 patch_targets,
2717 merge_policy,
2718 dry_run,
2719 in_place,
2720 output,
2721 force,
2722 )
2723 .await
2724 }
2725 Commands::CloneRowBand {
2726 file,
2727 sheet_name,
2728 source_rows,
2729 before,
2730 after,
2731 insert_at,
2732 repeat,
2733 expand_adjacent_sums,
2734 patch_targets,
2735 merge_policy,
2736 dry_run,
2737 in_place,
2738 output,
2739 force,
2740 } => {
2741 commands::write::clone_row_band(
2742 file,
2743 sheet_name,
2744 source_rows,
2745 before,
2746 after,
2747 insert_at,
2748 repeat,
2749 expand_adjacent_sums,
2750 patch_targets,
2751 merge_policy,
2752 dry_run,
2753 in_place,
2754 output,
2755 force,
2756 )
2757 .await
2758 }
2759 Commands::TransformBatch {
2760 file,
2761 ops,
2762 dry_run,
2763 in_place,
2764 output,
2765 force,
2766 print_schema,
2767 formula_parse_policy,
2768 } => {
2769 if print_schema {
2770 commands::write::batch_payload_schema(
2771 commands::write::BatchSchemaCommand::Transform,
2772 )
2773 } else {
2774 let file = file.ok_or_else(|| {
2775 anyhow::anyhow!("invalid argument: transform-batch requires <FILE>")
2776 })?;
2777 let ops = ops.ok_or_else(|| {
2778 anyhow::anyhow!("invalid argument: transform-batch requires --ops @<path>")
2779 })?;
2780 commands::write::transform_batch(
2781 file,
2782 ops,
2783 dry_run,
2784 in_place,
2785 output,
2786 force,
2787 formula_parse_policy,
2788 )
2789 .await
2790 }
2791 }
2792 Commands::StyleBatch {
2793 file,
2794 ops,
2795 dry_run,
2796 in_place,
2797 output,
2798 force,
2799 print_schema,
2800 } => {
2801 if print_schema {
2802 commands::write::batch_payload_schema(commands::write::BatchSchemaCommand::Style)
2803 } else {
2804 let file = file.ok_or_else(|| {
2805 anyhow::anyhow!("invalid argument: style-batch requires <FILE>")
2806 })?;
2807 let ops = ops.ok_or_else(|| {
2808 anyhow::anyhow!("invalid argument: style-batch requires --ops @<path>")
2809 })?;
2810 commands::write::style_batch(file, ops, dry_run, in_place, output, force).await
2811 }
2812 }
2813 Commands::ApplyFormulaPattern {
2814 file,
2815 ops,
2816 dry_run,
2817 in_place,
2818 output,
2819 force,
2820 print_schema,
2821 } => {
2822 if print_schema {
2823 commands::write::batch_payload_schema(
2824 commands::write::BatchSchemaCommand::ApplyFormulaPattern,
2825 )
2826 } else {
2827 let file = file.ok_or_else(|| {
2828 anyhow::anyhow!("invalid argument: apply-formula-pattern requires <FILE>")
2829 })?;
2830 let ops = ops.ok_or_else(|| {
2831 anyhow::anyhow!(
2832 "invalid argument: apply-formula-pattern requires --ops @<path>"
2833 )
2834 })?;
2835 commands::write::apply_formula_pattern(file, ops, dry_run, in_place, output, force)
2836 .await
2837 }
2838 }
2839 Commands::StructureBatch {
2840 file,
2841 ops,
2842 dry_run,
2843 in_place,
2844 output,
2845 force,
2846 print_schema,
2847 formula_parse_policy,
2848 impact_report,
2849 show_formula_delta,
2850 } => {
2851 if print_schema {
2852 commands::write::batch_payload_schema(
2853 commands::write::BatchSchemaCommand::Structure,
2854 )
2855 } else {
2856 let file = file.ok_or_else(|| {
2857 anyhow::anyhow!("invalid argument: structure-batch requires <FILE>")
2858 })?;
2859 let ops = ops.ok_or_else(|| {
2860 anyhow::anyhow!("invalid argument: structure-batch requires --ops @<path>")
2861 })?;
2862 commands::write::structure_batch(
2863 file,
2864 ops,
2865 dry_run,
2866 in_place,
2867 output,
2868 force,
2869 formula_parse_policy,
2870 impact_report,
2871 show_formula_delta,
2872 )
2873 .await
2874 }
2875 }
2876 Commands::CheckRefImpact {
2877 file,
2878 ops,
2879 show_formula_delta,
2880 } => commands::write::check_ref_impact(file, ops, show_formula_delta).await,
2881 Commands::ColumnSizeBatch {
2882 file,
2883 ops,
2884 dry_run,
2885 in_place,
2886 output,
2887 force,
2888 print_schema,
2889 } => {
2890 if print_schema {
2891 commands::write::batch_payload_schema(
2892 commands::write::BatchSchemaCommand::ColumnSize,
2893 )
2894 } else {
2895 let file = file.ok_or_else(|| {
2896 anyhow::anyhow!("invalid argument: column-size-batch requires <FILE>")
2897 })?;
2898 let ops = ops.ok_or_else(|| {
2899 anyhow::anyhow!("invalid argument: column-size-batch requires --ops @<path>")
2900 })?;
2901 commands::write::column_size_batch(file, ops, dry_run, in_place, output, force)
2902 .await
2903 }
2904 }
2905 Commands::SheetLayoutBatch {
2906 file,
2907 ops,
2908 dry_run,
2909 in_place,
2910 output,
2911 force,
2912 print_schema,
2913 } => {
2914 if print_schema {
2915 commands::write::batch_payload_schema(
2916 commands::write::BatchSchemaCommand::SheetLayout,
2917 )
2918 } else {
2919 let file = file.ok_or_else(|| {
2920 anyhow::anyhow!("invalid argument: sheet-layout-batch requires <FILE>")
2921 })?;
2922 let ops = ops.ok_or_else(|| {
2923 anyhow::anyhow!("invalid argument: sheet-layout-batch requires --ops @<path>")
2924 })?;
2925 commands::write::sheet_layout_batch(file, ops, dry_run, in_place, output, force)
2926 .await
2927 }
2928 }
2929 Commands::RulesBatch {
2930 file,
2931 ops,
2932 dry_run,
2933 in_place,
2934 output,
2935 force,
2936 print_schema,
2937 formula_parse_policy,
2938 } => {
2939 if print_schema {
2940 commands::write::batch_payload_schema(commands::write::BatchSchemaCommand::Rules)
2941 } else {
2942 let file = file.ok_or_else(|| {
2943 anyhow::anyhow!("invalid argument: rules-batch requires <FILE>")
2944 })?;
2945 let ops = ops.ok_or_else(|| {
2946 anyhow::anyhow!("invalid argument: rules-batch requires --ops @<path>")
2947 })?;
2948 commands::write::rules_batch(
2949 file,
2950 ops,
2951 dry_run,
2952 in_place,
2953 output,
2954 force,
2955 formula_parse_policy,
2956 )
2957 .await
2958 }
2959 }
2960 Commands::Sheetport { command } => match command {
2961 SheetportCommands::Manifest(manifest_command) => match manifest_command {
2962 SheetportManifestCommands::Candidates { file, sheet_filter } => {
2963 commands::read::sheetport_manifest_candidates(file, sheet_filter).await
2964 }
2965 SheetportManifestCommands::Schema => commands::read::sheetport_manifest_schema(),
2966 SheetportManifestCommands::Validate { manifest } => {
2967 commands::read::sheetport_manifest_validate(manifest)
2968 }
2969 SheetportManifestCommands::Normalize { manifest, output } => {
2970 commands::read::sheetport_manifest_normalize(manifest, output)
2971 }
2972 },
2973 SheetportCommands::BindCheck { file, manifest } => {
2974 commands::read::sheetport_bind_check(file, manifest).await
2975 }
2976 SheetportCommands::Run {
2977 file,
2978 manifest,
2979 inputs,
2980 rng_seed,
2981 freeze_volatile,
2982 } => {
2983 commands::read::sheetport_run(file, manifest, inputs, rng_seed, freeze_volatile)
2984 .await
2985 }
2986 },
2987 Commands::ReplaceInFormulas {
2988 file,
2989 sheet,
2990 find,
2991 replace,
2992 range,
2993 regex,
2994 case_sensitive,
2995 dry_run,
2996 in_place,
2997 output,
2998 force,
2999 formula_parse_policy,
3000 } => {
3001 commands::write::replace_in_formulas(
3002 file,
3003 sheet,
3004 find,
3005 replace,
3006 range,
3007 regex,
3008 case_sensitive.unwrap_or(true),
3009 dry_run,
3010 in_place,
3011 output,
3012 force,
3013 formula_parse_policy,
3014 )
3015 .await
3016 }
3017 Commands::Recalculate {
3018 file,
3019 output,
3020 force,
3021 ignore_sheets,
3022 changed_cells,
3023 } => commands::recalc::recalculate(file, output, force, ignore_sheets, changed_cells).await,
3024 Commands::Verify {
3025 baseline,
3026 current,
3027 targets,
3028 sheet_name,
3029 named_ranges,
3030 errors_only,
3031 targets_only,
3032 } => {
3033 commands::verify::verify(
3034 baseline,
3035 current,
3036 targets,
3037 sheet_name,
3038 named_ranges,
3039 errors_only,
3040 targets_only,
3041 )
3042 .await
3043 }
3044 Commands::Diff {
3045 original,
3046 modified,
3047 sheet,
3048 sheets,
3049 range,
3050 details,
3051 limit,
3052 offset,
3053 exclude_recalc_result,
3054 } => {
3055 commands::diff::diff(commands::diff::DiffCommandArgs {
3056 original,
3057 modified,
3058 sheet,
3059 sheets,
3060 range,
3061 details,
3062 limit,
3063 offset,
3064 exclude_recalc_result,
3065 })
3066 .await
3067 }
3068 Commands::Schema { command } => run_schema_command(command),
3069 Commands::Example { command } => run_example_command(command),
3070 Commands::Session(command) => match *command {
3071 SessionCommands::Start {
3072 base,
3073 label,
3074 workspace,
3075 } => commands::session::session_start(base, label, workspace).await,
3076 SessionCommands::Log {
3077 session,
3078 since,
3079 kind,
3080 workspace,
3081 } => commands::session::session_log(session, workspace, since, kind).await,
3082 SessionCommands::Branches { session, workspace } => {
3083 commands::session::session_branches(session, workspace).await
3084 }
3085 SessionCommands::Switch {
3086 session,
3087 branch,
3088 workspace,
3089 } => commands::session::session_switch(session, branch, workspace).await,
3090 SessionCommands::Checkout {
3091 session,
3092 op_id,
3093 workspace,
3094 } => commands::session::session_checkout(session, op_id, workspace).await,
3095 SessionCommands::Undo { session, workspace } => {
3096 commands::session::session_undo(session, workspace).await
3097 }
3098 SessionCommands::Redo { session, workspace } => {
3099 commands::session::session_redo(session, workspace).await
3100 }
3101 SessionCommands::Fork {
3102 session,
3103 from,
3104 label,
3105 branch_name,
3106 workspace,
3107 } => {
3108 commands::session::session_fork(session, from, label, branch_name, workspace).await
3109 }
3110 SessionCommands::Op {
3111 session,
3112 ops,
3113 workspace,
3114 } => commands::session::session_op_stage(session, ops, workspace).await,
3115 SessionCommands::Apply {
3116 session,
3117 staged_id,
3118 workspace,
3119 } => commands::session::session_apply(session, staged_id, workspace).await,
3120 SessionCommands::Materialize {
3121 session,
3122 output,
3123 force,
3124 workspace,
3125 } => commands::session::session_materialize(session, output, workspace, force).await,
3126 },
3127 Commands::RunManifest {
3128 file,
3129 manifest,
3130 inputs,
3131 rng_seed,
3132 freeze_volatile,
3133 } => commands::read::sheetport_run(file, manifest, inputs, rng_seed, freeze_volatile).await,
3134 }
3135}
3136
3137fn run_schema_command(command: DiscoverabilityCommands) -> Result<Value> {
3138 match command {
3139 DiscoverabilityCommands::TransformBatch => {
3140 commands::write::batch_payload_schema(commands::write::BatchSchemaCommand::Transform)
3141 }
3142 DiscoverabilityCommands::StyleBatch => {
3143 commands::write::batch_payload_schema(commands::write::BatchSchemaCommand::Style)
3144 }
3145 DiscoverabilityCommands::ApplyFormulaPattern => commands::write::batch_payload_schema(
3146 commands::write::BatchSchemaCommand::ApplyFormulaPattern,
3147 ),
3148 DiscoverabilityCommands::StructureBatch => {
3149 commands::write::batch_payload_schema(commands::write::BatchSchemaCommand::Structure)
3150 }
3151 DiscoverabilityCommands::ColumnSizeBatch => {
3152 commands::write::batch_payload_schema(commands::write::BatchSchemaCommand::ColumnSize)
3153 }
3154 DiscoverabilityCommands::SheetLayoutBatch => {
3155 commands::write::batch_payload_schema(commands::write::BatchSchemaCommand::SheetLayout)
3156 }
3157 DiscoverabilityCommands::RulesBatch => {
3158 commands::write::batch_payload_schema(commands::write::BatchSchemaCommand::Rules)
3159 }
3160 DiscoverabilityCommands::SessionOp { kind } => {
3161 commands::session::session_payload_schema(kind)
3162 }
3163 }
3164}
3165
3166fn run_example_command(command: DiscoverabilityCommands) -> Result<Value> {
3167 match command {
3168 DiscoverabilityCommands::TransformBatch => {
3169 commands::write::batch_payload_example(commands::write::BatchSchemaCommand::Transform)
3170 }
3171 DiscoverabilityCommands::StyleBatch => {
3172 commands::write::batch_payload_example(commands::write::BatchSchemaCommand::Style)
3173 }
3174 DiscoverabilityCommands::ApplyFormulaPattern => commands::write::batch_payload_example(
3175 commands::write::BatchSchemaCommand::ApplyFormulaPattern,
3176 ),
3177 DiscoverabilityCommands::StructureBatch => {
3178 commands::write::batch_payload_example(commands::write::BatchSchemaCommand::Structure)
3179 }
3180 DiscoverabilityCommands::ColumnSizeBatch => {
3181 commands::write::batch_payload_example(commands::write::BatchSchemaCommand::ColumnSize)
3182 }
3183 DiscoverabilityCommands::SheetLayoutBatch => {
3184 commands::write::batch_payload_example(commands::write::BatchSchemaCommand::SheetLayout)
3185 }
3186 DiscoverabilityCommands::RulesBatch => {
3187 commands::write::batch_payload_example(commands::write::BatchSchemaCommand::Rules)
3188 }
3189 DiscoverabilityCommands::SessionOp { kind } => {
3190 commands::session::session_payload_example(kind)
3191 }
3192 }
3193}
3194
3195fn first_subcommand_index(argv: &[OsString]) -> Option<usize> {
3196 let mut expect_global_value = false;
3197
3198 for (index, arg) in argv.iter().enumerate().skip(1) {
3199 let token = arg.to_string_lossy();
3200
3201 if expect_global_value {
3202 expect_global_value = false;
3203 continue;
3204 }
3205
3206 match token.as_ref() {
3207 "--output-format" | "--shape" | "--format" => {
3208 expect_global_value = true;
3209 continue;
3210 }
3211 "--compact" | "--quiet" => continue,
3212 _ => {}
3213 }
3214
3215 if token.starts_with("--output-format=")
3216 || token.starts_with("--shape=")
3217 || token.starts_with("--format=")
3218 {
3219 continue;
3220 }
3221
3222 if token.starts_with('-') {
3223 continue;
3224 }
3225
3226 return Some(index);
3227 }
3228
3229 None
3230}
3231
3232fn is_legacy_output_format(value: &str) -> bool {
3233 matches!(value, "json" | "csv")
3234}
3235
3236fn normalize_legacy_global_format_argv(argv: Vec<OsString>) -> Vec<OsString> {
3237 if argv.len() <= 1 {
3238 return argv;
3239 }
3240
3241 let first_subcommand_index = first_subcommand_index(&argv);
3242 let first_subcommand_name = first_subcommand_index
3243 .map(|index| argv[index].to_string_lossy().into_owned())
3244 .unwrap_or_default();
3245 let second_subcommand_name = first_subcommand_index.and_then(|index| {
3246 argv.get(index + 1)
3247 .map(|value| value.to_string_lossy().into_owned())
3248 });
3249 let preserve_sheet_page_format = first_subcommand_name == "sheet-page"
3250 || first_subcommand_name == "range-export"
3251 || first_subcommand_name == "range-values"
3252 || (first_subcommand_name == "read"
3253 && matches!(
3254 second_subcommand_name.as_deref(),
3255 Some("page") | Some("export") | Some("values")
3256 ));
3257
3258 let mut normalized = Vec::with_capacity(argv.len());
3259 normalized.push(argv[0].clone());
3260
3261 let mut index = 1usize;
3262 while index < argv.len() {
3263 let token = argv[index].to_string_lossy();
3264 let can_rewrite_here = !preserve_sheet_page_format
3265 || first_subcommand_index
3266 .map(|subcommand_index| index < subcommand_index)
3267 .unwrap_or(true);
3268
3269 if can_rewrite_here && token == "--format" && index + 1 < argv.len() {
3270 let value = argv[index + 1].to_string_lossy();
3271 if is_legacy_output_format(value.as_ref()) {
3272 normalized.push(OsString::from("--output-format"));
3273 normalized.push(argv[index + 1].clone());
3274 index += 2;
3275 continue;
3276 }
3277 }
3278
3279 if can_rewrite_here
3280 && let Some(value) = token.strip_prefix("--format=")
3281 && is_legacy_output_format(value)
3282 {
3283 normalized.push(OsString::from(format!("--output-format={value}")));
3284 index += 1;
3285 continue;
3286 }
3287
3288 normalized.push(argv[index].clone());
3289 index += 1;
3290 }
3291
3292 normalized
3293}
3294
3295#[derive(Debug)]
3296enum ResolvedSurfaceCommand {
3297 Command(Commands),
3298 Schema(DiscoverabilityCommands),
3299 Example(DiscoverabilityCommands),
3300}
3301
3302fn flat_to_canonical_command(flat: &str) -> Option<&'static str> {
3303 match flat {
3304 "list-sheets" => Some("read sheets"),
3305 "sheet-overview" => Some("read overview"),
3306 "range-values" => Some("read values"),
3307 "range-export" => Some("read export"),
3308 "inspect-cells" => Some("read cells"),
3309 "sheet-page" => Some("read page"),
3310 "read-table" => Some("read table"),
3311 "named-ranges" => Some("read names"),
3312 "describe" => Some("read workbook"),
3313 "layout-page" => Some("read layout"),
3314 "find-value" => Some("analyze find-value"),
3315 "find-formula" => Some("analyze find-formula"),
3316 "formula-map" => Some("analyze formula-map"),
3317 "formula-trace" => Some("analyze formula-trace"),
3318 "scan-volatiles" => Some("analyze scan-volatiles"),
3319 "sheet-statistics" => Some("analyze sheet-statistics"),
3320 "table-profile" => Some("analyze table-profile"),
3321 "check-ref-impact" => Some("analyze ref-impact"),
3322 "edit" => Some("write cells"),
3323 "range-import" => Some("write import"),
3324 "append-region" => Some("write append"),
3325 "clone-template-row" => Some("write clone-template-row"),
3326 "clone-row-band" => Some("write clone-row-band"),
3327 "replace-in-formulas" => Some("write formulas replace"),
3328 "transform-batch" => Some("write batch transform"),
3329 "style-batch" => Some("write batch style"),
3330 "apply-formula-pattern" => Some("write batch formula-pattern"),
3331 "structure-batch" => Some("write batch structure"),
3332 "column-size-batch" => Some("write batch column-size"),
3333 "sheet-layout-batch" => Some("write batch sheet-layout"),
3334 "rules-batch" => Some("write batch rules"),
3335 "define-name" => Some("write name define"),
3336 "update-name" => Some("write name update"),
3337 "delete-name" => Some("write name delete"),
3338 "create-workbook" => Some("workbook create"),
3339 "copy" => Some("workbook copy"),
3340 "recalculate" => Some("workbook recalculate"),
3341 "verify" => Some("verify proof"),
3342 "diff" => Some("verify diff"),
3343 "run-manifest" => Some("sheetport run"),
3344 _ => None,
3345 }
3346}
3347
3348fn flat_to_nested_tokens(flat: &str) -> Option<&'static [&'static str]> {
3349 match flat {
3350 "list-sheets" => Some(&["read", "sheets"]),
3351 "sheet-overview" => Some(&["read", "overview"]),
3352 "range-values" => Some(&["read", "values"]),
3353 "range-export" => Some(&["read", "export"]),
3354 "inspect-cells" => Some(&["read", "cells"]),
3355 "sheet-page" => Some(&["read", "page"]),
3356 "read-table" => Some(&["read", "table"]),
3357 "named-ranges" => Some(&["read", "names"]),
3358 "describe" => Some(&["read", "workbook"]),
3359 "layout-page" => Some(&["read", "layout"]),
3360 "find-value" => Some(&["analyze", "find-value"]),
3361 "find-formula" => Some(&["analyze", "find-formula"]),
3362 "formula-map" => Some(&["analyze", "formula-map"]),
3363 "formula-trace" => Some(&["analyze", "formula-trace"]),
3364 "scan-volatiles" => Some(&["analyze", "scan-volatiles"]),
3365 "sheet-statistics" => Some(&["analyze", "sheet-statistics"]),
3366 "table-profile" => Some(&["analyze", "table-profile"]),
3367 "check-ref-impact" => Some(&["analyze", "ref-impact"]),
3368 "edit" => Some(&["write", "cells"]),
3369 "range-import" => Some(&["write", "import"]),
3370 "append-region" => Some(&["write", "append"]),
3371 "clone-template-row" => Some(&["write", "clone-template-row"]),
3372 "clone-row-band" => Some(&["write", "clone-row-band"]),
3373 "replace-in-formulas" => Some(&["write", "formulas", "replace"]),
3374 "transform-batch" => Some(&["write", "batch", "transform"]),
3375 "style-batch" => Some(&["write", "batch", "style"]),
3376 "apply-formula-pattern" => Some(&["write", "batch", "formula-pattern"]),
3377 "structure-batch" => Some(&["write", "batch", "structure"]),
3378 "column-size-batch" => Some(&["write", "batch", "column-size"]),
3379 "sheet-layout-batch" => Some(&["write", "batch", "sheet-layout"]),
3380 "rules-batch" => Some(&["write", "batch", "rules"]),
3381 "define-name" => Some(&["write", "name", "define"]),
3382 "update-name" => Some(&["write", "name", "update"]),
3383 "delete-name" => Some(&["write", "name", "delete"]),
3384 "create-workbook" => Some(&["workbook", "create"]),
3385 "copy" => Some(&["workbook", "copy"]),
3386 "recalculate" => Some(&["workbook", "recalculate"]),
3387 "verify" => Some(&["verify", "proof"]),
3388 "diff" => Some(&["verify", "diff"]),
3389 "run-manifest" => Some(&["sheetport", "run"]),
3390 _ => None,
3391 }
3392}
3393
3394fn legacy_discoverability_tokens(target: &str) -> Option<&'static [&'static str]> {
3395 match target {
3396 "transform-batch" => Some(&["write", "batch", "transform"]),
3397 "style-batch" => Some(&["write", "batch", "style"]),
3398 "apply-formula-pattern" => Some(&["write", "batch", "formula-pattern"]),
3399 "structure-batch" => Some(&["write", "batch", "structure"]),
3400 "column-size-batch" => Some(&["write", "batch", "column-size"]),
3401 "sheet-layout-batch" => Some(&["write", "batch", "sheet-layout"]),
3402 "rules-batch" => Some(&["write", "batch", "rules"]),
3403 _ => None,
3404 }
3405}
3406
3407fn canonical_leaf_path_to_flat(tokens: &[String]) -> Option<&'static str> {
3408 match tokens {
3409 [a, b] if a == "read" && b == "sheets" => Some("list-sheets"),
3410 [a, b] if a == "read" && b == "overview" => Some("sheet-overview"),
3411 [a, b] if a == "read" && b == "values" => Some("range-values"),
3412 [a, b] if a == "read" && b == "export" => Some("range-export"),
3413 [a, b] if a == "read" && b == "cells" => Some("inspect-cells"),
3414 [a, b] if a == "read" && b == "page" => Some("sheet-page"),
3415 [a, b] if a == "read" && b == "table" => Some("read-table"),
3416 [a, b] if a == "read" && b == "names" => Some("named-ranges"),
3417 [a, b] if a == "read" && b == "workbook" => Some("describe"),
3418 [a, b] if a == "read" && b == "layout" => Some("layout-page"),
3419 [a, b] if a == "analyze" && b == "find-value" => Some("find-value"),
3420 [a, b] if a == "analyze" && b == "find-formula" => Some("find-formula"),
3421 [a, b] if a == "analyze" && b == "formula-map" => Some("formula-map"),
3422 [a, b] if a == "analyze" && b == "formula-trace" => Some("formula-trace"),
3423 [a, b] if a == "analyze" && b == "scan-volatiles" => Some("scan-volatiles"),
3424 [a, b] if a == "analyze" && b == "sheet-statistics" => Some("sheet-statistics"),
3425 [a, b] if a == "analyze" && b == "table-profile" => Some("table-profile"),
3426 [a, b] if a == "analyze" && b == "ref-impact" => Some("check-ref-impact"),
3427 [a, b] if a == "write" && b == "cells" => Some("edit"),
3428 [a, b] if a == "write" && b == "import" => Some("range-import"),
3429 [a, b] if a == "write" && b == "append" => Some("append-region"),
3430 [a, b] if a == "write" && b == "clone-template-row" => Some("clone-template-row"),
3431 [a, b] if a == "write" && b == "clone-row-band" => Some("clone-row-band"),
3432 [a, b] if a == "workbook" && b == "create" => Some("create-workbook"),
3433 [a, b] if a == "workbook" && b == "copy" => Some("copy"),
3434 [a, b] if a == "workbook" && b == "recalculate" => Some("recalculate"),
3435 [a, b] if a == "verify" && b == "proof" => Some("verify"),
3436 [a, b] if a == "verify" && b == "diff" => Some("diff"),
3437 [a, b, c] if a == "write" && b == "formulas" && c == "replace" => {
3438 Some("replace-in-formulas")
3439 }
3440 [a, b, c] if a == "write" && b == "name" && c == "define" => Some("define-name"),
3441 [a, b, c] if a == "write" && b == "name" && c == "update" => Some("update-name"),
3442 [a, b, c] if a == "write" && b == "name" && c == "delete" => Some("delete-name"),
3443 [a, b, c] if a == "write" && b == "batch" && c == "transform" => Some("transform-batch"),
3444 [a, b, c] if a == "write" && b == "batch" && c == "style" => Some("style-batch"),
3445 [a, b, c] if a == "write" && b == "batch" && c == "formula-pattern" => {
3446 Some("apply-formula-pattern")
3447 }
3448 [a, b, c] if a == "write" && b == "batch" && c == "structure" => Some("structure-batch"),
3449 [a, b, c] if a == "write" && b == "batch" && c == "column-size" => {
3450 Some("column-size-batch")
3451 }
3452 [a, b, c] if a == "write" && b == "batch" && c == "sheet-layout" => {
3453 Some("sheet-layout-batch")
3454 }
3455 [a, b, c] if a == "write" && b == "batch" && c == "rules" => Some("rules-batch"),
3456 _ => None,
3457 }
3458}
3459
3460fn rewrite_flat_surface_text(text: &str) -> String {
3461 let mut rewritten = text.replace("agent-spreadsheet", "asp");
3462
3463 let replacements = [
3464 ("session-op", "session op"),
3465 (
3466 "asp schema transform-batch",
3467 "asp schema write batch transform",
3468 ),
3469 ("asp schema style-batch", "asp schema write batch style"),
3470 (
3471 "asp schema apply-formula-pattern",
3472 "asp schema write batch formula-pattern",
3473 ),
3474 (
3475 "asp schema structure-batch",
3476 "asp schema write batch structure",
3477 ),
3478 (
3479 "asp schema column-size-batch",
3480 "asp schema write batch column-size",
3481 ),
3482 (
3483 "asp schema sheet-layout-batch",
3484 "asp schema write batch sheet-layout",
3485 ),
3486 ("asp schema rules-batch", "asp schema write batch rules"),
3487 (
3488 "asp example transform-batch",
3489 "asp example write batch transform",
3490 ),
3491 ("asp example style-batch", "asp example write batch style"),
3492 (
3493 "asp example apply-formula-pattern",
3494 "asp example write batch formula-pattern",
3495 ),
3496 (
3497 "asp example structure-batch",
3498 "asp example write batch structure",
3499 ),
3500 (
3501 "asp example column-size-batch",
3502 "asp example write batch column-size",
3503 ),
3504 (
3505 "asp example sheet-layout-batch",
3506 "asp example write batch sheet-layout",
3507 ),
3508 ("asp example rules-batch", "asp example write batch rules"),
3509 ];
3510 for (from, to) in replacements {
3511 rewritten = rewritten.replace(from, to);
3512 rewritten = rewritten.replace(&format!("`{from}`"), &format!("`{to}`"));
3513 }
3514
3515 let flat_commands = [
3516 "list-sheets",
3517 "sheet-overview",
3518 "range-values",
3519 "range-export",
3520 "inspect-cells",
3521 "sheet-page",
3522 "read-table",
3523 "named-ranges",
3524 "describe",
3525 "layout-page",
3526 "find-value",
3527 "find-formula",
3528 "formula-map",
3529 "formula-trace",
3530 "scan-volatiles",
3531 "sheet-statistics",
3532 "table-profile",
3533 "check-ref-impact",
3534 "edit",
3535 "range-import",
3536 "append-region",
3537 "clone-template-row",
3538 "clone-row-band",
3539 "replace-in-formulas",
3540 "transform-batch",
3541 "style-batch",
3542 "apply-formula-pattern",
3543 "structure-batch",
3544 "column-size-batch",
3545 "sheet-layout-batch",
3546 "rules-batch",
3547 "define-name",
3548 "update-name",
3549 "delete-name",
3550 "create-workbook",
3551 "copy",
3552 "recalculate",
3553 "verify",
3554 "diff",
3555 "run-manifest",
3556 ];
3557 for flat in flat_commands {
3558 if let Some(canonical) = flat_to_canonical_command(flat) {
3559 rewritten = rewritten.replace(&format!("asp {flat}"), &format!("asp {canonical}"));
3560 rewritten = rewritten.replace(&format!("`{flat}`"), &format!("`{canonical}`"));
3561 }
3562 }
3563
3564 rewritten
3565}
3566
3567fn emit_rewritten_clap_error_and_exit(error: clap::Error) -> ! {
3568 let kind = error.kind();
3569 let code = error.exit_code();
3570 let rendered = rewrite_flat_surface_text(&error.to_string());
3571 match kind {
3572 clap::error::ErrorKind::DisplayHelp | clap::error::ErrorKind::DisplayVersion => {
3573 print!("{rendered}");
3574 std::process::exit(0);
3575 }
3576 _ => {
3577 eprint!("{rendered}");
3578 std::process::exit(code);
3579 }
3580 }
3581}
3582
3583fn normalize_legacy_command_argv(argv: Vec<OsString>) -> (Vec<OsString>, Vec<String>) {
3584 if argv.len() <= 1 {
3585 return (argv, Vec::new());
3586 }
3587
3588 let Some(index) = first_subcommand_index(&argv) else {
3589 return (argv, Vec::new());
3590 };
3591
3592 let token = argv[index].to_string_lossy().into_owned();
3593 let mut warnings = Vec::new();
3594
3595 if let Some(path) = flat_to_nested_tokens(&token) {
3596 let next_token = argv
3597 .get(index + 1)
3598 .map(|value| value.to_string_lossy().into_owned());
3599 let conflicts_with_canonical_group =
3600 token == "verify" && matches!(next_token.as_deref(), Some("proof") | Some("diff"));
3601
3602 if !conflicts_with_canonical_group {
3603 let mut normalized = Vec::with_capacity(argv.len() + path.len());
3604 normalized.extend_from_slice(&argv[..index]);
3605 for part in path {
3606 normalized.push(OsString::from(part));
3607 }
3608 normalized.extend_from_slice(&argv[index + 1..]);
3609 warnings.push(format!(
3610 "warning: '{}' is deprecated; use '{}'",
3611 token,
3612 flat_to_canonical_command(&token).unwrap_or_default()
3613 ));
3614 return (normalized, warnings);
3615 }
3616 }
3617
3618 if (token == "schema" || token == "example") && index + 1 < argv.len() {
3619 let target = argv[index + 1].to_string_lossy().into_owned();
3620 if let Some(path) = legacy_discoverability_tokens(&target) {
3621 let mut normalized = Vec::with_capacity(argv.len() + path.len());
3622 normalized.extend_from_slice(&argv[..=index]);
3623 for part in path {
3624 normalized.push(OsString::from(part));
3625 }
3626 normalized.extend_from_slice(&argv[index + 2..]);
3627 warnings.push(format!(
3628 "warning: '{} {}' is deprecated; use '{} {}'",
3629 token,
3630 target,
3631 token,
3632 path.join(" ")
3633 ));
3634 return (normalized, warnings);
3635 }
3636
3637 if target == "session-op" {
3638 let mut normalized = Vec::with_capacity(argv.len() + 1);
3639 normalized.extend_from_slice(&argv[..=index]);
3640 normalized.push(OsString::from("session"));
3641 normalized.push(OsString::from("op"));
3642 normalized.extend_from_slice(&argv[index + 2..]);
3643 warnings.push(format!(
3644 "warning: '{} session-op' is deprecated; use '{} session op'",
3645 token, token
3646 ));
3647 return (normalized, warnings);
3648 }
3649 }
3650
3651 (argv, warnings)
3652}
3653
3654fn maybe_emit_forwarded_leaf_help(argv: &[OsString]) {
3655 let Some(last) = argv.last() else {
3656 return;
3657 };
3658 let last = last.to_string_lossy();
3659 if last.as_ref() != "--help" && last.as_ref() != "-h" {
3660 return;
3661 }
3662
3663 let argv_without_help = &argv[..argv.len() - 1];
3664 let Some(index) = first_subcommand_index(argv_without_help) else {
3665 return;
3666 };
3667
3668 let mut tokens = Vec::new();
3669 for arg in &argv_without_help[index..] {
3670 let token = arg.to_string_lossy();
3671 if token.starts_with('-') {
3672 break;
3673 }
3674 tokens.push(token.into_owned());
3675 }
3676
3677 if let Some(flat) = canonical_leaf_path_to_flat(&tokens) {
3678 let translated = vec![
3679 OsString::from("asp"),
3680 OsString::from(flat),
3681 OsString::from("--help"),
3682 ];
3683 match Cli::try_parse_from(translated) {
3684 Ok(_) => unreachable!("help parse should not succeed"),
3685 Err(error) => emit_rewritten_clap_error_and_exit(error),
3686 }
3687 }
3688}
3689
3690fn parse_flat_command_from_surface(
3691 flat_command: &'static str,
3692 args: Vec<OsString>,
3693) -> Result<Commands, clap::Error> {
3694 let mut argv = vec![OsString::from("asp"), OsString::from(flat_command)];
3695 argv.extend(args);
3696 Cli::try_parse_from(argv).map(|cli| cli.command)
3697}
3698
3699fn resolve_surface_discoverability(
3700 command: SurfaceDiscoverabilityCommands,
3701) -> DiscoverabilityCommands {
3702 match command {
3703 SurfaceDiscoverabilityCommands::Write(command) => match command {
3704 SurfaceDiscoverabilityWriteCommands::Batch(command) => match command {
3705 SurfaceDiscoverabilityBatchCommands::Transform => {
3706 DiscoverabilityCommands::TransformBatch
3707 }
3708 SurfaceDiscoverabilityBatchCommands::Style => DiscoverabilityCommands::StyleBatch,
3709 SurfaceDiscoverabilityBatchCommands::FormulaPattern => {
3710 DiscoverabilityCommands::ApplyFormulaPattern
3711 }
3712 SurfaceDiscoverabilityBatchCommands::Structure => {
3713 DiscoverabilityCommands::StructureBatch
3714 }
3715 SurfaceDiscoverabilityBatchCommands::ColumnSize => {
3716 DiscoverabilityCommands::ColumnSizeBatch
3717 }
3718 SurfaceDiscoverabilityBatchCommands::SheetLayout => {
3719 DiscoverabilityCommands::SheetLayoutBatch
3720 }
3721 SurfaceDiscoverabilityBatchCommands::Rules => DiscoverabilityCommands::RulesBatch,
3722 },
3723 },
3724 SurfaceDiscoverabilityCommands::Session(command) => match command {
3725 SurfaceDiscoverabilitySessionCommands::Op { kind } => {
3726 DiscoverabilityCommands::SessionOp { kind }
3727 }
3728 },
3729 }
3730}
3731
3732fn resolve_surface_command(
3733 command: SurfaceCommands,
3734) -> Result<ResolvedSurfaceCommand, clap::Error> {
3735 match command {
3736 SurfaceCommands::Read(command) => match command {
3737 SurfaceReadCommands::Sheets(args) => {
3738 parse_flat_command_from_surface("list-sheets", args.args)
3739 .map(ResolvedSurfaceCommand::Command)
3740 }
3741 SurfaceReadCommands::Overview(args) => {
3742 parse_flat_command_from_surface("sheet-overview", args.args)
3743 .map(ResolvedSurfaceCommand::Command)
3744 }
3745 SurfaceReadCommands::Values(args) => {
3746 parse_flat_command_from_surface("range-values", args.args)
3747 .map(ResolvedSurfaceCommand::Command)
3748 }
3749 SurfaceReadCommands::Export(args) => {
3750 parse_flat_command_from_surface("range-export", args.args)
3751 .map(ResolvedSurfaceCommand::Command)
3752 }
3753 SurfaceReadCommands::Cells(args) => {
3754 parse_flat_command_from_surface("inspect-cells", args.args)
3755 .map(ResolvedSurfaceCommand::Command)
3756 }
3757 SurfaceReadCommands::Page(args) => {
3758 parse_flat_command_from_surface("sheet-page", args.args)
3759 .map(ResolvedSurfaceCommand::Command)
3760 }
3761 SurfaceReadCommands::Table(args) => {
3762 parse_flat_command_from_surface("read-table", args.args)
3763 .map(ResolvedSurfaceCommand::Command)
3764 }
3765 SurfaceReadCommands::Names(args) => {
3766 parse_flat_command_from_surface("named-ranges", args.args)
3767 .map(ResolvedSurfaceCommand::Command)
3768 }
3769 SurfaceReadCommands::Workbook(args) => {
3770 parse_flat_command_from_surface("describe", args.args)
3771 .map(ResolvedSurfaceCommand::Command)
3772 }
3773 SurfaceReadCommands::Layout(args) => {
3774 parse_flat_command_from_surface("layout-page", args.args)
3775 .map(ResolvedSurfaceCommand::Command)
3776 }
3777 },
3778 SurfaceCommands::Analyze(command) => match command {
3779 SurfaceAnalyzeCommands::FindValue(args) => {
3780 parse_flat_command_from_surface("find-value", args.args)
3781 .map(ResolvedSurfaceCommand::Command)
3782 }
3783 SurfaceAnalyzeCommands::FindFormula(args) => {
3784 parse_flat_command_from_surface("find-formula", args.args)
3785 .map(ResolvedSurfaceCommand::Command)
3786 }
3787 SurfaceAnalyzeCommands::FormulaMap(args) => {
3788 parse_flat_command_from_surface("formula-map", args.args)
3789 .map(ResolvedSurfaceCommand::Command)
3790 }
3791 SurfaceAnalyzeCommands::FormulaTrace(args) => {
3792 parse_flat_command_from_surface("formula-trace", args.args)
3793 .map(ResolvedSurfaceCommand::Command)
3794 }
3795 SurfaceAnalyzeCommands::ScanVolatiles(args) => {
3796 parse_flat_command_from_surface("scan-volatiles", args.args)
3797 .map(ResolvedSurfaceCommand::Command)
3798 }
3799 SurfaceAnalyzeCommands::SheetStatistics(args) => {
3800 parse_flat_command_from_surface("sheet-statistics", args.args)
3801 .map(ResolvedSurfaceCommand::Command)
3802 }
3803 SurfaceAnalyzeCommands::TableProfile(args) => {
3804 parse_flat_command_from_surface("table-profile", args.args)
3805 .map(ResolvedSurfaceCommand::Command)
3806 }
3807 SurfaceAnalyzeCommands::RefImpact(args) => {
3808 parse_flat_command_from_surface("check-ref-impact", args.args)
3809 .map(ResolvedSurfaceCommand::Command)
3810 }
3811 },
3812 SurfaceCommands::Write(command) => match command {
3813 SurfaceWriteCommands::Cells(args) => parse_flat_command_from_surface("edit", args.args)
3814 .map(ResolvedSurfaceCommand::Command),
3815 SurfaceWriteCommands::Import(args) => {
3816 parse_flat_command_from_surface("range-import", args.args)
3817 .map(ResolvedSurfaceCommand::Command)
3818 }
3819 SurfaceWriteCommands::Append(args) => {
3820 parse_flat_command_from_surface("append-region", args.args)
3821 .map(ResolvedSurfaceCommand::Command)
3822 }
3823 SurfaceWriteCommands::CloneTemplateRow(args) => {
3824 parse_flat_command_from_surface("clone-template-row", args.args)
3825 .map(ResolvedSurfaceCommand::Command)
3826 }
3827 SurfaceWriteCommands::CloneRowBand(args) => {
3828 parse_flat_command_from_surface("clone-row-band", args.args)
3829 .map(ResolvedSurfaceCommand::Command)
3830 }
3831 SurfaceWriteCommands::Formulas(command) => match command {
3832 SurfaceWriteFormulaCommands::Replace(args) => {
3833 parse_flat_command_from_surface("replace-in-formulas", args.args)
3834 .map(ResolvedSurfaceCommand::Command)
3835 }
3836 },
3837 SurfaceWriteCommands::Name(command) => match command {
3838 SurfaceWriteNameCommands::Define(args) => {
3839 parse_flat_command_from_surface("define-name", args.args)
3840 .map(ResolvedSurfaceCommand::Command)
3841 }
3842 SurfaceWriteNameCommands::Update(args) => {
3843 parse_flat_command_from_surface("update-name", args.args)
3844 .map(ResolvedSurfaceCommand::Command)
3845 }
3846 SurfaceWriteNameCommands::Delete(args) => {
3847 parse_flat_command_from_surface("delete-name", args.args)
3848 .map(ResolvedSurfaceCommand::Command)
3849 }
3850 },
3851 SurfaceWriteCommands::Batch(command) => match command {
3852 SurfaceWriteBatchCommands::Transform(args) => {
3853 parse_flat_command_from_surface("transform-batch", args.args)
3854 .map(ResolvedSurfaceCommand::Command)
3855 }
3856 SurfaceWriteBatchCommands::Style(args) => {
3857 parse_flat_command_from_surface("style-batch", args.args)
3858 .map(ResolvedSurfaceCommand::Command)
3859 }
3860 SurfaceWriteBatchCommands::FormulaPattern(args) => {
3861 parse_flat_command_from_surface("apply-formula-pattern", args.args)
3862 .map(ResolvedSurfaceCommand::Command)
3863 }
3864 SurfaceWriteBatchCommands::Structure(args) => {
3865 parse_flat_command_from_surface("structure-batch", args.args)
3866 .map(ResolvedSurfaceCommand::Command)
3867 }
3868 SurfaceWriteBatchCommands::ColumnSize(args) => {
3869 parse_flat_command_from_surface("column-size-batch", args.args)
3870 .map(ResolvedSurfaceCommand::Command)
3871 }
3872 SurfaceWriteBatchCommands::SheetLayout(args) => {
3873 parse_flat_command_from_surface("sheet-layout-batch", args.args)
3874 .map(ResolvedSurfaceCommand::Command)
3875 }
3876 SurfaceWriteBatchCommands::Rules(args) => {
3877 parse_flat_command_from_surface("rules-batch", args.args)
3878 .map(ResolvedSurfaceCommand::Command)
3879 }
3880 },
3881 },
3882 SurfaceCommands::Workbook(command) => match command {
3883 SurfaceWorkbookCommands::Create(args) => {
3884 parse_flat_command_from_surface("create-workbook", args.args)
3885 .map(ResolvedSurfaceCommand::Command)
3886 }
3887 SurfaceWorkbookCommands::Copy(args) => {
3888 parse_flat_command_from_surface("copy", args.args)
3889 .map(ResolvedSurfaceCommand::Command)
3890 }
3891 SurfaceWorkbookCommands::Recalculate(args) => {
3892 parse_flat_command_from_surface("recalculate", args.args)
3893 .map(ResolvedSurfaceCommand::Command)
3894 }
3895 },
3896 SurfaceCommands::Verify(command) => match command {
3897 SurfaceVerifyCommands::Proof(args) => {
3898 parse_flat_command_from_surface("verify", args.args)
3899 .map(ResolvedSurfaceCommand::Command)
3900 }
3901 SurfaceVerifyCommands::Diff(args) => parse_flat_command_from_surface("diff", args.args)
3902 .map(ResolvedSurfaceCommand::Command),
3903 },
3904 SurfaceCommands::Schema { command } => Ok(ResolvedSurfaceCommand::Schema(
3905 resolve_surface_discoverability(command),
3906 )),
3907 SurfaceCommands::Example { command } => Ok(ResolvedSurfaceCommand::Example(
3908 resolve_surface_discoverability(command),
3909 )),
3910 SurfaceCommands::Session(command) => {
3911 Ok(ResolvedSurfaceCommand::Command(Commands::Session(command)))
3912 }
3913 SurfaceCommands::Sheetport { command } => {
3914 Ok(ResolvedSurfaceCommand::Command(Commands::Sheetport {
3915 command,
3916 }))
3917 }
3918 }
3919}
3920
3921pub async fn run() -> Result<()> {
3922 let argv = normalize_legacy_global_format_argv(std::env::args_os().collect());
3923 let (argv, warnings) = normalize_legacy_command_argv(argv);
3924 maybe_emit_forwarded_leaf_help(&argv);
3925
3926 let surface = match SurfaceCli::try_parse_from(argv) {
3927 Ok(cli) => cli,
3928 Err(error) => error.exit(),
3929 };
3930
3931 let result = match resolve_surface_command(surface.command) {
3932 Ok(ResolvedSurfaceCommand::Command(command)) => {
3933 run_with_options(
3934 command,
3935 surface.output_format,
3936 surface.shape,
3937 surface.compact,
3938 surface.quiet,
3939 )
3940 .await
3941 }
3942 Ok(ResolvedSurfaceCommand::Schema(command)) => match run_schema_command(command) {
3943 Ok(payload) => {
3944 if let Err(error) = output::emit_value(
3945 &payload,
3946 surface.output_format,
3947 surface.shape,
3948 output::CompactProjectionTarget::None,
3949 surface.compact,
3950 surface.quiet,
3951 ) {
3952 emit_error_and_exit(error);
3953 }
3954 Ok(())
3955 }
3956 Err(error) => emit_error_and_exit(error),
3957 },
3958 Ok(ResolvedSurfaceCommand::Example(command)) => match run_example_command(command) {
3959 Ok(payload) => {
3960 if let Err(error) = output::emit_value(
3961 &payload,
3962 surface.output_format,
3963 surface.shape,
3964 output::CompactProjectionTarget::None,
3965 surface.compact,
3966 surface.quiet,
3967 ) {
3968 emit_error_and_exit(error);
3969 }
3970 Ok(())
3971 }
3972 Err(error) => emit_error_and_exit(error),
3973 },
3974 Err(error) => emit_rewritten_clap_error_and_exit(error),
3975 };
3976
3977 if result.is_ok() && !surface.quiet {
3978 for warning in &warnings {
3979 eprintln!("{warning}");
3980 }
3981 }
3982
3983 result
3984}
3985
3986pub async fn run_with_options(
3987 command: Commands,
3988 format: OutputFormat,
3989 shape: OutputShape,
3990 compact: bool,
3991 quiet: bool,
3992) -> Result<()> {
3993 if let Err(error) = errors::ensure_output_supported(format) {
3994 emit_error_and_exit(error);
3995 }
3996
3997 let projection_target = compact_projection_target_for_command(&command);
3998 let emit_layout_ascii_direct = matches!(
3999 &command,
4000 Commands::LayoutPage {
4001 render: Some(LayoutRenderArg::Ascii),
4002 ..
4003 }
4004 );
4005
4006 match run_command(command).await {
4007 Ok(payload) => {
4008 if emit_layout_ascii_direct {
4009 if let Some(ascii) = payload.get("ascii_render").and_then(|v| v.as_str()) {
4010 print!("{ascii}");
4011 if !ascii.ends_with('\n') {
4012 println!();
4013 }
4014 return Ok(());
4015 }
4016 emit_error_and_exit(anyhow::anyhow!(
4017 "layout-page --render ascii expected ascii_render in response"
4018 ));
4019 }
4020
4021 if let Err(error) =
4022 output::emit_value(&payload, format, shape, projection_target, compact, quiet)
4023 {
4024 emit_error_and_exit(error);
4025 }
4026 Ok(())
4027 }
4028 Err(error) => emit_error_and_exit(error),
4029 }
4030}
4031
4032fn compact_projection_target_for_command(command: &Commands) -> output::CompactProjectionTarget {
4033 match command {
4034 Commands::RangeValues { .. } => output::CompactProjectionTarget::RangeValues,
4035 Commands::ReadTable { .. } => output::CompactProjectionTarget::ReadTable,
4036 Commands::SheetPage { .. } => output::CompactProjectionTarget::SheetPage,
4037 Commands::FormulaTrace { .. } => output::CompactProjectionTarget::FormulaTrace,
4038 _ => output::CompactProjectionTarget::None,
4039 }
4040}
4041
4042fn emit_error_and_exit(error: anyhow::Error) -> ! {
4043 let envelope = errors::envelope_for(&error);
4044 let stderr = std::io::stderr();
4045 let mut handle = stderr.lock();
4046 if serde_json::to_writer(&mut handle, &envelope).is_err() {
4047 eprintln!("{{\"code\":\"COMMAND_FAILED\",\"message\":\"{}\"}}", error);
4048 } else {
4049 use std::io::Write;
4050 let _ = handle.write_all(b"\n");
4051 }
4052 std::process::exit(1)
4053}
4054
4055#[cfg(test)]
4056mod tests {
4057 use super::*;
4058
4059 #[test]
4060 fn parses_global_flags_and_read_table() {
4061 let cli = Cli::try_parse_from([
4062 "agent-spreadsheet",
4063 "--output-format",
4064 "json",
4065 "--shape",
4066 "compact",
4067 "--compact",
4068 "--quiet",
4069 "read-table",
4070 "workbook.xlsx",
4071 "--sheet",
4072 "Sheet1",
4073 "--range",
4074 "A1:B10",
4075 "--table-name",
4076 "SalesTable",
4077 "--region-id",
4078 "7",
4079 "--limit",
4080 "10",
4081 "--offset",
4082 "2",
4083 "--sample-mode",
4084 "first",
4085 "--filters-json",
4086 r#"[{"column":"Name","op":"eq","value":"Alice"}]"#,
4087 "--table-format",
4088 "values",
4089 ])
4090 .expect("parse command");
4091
4092 assert!(matches!(cli.shape, OutputShape::Compact));
4093 assert!(cli.compact);
4094 assert!(cli.quiet);
4095 match cli.command {
4096 Commands::ReadTable {
4097 file,
4098 sheet,
4099 range,
4100 table_name,
4101 region_id,
4102 limit,
4103 offset,
4104 sample_mode,
4105 filters_json,
4106 filters_file,
4107 table_format,
4108 ..
4109 } => {
4110 assert_eq!(file, PathBuf::from("workbook.xlsx"));
4111 assert_eq!(sheet.as_deref(), Some("Sheet1"));
4112 assert_eq!(range.as_deref(), Some("A1:B10"));
4113 assert_eq!(table_name.as_deref(), Some("SalesTable"));
4114 assert_eq!(region_id, Some(7));
4115 assert_eq!(limit, Some(10));
4116 assert_eq!(offset, Some(2));
4117 assert!(matches!(sample_mode, Some(TableSampleModeArg::First)));
4118 assert_eq!(
4119 filters_json.as_deref(),
4120 Some(r#"[{"column":"Name","op":"eq","value":"Alice"}]"#)
4121 );
4122 assert!(filters_file.is_none());
4123 assert!(matches!(table_format, Some(TableReadFormat::Values)));
4124 }
4125 other => panic!("unexpected command: {other:?}"),
4126 }
4127 }
4128
4129 #[test]
4130 fn parses_formula_trace_direction() {
4131 let cli = Cli::try_parse_from([
4132 "agent-spreadsheet",
4133 "formula-trace",
4134 "workbook.xlsx",
4135 "Sheet1",
4136 "C3",
4137 "dependents",
4138 "--depth",
4139 "2",
4140 "--page-size",
4141 "15",
4142 "--cursor-depth",
4143 "2",
4144 "--cursor-offset",
4145 "5",
4146 ])
4147 .expect("parse command");
4148
4149 assert!(matches!(cli.shape, OutputShape::Canonical));
4150
4151 match cli.command {
4152 Commands::FormulaTrace {
4153 direction,
4154 cell,
4155 sheet,
4156 depth,
4157 page_size,
4158 cursor_depth,
4159 cursor_offset,
4160 ..
4161 } => {
4162 assert_eq!(cell, "C3");
4163 assert_eq!(sheet, "Sheet1");
4164 assert_eq!(depth, Some(2));
4165 assert_eq!(page_size, Some(15));
4166 assert_eq!(cursor_depth, Some(2));
4167 assert_eq!(cursor_offset, Some(5));
4168 assert!(matches!(direction, TraceDirectionArg::Dependents));
4169 }
4170 other => panic!("unexpected command: {other:?}"),
4171 }
4172 }
4173
4174 #[test]
4175 fn parses_range_values_include_formulas_flag() {
4176 let cli = Cli::try_parse_from([
4177 "agent-spreadsheet",
4178 "range-values",
4179 "workbook.xlsx",
4180 "Sheet1",
4181 "A1:C10",
4182 "--include-formulas",
4183 ])
4184 .expect("parse command");
4185
4186 match cli.command {
4187 Commands::RangeValues {
4188 file,
4189 sheet,
4190 ranges,
4191 format,
4192 include_formulas,
4193 ..
4194 } => {
4195 assert_eq!(file, PathBuf::from("workbook.xlsx"));
4196 assert_eq!(sheet, "Sheet1");
4197 assert_eq!(ranges, vec!["A1:C10".to_string()]);
4198 assert!(format.is_none());
4199 assert_eq!(include_formulas, Some(true));
4200 }
4201 other => panic!("unexpected command: {other:?}"),
4202 }
4203 }
4204
4205 #[test]
4206 fn parses_range_values_format_argument() {
4207 let cli = Cli::try_parse_from([
4208 "agent-spreadsheet",
4209 "range-values",
4210 "workbook.xlsx",
4211 "Sheet1",
4212 "A1:C10",
4213 "--format",
4214 "json",
4215 ])
4216 .expect("parse command");
4217
4218 match cli.command {
4219 Commands::RangeValues { format, .. } => {
4220 assert!(matches!(format, Some(RangeValuesFormatArg::Json)));
4221 }
4222 other => panic!("unexpected command: {other:?}"),
4223 }
4224 }
4225
4226 #[test]
4227 fn parses_diff_arguments_with_paging_and_filters() {
4228 let cli = Cli::try_parse_from([
4229 "agent-spreadsheet",
4230 "diff",
4231 "baseline.xlsx",
4232 "candidate.xlsx",
4233 "--sheet",
4234 "Sheet1",
4235 "--range",
4236 "A1:C20",
4237 "--details",
4238 "--limit",
4239 "150",
4240 "--offset",
4241 "300",
4242 ])
4243 .expect("parse diff command");
4244
4245 match cli.command {
4246 Commands::Diff {
4247 original,
4248 modified,
4249 sheet,
4250 sheets,
4251 range,
4252 details,
4253 limit,
4254 offset,
4255 exclude_recalc_result,
4256 } => {
4257 assert_eq!(original, PathBuf::from("baseline.xlsx"));
4258 assert_eq!(modified, PathBuf::from("candidate.xlsx"));
4259 assert_eq!(sheet.as_deref(), Some("Sheet1"));
4260 assert!(sheets.is_none());
4261 assert_eq!(range.as_deref(), Some("A1:C20"));
4262 assert!(details);
4263 assert_eq!(limit, 150);
4264 assert_eq!(offset, 300);
4265 assert!(!exclude_recalc_result);
4266 }
4267 other => panic!("unexpected command: {other:?}"),
4268 }
4269 }
4270
4271 #[test]
4272 fn parses_diff_defaults_to_summary_only() {
4273 let cli = Cli::try_parse_from([
4274 "agent-spreadsheet",
4275 "diff",
4276 "baseline.xlsx",
4277 "candidate.xlsx",
4278 ])
4279 .expect("parse diff command defaults");
4280
4281 match cli.command {
4282 Commands::Diff {
4283 details,
4284 limit,
4285 offset,
4286 exclude_recalc_result,
4287 ..
4288 } => {
4289 assert!(!details);
4290 assert_eq!(limit, 200);
4291 assert_eq!(offset, 0);
4292 assert!(!exclude_recalc_result);
4293 }
4294 other => panic!("unexpected command: {other:?}"),
4295 }
4296 }
4297
4298 #[test]
4299 fn parses_diff_exclude_recalc_result_flag() {
4300 let cli = Cli::try_parse_from([
4301 "agent-spreadsheet",
4302 "diff",
4303 "baseline.xlsx",
4304 "candidate.xlsx",
4305 "--exclude-recalc-result",
4306 ])
4307 .expect("parse diff command with exclude recalc flag");
4308
4309 match cli.command {
4310 Commands::Diff {
4311 exclude_recalc_result,
4312 ..
4313 } => {
4314 assert!(exclude_recalc_result);
4315 }
4316 other => panic!("unexpected command: {other:?}"),
4317 }
4318 }
4319
4320 #[test]
4321 fn parses_range_import_arguments() {
4322 let cli = Cli::try_parse_from([
4323 "agent-spreadsheet",
4324 "range-import",
4325 "workbook.xlsx",
4326 "Sheet1",
4327 "--anchor",
4328 "B7",
4329 "--from-grid",
4330 "region.json",
4331 "--in-place",
4332 ])
4333 .expect("parse range-import");
4334
4335 match cli.command {
4336 Commands::RangeImport {
4337 file,
4338 sheet,
4339 anchor,
4340 from_grid,
4341 from_csv,
4342 header,
4343 clear_target,
4344 dry_run,
4345 in_place,
4346 output,
4347 force,
4348 } => {
4349 assert_eq!(file, PathBuf::from("workbook.xlsx"));
4350 assert_eq!(sheet, "Sheet1");
4351 assert_eq!(anchor, "B7");
4352 assert_eq!(from_grid.as_deref(), Some("region.json"));
4353 assert!(from_csv.is_none());
4354 assert!(!header);
4355 assert!(!clear_target);
4356 assert!(!dry_run);
4357 assert!(in_place);
4358 assert!(output.is_none());
4359 assert!(!force);
4360 }
4361 other => panic!("unexpected command: {other:?}"),
4362 }
4363 }
4364
4365 #[test]
4366 fn parses_range_import_from_csv_arguments() {
4367 let cli = Cli::try_parse_from([
4368 "agent-spreadsheet",
4369 "range-import",
4370 "workbook.xlsx",
4371 "Sheet1",
4372 "--anchor",
4373 "B7",
4374 "--from-csv",
4375 "data.csv",
4376 "--header",
4377 "--in-place",
4378 ])
4379 .expect("parse range-import csv");
4380
4381 match cli.command {
4382 Commands::RangeImport {
4383 from_grid,
4384 from_csv,
4385 header,
4386 ..
4387 } => {
4388 assert!(from_grid.is_none());
4389 assert_eq!(from_csv.as_deref(), Some("data.csv"));
4390 assert!(header);
4391 }
4392 other => panic!("unexpected command: {other:?}"),
4393 }
4394 }
4395
4396 #[test]
4397 fn parses_append_region_from_csv_arguments() {
4398 let cli = Cli::try_parse_from([
4399 "agent-spreadsheet",
4400 "append-region",
4401 "workbook.xlsx",
4402 "--sheet",
4403 "Sheet1",
4404 "--region-id",
4405 "7",
4406 "--from-csv",
4407 "rows.csv",
4408 "--header",
4409 "--dry-run",
4410 ])
4411 .expect("parse append-region csv");
4412
4413 match cli.command {
4414 Commands::AppendRegion {
4415 file,
4416 sheet_name,
4417 region_id,
4418 table_name,
4419 rows,
4420 from_csv,
4421 header,
4422 footer_policy,
4423 dry_run,
4424 ..
4425 } => {
4426 assert_eq!(file, PathBuf::from("workbook.xlsx"));
4427 assert_eq!(sheet_name, "Sheet1");
4428 assert_eq!(region_id, Some(7));
4429 assert!(table_name.is_none());
4430 assert!(rows.is_none());
4431 assert_eq!(from_csv.as_deref(), Some("rows.csv"));
4432 assert!(header);
4433 assert!(matches!(footer_policy, AppendRegionFooterPolicyArg::Auto));
4434 assert!(dry_run);
4435 }
4436 other => panic!("unexpected command: {other:?}"),
4437 }
4438 }
4439
4440 #[test]
4441 fn parses_append_region_table_target_with_footer_policy() {
4442 let cli = Cli::try_parse_from([
4443 "agent-spreadsheet",
4444 "append-region",
4445 "workbook.xlsx",
4446 "--sheet",
4447 "Sheet1",
4448 "--table-name",
4449 "SalesTable",
4450 "--rows",
4451 "@rows.json",
4452 "--footer-policy",
4453 "append-at-end",
4454 "--output",
4455 "updated.xlsx",
4456 ])
4457 .expect("parse append-region table target");
4458
4459 match cli.command {
4460 Commands::AppendRegion {
4461 region_id,
4462 table_name,
4463 rows,
4464 footer_policy,
4465 output,
4466 ..
4467 } => {
4468 assert!(region_id.is_none());
4469 assert_eq!(table_name.as_deref(), Some("SalesTable"));
4470 assert_eq!(rows.as_deref(), Some("@rows.json"));
4471 assert!(matches!(
4472 footer_policy,
4473 AppendRegionFooterPolicyArg::AppendAtEnd
4474 ));
4475 assert_eq!(output, Some(PathBuf::from("updated.xlsx")));
4476 }
4477 other => panic!("unexpected command: {other:?}"),
4478 }
4479 }
4480
4481 #[test]
4482 fn parses_clone_template_row_arguments() {
4483 let cli = Cli::try_parse_from([
4484 "agent-spreadsheet",
4485 "clone-template-row",
4486 "workbook.xlsx",
4487 "--sheet",
4488 "Sheet1",
4489 "--source-row",
4490 "12",
4491 "--after",
4492 "12",
4493 "--count",
4494 "2",
4495 "--expand-adjacent-sums",
4496 "--patch-targets",
4497 "all-non-formula",
4498 "--merge-policy",
4499 "strict",
4500 "--dry-run",
4501 ])
4502 .expect("parse clone-template-row");
4503
4504 match cli.command {
4505 Commands::CloneTemplateRow {
4506 file,
4507 sheet_name,
4508 source_row,
4509 before,
4510 after,
4511 insert_at,
4512 count,
4513 expand_adjacent_sums,
4514 patch_targets,
4515 merge_policy,
4516 dry_run,
4517 ..
4518 } => {
4519 assert_eq!(file, PathBuf::from("workbook.xlsx"));
4520 assert_eq!(sheet_name, "Sheet1");
4521 assert_eq!(source_row, 12);
4522 assert_eq!(before, None);
4523 assert_eq!(after, Some(12));
4524 assert_eq!(insert_at, None);
4525 assert_eq!(count, 2);
4526 assert!(expand_adjacent_sums);
4527 assert!(matches!(patch_targets, ClonePatchTargetsArg::AllNonFormula));
4528 assert!(matches!(merge_policy, CloneMergePolicyArg::Strict));
4529 assert!(dry_run);
4530 }
4531 other => panic!("unexpected command: {other:?}"),
4532 }
4533 }
4534
4535 #[test]
4536 fn parses_clone_row_band_arguments() {
4537 let cli = Cli::try_parse_from([
4538 "agent-spreadsheet",
4539 "clone-row-band",
4540 "workbook.xlsx",
4541 "--sheet",
4542 "Sheet1",
4543 "--source-rows",
4544 "12:14",
4545 "--before",
4546 "20",
4547 "--repeat",
4548 "2",
4549 "--patch-targets",
4550 "none",
4551 "--merge-policy",
4552 "safe",
4553 "--output",
4554 "updated.xlsx",
4555 ])
4556 .expect("parse clone-row-band");
4557
4558 match cli.command {
4559 Commands::CloneRowBand {
4560 file,
4561 sheet_name,
4562 source_rows,
4563 before,
4564 after,
4565 insert_at,
4566 repeat,
4567 patch_targets,
4568 merge_policy,
4569 output,
4570 ..
4571 } => {
4572 assert_eq!(file, PathBuf::from("workbook.xlsx"));
4573 assert_eq!(sheet_name, "Sheet1");
4574 assert_eq!(source_rows, "12:14");
4575 assert_eq!(before, Some(20));
4576 assert_eq!(after, None);
4577 assert_eq!(insert_at, None);
4578 assert_eq!(repeat, 2);
4579 assert!(matches!(patch_targets, ClonePatchTargetsArg::None));
4580 assert!(matches!(merge_policy, CloneMergePolicyArg::Safe));
4581 assert_eq!(output, Some(PathBuf::from("updated.xlsx")));
4582 }
4583 other => panic!("unexpected command: {other:?}"),
4584 }
4585 }
4586
4587 #[test]
4588 fn parses_inspect_cells_arguments() {
4589 let cli = Cli::try_parse_from([
4590 "agent-spreadsheet",
4591 "inspect-cells",
4592 "workbook.xlsx",
4593 "Sheet1",
4594 "A1:C10",
4595 "D4",
4596 "--include-empty",
4597 ])
4598 .expect("parse command");
4599
4600 match cli.command {
4601 Commands::InspectCells {
4602 file,
4603 sheet,
4604 targets,
4605 include_empty,
4606 budget,
4607 ..
4608 } => {
4609 assert_eq!(file, PathBuf::from("workbook.xlsx"));
4610 assert_eq!(sheet, "Sheet1");
4611 assert_eq!(targets, vec!["A1:C10", "D4"]);
4612 assert!(include_empty);
4613 assert_eq!(budget, None);
4614 }
4615 other => panic!("unexpected command: {other:?}"),
4616 }
4617 }
4618
4619 #[test]
4620 fn parses_sheet_page_arguments() {
4621 let cli = Cli::try_parse_from([
4622 "agent-spreadsheet",
4623 "sheet-page",
4624 "workbook.xlsx",
4625 "Sheet1",
4626 "--start-row",
4627 "2",
4628 "--page-size",
4629 "5",
4630 "--columns",
4631 "A,C:E",
4632 "--columns-by-header",
4633 "Name,Total",
4634 "--include-formulas",
4635 "--include-styles",
4636 "--include-header",
4637 "--format",
4638 "compact",
4639 ])
4640 .expect("parse command");
4641
4642 match cli.command {
4643 Commands::SheetPage {
4644 file,
4645 sheet,
4646 start_row,
4647 page_size,
4648 columns,
4649 columns_by_header,
4650 include_formulas,
4651 include_styles,
4652 include_header,
4653 format,
4654 ..
4655 } => {
4656 assert_eq!(file, PathBuf::from("workbook.xlsx"));
4657 assert_eq!(sheet, "Sheet1");
4658 assert_eq!(start_row, Some(2));
4659 assert_eq!(page_size, Some(5));
4660 assert_eq!(columns, Some(vec!["A".to_string(), "C:E".to_string()]));
4661 assert_eq!(
4662 columns_by_header,
4663 Some(vec!["Name".to_string(), "Total".to_string()])
4664 );
4665 assert_eq!(include_formulas, Some(true));
4666 assert_eq!(include_styles, Some(true));
4667 assert_eq!(include_header, Some(true));
4668 assert!(matches!(format, SheetPageFormatArg::Compact));
4669 }
4670 other => panic!("unexpected command: {other:?}"),
4671 }
4672 }
4673
4674 #[test]
4675 fn parses_create_workbook_arguments() {
4676 let cli = Cli::try_parse_from([
4677 "agent-spreadsheet",
4678 "create-workbook",
4679 "workbook.xlsx",
4680 "--sheets",
4681 "Inputs,Calc,Output",
4682 "--overwrite",
4683 ])
4684 .expect("parse create-workbook");
4685
4686 match cli.command {
4687 Commands::CreateWorkbook {
4688 path,
4689 sheets,
4690 overwrite,
4691 } => {
4692 assert_eq!(path, PathBuf::from("workbook.xlsx"));
4693 assert_eq!(
4694 sheets,
4695 Some(vec![
4696 "Inputs".to_string(),
4697 "Calc".to_string(),
4698 "Output".to_string(),
4699 ])
4700 );
4701 assert!(overwrite);
4702 }
4703 other => panic!("unexpected command: {other:?}"),
4704 }
4705 }
4706
4707 #[test]
4708 fn parses_transform_batch_arguments() {
4709 let cli = Cli::try_parse_from([
4710 "agent-spreadsheet",
4711 "transform-batch",
4712 "workbook.xlsx",
4713 "--ops",
4714 "@ops.json",
4715 "--output",
4716 "out.xlsx",
4717 "--force",
4718 ])
4719 .expect("parse transform-batch");
4720
4721 match cli.command {
4722 Commands::TransformBatch {
4723 file,
4724 ops,
4725 dry_run,
4726 in_place,
4727 output,
4728 force,
4729 print_schema,
4730 formula_parse_policy,
4731 } => {
4732 assert_eq!(file, Some(PathBuf::from("workbook.xlsx")));
4733 assert_eq!(ops, Some("@ops.json".to_string()));
4734 assert!(!dry_run);
4735 assert!(!in_place);
4736 assert_eq!(output, Some(PathBuf::from("out.xlsx")));
4737 assert!(force);
4738 assert!(!print_schema);
4739 assert_eq!(formula_parse_policy, None);
4740 }
4741 other => panic!("unexpected command: {other:?}"),
4742 }
4743 }
4744
4745 #[test]
4746 fn parses_style_batch_arguments() {
4747 let cli = Cli::try_parse_from([
4748 "agent-spreadsheet",
4749 "style-batch",
4750 "workbook.xlsx",
4751 "--ops",
4752 "@style.json",
4753 "--dry-run",
4754 ])
4755 .expect("parse style-batch");
4756
4757 match cli.command {
4758 Commands::StyleBatch {
4759 file,
4760 ops,
4761 dry_run,
4762 in_place,
4763 output,
4764 force,
4765 print_schema,
4766 } => {
4767 assert_eq!(file, Some(PathBuf::from("workbook.xlsx")));
4768 assert_eq!(ops, Some("@style.json".to_string()));
4769 assert!(dry_run);
4770 assert!(!in_place);
4771 assert!(output.is_none());
4772 assert!(!force);
4773 assert!(!print_schema);
4774 }
4775 other => panic!("unexpected command: {other:?}"),
4776 }
4777 }
4778
4779 #[test]
4780 fn parses_apply_formula_pattern_arguments() {
4781 let cli = Cli::try_parse_from([
4782 "agent-spreadsheet",
4783 "apply-formula-pattern",
4784 "workbook.xlsx",
4785 "--ops",
4786 "@formula.json",
4787 "--in-place",
4788 ])
4789 .expect("parse apply-formula-pattern");
4790
4791 match cli.command {
4792 Commands::ApplyFormulaPattern {
4793 file,
4794 ops,
4795 dry_run,
4796 in_place,
4797 output,
4798 force,
4799 print_schema,
4800 } => {
4801 assert_eq!(file, Some(PathBuf::from("workbook.xlsx")));
4802 assert_eq!(ops, Some("@formula.json".to_string()));
4803 assert!(!dry_run);
4804 assert!(in_place);
4805 assert!(output.is_none());
4806 assert!(!force);
4807 assert!(!print_schema);
4808 }
4809 other => panic!("unexpected command: {other:?}"),
4810 }
4811 }
4812
4813 #[test]
4814 fn parses_phase_b_batch_write_arguments() {
4815 let structure = Cli::try_parse_from([
4816 "agent-spreadsheet",
4817 "structure-batch",
4818 "workbook.xlsx",
4819 "--ops",
4820 "@structure.json",
4821 "--output",
4822 "out.xlsx",
4823 ])
4824 .expect("parse structure-batch");
4825 match structure.command {
4826 Commands::StructureBatch {
4827 file,
4828 ops,
4829 output,
4830 print_schema,
4831 ..
4832 } => {
4833 assert_eq!(file, Some(PathBuf::from("workbook.xlsx")));
4834 assert_eq!(ops, Some("@structure.json".to_string()));
4835 assert_eq!(output, Some(PathBuf::from("out.xlsx")));
4836 assert!(!print_schema);
4837 }
4838 other => panic!("unexpected command: {other:?}"),
4839 }
4840
4841 let column = Cli::try_parse_from([
4842 "agent-spreadsheet",
4843 "column-size-batch",
4844 "workbook.xlsx",
4845 "--ops",
4846 "@columns.json",
4847 "--in-place",
4848 ])
4849 .expect("parse column-size-batch");
4850 match column.command {
4851 Commands::ColumnSizeBatch {
4852 ops,
4853 in_place,
4854 print_schema,
4855 ..
4856 } => {
4857 assert_eq!(ops, Some("@columns.json".to_string()));
4858 assert!(in_place);
4859 assert!(!print_schema);
4860 }
4861 other => panic!("unexpected command: {other:?}"),
4862 }
4863
4864 let layout = Cli::try_parse_from([
4865 "agent-spreadsheet",
4866 "sheet-layout-batch",
4867 "workbook.xlsx",
4868 "--ops",
4869 "@layout.json",
4870 "--dry-run",
4871 ])
4872 .expect("parse sheet-layout-batch");
4873 match layout.command {
4874 Commands::SheetLayoutBatch {
4875 ops,
4876 dry_run,
4877 print_schema,
4878 ..
4879 } => {
4880 assert_eq!(ops, Some("@layout.json".to_string()));
4881 assert!(dry_run);
4882 assert!(!print_schema);
4883 }
4884 other => panic!("unexpected command: {other:?}"),
4885 }
4886 }
4887
4888 #[test]
4889 fn parses_rules_batch_arguments() {
4890 let cli = Cli::try_parse_from([
4891 "agent-spreadsheet",
4892 "rules-batch",
4893 "workbook.xlsx",
4894 "--ops",
4895 "@rules.json",
4896 "--output",
4897 "rules.xlsx",
4898 "--force",
4899 ])
4900 .expect("parse rules-batch");
4901
4902 match cli.command {
4903 Commands::RulesBatch {
4904 file,
4905 ops,
4906 dry_run,
4907 in_place,
4908 output,
4909 force,
4910 print_schema,
4911 formula_parse_policy,
4912 } => {
4913 assert_eq!(file, Some(PathBuf::from("workbook.xlsx")));
4914 assert_eq!(ops, Some("@rules.json".to_string()));
4915 assert!(!dry_run);
4916 assert!(!in_place);
4917 assert_eq!(output, Some(PathBuf::from("rules.xlsx")));
4918 assert!(force);
4919 assert!(!print_schema);
4920 assert!(formula_parse_policy.is_none());
4921 }
4922 other => panic!("unexpected command: {other:?}"),
4923 }
4924 }
4925
4926 #[test]
4927 fn parses_global_schema_and_example_commands() {
4928 let transform = Cli::try_parse_from(["asp", "schema", "transform-batch"])
4929 .expect("parse schema transform-batch");
4930 match transform.command {
4931 Commands::Schema {
4932 command: DiscoverabilityCommands::TransformBatch,
4933 } => {}
4934 other => panic!("unexpected command: {other:?}"),
4935 }
4936
4937 let style = Cli::try_parse_from(["asp", "example", "style-batch"])
4938 .expect("parse example style-batch");
4939 match style.command {
4940 Commands::Example {
4941 command: DiscoverabilityCommands::StyleBatch,
4942 } => {}
4943 other => panic!("unexpected command: {other:?}"),
4944 }
4945
4946 let session_schema =
4947 Cli::try_parse_from(["asp", "schema", "session-op", "transform.write_matrix"])
4948 .expect("parse schema session-op");
4949 match session_schema.command {
4950 Commands::Schema {
4951 command: DiscoverabilityCommands::SessionOp { kind },
4952 } => {
4953 assert_eq!(kind, "transform.write_matrix");
4954 }
4955 other => panic!("unexpected command: {other:?}"),
4956 }
4957
4958 let session_example =
4959 Cli::try_parse_from(["asp", "example", "session-op", "structure.insert_rows"])
4960 .expect("parse example session-op");
4961 match session_example.command {
4962 Commands::Example {
4963 command: DiscoverabilityCommands::SessionOp { kind },
4964 } => {
4965 assert_eq!(kind, "structure.insert_rows");
4966 }
4967 other => panic!("unexpected command: {other:?}"),
4968 }
4969 }
4970
4971 #[test]
4972 fn parses_named_ranges_and_scan_volatiles_arguments() {
4973 let named = Cli::try_parse_from([
4974 "agent-spreadsheet",
4975 "named-ranges",
4976 "workbook.xlsx",
4977 "--sheet",
4978 "Sheet1",
4979 "--name-prefix",
4980 "Sales",
4981 ])
4982 .expect("parse named-ranges");
4983
4984 match named.command {
4985 Commands::NamedRanges {
4986 file,
4987 sheet,
4988 name_prefix,
4989 ..
4990 } => {
4991 assert_eq!(file, PathBuf::from("workbook.xlsx"));
4992 assert_eq!(sheet.as_deref(), Some("Sheet1"));
4993 assert_eq!(name_prefix.as_deref(), Some("Sales"));
4994 }
4995 other => panic!("unexpected command: {other:?}"),
4996 }
4997
4998 let volatiles = Cli::try_parse_from([
4999 "agent-spreadsheet",
5000 "scan-volatiles",
5001 "workbook.xlsx",
5002 "--sheet",
5003 "Sheet1",
5004 "--limit",
5005 "10",
5006 "--offset",
5007 "5",
5008 ])
5009 .expect("parse scan-volatiles");
5010
5011 match volatiles.command {
5012 Commands::ScanVolatiles {
5013 file,
5014 sheet,
5015 limit,
5016 offset,
5017 formula_parse_policy,
5018 } => {
5019 assert_eq!(file, PathBuf::from("workbook.xlsx"));
5020 assert_eq!(sheet.as_deref(), Some("Sheet1"));
5021 assert_eq!(limit, Some(10));
5022 assert_eq!(offset, Some(5));
5023 assert!(formula_parse_policy.is_none());
5024 }
5025 other => panic!("unexpected command: {other:?}"),
5026 }
5027 }
5028
5029 #[test]
5030 fn parses_find_value_label_direction_arguments() {
5031 let find = Cli::try_parse_from([
5032 "agent-spreadsheet",
5033 "find-value",
5034 "workbook.xlsx",
5035 "Amount",
5036 "--sheet",
5037 "Sheet1",
5038 "--mode",
5039 "label",
5040 "--label-direction",
5041 "below",
5042 ])
5043 .expect("parse find-value");
5044
5045 match find.command {
5046 Commands::FindValue {
5047 file,
5048 query,
5049 sheet,
5050 mode,
5051 label_direction,
5052 ..
5053 } => {
5054 assert_eq!(file, PathBuf::from("workbook.xlsx"));
5055 assert_eq!(query, "Amount");
5056 assert_eq!(sheet.as_deref(), Some("Sheet1"));
5057 assert!(matches!(mode, Some(FindValueMode::Label)));
5058 assert!(matches!(label_direction, Some(LabelDirectionArg::Below)));
5059 }
5060 other => panic!("unexpected command: {other:?}"),
5061 }
5062 }
5063
5064 #[test]
5065 fn parses_find_formula_and_sheet_statistics_arguments() {
5066 let find = Cli::try_parse_from([
5067 "agent-spreadsheet",
5068 "find-formula",
5069 "workbook.xlsx",
5070 "SUM(",
5071 "--sheet",
5072 "Sheet1",
5073 "--limit",
5074 "25",
5075 "--offset",
5076 "50",
5077 ])
5078 .expect("parse find-formula");
5079
5080 match find.command {
5081 Commands::FindFormula {
5082 file,
5083 query,
5084 sheet,
5085 limit,
5086 offset,
5087 } => {
5088 assert_eq!(file, PathBuf::from("workbook.xlsx"));
5089 assert_eq!(query, "SUM(");
5090 assert_eq!(sheet.as_deref(), Some("Sheet1"));
5091 assert_eq!(limit, Some(25));
5092 assert_eq!(offset, Some(50));
5093 }
5094 other => panic!("unexpected command: {other:?}"),
5095 }
5096
5097 let stats = Cli::try_parse_from([
5098 "agent-spreadsheet",
5099 "sheet-statistics",
5100 "workbook.xlsx",
5101 "Summary",
5102 ])
5103 .expect("parse sheet-statistics");
5104
5105 match stats.command {
5106 Commands::SheetStatistics { file, sheet } => {
5107 assert_eq!(file, PathBuf::from("workbook.xlsx"));
5108 assert_eq!(sheet, "Summary");
5109 }
5110 other => panic!("unexpected command: {other:?}"),
5111 }
5112
5113 assert!(
5114 Cli::try_parse_from(["agent-spreadsheet", "find-formula", "workbook.xlsx"]).is_err(),
5115 "missing QUERY should fail clap parsing"
5116 );
5117 }
5118
5119 #[test]
5120 fn parses_sheet_page_all_required_formats() {
5121 for (raw, expected) in [
5122 ("full", SheetPageFormatArg::Full),
5123 ("compact", SheetPageFormatArg::Compact),
5124 ("values_only", SheetPageFormatArg::ValuesOnly),
5125 ] {
5126 let cli = Cli::try_parse_from([
5127 "agent-spreadsheet",
5128 "sheet-page",
5129 "workbook.xlsx",
5130 "Sheet1",
5131 "--format",
5132 raw,
5133 ])
5134 .expect("parse format value");
5135
5136 match cli.command {
5137 Commands::SheetPage { format, .. } => {
5138 assert!(
5139 matches!(
5140 (format, expected),
5141 (SheetPageFormatArg::Full, SheetPageFormatArg::Full)
5142 | (SheetPageFormatArg::Compact, SheetPageFormatArg::Compact)
5143 | (
5144 SheetPageFormatArg::ValuesOnly,
5145 SheetPageFormatArg::ValuesOnly
5146 )
5147 ),
5148 "format mismatch for {raw}"
5149 );
5150 }
5151 other => panic!("unexpected command: {other:?}"),
5152 }
5153 }
5154 }
5155
5156 #[test]
5157 fn normalizes_legacy_global_format_for_non_sheet_page_commands() {
5158 let normalized = normalize_legacy_global_format_argv(
5159 [
5160 "agent-spreadsheet",
5161 "list-sheets",
5162 "workbook.xlsx",
5163 "--format",
5164 "json",
5165 ]
5166 .into_iter()
5167 .map(OsString::from)
5168 .collect(),
5169 );
5170
5171 let tokens = normalized
5172 .iter()
5173 .map(|arg| arg.to_string_lossy().into_owned())
5174 .collect::<Vec<_>>();
5175
5176 assert_eq!(
5177 tokens,
5178 vec![
5179 "agent-spreadsheet",
5180 "list-sheets",
5181 "workbook.xlsx",
5182 "--output-format",
5183 "json"
5184 ]
5185 );
5186 }
5187
5188 #[test]
5189 fn preserves_sheet_page_local_format_flag() {
5190 let normalized = normalize_legacy_global_format_argv(
5191 [
5192 "agent-spreadsheet",
5193 "sheet-page",
5194 "workbook.xlsx",
5195 "Sheet1",
5196 "--format",
5197 "compact",
5198 ]
5199 .into_iter()
5200 .map(OsString::from)
5201 .collect(),
5202 );
5203
5204 let tokens = normalized
5205 .iter()
5206 .map(|arg| arg.to_string_lossy().into_owned())
5207 .collect::<Vec<_>>();
5208
5209 assert_eq!(
5210 tokens,
5211 vec![
5212 "agent-spreadsheet",
5213 "sheet-page",
5214 "workbook.xlsx",
5215 "Sheet1",
5216 "--format",
5217 "compact"
5218 ]
5219 );
5220 }
5221
5222 #[test]
5223 fn preserves_range_values_local_format_flag() {
5224 let normalized = normalize_legacy_global_format_argv(
5225 [
5226 "agent-spreadsheet",
5227 "range-values",
5228 "workbook.xlsx",
5229 "Sheet1",
5230 "A1:B2",
5231 "--format",
5232 "json",
5233 ]
5234 .into_iter()
5235 .map(OsString::from)
5236 .collect(),
5237 );
5238
5239 let tokens = normalized
5240 .iter()
5241 .map(|arg| arg.to_string_lossy().into_owned())
5242 .collect::<Vec<_>>();
5243
5244 assert_eq!(
5245 tokens,
5246 vec![
5247 "agent-spreadsheet",
5248 "range-values",
5249 "workbook.xlsx",
5250 "Sheet1",
5251 "A1:B2",
5252 "--format",
5253 "json"
5254 ]
5255 );
5256 }
5257
5258 #[test]
5259 fn parses_sheetport_manifest_validate_arguments() {
5260 let cli = Cli::try_parse_from([
5261 "agent-spreadsheet",
5262 "sheetport",
5263 "manifest",
5264 "validate",
5265 "manifest.yaml",
5266 ])
5267 .expect("parse sheetport manifest validate");
5268
5269 match cli.command {
5270 Commands::Sheetport { command } => match command {
5271 SheetportCommands::Manifest(SheetportManifestCommands::Validate { manifest }) => {
5272 assert_eq!(manifest, PathBuf::from("manifest.yaml"));
5273 }
5274 other => panic!("unexpected sheetport command: {other:?}"),
5275 },
5276 other => panic!("unexpected command: {other:?}"),
5277 }
5278 }
5279
5280 #[test]
5281 fn parses_sheetport_run_arguments() {
5282 let cli = Cli::try_parse_from([
5283 "agent-spreadsheet",
5284 "sheetport",
5285 "run",
5286 "workbook.xlsx",
5287 "manifest.yaml",
5288 "--inputs",
5289 "@inputs.json",
5290 "--rng-seed",
5291 "42",
5292 "--freeze-volatile",
5293 ])
5294 .expect("parse sheetport run");
5295
5296 match cli.command {
5297 Commands::Sheetport { command } => match command {
5298 SheetportCommands::Run {
5299 file,
5300 manifest,
5301 inputs,
5302 rng_seed,
5303 freeze_volatile,
5304 } => {
5305 assert_eq!(file, PathBuf::from("workbook.xlsx"));
5306 assert_eq!(manifest, PathBuf::from("manifest.yaml"));
5307 assert_eq!(inputs.as_deref(), Some("@inputs.json"));
5308 assert_eq!(rng_seed, Some(42));
5309 assert!(freeze_volatile);
5310 }
5311 other => panic!("unexpected sheetport command: {other:?}"),
5312 },
5313 other => panic!("unexpected command: {other:?}"),
5314 }
5315 }
5316
5317 #[test]
5318 fn surface_cli_parses_nested_read_table_and_resolves_to_internal_command() {
5319 let cli = SurfaceCli::try_parse_from([
5320 "asp",
5321 "read",
5322 "table",
5323 "workbook.xlsx",
5324 "--sheet",
5325 "Sheet1",
5326 "--table-format",
5327 "csv",
5328 ])
5329 .expect("parse surface read table");
5330
5331 let resolved = resolve_surface_command(cli.command).expect("resolve surface command");
5332 match resolved {
5333 ResolvedSurfaceCommand::Command(Commands::ReadTable {
5334 file,
5335 sheet,
5336 table_format,
5337 ..
5338 }) => {
5339 assert_eq!(file, PathBuf::from("workbook.xlsx"));
5340 assert_eq!(sheet.as_deref(), Some("Sheet1"));
5341 assert!(matches!(table_format, Some(TableReadFormat::Csv)));
5342 }
5343 other => panic!("unexpected resolved command: {other:?}"),
5344 }
5345 }
5346
5347 #[test]
5348 fn surface_cli_parses_nested_schema_targets() {
5349 let cli = SurfaceCli::try_parse_from(["asp", "schema", "write", "batch", "transform"])
5350 .expect("parse surface schema target");
5351
5352 let resolved = resolve_surface_command(cli.command).expect("resolve schema command");
5353 match resolved {
5354 ResolvedSurfaceCommand::Schema(DiscoverabilityCommands::TransformBatch) => {}
5355 other => panic!("unexpected resolved command: {other:?}"),
5356 }
5357 }
5358
5359 #[test]
5360 fn normalizes_legacy_flat_command_to_nested_surface() {
5361 let (normalized, warnings) = normalize_legacy_command_argv(
5362 ["agent-spreadsheet", "append-region", "workbook.xlsx"]
5363 .into_iter()
5364 .map(OsString::from)
5365 .collect(),
5366 );
5367
5368 let tokens = normalized
5369 .iter()
5370 .map(|arg| arg.to_string_lossy().into_owned())
5371 .collect::<Vec<_>>();
5372
5373 assert_eq!(
5374 tokens,
5375 vec!["agent-spreadsheet", "write", "append", "workbook.xlsx"]
5376 );
5377 assert_eq!(
5378 warnings,
5379 vec!["warning: 'append-region' is deprecated; use 'write append'"]
5380 );
5381 }
5382
5383 #[test]
5384 fn legacy_normalization_preserves_canonical_verify_group() {
5385 let (normalized, warnings) = normalize_legacy_command_argv(
5386 ["agent-spreadsheet", "verify", "diff", "a.xlsx", "b.xlsx"]
5387 .into_iter()
5388 .map(OsString::from)
5389 .collect(),
5390 );
5391
5392 let tokens = normalized
5393 .iter()
5394 .map(|arg| arg.to_string_lossy().into_owned())
5395 .collect::<Vec<_>>();
5396
5397 assert_eq!(
5398 tokens,
5399 vec!["agent-spreadsheet", "verify", "diff", "a.xlsx", "b.xlsx"]
5400 );
5401 assert!(warnings.is_empty());
5402 }
5403}