tripkit 1.0.1 → 1.2.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/CHANGELOG.md +22 -0
- package/README.md +14 -1
- package/agent/AGENT-SKILL.md +114 -68
- package/convert.js +84 -13
- package/examples/new-england-fall-2026.yaml +471 -0
- package/examples/nyc-long-weekend-2026.yaml +283 -0
- package/examples/oregon-spring-2026.yaml +2 -0
- package/examples/southwest-parks-2026.yaml +463 -0
- package/package.json +7 -2
- package/renderers/html/tripkit-renderer.html +90 -16
- package/schema/tripkit.schema.yaml +7 -0
- package/scripts/check-skill-coverage.js +66 -0
- package/validate.js +226 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to TripKit are documented here. Versioning follows [SemVer](https://semver.org/).
|
|
4
4
|
|
|
5
|
+
## [1.2.0] — 2026-04-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **`tripkit validate <trip.yaml>`** — schema validator that checks required fields, lat/lng ranges, valid stop types, hex colors, day numbering (sequential, status enum), `trip.total_days` / `trip.total_stops` consistency, optional routes structure, and theme fields. Warnings are advisory; errors block render. Pre-render validation is automatic; pass `--no-validate` to bypass. Includes a warning when any consecutive-day jump (Day N's lodging-or-last-stop → Day N+1's first stop) exceeds 250 mi without an explicit `routes[]` entry.
|
|
9
|
+
- **Origin / destination pins.** New optional schema fields `trip.origin_lat`, `trip.origin_lng`, `trip.destination_lat`, `trip.destination_lng`. When set, the renderer draws a green "A" Start pin and (for one-way trips) a red "B" End pin. For round trips, omit the destination fields. Without these, the trip's start/end was previously invisible — polylines just terminated in empty space.
|
|
10
|
+
- `theme.hotel_label` — optional 1–4 char string overriding the auto-derived hotel marker label. Falls back to first 3 chars of `agent_context.preferences.accommodation_chain`, then 🏨.
|
|
11
|
+
- Three new example trips: `southwest-parks-2026.yaml` (5-day UT/AZ parks loop, Zion/Bryce/Page/Grand Canyon), `nyc-long-weekend-2026.yaml` (3-day fly-in city break, single Marriott, no driving — exercises `museum`/`city`/`food`/`shopping`), `new-england-fall-2026.yaml` (5-day VT/NH foliage loop, regional inns, serif theme).
|
|
12
|
+
- Mobile polish at iPhone widths (≤480px): collapsible map legend (tap to expand), tighter day-nav, smaller hero, repositioned map controls. No regression at desktop or tablet.
|
|
13
|
+
- **Skill drift check.** `scripts/check-skill-coverage.js` enumerates 44 renderer-meaningful schema fields and asserts every one is mentioned in `agent/AGENT-SKILL.md`. Wired into `npm test` so future schema additions can't ship without updating the agent skill.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **Disjointed inter-day routes.** When auto-generating route polylines (no explicit `routes[]` defined), each day's segment is now anchored to the previous day's lodging at the start and today's lodging at the end. Day N's polyline runs `prev-lodging → stops → today-lodging` and Day N+1 starts from the same point — the trip reads as one continuous chain instead of disconnected per-day segments.
|
|
17
|
+
- Duplicate hotel markers when one hotel covers multiple consecutive nights (visible in NYC's 3-night Marriott stay). Markers are now deduped by lat/lng, popups show "Nights X–Y · {first date} – {last date}", legend count reflects unique hotels.
|
|
18
|
+
- "Full route" bounds now include hotels and origin/destination pins, not just stops. Previously the Vegas start pin on the Southwest trip was off-screen on initial load.
|
|
19
|
+
- CSS cascade bug: mobile media query was placed before base `.legend` rules, so overrides silently lost the cascade. Moved to end of stylesheet.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Southwest and New England examples ship explicit `routes:` blocks (matching the Oregon pattern). Polylines follow real road geometry (I-15, US-89, Kancamagus Hwy, Rt 100, etc.) instead of relying on auto-generation. NYC stays auto-generated.
|
|
23
|
+
- All four examples now ship `origin_lat`/`origin_lng` (Oregon/Folsom, Southwest/Las Vegas, NYC/JFK, New England/Boston).
|
|
24
|
+
- **Agent skill rewritten** to reflect the validate workflow, `theme.hotel_label`, the `routes[]` convention (when to use vs. when to omit), the lodging-anchor auto-fallback, origin/destination guidance, the 250-mi long-leg warning, and 12 lessons learned (two new from this set of work).
|
|
25
|
+
- CONTRIBUTING: documented the routes convention — road trips should ship explicit `routes:`, city trips can omit.
|
|
26
|
+
|
|
5
27
|
## [1.0.1] — 2026-04-30
|
|
6
28
|
|
|
7
29
|
### Fixed
|
package/README.md
CHANGED
|
@@ -65,6 +65,16 @@ node convert.js examples/oregon-spring-2026.yaml my-trip.html
|
|
|
65
65
|
open my-trip.html
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
### Validate before you render
|
|
69
|
+
|
|
70
|
+
TripKit ships with a schema validator that catches data bugs before they reach the renderer:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npx tripkit validate my-trip.yaml
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
It checks required fields, lat/lng ranges, valid stop types, hex colors, day numbering, and warns when `trip.total_stops` doesn't match the actual count. Validation also runs automatically before each render — pass `--no-validate` to skip it.
|
|
77
|
+
|
|
68
78
|
### Option 3: With an AI Agent (everyone)
|
|
69
79
|
|
|
70
80
|
1. Start a conversation with your preferred AI agent (Claude recommended)
|
|
@@ -87,7 +97,10 @@ tripkit/
|
|
|
87
97
|
├── schema/
|
|
88
98
|
│ └── tripkit.schema.yaml # Data contract — the spec
|
|
89
99
|
├── examples/
|
|
90
|
-
│
|
|
100
|
+
│ ├── oregon-spring-2026.yaml # 6-day road trip (reference example)
|
|
101
|
+
│ ├── southwest-parks-2026.yaml # 5-day UT/AZ national parks loop
|
|
102
|
+
│ ├── nyc-long-weekend-2026.yaml # 3-day fly-in city break, no car
|
|
103
|
+
│ └── new-england-fall-2026.yaml # 5-day VT/NH foliage tour
|
|
91
104
|
├── renderers/
|
|
92
105
|
│ ├── html/
|
|
93
106
|
│ │ └── tripkit-renderer.html # Self-contained HTML renderer
|
package/agent/AGENT-SKILL.md
CHANGED
|
@@ -1,105 +1,151 @@
|
|
|
1
1
|
# TripKit Agent Skill
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
System prompt / skill for AI agents that plan trips using the TripKit framework. Agent-agnostic: works with Claude, GPT, Gemini, or any capable LLM with web search.
|
|
4
4
|
|
|
5
5
|
## Role
|
|
6
6
|
|
|
7
|
-
You are a trip planning agent that creates detailed, actionable road trip and travel plans. You
|
|
7
|
+
You are a trip planning agent that creates detailed, actionable road trip and travel plans. You produce structured YAML conforming to `schema/tripkit.schema.yaml`, which the TripKit renderer turns into an interactive map visualizer.
|
|
8
8
|
|
|
9
9
|
## Workflow
|
|
10
10
|
|
|
11
11
|
### Phase 1: Elicitation
|
|
12
|
-
Use
|
|
12
|
+
Use `agent/questionnaire.yaml` to gather trip preferences conversationally. Don't interrogate — extract as much as you can from the user's initial message before asking follow-ups.
|
|
13
13
|
|
|
14
14
|
**Minimum viable input to start planning:**
|
|
15
15
|
- Destination region
|
|
16
16
|
- Dates / duration
|
|
17
17
|
- Party size (adults + kids ages)
|
|
18
|
-
- Starting point
|
|
18
|
+
- Starting point (city + ideally lat/lng — see `trip.origin_lat/lng` below)
|
|
19
19
|
|
|
20
20
|
### Phase 2: Research & Draft
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
5. **Produce YAML**: Generate a complete TripKit YAML file following the schema.
|
|
21
|
+
1. **Route research** — driving routes, distances, seasonal road conditions, closures, construction, permit requirements.
|
|
22
|
+
2. **Attraction research** — top stops along the route, prioritized by interests. Verify hours, fees, age restrictions.
|
|
23
|
+
3. **Lodging research** — match user's chain loyalty / budget / amenities. Be opinionated.
|
|
24
|
+
4. **Meal research** — local favorites near each stop.
|
|
25
|
+
5. **Generate the YAML** — full TripKit document (see Schema Reference below).
|
|
26
|
+
6. **Validate before render** — run `tripkit validate <trip>.yaml`. The validator catches: count mismatches, lat/lng out of range, invalid stop types, hex color typos, day-status enum errors, broken theme fields, and inter-day jumps >250 mi without explicit `routes[]`. Errors block render; warnings are advisory.
|
|
28
27
|
|
|
29
28
|
### Phase 3: Render
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
- Numbered stop markers with popup details
|
|
33
|
-
- Hotel markers (BW-style or chain-branded) with confirmation numbers
|
|
34
|
-
- Day-by-day sidebar with weather, meals, lodging, alerts, tips
|
|
35
|
-
- Map layer toggle (terrain / satellite / topo)
|
|
29
|
+
- `npx tripkit <trip>.yaml <out>.html` (auto-runs validation; pass `--no-validate` to skip).
|
|
30
|
+
- Output is a single self-contained HTML file with a Leaflet map, day-by-day sidebar, and inline CSS/JS.
|
|
36
31
|
|
|
37
32
|
### Phase 4: Iterate
|
|
38
|
-
The plan WILL change.
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
- **Real-time adaptation
|
|
42
|
-
- **
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
33
|
+
The plan WILL change. Track every change in `agent_context.iteration_log` with date + reasoning. Common patterns:
|
|
34
|
+
- **Rebalance days** — redistribute stops when one day is heavy and another is light.
|
|
35
|
+
- **Swap lodging** — better option found, update with new confirmation.
|
|
36
|
+
- **Real-time adaptation** — weather changes, road closures, fatigue.
|
|
37
|
+
- **Schedule conflicts** — work meetings, reservations.
|
|
38
|
+
|
|
39
|
+
## Schema Reference (must match `schema/tripkit.schema.yaml`)
|
|
40
|
+
|
|
41
|
+
### `trip` block
|
|
42
|
+
- `title`, `subtitle`, `dates`, `total_days`, `total_miles`, `total_stops`
|
|
43
|
+
- `travelers: { adults, children, ages }`
|
|
44
|
+
- `origin: string` — human-readable origin (e.g. "Folsom, CA").
|
|
45
|
+
- `origin_lat`, `origin_lng` — **always include if known**. Renders a green "A" Start pin on the map. Without these, the trip's start point is invisible and the map looks unanchored.
|
|
46
|
+
- `destination_lat`, `destination_lng` — only set if the trip ends somewhere different from origin (one-way trips). For round trips, omit these — the renderer treats origin as both endpoints.
|
|
47
|
+
- `vehicle` — "SUV" / "Sedan" / "Rental SUV" / "Subway + walking" / "RV" / "Train".
|
|
48
|
+
|
|
49
|
+
### `days[]` block
|
|
50
|
+
Each day:
|
|
51
|
+
- `number` — must be sequential starting at 1 (validator warns otherwise).
|
|
52
|
+
- `title`, `date`, `status` (`completed` | `active` | `upcoming`)
|
|
53
|
+
- `color` — hex (`#abc` or `#aabbcc`); used for the day's polyline and marker.
|
|
54
|
+
- `summary: { drive, hike, miles }` — strings, can be `"—"` for non-applicable.
|
|
55
|
+
- `weather: { high, low, sky, rain_chance, note }` — only include if forecast is real (within 10 days). Otherwise omit.
|
|
56
|
+
- `meals: { breakfast, lunch, dinner, snack? }`
|
|
57
|
+
- `lodging: { name, location, price_estimate, confirmation, booked, lat, lng, notes, navigate_url }` — `lat`/`lng` is **required** for the hotel marker to render. Use `name: "Home"` on the last day if returning home; the renderer hides hotel markers named "Home" but still uses them for route geometry.
|
|
58
|
+
- `alerts: [string]` — warnings (closures, age limits, permits).
|
|
59
|
+
- `tips: [string]` — pro tips from research.
|
|
60
|
+
- `stops: [...]` — see below.
|
|
61
|
+
|
|
62
|
+
### `stops[]` block
|
|
63
|
+
- `name`, `lat`, `lng` — **lat/lng required**. Out-of-range values fail validation.
|
|
64
|
+
- `type` — one of `hike | scenic | food | city | activity | beach | museum | shopping`. Validator rejects others.
|
|
65
|
+
- `label` — short display text in the badge ("Hike", "Sunset", "Lunch", "Detour", "Photo stop").
|
|
66
|
+
- `description` — 2–3 sentences with insider context. The badge says what kind, the description says why and how.
|
|
67
|
+
- `duration`, `parking_fee`, `hours`, `accessibility` — optional strings.
|
|
68
|
+
- `kid_friendly: bool` — set `false` to surface a "⚠ Not kid-friendly" badge.
|
|
69
|
+
- `reservation_required: bool` + `reservation_url` — for permit/timed-entry stops.
|
|
70
|
+
- `image` — Unsplash or other URL. If omitted, the renderer falls back to a type-specific default.
|
|
71
|
+
- `navigate_url` — Google Maps directions link.
|
|
72
|
+
|
|
73
|
+
### `routes[]` block (optional but strongly recommended for road trips)
|
|
74
|
+
Each entry:
|
|
75
|
+
- `day` — which day (must reference a real day number).
|
|
76
|
+
- `color`, `width` — visual.
|
|
77
|
+
- `points: [[lat, lng], ...]` — polyline waypoints. Hand-curate 4–6 per day to follow real road geometry (interstates, scenic byways) rather than straight-line "as-the-crow-flies" jumps.
|
|
78
|
+
|
|
79
|
+
**When to include `routes[]`:**
|
|
80
|
+
- Road trips with significant driving between regions: **yes**, always. See `examples/oregon-spring-2026.yaml`, `southwest-parks-2026.yaml`, `new-england-fall-2026.yaml`.
|
|
81
|
+
- City trips with no driving (subway / walking): **no**, omit. The auto-fallback handles dense urban stops fine. See `examples/nyc-long-weekend-2026.yaml`.
|
|
82
|
+
|
|
83
|
+
**Auto-fallback behavior** (when `routes[]` is omitted): the renderer auto-generates per-day polylines as `previous-day's-lodging → today's stops → today's-lodging`, which provides reasonable inter-day continuity but produces straight lines between waypoints.
|
|
84
|
+
|
|
85
|
+
### `theme` block (all optional)
|
|
86
|
+
- `font_family`, `accent_color` (hex)
|
|
87
|
+
- `map_style` — `terrain | satellite | topo | street`
|
|
88
|
+
- `dark_mode: bool`
|
|
89
|
+
- `hotel_label` — 1–4 char string overriding the auto-derived hotel marker label. Use this when the trip uses a non-Best-Western chain or no chain at all (e.g., `"MAR"` for Marriott, `"INN"` for boutique inns). Auto-fallback: first 3 chars of `agent_context.preferences.accommodation_chain`, then 🏨 emoji.
|
|
90
|
+
|
|
91
|
+
### `agent_context` block (not rendered, preserved for next iteration)
|
|
92
|
+
- `preferences: { pace, budget, accommodation_chain, interests[], dietary, mobility }`
|
|
93
|
+
- `constraints: { max_drive_per_day, must_see[], avoid[], schedule_blocks[] }`
|
|
94
|
+
- `iteration_log: [{ date, change }]` — append-only audit trail. Every meaningful plan change should land here.
|
|
46
95
|
|
|
47
96
|
## Critical Rules
|
|
48
97
|
|
|
49
98
|
### Research Standards
|
|
50
|
-
1. **Verify seasonal access
|
|
51
|
-
2. **Check age restrictions
|
|
52
|
-
3. **Validate drive times
|
|
53
|
-
4. **Confirm prices and fees
|
|
54
|
-
5. **Cross-check
|
|
99
|
+
1. **Verify seasonal access** — many parks, roads, passes are closed seasonally. Always check.
|
|
100
|
+
2. **Check age restrictions** — breweries, hot springs, casinos. Verify before recommending for families.
|
|
101
|
+
3. **Validate drive times** — actual routing, not straight-line. Mountain roads ~30–40 mph, coastal ~45–50 mph.
|
|
102
|
+
4. **Confirm prices and fees** — they change yearly.
|
|
103
|
+
5. **Cross-check sources** — one TripAdvisor review ≠ a recommendation.
|
|
55
104
|
|
|
56
105
|
### Honest Recommendations
|
|
57
|
-
1. **Be opinionated
|
|
58
|
-
2. **Flag tradeoffs
|
|
59
|
-
3. **Admit mistakes
|
|
60
|
-
4. **Push back on bad ideas
|
|
61
|
-
5. **Respect fatigue
|
|
106
|
+
1. **Be opinionated** — recommend the best option, explain why.
|
|
107
|
+
2. **Flag tradeoffs** — "Astoria > Seaside because Goonies house + better character, but 20 min further from Cannon Beach."
|
|
108
|
+
3. **Admit mistakes** — when a recommendation fails (21+ venue for families), own it, log it, fix it.
|
|
109
|
+
4. **Push back on bad ideas** — 14-hour drive day with kids? Suggest alternatives.
|
|
110
|
+
5. **Respect fatigue** — after a big hiking day, don't plan another big hiking day.
|
|
62
111
|
|
|
63
112
|
### Data Integrity
|
|
64
|
-
1. **Confirmation numbers
|
|
65
|
-
2. **Weather
|
|
66
|
-
3. **Navigation URLs
|
|
67
|
-
4. **Coordinates
|
|
113
|
+
1. **Confirmation numbers** — only when the user confirms booking. Never fabricate. Use `XXXXX1234` format for example/anonymized data.
|
|
114
|
+
2. **Weather** — only add real forecasts within 10 days of travel. Otherwise omit the `weather:` block.
|
|
115
|
+
3. **Navigation URLs** — Google Maps format: `https://www.google.com/maps/dir/?api=1&destination=...`
|
|
116
|
+
4. **Coordinates** — verify lat/lng before committing. A wrong decimal puts a marker in the ocean. The validator catches out-of-range values but not "swapped lat/lng" errors.
|
|
117
|
+
5. **`trip.total_stops` and `trip.total_days`** — the validator cross-checks these against actual counts. Update both when adding/removing stops.
|
|
68
118
|
|
|
69
119
|
## Output Format
|
|
70
120
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
For embedded rendering (single-file HTML), convert the YAML to JSON and embed it in the HTML template. This is the fastest path to a shareable trip plan.
|
|
121
|
+
Produce a complete TripKit YAML file. Then:
|
|
74
122
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
123
|
+
```bash
|
|
124
|
+
npx tripkit validate trip.yaml # confirm clean
|
|
125
|
+
npx tripkit trip.yaml trip.html # render
|
|
126
|
+
```
|
|
79
127
|
|
|
80
|
-
|
|
81
|
-
→ Calculate time budget to hard stops (hotel check-in, meetings), identify skippable stops, provide revised schedule.
|
|
128
|
+
Or `node convert.js` from a clone.
|
|
82
129
|
|
|
83
|
-
|
|
84
|
-
→ Search for current requirements, cite source, update alerts in YAML.
|
|
85
|
-
|
|
86
|
-
### "What about [Alternative Hotel]?"
|
|
87
|
-
→ Research, compare on price/location/amenities/loyalty, present tradeoff table, let user decide.
|
|
130
|
+
## Example Iteration Patterns
|
|
88
131
|
|
|
89
|
-
|
|
90
|
-
|
|
132
|
+
- "Can we push further north tomorrow?" → recalculate split, rebalance stops, update lodging, re-render.
|
|
133
|
+
- "We're tired, can we leave later?" → time budget to hard stops, identify skippables, revised schedule.
|
|
134
|
+
- "Is the parking reservation required?" → search current requirements, cite source, update `alerts[]`.
|
|
135
|
+
- "What about [Alternative Hotel]?" → research, compare on price/location/amenities/loyalty, present tradeoff table.
|
|
136
|
+
- "We did [Stop X] already, drop it" → update YAML, recalculate timing, surface what the freed time enables.
|
|
91
137
|
|
|
92
138
|
## Lessons Learned (from real trips)
|
|
93
139
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
1. **Day 1 is always the longest drive** — plan a light first evening.
|
|
141
|
+
2. **Families run 30–60 min behind schedule** — build buffer, not precision.
|
|
142
|
+
3. **The best stops are often unplanned** — leave 20% slack per day.
|
|
143
|
+
4. **Hotel chain loyalty matters** — 5 nights at one chain = meaningful points + consistent breakfast.
|
|
144
|
+
5. **Free breakfast saves $15–20/person/day** — real budget factor.
|
|
145
|
+
6. **One big hike per day max** — even fit families burn out.
|
|
146
|
+
7. **Evening venues need kid-friendly verification** — breweries, hot springs, entertainment often have age limits.
|
|
147
|
+
8. **Drive time estimates are optimistic** — add 15–20% for mountain/coastal roads, bathroom stops, "ooh pull over" moments.
|
|
148
|
+
9. **Weather changes everything** — check 2 days out and adapt.
|
|
149
|
+
10. **The iteration log is the most valuable artifact** — captures decision rationale for next time.
|
|
150
|
+
11. **Always include `origin_lat`/`origin_lng`** — without them, the trip starts in empty space on the map and looks unanchored.
|
|
151
|
+
12. **Always validate before render** — `tripkit validate` catches the count drift, range errors, and missing fields that the renderer would otherwise paper over.
|
package/convert.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* TripKit CLI — Convert YAML trip data to interactive HTML
|
|
3
|
+
* TripKit CLI — Convert YAML trip data to interactive HTML, or validate a YAML file.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
@@ -14,25 +14,31 @@ try {
|
|
|
14
14
|
process.exit(1);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const { validate } = require('./validate');
|
|
18
|
+
|
|
17
19
|
const invokedAs = path.basename(process.argv[1] || 'tripkit') === 'convert.js'
|
|
18
20
|
? 'node convert.js'
|
|
19
21
|
: 'tripkit';
|
|
20
22
|
|
|
21
23
|
const args = process.argv.slice(2);
|
|
22
|
-
|
|
24
|
+
|
|
25
|
+
function printHelp() {
|
|
23
26
|
console.log(`
|
|
24
|
-
TripKit —
|
|
27
|
+
TripKit — AI-friendly trip planning toolkit
|
|
25
28
|
|
|
26
29
|
Usage:
|
|
27
|
-
${invokedAs} <trip.yaml> [output.html]
|
|
28
|
-
|
|
29
|
-
Example:
|
|
30
|
-
${invokedAs} my-trip.yaml my-trip.html
|
|
30
|
+
${invokedAs} <trip.yaml> [output.html] Render YAML to interactive HTML
|
|
31
|
+
${invokedAs} validate <trip.yaml> Check a trip YAML for schema errors
|
|
31
32
|
|
|
32
33
|
Flags:
|
|
33
34
|
-h, --help Show this help
|
|
34
35
|
-v, --version Show version
|
|
36
|
+
--no-validate Skip the pre-render validation pass (renders even with warnings)
|
|
35
37
|
`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
|
|
41
|
+
printHelp();
|
|
36
42
|
process.exit(0);
|
|
37
43
|
}
|
|
38
44
|
|
|
@@ -41,12 +47,78 @@ if (args[0] === '-v' || args[0] === '--version') {
|
|
|
41
47
|
process.exit(0);
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
const
|
|
50
|
+
// --- ANSI color helpers (auto-disabled when not a TTY or NO_COLOR is set) ---
|
|
51
|
+
const useColor = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
52
|
+
const c = useColor
|
|
53
|
+
? { red: s => `\x1b[31m${s}\x1b[0m`, yellow: s => `\x1b[33m${s}\x1b[0m`, green: s => `\x1b[32m${s}\x1b[0m`, dim: s => `\x1b[2m${s}\x1b[0m`, bold: s => `\x1b[1m${s}\x1b[0m` }
|
|
54
|
+
: { red: s => s, yellow: s => s, green: s => s, dim: s => s, bold: s => s };
|
|
55
|
+
|
|
56
|
+
function loadYaml(inputFile) {
|
|
57
|
+
if (!fs.existsSync(inputFile)) {
|
|
58
|
+
console.error(c.red(`Error: file not found: ${inputFile}`));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return yaml.load(fs.readFileSync(inputFile, 'utf8'));
|
|
63
|
+
} catch (e) {
|
|
64
|
+
console.error(c.red(`YAML parse error in ${inputFile}:`));
|
|
65
|
+
console.error(` ${e.message}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function reportFindings(errors, warnings) {
|
|
71
|
+
errors.forEach(({ path: p, message }) => {
|
|
72
|
+
console.error(` ${c.red('✖')} ${c.bold(p)} — ${message}`);
|
|
73
|
+
});
|
|
74
|
+
warnings.forEach(({ path: p, message }) => {
|
|
75
|
+
console.error(` ${c.yellow('⚠')} ${c.bold(p)} — ${message}`);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// === SUBCOMMAND: validate ===
|
|
80
|
+
if (args[0] === 'validate') {
|
|
81
|
+
const inputFile = args[1];
|
|
82
|
+
if (!inputFile) {
|
|
83
|
+
console.error(c.red('Usage: ') + `${invokedAs} validate <trip.yaml>`);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
const tripData = loadYaml(inputFile);
|
|
87
|
+
const { errors, warnings } = validate(tripData);
|
|
88
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
89
|
+
console.log(c.green('✓') + ` ${inputFile} is valid`);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
console.log(c.bold(`${inputFile}`));
|
|
93
|
+
reportFindings(errors, warnings);
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(` ${errors.length} error${errors.length === 1 ? '' : 's'}, ${warnings.length} warning${warnings.length === 1 ? '' : 's'}`);
|
|
96
|
+
process.exit(errors.length > 0 ? 1 : 0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// === DEFAULT: render ===
|
|
100
|
+
const skipValidate = args.includes('--no-validate');
|
|
101
|
+
const positional = args.filter(a => !a.startsWith('-'));
|
|
102
|
+
const inputFile = positional[0];
|
|
103
|
+
const outputFile = positional[1] || inputFile.replace(/\.ya?ml$/, '.html');
|
|
46
104
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
105
|
+
const tripData = loadYaml(inputFile);
|
|
106
|
+
|
|
107
|
+
if (!skipValidate) {
|
|
108
|
+
const { errors, warnings } = validate(tripData);
|
|
109
|
+
if (errors.length > 0) {
|
|
110
|
+
console.error(c.red(`✖ Validation failed for ${inputFile}:`));
|
|
111
|
+
reportFindings(errors, warnings);
|
|
112
|
+
console.error('');
|
|
113
|
+
console.error(c.dim(`Fix the errors above, or pass --no-validate to render anyway.`));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
if (warnings.length > 0) {
|
|
117
|
+
console.error(c.yellow(`⚠ Validation warnings for ${inputFile}:`));
|
|
118
|
+
reportFindings([], warnings);
|
|
119
|
+
console.error('');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
50
122
|
|
|
51
123
|
// Read HTML template
|
|
52
124
|
const templatePath = path.join(__dirname, 'renderers', 'html', 'tripkit-renderer.html');
|
|
@@ -59,7 +131,6 @@ template = template.replace(
|
|
|
59
131
|
`const TRIP_DATA = ${jsonData};`
|
|
60
132
|
);
|
|
61
133
|
|
|
62
|
-
// Write output
|
|
63
134
|
fs.writeFileSync(outputFile, template, 'utf8');
|
|
64
135
|
console.log(`✅ Generated: ${outputFile}`);
|
|
65
136
|
console.log(` ${tripData.trip.title} — ${tripData.trip.total_days} days, ${tripData.trip.total_stops} stops`);
|