@revos/cli 0.2.0 → 0.2.2

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.
Files changed (152) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +286 -41
  3. package/dist/adapters/oclif/commands/action-runs/get.mjs +1 -1
  4. package/dist/adapters/oclif/commands/action-runs/list.mjs +8 -2
  5. package/dist/adapters/oclif/commands/actions/get-input-schema.mjs +2 -2
  6. package/dist/adapters/oclif/commands/actions/get-params-schema.mjs +2 -2
  7. package/dist/adapters/oclif/commands/actions/get.mjs +1 -1
  8. package/dist/adapters/oclif/commands/actions/list.mjs +8 -4
  9. package/dist/adapters/oclif/commands/ai-instructions/create.mjs +1 -1
  10. package/dist/adapters/oclif/commands/ai-instructions/delete.mjs +1 -1
  11. package/dist/adapters/oclif/commands/ai-instructions/get.mjs +1 -1
  12. package/dist/adapters/oclif/commands/ai-instructions/list.mjs +8 -2
  13. package/dist/adapters/oclif/commands/ai-instructions/update.mjs +1 -1
  14. package/dist/adapters/oclif/commands/api.d.mts +11 -0
  15. package/dist/adapters/oclif/commands/api.mjs +112 -0
  16. package/dist/adapters/oclif/commands/apply.d.mts +28 -0
  17. package/dist/adapters/oclif/commands/apply.mjs +77 -0
  18. package/dist/adapters/oclif/commands/auth/login.d.mts +5 -4
  19. package/dist/adapters/oclif/commands/auth/login.mjs +22 -11
  20. package/dist/adapters/oclif/commands/auth/logout.d.mts +1 -1
  21. package/dist/adapters/oclif/commands/auth/logout.mjs +7 -3
  22. package/dist/adapters/oclif/commands/auth/status.d.mts +2 -2
  23. package/dist/adapters/oclif/commands/auth/status.mjs +2 -2
  24. package/dist/adapters/oclif/commands/connections/create.d.mts +6 -0
  25. package/dist/adapters/oclif/commands/connections/create.mjs +8 -0
  26. package/dist/adapters/oclif/commands/connections/delete.d.mts +6 -0
  27. package/dist/adapters/oclif/commands/connections/delete.mjs +8 -0
  28. package/dist/adapters/oclif/commands/connections/get.d.mts +6 -0
  29. package/dist/adapters/oclif/commands/connections/get.mjs +8 -0
  30. package/dist/adapters/oclif/commands/connections/list.d.mts +6 -0
  31. package/dist/adapters/oclif/commands/connections/list.mjs +14 -0
  32. package/dist/adapters/oclif/commands/connections/update.d.mts +6 -0
  33. package/dist/adapters/oclif/commands/connections/update.mjs +8 -0
  34. package/dist/adapters/oclif/commands/cubes/create.d.mts +6 -0
  35. package/dist/adapters/oclif/commands/cubes/create.mjs +8 -0
  36. package/dist/adapters/oclif/commands/cubes/delete.d.mts +6 -0
  37. package/dist/adapters/oclif/commands/cubes/delete.mjs +8 -0
  38. package/dist/adapters/oclif/commands/cubes/get.d.mts +6 -0
  39. package/dist/adapters/oclif/commands/cubes/get.mjs +8 -0
  40. package/dist/adapters/oclif/commands/cubes/list.d.mts +6 -0
  41. package/dist/adapters/oclif/commands/cubes/list.mjs +13 -0
  42. package/dist/adapters/oclif/commands/cubes/update.d.mts +6 -0
  43. package/dist/adapters/oclif/commands/cubes/update.mjs +8 -0
  44. package/dist/adapters/oclif/commands/diff.d.mts +27 -0
  45. package/dist/adapters/oclif/commands/diff.mjs +66 -0
  46. package/dist/adapters/oclif/commands/gservice-account-keys/get.mjs +1 -1
  47. package/dist/adapters/oclif/commands/gservice-account-keys/reveal.mjs +2 -2
  48. package/dist/adapters/oclif/commands/gservice-accounts/create.mjs +1 -1
  49. package/dist/adapters/oclif/commands/gservice-accounts/delete.mjs +1 -1
  50. package/dist/adapters/oclif/commands/gservice-accounts/get.mjs +1 -1
  51. package/dist/adapters/oclif/commands/gservice-accounts/list.mjs +7 -2
  52. package/dist/adapters/oclif/commands/init.d.mts +2 -1
  53. package/dist/adapters/oclif/commands/init.mjs +28 -24
  54. package/dist/adapters/oclif/commands/org/create.mjs +1 -1
  55. package/dist/adapters/oclif/commands/org/current.d.mts +2 -2
  56. package/dist/adapters/oclif/commands/org/current.mjs +2 -2
  57. package/dist/adapters/oclif/commands/org/get.mjs +1 -1
  58. package/dist/adapters/oclif/commands/org/list.d.mts +3 -11
  59. package/dist/adapters/oclif/commands/org/list.mjs +26 -26
  60. package/dist/adapters/oclif/commands/org/switch.d.mts +3 -2
  61. package/dist/adapters/oclif/commands/org/switch.mjs +13 -5
  62. package/dist/adapters/oclif/commands/pull.d.mts +28 -0
  63. package/dist/adapters/oclif/commands/pull.mjs +88 -0
  64. package/dist/adapters/oclif/commands/score-groups/create.mjs +3 -2
  65. package/dist/adapters/oclif/commands/score-groups/delete.mjs +1 -1
  66. package/dist/adapters/oclif/commands/score-groups/get.mjs +1 -1
  67. package/dist/adapters/oclif/commands/score-groups/list.mjs +3 -2
  68. package/dist/adapters/oclif/commands/score-groups/update.mjs +1 -1
  69. package/dist/adapters/oclif/commands/scores/create.mjs +3 -2
  70. package/dist/adapters/oclif/commands/scores/delete.mjs +1 -1
  71. package/dist/adapters/oclif/commands/scores/list.mjs +3 -2
  72. package/dist/adapters/oclif/commands/scores/update.mjs +1 -1
  73. package/dist/adapters/oclif/commands/segments/create.mjs +1 -1
  74. package/dist/adapters/oclif/commands/segments/delete.mjs +1 -1
  75. package/dist/adapters/oclif/commands/segments/evaluate.mjs +2 -2
  76. package/dist/adapters/oclif/commands/segments/get-evaluation-history.mjs +2 -2
  77. package/dist/adapters/oclif/commands/segments/get-version.mjs +2 -2
  78. package/dist/adapters/oclif/commands/segments/get.mjs +1 -1
  79. package/dist/adapters/oclif/commands/segments/list-versions.mjs +16 -5
  80. package/dist/adapters/oclif/commands/segments/list.mjs +9 -2
  81. package/dist/adapters/oclif/commands/segments/restore-version.mjs +2 -2
  82. package/dist/adapters/oclif/commands/segments/update.mjs +1 -1
  83. package/dist/adapters/oclif/commands/sources/create.d.mts +11 -0
  84. package/dist/adapters/oclif/commands/sources/create.mjs +16 -0
  85. package/dist/adapters/oclif/commands/sources/delete.d.mts +6 -0
  86. package/dist/adapters/oclif/commands/sources/delete.mjs +8 -0
  87. package/dist/adapters/oclif/commands/sources/get.d.mts +6 -0
  88. package/dist/adapters/oclif/commands/sources/get.mjs +8 -0
  89. package/dist/adapters/oclif/commands/sources/list-streams.d.mts +6 -0
  90. package/dist/adapters/oclif/commands/sources/list-streams.mjs +31 -0
  91. package/dist/adapters/oclif/commands/sources/list.d.mts +6 -0
  92. package/dist/adapters/oclif/commands/sources/list.mjs +13 -0
  93. package/dist/adapters/oclif/commands/sources/update.d.mts +15 -0
  94. package/dist/adapters/oclif/commands/sources/update.mjs +21 -0
  95. package/dist/adapters/oclif/commands/status.d.mts +26 -0
  96. package/dist/adapters/oclif/commands/status.mjs +77 -0
  97. package/dist/adapters/oclif/commands/table-views/create.mjs +3 -2
  98. package/dist/adapters/oclif/commands/table-views/delete.mjs +1 -1
  99. package/dist/adapters/oclif/commands/table-views/list.mjs +3 -2
  100. package/dist/adapters/oclif/commands/table-views/update.mjs +1 -1
  101. package/dist/adapters/oclif/commands/tables/create.mjs +1 -1
  102. package/dist/adapters/oclif/commands/tables/delete.mjs +1 -1
  103. package/dist/adapters/oclif/commands/tables/get.mjs +1 -1
  104. package/dist/adapters/oclif/commands/tables/list.mjs +3 -2
  105. package/dist/adapters/oclif/commands/tables/update.mjs +1 -1
  106. package/dist/{base.command-d7VW6WTp.d.mts → base.command-D7X3ZNtY.d.mts} +0 -1
  107. package/dist/{base.command-DlVQ9Cqa.mjs → base.command-cV5d65r8.mjs} +15 -12
  108. package/dist/chunk-CfYAbeIz.mjs +13 -0
  109. package/dist/core-CMrP5BQS.mjs +2378 -0
  110. package/dist/{factory-D9sR_S_g.mjs → factory-C6XLqhT9.mjs} +44 -10
  111. package/dist/iac-render-BSZZEP0n.mjs +17 -0
  112. package/dist/index-BqKwXXAo.d.mts +598 -0
  113. package/dist/index.d.mts +3 -4
  114. package/dist/index.mjs +2 -2
  115. package/dist/{presets-Cvazkjmu.mjs → presets-CJbFbHlw.mjs} +35 -8
  116. package/dist/templates/.claude/settings.json +39 -0
  117. package/dist/templates/.devcontainer/devcontainer.json +2 -2
  118. package/dist/templates/.devcontainer/setup.sh +3 -0
  119. package/dist/templates/AGENTS.md +36 -13
  120. package/dist/templates/dbt/dbt_project.yml +2 -2
  121. package/dist/templates/skills/create-connections/SKILL.md +210 -0
  122. package/dist/templates/skills/create-connections/references/mappers.md +152 -0
  123. package/dist/templates/skills/{create-semantic-model → create-cubes}/SKILL.md +28 -26
  124. package/dist/templates/skills/create-cubes/references/bq-pk-fk-conventions.md +183 -0
  125. package/dist/templates/skills/{create-semantic-model → create-cubes}/references/cube-examples.md +85 -7
  126. package/dist/templates/skills/create-cubes/references/hubspot-entities.md +289 -0
  127. package/dist/templates/skills/create-cubes/references/jira-entities.md +201 -0
  128. package/dist/templates/skills/create-cubes/references/netsuite-entities.md +121 -0
  129. package/dist/templates/skills/create-cubes/references/stripe-entities.md +114 -0
  130. package/dist/templates/skills/create-dbt-transformations/SKILL.md +62 -33
  131. package/dist/templates/skills/create-dbt-transformations/references/edge-cases.md +21 -3
  132. package/dist/templates/skills/create-dbt-transformations/references/schema-conventions.md +21 -7
  133. package/dist/templates/skills/create-dbt-transformations/references/sql-templates.md +34 -20
  134. package/dist/templates/skills/explore-lakehouse/SKILL.md +8 -4
  135. package/dist/templates/skills/load-sample-data/SKILL.md +119 -0
  136. package/dist/templates/skills/visualize-semantic-model/SKILL.md +159 -0
  137. package/dist/templates/skills/visualize-semantic-model/scripts/render_graph.py +186 -0
  138. package/dist/{types-Y_ht_ja5.d.mts → types-CGjxcj4L.d.mts} +3 -0
  139. package/package.json +48 -6
  140. package/dist/adapters/oclif/commands/overlays/diff.d.mts +0 -19
  141. package/dist/adapters/oclif/commands/overlays/diff.mjs +0 -80
  142. package/dist/adapters/oclif/commands/overlays/pull.d.mts +0 -15
  143. package/dist/adapters/oclif/commands/overlays/pull.mjs +0 -44
  144. package/dist/adapters/oclif/commands/overlays/push.d.mts +0 -18
  145. package/dist/adapters/oclif/commands/overlays/push.mjs +0 -59
  146. package/dist/adapters/oclif/commands/overlays/status.d.mts +0 -18
  147. package/dist/adapters/oclif/commands/overlays/status.mjs +0 -53
  148. package/dist/core-gKJ_V-K5.mjs +0 -973
  149. package/dist/index-KAzwt5vr.d.mts +0 -190
  150. package/dist/types-C_p_6rkj.d.mts +0 -69
  151. /package/dist/templates/skills/{create-semantic-model → create-cubes}/references/key-patterns.md +0 -0
  152. /package/dist/templates/skills/{create-semantic-model → create-cubes}/references/validation-queries.md +0 -0
@@ -0,0 +1,159 @@
1
+ ---
2
+ name: visualize-semantic-model
3
+ description: >
4
+ Generate a model-graph.png visualization of Cube.dev semantic models — render the
5
+ cube graph, show relationships between cubes, draw the fact spine, or diagram the
6
+ semantic layer. Use whenever the user mentions visualizing, drawing, diagramming, or
7
+ graphing the semantic model / cube model / cube relationships, even if they don't
8
+ explicitly ask for a PNG. Triggers: "visualize the semantic model", "draw the cube
9
+ graph", "show relationships between cubes", "generate model-graph.png", "diagram the
10
+ semantic layer", "render the cube model". Accepts an optional folder argument
11
+ (defaults to `cubes/`).
12
+ ---
13
+
14
+ # Visualize Semantic Model
15
+
16
+ Render a dark-themed directed graph of a Cube.dev model by parsing cube YAML files,
17
+ detecting the fact spine, then invoking the bundled renderer with a JSON spec.
18
+
19
+ The renderer (`scripts/render_graph.py`) is pure layout + drawing — it takes a graph
20
+ spec on stdin and writes the PNG. Keeping it bundled means every run produces visually
21
+ consistent output and you don't reinvent matplotlib code each time.
22
+
23
+ ---
24
+
25
+ ## Step 1: Resolve the cubes folder
26
+
27
+ If the user passed a folder argument, use it. Otherwise default to `cubes/`. If the
28
+ folder doesn't exist, ask the user where the cube definitions live before proceeding.
29
+
30
+ ```bash
31
+ find <folder> -name "*.yml" -not -name "model-graph.*" | sort
32
+ ```
33
+
34
+ ---
35
+
36
+ ## Step 2: Parse the cube graph
37
+
38
+ For each `.yml` file extract:
39
+
40
+ - `name` — cube name
41
+ - `joins` — map of `target_cube → { relationship, sql }`
42
+ - A short join-key label parsed from each join's `sql`
43
+
44
+ **Join-key label rules.** Strip `${CUBE}.` and `${<target>}.` prefixes. For a single
45
+ equality, use the LHS column. For composite keys (multiple `AND`), join the LHS column
46
+ names with `+`.
47
+
48
+ ```yaml
49
+ sql: "${CUBE}.user_id = ${users}.user_id"
50
+ # → "user_id"
51
+
52
+ sql: "${CUBE}.traffic_source = ${rev}.traffic_source AND ${CUBE}.order_date = ${rev}.order_date"
53
+ # → "traffic_source + order_date"
54
+ ```
55
+
56
+ **Cardinality** (always render edges from the _one_ side to the _many_ side):
57
+
58
+ | Declared on this cube | This cube's side | Other cube's side |
59
+ | --------------------- | ---------------- | ----------------- |
60
+ | `many_to_one` | ∞ | 1 |
61
+ | `one_to_many` | 1 | ∞ |
62
+ | `one_to_one` | 1 | 1 |
63
+
64
+ **Fact-spine detection.** The cube with the most `many_to_one` joins (it holds the FKs)
65
+ is the spine. Tie-break by preferring names containing `enriched`, `fact`, or `items`.
66
+ If no cube has any `many_to_one` joins (e.g. the model is all `one_to_many` from a hub),
67
+ fall back to the cube with the most outgoing joins of any kind. If still ambiguous, ask
68
+ the user which cube to treat as the spine.
69
+
70
+ **Edge cases — stop and tell the user instead of rendering:**
71
+
72
+ - The folder contains fewer than 2 cubes → at least two cubes are needed.
73
+ - No `joins` found anywhere → there are no relationships to draw.
74
+
75
+ ---
76
+
77
+ ## Step 3: Build the graph spec
78
+
79
+ Build a JSON object the renderer understands:
80
+
81
+ ```json
82
+ {
83
+ "title": "Semantic Model — <project_name>",
84
+ "fact_spine": {
85
+ "name": "<fact_cube>",
86
+ "pk": "<pk_col>",
87
+ "fks": ["fk_a", "fk_b", "fk_c", "fk_d"]
88
+ },
89
+ "dimensions": [
90
+ { "name": "<dim_cube>", "pk": "<pk_col>", "extras": ["metric1", "metric2"] }
91
+ ],
92
+ "edges": [
93
+ {
94
+ "from": "<dim_cube>",
95
+ "to": "<fact_cube>",
96
+ "label": "<join_key>",
97
+ "from_card": "1",
98
+ "to_card": "∞"
99
+ }
100
+ ]
101
+ }
102
+ ```
103
+
104
+ `from_card` / `to_card` are the labels rendered at each end of the arrow — `"1"` or
105
+ `"∞"`. For `one_to_one` joins both ends are `"1"`.
106
+
107
+ The arrow always points _from_ dimension _to_ fact, regardless of how the relationship
108
+ was declared on the YAML.
109
+
110
+ ---
111
+
112
+ ## Step 4: Render
113
+
114
+ ```bash
115
+ python3 -c "import matplotlib" 2>/dev/null || python3 -m pip install matplotlib --quiet
116
+
117
+ python3 .claude/skills/visualize-semantic-model/scripts/render_graph.py \
118
+ --output <folder>/model-graph.png \
119
+ <<'EOF'
120
+ { ... the JSON spec from Step 3 ... }
121
+ EOF
122
+ ```
123
+
124
+ If `<folder>/model-graph.png` already exists and the user did not explicitly ask to
125
+ regenerate, ask before overwriting.
126
+
127
+ ---
128
+
129
+ ## Step 5: Show the result
130
+
131
+ Use the Read tool (not a Python `Read()` call — Read is a Claude Code tool) on the PNG
132
+ path so the image renders inline in chat:
133
+
134
+ > Read `<folder>/model-graph.png`
135
+
136
+ ---
137
+
138
+ ## Final response template
139
+
140
+ ```text
141
+ Generated: <folder>/model-graph.png
142
+
143
+ Cubes visualized: <n>
144
+ Fact spine: <cube_name>
145
+ Dimensions: <dim1>, <dim2>, ...
146
+ Edges: <dim1> → <fact> (<join_key>) [1:∞]
147
+ <dim2> → <fact> (<join_key>) [1:∞]
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Rules
153
+
154
+ - Always parse YAML — never hardcode cube names or relationships.
155
+ - Edge direction is always **dimension → fact** (arrow tail at dimension, head at fact).
156
+ - The `1` and `∞` markers are positioned by the renderer based on `from_card`/`to_card`;
157
+ set them correctly per the cardinality table above.
158
+ - Do not overwrite an existing `model-graph.png` without confirming.
159
+ - After saving, always show the image inline using the Read tool.
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ """Render a Cube.dev semantic model graph to a PNG.
3
+
4
+ Reads a JSON spec from stdin (see SKILL.md for the schema) and writes a dark-themed
5
+ directed-graph PNG to --output. Layout is automatic: the fact spine sits centered;
6
+ dimensions are evenly distributed in a ring around it.
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import math
12
+ import sys
13
+
14
+ import matplotlib
15
+
16
+ matplotlib.use("Agg")
17
+ import matplotlib.pyplot as plt
18
+ from matplotlib.patches import FancyBboxPatch
19
+
20
+ INDIGO = "#6366f1"
21
+ SKY = "#0ea5e9"
22
+ BG = "#0f1117"
23
+ NODE_FILL = "#1e293b"
24
+ TITLE_FG = "#f1f5f9"
25
+ SUB_FG = "#94a3b8"
26
+ LEGEND_FG = "#64748b"
27
+
28
+ W, H = 13.0, 7.0
29
+ SPINE_CENTER = (W / 2, H / 2)
30
+ DIM_RADIUS_X = 4.6
31
+ DIM_RADIUS_Y = 2.2
32
+
33
+
34
+ def draw_node(ax, cx, cy, w, h, title, subtitle_lines, tag, border, tag_color):
35
+ box = FancyBboxPatch(
36
+ (cx - w / 2, cy - h / 2), w, h,
37
+ boxstyle="round,pad=0.05", linewidth=2,
38
+ edgecolor=border, facecolor=NODE_FILL, zorder=3,
39
+ )
40
+ ax.add_patch(box)
41
+ ax.text(cx, cy + h / 2 - 0.28, tag, ha="center", va="top",
42
+ fontsize=7.5, fontweight="bold", color=tag_color,
43
+ fontfamily="monospace", zorder=4)
44
+ ax.text(cx, cy + 0.12, title, ha="center", va="center",
45
+ fontsize=9.5, fontweight="bold", color=TITLE_FG, zorder=4)
46
+ for i, line in enumerate(subtitle_lines):
47
+ ax.text(cx, cy - 0.32 - i * 0.32, line, ha="center", va="center",
48
+ fontsize=8, color=SUB_FG, zorder=4)
49
+
50
+
51
+ def draw_edge(ax, x1, y1, x2, y2, label, from_card, to_card, color):
52
+ ax.annotate(
53
+ "", xy=(x2, y2), xytext=(x1, y1),
54
+ arrowprops=dict(arrowstyle="->", color=color, lw=1.8,
55
+ connectionstyle="arc3,rad=0.0"),
56
+ zorder=2,
57
+ )
58
+ mx, my = (x1 + x2) / 2, (y1 + y2) / 2
59
+ dx, dy = x2 - x1, y2 - y1
60
+ length = math.hypot(-dy, dx) or 1.0
61
+ ox, oy = -dy / length * 0.22, dx / length * 0.22
62
+ ax.text(mx + ox, my + oy, label, ha="center", va="center",
63
+ fontsize=7.5, color=LEGEND_FG, zorder=4)
64
+ # cardinality markers near each endpoint
65
+ ax.text(x1 + dx * 0.08, y1 + dy * 0.08 + 0.18, from_card,
66
+ ha="center", va="center", fontsize=11, fontweight="bold",
67
+ color=SUB_FG, zorder=5)
68
+ ax.text(x2 - dx * 0.08, y2 - dy * 0.08 + 0.18, to_card,
69
+ ha="center", va="center", fontsize=11, fontweight="bold",
70
+ color=SUB_FG, zorder=5)
71
+
72
+
73
+ def fact_subtitle_lines(spine):
74
+ lines = [f"PK: {spine['pk']}"] if spine.get("pk") else []
75
+ fks = spine.get("fks") or []
76
+ # pack FKs two per line
77
+ for i in range(0, len(fks), 2):
78
+ chunk = fks[i:i + 2]
79
+ lines.append("FK: " + " · ".join(chunk))
80
+ return lines
81
+
82
+
83
+ def dim_subtitle_lines(dim):
84
+ lines = []
85
+ if dim.get("pk"):
86
+ lines.append(f"PK: {dim['pk']}")
87
+ if dim.get("extras"):
88
+ lines.append(" · ".join(dim["extras"]))
89
+ return lines
90
+
91
+
92
+ def dim_position(i, n):
93
+ # Distribute dims evenly around an ellipse centered on the spine.
94
+ # Start from the left (angle = π) and go clockwise so a single dim sits left.
95
+ angle = math.pi - (2 * math.pi * i / n)
96
+ cx = SPINE_CENTER[0] + DIM_RADIUS_X * math.cos(angle)
97
+ cy = SPINE_CENTER[1] + DIM_RADIUS_Y * math.sin(angle)
98
+ return cx, cy
99
+
100
+
101
+ def edge_endpoints(src, dst, src_w=3.0, src_h=1.6, dst_w=3.2, dst_h=2.0):
102
+ # Stop the arrow at the bounding boxes so it doesn't overlap node text.
103
+ sx, sy = src
104
+ dx, dy = dst
105
+ vx, vy = dx - sx, dy - sy
106
+ length = math.hypot(vx, vy) or 1.0
107
+ ux, uy = vx / length, vy / length
108
+ # rough rectangular inset — works fine for the box sizes we use
109
+ src_inset = max(src_w / 2 * abs(ux), src_h / 2 * abs(uy))
110
+ dst_inset = max(dst_w / 2 * abs(ux), dst_h / 2 * abs(uy))
111
+ return (sx + ux * src_inset, sy + uy * src_inset,
112
+ dx - ux * dst_inset, dy - uy * dst_inset)
113
+
114
+
115
+ def render(spec, output_path):
116
+ fig, ax = plt.subplots(figsize=(W, H))
117
+ fig.patch.set_facecolor(BG)
118
+ ax.set_facecolor(BG)
119
+ ax.set_xlim(0, W)
120
+ ax.set_ylim(0, H)
121
+ ax.axis("off")
122
+
123
+ spine = spec["fact_spine"]
124
+ dims = spec.get("dimensions", [])
125
+ edges = spec.get("edges", [])
126
+
127
+ # nodes
128
+ draw_node(ax, *SPINE_CENTER, 3.2, 2.0,
129
+ spine["name"], fact_subtitle_lines(spine),
130
+ "FACT SPINE", INDIGO, "#818cf8")
131
+
132
+ positions = {spine["name"]: SPINE_CENTER}
133
+ n = max(len(dims), 1)
134
+ for i, dim in enumerate(dims):
135
+ cx, cy = dim_position(i, n)
136
+ positions[dim["name"]] = (cx, cy)
137
+ draw_node(ax, cx, cy, 3.0, 1.6,
138
+ dim["name"], dim_subtitle_lines(dim),
139
+ "DIMENSION", SKY, "#38bdf8")
140
+
141
+ # edges
142
+ for e in edges:
143
+ src = positions.get(e["from"])
144
+ dst = positions.get(e["to"])
145
+ if not src or not dst:
146
+ continue
147
+ x1, y1, x2, y2 = edge_endpoints(src, dst)
148
+ draw_edge(ax, x1, y1, x2, y2,
149
+ e.get("label", ""),
150
+ e.get("from_card", "1"),
151
+ e.get("to_card", "∞"),
152
+ SKY)
153
+
154
+ # title
155
+ ax.text(W / 2, H - 0.3, spec.get("title", "Semantic Model"),
156
+ ha="center", va="center", fontsize=12,
157
+ fontweight="bold", color="#e2e8f0")
158
+
159
+ # legend
160
+ for i, (col, lbl) in enumerate([(INDIGO, "Fact spine"),
161
+ (SKY, "Dimension / edge")]):
162
+ lx = 4.0 + i * 2.8
163
+ ax.plot([lx, lx + 0.5], [0.35, 0.35], color=col, lw=2.5)
164
+ ax.text(lx + 0.65, 0.35, lbl, va="center", fontsize=8, color=LEGEND_FG)
165
+ ax.text(10.5, 0.35, "1 = one side ∞ = many side",
166
+ va="center", fontsize=8, color=LEGEND_FG)
167
+
168
+ plt.tight_layout(pad=0.3)
169
+ plt.savefig(output_path, dpi=160, bbox_inches="tight", facecolor=BG)
170
+ print(f"Saved: {output_path}")
171
+
172
+
173
+ def main():
174
+ parser = argparse.ArgumentParser(description=__doc__)
175
+ parser.add_argument("--output", required=True, help="Path to write the PNG.")
176
+ parser.add_argument("--input", default="-",
177
+ help="JSON spec path, or '-' for stdin (default).")
178
+ args = parser.parse_args()
179
+
180
+ raw = sys.stdin.read() if args.input == "-" else open(args.input).read()
181
+ spec = json.loads(raw)
182
+ render(spec, args.output)
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -8,6 +8,9 @@ interface StoredCredentials {
8
8
  userId?: string;
9
9
  email?: string;
10
10
  organizationId?: string;
11
+ apiUrl?: string;
12
+ authUrl?: string;
13
+ authClientId?: string;
11
14
  }
12
15
  interface AuthResult {
13
16
  success: boolean;
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@revos/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "RevOS CLI for managing RevOS platform resources",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "main": "dist/index.js",
7
8
  "types": "dist/index.d.ts",
@@ -29,25 +30,66 @@
29
30
  "org": {
30
31
  "description": "Organization management commands"
31
32
  },
32
- "overlays": {
33
- "description": "Manage Cube overlays"
33
+ "sources": {
34
+ "description": "Manage data sources (list/get/delete via API; create/update open RevOS UI)"
35
+ },
36
+ "tables": {
37
+ "description": "Manage tables (scoring models)"
38
+ },
39
+ "table-views": {
40
+ "description": "Manage table views"
41
+ },
42
+ "cubes": {
43
+ "description": "Manage Cube.dev semantic models"
44
+ },
45
+ "connections": {
46
+ "description": "Manage Connections (syncs from Sources to BigQuery)"
47
+ },
48
+ "scores": {
49
+ "description": "Manage scores attached to a scoring model"
50
+ },
51
+ "score-groups": {
52
+ "description": "Manage score groups"
53
+ },
54
+ "segments": {
55
+ "description": "Manage segments and their evaluation history"
56
+ },
57
+ "actions": {
58
+ "description": "Inspect actions and their input/params schemas"
59
+ },
60
+ "action-runs": {
61
+ "description": "Inspect action run history"
62
+ },
63
+ "ai-instructions": {
64
+ "description": "Manage AI instructions"
65
+ },
66
+ "gservice-accounts": {
67
+ "description": "Manage Google service accounts"
68
+ },
69
+ "gservice-account-keys": {
70
+ "description": "Manage Google service account keys"
34
71
  }
35
72
  }
36
73
  },
37
74
  "files": [
38
75
  "dist",
39
- "bin"
76
+ "bin",
77
+ "LICENSE"
40
78
  ],
41
79
  "dependencies": {
80
+ "@dagrejs/graphlib": "^2.2.4",
42
81
  "@inquirer/search": "^4.1.7",
43
82
  "@inquirer/select": "^4.4.2",
44
83
  "@oclif/core": "^4.2.10",
45
84
  "@oclif/plugin-warn-if-update-available": "^3.1.61",
46
85
  "@oclif/table": "^0.5.4",
47
86
  "chalk": "^4.1.2",
87
+ "microdiff": "^1.5.0",
48
88
  "open": "^10.1.0",
49
89
  "yaml": "^2.8.3",
50
- "@revos/api-client": "0.1.0"
90
+ "zod": "^4.3.5",
91
+ "zod-validation-error": "^4.0.1",
92
+ "@revos/api-client": "0.1.1"
51
93
  },
52
94
  "devDependencies": {
53
95
  "@swc/core": "^1.7.26",
@@ -56,7 +98,7 @@
56
98
  "@types/node": "^22.5.4",
57
99
  "jest": "^29.7.0",
58
100
  "tsdown": "^0.21.9",
59
- "typescript": "^5.7.2",
101
+ "typescript": "^5.9.0",
60
102
  "@revos/config-eslint": "0.0.0"
61
103
  },
62
104
  "publishConfig": {
@@ -1,19 +0,0 @@
1
- import { o as DiffResult } from "../../../../types-C_p_6rkj.mjs";
2
- import { t as BaseCommand } from "../../../../base.command-d7VW6WTp.mjs";
3
- import * as _$_oclif_core_interfaces0 from "@oclif/core/interfaces";
4
-
5
- //#region src/adapters/oclif/commands/overlays/diff.d.ts
6
- declare class OverlaysDiff extends BaseCommand<typeof OverlaysDiff> {
7
- static description: string;
8
- static flags: {
9
- dir: _$_oclif_core_interfaces0.OptionFlag<string, _$_oclif_core_interfaces0.CustomOptions>;
10
- };
11
- static args: {
12
- files: _$_oclif_core_interfaces0.Arg<string[] | undefined, Record<string, unknown>>;
13
- };
14
- run(): Promise<DiffResult>;
15
- private renderEntry;
16
- private renderDataDiff;
17
- }
18
- //#endregion
19
- export { OverlaysDiff as default };
@@ -1,80 +0,0 @@
1
- import { p as createApiClient, r as DiffService, y as getConfig } from "../../../../core-gKJ_V-K5.mjs";
2
- import { t as BaseCommand } from "../../../../base.command-DlVQ9Cqa.mjs";
3
- import chalk from "chalk";
4
- import { Args, Flags } from "@oclif/core";
5
- //#region src/adapters/oclif/commands/overlays/diff.ts
6
- var OverlaysDiff = class extends BaseCommand {
7
- static description = "Show differences between local files and remote overlays";
8
- static flags = { dir: Flags.string({
9
- char: "d",
10
- description: "Directory containing overlay files",
11
- default: "./semantic"
12
- }) };
13
- static args = { files: Args.string({
14
- description: "Specific files to diff (optional)",
15
- required: false,
16
- multiple: true
17
- }) };
18
- async run() {
19
- const { flags, args } = this;
20
- const files = args.files ?? [];
21
- const result = await new DiffService({ api: createApiClient(await getConfig()) }).execute({
22
- dir: flags.dir,
23
- files: files.length > 0 ? files : void 0
24
- });
25
- if (!this.jsonEnabled()) {
26
- if (result.entries.length === 0) {
27
- this.warn("No overlay files found.");
28
- return result;
29
- }
30
- for (const entry of result.entries) this.renderEntry(entry);
31
- this.log("");
32
- }
33
- return result;
34
- }
35
- renderEntry(entry) {
36
- this.log(chalk.bold(`\n${entry.name}:`));
37
- if (entry.isRemoteOnly) {
38
- this.log(chalk.red(" - Exists only on remote (not in local files)"));
39
- return;
40
- }
41
- if (entry.isSynced) {
42
- this.log(chalk.gray(" No changes"));
43
- return;
44
- }
45
- if (entry.isNew) {
46
- this.log(chalk.green("+ New overlay (will be created)"));
47
- for (const change of entry.changes) if (change.field === "data") {
48
- this.log(chalk.green(` ${change.field}:`));
49
- for (const line of (change.newValue ?? "").split("\n")) this.log(chalk.green(` ${line}`));
50
- } else this.log(chalk.green(` ${change.field}: ${change.newValue}`));
51
- return;
52
- }
53
- for (const change of entry.changes) if (change.field === "data") {
54
- this.log(chalk.yellow(" ~ data: (modified)"));
55
- this.renderDataDiff(change.oldValue ?? "", change.newValue ?? "");
56
- } else {
57
- this.log(chalk.red(` - ${change.field}: ${change.oldValue}`));
58
- this.log(chalk.green(` + ${change.field}: ${change.newValue}`));
59
- }
60
- }
61
- renderDataDiff(oldValue, newValue) {
62
- const oldLines = oldValue.split("\n");
63
- const newLines = newValue.split("\n");
64
- const maxLines = Math.max(oldLines.length, newLines.length);
65
- const maxDiffLines = 20;
66
- let diffCount = 0;
67
- for (let i = 0; i < maxLines && diffCount < maxDiffLines; i++) {
68
- const oldLine = oldLines[i] ?? "";
69
- const newLine = newLines[i] ?? "";
70
- if (oldLine !== newLine) {
71
- if (oldLine) this.log(chalk.red(` - ${oldLine}`));
72
- if (newLine) this.log(chalk.green(` + ${newLine}`));
73
- diffCount++;
74
- }
75
- }
76
- if (diffCount >= maxDiffLines) this.log(chalk.gray(` ... (${maxLines - maxDiffLines} more lines)`));
77
- }
78
- };
79
- //#endregion
80
- export { OverlaysDiff as default };
@@ -1,15 +0,0 @@
1
- import { u as PullResult } from "../../../../types-C_p_6rkj.mjs";
2
- import { t as BaseCommand } from "../../../../base.command-d7VW6WTp.mjs";
3
- import * as _$_oclif_core_interfaces0 from "@oclif/core/interfaces";
4
-
5
- //#region src/adapters/oclif/commands/overlays/pull.d.ts
6
- declare class OverlaysPull extends BaseCommand<typeof OverlaysPull> {
7
- static description: string;
8
- static flags: {
9
- dir: _$_oclif_core_interfaces0.OptionFlag<string, _$_oclif_core_interfaces0.CustomOptions>;
10
- name: _$_oclif_core_interfaces0.OptionFlag<string[] | undefined, _$_oclif_core_interfaces0.CustomOptions>;
11
- };
12
- run(): Promise<PullResult>;
13
- }
14
- //#endregion
15
- export { OverlaysPull as default };
@@ -1,44 +0,0 @@
1
- import { a as PullService, p as createApiClient, y as getConfig } from "../../../../core-gKJ_V-K5.mjs";
2
- import { t as BaseCommand } from "../../../../base.command-DlVQ9Cqa.mjs";
3
- import chalk from "chalk";
4
- import { Flags } from "@oclif/core";
5
- //#region src/adapters/oclif/commands/overlays/pull.ts
6
- var OverlaysPull = class extends BaseCommand {
7
- static description = "Pull overlays from RevOS API to local files";
8
- static flags = {
9
- dir: Flags.string({
10
- char: "d",
11
- description: "Directory to save overlay files",
12
- default: "./semantic"
13
- }),
14
- name: Flags.string({
15
- char: "n",
16
- description: "Specific overlay names to pull",
17
- multiple: true
18
- })
19
- };
20
- async run() {
21
- const { flags } = this;
22
- const result = await new PullService({ api: createApiClient(await getConfig()) }).execute({
23
- dir: flags.dir,
24
- names: flags.name
25
- });
26
- if (!this.jsonEnabled()) {
27
- if (result.pulled.length === 0 && result.notFound.length === 0) {
28
- this.warn("No overlays to pull.");
29
- return result;
30
- }
31
- if (result.notFound.length > 0) this.warn(`The following overlays were not found:\n${result.notFound.map((n) => ` - ${n}`).join("\n")}`);
32
- for (const item of result.pulled) this.log(chalk.green(`↓ Pulled: ${item.name} → ${item.filePath}`));
33
- for (const e of result.errors) this.log(chalk.red(`✗ Error saving ${e.name}: ${e.error}`));
34
- this.log("");
35
- this.log(chalk.bold("Summary:"));
36
- this.log(chalk.green(` Pulled: ${result.pulled.length}`));
37
- if (result.errors.length > 0) this.log(chalk.red(` Errors: ${result.errors.length}`));
38
- }
39
- if (result.errors.length > 0) this.exit(1);
40
- return result;
41
- }
42
- };
43
- //#endregion
44
- export { OverlaysPull as default };
@@ -1,18 +0,0 @@
1
- import { d as PushResult } from "../../../../types-C_p_6rkj.mjs";
2
- import { t as BaseCommand } from "../../../../base.command-d7VW6WTp.mjs";
3
- import * as _$_oclif_core_interfaces0 from "@oclif/core/interfaces";
4
-
5
- //#region src/adapters/oclif/commands/overlays/push.d.ts
6
- declare class OverlaysPush extends BaseCommand<typeof OverlaysPush> {
7
- static description: string;
8
- static flags: {
9
- dir: _$_oclif_core_interfaces0.OptionFlag<string, _$_oclif_core_interfaces0.CustomOptions>;
10
- force: _$_oclif_core_interfaces0.BooleanFlag<boolean>;
11
- };
12
- static args: {
13
- files: _$_oclif_core_interfaces0.Arg<string[] | undefined, Record<string, unknown>>;
14
- };
15
- run(): Promise<PushResult>;
16
- }
17
- //#endregion
18
- export { OverlaysPush as default };
@@ -1,59 +0,0 @@
1
- import { o as PushService, p as createApiClient, y as getConfig } from "../../../../core-gKJ_V-K5.mjs";
2
- import { t as BaseCommand } from "../../../../base.command-DlVQ9Cqa.mjs";
3
- import chalk from "chalk";
4
- import { Args, Flags } from "@oclif/core";
5
- //#region src/adapters/oclif/commands/overlays/push.ts
6
- var OverlaysPush = class extends BaseCommand {
7
- static description = "Push overlay files to RevOS API";
8
- static flags = {
9
- dir: Flags.string({
10
- char: "d",
11
- description: "Directory containing overlay files",
12
- default: "./semantic"
13
- }),
14
- force: Flags.boolean({
15
- char: "f",
16
- description: "Force push even if remote is newer",
17
- default: false
18
- })
19
- };
20
- static args = { files: Args.string({
21
- description: "Specific files to push (optional)",
22
- required: false,
23
- multiple: true
24
- }) };
25
- async run() {
26
- const { flags, args } = this;
27
- const files = args.files ?? [];
28
- const result = await new PushService({ api: createApiClient(await getConfig()) }).execute({
29
- dir: flags.dir,
30
- force: flags.force,
31
- files: files.length > 0 ? files : void 0
32
- });
33
- if (!this.jsonEnabled()) {
34
- if (result.created.length === 0 && result.updated.length === 0 && result.conflicts.length === 0 && result.errors.length === 0) {
35
- this.warn("No overlay files found.");
36
- return result;
37
- }
38
- const remoteOnlyConflicts = result.conflicts.filter((c) => c.reason === "exists remotely but not locally");
39
- if (remoteOnlyConflicts.length > 0) {
40
- for (const c of remoteOnlyConflicts) this.log(chalk.red(` - ${c.name}`));
41
- this.error("The following overlays exist remotely but not locally. Run 'revos overlays pull' first.", { exit: 1 });
42
- }
43
- for (const name of result.created) this.log(chalk.green(`+ Created: ${name}`));
44
- for (const name of result.updated) this.log(chalk.blue(`~ Updated: ${name}`));
45
- for (const c of result.conflicts) this.log(chalk.yellow(`! Conflict: ${c.name} - ${c.reason}`));
46
- for (const e of result.errors) this.log(chalk.red(`✗ Error: ${e.name} - ${e.error}`));
47
- this.log("");
48
- this.log(chalk.bold("Summary:"));
49
- if (result.created.length > 0) this.log(chalk.green(` Created: ${result.created.length}`));
50
- if (result.updated.length > 0) this.log(chalk.blue(` Updated: ${result.updated.length}`));
51
- if (result.conflicts.length > 0) this.log(chalk.yellow(` Conflicts: ${result.conflicts.length}`));
52
- if (result.errors.length > 0) this.log(chalk.red(` Errors: ${result.errors.length}`));
53
- }
54
- if (result.errors.length > 0 || result.conflicts.length > 0) this.exit(1);
55
- return result;
56
- }
57
- };
58
- //#endregion
59
- export { OverlaysPush as default };