cursor-setup-system 0.0.74

Install, update, back up, restore and remove complete Cursor CLI configurations. Built by NDDev.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
//! The exact argv contract, parsed once and refused closed.
//!
//! The shape comes from the consumer, not from this crate:
//!
//! ```text
//! <executable> provider-info
//! <executable> <command> --target <absolute-resolved-dir> --json [command arguments]
//! ```
//!
//! `provider-info` receives neither `--target` nor `--json`. Every other command
//! receives both, immediately after the command name.
//!
//! # What each command carries
//!
//! | Command | Arguments beyond `--target --json` |
//! | --- | --- |
//! | `validate-bundle` | the five bundle flags |
//! | `plan-operation` | operation, release digest, operation id, expiry; optional backup ref, permission profile, bundle |
//! | `apply-operation` | plan path, plan digest, release digest; optional bundle |
//! | `recover-operation` | none — it reads the journal to know what it is resolving |
//! | `status` | none |
//!
//! `--expected-target-digest` is a v1/v2 flag and is **not** part of v3.
//! In v3 the provider observes the target itself and reports the digest it saw;
//! the consumer compares that against its own observation. Accepting the flag
//! here would invite a caller to assert a snapshot the provider never took.
//!
//! # Why order is not enforced
//!
//! The consumer emits these flags in a fixed order, and this parser accepts any
//! order. Refusing a well-formed call because two flags were swapped would break
//! a caller that did nothing wrong, while accepting a wider set than the consumer
//! emits costs nothing — the required set is still checked exactly, and an
//! unknown flag is still a refusal.

use std::collections::BTreeMap;
use std::path::PathBuf;

use crate::provider_v3::error::{Error, Result};
use crate::provider_v3::plan::BundleBinding;
use crate::provider_v3::reason::WireReason;
use crate::provider_v3::vocabulary::{Command, Operation, TargetScope};

/// The five flags that bind one bundle.
const BUNDLE_FLAGS: &[&str] = &[
    "--bundle",
    "--bundle-format",
    "--bundle-digest",
    "--artifact-digest",
    "--bundle-size",
];

/// What one command requires and what it accepts, stated once as data.
///
/// This table exists because of a measurement, not a preference. A peer
/// building the consumer half spent five round-trips discovering that
/// `plan-operation` takes seven required arguments — and they already knew the
/// shape from their own conformance code. Each missing flag surfaced singly,
/// and `--help` answered `--help has no value`, because it was parsed as a flag
/// that takes one. The one question a caller could ask was met with a complaint
/// about its grammar.
///
/// The refusals themselves were right, and they are unchanged. What was missing
/// was any way to ask.
///
/// It is one table read by two callers — [`usage`] renders it and [`parse`]
/// checks against it — because two lists of the same requirement eventually
/// disagree. A test binds it to the parser by removing each named flag from a
/// complete invocation and requiring the refusal to name it.
pub struct Usage {
    /// The command this describes.
    pub command: Command,
    /// Flags without which the command cannot run.
    pub required: &'static [&'static str],
    /// Flags the command accepts, each with why a caller would pass it.
    pub optional: &'static [(&'static str, &'static str)],
    /// One line on what the command does with them.
    pub note: &'static str,
}

/// The plan command's own usage, lifted out of `usage`.
///
/// Not a refactor for tidiness: `usage` crossed `clippy::too_many_lines` at 101
/// the moment `--target-scope` was added to this arm, and this is the arm that
/// grows -- every optional field a request gains lands here. Splitting the one
/// that moves keeps the rest where a reader expects it.
const fn plan_usage(command: Command) -> Usage {
    Usage {
        command,
        required: &[
            "--target",
            "--json",
            "--operation",
            "--provider-release-digest",
            "--operation-id",
            "--expires-at",
        ],
        optional: &[
            (
                "--prefix",
                "where a program lives; required by every software_* operation",
            ),
            ("--backup-ref", "which slot a restore returns to"),
            (
                "--capture-mode",
                "complete_native captures the entire declared native surface",
            ),
            ("--permission-profile", "a profile this build declares"),
            (
                "--software-version",
                "exactly one pinned version, when not the current one",
            ),
            (
                "--bundle …",
                "the five bundle flags, for install and replace",
            ),
            (
                "--target-scope",
                "which scope this target is; accepted and not yet acted on",
            ),
            (
                "--instruction-section",
                "marked user-global instruction bytes for patch_instruction_region",
            ),
        ],
        note: "Produce a plan. Always pure: reads the target and the local disk, opens no socket.",
    }
}

/// The arguments one command takes.
#[must_use]
pub const fn usage(command: Command) -> Usage {
    match command {
        Command::ProviderInfo => Usage {
            command,
            required: &[],
            optional: &[],
            note: "Report capabilities. Takes no arguments at all, not even --json.",
        },
        Command::Status => Usage {
            command,
            required: &["--target", "--json"],
            // Accepted since 0.0.55, declared through `status_request_fields`
            // once the kit names the member: a workspace that nobody has
            // installed into yet has no record to read a scope from, and the
            // consumer binds a plan to the identity `status` reports -- so it
            // must be able to ask about the scope it is about to plan under.
            optional: &[(
                "--target-scope",
                "which scope to measure the target under; absent, the target's own record decides",
            )],
            note: "Report the target's current state. Never changes it.",
        },
        Command::RecoverOperation => Usage {
            command,
            required: &["--target", "--json"],
            optional: &[],
            note: "Resolve an interrupted operation. Reads the journal to know what it is resolving.",
        },
        Command::ValidateBundle => Usage {
            command,
            required: &[
                "--target",
                "--json",
                "--bundle",
                "--bundle-format",
                "--bundle-digest",
                "--artifact-digest",
                "--bundle-size",
            ],
            optional: &[],
            note: "Check a bundle against the exact claim that named it. Touches nothing.",
        },
        Command::PlanOperation => plan_usage(command),
        Command::ApplyOperation => Usage {
            command,
            required: &[
                "--target",
                "--json",
                "--plan",
                "--plan-digest",
                "--provider-release-digest",
            ],
            optional: &[
                (
                    "--prefix",
                    "where a program lives; required by every software_* operation",
                ),
                (
                    "--software-artifact",
                    "one per software_artifacts entry, in the plan's order",
                ),
                (
                    "--bundle …",
                    "the five bundle flags, for install and replace",
                ),
            ],
            note: "Apply one exact plan under the target lock. --plan is the plan object, \
                   written canonically -- not the envelope the planner printed around it.",
        },
        Command::Launch => Usage {
            command,
            required: &["--target", "--json", "--prefix"],
            optional: &[(
                "-- <args>",
                "everything after a bare -- goes to the product verbatim",
            )],
            note: "Start the exact executable a software install placed. Never a name found on PATH.",
        },
    }
}

/// Render one command's arguments for a caller who asked.
#[must_use]
pub fn render_usage(command: Command) -> String {
    use std::fmt::Write as _;
    let shape = usage(command);
    let mut out = format!("{command}\n\n  {}\n", shape.note);
    if !shape.required.is_empty() {
        out.push_str("\nRequired:\n");
        for flag in shape.required {
            let _ = writeln!(out, "  {flag}");
        }
    }
    if !shape.optional.is_empty() {
        out.push_str("\nOptional:\n");
        for (flag, why) in shape.optional {
            let _ = writeln!(out, "  {flag:<22} {why}");
        }
    }
    out
}

/// One parsed invocation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Invocation {
    /// Report capabilities. Carries no target.
    ProviderInfo,
    /// Check a bundle without touching the target.
    ValidateBundle {
        /// The target the caller named.
        target: PathBuf,
        /// The bundle to check.
        bundle: Bundle,
    },
    /// Produce a plan. Pure.
    PlanOperation {
        /// The target the caller named.
        target: PathBuf,
        /// Everything the plan is bound to.
        request: PlanRequest,
    },
    /// Apply an exact plan.
    ApplyOperation {
        /// The target the caller named.
        target: PathBuf,
        /// The approved plan artifact on disk.
        plan_path: PathBuf,
        /// The digest that plan must have.
        plan_digest: String,
        /// The release digest the consumer verified.
        provider_release_digest: String,
        /// The bundle, when the operation carries one.
        bundle: Option<Bundle>,
        /// The program directory, when the operation installs software.
        prefix: Option<PathBuf>,
        /// The downloaded files, one per artifact the plan named, in its order.
        ///
        /// The contract gives software a download phase between planning and
        /// applying and gives the provider no command to run it in -- there is
        /// no `download` among the seven. So the consumer fetches what the plan
        /// named and hands the files back here, which is why this provider never
        /// opens a socket in any phase. The order is how each file is matched to
        /// its entry, so nothing about which is which has to be inferred.
        software_artifacts: Vec<PathBuf>,
    },
    /// Resolve an interrupted operation from its journal.
    RecoverOperation {
        /// The target the caller named.
        target: PathBuf,
    },
    /// Report the target's state without changing it.
    Status {
        /// The target the caller named.
        target: PathBuf,
        /// The scope the caller is asking about, when it said. Absent, the
        /// scope is read from the target's own record, as it always was.
        target_scope: Option<TargetScope>,
    },
    /// Start the product. Optional command.
    Launch {
        /// The target the caller named.
        ///
        /// Becomes the product's configuration home, through the environment
        /// variable the product documents for it. A product that documents none
        /// cannot honour a target, which is why this command is not declared
        /// there.
        target: PathBuf,
        /// The program directory holding what a software install placed.
        prefix: Option<PathBuf>,
        /// Everything after a bare `--`, handed to the product verbatim.
        arguments: Vec<String>,
    },
}

/// A bundle as the argv names it: identity plus where the bytes are.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bundle {
    /// Where the literal artifact sits on this machine.
    ///
    /// Not part of identity, and never recorded in a plan.
    pub path: PathBuf,
    /// The identity two parties agree on.
    pub binding: BundleBinding,
}

/// Everything `plan-operation` binds a plan to.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlanRequest {
    /// The operation to plan.
    pub operation: Operation,
    /// The release digest the consumer verified before invoking.
    pub provider_release_digest: String,
    /// The stable operation identifier the consumer minted.
    pub operation_id: String,
    /// When the plan stops being applicable.
    pub expires_at: String,
    /// The backup this operation reads or writes.
    pub backup_ref: Option<String>,
    /// Explicit complete preservation; absent retains recorded-file backup behavior.
    pub capture_mode: Option<String>,
    /// The permission profile to apply.
    pub permission_profile: Option<String>,
    /// The bundle, when the operation carries one.
    pub bundle: Option<Bundle>,
    /// The program directory, when the operation installs software.
    ///
    /// Not the target. The configuration a provider owns and the program it
    /// installs are different paths with different lifetimes, and conflating
    /// them would tie a program to one of the several targets it can serve.
    pub prefix: Option<PathBuf>,
    /// The exact version to install.
    ///
    /// Omitted means the version this build pins. Given means exactly that one,
    /// and anything else is refused rather than quietly installing a neighbour.
    pub software_version: Option<String>,
    /// Which scope the consumer resolved this target to be.
    ///
    /// **Accepted, and not yet acted on.** This build records it and plans the
    /// same way for every value, because nothing downstream branches on a scope
    /// yet. It is parsed regardless, for an ordering reason that is the mirror
    /// image of the one governing response fields.
    ///
    /// A *response* field may be declared only after the consumer accepts it:
    /// the consumer ships, then a provider may say it. A *request* field is the
    /// other way round. A consumer that starts sending `--target-scope` to a
    /// provider whose parser has never heard of it makes every older provider
    /// refuse the invocation outright -- measured before this existed:
    /// `--target-scope is not an argument of this command`. So a provider must
    /// tolerate the flag in a release *before* any consumer sends it, and only
    /// then branch on it.
    ///
    /// This is that first release. `scoped_projection_profiles` has been
    /// declarable since `0.0.7` and operable never, because no request field
    /// carried a scope; this is the field, arriving one release ahead of the
    /// behaviour on purpose.
    /// Which scope the consumer resolved this target to be.
    pub target_scope: Option<TargetScope>,
    /// Marked instruction bytes for `patch_instruction_region`.
    ///
    /// Absent on every other operation. The consumer sends this only after a
    /// provider declares both the operation and `instruction_section`.
    pub instruction_section: Option<String>,
}

/// Read `--target-scope`, refusing a value this build does not know.
///
/// **Not "accept anything".** Ignoring the value would be honest -- nothing
/// branches on it yet -- but it would also swallow a typo, and a flag that
/// cannot fail is not a flag. More importantly it would swallow a *future*
/// scope: a consumer sending a third scope to this build must be refused rather
/// than silently served a `global` plan, because a plan made against the wrong
/// scope is a correct-looking answer to a question nobody asked.
///
/// So the value is parsed against the closed set the kit publishes, and the
/// refusal names what this build knows.
fn take_target_scope(flags: &mut Flags) -> Result<Option<TargetScope>> {
    let Some(named) = flags.take_optional("--target-scope") else {
        return Ok(None);
    };
    TargetScope::parse(&named).map(Some).ok_or_else(|| {
        Error::refuse(
            WireReason::UnsupportedOperation,
            format!(
                "{named:?} is not a target scope this build knows; it knows {}",
                TargetScope::ALL
                    .iter()
                    .map(|scope| scope.as_str())
                    .collect::<Vec<_>>()
                    .join(", ")
            ),
        )
    })
}

impl Invocation {
    /// The target this invocation names, when it names one.
    #[must_use]
    pub fn target(&self) -> Option<&PathBuf> {
        match self {
            Self::ProviderInfo => None,
            Self::ValidateBundle { target, .. }
            | Self::PlanOperation { target, .. }
            | Self::ApplyOperation { target, .. }
            | Self::RecoverOperation { target }
            | Self::Status { target, .. }
            | Self::Launch { target, .. } => Some(target),
        }
    }

    /// The command this invocation is.
    #[must_use]
    pub const fn command(&self) -> Command {
        match self {
            Self::ProviderInfo => Command::ProviderInfo,
            Self::ValidateBundle { .. } => Command::ValidateBundle,
            Self::PlanOperation { .. } => Command::PlanOperation,
            Self::ApplyOperation { .. } => Command::ApplyOperation,
            Self::RecoverOperation { .. } => Command::RecoverOperation,
            Self::Status { .. } => Command::Status,
            Self::Launch { .. } => Command::Launch,
        }
    }
}

/// Parse one invocation from the arguments after the executable name.
///
/// # Errors
///
/// Refuses an unknown command, a missing or repeated flag, an unknown flag, a
/// bundle size that is not a decimal number, and an operation outside the closed
/// set. Each refusal names a reason; none of them guesses.
pub fn parse<I, S>(arguments: I) -> Result<Invocation>
where
    I: IntoIterator<Item = S>,
    S: Into<String>,
{
    let tokens: Vec<String> = arguments.into_iter().map(Into::into).collect();
    let Some(name) = tokens.first() else {
        return Err(local("no command was given"));
    };
    let Some(command) = Command::parse(name) else {
        return Err(local(format!(
            "{name:?} is not a provider protocol v3 command"
        )));
    };

    let rest = tokens.get(1..).unwrap_or_default();
    if !command.takes_target() {
        if !rest.is_empty() {
            return Err(local(format!("{command} takes no arguments")));
        }
        return Ok(Invocation::ProviderInfo);
    }

    // Everything after a bare `--` belongs to the product `launch` starts, so
    // it is taken off before this parser sees it. No other command has anything
    // to pass on, and one that finds a `--` gets an empty tail and refuses the
    // leftovers the same way it always would.
    let (mine, passthrough) = Flags::split_passthrough(rest);
    let mut flags = Flags::parse(&mine)?;

    // Every missing argument at once, rather than the first one alphabetically.
    // Learning a command used to cost one invocation per argument, and the
    // count is seven for two of these.
    let missing: Vec<&str> = usage(command)
        .required
        .iter()
        .copied()
        .filter(|flag| !flags.holds(flag))
        .collect();
    if !missing.is_empty() {
        return Err(local(format!(
            "{command} is missing {}; run `{command} --help` for what it takes",
            missing.join(", ")
        )));
    }

    let target = PathBuf::from(flags.take_required("--target")?);
    if !flags.take_switch("--json") {
        return Err(local(format!("{command} requires --json")));
    }

    let invocation = match command {
        Command::ProviderInfo => return Err(local("provider-info never reaches this branch")),
        Command::Status => Invocation::Status {
            target,
            target_scope: take_target_scope(&mut flags)?,
        },
        Command::RecoverOperation => Invocation::RecoverOperation { target },
        Command::Launch => Invocation::Launch {
            target,
            prefix: flags.take_prefix()?,
            arguments: passthrough,
        },
        Command::ValidateBundle => {
            let Some(bundle) = flags.take_bundle()? else {
                return Err(local("validate-bundle requires a bundle"));
            };
            Invocation::ValidateBundle { target, bundle }
        }
        Command::PlanOperation => {
            let operation_name = flags.take_required("--operation")?;
            let Some(operation) = Operation::parse(&operation_name) else {
                return Err(Error::refuse(
                    WireReason::UnsupportedOperation,
                    format!("{operation_name:?} is not an operation this protocol defines"),
                ));
            };
            Invocation::PlanOperation {
                target,
                request: PlanRequest {
                    operation,
                    provider_release_digest: flags.take_required("--provider-release-digest")?,
                    operation_id: flags.take_required("--operation-id")?,
                    expires_at: flags.take_required("--expires-at")?,
                    backup_ref: flags.take_optional("--backup-ref"),
                    capture_mode: flags.take_optional("--capture-mode"),
                    permission_profile: flags.take_optional("--permission-profile"),
                    bundle: flags.take_bundle()?,
                    prefix: flags.take_prefix()?,
                    software_version: flags.take_optional("--software-version"),
                    target_scope: take_target_scope(&mut flags)?,
                    instruction_section: flags.take_optional("--instruction-section"),
                },
            }
        }
        Command::ApplyOperation => Invocation::ApplyOperation {
            target,
            plan_path: PathBuf::from(flags.take_required("--plan")?),
            plan_digest: flags.take_required("--plan-digest")?,
            provider_release_digest: flags.take_required("--provider-release-digest")?,
            bundle: flags.take_bundle()?,
            prefix: flags.take_prefix()?,
            software_artifacts: flags
                .take_repeated("--software-artifact")
                .into_iter()
                .map(PathBuf::from)
                .collect(),
        },
    };

    flags.require_exhausted()?;
    Ok(invocation)
}

fn local(detail: impl Into<String>) -> Error {
    Error::refuse(WireReason::ProviderUnavailable, detail)
}

/// Name-to-value pairs, consumed as each command claims what it needs.
///
/// Anything left over at the end is an unknown flag, which is a refusal rather
/// than something to ignore. A provider that silently dropped an argument it did
/// not understand would report success for a request it only partly performed.
struct Flags {
    values: BTreeMap<String, Vec<String>>,
    switches: Vec<String>,
}

/// The flags a caller may give more than once.
///
/// Exactly one: `apply-operation` receives one downloaded file per artifact the
/// plan named, in the plan's order. Every other flag is still refused twice
/// over, because a second value where one is expected is a caller that meant
/// two different things and only one of them would happen.
const REPEATABLE: &[&str] = &["--software-artifact"];

/// True when `token` is another flag, not a value that happens to start with
/// dashes. Cursor's `alwaysApply` payload begins with YAML `---`; treating
/// every `--` prefix as a missing value refused that first write.
fn looks_like_flag(token: &str) -> bool {
    token
        .strip_prefix("--")
        .is_some_and(|rest| rest.starts_with(|c: char| c.is_ascii_alphabetic()))
}

impl Flags {
    /// Split a bare `--` off the end, keeping what follows verbatim.
    ///
    /// Only `launch` has anything to pass on, and what it passes belongs to
    /// another program: `-p`, `--help` and `--version` all mean something to the
    /// product and nothing here. A separator is the one way to say "stop
    /// reading these as mine" without guessing which of them are.
    fn split_passthrough(tokens: &[String]) -> (Vec<String>, Vec<String>) {
        match tokens.iter().position(|token| token == "--") {
            Some(at) => (tokens[..at].to_vec(), tokens[at + 1..].to_vec()),
            None => (tokens.to_vec(), Vec::new()),
        }
    }

    fn parse(tokens: &[String]) -> Result<Self> {
        let mut values: BTreeMap<String, Vec<String>> = BTreeMap::new();
        let mut switches = Vec::new();
        let mut index = 0;
        while index < tokens.len() {
            let Some(token) = tokens.get(index) else {
                break;
            };
            if !token.starts_with("--") {
                return Err(local(format!("{token:?} is not a flag")));
            }
            if token == "--json" {
                if switches.iter().any(|switch| switch == token) {
                    return Err(local("--json was given twice"));
                }
                switches.push(token.clone());
                index += 1;
                continue;
            }
            let Some(value) = tokens.get(index + 1) else {
                return Err(local(format!("{token} has no value")));
            };
            if looks_like_flag(value) {
                return Err(local(format!("{token} has no value")));
            }
            let seen = values.entry(token.clone()).or_default();
            if !seen.is_empty() && !REPEATABLE.contains(&token.as_str()) {
                return Err(local(format!("{token} was given twice")));
            }
            seen.push(value.clone());
            index += 2;
        }
        Ok(Self { values, switches })
    }

    /// Whether a flag was given, without consuming it.
    ///
    /// `--json` is a switch and lives in its own list; asking about it here
    /// keeps the completeness check able to name it beside the others rather
    /// than leaving one required argument to a separate refusal further down.
    fn holds(&self, name: &str) -> bool {
        if name == "--json" {
            return self.switches.iter().any(|switch| switch == name);
        }
        self.values.contains_key(name)
    }

    fn take_required(&mut self, name: &str) -> Result<String> {
        self.take_optional(name)
            .ok_or_else(|| local(format!("{name} is required")))
    }

    fn take_optional(&mut self, name: &str) -> Option<String> {
        self.values.remove(name)?.into_iter().next()
    }

    /// Every value of a flag a caller may repeat, in the order they were given.
    ///
    /// The order is load-bearing: it is how `apply` knows which file answers
    /// which entry of the plan's `software_artifacts` array.
    fn take_repeated(&mut self, name: &str) -> Vec<String> {
        self.values.remove(name).unwrap_or_default()
    }

    fn take_switch(&mut self, name: &str) -> bool {
        if let Some(position) = self.switches.iter().position(|switch| switch == name) {
            self.switches.remove(position);
            return true;
        }
        false
    }

    /// Take all five bundle flags, or none of them.
    ///
    /// A partial set is refused rather than filled in. Four of the five describe
    /// an identity; guessing the fifth would let a caller bind bytes it never
    /// named.
    fn take_bundle(&mut self) -> Result<Option<Bundle>> {
        let present = BUNDLE_FLAGS
            .iter()
            .filter(|flag| self.values.contains_key(**flag))
            .count();
        if present == 0 {
            return Ok(None);
        }
        if present != BUNDLE_FLAGS.len() {
            return Err(local(
                "a bundle is named by all five of --bundle, --bundle-format, \
                 --bundle-digest, --artifact-digest and --bundle-size",
            ));
        }
        let path = PathBuf::from(self.take_required("--bundle")?);
        let bundle_format = self.take_required("--bundle-format")?;
        let bundle_digest = self.take_required("--bundle-digest")?;
        let artifact_digest = self.take_required("--artifact-digest")?;
        let raw_size = self.take_required("--bundle-size")?;
        let bundle_size: u64 = raw_size.parse().map_err(|_| {
            local(format!(
                "--bundle-size {raw_size:?} is not a decimal byte count"
            ))
        })?;

        Ok(Some(Bundle {
            path,
            binding: BundleBinding {
                bundle_format,
                bundle_digest,
                artifact_digest,
                bundle_size,
            },
        }))
    }

    /// The program directory, checked to be absolute.
    ///
    /// The contract says both `--target` and `--prefix` are absolute. A relative
    /// one would resolve against whatever directory the caller happened to be
    /// in, which is not a property a plan can be bound to.
    fn take_prefix(&mut self) -> Result<Option<PathBuf>> {
        let Some(text) = self.take_optional("--prefix") else {
            return Ok(None);
        };
        let path = PathBuf::from(&text);
        if !path.is_absolute() {
            return Err(local(format!("--prefix {text:?} is not an absolute path")));
        }
        Ok(Some(path))
    }

    fn require_exhausted(&self) -> Result<()> {
        if let Some((name, _)) = self.values.iter().next() {
            return Err(local(format!("{name} is not an argument of this command")));
        }
        if let Some(switch) = self.switches.first() {
            return Err(local(format!(
                "{switch} is not an argument of this command"
            )));
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used, clippy::panic)]

    use super::*;

    const DIGEST: &str = "sha256:2222222222222222222222222222222222222222222222222222222222222222";

    fn bundle_flags() -> Vec<String> {
        [
            "--bundle",
            "/tmp/bundle.zip",
            "--bundle-format",
            "ai-stp-bundle/1",
            "--bundle-digest",
            DIGEST,
            "--artifact-digest",
            DIGEST,
            "--bundle-size",
            "4096",
        ]
        .iter()
        .map(|s| (*s).to_owned())
        .collect()
    }

    fn with_target(command: &str, extra: &[&str]) -> Vec<String> {
        let mut tokens = vec![
            command.to_owned(),
            "--target".to_owned(),
            "/tmp/target".to_owned(),
            "--json".to_owned(),
        ];
        tokens.extend(extra.iter().map(|s| (*s).to_owned()));
        tokens
    }

    /// A complete, well-formed invocation of one command.
    ///
    /// Paths come from `std::env::temp_dir()` rather than `/tmp`, because **on
    /// Windows a rooted path is not an absolute path**: `Path::new("/tmp")`
    /// answers `is_absolute() == false` there, since absolute means a drive or
    /// a UNC prefix. This project has met that once before -- a fixture passing
    /// `/tmp` made a test prove the right thing on two systems and nothing at
    /// all on the third -- and this test found it again on the first Windows run
    /// after it was written, with `--prefix "/tmp/prefix" is not an absolute
    /// path`. The product code was right both times.
    fn complete(command: Command) -> Vec<String> {
        let temporary = |name: &str| {
            std::env::temp_dir()
                .join(name)
                .to_string_lossy()
                .into_owned()
        };
        let mut tokens = vec![command.as_str().to_owned()];
        for flag in usage(command).required {
            tokens.push((*flag).to_owned());
            if *flag == "--json" {
                continue;
            }
            tokens.push(match *flag {
                "--target" => temporary("target"),
                "--operation" => "install".to_owned(),
                "--bundle" | "--plan" => temporary("file"),
                "--bundle-format" => "ai-stp-bundle/1".to_owned(),
                "--bundle-size" => "4096".to_owned(),
                "--operation-id" => "operation_00000000000000000000000".to_owned(),
                "--expires-at" => "2027-01-01T00:00:00.000Z".to_owned(),
                "--prefix" => temporary("prefix"),
                _ => DIGEST.to_owned(),
            });
        }
        // `install` arrives as a bundle, so a complete plan for it carries one.
        if command == Command::PlanOperation {
            tokens.extend(bundle_flags());
        }
        tokens
    }

    /// The table and the parser must demand the same set.
    ///
    /// [`usage`] is read by `--help` and by the completeness refusal, and it
    /// would be worth nothing if it drifted from what `parse` actually enforces
    /// -- a caller would be told the truth about a command that then refused
    /// something else. So every flag the table calls required is removed from a
    /// complete invocation, one at a time, and the refusal must name it.
    #[test]
    fn every_flag_the_table_calls_required_is_one_the_parser_demands() {
        for command in Command::ALL.iter().copied().filter(|c| c.takes_target()) {
            let whole = complete(command);
            parse(whole.clone())
                .unwrap_or_else(|e| panic!("{command}: a complete invocation was refused: {e}"));

            for flag in usage(command).required {
                let mut without = Vec::new();
                let mut skip = false;
                for token in &whole {
                    if skip {
                        skip = false;
                        continue;
                    }
                    if token == flag {
                        skip = *flag != "--json";
                        continue;
                    }
                    without.push(token.clone());
                }
                let error = parse(without)
                    .err()
                    .unwrap_or_else(|| panic!("{command} was accepted without {flag}"));
                assert!(
                    error.detail().contains(flag),
                    "{command} without {flag} refused without naming it: {}",
                    error.detail()
                );
            }
        }
    }

    /// The count is the point: learning a command used to cost one invocation
    /// per argument.
    #[test]
    fn one_refusal_names_every_missing_argument() {
        let error = parse(["plan-operation", "--target", "/tmp/target"]).unwrap_err();
        for flag in [
            "--json",
            "--operation",
            "--provider-release-digest",
            "--operation-id",
            "--expires-at",
        ] {
            assert!(
                error.detail().contains(flag),
                "the refusal did not name {flag}: {}",
                error.detail()
            );
        }
        assert!(
            error.detail().contains("--help"),
            "it does not say how to ask"
        );
    }

    /// Every command can be asked what it takes, and the answer names the same
    /// flags the refusal would.
    #[test]
    fn every_command_can_be_asked_what_it_takes() {
        for command in Command::ALL {
            let rendered = render_usage(*command);
            assert!(rendered.contains(command.as_str()));
            assert!(!usage(*command).note.is_empty());
            for flag in usage(*command).required {
                assert!(
                    rendered.contains(flag),
                    "{command} help omits its own required {flag}"
                );
            }
        }
    }

    #[test]
    fn provider_info_takes_neither_target_nor_json() {
        assert_eq!(parse(["provider-info"]).unwrap(), Invocation::ProviderInfo);
        assert!(parse(["provider-info", "--target", "/tmp/x"]).is_err());
        assert!(parse(["provider-info", "--json"]).is_err());
    }

    #[test]
    fn every_other_command_requires_both() {
        for command in Command::ALL.iter().filter(|c| c.takes_target()) {
            let name = command.as_str();
            assert!(parse([name]).is_err(), "{name} accepted no target");
            assert!(
                parse([name, "--target", "/tmp/target"]).is_err(),
                "{name} accepted no --json"
            );
        }
    }

    #[test]
    fn status_and_recover_carry_nothing_else() {
        assert_eq!(
            parse(with_target("status", &[])).unwrap(),
            Invocation::Status {
                target: PathBuf::from("/tmp/target"),
                target_scope: None,
            }
        );
        assert!(
            parse(with_target(
                "recover-operation",
                &["--target-scope", "project"]
            ))
            .is_err(),
            "recovery reads its scope from the journal, never from argv"
        );
        assert_eq!(
            parse(with_target("recover-operation", &[])).unwrap(),
            Invocation::RecoverOperation {
                target: PathBuf::from("/tmp/target")
            }
        );
    }

    /// The consumer binds a plan to the identity `status` reports a moment
    /// before, and a workspace nobody has installed into has no record to
    /// read a scope from -- so `status` must be able to be asked.
    #[test]
    fn status_may_be_asked_about_a_scope() {
        assert_eq!(
            parse(with_target("status", &["--target-scope", "project"])).unwrap(),
            Invocation::Status {
                target: PathBuf::from("/tmp/target"),
                target_scope: Some(TargetScope::Project),
            }
        );
        let error = parse(with_target("status", &["--target-scope", "galaxy"])).unwrap_err();
        assert!(
            error.detail().contains("not a target scope"),
            "{}",
            error.detail()
        );
    }

    #[test]
    fn a_full_plan_request_parses_every_field() {
        let mut tokens = with_target(
            "plan-operation",
            &[
                "--operation",
                "install",
                "--provider-release-digest",
                DIGEST,
                "--operation-id",
                "operation_01TEST",
                "--expires-at",
                "2026-08-23T15:00:00Z",
                "--backup-ref",
                "slot-000000000001",
                "--permission-profile",
                "default",
            ],
        );
        tokens.extend(bundle_flags());

        let Invocation::PlanOperation { target, request } = parse(tokens).unwrap() else {
            panic!("expected a plan invocation");
        };
        assert_eq!(target, PathBuf::from("/tmp/target"));
        assert_eq!(request.operation, Operation::Install);
        assert_eq!(request.operation_id, "operation_01TEST");
        assert_eq!(request.backup_ref.as_deref(), Some("slot-000000000001"));
        assert_eq!(request.permission_profile.as_deref(), Some("default"));
        let bundle = request.bundle.unwrap();
        assert_eq!(bundle.path, PathBuf::from("/tmp/bundle.zip"));
        assert_eq!(bundle.binding.bundle_size, 4096);
    }

    #[test]
    fn a_plan_without_the_optional_parts_still_parses() {
        let tokens = with_target(
            "plan-operation",
            &[
                "--operation",
                "backup",
                "--provider-release-digest",
                DIGEST,
                "--operation-id",
                "operation_01TEST",
                "--expires-at",
                "2026-08-23T15:00:00Z",
            ],
        );
        let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else {
            panic!("expected a plan invocation");
        };
        assert_eq!(request.operation, Operation::Backup);
        assert!(request.bundle.is_none());
        assert!(request.backup_ref.is_none());
    }

    #[test]
    fn an_operation_outside_the_closed_set_is_refused_by_its_contract_reason() {
        let tokens = with_target(
            "plan-operation",
            &[
                "--operation",
                "reformat",
                "--provider-release-digest",
                DIGEST,
                "--operation-id",
                "operation_01TEST",
                "--expires-at",
                "2026-08-23T15:00:00Z",
            ],
        );
        let error = parse(tokens).unwrap_err();
        assert_eq!(error.reason(), Some(WireReason::UnsupportedOperation));
    }

    #[test]
    fn patch_instruction_region_parses_the_marked_section() {
        let tokens = with_target(
            "plan-operation",
            &[
                "--operation",
                "patch_instruction_region",
                "--provider-release-digest",
                DIGEST,
                "--operation-id",
                "operation_01TEST",
                "--expires-at",
                "2026-08-23T15:00:00Z",
                "--instruction-section",
                ":::begin-ai-stp\nhello\n:::end-ai-stp\n",
            ],
        );
        let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else {
            panic!("expected a plan invocation");
        };
        assert_eq!(request.operation, Operation::PatchInstructionRegion);
        assert_eq!(
            request.instruction_section.as_deref(),
            Some(":::begin-ai-stp\nhello\n:::end-ai-stp\n")
        );
    }

    #[test]
    fn patch_instruction_region_parses_yaml_frontmatter_before_markers() {
        let section = "---\nalwaysApply: true\n---\n\n:::begin-ai-stp\nhello\n:::end-ai-stp\n";
        let tokens = with_target(
            "plan-operation",
            &[
                "--operation",
                "patch_instruction_region",
                "--provider-release-digest",
                DIGEST,
                "--instruction-section",
                section,
                "--operation-id",
                "operation_01TEST",
                "--expires-at",
                "2026-08-23T15:00:00Z",
            ],
        );
        let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else {
            panic!("expected a plan invocation");
        };
        assert_eq!(request.instruction_section.as_deref(), Some(section));
    }

    /// A consumer that sends a scope must not be refused by a build that cannot
    /// yet use it.
    ///
    /// This is the ordering that governs a *request* field, and it runs the
    /// opposite way to the one governing a response field. A response field is
    /// the consumer's move first: it accepts, it ships, and only then may a
    /// provider declare it. A request field is a provider's move first, because
    /// a consumer that starts sending an unknown flag makes every older
    /// provider refuse the invocation outright -- which is exactly what this
    /// build did before the flag existed:
    ///
    /// ```text
    /// --target-scope is not an argument of this command
    /// ```
    ///
    /// So the flag is accepted a release ahead of anything branching on it.
    #[test]
    fn a_scope_this_build_cannot_yet_use_is_still_accepted() {
        for named in TargetScope::ALL {
            let tokens = with_target(
                "plan-operation",
                &[
                    "--operation",
                    "backup",
                    "--provider-release-digest",
                    DIGEST,
                    "--operation-id",
                    "operation_01TEST",
                    "--expires-at",
                    "2026-08-23T15:00:00Z",
                    "--target-scope",
                    named.as_str(),
                ],
            );
            let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else {
                panic!("plan-operation did not parse with --target-scope {named:?}");
            };
            assert_eq!(request.target_scope, Some(*named));
        }
    }

    /// And a scope it does not know is refused rather than silently served.
    ///
    /// Accepting any string would swallow a typo, and worse: it would swallow a
    /// *future* scope. A consumer sending a third one to this build must be
    /// refused rather than handed a `global` plan, because a plan made against
    /// the wrong scope is a correct-looking answer to a question nobody asked.
    #[test]
    fn a_scope_this_build_does_not_know_is_refused_by_name() {
        let tokens = with_target(
            "plan-operation",
            &[
                "--operation",
                "backup",
                "--provider-release-digest",
                DIGEST,
                "--operation-id",
                "operation_01TEST",
                "--expires-at",
                "2026-08-23T15:00:00Z",
                "--target-scope",
                "machine_root",
            ],
        );
        let error = parse(tokens).unwrap_err();
        assert_eq!(error.reason(), Some(WireReason::UnsupportedOperation));
        let said = error.to_string();
        assert!(said.contains("machine_root"), "{said}");
        for known in TargetScope::ALL {
            assert!(said.contains(known.as_str()), "{said} omits {known:?}");
        }
    }

    /// Omitting it is unchanged, which is the whole point of a tolerated field.
    #[test]
    fn a_plan_without_a_scope_still_parses_and_says_so() {
        let tokens = with_target(
            "plan-operation",
            &[
                "--operation",
                "backup",
                "--provider-release-digest",
                DIGEST,
                "--operation-id",
                "operation_01TEST",
                "--expires-at",
                "2026-08-23T15:00:00Z",
            ],
        );
        let Invocation::PlanOperation { request, .. } = parse(tokens).unwrap() else {
            panic!("plan-operation without a scope did not parse");
        };
        assert_eq!(request.target_scope, None);
    }

    #[test]
    fn apply_binds_the_plan_path_and_its_digest() {
        let mut tokens = with_target(
            "apply-operation",
            &[
                "--plan",
                "/tmp/plan.json",
                "--plan-digest",
                DIGEST,
                "--provider-release-digest",
                DIGEST,
            ],
        );
        tokens.extend(bundle_flags());
        let Invocation::ApplyOperation {
            plan_path,
            plan_digest,
            bundle,
            ..
        } = parse(tokens).unwrap()
        else {
            panic!("expected an apply invocation");
        };
        assert_eq!(plan_path, PathBuf::from("/tmp/plan.json"));
        assert_eq!(plan_digest, DIGEST);
        assert!(bundle.is_some());
    }

    #[test]
    fn a_partial_bundle_is_refused_rather_than_completed() {
        let mut partial = bundle_flags();
        partial.truncate(partial.len() - 2); // drop --bundle-size and its value

        // On `validate-bundle` the bundle *is* the command, so the five flags
        // are required and the completeness check names the missing one before
        // the bundle reader is reached. Naming the exact flag is the better
        // answer of the two, and it is the one a caller gets here.
        let error =
            parse([with_target("validate-bundle", &[]), partial.clone()].concat()).unwrap_err();
        assert!(
            error.detail().contains("--bundle-size"),
            "{}",
            error.detail()
        );

        // On `plan-operation` a bundle is optional, so the invariant that still
        // has to be stated is all-five-or-none. This is the refusal that would
        // otherwise have been lost when the completeness check went in.
        let plan = parse(
            [
                with_target(
                    "plan-operation",
                    &[
                        "--operation",
                        "install",
                        "--provider-release-digest",
                        DIGEST,
                        "--operation-id",
                        "operation_00000000000000000000000",
                        "--expires-at",
                        "2027-01-01T00:00:00.000Z",
                    ],
                ),
                partial,
            ]
            .concat(),
        )
        .unwrap_err();
        assert!(plan.detail().contains("all five"), "{}", plan.detail());
    }

    #[test]
    fn a_bundle_size_that_is_not_a_number_is_refused() {
        let mut flags = bundle_flags();
        let last = flags.len() - 1;
        flags[last] = "four thousand".to_owned();
        let tokens = [with_target("validate-bundle", &[]), flags].concat();
        assert!(
            parse(tokens)
                .unwrap_err()
                .detail()
                .contains("decimal byte count")
        );
    }

    #[test]
    fn an_unknown_flag_is_refused_rather_than_ignored() {
        // Dropping an argument silently would report success for a request the
        // provider only partly performed.
        let tokens = with_target("status", &["--dry-run", "true"]);
        assert!(parse(tokens).unwrap_err().detail().contains("--dry-run"));
    }

    #[test]
    fn the_v1_expected_target_digest_flag_is_not_a_v3_argument() {
        let tokens = with_target(
            "plan-operation",
            &[
                "--operation",
                "install",
                "--provider-release-digest",
                DIGEST,
                "--operation-id",
                "operation_01TEST",
                "--expires-at",
                "2026-08-23T15:00:00Z",
                "--expected-target-digest",
                DIGEST,
            ],
        );
        assert!(
            parse(tokens)
                .unwrap_err()
                .detail()
                .contains("--expected-target-digest")
        );
    }

    #[test]
    fn a_repeated_flag_is_refused_rather_than_last_one_winning() {
        let tokens = [
            "status", "--target", "/tmp/a", "--target", "/tmp/b", "--json",
        ];
        assert!(parse(tokens).unwrap_err().detail().contains("twice"));
    }

    #[test]
    fn a_flag_with_no_value_is_refused() {
        assert!(
            parse(["status", "--target", "--json"])
                .unwrap_err()
                .detail()
                .contains("no value")
        );
        assert!(
            parse(["status", "--target"])
                .unwrap_err()
                .detail()
                .contains("no value")
        );
    }

    #[test]
    fn order_within_the_accepted_set_does_not_change_the_result() {
        let ordered = parse(with_target("status", &[])).unwrap();
        let swapped = parse(["status", "--json", "--target", "/tmp/target"]).unwrap();
        assert_eq!(ordered, swapped);
    }

    #[test]
    fn an_unknown_command_is_refused_and_never_guessed_at() {
        assert!(
            parse(["plan"])
                .unwrap_err()
                .detail()
                .contains("not a provider protocol v3")
        );
        assert!(
            parse(Vec::<String>::new())
                .unwrap_err()
                .detail()
                .contains("no command")
        );
    }

    #[test]
    fn a_parsed_invocation_reports_the_command_and_target_it_carries() {
        let invocation = parse(with_target("status", &[])).unwrap();
        assert_eq!(invocation.command(), Command::Status);
        assert_eq!(invocation.target(), Some(&PathBuf::from("/tmp/target")));
        assert_eq!(parse(["provider-info"]).unwrap().target(), None);
    }
}