Skip to main content

dotrepo_cli/
cli.rs

1use clap::{Parser, Subcommand, ValueEnum};
2use dotrepo_core::{ClaimEventKind, DoctorSurface, ImportMode};
3use std::path::PathBuf;
4
5#[derive(Parser)]
6#[command(name = "dotrepo")]
7#[command(about = "reference cli for the dotrepo protocol")]
8pub struct Cli {
9    /// Repository root containing `.repo` or overlay records.
10    #[arg(long, default_value = ".")]
11    pub root: PathBuf,
12    #[command(subcommand)]
13    pub command: Command,
14}
15
16#[derive(Subcommand)]
17pub enum Command {
18    /// Initialize a canonical root `.repo` scaffold.
19    Init {
20        /// Overwrite an existing root `.repo`.
21        #[arg(long)]
22        force: bool,
23    },
24    /// Import conventional repository surfaces into a dotrepo record.
25    Import {
26        /// Record mode for the imported manifest.
27        #[arg(long, value_enum, default_value_t = ImportModeArg::Native)]
28        mode: ImportModeArg,
29        /// Explicit repository source URL for overlays.
30        #[arg(long)]
31        source: Option<String>,
32        /// Overwrite previously imported artifacts.
33        #[arg(long)]
34        force: bool,
35    },
36    /// Bootstrap a native `.repo` from an existing public overlay record.
37    AdoptOverlay {
38        /// Path to an overlay record.toml from the public index.
39        overlay_record: PathBuf,
40        /// Overwrite an existing root `.repo`.
41        #[arg(long)]
42        force: bool,
43    },
44    /// Validate only the root `.repo` or root `record.toml`.
45    Validate,
46    /// Validate a public index tree rooted at `index/`.
47    ValidateIndex {
48        /// Index root to validate.
49        #[arg(long, default_value = "index")]
50        index_root: PathBuf,
51    },
52    /// Analyze index records for promotion eligibility to verified status.
53    PromotionReport {
54        /// Index root to analyze.
55        #[arg(long, default_value = "index")]
56        index_root: PathBuf,
57        /// Apply eligible draft/imported/inferred promotions after reporting.
58        #[arg(long)]
59        apply: bool,
60        /// Maximum number of promotions to apply.
61        #[arg(long, requires = "apply")]
62        limit: Option<usize>,
63        /// Emit the full report as JSON.
64        #[arg(long)]
65        json: bool,
66        /// Show per-record details including blockers.
67        #[arg(long)]
68        verbose: bool,
69    },
70    /// Query one field from the selected record, preserving trust-aware selection in `--json`.
71    Query {
72        /// Dot-path such as `repo.name` or `record.trust.provenance`.
73        path: String,
74        /// Emit the full conflict-aware query report as JSON.
75        #[arg(long, conflicts_with = "raw")]
76        json: bool,
77        /// Emit only the selected scalar value. Refuses when competing records exist.
78        #[arg(long, conflicts_with = "json")]
79        raw: bool,
80    },
81    /// Render generated compatibility surfaces or fail when they drift.
82    Generate {
83        /// Check generated surfaces for drift without writing files.
84        #[arg(long)]
85        check: bool,
86    },
87    /// Inspect unmanaged conventional files at the repository root.
88    Doctor {
89        /// Emit the full doctor report as JSON.
90        #[arg(long)]
91        json: bool,
92    },
93    /// Adopt one supported Markdown surface into managed-region sync.
94    Manage {
95        /// Conventional surface to manage.
96        surface: PreviewSurfaceArg,
97        /// Convert the existing file into a managed-region file.
98        #[arg(long)]
99        adopt: bool,
100    },
101    /// Preview how dotrepo would render or replace one managed surface.
102    Preview {
103        /// Conventional surface to preview.
104        #[arg(long, value_enum, conflicts_with = "all")]
105        surface: Option<PreviewSurfaceArg>,
106        /// Preview every supported surface.
107        #[arg(long, conflicts_with = "surface")]
108        all: bool,
109        /// Emit the full preview report as JSON.
110        #[arg(long)]
111        json: bool,
112    },
113    /// Inspect trust, authority handoff, and competing records for one repository identity.
114    Trust {
115        /// Emit the full conflict-aware trust report as JSON.
116        #[arg(long)]
117        json: bool,
118    },
119    /// Summarize native adoption readiness for the current repository.
120    AdoptionStatus {
121        /// Emit the full adoption readiness report as JSON.
122        #[arg(long)]
123        json: bool,
124    },
125    /// Scaffold CI for the canonical native-repo maintainer loop.
126    Ci {
127        #[command(subcommand)]
128        command: CiCommand,
129    },
130    /// Inspect one maintainer-claim directory from the index.
131    Claim {
132        /// Claim directory relative to --root.
133        path: PathBuf,
134        /// Emit the full claim inspection report as JSON.
135        #[arg(long)]
136        json: bool,
137    },
138    /// Scaffold a draft maintainer-claim directory for one index repository.
139    ClaimInit {
140        /// Repository host under index_root/repos/<host>/<owner>/<repo>/.
141        #[arg(long)]
142        host: String,
143        /// Repository owner under index_root/repos/<host>/<owner>/<repo>/.
144        #[arg(long)]
145        owner: String,
146        /// Repository name under index_root/repos/<host>/<owner>/<repo>/.
147        #[arg(long)]
148        repo: String,
149        /// Claim directory name under claims/<claim-id>/.
150        #[arg(long)]
151        claim_id: String,
152        /// Claimant display name recorded in claim.toml.
153        #[arg(long)]
154        claimant_name: String,
155        /// Claimed repository role, such as `maintainer`.
156        #[arg(long)]
157        asserted_role: String,
158        /// Optional claimant contact detail.
159        #[arg(long)]
160        contact: Option<String>,
161        /// Additional record source URLs tied to this claim.
162        #[arg(long = "record-source")]
163        record_sources: Vec<String>,
164        /// Optional canonical repository URL for the claim target.
165        #[arg(long)]
166        canonical_repo_url: Option<String>,
167        /// Create a placeholder review.md next to claim.toml.
168        #[arg(long)]
169        review_md: bool,
170        /// Replace an existing empty scaffold, but never overwrite event history.
171        #[arg(long)]
172        force: bool,
173    },
174    /// Scaffold a maintainer claim from the current native `.repo`.
175    ClaimFromNative {
176        /// Index root where the overlay claim directory should be created.
177        #[arg(long, default_value = "index")]
178        index_root: PathBuf,
179        /// Claim directory name under claims/<claim-id>/.
180        #[arg(long)]
181        claim_id: String,
182        /// Claimant display name recorded in claim.toml.
183        #[arg(long)]
184        claimant_name: String,
185        /// Claimed repository role, such as `maintainer`.
186        #[arg(long, default_value = "maintainer")]
187        asserted_role: String,
188        /// Optional claimant contact detail.
189        #[arg(long)]
190        contact: Option<String>,
191        /// Create a placeholder review.md next to claim.toml.
192        #[arg(long)]
193        review_md: bool,
194        /// Replace an existing empty scaffold, but never overwrite event history.
195        #[arg(long)]
196        force: bool,
197    },
198    /// Append a new claim event and update the current claim state.
199    ClaimEvent {
200        /// Claim directory relative to --root.
201        path: PathBuf,
202        /// Event kind to append.
203        #[arg(long, value_enum)]
204        kind: ClaimEventKindArg,
205        /// Actor label recorded in the event.
206        #[arg(long)]
207        actor: String,
208        /// Short event summary recorded in the audit trail.
209        #[arg(long)]
210        summary: String,
211        /// Optional corrected current state when kind=corrected.
212        #[arg(long, value_enum)]
213        corrected_state: Option<CorrectedClaimStateArg>,
214        /// Optional canonical `.repo` path recorded for accepted handoff.
215        #[arg(long)]
216        canonical_record_path: Option<String>,
217        /// Optional canonical mirror record path recorded for accepted handoff.
218        #[arg(long)]
219        canonical_mirror_path: Option<String>,
220    },
221    /// Append a submitted claim event with the claim path derived from the current native `.repo`.
222    ClaimSubmitNative {
223        /// Index root containing the claim directory.
224        #[arg(long, default_value = "index")]
225        index_root: PathBuf,
226        /// Claim directory name under claims/<claim-id>/.
227        #[arg(long)]
228        claim_id: String,
229        /// Actor label recorded in the event.
230        #[arg(long, default_value = "claimant")]
231        actor: String,
232        /// Short event summary recorded in the audit trail.
233        #[arg(long, default_value = "Submitted maintainer claim.")]
234        summary: String,
235    },
236    /// Append an accepted claim event with canonical links from the current native `.repo`.
237    ClaimAcceptNative {
238        /// Index root containing the claim directory.
239        #[arg(long, default_value = "index")]
240        index_root: PathBuf,
241        /// Claim directory relative to --index-root.
242        #[arg(
243            value_name = "CLAIM_PATH",
244            help = "Claim directory relative to --index-root"
245        )]
246        path: Option<PathBuf>,
247        /// Claim directory name under claims/<claim-id>; derives path from repo.homepage.
248        #[arg(long)]
249        claim_id: Option<String>,
250        /// Actor label recorded in the event.
251        #[arg(long, default_value = "index-reviewer")]
252        actor: String,
253        /// Short event summary recorded in the audit trail.
254        #[arg(
255            long,
256            default_value = "Accepted maintainer claim with canonical native record."
257        )]
258        summary: String,
259    },
260    /// Inspect or export public read-only index responses.
261    Public {
262        #[command(subcommand)]
263        command: PublicCommand,
264    },
265}
266
267#[derive(Subcommand)]
268pub enum PublicCommand {
269    /// Render one public repository summary response as JSON.
270    Summary {
271        #[arg(long, default_value = "index")]
272        index_root: PathBuf,
273        host: String,
274        owner: String,
275        repo: String,
276        /// URL base path prefix for hosted public links, such as `/dotrepo`.
277        #[arg(long, default_value = "/")]
278        base_path: String,
279        /// Advisory staleness window in hours for the rendered response.
280        #[arg(long)]
281        stale_after_hours: Option<i64>,
282    },
283    /// Render one compact public research profile response as JSON.
284    Profile {
285        #[arg(long, default_value = "index")]
286        index_root: PathBuf,
287        host: String,
288        owner: String,
289        repo: String,
290        /// URL base path prefix for hosted public links, such as `/dotrepo`.
291        #[arg(long, default_value = "/")]
292        base_path: String,
293        /// Advisory staleness window in hours for the rendered response.
294        #[arg(long)]
295        stale_after_hours: Option<i64>,
296    },
297    /// Render compact public research profiles for multiple repositories.
298    BatchProfiles {
299        #[arg(long, default_value = "index")]
300        index_root: PathBuf,
301        /// Repository identity as host/owner/repo or https://host/owner/repo.
302        #[arg(long = "repo", required = true)]
303        repos: Vec<String>,
304        /// URL base path prefix for hosted public links, such as `/dotrepo`.
305        #[arg(long, default_value = "/")]
306        base_path: String,
307        /// Advisory staleness window in hours for the rendered response.
308        #[arg(long)]
309        stale_after_hours: Option<i64>,
310    },
311    /// Render public query responses for multiple repositories and dot paths.
312    BatchQuery {
313        #[arg(long, default_value = "index")]
314        index_root: PathBuf,
315        /// Repository identity as host/owner/repo or https://host/owner/repo.
316        #[arg(long = "repo", required = true)]
317        repos: Vec<String>,
318        /// Dot path to query. Repeat for multiple fields.
319        #[arg(long = "path", required = true)]
320        paths: Vec<String>,
321        /// URL base path prefix for hosted public links, such as `/dotrepo`.
322        #[arg(long, default_value = "/")]
323        base_path: String,
324        /// Advisory staleness window in hours for the rendered response.
325        #[arg(long)]
326        stale_after_hours: Option<i64>,
327    },
328    /// Compare compact public research profiles for multiple repositories.
329    Compare {
330        #[arg(long, default_value = "index")]
331        index_root: PathBuf,
332        /// Repository identity as host/owner/repo or https://host/owner/repo.
333        #[arg(long = "repo", required = true)]
334        repos: Vec<String>,
335        /// URL base path prefix for hosted public links, such as `/dotrepo`.
336        #[arg(long, default_value = "/")]
337        base_path: String,
338        /// Advisory staleness window in hours for the rendered response.
339        #[arg(long)]
340        stale_after_hours: Option<i64>,
341    },
342    /// Traverse public repository references declared in the selected profile.
343    Relations {
344        #[arg(long, default_value = "index")]
345        index_root: PathBuf,
346        host: String,
347        owner: String,
348        repo: String,
349        /// URL base path prefix for hosted public links, such as `/dotrepo`.
350        #[arg(long, default_value = "/")]
351        base_path: String,
352        /// Advisory staleness window in hours for the rendered response.
353        #[arg(long)]
354        stale_after_hours: Option<i64>,
355    },
356    /// Search compact public research profiles by text and structured filters.
357    Search {
358        #[arg(long, default_value = "index")]
359        index_root: PathBuf,
360        /// Text query matched against identity, name, purpose, homepage, license, languages, and topics.
361        #[arg(long)]
362        q: Option<String>,
363        /// Required language. Repeat for multiple required languages.
364        #[arg(long = "language")]
365        languages: Vec<String>,
366        /// Required topic. Repeat for multiple required topics.
367        #[arg(long = "topic")]
368        topics: Vec<String>,
369        /// Required selected record status. Repeat for accepted statuses.
370        #[arg(long = "status")]
371        statuses: Vec<String>,
372        /// Required trust confidence. Repeat for accepted confidence values.
373        #[arg(long = "confidence")]
374        confidences: Vec<String>,
375        /// Require a build command signal.
376        #[arg(long)]
377        require_build: bool,
378        /// Require a test command signal.
379        #[arg(long)]
380        require_test: bool,
381        /// Require documentation signal.
382        #[arg(long)]
383        require_docs: bool,
384        /// Require security contact signal.
385        #[arg(long)]
386        require_security_contact: bool,
387        /// Require license signal.
388        #[arg(long)]
389        require_license: bool,
390        /// Maximum results to return.
391        #[arg(long)]
392        limit: Option<usize>,
393        /// URL base path prefix for hosted public links, such as `/dotrepo`.
394        #[arg(long, default_value = "/")]
395        base_path: String,
396        /// Advisory staleness window in hours for the rendered response.
397        #[arg(long)]
398        stale_after_hours: Option<i64>,
399    },
400    /// Render one public trust response as JSON.
401    Trust {
402        #[arg(long, default_value = "index")]
403        index_root: PathBuf,
404        host: String,
405        owner: String,
406        repo: String,
407        /// URL base path prefix for hosted public links, such as `/dotrepo`.
408        #[arg(long, default_value = "/")]
409        base_path: String,
410        /// Advisory staleness window in hours for the rendered response.
411        #[arg(long)]
412        stale_after_hours: Option<i64>,
413    },
414    /// Render one public query response as JSON.
415    Query {
416        #[arg(long, default_value = "index")]
417        index_root: PathBuf,
418        host: String,
419        owner: String,
420        repo: String,
421        path: String,
422        /// URL base path prefix for hosted public links, such as `/dotrepo`.
423        #[arg(long, default_value = "/")]
424        base_path: String,
425        /// Advisory staleness window in hours for the rendered response.
426        #[arg(long)]
427        stale_after_hours: Option<i64>,
428    },
429    /// Export the static-first public JSON tree for repository summary, profile, and trust.
430    Export {
431        #[arg(long, default_value = "index")]
432        index_root: PathBuf,
433        #[arg(long, default_value = "public")]
434        out_dir: PathBuf,
435        /// URL base path prefix for hosted public links, such as `/dotrepo`.
436        #[arg(long, default_value = "/")]
437        base_path: String,
438        /// Advisory staleness window in hours for exported responses.
439        #[arg(long)]
440        stale_after_hours: Option<i64>,
441        /// Fixed RFC 3339 generation timestamp for deterministic export review.
442        #[arg(long)]
443        generated_at: Option<String>,
444        /// Fixed RFC 3339 staleness timestamp for deterministic export review.
445        #[arg(long)]
446        stale_after: Option<String>,
447        /// Previously published pagedigest manifest that carries revision
448        /// state forward when exporting to a fresh directory. Defaults to
449        /// `.well-known/pagedigest.json` inside --out-dir when present.
450        #[arg(long)]
451        pagedigest_previous: Option<PathBuf>,
452    },
453}
454
455#[derive(Subcommand)]
456pub enum CiCommand {
457    /// Write the starter GitHub Actions workflow for native-repo checks.
458    Init {
459        /// Replace an existing workflow file.
460        #[arg(long)]
461        force: bool,
462        /// Release version to pin in the workflow, such as `1.0.0`.
463        #[arg(long)]
464        version: Option<String>,
465    },
466}
467
468#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
469pub enum ImportModeArg {
470    Native,
471    Overlay,
472}
473
474#[derive(Clone, Copy, Debug, Eq, PartialEq, ValueEnum)]
475pub enum PreviewSurfaceArg {
476    Readme,
477    Security,
478    Contributing,
479    Codeowners,
480    PullRequestTemplate,
481}
482
483#[derive(Clone, Debug, Eq, PartialEq, ValueEnum)]
484pub enum ClaimEventKindArg {
485    Submitted,
486    ReviewStarted,
487    Accepted,
488    Rejected,
489    Withdrawn,
490    Disputed,
491    Corrected,
492}
493
494#[derive(Clone, Debug, Eq, PartialEq, ValueEnum)]
495pub enum CorrectedClaimStateArg {
496    Submitted,
497    InReview,
498    Accepted,
499    Rejected,
500    Withdrawn,
501    Disputed,
502}
503
504impl From<ImportModeArg> for ImportMode {
505    fn from(value: ImportModeArg) -> Self {
506        match value {
507            ImportModeArg::Native => ImportMode::Native,
508            ImportModeArg::Overlay => ImportMode::Overlay,
509        }
510    }
511}
512
513impl From<PreviewSurfaceArg> for DoctorSurface {
514    fn from(value: PreviewSurfaceArg) -> Self {
515        match value {
516            PreviewSurfaceArg::Readme => DoctorSurface::Readme,
517            PreviewSurfaceArg::Security => DoctorSurface::Security,
518            PreviewSurfaceArg::Contributing => DoctorSurface::Contributing,
519            PreviewSurfaceArg::Codeowners => DoctorSurface::Codeowners,
520            PreviewSurfaceArg::PullRequestTemplate => DoctorSurface::PullRequestTemplate,
521        }
522    }
523}
524
525impl From<ClaimEventKindArg> for ClaimEventKind {
526    fn from(value: ClaimEventKindArg) -> Self {
527        match value {
528            ClaimEventKindArg::Submitted => ClaimEventKind::Submitted,
529            ClaimEventKindArg::ReviewStarted => ClaimEventKind::ReviewStarted,
530            ClaimEventKindArg::Accepted => ClaimEventKind::Accepted,
531            ClaimEventKindArg::Rejected => ClaimEventKind::Rejected,
532            ClaimEventKindArg::Withdrawn => ClaimEventKind::Withdrawn,
533            ClaimEventKindArg::Disputed => ClaimEventKind::Disputed,
534            ClaimEventKindArg::Corrected => ClaimEventKind::Corrected,
535        }
536    }
537}
538
539impl From<CorrectedClaimStateArg> for dotrepo_core::ClaimState {
540    fn from(value: CorrectedClaimStateArg) -> Self {
541        match value {
542            CorrectedClaimStateArg::Submitted => dotrepo_core::ClaimState::Submitted,
543            CorrectedClaimStateArg::InReview => dotrepo_core::ClaimState::InReview,
544            CorrectedClaimStateArg::Accepted => dotrepo_core::ClaimState::Accepted,
545            CorrectedClaimStateArg::Rejected => dotrepo_core::ClaimState::Rejected,
546            CorrectedClaimStateArg::Withdrawn => dotrepo_core::ClaimState::Withdrawn,
547            CorrectedClaimStateArg::Disputed => dotrepo_core::ClaimState::Disputed,
548        }
549    }
550}