yamchart 0.7.2 → 0.8.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/CLAUDE.md +51 -0
- package/dist/{advisor-5BTRAICH.js → advisor-57BOMUUF.js} +11 -10
- package/dist/{advisor-5BTRAICH.js.map → advisor-57BOMUUF.js.map} +1 -1
- package/dist/{chunk-5VA43CTW.js → chunk-E2UZXDF6.js} +37 -42
- package/dist/chunk-E2UZXDF6.js.map +1 -0
- package/dist/{chunk-SSUVEADJ.js → chunk-JYQKDWLG.js} +76 -14
- package/dist/chunk-JYQKDWLG.js.map +1 -0
- package/dist/{chunk-565OEBW7.js → chunk-OTAUP5RC.js} +3 -3
- package/dist/chunk-UND73EOB.js +449 -0
- package/dist/chunk-UND73EOB.js.map +1 -0
- package/dist/{chunk-UDRJFEJU.js → chunk-X6LQGWUX.js} +349 -176
- package/dist/chunk-X6LQGWUX.js.map +1 -0
- package/dist/{connection-utils-AGSQ5HNN.js → connection-utils-FTSZU2VF.js} +5 -4
- package/dist/{describe-AA4UT4U6.js → describe-TGIOBNJB.js} +5 -4
- package/dist/{describe-AA4UT4U6.js.map → describe-TGIOBNJB.js.map} +1 -1
- package/dist/{dev-EUWIRL4N.js → dev-MRZ76V74.js} +485 -44
- package/dist/dev-MRZ76V74.js.map +1 -0
- package/dist/{dist-LZNDQDH3.js → dist-PVHFUYP2.js} +23 -3
- package/dist/{dist-YTGUIBKG.js → dist-WDTDQDTX.js} +72 -1
- package/dist/dist-WDTDQDTX.js.map +1 -0
- package/dist/index.js +135 -17
- package/dist/index.js.map +1 -1
- package/dist/{init-CI4VARQG.js → init-UYQE5DPU.js} +2 -2
- package/dist/{init-CI4VARQG.js.map → init-UYQE5DPU.js.map} +1 -1
- package/dist/public/assets/EventManagement-BlxJ2TFw.js +18 -0
- package/dist/public/assets/{LoginPage-BwMmpq8e.js → LoginPage-BT8ikmPn.js} +1 -1
- package/dist/public/assets/PublicViewer-B4uFxgbt.js +1 -0
- package/dist/public/assets/{SetupWizard-DU8R2dPp.js → SetupWizard-njrOhCzw.js} +1 -1
- package/dist/public/assets/ShareManagement-Bg16oJhW.js +1 -0
- package/dist/public/assets/UserManagement-tiCIT4UY.js +1 -0
- package/dist/public/assets/index-B41yj3io.js +187 -0
- package/dist/public/assets/{index-C0hWblBI.css → index-BZ25r23j.css} +1 -1
- package/dist/public/assets/{index.es-YeJujQ5o.js → index.es-BuktD_R2.js} +1 -1
- package/dist/public/assets/{jspdf.es.min-BzZgn3ka.js → jspdf.es.min-DFRl2hZQ.js} +3 -3
- package/dist/public/index.html +2 -2
- package/dist/{query-THDVBBVZ.js → query-JRMMNXX6.js} +5 -4
- package/dist/{query-THDVBBVZ.js.map → query-JRMMNXX6.js.map} +1 -1
- package/dist/{sample-6WPQS7PA.js → sample-VGIY4U4J.js} +5 -4
- package/dist/{sample-6WPQS7PA.js.map → sample-VGIY4U4J.js.map} +1 -1
- package/dist/{search-657DXBRS.js → search-QSYNG4SR.js} +5 -4
- package/dist/{search-657DXBRS.js.map → search-QSYNG4SR.js.map} +1 -1
- package/dist/semantic-RAP3S5PQ.js +39 -0
- package/dist/semantic-RAP3S5PQ.js.map +1 -0
- package/dist/{sync-warehouse-4KBV5S3L.js → sync-warehouse-XHTBZH25.js} +5 -4
- package/dist/{sync-warehouse-4KBV5S3L.js.map → sync-warehouse-XHTBZH25.js.map} +1 -1
- package/dist/{tables-KLDBUUSE.js → tables-UOO342TA.js} +5 -4
- package/dist/{tables-KLDBUUSE.js.map → tables-UOO342TA.js.map} +1 -1
- package/dist/templates/default/.claude/skills/databricks/SKILL.md +76 -0
- package/dist/templates/default/.claude/skills/dbt-advisor/SKILL.md +107 -0
- package/dist/templates/default/.claude/skills/duckdb/SKILL.md +67 -0
- package/dist/templates/default/.claude/skills/mysql/SKILL.md +69 -0
- package/dist/templates/default/.claude/skills/postgres/SKILL.md +69 -0
- package/dist/templates/default/.claude/skills/snowflake/SKILL.md +64 -0
- package/dist/templates/default/CLAUDE.md +7 -0
- package/dist/templates/default/docs/yamchart-reference.md +222 -1
- package/dist/{test-JSAWS5ZP.js → test-TXRZWNXK.js} +5 -4
- package/dist/{test-JSAWS5ZP.js.map → test-TXRZWNXK.js.map} +1 -1
- package/dist/update-UKMEWCSO.js +220 -0
- package/dist/update-UKMEWCSO.js.map +1 -0
- package/package.json +5 -3
- package/dist/chunk-5VA43CTW.js.map +0 -1
- package/dist/chunk-SSUVEADJ.js.map +0 -1
- package/dist/chunk-UDRJFEJU.js.map +0 -1
- package/dist/dev-EUWIRL4N.js.map +0 -1
- package/dist/dist-YTGUIBKG.js.map +0 -1
- package/dist/public/assets/PublicViewer-vIDojjIR.js +0 -1
- package/dist/public/assets/ShareManagement-7iS6lM2T.js +0 -1
- package/dist/public/assets/UserManagement-By7YRZrF.js +0 -1
- package/dist/public/assets/index-CXx1PiRF.js +0 -174
- package/dist/update-HCR6MYJX.js +0 -88
- package/dist/update-HCR6MYJX.js.map +0 -1
- /package/dist/{chunk-565OEBW7.js.map → chunk-OTAUP5RC.js.map} +0 -0
- /package/dist/{connection-utils-AGSQ5HNN.js.map → connection-utils-FTSZU2VF.js.map} +0 -0
- /package/dist/{dist-LZNDQDH3.js.map → dist-PVHFUYP2.js.map} +0 -0
|
@@ -71,6 +71,10 @@ yamchart sync-warehouse --json # Output sync summary as JSON
|
|
|
71
71
|
yamchart lineage <model> # Show upstream dependency tree
|
|
72
72
|
yamchart lineage <model> --depth 2 # Limit recursion depth
|
|
73
73
|
yamchart lineage <model> --json # JSON output
|
|
74
|
+
yamchart semantic # List semantic layer entities
|
|
75
|
+
yamchart semantic --json # JSON output
|
|
76
|
+
yamchart semantic query -e <entity> -m <measures> # Structured query
|
|
77
|
+
yamchart semantic query -e <entity> -m <measures> --sql # SQL only
|
|
74
78
|
```
|
|
75
79
|
|
|
76
80
|
## SQL Models (`models/*.sql`)
|
|
@@ -264,6 +268,28 @@ Column headers are automatically humanized (`total_revenue` → "Total Revenue")
|
|
|
264
268
|
**Summary row:** Per-column `summary` with `sum`, `avg`, `min`, `max`, or `count`. Computed client-side, formatted with the column's `format`, sticky at the bottom.
|
|
265
269
|
**Default sort:** `sort: { field: revenue, direction: desc }` sets the initial sort column and direction. Users can still click headers to change sorting interactively.
|
|
266
270
|
|
|
271
|
+
**Pivot table** — transform flat query results into grouped, pivoted tables with subtotals and heatmap coloring:
|
|
272
|
+
```yaml
|
|
273
|
+
chart:
|
|
274
|
+
type: table
|
|
275
|
+
pivot:
|
|
276
|
+
rows: [plan, event] # Row grouping fields (first = group, rest = detail)
|
|
277
|
+
columns: quarter # Field whose distinct values become column headers
|
|
278
|
+
values:
|
|
279
|
+
- field: event_count
|
|
280
|
+
aggregate: sum # sum | avg | min | max | count (default: sum)
|
|
281
|
+
format: "$" # Optional number format
|
|
282
|
+
heatmap: true # Enable green heatmap coloring (optional, off by default)
|
|
283
|
+
subtotals: true # Per-group subtotal rows (default: true)
|
|
284
|
+
grandTotals: true # Grand total row (default: true)
|
|
285
|
+
heatmap:
|
|
286
|
+
color: "#4CAF50" # Heatmap color (default: green)
|
|
287
|
+
scope: column # column | global — scale per-column or across all
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The SQL model should return flat rows with all fields (e.g. `SELECT plan, event, quarter, count(*) as event_count FROM events GROUP BY plan, event, quarter`). Yamchart pivots client-side.
|
|
291
|
+
Row groups are collapsible (click to toggle). Heatmap coloring auto-scales per column by default.
|
|
292
|
+
|
|
267
293
|
**Combo** — mixed types with dual y-axes:
|
|
268
294
|
```yaml
|
|
269
295
|
chart:
|
|
@@ -357,7 +383,7 @@ parameters:
|
|
|
357
383
|
|
|
358
384
|
### Drill-Down
|
|
359
385
|
|
|
360
|
-
|
|
386
|
+
Click any chart element to open a context menu with options to cross-filter, view detail data, or navigate to a related chart.
|
|
361
387
|
|
|
362
388
|
```yaml
|
|
363
389
|
drillDown:
|
|
@@ -772,6 +798,77 @@ fct_revenue
|
|
|
772
798
|
|
|
773
799
|
**Note:** dbt lineage data requires `sync-dbt` with a dbt project that has run `dbt compile` or `dbt run` (which generates `target/manifest.json`). If the manifest is missing, `sync-dbt` will warn you. Yamchart model lineage works independently by parsing SQL files directly.
|
|
774
800
|
|
|
801
|
+
## Semantic Layer
|
|
802
|
+
|
|
803
|
+
The semantic layer auto-generates structured query entities from your catalog. It classifies columns as measures (numeric, aggregatable) or dimensions (categorical, temporal), enabling structured queries without writing SQL.
|
|
804
|
+
|
|
805
|
+
### How It Works
|
|
806
|
+
|
|
807
|
+
1. Run `yamchart sync-dbt` or `yamchart sync-warehouse` to populate `.yamchart/catalog.json`
|
|
808
|
+
2. The semantic layer automatically builds entities from gold/marts/core tables (or tables prefixed with `dim_`, `fct_`, `fact_`)
|
|
809
|
+
3. Query entities using the CLI or the AI advisor
|
|
810
|
+
|
|
811
|
+
### Configuration
|
|
812
|
+
|
|
813
|
+
Optional in `yamchart.yaml`:
|
|
814
|
+
|
|
815
|
+
```yaml
|
|
816
|
+
semantic:
|
|
817
|
+
include:
|
|
818
|
+
paths: [gold/, marts/, core/] # Catalog paths to include (default)
|
|
819
|
+
prefixes: [dim_, fct_, fact_] # Table name prefixes to include (default)
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
### CLI Commands
|
|
823
|
+
|
|
824
|
+
```bash
|
|
825
|
+
yamchart semantic # List all semantic entities
|
|
826
|
+
yamchart semantic --json # JSON output
|
|
827
|
+
|
|
828
|
+
yamchart semantic query -e fct_revenue -m revenue,order_count -d region
|
|
829
|
+
yamchart semantic query -e fct_revenue -m revenue -f region=US --limit 50
|
|
830
|
+
yamchart semantic query -e fct_revenue -m revenue -d region --order revenue --desc
|
|
831
|
+
yamchart semantic query -e fct_revenue -m revenue --sql # Print SQL only
|
|
832
|
+
yamchart semantic query -e fct_revenue -m revenue --json # JSON output
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
| Option | Description |
|
|
836
|
+
|--------|-------------|
|
|
837
|
+
| `-e, --entity <name>` | Entity name (required) |
|
|
838
|
+
| `-m, --measures <m1,m2>` | Comma-separated measures (required) |
|
|
839
|
+
| `-d, --dimensions <d1,d2>` | Comma-separated dimensions |
|
|
840
|
+
| `-f, --filter <dim=value>` | Filter (repeatable) |
|
|
841
|
+
| `--order <field>` | Order by field |
|
|
842
|
+
| `--desc` | Descending order |
|
|
843
|
+
| `-l, --limit <n>` | Row limit (default: 100) |
|
|
844
|
+
| `-c, --connection <name>` | Override connection |
|
|
845
|
+
| `--sql` | Print compiled SQL without executing |
|
|
846
|
+
| `--json` | JSON output |
|
|
847
|
+
|
|
848
|
+
### Supported Aggregations
|
|
849
|
+
|
|
850
|
+
| Type | SQL Generated |
|
|
851
|
+
|------|--------------|
|
|
852
|
+
| `sum` | `SUM("column")` |
|
|
853
|
+
| `count` | `COUNT("column")` |
|
|
854
|
+
| `avg` | `AVG("column")` |
|
|
855
|
+
| `min` | `MIN("column")` |
|
|
856
|
+
| `max` | `MAX("column")` |
|
|
857
|
+
| `count_distinct` | `COUNT(DISTINCT "column")` |
|
|
858
|
+
|
|
859
|
+
### Advisor Integration
|
|
860
|
+
|
|
861
|
+
The AI advisor can discover and query the semantic layer:
|
|
862
|
+
|
|
863
|
+
- **`list_semantic_entities`** — Lists all entities with measures and dimensions
|
|
864
|
+
- **`semantic_query`** — Compiles a structured query to SQL
|
|
865
|
+
|
|
866
|
+
```bash
|
|
867
|
+
yamchart advisor "What is total revenue by region?"
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
The advisor uses `list_semantic_entities` to discover available fields, then `semantic_query` to compile and execute the query.
|
|
871
|
+
|
|
775
872
|
## Axis Types
|
|
776
873
|
|
|
777
874
|
| Type | Use | Example |
|
|
@@ -781,6 +878,130 @@ fct_revenue
|
|
|
781
878
|
| `nominal` | Unordered categories | `Electronics` |
|
|
782
879
|
| `quantitative` | Numbers | `250.5` |
|
|
783
880
|
|
|
881
|
+
## Chart Events (`events.yaml`)
|
|
882
|
+
|
|
883
|
+
Annotate time-series charts with notable dates — product launches, outages, campaigns, etc. Events appear as dashed vertical lines (point events) or shaded regions (range events) on line, bar, area, and combo charts.
|
|
884
|
+
|
|
885
|
+
Create `events.yaml` in your project root:
|
|
886
|
+
|
|
887
|
+
```yaml
|
|
888
|
+
events:
|
|
889
|
+
- date: "2025-01-15"
|
|
890
|
+
label: "V2 Launch"
|
|
891
|
+
description: "Shipped new pricing page"
|
|
892
|
+
color: "#10b981"
|
|
893
|
+
|
|
894
|
+
- date: "2025-02-01"
|
|
895
|
+
endDate: "2025-02-14"
|
|
896
|
+
label: "Promo Campaign"
|
|
897
|
+
color: "#3b82f6"
|
|
898
|
+
|
|
899
|
+
- date: "2025-03-01"
|
|
900
|
+
label: "Outage"
|
|
901
|
+
charts: [revenue-trend, orders-trend] # Scope to specific charts
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
| Field | Required | Description |
|
|
905
|
+
|-------|----------|-------------|
|
|
906
|
+
| `date` | Yes | Event start date (ISO format) |
|
|
907
|
+
| `endDate` | No | Event end date — creates a shaded range instead of a line |
|
|
908
|
+
| `label` | Yes | Short label shown on the chart |
|
|
909
|
+
| `description` | No | Longer description shown in tooltip and management UI |
|
|
910
|
+
| `color` | No | Hex color for the line/region (default: theme text color) |
|
|
911
|
+
| `charts` | No | Array of chart names — limits which charts show this event. Omit for all charts. |
|
|
912
|
+
|
|
913
|
+
**Point events** (no `endDate`) render as dashed vertical lines. **Range events** (with `endDate`) render as semi-transparent shaded regions.
|
|
914
|
+
|
|
915
|
+
**Management UI:** Access via the events icon in the top bar. Add, edit, and delete events with inline editing, date pickers, and chart scope selection.
|
|
916
|
+
|
|
917
|
+
**Date clamping:** Events outside the chart's visible date range are automatically clamped to the chart boundaries, so a range extending beyond the data still renders correctly.
|
|
918
|
+
|
|
919
|
+
## Chart Goals
|
|
920
|
+
|
|
921
|
+
Add target lines, cumulative targets, or model-driven goal lines to line, bar, area, and combo charts. KPI and gauge charts support simple target values with progress display.
|
|
922
|
+
|
|
923
|
+
### Fixed Goal — constant horizontal target line
|
|
924
|
+
|
|
925
|
+
```yaml
|
|
926
|
+
chart:
|
|
927
|
+
type: bar
|
|
928
|
+
x: { field: month, type: temporal }
|
|
929
|
+
y: { field: revenue, format: "$,.0f" }
|
|
930
|
+
goals:
|
|
931
|
+
- type: fixed
|
|
932
|
+
value: 100000
|
|
933
|
+
label: "Monthly Target"
|
|
934
|
+
color: "#22c55e"
|
|
935
|
+
style: dashed # solid, dashed, or dotted
|
|
936
|
+
zone:
|
|
937
|
+
tolerance: 0.10 # Shaded band at ±10% of target
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### Cumulative Goal — target value to reach by a deadline
|
|
941
|
+
|
|
942
|
+
```yaml
|
|
943
|
+
goals:
|
|
944
|
+
- type: cumulative
|
|
945
|
+
value: 1000000 # Target to reach
|
|
946
|
+
by: "2025-12-31" # Deadline date
|
|
947
|
+
from: "2025-01-01" # Optional start date (defaults to first data point)
|
|
948
|
+
label: "Annual Target"
|
|
949
|
+
color: "#f59e0b"
|
|
950
|
+
style: dashed
|
|
951
|
+
zone:
|
|
952
|
+
tolerance: 0.05
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
Renders as a diagonal line from `from` (or chart start) to `by`, showing the ideal pace toward the target.
|
|
956
|
+
|
|
957
|
+
### Model Goal — target values from a SQL model
|
|
958
|
+
|
|
959
|
+
```yaml
|
|
960
|
+
goals:
|
|
961
|
+
- type: model
|
|
962
|
+
source:
|
|
963
|
+
model: revenue_targets # SQL model name
|
|
964
|
+
value_field: target # Column with target values
|
|
965
|
+
date_field: month # Optional date column (aligns to x-axis)
|
|
966
|
+
label: "Plan"
|
|
967
|
+
color: "#8b5cf6"
|
|
968
|
+
style: dotted
|
|
969
|
+
zone:
|
|
970
|
+
tolerance: 0.10
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
The model is queried alongside the chart data. Each row provides a target value, plotted as a goal line that can vary over time.
|
|
974
|
+
|
|
975
|
+
### KPI / Gauge Goal — target value with progress
|
|
976
|
+
|
|
977
|
+
```yaml
|
|
978
|
+
# KPI chart
|
|
979
|
+
chart:
|
|
980
|
+
type: kpi
|
|
981
|
+
value: { field: revenue }
|
|
982
|
+
format: { type: currency, currency: USD }
|
|
983
|
+
goal:
|
|
984
|
+
value: 500000
|
|
985
|
+
label: "Q1 Target"
|
|
986
|
+
show_progress: true # Shows progress bar and percentage
|
|
987
|
+
color: "#22c55e"
|
|
988
|
+
|
|
989
|
+
# Gauge chart
|
|
990
|
+
chart:
|
|
991
|
+
type: gauge
|
|
992
|
+
value: { field: score }
|
|
993
|
+
min: 0
|
|
994
|
+
max: 100
|
|
995
|
+
goal:
|
|
996
|
+
value: 85
|
|
997
|
+
label: "Target"
|
|
998
|
+
show_progress: true
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
### Goal Zones
|
|
1002
|
+
|
|
1003
|
+
All goal types (fixed, cumulative, model) support a `zone` with `tolerance` (0–1). This renders a semi-transparent shaded band around the goal line at ±tolerance of the target value. For example, `tolerance: 0.10` on a target of 100,000 shades the 90,000–110,000 range.
|
|
1004
|
+
|
|
784
1005
|
## Number Formats (d3-format)
|
|
785
1006
|
|
|
786
1007
|
| Format | Output | Use |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createConnector,
|
|
3
3
|
resolveConnection
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-OTAUP5RC.js";
|
|
5
5
|
import {
|
|
6
6
|
detail,
|
|
7
7
|
error,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
success,
|
|
11
11
|
warning
|
|
12
12
|
} from "./chunk-HJVVHYVN.js";
|
|
13
|
-
import "./chunk-
|
|
13
|
+
import "./chunk-X6LQGWUX.js";
|
|
14
14
|
import {
|
|
15
15
|
createTemplateContext,
|
|
16
16
|
expandDatePreset,
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
parseModelMetadata,
|
|
19
19
|
renderTemplate,
|
|
20
20
|
runAll
|
|
21
|
-
} from "./chunk-
|
|
21
|
+
} from "./chunk-JYQKDWLG.js";
|
|
22
|
+
import "./chunk-UND73EOB.js";
|
|
22
23
|
import "./chunk-DGUM43GV.js";
|
|
23
24
|
|
|
24
25
|
// src/commands/test.ts
|
|
@@ -191,4 +192,4 @@ export {
|
|
|
191
192
|
formatTestOutput,
|
|
192
193
|
testProject
|
|
193
194
|
};
|
|
194
|
-
//# sourceMappingURL=test-
|
|
195
|
+
//# sourceMappingURL=test-TXRZWNXK.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/commands/test.ts"],"sourcesContent":["import { readFile, readdir } from 'fs/promises';\nimport { join, extname } from 'path';\nimport {\n parseModelMetadata,\n runAll,\n renderTemplate,\n createTemplateContext,\n expandDatePreset,\n isDatePreset,\n type TestSuiteResult,\n type TestModelInput,\n} from '@yamchart/query';\nimport type { ModelMetadata } from '@yamchart/schema';\nimport * as output from '../utils/output.js';\nimport { resolveConnection, createConnector } from './connection-utils.js';\n\nexport interface TestOptions {\n connection?: string;\n json?: boolean;\n}\n\nexport interface TestResult {\n success: boolean;\n suite: TestSuiteResult;\n connectionName: string;\n}\n\nexport async function testProject(\n projectDir: string,\n modelFilter: string | undefined,\n options: TestOptions,\n): Promise<TestResult> {\n const connection = await resolveConnection(projectDir, options.connection);\n const connector = createConnector(connection, projectDir);\n\n const modelsDir = join(projectDir, 'models');\n const allModels = await loadModels(modelsDir);\n\n let modelsToTest = allModels;\n if (modelFilter) {\n modelsToTest = allModels.filter((m) => m.name === modelFilter);\n if (modelsToTest.length === 0) {\n throw new Error(`Model \"${modelFilter}\" not found`);\n }\n }\n\n // Build refs map: model name -> model name (identity for standalone execution)\n const refs: Record<string, string> = {};\n for (const m of allModels) {\n refs[m.name] = m.name;\n }\n\n const testInputs: TestModelInput[] = [];\n for (const model of modelsToTest) {\n try {\n const compiledSql = compileWithDefaults(model.sql, model.metadata, refs);\n testInputs.push({ compiledSql, metadata: model.metadata });\n } catch {\n // If compilation fails, include raw SQL so the error is reported during test execution\n testInputs.push({ compiledSql: model.sql, metadata: model.metadata });\n }\n }\n\n try {\n await connector.connect();\n const suite = await runAll(testInputs, connector);\n return { success: suite.failed === 0, suite, connectionName: connection.name };\n } finally {\n await connector.disconnect();\n }\n}\n\nfunction resolveDynamicDefault(value: string): string {\n const trimmed = value.trim().toLowerCase();\n if (\n trimmed === 'current_date()' ||\n trimmed === 'current_date' ||\n trimmed === 'now()' ||\n trimmed.includes('current_date')\n ) {\n return formatISODate(new Date());\n }\n return value;\n}\n\nfunction formatISODate(date: Date): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, '0');\n const d = String(date.getDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\nfunction compileWithDefaults(\n sql: string,\n metadata: ModelMetadata,\n refs: Record<string, string>,\n): string {\n const params: Record<string, unknown> = {};\n if (metadata.params) {\n for (const p of metadata.params) {\n if (p.default !== undefined) {\n params[p.name] = resolveDynamicDefault(p.default);\n }\n }\n }\n\n // Expand date presets into start_date/end_date\n if (typeof params.date_range === 'string' && isDatePreset(params.date_range)) {\n const range = expandDatePreset(params.date_range);\n if (range) {\n params.start_date = range.start_date;\n params.end_date = range.end_date;\n }\n }\n\n const context = createTemplateContext(params, refs);\n return renderTemplate(sql, context);\n}\n\ninterface LoadedModel {\n name: string;\n sql: string;\n metadata: ModelMetadata;\n}\n\nasync function loadModels(dir: string): Promise<LoadedModel[]> {\n const models: LoadedModel[] = [];\n\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return models;\n }\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n const subModels = await loadModels(fullPath);\n models.push(...subModels);\n } else if (extname(entry.name) === '.sql') {\n const content = await readFile(fullPath, 'utf-8');\n try {\n const parsed = parseModelMetadata(content);\n models.push({ name: parsed.name, sql: parsed.sql, metadata: parsed });\n } catch {\n // Skip unparseable models\n }\n }\n }\n\n return models;\n}\n\nexport function formatTestOutput(result: TestResult, connectionName: string): void {\n const { suite } = result;\n\n const testedModels = suite.models.filter(\n (m) => m.schemaCheck || m.assertions.length > 0 || m.error,\n );\n\n output.header(`Testing ${suite.models.length} model(s) against ${connectionName}...`);\n\n for (const model of suite.models) {\n const hasChecks = model.schemaCheck || model.assertions.length > 0 || model.error;\n if (!hasChecks) continue;\n\n console.log(` ${model.modelName}`);\n\n if (model.error) {\n output.error(` error: ${model.error}`);\n continue;\n }\n\n if (model.schemaCheck) {\n formatSchemaCheck(model.schemaCheck);\n }\n\n for (const assertion of model.assertions) {\n formatAssertion(assertion);\n }\n\n output.newline();\n }\n\n // Summary line\n const totalChecks = suite.passed + suite.failed;\n const summary = `${testedModels.length} model(s), ${totalChecks} check(s): ${suite.passed} passed, ${suite.failed} failed`;\n const skippedSuffix = suite.skipped > 0 ? ` (${suite.skipped} skipped)` : '';\n const durationSuffix = ` (${suite.durationMs.toFixed(0)}ms)`;\n\n if (suite.failed === 0) {\n output.success(`${summary}${skippedSuffix}${durationSuffix}`);\n } else {\n output.error(`${summary}${skippedSuffix}${durationSuffix}`);\n }\n}\n\nfunction formatSchemaCheck(check: NonNullable<TestSuiteResult['models'][0]['schemaCheck']>): void {\n if (check.passed) {\n output.success(` schema: returns expected columns (${check.expectedColumns.join(', ')})`);\n return;\n }\n\n const issues: string[] = [];\n if (check.missingColumns.length > 0) {\n issues.push(`missing: ${check.missingColumns.join(', ')}`);\n }\n for (const m of check.typeMismatches) {\n issues.push(`${m.column}: expected ${m.expected}, got ${m.actual}`);\n }\n output.error(` schema: ${issues.join('; ')}`);\n}\n\nfunction formatAssertion(assertion: TestSuiteResult['models'][0]['assertions'][0]): void {\n const label = extractAssertionLabel(assertion.sql);\n\n if (assertion.warning) {\n output.warning(` ${label}`);\n output.detail(` ${assertion.warning}`);\n }\n\n if (assertion.error) {\n output.error(` test: ${label} -- error`);\n output.detail(` ${assertion.error}`);\n } else if (assertion.passed) {\n output.success(` test: ${label}`);\n } else {\n output.error(` test: ${label} -- ${assertion.violationCount} failing row(s)`);\n if (assertion.sampleViolations) {\n for (const row of assertion.sampleViolations.slice(0, 3)) {\n output.detail(` ${JSON.stringify(row)}`);\n }\n }\n }\n}\n\nfunction extractAssertionLabel(sql: string): string {\n const whereMatch = sql.match(/WHERE\\s+(.+?)$/i);\n if (whereMatch?.[1]) {\n const clause = whereMatch[1].trim();\n return clause.length > 60 ? clause.slice(0, 57) + '...' : clause;\n }\n return sql.length > 60 ? sql.slice(0, 57) + '...' : sql;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/commands/test.ts"],"sourcesContent":["import { readFile, readdir } from 'fs/promises';\nimport { join, extname } from 'path';\nimport {\n parseModelMetadata,\n runAll,\n renderTemplate,\n createTemplateContext,\n expandDatePreset,\n isDatePreset,\n type TestSuiteResult,\n type TestModelInput,\n} from '@yamchart/query';\nimport type { ModelMetadata } from '@yamchart/schema';\nimport * as output from '../utils/output.js';\nimport { resolveConnection, createConnector } from './connection-utils.js';\n\nexport interface TestOptions {\n connection?: string;\n json?: boolean;\n}\n\nexport interface TestResult {\n success: boolean;\n suite: TestSuiteResult;\n connectionName: string;\n}\n\nexport async function testProject(\n projectDir: string,\n modelFilter: string | undefined,\n options: TestOptions,\n): Promise<TestResult> {\n const connection = await resolveConnection(projectDir, options.connection);\n const connector = createConnector(connection, projectDir);\n\n const modelsDir = join(projectDir, 'models');\n const allModels = await loadModels(modelsDir);\n\n let modelsToTest = allModels;\n if (modelFilter) {\n modelsToTest = allModels.filter((m) => m.name === modelFilter);\n if (modelsToTest.length === 0) {\n throw new Error(`Model \"${modelFilter}\" not found`);\n }\n }\n\n // Build refs map: model name -> model name (identity for standalone execution)\n const refs: Record<string, string> = {};\n for (const m of allModels) {\n refs[m.name] = m.name;\n }\n\n const testInputs: TestModelInput[] = [];\n for (const model of modelsToTest) {\n try {\n const compiledSql = compileWithDefaults(model.sql, model.metadata, refs);\n testInputs.push({ compiledSql, metadata: model.metadata });\n } catch {\n // If compilation fails, include raw SQL so the error is reported during test execution\n testInputs.push({ compiledSql: model.sql, metadata: model.metadata });\n }\n }\n\n try {\n await connector.connect();\n const suite = await runAll(testInputs, connector);\n return { success: suite.failed === 0, suite, connectionName: connection.name };\n } finally {\n await connector.disconnect();\n }\n}\n\nfunction resolveDynamicDefault(value: string): string {\n const trimmed = value.trim().toLowerCase();\n if (\n trimmed === 'current_date()' ||\n trimmed === 'current_date' ||\n trimmed === 'now()' ||\n trimmed.includes('current_date')\n ) {\n return formatISODate(new Date());\n }\n return value;\n}\n\nfunction formatISODate(date: Date): string {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, '0');\n const d = String(date.getDate()).padStart(2, '0');\n return `${y}-${m}-${d}`;\n}\n\nfunction compileWithDefaults(\n sql: string,\n metadata: ModelMetadata,\n refs: Record<string, string>,\n): string {\n const params: Record<string, unknown> = {};\n if (metadata.params) {\n for (const p of metadata.params) {\n if (p.default !== undefined) {\n params[p.name] = resolveDynamicDefault(p.default);\n }\n }\n }\n\n // Expand date presets into start_date/end_date\n if (typeof params.date_range === 'string' && isDatePreset(params.date_range)) {\n const range = expandDatePreset(params.date_range);\n if (range) {\n params.start_date = range.start_date;\n params.end_date = range.end_date;\n }\n }\n\n const context = createTemplateContext(params, refs);\n return renderTemplate(sql, context);\n}\n\ninterface LoadedModel {\n name: string;\n sql: string;\n metadata: ModelMetadata;\n}\n\nasync function loadModels(dir: string): Promise<LoadedModel[]> {\n const models: LoadedModel[] = [];\n\n let entries;\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return models;\n }\n\n for (const entry of entries) {\n const fullPath = join(dir, entry.name);\n if (entry.isDirectory()) {\n const subModels = await loadModels(fullPath);\n models.push(...subModels);\n } else if (extname(entry.name) === '.sql') {\n const content = await readFile(fullPath, 'utf-8');\n try {\n const parsed = parseModelMetadata(content);\n models.push({ name: parsed.name, sql: parsed.sql, metadata: parsed });\n } catch {\n // Skip unparseable models\n }\n }\n }\n\n return models;\n}\n\nexport function formatTestOutput(result: TestResult, connectionName: string): void {\n const { suite } = result;\n\n const testedModels = suite.models.filter(\n (m) => m.schemaCheck || m.assertions.length > 0 || m.error,\n );\n\n output.header(`Testing ${suite.models.length} model(s) against ${connectionName}...`);\n\n for (const model of suite.models) {\n const hasChecks = model.schemaCheck || model.assertions.length > 0 || model.error;\n if (!hasChecks) continue;\n\n console.log(` ${model.modelName}`);\n\n if (model.error) {\n output.error(` error: ${model.error}`);\n continue;\n }\n\n if (model.schemaCheck) {\n formatSchemaCheck(model.schemaCheck);\n }\n\n for (const assertion of model.assertions) {\n formatAssertion(assertion);\n }\n\n output.newline();\n }\n\n // Summary line\n const totalChecks = suite.passed + suite.failed;\n const summary = `${testedModels.length} model(s), ${totalChecks} check(s): ${suite.passed} passed, ${suite.failed} failed`;\n const skippedSuffix = suite.skipped > 0 ? ` (${suite.skipped} skipped)` : '';\n const durationSuffix = ` (${suite.durationMs.toFixed(0)}ms)`;\n\n if (suite.failed === 0) {\n output.success(`${summary}${skippedSuffix}${durationSuffix}`);\n } else {\n output.error(`${summary}${skippedSuffix}${durationSuffix}`);\n }\n}\n\nfunction formatSchemaCheck(check: NonNullable<TestSuiteResult['models'][0]['schemaCheck']>): void {\n if (check.passed) {\n output.success(` schema: returns expected columns (${check.expectedColumns.join(', ')})`);\n return;\n }\n\n const issues: string[] = [];\n if (check.missingColumns.length > 0) {\n issues.push(`missing: ${check.missingColumns.join(', ')}`);\n }\n for (const m of check.typeMismatches) {\n issues.push(`${m.column}: expected ${m.expected}, got ${m.actual}`);\n }\n output.error(` schema: ${issues.join('; ')}`);\n}\n\nfunction formatAssertion(assertion: TestSuiteResult['models'][0]['assertions'][0]): void {\n const label = extractAssertionLabel(assertion.sql);\n\n if (assertion.warning) {\n output.warning(` ${label}`);\n output.detail(` ${assertion.warning}`);\n }\n\n if (assertion.error) {\n output.error(` test: ${label} -- error`);\n output.detail(` ${assertion.error}`);\n } else if (assertion.passed) {\n output.success(` test: ${label}`);\n } else {\n output.error(` test: ${label} -- ${assertion.violationCount} failing row(s)`);\n if (assertion.sampleViolations) {\n for (const row of assertion.sampleViolations.slice(0, 3)) {\n output.detail(` ${JSON.stringify(row)}`);\n }\n }\n }\n}\n\nfunction extractAssertionLabel(sql: string): string {\n const whereMatch = sql.match(/WHERE\\s+(.+?)$/i);\n if (whereMatch?.[1]) {\n const clause = whereMatch[1].trim();\n return clause.length > 60 ? clause.slice(0, 57) + '...' : clause;\n }\n return sql.length > 60 ? sql.slice(0, 57) + '...' : sql;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import {
|
|
2
|
+
checkForUpdate,
|
|
3
|
+
fetchReleaseNotes
|
|
4
|
+
} from "./chunk-F6QAWPYU.js";
|
|
5
|
+
import {
|
|
6
|
+
detail,
|
|
7
|
+
error,
|
|
8
|
+
header,
|
|
9
|
+
info,
|
|
10
|
+
newline,
|
|
11
|
+
spinner,
|
|
12
|
+
success
|
|
13
|
+
} from "./chunk-HJVVHYVN.js";
|
|
14
|
+
import "./chunk-DGUM43GV.js";
|
|
15
|
+
|
|
16
|
+
// src/commands/update.ts
|
|
17
|
+
import { execSync } from "child_process";
|
|
18
|
+
import { createInterface } from "readline";
|
|
19
|
+
import { readFile, writeFile, mkdir, access, readdir } from "fs/promises";
|
|
20
|
+
import { join, dirname, basename } from "path";
|
|
21
|
+
import { fileURLToPath } from "url";
|
|
22
|
+
import { parse as parseYaml } from "yaml";
|
|
23
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
async function runUpdate(currentVersion) {
|
|
25
|
+
const spin = spinner("Checking for updates...");
|
|
26
|
+
const update = await checkForUpdate(currentVersion, { skipCache: true });
|
|
27
|
+
spin.stop();
|
|
28
|
+
if (!update) {
|
|
29
|
+
success(`yamchart ${currentVersion} is the latest version`);
|
|
30
|
+
} else {
|
|
31
|
+
info(`Update available: ${update.current} \u2192 ${update.latest}`);
|
|
32
|
+
const notes = await fetchReleaseNotes(update.latest);
|
|
33
|
+
if (notes) {
|
|
34
|
+
newline();
|
|
35
|
+
header(`What's new in ${update.latest}:`);
|
|
36
|
+
for (const line of notes.split("\n")) {
|
|
37
|
+
detail(line);
|
|
38
|
+
}
|
|
39
|
+
newline();
|
|
40
|
+
}
|
|
41
|
+
const installSpin = spinner("Installing yamchart@latest...");
|
|
42
|
+
try {
|
|
43
|
+
execSync("npm install -g yamchart@latest", { stdio: "pipe" });
|
|
44
|
+
installSpin.stop();
|
|
45
|
+
success(`Updated yamchart ${update.current} \u2192 ${update.latest}`);
|
|
46
|
+
} catch {
|
|
47
|
+
installSpin.stop();
|
|
48
|
+
error("Automatic install failed. Run manually:");
|
|
49
|
+
detail("npm install -g yamchart@latest");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
await refreshProjectFiles();
|
|
54
|
+
}
|
|
55
|
+
async function refreshDocs() {
|
|
56
|
+
const projectFile = join(process.cwd(), "yamchart.yaml");
|
|
57
|
+
try {
|
|
58
|
+
await access(projectFile);
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const bundled = await readBundledReference();
|
|
63
|
+
if (!bundled) return;
|
|
64
|
+
const refPath = join(process.cwd(), "docs", "yamchart-reference.md");
|
|
65
|
+
try {
|
|
66
|
+
const existing = await readFile(refPath, "utf-8");
|
|
67
|
+
if (existing === bundled) return;
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
await mkdir(join(process.cwd(), "docs"), { recursive: true });
|
|
71
|
+
await writeFile(refPath, bundled, "utf-8");
|
|
72
|
+
success("Updated docs/yamchart-reference.md");
|
|
73
|
+
}
|
|
74
|
+
async function readBundledReference() {
|
|
75
|
+
const distPath = join(__dirname, "templates", "default", "docs", "yamchart-reference.md");
|
|
76
|
+
const srcPath = join(__dirname, "..", "templates", "default", "docs", "yamchart-reference.md");
|
|
77
|
+
for (const path of [distPath, srcPath]) {
|
|
78
|
+
try {
|
|
79
|
+
return await readFile(path, "utf-8");
|
|
80
|
+
} catch {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
async function refreshProjectFiles() {
|
|
87
|
+
const projectFile = join(process.cwd(), "yamchart.yaml");
|
|
88
|
+
try {
|
|
89
|
+
await access(projectFile);
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
await refreshDocs();
|
|
94
|
+
await refreshSkills();
|
|
95
|
+
await refreshClaudeMd();
|
|
96
|
+
}
|
|
97
|
+
async function refreshSkills() {
|
|
98
|
+
try {
|
|
99
|
+
await access(join(process.cwd(), "yamchart.yaml"));
|
|
100
|
+
} catch {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const bundledDir = await getBundledSkillsDir();
|
|
104
|
+
if (!bundledDir) return;
|
|
105
|
+
let entries;
|
|
106
|
+
try {
|
|
107
|
+
entries = await readdir(bundledDir, { withFileTypes: true });
|
|
108
|
+
} catch {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
let updated = 0;
|
|
112
|
+
let created = 0;
|
|
113
|
+
for (const entry of entries) {
|
|
114
|
+
if (!entry.isDirectory()) continue;
|
|
115
|
+
const skillFile = join(bundledDir, entry.name, "SKILL.md");
|
|
116
|
+
let bundledContent;
|
|
117
|
+
try {
|
|
118
|
+
bundledContent = await readFile(skillFile, "utf-8");
|
|
119
|
+
} catch {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const targetDir = join(process.cwd(), ".claude", "skills", entry.name);
|
|
123
|
+
const targetFile = join(targetDir, "SKILL.md");
|
|
124
|
+
let isNew = false;
|
|
125
|
+
try {
|
|
126
|
+
const existing = await readFile(targetFile, "utf-8");
|
|
127
|
+
if (existing === bundledContent) continue;
|
|
128
|
+
} catch {
|
|
129
|
+
isNew = true;
|
|
130
|
+
}
|
|
131
|
+
await mkdir(targetDir, { recursive: true });
|
|
132
|
+
await writeFile(targetFile, bundledContent, "utf-8");
|
|
133
|
+
if (isNew) created++;
|
|
134
|
+
else updated++;
|
|
135
|
+
}
|
|
136
|
+
if (created > 0 || updated > 0) {
|
|
137
|
+
const parts = [];
|
|
138
|
+
if (created > 0) parts.push(`${created} new`);
|
|
139
|
+
if (updated > 0) parts.push(`${updated} updated`);
|
|
140
|
+
success(`Synced .claude/skills/ (${parts.join(", ")})`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function refreshClaudeMd(options = {}) {
|
|
144
|
+
try {
|
|
145
|
+
await access(join(process.cwd(), "yamchart.yaml"));
|
|
146
|
+
} catch {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const bundled = await readBundledFile("CLAUDE.md");
|
|
150
|
+
if (!bundled) return;
|
|
151
|
+
const projectName = await getProjectName();
|
|
152
|
+
const content = bundled.replace(/\{\{name\}\}/g, projectName);
|
|
153
|
+
const targetPath = join(process.cwd(), "CLAUDE.md");
|
|
154
|
+
try {
|
|
155
|
+
const existing = await readFile(targetPath, "utf-8");
|
|
156
|
+
if (existing === content) return;
|
|
157
|
+
const confirmFn = options.confirm ?? promptConfirm;
|
|
158
|
+
const shouldOverwrite = await confirmFn(
|
|
159
|
+
"CLAUDE.md differs from the current template. Overwrite? (y/n) "
|
|
160
|
+
);
|
|
161
|
+
if (!shouldOverwrite) {
|
|
162
|
+
info("Skipped CLAUDE.md update");
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
} catch {
|
|
166
|
+
}
|
|
167
|
+
await writeFile(targetPath, content, "utf-8");
|
|
168
|
+
success("Updated CLAUDE.md");
|
|
169
|
+
}
|
|
170
|
+
async function getBundledSkillsDir() {
|
|
171
|
+
const distPath = join(__dirname, "templates", "default", ".claude", "skills");
|
|
172
|
+
const srcPath = join(__dirname, "..", "templates", "default", ".claude", "skills");
|
|
173
|
+
for (const path of [distPath, srcPath]) {
|
|
174
|
+
try {
|
|
175
|
+
await access(path);
|
|
176
|
+
return path;
|
|
177
|
+
} catch {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
async function readBundledFile(relativePath) {
|
|
184
|
+
const distPath = join(__dirname, "templates", "default", relativePath);
|
|
185
|
+
const srcPath = join(__dirname, "..", "templates", "default", relativePath);
|
|
186
|
+
for (const path of [distPath, srcPath]) {
|
|
187
|
+
try {
|
|
188
|
+
return await readFile(path, "utf-8");
|
|
189
|
+
} catch {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
async function getProjectName() {
|
|
196
|
+
try {
|
|
197
|
+
const raw = await readFile(join(process.cwd(), "yamchart.yaml"), "utf-8");
|
|
198
|
+
const parsed = parseYaml(raw);
|
|
199
|
+
if (parsed?.name) return parsed.name;
|
|
200
|
+
} catch {
|
|
201
|
+
}
|
|
202
|
+
return basename(process.cwd());
|
|
203
|
+
}
|
|
204
|
+
async function promptConfirm(message) {
|
|
205
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
206
|
+
return new Promise((resolve) => {
|
|
207
|
+
rl.question(message, (answer) => {
|
|
208
|
+
rl.close();
|
|
209
|
+
resolve(answer.trim().toLowerCase() === "y");
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
export {
|
|
214
|
+
refreshClaudeMd,
|
|
215
|
+
refreshDocs,
|
|
216
|
+
refreshProjectFiles,
|
|
217
|
+
refreshSkills,
|
|
218
|
+
runUpdate
|
|
219
|
+
};
|
|
220
|
+
//# sourceMappingURL=update-UKMEWCSO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/update.ts"],"sourcesContent":["import { execSync } from 'child_process';\nimport { createInterface } from 'readline';\nimport { readFile, writeFile, mkdir, access, readdir } from 'fs/promises';\nimport { join, dirname, basename } from 'path';\nimport { fileURLToPath } from 'url';\nimport { parse as parseYaml } from 'yaml';\nimport * as output from '../utils/output.js';\nimport { checkForUpdate, fetchReleaseNotes } from '../utils/update-check.js';\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport async function runUpdate(currentVersion: string): Promise<void> {\n const spin = output.spinner('Checking for updates...');\n\n const update = await checkForUpdate(currentVersion, { skipCache: true });\n\n spin.stop();\n\n if (!update) {\n output.success(`yamchart ${currentVersion} is the latest version`);\n } else {\n output.info(`Update available: ${update.current} → ${update.latest}`);\n\n // Fetch and display release notes\n const notes = await fetchReleaseNotes(update.latest);\n if (notes) {\n output.newline();\n output.header(`What's new in ${update.latest}:`);\n for (const line of notes.split('\\n')) {\n output.detail(line);\n }\n output.newline();\n }\n\n const installSpin = output.spinner('Installing yamchart@latest...');\n try {\n execSync('npm install -g yamchart@latest', { stdio: 'pipe' });\n installSpin.stop();\n output.success(`Updated yamchart ${update.current} → ${update.latest}`);\n } catch {\n installSpin.stop();\n output.error('Automatic install failed. Run manually:');\n output.detail('npm install -g yamchart@latest');\n return;\n }\n }\n\n // Refresh project files if we're in a yamchart project\n await refreshProjectFiles();\n}\n\n/**\n * Refresh docs/yamchart-reference.md from the bundled template.\n * Only runs when yamchart.yaml exists in the current directory.\n */\nexport async function refreshDocs(): Promise<void> {\n const projectFile = join(process.cwd(), 'yamchart.yaml');\n try {\n await access(projectFile);\n } catch {\n return; // Not in a yamchart project\n }\n\n const bundled = await readBundledReference();\n if (!bundled) return;\n\n const refPath = join(process.cwd(), 'docs', 'yamchart-reference.md');\n\n try {\n const existing = await readFile(refPath, 'utf-8');\n if (existing === bundled) return; // Already up to date\n } catch {\n // File doesn't exist — create it\n }\n\n await mkdir(join(process.cwd(), 'docs'), { recursive: true });\n await writeFile(refPath, bundled, 'utf-8');\n output.success('Updated docs/yamchart-reference.md');\n}\n\nasync function readBundledReference(): Promise<string | null> {\n // Production: dist/templates/default/docs/yamchart-reference.md\n const distPath = join(__dirname, 'templates', 'default', 'docs', 'yamchart-reference.md');\n // Development: src/commands → src/templates\n const srcPath = join(__dirname, '..', 'templates', 'default', 'docs', 'yamchart-reference.md');\n\n for (const path of [distPath, srcPath]) {\n try {\n return await readFile(path, 'utf-8');\n } catch {\n continue;\n }\n }\n return null;\n}\n\n/**\n * Refresh all managed project files (docs, skills, CLAUDE.md).\n * Only runs when yamchart.yaml exists in the current directory.\n */\nexport async function refreshProjectFiles(): Promise<void> {\n const projectFile = join(process.cwd(), 'yamchart.yaml');\n try {\n await access(projectFile);\n } catch {\n return; // Not in a yamchart project\n }\n\n await refreshDocs();\n await refreshSkills();\n await refreshClaudeMd();\n}\n\n/**\n * Refresh .claude/skills/ from bundled templates.\n * Overwrites bundled skills (managed by yamchart), leaves user-created skills alone.\n * Only runs when yamchart.yaml exists in the current directory.\n */\nexport async function refreshSkills(): Promise<void> {\n try {\n await access(join(process.cwd(), 'yamchart.yaml'));\n } catch {\n return;\n }\n\n const bundledDir = await getBundledSkillsDir();\n if (!bundledDir) return;\n\n let entries: Awaited<ReturnType<typeof readdir>>;\n try {\n entries = await readdir(bundledDir, { withFileTypes: true });\n } catch {\n return;\n }\n\n let updated = 0;\n let created = 0;\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const skillFile = join(bundledDir, entry.name, 'SKILL.md');\n let bundledContent: string;\n try {\n bundledContent = await readFile(skillFile, 'utf-8');\n } catch {\n continue; // Skip directories without SKILL.md\n }\n\n const targetDir = join(process.cwd(), '.claude', 'skills', entry.name);\n const targetFile = join(targetDir, 'SKILL.md');\n\n let isNew = false;\n try {\n const existing = await readFile(targetFile, 'utf-8');\n if (existing === bundledContent) continue; // Already up to date\n } catch {\n isNew = true; // File doesn't exist\n }\n\n await mkdir(targetDir, { recursive: true });\n await writeFile(targetFile, bundledContent, 'utf-8');\n if (isNew) created++;\n else updated++;\n }\n\n if (created > 0 || updated > 0) {\n const parts: string[] = [];\n if (created > 0) parts.push(`${created} new`);\n if (updated > 0) parts.push(`${updated} updated`);\n output.success(`Synced .claude/skills/ (${parts.join(', ')})`);\n }\n}\n\n/**\n * Refresh CLAUDE.md from bundled template.\n * If the file exists and differs, prompts the user before overwriting.\n * Only runs when yamchart.yaml exists in the current directory.\n */\nexport async function refreshClaudeMd(\n options: { confirm?: (message: string) => Promise<boolean> } = {}\n): Promise<void> {\n try {\n await access(join(process.cwd(), 'yamchart.yaml'));\n } catch {\n return;\n }\n\n const bundled = await readBundledFile('CLAUDE.md');\n if (!bundled) return;\n\n // Substitute {{name}} with project name from yamchart.yaml\n const projectName = await getProjectName();\n const content = bundled.replace(/\\{\\{name\\}\\}/g, projectName);\n\n const targetPath = join(process.cwd(), 'CLAUDE.md');\n\n try {\n const existing = await readFile(targetPath, 'utf-8');\n if (existing === content) return; // Already up to date\n\n // File exists and differs — prompt user\n const confirmFn = options.confirm ?? promptConfirm;\n const shouldOverwrite = await confirmFn(\n 'CLAUDE.md differs from the current template. Overwrite? (y/n) '\n );\n if (!shouldOverwrite) {\n output.info('Skipped CLAUDE.md update');\n return;\n }\n } catch {\n // File doesn't exist — create without prompting\n }\n\n await writeFile(targetPath, content, 'utf-8');\n output.success('Updated CLAUDE.md');\n}\n\nasync function getBundledSkillsDir(): Promise<string | null> {\n const distPath = join(__dirname, 'templates', 'default', '.claude', 'skills');\n const srcPath = join(__dirname, '..', 'templates', 'default', '.claude', 'skills');\n\n for (const path of [distPath, srcPath]) {\n try {\n await access(path);\n return path;\n } catch {\n continue;\n }\n }\n return null;\n}\n\nasync function readBundledFile(relativePath: string): Promise<string | null> {\n const distPath = join(__dirname, 'templates', 'default', relativePath);\n const srcPath = join(__dirname, '..', 'templates', 'default', relativePath);\n\n for (const path of [distPath, srcPath]) {\n try {\n return await readFile(path, 'utf-8');\n } catch {\n continue;\n }\n }\n return null;\n}\n\nasync function getProjectName(): Promise<string> {\n try {\n const raw = await readFile(join(process.cwd(), 'yamchart.yaml'), 'utf-8');\n const parsed = parseYaml(raw) as { name?: string };\n if (parsed?.name) return parsed.name;\n } catch {\n // Fall through to basename\n }\n return basename(process.cwd());\n}\n\nasync function promptConfirm(message: string): Promise<boolean> {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n return new Promise((resolve) => {\n rl.question(message, (answer) => {\n rl.close();\n resolve(answer.trim().toLowerCase() === 'y');\n });\n });\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yamchart",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Git-native business intelligence dashboards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,19 +61,21 @@
|
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@types/node": "^22.0.0",
|
|
64
|
+
"snowflake-sdk": "^2.3.4",
|
|
64
65
|
"tsup": "^8.0.0",
|
|
65
66
|
"typescript": "^5.7.0",
|
|
66
67
|
"vitest": "^2.1.0",
|
|
67
68
|
"@yamchart/advisor": "0.1.0",
|
|
68
|
-
"@yamchart/config": "0.1.2",
|
|
69
69
|
"@yamchart/auth-local": "0.1.0",
|
|
70
|
+
"@yamchart/config": "0.1.2",
|
|
70
71
|
"@yamchart/query": "0.1.2",
|
|
71
72
|
"@yamchart/schema": "0.1.2",
|
|
72
73
|
"@yamchart/server": "0.1.2"
|
|
73
74
|
},
|
|
74
75
|
"files": [
|
|
75
76
|
"dist",
|
|
76
|
-
"bin"
|
|
77
|
+
"bin",
|
|
78
|
+
"CLAUDE.md"
|
|
77
79
|
],
|
|
78
80
|
"publishConfig": {
|
|
79
81
|
"access": "public"
|