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
(* src/package_manager/package_doctor.ml *)
(* Implementation of `t doctor` for package validation *)
type issue_level = Error | Warning | Suggestion
type issue = {
level : issue_level;
message : string;
suggestion : string option;
}
let check_file_exists path description =
if not (Sys.file_exists path) then
Some {
level = Error;
message = Printf.sprintf "Missing %s: %s" description path;
suggestion = Some (Printf.sprintf "Create %s" path);
}
else None
let check_directory_exists path description =
if Sys.file_exists path && not (Sys.is_directory path) then
Some {
level = Error;
message = Printf.sprintf "%s is not a directory: %s" description path;
suggestion = Some (Printf.sprintf "Remove %s and create it as a directory" path);
}
else if not (Sys.file_exists path) then
Some {
level = Warning;
message = Printf.sprintf "Missing %s directory: %s" description path;
suggestion = Some (Printf.sprintf "mkdir %s" path);
}
else None
let check_files_in_dir dir pattern description =
if Sys.file_exists dir && Sys.is_directory dir then
let entries = Sys.readdir dir in
let matched = Array.exists (fun e ->
(* Simple suffix check for now *)
String.length e >= String.length pattern &&
String.sub e (String.length e - String.length pattern) (String.length pattern) = pattern
) entries in
if not matched then
Some {
level = Warning;
message = Printf.sprintf "No %s found in %s" description dir;
suggestion = None;
}
else None
else None
let validate_package_structure dir =
let issues = ref [] in
let add_issue = function
| Some i -> issues := i :: !issues
| None -> ()
in
(* Check config files *)
add_issue (check_file_exists (Filename.concat dir "DESCRIPTION.toml") "package configuration");
add_issue (check_file_exists (Filename.concat dir "flake.nix") "Nix flake definition");
(* Check directories *)
add_issue (check_directory_exists (Filename.concat dir "src") "source");
add_issue (check_directory_exists (Filename.concat dir "tests") "tests");
(* Check content *)
add_issue (check_files_in_dir (Filename.concat dir "src") ".t" "T source files");
add_issue (check_files_in_dir (Filename.concat dir "tests") ".t" "test files");
(* Check optional but recommended files *)
let readme = Filename.concat dir "README.md" in
if not (Sys.file_exists readme) then
add_issue (Some {
level = Suggestion;
message = "No README.md found";
suggestion = Some "Create a README.md to document your package";
});
let license = Filename.concat dir "LICENSE" in
if not (Sys.file_exists license) then
add_issue (Some {
level = Warning;
message = "No LICENSE file found";
suggestion = Some "Add a LICENSE file to clarify usage rights";
});
List.rev !issues
let validate_project_structure dir =
let issues = ref [] in
let add_issue = function
| Some i -> issues := i :: !issues
| None -> ()
in
add_issue (check_file_exists (Filename.concat dir "tproject.toml") "project configuration");
add_issue (check_file_exists (Filename.concat dir "flake.nix") "Nix flake definition");
add_issue (check_directory_exists (Filename.concat dir "src") "source");
add_issue (check_directory_exists (Filename.concat dir "data") "data");
add_issue (check_directory_exists (Filename.concat dir "outputs") "outputs");
List.rev !issues
let check_nix_installation () =
let code = Sys.command "command -v nix >/dev/null 2>&1" in
if code <> 0 then
Some {
level = Error;
message = "Nix is not installed or not in PATH";
suggestion = Some "Install Nix: https://nixos.org/download.html";
}
else None
(*
--# Run Package/Project Doctor
--#
--# Validates the structure of a T package or project, checking for required files, directories,
--# valid Nix configuration, and ensuring proper documentation setup.
--#
--# @name run_doctor
--# @return :: Unit Prints the validation results to the console.
--# @family package_manager
--# @export
*)
let run_doctor () =
let dir = Sys.getcwd () in
Printf.printf "Running T Doctor in %s...\n\n" dir;
let is_package = Sys.file_exists (Filename.concat dir "DESCRIPTION.toml") in
let is_project = Sys.file_exists (Filename.concat dir "tproject.toml") in
let issues =
if is_package then begin
Printf.printf "Detected T Package.\n";
validate_package_structure dir
end else if is_project then begin
Printf.printf "Detected T Project.\n";
validate_project_structure dir
end else begin
Printf.printf "Neither DESCRIPTION.toml nor tproject.toml found.\n";
[{
level = Error;
message = "Not a T package or project directory";
suggestion = Some "Run `t init --package` or `t init --project`";
}]
end
in
let nix_issue = check_nix_installation () in
let issues = match nix_issue with Some i -> i :: issues | None -> issues in
(* Check Documentation *)
let doc_issues =
match Documentation_manager.validate_docs dir with
| Ok () -> []
| Error msg ->
[{ level = Warning; message = msg; suggestion = Some "Run `t docs` to debug or create docs/index.md" }]
in
let issues = doc_issues @ issues in
if issues = [] then
Printf.printf "\nā Everything looks good!\n"
else begin
Printf.printf "\nFound %d issue%s:\n\n" (List.length issues) (if List.length issues > 1 then "s" else "");
List.iter (fun i ->
let label = match i.level with
| Error -> "\027[31m[ERROR]\027[0m"
| Warning -> "\027[33m[WARN]\027[0m "
| Suggestion -> "\027[34m[INFO]\027[0m "
in
Printf.printf "%s %s\n" label i.message;
match i.suggestion with
| Some s -> Printf.printf " ā Suggestion: %s\n" s
| None -> ()
) issues;
Printf.printf "\n"
end