@trail-cli/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/trail.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { register } from "tsx/esm/api";
3
+
4
+ register();
5
+ await import("../src/cli.ts");
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@trail-cli/cli",
3
+ "version": "0.1.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ndivhomakhuvha/trail.git"
10
+ },
11
+ "type": "module",
12
+ "bin": {
13
+ "trail": "./bin/trail.js"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "src/cli.ts",
18
+ "src/build.ts",
19
+ "src/preview.ts",
20
+ "src/validate.ts"
21
+ ],
22
+ "scripts": {
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "@trail-cli/guide-schema": "workspace:*",
27
+ "commander": "^12.1.0",
28
+ "tsx": "^4.19.1"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^20.14.15",
32
+ "typescript": "^5.5.4"
33
+ }
34
+ }
package/src/build.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { REPO_ROOT, resolveDiscoveredGuides } from "./validate.js";
4
+
5
+ const OUTPUT_ROOT = path.join(REPO_ROOT, "dist", "releases");
6
+
7
+ export interface BuildOptions {
8
+ guidesRoot?: string;
9
+ outputRoot?: string;
10
+ }
11
+
12
+ export function runBuild(options: BuildOptions = {}): void {
13
+ const outputRoot = options.outputRoot ?? OUTPUT_ROOT;
14
+ const guidesRoot = options.guidesRoot;
15
+ const guides = resolveDiscoveredGuides(guidesRoot);
16
+
17
+ if (guides.length === 0) {
18
+ rmSync(outputRoot, { recursive: true, force: true });
19
+ console.log(`No guides found under ${guidesRoot ?? "guides/releases/"}.`);
20
+ return;
21
+ }
22
+
23
+ const failures = guides.filter((result) => !result.success);
24
+
25
+ if (failures.length > 0) {
26
+ for (const result of failures) {
27
+ console.error(`✗ ${result.source.release}/${result.source.file}`);
28
+ for (const issue of result.issues) {
29
+ console.error(` ${issue.path}: ${issue.message}`);
30
+ }
31
+ }
32
+
33
+ console.log("");
34
+ console.error("Build failed: one or more guides are invalid.");
35
+ process.exitCode = 1;
36
+ return;
37
+ }
38
+
39
+ mkdirSync(path.dirname(outputRoot), { recursive: true });
40
+ const stagingRoot = mkdtempSync(`${outputRoot}.tmp-`);
41
+
42
+ for (const result of guides) {
43
+ if (!result.success) continue;
44
+
45
+ const outDir = path.join(stagingRoot, result.source.release);
46
+ mkdirSync(outDir, { recursive: true });
47
+ const outFile = path.join(outDir, `${result.guide.id}.json`);
48
+ writeFileSync(outFile, JSON.stringify(result.guide, null, 2));
49
+ console.log(`✓ built ${result.source.release}/${path.basename(outFile)}`);
50
+ }
51
+
52
+ rmSync(outputRoot, { recursive: true, force: true });
53
+ renameSync(stagingRoot, outputRoot);
54
+
55
+ console.log("");
56
+ console.log(`Built ${guides.length} guide(s) to ${outputRoot}.`);
57
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,25 @@
1
+ import { Command } from "commander";
2
+ import { runValidate } from "./validate.js";
3
+ import { runBuild } from "./build.js";
4
+ import { runPreview } from "./preview.js";
5
+
6
+ const program = new Command();
7
+
8
+ program.name("trail").description("Trail CLI").version("0.1.0");
9
+
10
+ program
11
+ .command("validate")
12
+ .description("Validate all YAML guides under guides/releases/")
13
+ .action(() => runValidate());
14
+
15
+ program
16
+ .command("build")
17
+ .description("Convert YAML guides into static JSON assets under dist/releases/")
18
+ .action(() => runBuild());
19
+
20
+ program
21
+ .command("preview")
22
+ .description("Start a local preview server for the viewer")
23
+ .action(() => runPreview());
24
+
25
+ program.parse(process.argv);
package/src/preview.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { spawn } from "node:child_process";
2
+ import { REPO_ROOT } from "./validate.js";
3
+
4
+ export function runPreview(): void {
5
+ console.log("Starting local preview server...");
6
+ console.log("Example: http://localhost:5173/g/2026.08.26/PAY-1245");
7
+ console.log("");
8
+
9
+ const child = spawn("pnpm", ["--filter", "@trail-cli/viewer", "dev"], {
10
+ cwd: REPO_ROOT,
11
+ stdio: "inherit",
12
+ shell: process.platform === "win32",
13
+ });
14
+
15
+ child.on("exit", (code) => {
16
+ process.exitCode = code ?? 0;
17
+ });
18
+ }
@@ -0,0 +1,137 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import {
5
+ parseAuthorGuideYaml,
6
+ resolveGuide,
7
+ type AuthorGuide,
8
+ type Guide,
9
+ type GuideValidationIssue,
10
+ } from "@trail-cli/guide-schema";
11
+
12
+ const here = path.dirname(fileURLToPath(import.meta.url));
13
+ export const REPO_ROOT = path.resolve(here, "../../../");
14
+ export const GUIDES_ROOT = path.join(REPO_ROOT, "guides", "releases");
15
+
16
+ export interface DiscoveredGuide {
17
+ release: string;
18
+ file: string;
19
+ filePath: string;
20
+ }
21
+
22
+ /** Walks `guides/releases/<release>/*.yaml`. Returns an empty list if the directory doesn't exist. */
23
+ export function discoverGuides(guidesRoot = GUIDES_ROOT): DiscoveredGuide[] {
24
+ if (!existsSync(guidesRoot)) return [];
25
+
26
+ const guides: DiscoveredGuide[] = [];
27
+ const releases = readdirSync(guidesRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory());
28
+
29
+ for (const release of releases) {
30
+ const releaseDir = path.join(guidesRoot, release.name);
31
+ const files = readdirSync(releaseDir).filter((f) => f.endsWith(".yaml") || f.endsWith(".yml"));
32
+ for (const file of files.sort()) {
33
+ guides.push({ release: release.name, file, filePath: path.join(releaseDir, file) });
34
+ }
35
+ }
36
+
37
+ return guides;
38
+ }
39
+
40
+ export interface ValidationResult {
41
+ file: string;
42
+ success: boolean;
43
+ issues?: GuideValidationIssue[];
44
+ }
45
+
46
+ export type ResolvedGuideResult =
47
+ | { source: DiscoveredGuide; success: true; guide: Guide }
48
+ | { source: DiscoveredGuide; success: false; issues: GuideValidationIssue[] };
49
+
50
+ /** Parses and resolves all guides per release, so extensions can only reference local bases. */
51
+ export function resolveDiscoveredGuides(guidesRoot = GUIDES_ROOT): ResolvedGuideResult[] {
52
+ const parsed = discoverGuides(guidesRoot).map((source) => ({
53
+ source,
54
+ result: parseAuthorGuideYaml(readFileSync(source.filePath, "utf-8")),
55
+ }));
56
+ const releases = new Map<string, typeof parsed>();
57
+ for (const entry of parsed) {
58
+ const release = releases.get(entry.source.release) ?? [];
59
+ release.push(entry);
60
+ releases.set(entry.source.release, release);
61
+ }
62
+
63
+ const results: ResolvedGuideResult[] = [];
64
+ for (const [release, entries] of releases) {
65
+ const guidesById = new Map<string, AuthorGuide>();
66
+ const duplicateIds = new Set<string>();
67
+ for (const { result } of entries) {
68
+ if (!result.success) continue;
69
+ if (guidesById.has(result.guide.id)) duplicateIds.add(result.guide.id);
70
+ else guidesById.set(result.guide.id, result.guide);
71
+ }
72
+
73
+ for (const { source, result } of entries) {
74
+ if (!result.success) {
75
+ results.push({ source, success: false, issues: result.issues });
76
+ } else if (duplicateIds.has(result.guide.id)) {
77
+ results.push({
78
+ source,
79
+ success: false,
80
+ issues: [{ path: "id", message: `Duplicate guide id in release ${release}: ${result.guide.id}.` }],
81
+ });
82
+ } else {
83
+ const resolved = resolveGuide(result.guide, guidesById);
84
+ results.push(
85
+ resolved.success
86
+ ? { source, success: true, guide: resolved.guide }
87
+ : { source, success: false, issues: resolved.issues },
88
+ );
89
+ }
90
+ }
91
+ }
92
+ return results;
93
+ }
94
+
95
+ export function validateGuides(): { results: ValidationResult[] } {
96
+ const results = resolveDiscoveredGuides().map((result): ValidationResult => {
97
+ const file = `${result.source.release}/${result.source.file}`;
98
+ return result.success ? { file, success: true } : { file, success: false, issues: result.issues };
99
+ });
100
+ return { results };
101
+ }
102
+
103
+ export function runValidate(): void {
104
+ const { results } = validateGuides();
105
+
106
+ if (results.length === 0) {
107
+ console.log("No guides found under guides/releases/.");
108
+ return;
109
+ }
110
+
111
+ const failures = results.filter((r) => !r.success);
112
+
113
+ for (const result of results) {
114
+ if (result.success) {
115
+ console.log(`✓ ${result.file}`);
116
+ }
117
+ }
118
+
119
+ if (failures.length > 0) {
120
+ for (const failure of failures) {
121
+ console.log("");
122
+ console.log(`✗ ${failure.file}`);
123
+ console.log("");
124
+ for (const issue of failure.issues ?? []) {
125
+ console.log(`${issue.path}:`);
126
+ console.log(issue.message);
127
+ }
128
+ }
129
+ console.log("");
130
+ console.error(`${failures.length} of ${results.length} guide(s) invalid.`);
131
+ process.exitCode = 1;
132
+ return;
133
+ }
134
+
135
+ console.log("");
136
+ console.log(`${results.length} guides valid.`);
137
+ }