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
(* src/package_manager/scaffold.ml *)
(* Directory/file creation for `t init --package` and `t init --project` *)
open Package_types
(* --- Utility Helpers --- *)
let create_dir path =
if not (Sys.file_exists path) then
let rec mkdir_p dir =
if Sys.file_exists dir then ()
else begin
mkdir_p (Filename.dirname dir);
(try Unix.mkdir dir 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ())
end
in
mkdir_p path
let write_file path content =
let ch = open_out path in
output_string ch content;
close_out ch
let git_init dir =
let cmd = Printf.sprintf "git init %s" (Filename.quote dir) in
let code = Sys.command cmd in
if code <> 0 then
Printf.eprintf "Warning: git init failed (exit code %d)\n" code
let copy_agent_files dir is_package context =
(* Locate agents directory.
In a development environment, it's in the repo root.
In a Nix installation, it might be in a share directory. *)
let agents_dir =
match Sys.getenv_opt "TLANG_AGENTS_DIR" with
| Some d -> d
| None ->
let exe_dir = Filename.dirname Sys.executable_name in
let share_dir = Filename.concat (Filename.dirname exe_dir) "share/tlang/agents" in
if Sys.file_exists "agents" && Sys.is_directory "agents" then "agents"
else if Sys.file_exists share_dir && Sys.is_directory share_dir then share_dir
else "agents" (* Fallback to local *)
in
let agents_template = if is_package then "agents-package.md" else "agents-project.md" in
let ref_template =
match String.lowercase_ascii context with
| "small" -> "t-reference-small.md"
| "full" -> "t-reference-full.md"
| "huge" -> "t-reference-huge.md"
| _ -> "t-reference-medium.md"
in
let cp src dest =
let src_path = Filename.concat agents_dir src in
if Sys.file_exists src_path then begin
let ic = open_in src_path in
let content = really_input_string ic (in_channel_length ic) in
close_in ic;
let oc = open_out (Filename.concat dir dest) in
output_string oc content;
close_out oc;
true
end else false
in
let agents_ok = cp agents_template "AGENTS.md" in
let ref_ok = cp ref_template "T-LANGUAGE-REFERENCE.md" in
if agents_ok && ref_ok then begin
(* Add reference to .gitignore *)
let gitignore_path = Filename.concat dir ".gitignore" in
let out = open_out_gen [Open_append; Open_creat] 0o644 gitignore_path in
output_string out "\n# AI Agent Context Reference\nT-LANGUAGE-REFERENCE.md\n";
close_out out;
true
end else begin
if not (Sys.file_exists agents_dir) then
Printf.eprintf "Warning: agents directory not found at '%s'. Skipping AGENTS.md creation.\n" agents_dir;
false
end
let default_tlang_tag = "v" ^ Version.version
(* Strip the "v" prefix from a tag to get a plain version number. *)
let strip_v_prefix tag =
if String.starts_with ~prefix:"v" tag then
String.sub tag 1 (String.length tag - 1)
else
tag
(* Validate and sanitize a tag string to ensure it only contains safe characters.
This prevents potential code injection when the tag is used in templates.
Only allows alphanumeric characters, dots, hyphens, and the "v" prefix. *)
let sanitize_tag tag =
let is_safe_char c =
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c = '.' || c = '-' || c = 'v'
in
let rec check i =
if i >= String.length tag then true
else if is_safe_char tag.[i] then check (i + 1)
else false
in
if check 0 then tag else default_tlang_tag
(* Parse a semantic version tag.
This parser handles basic semver tags (vX.Y.Z or X.Y.Z) but does NOT handle
pre-release versions (e.g., "v1.0.0-alpha") or build metadata (e.g., "v1.0.0+build123").
Tags with hyphens or plus signs will be silently ignored.
See https://semver.org/spec/v2.0.0.html for the full semver specification. *)
let parse_semver tag =
let without_v =
if String.starts_with ~prefix:"v" tag then String.sub tag 1 (String.length tag - 1)
else tag
in
let parse_component s =
let rec scan i =
if i < String.length s && s.[i] >= '0' && s.[i] <= '9' then scan (i + 1)
else i
in
let n = scan 0 in
if n = 0 then None else int_of_string_opt (String.sub s 0 n)
in
match String.split_on_char '.' without_v with
| [major; minor; patch] ->
begin match parse_component major, parse_component minor, parse_component patch with
| Some ma, Some mi, Some pa -> Some (ma, mi, pa)
| _ -> None
end
| _ -> None
let compare_semver (a_ma, a_mi, a_pa) (b_ma, b_mi, b_pa) =
match compare a_ma b_ma with
| 0 ->
begin match compare a_mi b_mi with
| 0 -> compare a_pa b_pa
| c -> c
end
| c -> c
let latest_tlang_tag () =
let cmd = "git ls-remote --tags https://github.com/b-rodrigues/tlang.git" in
let ic = Unix.open_process_in cmd in
let rec read_tags acc =
match input_line ic with
| line ->
begin match String.split_on_char '\t' line with
| [_sha; refname] when String.starts_with ~prefix:"refs/tags/" refname ->
let prefix_len = String.length "refs/tags/" in
let tag = String.sub refname prefix_len (String.length refname - prefix_len) in
if String.starts_with ~prefix:"v" tag && not (String.ends_with ~suffix:"^{}" tag) then
read_tags (tag :: acc)
else
read_tags acc
| _ -> read_tags acc
end
| exception End_of_file -> List.rev acc
in
let tags = read_tags [] in
let status = Unix.close_process_in ic in
(match status with
| Unix.WEXITED 0 -> ()
| Unix.WEXITED code ->
Printf.eprintf
"Warning: command `%s` exited with code %d; using default tlang tag %s if needed.\n%!"
cmd code default_tlang_tag
| Unix.WSIGNALED signal | Unix.WSTOPPED signal ->
Printf.eprintf
"Warning: command `%s` was terminated by signal %d; using default tlang tag %s if needed.\n%!"
cmd signal default_tlang_tag);
let versioned_tags =
List.fold_left
(fun acc tag ->
match parse_semver tag with
| Some semver -> (semver, tag) :: acc
| None -> acc)
[]
tags
in
match versioned_tags with
| [] -> default_tlang_tag
| (semver, tag) :: rest ->
let _, latest_tag =
List.fold_left
(fun (best_semver, best_tag) (candidate_semver, candidate_tag) ->
if compare_semver candidate_semver best_semver > 0 then
(candidate_semver, candidate_tag)
else
(best_semver, best_tag))
(semver, tag)
rest
in
latest_tag
(* ================================================================ *)
(* Package Templates *)
(* ================================================================ *)
let package_description_toml = {|[package]
name = "{{name}}"
version = "0.1.0"
description = "A brief description of what {{name}} does"
authors = ["{{author}}"]
license = "{{license}}"
homepage = ""
repository = ""
[dependencies]
# T packages this package depends on
# Format: package = { git = "repository-url", tag = "version" }
[t]
# Minimum T language version required
min_version = "{{tlang_version}}"
[additional-tools]
# Additional Nix packages for development (e.g., git, awk)
packages = []
[latex]
# LaTeX packages for documentation (starts from scheme-small)
packages = []
|}
let package_flake_nix = {|{
description = "{{name}} — a T package";
inputs = {
nixpkgs.url = "github:rstats-on-nix/nixpkgs/{{nixpkgs_date}}";
flake-utils.url = "github:numtide/flake-utils";
t-lang.url = "github:b-rodrigues/tlang/{{tlang_tag}}";
# Package dependencies as flake inputs
# Add each dependency from DESCRIPTION.toml as a flake input here
};
outputs = { self, nixpkgs, flake-utils, t-lang }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
# The package itself
packages.default = pkgs.stdenv.mkDerivation {
pname = "t-{{name}}";
version = "0.1.0";
src = ./.;
buildInputs = [
# Add package dependencies here
];
installPhase = ''
mkdir -p $out/lib/t/packages/{{name}}
cp -r src $out/lib/t/packages/{{name}}/
if [ -d "help" ]; then
cp -r help $out/lib/t/packages/{{name}}/
fi
'';
meta = {
description = "{{name}} — a T package";
};
};
# Development shell for hacking on the package
devShells.default = pkgs.mkShell {
buildInputs = [
t-lang.packages.${system}.default
pkgs.pandoc # For documentation generation
];
shellHook = ''
echo "=========================================="
echo "T Package Development Environment"
echo "Package: {{name}}"
echo "=========================================="
echo ""
echo "Available commands:"
echo " t repl - Start T REPL"
echo " t run <file> - Run a T file"
echo " t test - Run package tests"
echo " t doc --parse . - Parse sources for documentation"
echo " t doc --generate - Generate documentation"
echo ""
echo "Source files: src/"
echo "Tests: tests/"
echo ""
'';
};
}
);
}
|}
let package_readme = {|# {{name}}
A T package.
## Description
A brief description of what {{name}} does.
## Editor Support
This project includes support for the **T Language Server (LSP)**.
1. Configure your editor following the [Editor Support Guide](https://tstats-project.org/editors.html).
2. Always launch your editor from within the `nix develop` environment (or use `direnv`).
Once active, you'll get autocompletion for T functions, variables, and DataFrame columns (via `$`).
## Installation
Add the following to the `[dependencies]` section of your `tproject.toml`:
```toml
[dependencies]
{{name}} = { git = "https://github.com/username/{{name}}", tag = "v0.1.0" }
```
Then run `nix develop` to enter the environment with the package available.
## Usage
```t
-- Example usage of {{name}} functions
```
## Development
Enter the development shell:
```bash
nix develop
```
Run tests:
```bash
t test
```
## License
{{license}}
|}
let package_changelog = {|# Changelog
All notable changes to this package will be documented in this file.
## [0.1.0] - {{date}}
### Added
- Initial release
|}
let package_gitignore = {|# Build artifacts
_build/
result
*.exe
# Editor files
*~
.#*
\#*#
.merlin
# OS files
.DS_Store
Thumbs.db
|}
let package_src_example = {|-- {{name}} — main source file
--
-- Add your T functions here. Each function should include T-Doc comments
-- starting with --# to be picked up by the documentation generator.
--# Greet someone
--#
--# A placeholder function that returns a friendly greeting.
--#
--# @name greet
--# @param name :: String The name of the person to greet.
--# @return :: String A greeting message.
--# @example
--# greet("world")
--# -- Returns: "Hello, world!"
--#
--# @export
greet = \(name: String -> String) str_sprintf("Hello, %s!", name)
|}
let package_test_example = {|-- tests/test-{{name}}.t
-- Tests for {{name}}
-- Test: greet function
result = greet("world")
assert(result == "Hello, world!")
|}
(* ================================================================ *)
(* Project Templates *)
(* ================================================================ *)
let project_tproject_toml = {|[project]
name = "{{name}}"
description = "A T data analysis project"
[dependencies]
# T packages this project depends on
# Format: package = { git = "repository-url", tag = "version" }
# Example:
# stats = { git = "https://github.com/t-lang/stats", tag = "{{tlang_tag}}" }
[r-dependencies]
# R packages this project depends on
# Example:
# packages = ["dplyr", "ggplot2"]
packages = []
[py-dependencies]
# Python packages this project depends on
version = "python314"
# Example:
# packages = ["pandas", "numpy"]
packages = []
[visualization-tool]
# Optional plot opener used by `show_plot()`
# Example: command = "xdg-open"
command = ""
[additional-tools]
# Additional Nix packages for the project (e.g., git, awk, jq, quarto)
# If quarto is listed here, run `t update` and then `nix develop` to
# provision `_extensions/tlang` automatically from the T flake so Quarto
# can render `{t}` chunks.
packages = []
[latex]
# LaTeX packages (starts from scheme-small, add packages here)
# Example: packages = ["amsmath", "blindtext"]
packages = []
[t]
# Minimum T language version required
min_version = "{{tlang_version}}"
|}
(* project_flake_nix is generated dynamically by Nix_generator.generate_project_flake
to ensure dependencies are declared as proper flake inputs (locked in flake.lock)
rather than fetched via builtins.fetchGit which fails in pure evaluation mode. *)
let project_readme = {|# {{name}}
A T data analysis project.
## Getting Started
1. Enter the reproducible environment:
```bash
nix develop
```
2. Run the analysis:
```bash
t run src/pipeline.t
```
3. Start the interactive REPL:
```bash
t repl
```
## Project Structure
- `src/` — T source files
- `data/` — Input data files
- `outputs/` — Generated outputs
- `tests/` — Test files
## Dependencies
Dependencies are managed **declaratively** via `tproject.toml`.
To add a new dependency:
1. Add it to the `[dependencies]` section of `tproject.toml`:
```toml
[dependencies]
my-pkg = { git = "https://github.com/user/my-pkg", tag = "v0.1.0" }
```
2. Run `nix develop` — the package is automatically fetched
3. Commit `tproject.toml`
No imperative install commands — `flake.nix` reads `tproject.toml` directly.
## Editor Support
This project includes support for the **T Language Server (LSP)**.
1. Configure your editor following the [Editor Support Guide](https://tstats-project.org/editors.html).
2. Always launch your editor from within the `nix develop` environment (or use `direnv`).
Once active, you'll get autocompletion for T functions, variables, and DataFrame columns (via `$`).
## License
{{license}}
|}
let project_gitignore = {|# Build artifacts
_build/
result
*.exe
# Generated outputs (regenerated from source)
outputs/
# Auto-provisioned Quarto extension copy (from nix develop when quarto is enabled)
_extensions/tlang
# Editor files
*~
.#*
\#*#
.merlin
# OS files
.DS_Store
Thumbs.db
|}
let project_pipeline_example = {|-- {{name}} — main pipeline script
--
-- Run with: t run src/pipeline.t
-- import my_stats
-- import data_utils[read_clean, normalize]
-- p = pipeline {
-- raw = read_csv("data/dataset.csv")
-- clean = read_clean(raw) -- uses imported function
-- normed = normalize(clean) -- uses imported function
-- result = weighted_mean(normed.$x, normed.$w) -- uses imported function
-- }
-- build_pipeline(p)
print("Hello from {{name}} pipeline!")
|}
(* ================================================================ *)
(* Scaffolding Logic *)
(* ================================================================ *)
(* Cache the resolved tlang tag for the duration of the process to avoid
repeated network calls to latest_tlang_tag on every scaffold operation. *)
let resolved_tlang_tag_cache : string option ref = ref None
let resolve_tlang_tag () =
match !resolved_tlang_tag_cache with
| Some tag -> tag
| None ->
(* Prefer the version of the current T binary for scaffolding new projects.
This ensures the generated environment matches the tool used to create it.
Upgrades are handled separately via `t upgrade` which specifically
fetches the latest release from GitHub. *)
let tag = sanitize_tag default_tlang_tag in
resolved_tlang_tag_cache := Some tag;
tag
(*
--# Scaffold a new T package
--#
--# Generates the directory structure and boilerplate files for a new T language package.
--# Creates a `DESCRIPTION.toml`, `flake.nix`, `README.md`, and an initial test setup.
--#
--# @name scaffold_package
--# @param opts :: ScaffoldOptions The options provided via CLI (name, author, license, etc.).
--# @return :: Result[Unit] Ok(()) or an error message.
--# @family package_manager
--# @export
*)
let scaffold_package (opts : scaffold_options) : (unit, string) result =
match validate_name opts.target_name with
| Error msg -> Error msg
| Ok _name ->
let dir = opts.target_name in
(* Check if directory exists *)
if Sys.file_exists dir && not opts.force then
Error (Printf.sprintf "Directory '%s' already exists. Use --force to overwrite." dir)
else begin
let tlang_tag = resolve_tlang_tag () in
let tlang_version = strip_v_prefix tlang_tag in
let ctx = [
("tlang_tag", tlang_tag);
("tlang_version", tlang_version)
] @ Template_engine.context_of_options opts in
let sub = Template_engine.substitute ctx in
(* Create directory structure *)
create_dir dir;
create_dir (Filename.concat dir "src");
create_dir (Filename.concat dir "tests");
create_dir (Filename.concat dir "examples");
create_dir (Filename.concat dir "docs");
create_dir (Filename.concat dir "docs/reference");
create_dir (Filename.concat dir "help");
(* Write files from templates *)
write_file (Filename.concat dir "DESCRIPTION.toml") (sub package_description_toml);
write_file (Filename.concat dir "flake.nix") (sub package_flake_nix);
write_file (Filename.concat dir "README.md") (sub package_readme);
write_file (Filename.concat dir "CHANGELOG.md") (sub package_changelog);
write_file (Filename.concat dir "LICENSE") (Printf.sprintf "Licensed under %s\n\nSee https://spdx.org/licenses/%s.html for full text.\n" opts.license opts.license);
write_file (Filename.concat dir ".gitignore") package_gitignore;
write_file (Filename.concat dir "src/main.t") (sub package_src_example);
write_file (Filename.concat dir (Printf.sprintf "tests/test-%s.t" opts.target_name)) (sub package_test_example);
write_file (Filename.concat dir "docs/index.md") (Printf.sprintf "# %s\n\nPackage documentation.\n" opts.target_name);
(* Agent files *)
let _ = copy_agent_files dir true opts.agent_context in
(* Git init *)
if not opts.no_git then git_init dir;
(* Success message *)
Printf.printf "✓ Package '%s' created successfully!\n\n" opts.target_name;
Printf.printf " %s/\n" dir;
Printf.printf " ├── DESCRIPTION.toml\n";
Printf.printf " ├── flake.nix\n";
Printf.printf " ├── README.md\n";
Printf.printf " ├── CHANGELOG.md\n";
Printf.printf " ├── LICENSE\n";
Printf.printf " ├── .gitignore\n";
Printf.printf " ├── src/\n";
Printf.printf " │ └── main.t\n";
Printf.printf " ├── tests/\n";
Printf.printf " │ └── test-%s.t\n" opts.target_name;
Printf.printf " ├── examples/\n";
Printf.printf " └── docs/\n";
Printf.printf " └── index.md\n";
Printf.printf "\nNext steps:\n";
Printf.printf " cd %s\n" dir;
Printf.printf " nix develop # Enter development shell\n";
Printf.printf " t repl # Start the REPL\n";
Printf.printf " t run src/main.t # Run the example\n";
Printf.printf " t test # Run tests\n";
Ok ()
end
(* ================================================================ *)
(* Interactive Prompts *)
(* ================================================================ *)
let prompt_string label default =
Printf.printf "%s [%s]: " label default;
flush stdout;
let line = try read_line () with End_of_file -> "" in
if String.trim line = "" then default else String.trim line
let interactive_init ?(placeholder="my_package") default_name =
Printf.printf "\nInitializing new T package/project...\n";
let name = if default_name = "" then prompt_string "Name" placeholder else default_name in
let author = prompt_string "Author" (try Sys.getenv "USER" with Not_found -> "Anonymous") in
let license = prompt_string "License [EUPL-1.2, GPL-3.0-or-later, MIT] (visit https://spdx.org/licenses/ for all licenses)" "EUPL-1.2" in
let nixpkgs_date = prompt_string "Nixpkgs date (rstats-on-nix branch)" Version.nixpkgs_date in
let agent_context = prompt_string "Agent Context Level [small, medium, full, huge]" "medium" in
Printf.printf "\n";
{
target_name = name;
author = author;
license = license;
nixpkgs_date = nixpkgs_date;
no_git = false;
force = false;
interactive = true;
agent_context = agent_context;
}
(*
--# Scaffold a new T project
--#
--# Generates the directory structure and boilerplate files for a new T data analysis project.
--# Creates a `tproject.toml`, `flake.nix`, and sets up `data/`, `outputs/`, and `src/` directories.
--#
--# @name scaffold_project
--# @param opts :: ScaffoldOptions The options provided via CLI.
--# @return :: Result[Unit] Ok(()) or an error message.
--# @family package_manager
--# @export
*)
let scaffold_project (opts : scaffold_options) : (unit, string) result =
match validate_name opts.target_name with
| Error msg -> Error msg
| Ok _name ->
let dir = opts.target_name in
(* Check if directory exists *)
if Sys.file_exists dir && not opts.force then
Error (Printf.sprintf "Directory '%s' already exists. Use --force to overwrite." dir)
else begin
let tlang_tag = resolve_tlang_tag () in
let tlang_version = strip_v_prefix tlang_tag in
let ctx = [
("tlang_tag", tlang_tag);
("tlang_version", tlang_version)
] @ Template_engine.context_of_options opts in
let sub = Template_engine.substitute ctx in
(* Create directory structure *)
create_dir dir;
create_dir (Filename.concat dir "src");
create_dir (Filename.concat dir "data");
create_dir (Filename.concat dir "outputs");
create_dir (Filename.concat dir "tests");
(* Write files from templates *)
write_file (Filename.concat dir "tproject.toml") (sub project_tproject_toml);
let flake_content = Nix_generator.generate_project_flake
~project_name:opts.target_name
~nixpkgs_date:opts.nixpkgs_date
~t_version:tlang_version
~deps:[] () in
write_file (Filename.concat dir "flake.nix") flake_content;
write_file (Filename.concat dir "README.md") (sub project_readme);
write_file (Filename.concat dir ".gitignore") project_gitignore;
write_file (Filename.concat dir "src/pipeline.t") (sub project_pipeline_example);
(* Agent files *)
let _ = copy_agent_files dir false opts.agent_context in
(* Git init *)
if not opts.no_git then git_init dir;
(* Success message *)
Printf.printf "✓ Project '%s' created successfully!\n\n" opts.target_name;
Printf.printf " %s/\n" dir;
Printf.printf " ├── tproject.toml\n";
Printf.printf " ├── flake.nix\n";
Printf.printf " ├── README.md\n";
Printf.printf " ├── .gitignore\n";
Printf.printf " ├── src/\n";
Printf.printf " │ └── pipeline.t\n";
Printf.printf " ├── data/\n";
Printf.printf " ├── outputs/\n";
Printf.printf " └── tests/\n";
Printf.printf "\nNext steps:\n";
Printf.printf " cd %s\n" dir;
Printf.printf " nix develop # Enter reproducible environment\n";
Printf.printf " t repl # Start the REPL\n";
Printf.printf " t run src/pipeline.t # Run the pipeline\n";
Printf.printf "\nEditor Setup:\n";
Printf.printf " See https://tstats-project.org/editors.html for LSP configuration.\n";
Ok ()
end
(** Parse CLI flags for init commands.
Returns scaffold_options or an error message. *)
let parse_init_flags (args : string list) : (scaffold_options, string) result =
let name = ref None in
let author = ref "Your Name <email@example.com>" in
let license = ref "EUPL-1.2" in
let nixpkgs_date = ref Version.nixpkgs_date in
let no_git = ref false in
let force = ref false in
let agent_context = ref "medium" in
let show_help = ref false in
let error = ref None in
let rec parse = function
| [] -> ()
| "--author" :: v :: rest -> author := v; parse rest
| "--license" :: v :: rest -> license := v; parse rest
| "--nixpkgs-date" :: v :: rest -> nixpkgs_date := v; parse rest
| "--no-git" :: rest -> no_git := true; parse rest
| "--force" :: rest -> force := true; parse rest
| "--context" :: v :: rest -> agent_context := v; parse rest
| "--interactive" :: rest -> parse rest (* Handled in repl.ml mainly, but we can flag it *)
| "--help" :: _ -> show_help := true
| arg :: rest ->
if String.length arg > 0 && arg.[0] = '-' then
error := Some (Printf.sprintf "Unknown option: %s" arg)
else begin
(match !name with
| None -> name := Some arg
| Some _ -> error := Some (Printf.sprintf "Unexpected argument: %s" arg));
parse rest
end
in
parse args;
if !show_help then
Error ("Usage: t init --package|--project <name> [options]\n\n\
Options:\n\
\ --author <name> Author name and email (default: \"Your Name <email@example.com>\")\n\
\ --license <id> License identifier (default: EUPL-1.2)\n\
\ --nixpkgs-date <YYYY-MM-DD> Nixpkgs branch date (default: " ^ Version.nixpkgs_date ^ ")\n\
\ --no-git Skip git init\n\
\ --force Overwrite existing directory\n\
\ --context <level> Agent context level (small, medium, full, huge; default: medium)\n\
\ --help Show this help\n\
\ --interactive Prompt for options")
else match !error with
| Some msg -> Error msg
| None ->
match !name with
| Some n ->
Ok {
target_name = n;
author = !author;
license = !license;
nixpkgs_date = !nixpkgs_date;
no_git = !no_git;
force = !force;
interactive = List.mem "--interactive" args;
agent_context = !agent_context;
}
| None ->
if List.mem "--interactive" args then
Ok {
target_name = "";
author = !author;
license = !license;
nixpkgs_date = !nixpkgs_date;
no_git = !no_git;
force = !force;
interactive = true;
agent_context = !agent_context;
}
else
Error "Missing name. Usage: t init --package|--project <name>"