@revos/cli 0.2.1 → 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 (157) hide show
  1. package/README.md +271 -71
  2. package/dist/adapters/oclif/commands/action-runs/get.mjs +1 -1
  3. package/dist/adapters/oclif/commands/action-runs/list.mjs +8 -2
  4. package/dist/adapters/oclif/commands/actions/get-input-schema.mjs +2 -2
  5. package/dist/adapters/oclif/commands/actions/get-params-schema.mjs +2 -2
  6. package/dist/adapters/oclif/commands/actions/get.mjs +1 -1
  7. package/dist/adapters/oclif/commands/actions/list.mjs +8 -4
  8. package/dist/adapters/oclif/commands/ai-instructions/create.mjs +1 -1
  9. package/dist/adapters/oclif/commands/ai-instructions/delete.mjs +1 -1
  10. package/dist/adapters/oclif/commands/ai-instructions/get.mjs +1 -1
  11. package/dist/adapters/oclif/commands/ai-instructions/list.mjs +8 -2
  12. package/dist/adapters/oclif/commands/ai-instructions/update.mjs +1 -1
  13. package/dist/adapters/oclif/commands/api.d.mts +11 -0
  14. package/dist/adapters/oclif/commands/api.mjs +112 -0
  15. package/dist/adapters/oclif/commands/apply.d.mts +28 -0
  16. package/dist/adapters/oclif/commands/apply.mjs +77 -0
  17. package/dist/adapters/oclif/commands/auth/login.d.mts +5 -4
  18. package/dist/adapters/oclif/commands/auth/login.mjs +22 -11
  19. package/dist/adapters/oclif/commands/auth/logout.d.mts +1 -1
  20. package/dist/adapters/oclif/commands/auth/logout.mjs +2 -2
  21. package/dist/adapters/oclif/commands/auth/status.d.mts +2 -2
  22. package/dist/adapters/oclif/commands/auth/status.mjs +2 -2
  23. package/dist/adapters/oclif/commands/connections/create.d.mts +6 -0
  24. package/dist/adapters/oclif/commands/connections/create.mjs +8 -0
  25. package/dist/adapters/oclif/commands/connections/delete.d.mts +6 -0
  26. package/dist/adapters/oclif/commands/connections/delete.mjs +8 -0
  27. package/dist/adapters/oclif/commands/connections/get.d.mts +6 -0
  28. package/dist/adapters/oclif/commands/connections/get.mjs +8 -0
  29. package/dist/adapters/oclif/commands/connections/list.d.mts +6 -0
  30. package/dist/adapters/oclif/commands/connections/list.mjs +14 -0
  31. package/dist/adapters/oclif/commands/connections/update.d.mts +6 -0
  32. package/dist/adapters/oclif/commands/connections/update.mjs +8 -0
  33. package/dist/adapters/oclif/commands/cubes/create.d.mts +6 -0
  34. package/dist/adapters/oclif/commands/cubes/create.mjs +8 -0
  35. package/dist/adapters/oclif/commands/cubes/delete.d.mts +6 -0
  36. package/dist/adapters/oclif/commands/cubes/delete.mjs +8 -0
  37. package/dist/adapters/oclif/commands/cubes/get.d.mts +6 -0
  38. package/dist/adapters/oclif/commands/cubes/get.mjs +8 -0
  39. package/dist/adapters/oclif/commands/cubes/list.d.mts +6 -0
  40. package/dist/adapters/oclif/commands/cubes/list.mjs +13 -0
  41. package/dist/adapters/oclif/commands/cubes/update.d.mts +6 -0
  42. package/dist/adapters/oclif/commands/cubes/update.mjs +8 -0
  43. package/dist/adapters/oclif/commands/diff.d.mts +27 -0
  44. package/dist/adapters/oclif/commands/diff.mjs +66 -0
  45. package/dist/adapters/oclif/commands/gservice-account-keys/get.mjs +1 -1
  46. package/dist/adapters/oclif/commands/gservice-account-keys/reveal.mjs +2 -2
  47. package/dist/adapters/oclif/commands/gservice-accounts/create.mjs +1 -1
  48. package/dist/adapters/oclif/commands/gservice-accounts/delete.mjs +1 -1
  49. package/dist/adapters/oclif/commands/gservice-accounts/get.mjs +1 -1
  50. package/dist/adapters/oclif/commands/gservice-accounts/list.mjs +7 -2
  51. package/dist/adapters/oclif/commands/init.d.mts +2 -1
  52. package/dist/adapters/oclif/commands/init.mjs +26 -23
  53. package/dist/adapters/oclif/commands/org/create.mjs +1 -1
  54. package/dist/adapters/oclif/commands/org/current.d.mts +2 -2
  55. package/dist/adapters/oclif/commands/org/current.mjs +2 -2
  56. package/dist/adapters/oclif/commands/org/get.mjs +1 -1
  57. package/dist/adapters/oclif/commands/org/list.d.mts +3 -11
  58. package/dist/adapters/oclif/commands/org/list.mjs +26 -26
  59. package/dist/adapters/oclif/commands/org/switch.d.mts +3 -2
  60. package/dist/adapters/oclif/commands/org/switch.mjs +10 -3
  61. package/dist/adapters/oclif/commands/pull.d.mts +28 -0
  62. package/dist/adapters/oclif/commands/pull.mjs +88 -0
  63. package/dist/adapters/oclif/commands/score-groups/create.mjs +3 -2
  64. package/dist/adapters/oclif/commands/score-groups/delete.mjs +1 -1
  65. package/dist/adapters/oclif/commands/score-groups/get.mjs +1 -1
  66. package/dist/adapters/oclif/commands/score-groups/list.mjs +3 -2
  67. package/dist/adapters/oclif/commands/score-groups/update.mjs +1 -1
  68. package/dist/adapters/oclif/commands/scores/create.mjs +3 -2
  69. package/dist/adapters/oclif/commands/scores/delete.mjs +1 -1
  70. package/dist/adapters/oclif/commands/scores/list.mjs +3 -2
  71. package/dist/adapters/oclif/commands/scores/update.mjs +1 -1
  72. package/dist/adapters/oclif/commands/segments/create.mjs +1 -1
  73. package/dist/adapters/oclif/commands/segments/delete.mjs +1 -1
  74. package/dist/adapters/oclif/commands/segments/evaluate.mjs +2 -2
  75. package/dist/adapters/oclif/commands/segments/get-evaluation-history.mjs +2 -2
  76. package/dist/adapters/oclif/commands/segments/get-version.mjs +2 -2
  77. package/dist/adapters/oclif/commands/segments/get.mjs +1 -1
  78. package/dist/adapters/oclif/commands/segments/list-versions.mjs +16 -5
  79. package/dist/adapters/oclif/commands/segments/list.mjs +9 -2
  80. package/dist/adapters/oclif/commands/segments/restore-version.mjs +2 -2
  81. package/dist/adapters/oclif/commands/segments/update.mjs +1 -1
  82. package/dist/adapters/oclif/commands/sources/create.d.mts +11 -0
  83. package/dist/adapters/oclif/commands/sources/create.mjs +16 -0
  84. package/dist/adapters/oclif/commands/sources/delete.d.mts +6 -0
  85. package/dist/adapters/oclif/commands/sources/delete.mjs +8 -0
  86. package/dist/adapters/oclif/commands/sources/get.d.mts +6 -0
  87. package/dist/adapters/oclif/commands/sources/get.mjs +8 -0
  88. package/dist/adapters/oclif/commands/sources/list-streams.d.mts +6 -0
  89. package/dist/adapters/oclif/commands/sources/list-streams.mjs +31 -0
  90. package/dist/adapters/oclif/commands/sources/list.d.mts +6 -0
  91. package/dist/adapters/oclif/commands/sources/list.mjs +13 -0
  92. package/dist/adapters/oclif/commands/{integrations/get.d.mts → sources/update.d.mts} +4 -4
  93. package/dist/adapters/oclif/commands/sources/update.mjs +21 -0
  94. package/dist/adapters/oclif/commands/status.d.mts +26 -0
  95. package/dist/adapters/oclif/commands/status.mjs +77 -0
  96. package/dist/adapters/oclif/commands/table-views/create.mjs +3 -2
  97. package/dist/adapters/oclif/commands/table-views/delete.mjs +1 -1
  98. package/dist/adapters/oclif/commands/table-views/list.mjs +3 -2
  99. package/dist/adapters/oclif/commands/table-views/update.mjs +1 -1
  100. package/dist/adapters/oclif/commands/tables/create.mjs +1 -1
  101. package/dist/adapters/oclif/commands/tables/delete.mjs +1 -1
  102. package/dist/adapters/oclif/commands/tables/get.mjs +1 -1
  103. package/dist/adapters/oclif/commands/tables/list.mjs +3 -2
  104. package/dist/adapters/oclif/commands/tables/update.mjs +1 -1
  105. package/dist/{base.command-d7VW6WTp.d.mts → base.command-D7X3ZNtY.d.mts} +0 -1
  106. package/dist/{base.command-YiwlGlKs.mjs → base.command-cV5d65r8.mjs} +15 -12
  107. package/dist/chunk-CfYAbeIz.mjs +13 -0
  108. package/dist/core-CMrP5BQS.mjs +2378 -0
  109. package/dist/{factory-BrFKT8t-.mjs → factory-C6XLqhT9.mjs} +44 -10
  110. package/dist/iac-render-BSZZEP0n.mjs +17 -0
  111. package/dist/index-BqKwXXAo.d.mts +598 -0
  112. package/dist/index.d.mts +3 -4
  113. package/dist/index.mjs +2 -2
  114. package/dist/{presets-D9b6IWKy.mjs → presets-CJbFbHlw.mjs} +35 -8
  115. package/dist/templates/.claude/settings.json +39 -0
  116. package/dist/templates/.devcontainer/setup.sh +3 -0
  117. package/dist/templates/AGENTS.md +33 -20
  118. package/dist/templates/dbt/dbt_project.yml +2 -2
  119. package/dist/templates/skills/create-connections/SKILL.md +210 -0
  120. package/dist/templates/skills/create-connections/references/mappers.md +152 -0
  121. package/dist/templates/skills/{create-semantic-model → create-cubes}/SKILL.md +20 -18
  122. package/dist/templates/skills/create-cubes/references/bq-pk-fk-conventions.md +183 -0
  123. package/dist/templates/skills/{create-semantic-model → create-cubes}/references/cube-examples.md +2 -2
  124. package/dist/templates/skills/create-cubes/references/hubspot-entities.md +289 -0
  125. package/dist/templates/skills/create-cubes/references/jira-entities.md +201 -0
  126. package/dist/templates/skills/create-cubes/references/netsuite-entities.md +121 -0
  127. package/dist/templates/skills/create-cubes/references/stripe-entities.md +114 -0
  128. package/dist/templates/skills/create-dbt-transformations/SKILL.md +43 -22
  129. package/dist/templates/skills/create-dbt-transformations/references/edge-cases.md +20 -2
  130. package/dist/templates/skills/create-dbt-transformations/references/schema-conventions.md +21 -7
  131. package/dist/templates/skills/create-dbt-transformations/references/sql-templates.md +34 -20
  132. package/dist/templates/skills/explore-lakehouse/SKILL.md +3 -3
  133. package/dist/templates/skills/load-sample-data/SKILL.md +1 -1
  134. package/dist/templates/skills/visualize-semantic-model/SKILL.md +159 -0
  135. package/dist/templates/skills/visualize-semantic-model/scripts/render_graph.py +186 -0
  136. package/dist/{types-Y_ht_ja5.d.mts → types-CGjxcj4L.d.mts} +3 -0
  137. package/package.json +44 -7
  138. package/dist/adapters/oclif/commands/integrations/create.d.mts +0 -11
  139. package/dist/adapters/oclif/commands/integrations/create.mjs +0 -16
  140. package/dist/adapters/oclif/commands/integrations/get.mjs +0 -21
  141. package/dist/adapters/oclif/commands/integrations/list.d.mts +0 -11
  142. package/dist/adapters/oclif/commands/integrations/list.mjs +0 -16
  143. package/dist/adapters/oclif/commands/integrations/update.d.mts +0 -15
  144. package/dist/adapters/oclif/commands/integrations/update.mjs +0 -21
  145. package/dist/adapters/oclif/commands/overlays/diff.d.mts +0 -19
  146. package/dist/adapters/oclif/commands/overlays/diff.mjs +0 -80
  147. package/dist/adapters/oclif/commands/overlays/pull.d.mts +0 -15
  148. package/dist/adapters/oclif/commands/overlays/pull.mjs +0 -45
  149. package/dist/adapters/oclif/commands/overlays/push.d.mts +0 -18
  150. package/dist/adapters/oclif/commands/overlays/push.mjs +0 -59
  151. package/dist/adapters/oclif/commands/overlays/status.d.mts +0 -18
  152. package/dist/adapters/oclif/commands/overlays/status.mjs +0 -53
  153. package/dist/core-jpFPylBb.mjs +0 -997
  154. package/dist/index-DD2Vr-pu.d.mts +0 -193
  155. package/dist/types-C_p_6rkj.d.mts +0 -69
  156. /package/dist/templates/skills/{create-semantic-model → create-cubes}/references/key-patterns.md +0 -0
  157. /package/dist/templates/skills/{create-semantic-model → create-cubes}/references/validation-queries.md +0 -0
@@ -0,0 +1,2378 @@
1
+ import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import * as os from "os";
5
+ import { createServer } from "node:http";
6
+ import * as crypto from "crypto";
7
+ import { Client, client } from "@revos/api-client";
8
+ import { parseAllDocuments, parseDocument, stringify } from "yaml";
9
+ import { fromError } from "zod-validation-error";
10
+ import { z } from "zod";
11
+ import microdiff from "microdiff";
12
+ import chalk from "chalk";
13
+ import graphlib from "@dagrejs/graphlib";
14
+ import { execFileSync } from "child_process";
15
+ import search from "@inquirer/search";
16
+ //#region src/core/errors.ts
17
+ var ApiError = class extends Error {
18
+ name = "ApiError";
19
+ constructor(message, status, statusText, url, body) {
20
+ super(message);
21
+ this.status = status;
22
+ this.statusText = statusText;
23
+ this.url = url;
24
+ this.body = body;
25
+ }
26
+ };
27
+ //#endregion
28
+ //#region src/core/auth/credentials-store.ts
29
+ const REVOS_DIR = path.join(os.homedir(), ".revos");
30
+ const CREDENTIALS_FILE = path.join(REVOS_DIR, "credentials.json");
31
+ const isPosix = process.platform !== "win32";
32
+ function getCredentialsPath() {
33
+ return CREDENTIALS_FILE;
34
+ }
35
+ function loadCredentials() {
36
+ try {
37
+ if (!fs.existsSync(CREDENTIALS_FILE)) return null;
38
+ const content = fs.readFileSync(CREDENTIALS_FILE, "utf-8");
39
+ return JSON.parse(content);
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+ function saveCredentials(credentials) {
45
+ if (!fs.existsSync(REVOS_DIR)) fs.mkdirSync(REVOS_DIR, isPosix ? { mode: 448 } : void 0);
46
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(credentials, null, 2), isPosix ? {
47
+ mode: 384,
48
+ encoding: "utf-8"
49
+ } : { encoding: "utf-8" });
50
+ }
51
+ function deleteCredentials() {
52
+ try {
53
+ if (fs.existsSync(CREDENTIALS_FILE)) {
54
+ fs.unlinkSync(CREDENTIALS_FILE);
55
+ return true;
56
+ }
57
+ return false;
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+ function isTokenExpired(credentials) {
63
+ if (!credentials.expiresAt) return false;
64
+ return credentials.expiresAt < Date.now() + 300 * 1e3;
65
+ }
66
+ //#endregion
67
+ //#region src/core/auth/oauth-server.ts
68
+ const CALLBACK_TIMEOUT_MS = 120 * 1e3;
69
+ const OAUTH_CALLBACK_PORTS = [
70
+ 54321,
71
+ 54322,
72
+ 54323
73
+ ];
74
+ async function startOAuthServer() {
75
+ for (const port of OAUTH_CALLBACK_PORTS) try {
76
+ return await tryStartServer(port);
77
+ } catch (err) {
78
+ if (err.code !== "EADDRINUSE") throw err;
79
+ }
80
+ throw new Error(`Could not start OAuth callback server. Ports ${OAUTH_CALLBACK_PORTS.join(", ")} are all in use.`);
81
+ }
82
+ function tryStartServer(port) {
83
+ return new Promise((resolve, reject) => {
84
+ let callbackResolve;
85
+ let callbackReject;
86
+ let timeoutId;
87
+ const callbackPromise = new Promise((res, rej) => {
88
+ callbackResolve = res;
89
+ callbackReject = rej;
90
+ });
91
+ const server = createServer((req, res) => {
92
+ const url = new URL(req.url ?? "/", `http://localhost:${port}`);
93
+ if (url.pathname !== "/callback" || req.method !== "GET") {
94
+ res.writeHead(404);
95
+ res.end();
96
+ return;
97
+ }
98
+ const code = url.searchParams.get("code") ?? void 0;
99
+ const state = url.searchParams.get("state") ?? void 0;
100
+ const error = url.searchParams.get("error") ?? void 0;
101
+ const errorDescription = url.searchParams.get("error_description") ?? void 0;
102
+ if (error) {
103
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
104
+ res.end(getErrorHtml(errorDescription || error));
105
+ callbackReject(new Error(errorDescription || error));
106
+ return;
107
+ }
108
+ if (!code || !state) {
109
+ res.writeHead(400, { "Content-Type": "text/html; charset=utf-8" });
110
+ res.end(getErrorHtml("Missing code or state parameter"));
111
+ callbackReject(/* @__PURE__ */ new Error("Missing code or state parameter"));
112
+ return;
113
+ }
114
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
115
+ res.end(getSuccessHtml());
116
+ callbackResolve({
117
+ code,
118
+ state
119
+ });
120
+ });
121
+ server.listen(port, () => {
122
+ const shutdown = () => {
123
+ clearTimeout(timeoutId);
124
+ server.close();
125
+ };
126
+ const waitForCallback = async () => {
127
+ timeoutId = setTimeout(() => {
128
+ callbackReject(/* @__PURE__ */ new Error("Authentication timed out. Please try again."));
129
+ shutdown();
130
+ }, CALLBACK_TIMEOUT_MS);
131
+ try {
132
+ const result = await callbackPromise;
133
+ shutdown();
134
+ return result;
135
+ } catch (error) {
136
+ shutdown();
137
+ throw error;
138
+ }
139
+ };
140
+ resolve({
141
+ port,
142
+ waitForCallback,
143
+ shutdown
144
+ });
145
+ });
146
+ server.on("error", (err) => {
147
+ reject(err);
148
+ });
149
+ });
150
+ }
151
+ function getSuccessHtml() {
152
+ return `
153
+ <!DOCTYPE html>
154
+ <html>
155
+ <head>
156
+ <meta charset="utf-8">
157
+ <link rel="icon" href="https://www.revos.ai/favicons/favicon.ico">
158
+ <title>RevOS CLI - Authentication Successful</title>
159
+ <style>
160
+ body {
161
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
162
+ display: flex;
163
+ justify-content: center;
164
+ align-items: center;
165
+ height: 100vh;
166
+ margin: 0;
167
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
168
+ }
169
+ .container {
170
+ text-align: center;
171
+ background: white;
172
+ padding: 3rem;
173
+ border-radius: 12px;
174
+ box-shadow: 0 4px 24px rgba(0,0,0,0.15);
175
+ }
176
+ .checkmark {
177
+ font-size: 4rem;
178
+ margin-bottom: 1rem;
179
+ }
180
+ h1 { color: #333; margin-bottom: 0.5rem; }
181
+ p { color: #666; }
182
+ </style>
183
+ </head>
184
+ <body>
185
+ <div class="container">
186
+ <div class="checkmark">✓</div>
187
+ <h1>Authentication Successful</h1>
188
+ <p>You can close this window and return to the terminal.</p>
189
+ </div>
190
+ </body>
191
+ </html>
192
+ `;
193
+ }
194
+ function getErrorHtml(message) {
195
+ return `
196
+ <!DOCTYPE html>
197
+ <html>
198
+ <head>
199
+ <meta charset="utf-8">
200
+ <link rel="icon" href="https://www.revos.ai/favicons/favicon.ico">
201
+ <title>RevOS CLI - Authentication Failed</title>
202
+ <style>
203
+ body {
204
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
205
+ display: flex;
206
+ justify-content: center;
207
+ align-items: center;
208
+ height: 100vh;
209
+ margin: 0;
210
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
211
+ }
212
+ .container {
213
+ text-align: center;
214
+ background: white;
215
+ padding: 3rem;
216
+ border-radius: 12px;
217
+ box-shadow: 0 4px 24px rgba(0,0,0,0.15);
218
+ }
219
+ .error-icon {
220
+ font-size: 4rem;
221
+ margin-bottom: 1rem;
222
+ }
223
+ h1 { color: #333; margin-bottom: 0.5rem; }
224
+ p { color: #666; }
225
+ .error-message { color: #e53e3e; margin-top: 1rem; }
226
+ </style>
227
+ </head>
228
+ <body>
229
+ <div class="container">
230
+ <div class="error-icon">✗</div>
231
+ <h1>Authentication Failed</h1>
232
+ <p class="error-message">${escapeHtml(message)}</p>
233
+ <p>Please close this window and try again.</p>
234
+ </div>
235
+ </body>
236
+ </html>
237
+ `;
238
+ }
239
+ function escapeHtml(text) {
240
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
241
+ }
242
+ //#endregion
243
+ //#region src/core/auth/clerk-oauth.ts
244
+ const AUTH_ENVS = {
245
+ production: {
246
+ apiUrl: "https://api.revos.ai",
247
+ authUrl: "https://clerk.revos.ai",
248
+ authClientId: "Hkj5lEtyyF5DUQX6"
249
+ },
250
+ development: {
251
+ apiUrl: "https://api.revos.dev",
252
+ authUrl: "https://strong-minnow-46.clerk.accounts.dev",
253
+ authClientId: "o3eTaWPmeJAMfRPw"
254
+ }
255
+ };
256
+ let configOverride = {};
257
+ function setAuthConfig(config) {
258
+ configOverride = config;
259
+ }
260
+ function getActiveAuthConfig() {
261
+ return {
262
+ authUrl: getAuthUrl(),
263
+ authClientId: getAuthClientId()
264
+ };
265
+ }
266
+ function getAuthUrl() {
267
+ return configOverride.authUrl || process.env.REVOS_AUTH_URL || AUTH_ENVS.production.authUrl;
268
+ }
269
+ function getAuthClientId() {
270
+ return configOverride.authClientId || process.env.REVOS_AUTH_CLIENT_ID || AUTH_ENVS.production.authClientId;
271
+ }
272
+ function setAuthEnv(env) {
273
+ const config = AUTH_ENVS[env];
274
+ setAuthConfig({
275
+ authUrl: config.authUrl,
276
+ authClientId: config.authClientId
277
+ });
278
+ }
279
+ function generatePKCEChallenge() {
280
+ const codeVerifier = crypto.randomBytes(32).toString("base64url");
281
+ return {
282
+ codeVerifier,
283
+ codeChallenge: crypto.createHash("sha256").update(codeVerifier).digest("base64url"),
284
+ state: crypto.randomBytes(16).toString("base64url")
285
+ };
286
+ }
287
+ function buildAuthorizationUrl(redirectUri, pkce) {
288
+ const authUrl = getAuthUrl();
289
+ const clientId = getAuthClientId();
290
+ return `${authUrl}/oauth/authorize?${new URLSearchParams({
291
+ client_id: clientId,
292
+ redirect_uri: redirectUri,
293
+ response_type: "code",
294
+ scope: "profile email offline_access",
295
+ code_challenge: pkce.codeChallenge,
296
+ code_challenge_method: "S256",
297
+ state: pkce.state
298
+ }).toString()}`;
299
+ }
300
+ async function exchangeCodeForTokens(code, redirectUri, codeVerifier) {
301
+ const authUrl = getAuthUrl();
302
+ const clientId = getAuthClientId();
303
+ const params = new URLSearchParams({
304
+ grant_type: "authorization_code",
305
+ code,
306
+ redirect_uri: redirectUri,
307
+ code_verifier: codeVerifier,
308
+ client_id: clientId
309
+ });
310
+ const response = await fetch(`${authUrl}/oauth/token`, {
311
+ method: "POST",
312
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
313
+ body: params.toString()
314
+ });
315
+ if (!response.ok) {
316
+ const error = await response.text();
317
+ throw new Error(`Token exchange failed: ${error}`);
318
+ }
319
+ return response.json();
320
+ }
321
+ async function getUserInfo(accessToken) {
322
+ const authUrl = getAuthUrl();
323
+ const response = await fetch(`${authUrl}/oauth/userinfo`, { headers: { Authorization: `Bearer ${accessToken}` } });
324
+ if (!response.ok) {
325
+ const error = await response.text();
326
+ throw new Error(`Failed to get user info: ${error}`);
327
+ }
328
+ return response.json();
329
+ }
330
+ function tokenResponseToCredentials(tokenResponse, userInfo) {
331
+ const expiresAt = tokenResponse.expires_in ? Date.now() + tokenResponse.expires_in * 1e3 : void 0;
332
+ return {
333
+ accessToken: tokenResponse.access_token,
334
+ refreshToken: tokenResponse.refresh_token,
335
+ expiresAt,
336
+ userId: userInfo.sub,
337
+ email: userInfo.email
338
+ };
339
+ }
340
+ async function refreshAccessToken(refreshToken) {
341
+ const authUrl = getAuthUrl();
342
+ const clientId = getAuthClientId();
343
+ const params = new URLSearchParams({
344
+ grant_type: "refresh_token",
345
+ refresh_token: refreshToken,
346
+ client_id: clientId
347
+ });
348
+ const response = await fetch(`${authUrl}/oauth/token`, {
349
+ method: "POST",
350
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
351
+ body: params.toString()
352
+ });
353
+ if (!response.ok) {
354
+ const error = await response.text();
355
+ throw new Error(`Token refresh failed: ${error}`);
356
+ }
357
+ return response.json();
358
+ }
359
+ //#endregion
360
+ //#region src/core/config.ts
361
+ const DEFAULT_API_URL = "https://api.revos.ai";
362
+ async function getStoredAccessToken() {
363
+ const credentials = loadCredentials();
364
+ if (!credentials) return null;
365
+ if (!isTokenExpired(credentials)) return credentials.accessToken;
366
+ if (!credentials.refreshToken) return null;
367
+ try {
368
+ const tokenResponse = await refreshAccessToken(credentials.refreshToken);
369
+ const newCredentials = tokenResponseToCredentials(tokenResponse, await getUserInfo(tokenResponse.access_token));
370
+ newCredentials.organizationId = credentials.organizationId;
371
+ saveCredentials(newCredentials);
372
+ return newCredentials.accessToken;
373
+ } catch {
374
+ return null;
375
+ }
376
+ }
377
+ function getStoredOrganizationId() {
378
+ return loadCredentials()?.organizationId ?? void 0;
379
+ }
380
+ async function getConfig() {
381
+ const token = process.env.REVOS_TOKEN || await getStoredAccessToken();
382
+ if (!token) throw new Error("Not authenticated. Run 'revos auth login' or set REVOS_TOKEN environment variable.");
383
+ return {
384
+ apiUrl: process.env.REVOS_API_URL || "https://api.revos.ai",
385
+ token,
386
+ organizationId: process.env.REVOS_ORG_ID || getStoredOrganizationId()
387
+ };
388
+ }
389
+ //#endregion
390
+ //#region src/core/utils.ts
391
+ function formatError(error) {
392
+ return error instanceof Error ? error.message : String(error);
393
+ }
394
+ function sanitizeFileName(name) {
395
+ return name.replace(/[/\\:*?"<>|]/g, "_");
396
+ }
397
+ //#endregion
398
+ //#region src/core/url.ts
399
+ const DEFAULT_APP_URL = "https://app.revos.dev";
400
+ /**
401
+ * Derive the RevOS app (frontend) URL from the API URL.
402
+ * `https://api.revos.ai` → `https://app.revos.ai`
403
+ * `https://api.revos.dev` → `https://app.revos.dev`
404
+ */
405
+ function resolveAppUrl(apiUrl) {
406
+ try {
407
+ const parsed = new URL(apiUrl);
408
+ const host = parsed.hostname;
409
+ if (host.startsWith("api.")) {
410
+ parsed.hostname = host.replace(/^api\./, "app.");
411
+ parsed.pathname = "/";
412
+ return parsed.origin;
413
+ }
414
+ } catch {}
415
+ return DEFAULT_APP_URL;
416
+ }
417
+ //#endregion
418
+ //#region src/core/api/create-client.ts
419
+ function createApiClient(config) {
420
+ const headers = {};
421
+ if (config.organizationId) headers["X-Revos-Org"] = config.organizationId;
422
+ client.setConfig({
423
+ baseUrl: config.apiUrl,
424
+ auth: config.token,
425
+ headers
426
+ });
427
+ return new Client({ client });
428
+ }
429
+ /**
430
+ * Wraps an SDK call so a 404 returns `null` instead of throwing.
431
+ */
432
+ async function getOrNull(fn) {
433
+ try {
434
+ return await fn();
435
+ } catch (err) {
436
+ if (err instanceof ApiError && err.status === 404) return null;
437
+ throw err;
438
+ }
439
+ }
440
+ /**
441
+ * Walk a keyset-paginated list endpoint until exhausted. The cursor is
442
+ * opaque and sequential by definition — pages must be fetched in order.
443
+ */
444
+ async function walkAllPages(fetch) {
445
+ const out = [];
446
+ let pageToken;
447
+ do {
448
+ const res = await fetch(pageToken);
449
+ out.push(...unwrap(res));
450
+ pageToken = res.data?.metadata.nextPageToken ?? void 0;
451
+ } while (pageToken);
452
+ return out;
453
+ }
454
+ function unwrap(result) {
455
+ if (result.error) {
456
+ const body = result.error;
457
+ const status = result.response?.status ?? 0;
458
+ const statusText = result.response?.statusText ?? "";
459
+ const url = result.request?.url ?? "";
460
+ let message;
461
+ if (typeof body === "object" && body !== null && "message" in body) {
462
+ const msg = body.message;
463
+ message = typeof msg === "string" ? msg : JSON.stringify(msg);
464
+ } else if (typeof body === "string") message = body;
465
+ else message = JSON.stringify(body);
466
+ throw new ApiError(message, status, statusText, url, body);
467
+ }
468
+ return result.data.data;
469
+ }
470
+ //#endregion
471
+ //#region src/core/iac/types.ts
472
+ const API_VERSION = "revos/v1";
473
+ var IacAggregateError = class extends Error {
474
+ constructor(errors) {
475
+ super(formatErrors(errors));
476
+ this.errors = errors;
477
+ this.name = "IacAggregateError";
478
+ }
479
+ };
480
+ function formatErrors(errors) {
481
+ if (errors.length === 0) return "no errors";
482
+ return errors.map((e) => {
483
+ const loc = e.source ? `${e.source.file}${e.source.line ? `:${e.source.line}` : ""}${e.source.column ? `:${e.source.column}` : ""}` : "";
484
+ const head = loc ? `${loc} — ` : "";
485
+ const tail = e.hint ? `\n hint: ${e.hint}` : "";
486
+ return `${head}${e.message}${tail}`;
487
+ }).join("\n");
488
+ }
489
+ //#endregion
490
+ //#region src/core/iac/project.ts
491
+ const PROJECT_FILE = "revos.yaml";
492
+ const PROJECT_KIND = "Project";
493
+ var ProjectNotFoundError = class extends Error {
494
+ constructor(cwd) {
495
+ super(`Not in a revos project (no ${PROJECT_FILE} found in ${cwd} or any parent). Run \`revos init\` to create one.`);
496
+ this.name = "ProjectNotFoundError";
497
+ }
498
+ };
499
+ function discoverProject(opts = {}) {
500
+ const explicit = opts.projectPath ?? process.env.REVOS_PROJECT;
501
+ if (explicit) {
502
+ const resolved = path.resolve(explicit);
503
+ return readProjectFile(fs.statSync(resolved).isDirectory() ? path.join(resolved, PROJECT_FILE) : resolved);
504
+ }
505
+ const startDir = path.resolve(opts.cwd ?? process.cwd());
506
+ let current = startDir;
507
+ while (true) {
508
+ const candidate = path.join(current, PROJECT_FILE);
509
+ if (fs.existsSync(candidate)) {
510
+ const project = tryReadProjectFile(candidate);
511
+ if (project) return project;
512
+ }
513
+ const parent = path.dirname(current);
514
+ if (parent === current) break;
515
+ current = parent;
516
+ }
517
+ throw new ProjectNotFoundError(startDir);
518
+ }
519
+ function tryReadProjectFile(filePath) {
520
+ const doc = parseDocument(fs.readFileSync(filePath, "utf-8"));
521
+ if (doc.errors.length > 0) throw new Error(`${filePath}: invalid YAML — ${doc.errors[0].message}`);
522
+ const json = doc.toJS();
523
+ if (json.apiVersion !== "revos/v1" || json.kind !== "Project") return null;
524
+ if (!json.metadata?.orgId) throw new Error(`${filePath}: metadata.orgId is required`);
525
+ return {
526
+ path: filePath,
527
+ metadata: {
528
+ name: json.metadata.name,
529
+ orgId: json.metadata.orgId
530
+ }
531
+ };
532
+ }
533
+ function readProjectFile(filePath) {
534
+ const project = tryReadProjectFile(filePath);
535
+ if (!project) throw new Error(`${filePath}: not a revos Project file (expected apiVersion=${API_VERSION} kind=${PROJECT_KIND})`);
536
+ return project;
537
+ }
538
+ function projectRoot(project) {
539
+ return path.dirname(project.path);
540
+ }
541
+ function writeProjectFile(projectDir, metadata) {
542
+ const lines = [
543
+ "apiVersion: revos/v1",
544
+ "kind: Project",
545
+ "metadata:"
546
+ ];
547
+ if (metadata.name) lines.push(` name: ${metadata.name}`);
548
+ lines.push(` orgId: ${metadata.orgId}`, "");
549
+ const filePath = path.join(projectDir, PROJECT_FILE);
550
+ fs.writeFileSync(filePath, lines.join("\n"), "utf-8");
551
+ return filePath;
552
+ }
553
+ //#endregion
554
+ //#region src/core/iac/parser/load.ts
555
+ const HARDCODED_IGNORES = new Set([
556
+ ".git",
557
+ "node_modules",
558
+ "dist",
559
+ ".revos",
560
+ ".turbo",
561
+ ".next"
562
+ ]);
563
+ function scanYamlFiles(opts) {
564
+ const ignored = new Set([...HARDCODED_IGNORES, ...opts.ignored ?? []]);
565
+ const start = path.resolve(opts.startPath ?? opts.root);
566
+ const out = [];
567
+ walk$1(start, opts.root, ignored, out);
568
+ return out.sort((a, b) => a.path.localeCompare(b.path));
569
+ }
570
+ function walk$1(dir, root, ignored, out) {
571
+ let entries;
572
+ try {
573
+ entries = fs.readdirSync(dir, { withFileTypes: true });
574
+ } catch {
575
+ return;
576
+ }
577
+ for (const entry of entries) {
578
+ if (ignored.has(entry.name)) continue;
579
+ const fullPath = path.join(dir, entry.name);
580
+ if (entry.isDirectory()) walk$1(fullPath, root, ignored, out);
581
+ else if (entry.isFile() && isYamlFile(entry.name)) {
582
+ if (path.relative(root, fullPath) === "revos.yaml" || entry.name === "revos.yaml") continue;
583
+ const content = fs.readFileSync(fullPath, "utf-8");
584
+ out.push({
585
+ path: fullPath,
586
+ content
587
+ });
588
+ }
589
+ }
590
+ }
591
+ function isYamlFile(name) {
592
+ return name.endsWith(".yaml") || name.endsWith(".yml");
593
+ }
594
+ //#endregion
595
+ //#region src/core/iac/parser/documents.ts
596
+ function parseFiles(files) {
597
+ return files.map(parseFile);
598
+ }
599
+ function parseFile(file) {
600
+ const errors = [];
601
+ let docs;
602
+ try {
603
+ docs = parseAllDocuments(file.content, { keepSourceTokens: true });
604
+ } catch (err) {
605
+ errors.push({
606
+ code: "yaml.parse",
607
+ message: err instanceof Error ? err.message : String(err),
608
+ source: { file: file.path }
609
+ });
610
+ return {
611
+ file: file.path,
612
+ documents: [],
613
+ errors
614
+ };
615
+ }
616
+ const documents = [];
617
+ docs.forEach((document, index) => {
618
+ if (document.errors.length > 0) {
619
+ for (const e of document.errors) errors.push({
620
+ code: "yaml.parse",
621
+ message: e.message,
622
+ source: locationFromOffset(file.content, e.pos?.[0], file.path)
623
+ });
624
+ return;
625
+ }
626
+ const source = documentLocation(file, document);
627
+ documents.push({
628
+ file: file.path,
629
+ index,
630
+ document,
631
+ source
632
+ });
633
+ });
634
+ return {
635
+ file: file.path,
636
+ documents,
637
+ errors
638
+ };
639
+ }
640
+ function isResourceDocument(doc) {
641
+ if (doc.contents == null) return false;
642
+ const json = doc.toJS();
643
+ if (typeof json !== "object" || json === null) return false;
644
+ return json.apiVersion === API_VERSION;
645
+ }
646
+ function readResourceShape(doc) {
647
+ const json = doc.toJS();
648
+ if (typeof json !== "object" || json === null) return {};
649
+ return json;
650
+ }
651
+ function documentLocation(file, document) {
652
+ const contents = document.contents;
653
+ if (contents?.range) return locationFromOffset(file.content, contents.range[0], file.path);
654
+ return {
655
+ file: file.path,
656
+ line: 1,
657
+ column: 1
658
+ };
659
+ }
660
+ function locationFromOffset(content, offset, file) {
661
+ if (offset == null || offset < 0) return { file };
662
+ let line = 1;
663
+ let column = 1;
664
+ for (let i = 0; i < offset && i < content.length; i++) if (content[i] === "\n") {
665
+ line++;
666
+ column = 1;
667
+ } else column++;
668
+ return {
669
+ file,
670
+ line,
671
+ column
672
+ };
673
+ }
674
+ function asResourceDoc(parsed) {
675
+ const shape = readResourceShape(parsed.document);
676
+ if (shape.apiVersion !== "revos/v1") return null;
677
+ if (!shape.kind) return null;
678
+ if (!shape.metadata?.name) return null;
679
+ const spec = shape.spec ?? {};
680
+ return {
681
+ kind: shape.kind,
682
+ metadata: {
683
+ name: shape.metadata.name,
684
+ id: shape.metadata.id
685
+ },
686
+ spec,
687
+ rawSpec: shape.spec ?? {},
688
+ source: parsed.source,
689
+ docIndex: parsed.index,
690
+ document: parsed.document
691
+ };
692
+ }
693
+ //#endregion
694
+ //#region src/core/iac/parser/index.ts
695
+ function buildIndex(resources) {
696
+ const byAddress = /* @__PURE__ */ new Map();
697
+ const errors = [];
698
+ const seenAt = /* @__PURE__ */ new Map();
699
+ for (const r of resources) {
700
+ const addr = address(r.kind, r.metadata.name);
701
+ const prior = seenAt.get(addr);
702
+ if (prior) {
703
+ errors.push({
704
+ code: "duplicate",
705
+ message: `Duplicate resource ${addr} (also defined at ${formatLoc(prior)})`,
706
+ source: r.source,
707
+ hint: "Each (kind, metadata.name) pair must be unique within a project."
708
+ });
709
+ continue;
710
+ }
711
+ if (!isValidName(r.metadata.name)) {
712
+ errors.push({
713
+ code: "invalid-name",
714
+ message: `metadata.name "${r.metadata.name}" is invalid; must match ^[a-z][a-z0-9-]*$`,
715
+ source: r.source
716
+ });
717
+ continue;
718
+ }
719
+ if (r.metadata.id !== void 0 && !isLikelyId(r.metadata.id)) {
720
+ errors.push({
721
+ code: "tampered",
722
+ message: `${addr}: metadata.id appears tampered or corrupted`,
723
+ source: r.source,
724
+ hint: `Run \`revos pull ${addr}\` to recover.`
725
+ });
726
+ continue;
727
+ }
728
+ seenAt.set(addr, r.source);
729
+ byAddress.set(addr, r);
730
+ }
731
+ return {
732
+ byAddress,
733
+ all: [...byAddress.values()],
734
+ errors
735
+ };
736
+ }
737
+ function address(kind, name) {
738
+ return `${kind}/${name}`;
739
+ }
740
+ const NAME_RE = /^[a-z][a-z0-9-]*$/;
741
+ function isValidName(name) {
742
+ return NAME_RE.test(name);
743
+ }
744
+ function isLikelyId(id) {
745
+ return typeof id === "string" && id.trim().length > 0 && !id.includes("\n");
746
+ }
747
+ function formatLoc(loc) {
748
+ if (loc.line) return `${loc.file}:${loc.line}`;
749
+ return loc.file;
750
+ }
751
+ //#endregion
752
+ //#region src/core/iac/validate/validate.ts
753
+ function validateResource(resource, schema) {
754
+ const result = schema.safeParse(resource.spec);
755
+ if (!result.success) return {
756
+ resource,
757
+ errors: zodErrorsToIac(result.error, resource.source, resource.kind, resource.metadata.name)
758
+ };
759
+ return {
760
+ resource: {
761
+ ...resource,
762
+ spec: result.data
763
+ },
764
+ errors: []
765
+ };
766
+ }
767
+ function validateAll(resources, registry) {
768
+ const errors = [];
769
+ const validated = [];
770
+ for (const r of resources) {
771
+ const provider = registry.get(r.kind);
772
+ if (!provider) {
773
+ errors.push({
774
+ code: "unknown-kind",
775
+ message: `Unknown kind "${r.kind}"`,
776
+ source: r.source,
777
+ hint: `Known kinds: ${[...registry.kinds()].join(", ") || "(none)"}`
778
+ });
779
+ continue;
780
+ }
781
+ const result = validateResource(r, provider.schema);
782
+ errors.push(...result.errors);
783
+ if (result.errors.length === 0) validated.push(result.resource);
784
+ }
785
+ return {
786
+ resources: validated,
787
+ errors
788
+ };
789
+ }
790
+ function zodErrorsToIac(err, source, kind, name) {
791
+ return [{
792
+ code: "schema",
793
+ message: `${kind}/${name}: ${fromError(err, { prefix: null }).toString()}`,
794
+ source
795
+ }];
796
+ }
797
+ //#endregion
798
+ //#region src/core/iac/providers/registry.ts
799
+ var ProviderRegistry = class {
800
+ providers = /* @__PURE__ */ new Map();
801
+ register(provider) {
802
+ if (this.providers.has(provider.kind)) throw new Error(`Provider for kind "${provider.kind}" already registered`);
803
+ this.providers.set(provider.kind, provider);
804
+ }
805
+ get(kind) {
806
+ return this.providers.get(kind);
807
+ }
808
+ has(kind) {
809
+ return this.providers.has(kind);
810
+ }
811
+ kinds() {
812
+ return this.providers.keys();
813
+ }
814
+ };
815
+ //#endregion
816
+ //#region src/core/iac/util/slug.ts
817
+ /**
818
+ * Convert a free-form display name into a valid `metadata.name` slug.
819
+ *
820
+ * Rules: lowercase, ASCII alnum + `-`, must start with a letter. Empty
821
+ * or letter-less inputs fall back to `unnamed`.
822
+ */
823
+ function slugify(input) {
824
+ const base = input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/--+/g, "-");
825
+ if (base.length === 0) return "unnamed";
826
+ if (!/^[a-z]/.test(base)) return `r-${base}`;
827
+ return base;
828
+ }
829
+ //#endregion
830
+ //#region src/core/iac/providers/connection.provider.ts
831
+ const scheduleSchema = z.object({
832
+ units: z.number().int().positive(),
833
+ timeUnit: z.enum([
834
+ "minutes",
835
+ "hours",
836
+ "days",
837
+ "weeks",
838
+ "months"
839
+ ])
840
+ });
841
+ const syncModeSchema = z.enum([
842
+ "full_refresh_overwrite",
843
+ "full_refresh_append",
844
+ "incremental_append",
845
+ "incremental_deduped_history",
846
+ "full_refresh_overwrite_deduped"
847
+ ]);
848
+ const hashingMapperSchema = z.object({
849
+ type: z.literal("hashing"),
850
+ id: z.string().uuid().optional(),
851
+ mapperConfiguration: z.object({
852
+ targetField: z.string().min(1),
853
+ method: z.enum([
854
+ "MD2",
855
+ "MD5",
856
+ "SHA-1",
857
+ "SHA-224",
858
+ "SHA-256",
859
+ "SHA-384",
860
+ "SHA-512"
861
+ ]),
862
+ fieldNameSuffix: z.string()
863
+ })
864
+ });
865
+ const fieldRenamingMapperSchema = z.object({
866
+ type: z.literal("field-renaming"),
867
+ id: z.string().uuid().optional(),
868
+ mapperConfiguration: z.object({
869
+ originalFieldName: z.string().min(1),
870
+ newFieldName: z.string().min(1)
871
+ })
872
+ });
873
+ const fieldFilteringMapperSchema = z.object({
874
+ type: z.literal("field-filtering"),
875
+ id: z.string().uuid().optional(),
876
+ mapperConfiguration: z.object({ targetField: z.string().min(1) })
877
+ });
878
+ const rowFilteringEqualSchema = z.object({
879
+ type: z.literal("EQUAL"),
880
+ fieldName: z.string().min(1),
881
+ comparisonValue: z.string()
882
+ });
883
+ const rowFilteringOperationSchema = z.lazy(() => z.discriminatedUnion("type", [rowFilteringEqualSchema, z.object({
884
+ type: z.literal("NOT"),
885
+ conditions: z.array(rowFilteringOperationSchema).min(1)
886
+ })]));
887
+ const rowFilteringMapperSchema = z.object({
888
+ type: z.literal("row-filtering"),
889
+ id: z.string().uuid().optional(),
890
+ mapperConfiguration: z.object({ conditions: rowFilteringOperationSchema })
891
+ });
892
+ const encryptionMapperSchema = z.object({
893
+ type: z.literal("encryption"),
894
+ id: z.string().uuid().optional(),
895
+ mapperConfiguration: z.discriminatedUnion("algorithm", [z.object({
896
+ algorithm: z.literal("RSA"),
897
+ targetField: z.string().min(1),
898
+ fieldNameSuffix: z.string(),
899
+ publicKey: z.string().min(1)
900
+ }), z.object({
901
+ algorithm: z.literal("AES"),
902
+ targetField: z.string().min(1),
903
+ fieldNameSuffix: z.string(),
904
+ key: z.string().min(1),
905
+ mode: z.enum([
906
+ "CBC",
907
+ "CFB",
908
+ "OFB",
909
+ "CTR",
910
+ "GCM",
911
+ "ECB"
912
+ ]),
913
+ padding: z.enum(["NoPadding", "PKCS5Padding"])
914
+ })])
915
+ });
916
+ const streamMapperSchema = z.discriminatedUnion("type", [
917
+ hashingMapperSchema,
918
+ fieldRenamingMapperSchema,
919
+ fieldFilteringMapperSchema,
920
+ rowFilteringMapperSchema,
921
+ encryptionMapperSchema
922
+ ]);
923
+ const SYNC_MODES_REQUIRING_CURSOR = new Set(["incremental_append", "incremental_deduped_history"]);
924
+ const SYNC_MODES_REQUIRING_PRIMARY_KEY = new Set(["incremental_deduped_history", "full_refresh_overwrite_deduped"]);
925
+ const streamConfigSchema = z.object({
926
+ name: z.string().min(1),
927
+ namespace: z.string().optional(),
928
+ syncMode: syncModeSchema.optional(),
929
+ cursorField: z.array(z.string()).optional(),
930
+ primaryKey: z.array(z.array(z.string())).optional(),
931
+ selectedFields: z.array(z.object({ fieldPath: z.array(z.string()) })).optional(),
932
+ mappers: z.array(streamMapperSchema).optional()
933
+ }).superRefine((stream, ctx) => {
934
+ if (!stream.syncMode) return;
935
+ if (SYNC_MODES_REQUIRING_CURSOR.has(stream.syncMode) && !stream.cursorField?.length) ctx.addIssue({
936
+ code: z.ZodIssueCode.custom,
937
+ path: ["cursorField"],
938
+ message: `cursorField is required when syncMode is ${stream.syncMode}`
939
+ });
940
+ if (SYNC_MODES_REQUIRING_PRIMARY_KEY.has(stream.syncMode) && !stream.primaryKey?.length) ctx.addIssue({
941
+ code: z.ZodIssueCode.custom,
942
+ path: ["primaryKey"],
943
+ message: `primaryKey is required when syncMode is ${stream.syncMode}`
944
+ });
945
+ });
946
+ const prefixSchema = z.string().max(63, "spec.prefix must be 63 characters or fewer").regex(/^[a-z0-9_]*$/, "spec.prefix must contain only lowercase letters, digits, and underscores");
947
+ const connectionSpecSchema = z.object({
948
+ name: z.string().min(1, "spec.name is required"),
949
+ source: z.object({ id: z.string().min(1, "spec.source.id is required") }),
950
+ schedule: scheduleSchema.default({
951
+ units: 24,
952
+ timeUnit: "hours"
953
+ }),
954
+ streams: z.array(streamConfigSchema).default([]),
955
+ status: z.enum([
956
+ "active",
957
+ "inactive",
958
+ "deprecated"
959
+ ]).default("active"),
960
+ prefix: prefixSchema.optional()
961
+ });
962
+ function createConnectionProvider(client) {
963
+ return {
964
+ kind: "Connection",
965
+ schema: connectionSpecSchema,
966
+ sensitivePaths: [],
967
+ localOnlyPaths: [],
968
+ dependsOn: [],
969
+ extractRefs() {
970
+ return [];
971
+ },
972
+ normalize(spec) {
973
+ return remoteToSpec$1(spec);
974
+ },
975
+ computedPaths: ["prefix"],
976
+ inflateRemote(remote) {
977
+ return remoteToSpec$1(remote);
978
+ },
979
+ async listRemote() {
980
+ return (await client.list()).map((r) => ({
981
+ id: r.id,
982
+ remote: r
983
+ }));
984
+ },
985
+ deriveName(remote) {
986
+ const r = remote ?? {};
987
+ return slugify(typeof r.name === "string" ? r.name : "");
988
+ },
989
+ async create(_ctx, spec) {
990
+ const remote = await client.create(specToCreateBody$1(spec));
991
+ return {
992
+ id: remote.id,
993
+ spec: remoteToSpec$1(remote)
994
+ };
995
+ },
996
+ async read(_ctx, id) {
997
+ const remote = await client.get(id);
998
+ if (!remote) return null;
999
+ return {
1000
+ id: remote.id,
1001
+ spec: remoteToSpec$1(remote)
1002
+ };
1003
+ },
1004
+ async update(_ctx, id, spec) {
1005
+ const remote = await client.update(id, specToUpdateBody$1(spec));
1006
+ return {
1007
+ id: remote.id,
1008
+ spec: remoteToSpec$1(remote)
1009
+ };
1010
+ },
1011
+ async delete(_ctx, id) {
1012
+ await client.delete(id);
1013
+ }
1014
+ };
1015
+ }
1016
+ function specToCreateBody$1(spec) {
1017
+ return {
1018
+ name: spec.name,
1019
+ sourceId: spec.source.id,
1020
+ schedule: spec.schedule,
1021
+ streams: spec.streams.length > 0 ? spec.streams : void 0,
1022
+ status: spec.status === "deprecated" ? "inactive" : spec.status,
1023
+ ...spec.prefix !== void 0 && { prefix: spec.prefix }
1024
+ };
1025
+ }
1026
+ function specToUpdateBody$1(spec) {
1027
+ return {
1028
+ name: spec.name,
1029
+ schedule: spec.schedule,
1030
+ streams: spec.streams,
1031
+ status: spec.status,
1032
+ ...spec.prefix !== void 0 && { prefix: spec.prefix }
1033
+ };
1034
+ }
1035
+ /**
1036
+ * Map a server connection back to the IaC spec shape. Strips empty arrays
1037
+ * inside per-stream config that the server emits as defaults so they don't
1038
+ * cause phantom drift between fresh-pull and apply round-trips.
1039
+ */
1040
+ function remoteToSpec$1(remote) {
1041
+ if (typeof remote !== "object" || remote === null) return {
1042
+ name: "",
1043
+ source: { id: "" },
1044
+ schedule: {
1045
+ units: 24,
1046
+ timeUnit: "hours"
1047
+ },
1048
+ streams: [],
1049
+ status: "active"
1050
+ };
1051
+ const r = remote;
1052
+ const schedule = typeof r.schedule === "object" && r.schedule ? r.schedule : {
1053
+ units: 24,
1054
+ timeUnit: "hours"
1055
+ };
1056
+ const rawStreams = Array.isArray(r.streams) ? r.streams : [];
1057
+ return {
1058
+ name: typeof r.name === "string" ? r.name : "",
1059
+ source: { id: typeof r.sourceId === "string" ? r.sourceId : "" },
1060
+ schedule,
1061
+ streams: rawStreams.map(normalizeStream),
1062
+ status: r.status === "active" || r.status === "inactive" || r.status === "deprecated" ? r.status : "active",
1063
+ ...typeof r.prefix === "string" && { prefix: r.prefix }
1064
+ };
1065
+ }
1066
+ /**
1067
+ * Drop empty arrays the server emits as defaults — `cursorField: []`,
1068
+ * `primaryKey: []`, `selectedFields: []`, `mappers: []`. Keeping them would
1069
+ * either bloat the YAML on a discovery pull, or cause spurious diff churn
1070
+ * when the user edits a stream and re-applies.
1071
+ */
1072
+ function normalizeStream(s) {
1073
+ const out = { name: s.name };
1074
+ if (s.namespace) out.namespace = s.namespace;
1075
+ if (s.syncMode) out.syncMode = s.syncMode;
1076
+ if (s.cursorField && s.cursorField.length > 0) out.cursorField = s.cursorField;
1077
+ if (s.primaryKey && s.primaryKey.length > 0) out.primaryKey = s.primaryKey;
1078
+ if (s.selectedFields && s.selectedFields.length > 0) out.selectedFields = s.selectedFields;
1079
+ if (s.mappers && s.mappers.length > 0) out.mappers = s.mappers;
1080
+ return out;
1081
+ }
1082
+ //#endregion
1083
+ //#region src/core/iac/providers/cube.provider.ts
1084
+ const cubeSpecSchema = z.object({
1085
+ name: z.string().min(1, "spec.name is required"),
1086
+ definition: z.record(z.string(), z.unknown())
1087
+ });
1088
+ function createCubeProvider(client) {
1089
+ return {
1090
+ kind: "Cube",
1091
+ schema: cubeSpecSchema,
1092
+ sensitivePaths: [],
1093
+ localOnlyPaths: [],
1094
+ dependsOn: [],
1095
+ extractRefs() {
1096
+ return [];
1097
+ },
1098
+ normalize(remote) {
1099
+ return remoteToSpec(remote);
1100
+ },
1101
+ inflateRemote(remote) {
1102
+ return remoteToSpec(remote);
1103
+ },
1104
+ async listRemote() {
1105
+ return (await client.list()).map((r) => ({
1106
+ id: r.id,
1107
+ remote: r
1108
+ }));
1109
+ },
1110
+ deriveName(remote) {
1111
+ const r = remote ?? {};
1112
+ return slugify(typeof r.name === "string" ? r.name : "");
1113
+ },
1114
+ async create(_ctx, spec) {
1115
+ const remote = await client.create(specToCreateBody(spec));
1116
+ return {
1117
+ id: remote.id,
1118
+ spec: remoteToSpec(remote)
1119
+ };
1120
+ },
1121
+ async read(_ctx, id) {
1122
+ const remote = await client.get(id);
1123
+ if (!remote) return null;
1124
+ return {
1125
+ id: remote.id,
1126
+ spec: remoteToSpec(remote)
1127
+ };
1128
+ },
1129
+ async update(_ctx, id, spec) {
1130
+ const remote = await client.update(id, specToUpdateBody(spec));
1131
+ return {
1132
+ id: remote.id,
1133
+ spec: remoteToSpec(remote)
1134
+ };
1135
+ },
1136
+ async delete(_ctx, id) {
1137
+ await client.delete(id);
1138
+ }
1139
+ };
1140
+ }
1141
+ function specToCreateBody(spec) {
1142
+ return {
1143
+ name: spec.name,
1144
+ definition: ensureDefinitionName(spec.definition, spec.name)
1145
+ };
1146
+ }
1147
+ function specToUpdateBody(spec) {
1148
+ return {
1149
+ name: spec.name,
1150
+ definition: ensureDefinitionName(spec.definition, spec.name)
1151
+ };
1152
+ }
1153
+ /**
1154
+ * Cube.dev requires `definition.name` to match the cube name (it's the
1155
+ * identifier referenced by `${CUBE}` and joins). Mirror it from spec.name
1156
+ * so the YAML doesn't have to carry the same name twice.
1157
+ */
1158
+ function ensureDefinitionName(definition, name) {
1159
+ return {
1160
+ ...definition,
1161
+ name
1162
+ };
1163
+ }
1164
+ function remoteToSpec(remote) {
1165
+ if (typeof remote !== "object" || remote === null) return {
1166
+ name: "",
1167
+ definition: {}
1168
+ };
1169
+ const r = remote;
1170
+ const { name: _ignoredDefName, ...definitionWithoutName } = typeof r.definition === "object" && r.definition !== null ? r.definition : {};
1171
+ return {
1172
+ name: typeof r.name === "string" ? r.name : "",
1173
+ definition: definitionWithoutName
1174
+ };
1175
+ }
1176
+ //#endregion
1177
+ //#region src/core/iac/api/connections-client.ts
1178
+ /**
1179
+ * Adapter over the typed `@revos/api-client` SDK. The SDK client is already
1180
+ * configured with the project's org via `createApiClient({ organizationId })`,
1181
+ * so per-call methods don't take the org id.
1182
+ */
1183
+ function createSdkConnectionsClient(apiClient) {
1184
+ return {
1185
+ list: () => walkAllPages((pageToken) => apiClient.connections.list({
1186
+ pageSize: 100,
1187
+ pageToken
1188
+ })),
1189
+ get: (connectionId) => getOrNull(() => apiClient.connections.get({ id: connectionId }).then(unwrap)),
1190
+ create: (body) => apiClient.connections.create({ body }).then(unwrap),
1191
+ update: (connectionId, body) => apiClient.connections.update({
1192
+ id: connectionId,
1193
+ body
1194
+ }).then(unwrap),
1195
+ delete: async (connectionId) => {
1196
+ await apiClient.connections.delete({ id: connectionId });
1197
+ }
1198
+ };
1199
+ }
1200
+ //#endregion
1201
+ //#region src/core/iac/api/cubes-client.ts
1202
+ /**
1203
+ * Adapter over the typed `@revos/api-client` SDK. The SDK client is already
1204
+ * configured with the project's org via `createApiClient({ organizationId })`,
1205
+ * so per-call methods don't take the org id.
1206
+ */
1207
+ function createSdkCubesClient(apiClient) {
1208
+ return {
1209
+ list: () => apiClient.cubes.list().then(unwrap),
1210
+ get: (cubeId) => getOrNull(() => apiClient.cubes.get({ id: cubeId }).then(unwrap)),
1211
+ create: (body) => apiClient.cubes.create({ body }).then(unwrap),
1212
+ update: (cubeId, body) => apiClient.cubes.update({
1213
+ id: cubeId,
1214
+ body
1215
+ }).then(unwrap),
1216
+ delete: async (cubeId) => {
1217
+ await apiClient.cubes.delete({ id: cubeId });
1218
+ }
1219
+ };
1220
+ }
1221
+ //#endregion
1222
+ //#region src/core/iac/providers/build-registry.ts
1223
+ /** Build a `ProviderRegistry` populated with every IaC kind. */
1224
+ function buildIacRegistry(apiClient) {
1225
+ const registry = new ProviderRegistry();
1226
+ registry.register(createConnectionProvider(createSdkConnectionsClient(apiClient)));
1227
+ registry.register(createCubeProvider(createSdkCubesClient(apiClient)));
1228
+ return registry;
1229
+ }
1230
+ //#endregion
1231
+ //#region src/core/iac/ast/rewrite.ts
1232
+ /**
1233
+ * Apply edits to one or more documents in a YAML file in-place.
1234
+ *
1235
+ * Uses the `yaml` library's CST-aware Document AST so existing siblings,
1236
+ * comments, key order, and `---` separators are preserved byte-for-byte
1237
+ * outside the regions actually being modified.
1238
+ *
1239
+ * Returns the new file contents (and writes them to disk).
1240
+ */
1241
+ function rewriteYamlFile(filePath, edits) {
1242
+ const next = applyDocumentEdits(fs.readFileSync(filePath, "utf-8"), edits);
1243
+ fs.writeFileSync(filePath, next, "utf-8");
1244
+ return next;
1245
+ }
1246
+ /** Pure variant for tests / dry-run flows. */
1247
+ function applyDocumentEdits(src, edits) {
1248
+ const docs = parseAllDocuments(src, { keepSourceTokens: true });
1249
+ for (const edit of edits) {
1250
+ const doc = docs[edit.docIndex];
1251
+ if (!doc) throw new Error(`Cannot edit document #${edit.docIndex}: only ${docs.length} document(s) in source`);
1252
+ edit.mutate(doc);
1253
+ }
1254
+ return docs.map((d) => d.toString()).join("");
1255
+ }
1256
+ function setMetadataId(filePath, docIndex, id) {
1257
+ return rewriteYamlFile(filePath, [{
1258
+ docIndex,
1259
+ mutate: (doc) => doc.setIn(["metadata", "id"], id)
1260
+ }]);
1261
+ }
1262
+ //#endregion
1263
+ //#region src/core/iac/env/interpolate.ts
1264
+ const FULL_MATCH = /^\$\{env\.([A-Z_][A-Z0-9_]*)\}$/;
1265
+ const PARTIAL_MATCH = /\$\{env\.([A-Z_][A-Z0-9_]*)\}/g;
1266
+ /**
1267
+ * Substitute `${env.VAR}` placeholders in spec values.
1268
+ *
1269
+ * Whole-string match (`"${env.HOST}"`) substitutes with the env var raw value
1270
+ * (preserving its type would require typed envs; we always return string).
1271
+ * Partial-string match (`"https://${env.HOST}"`) substitutes inline.
1272
+ *
1273
+ * Caller passes the resource source for error reporting.
1274
+ */
1275
+ function interpolateSpec(spec, source, env = process.env) {
1276
+ const errors = [];
1277
+ return {
1278
+ value: walk(spec, source, env, errors),
1279
+ errors
1280
+ };
1281
+ }
1282
+ function walk(v, source, env, errors) {
1283
+ if (typeof v === "string") return interpolateString(v, source, env, errors);
1284
+ if (Array.isArray(v)) return v.map((x) => walk(x, source, env, errors));
1285
+ if (v !== null && typeof v === "object") {
1286
+ const out = {};
1287
+ for (const [k, val] of Object.entries(v)) out[k] = walk(val, source, env, errors);
1288
+ return out;
1289
+ }
1290
+ return v;
1291
+ }
1292
+ function interpolateString(s, source, env, errors) {
1293
+ const full = FULL_MATCH.exec(s);
1294
+ if (full) {
1295
+ const name = full[1];
1296
+ const v = env[name];
1297
+ if (v === void 0) {
1298
+ errors.push({
1299
+ code: "env.missing",
1300
+ message: `Environment variable ${name} is not set (referenced as \${env.${name}})`,
1301
+ source
1302
+ });
1303
+ return "";
1304
+ }
1305
+ return v;
1306
+ }
1307
+ return s.replace(PARTIAL_MATCH, (_, name) => {
1308
+ const v = env[name];
1309
+ if (v === void 0) {
1310
+ errors.push({
1311
+ code: "env.missing",
1312
+ message: `Environment variable ${name} is not set (referenced as \${env.${name}})`,
1313
+ source
1314
+ });
1315
+ return "";
1316
+ }
1317
+ return v;
1318
+ });
1319
+ }
1320
+ //#endregion
1321
+ //#region src/core/iac/loader.ts
1322
+ /**
1323
+ * Two-pass loader:
1324
+ * 1. Parse every yaml file, expand `${env.VAR}` in spec values, build the
1325
+ * `(kind, metadata.name)` index, detect duplicates and tampered metadata.
1326
+ * 2. Validate each resource's spec against its provider's schema.
1327
+ *
1328
+ * Returns the index and any accumulated errors. Caller decides whether to
1329
+ * proceed (e.g. `apply` aborts on any error; `status` may render partial state).
1330
+ */
1331
+ function loadResources(opts) {
1332
+ const parsedFiles = parseFiles(scanYamlFiles({
1333
+ root: projectRoot(opts.project),
1334
+ startPath: opts.startPath
1335
+ }));
1336
+ const errors = [];
1337
+ const docs = [];
1338
+ for (const pf of parsedFiles) {
1339
+ errors.push(...pf.errors);
1340
+ for (const pd of pf.documents) {
1341
+ const doc = asResourceDoc(pd);
1342
+ if (!doc) continue;
1343
+ const interp = interpolateSpec(doc.spec, doc.source, opts.env);
1344
+ errors.push(...interp.errors);
1345
+ docs.push({
1346
+ ...doc,
1347
+ spec: interp.value
1348
+ });
1349
+ }
1350
+ }
1351
+ const indexed = buildIndex(docs);
1352
+ errors.push(...indexed.errors);
1353
+ if (opts.validateSpecs === false) return {
1354
+ index: indexed,
1355
+ errors
1356
+ };
1357
+ const validated = validateAll(indexed.all, opts.registry);
1358
+ errors.push(...validated.errors);
1359
+ return {
1360
+ index: buildIndex(validated.resources),
1361
+ errors
1362
+ };
1363
+ }
1364
+ //#endregion
1365
+ //#region src/core/iac/diff/diff.ts
1366
+ const REDACTED = "(sensitive value)";
1367
+ /**
1368
+ * Compare two normalized specs and return a list of changes. Sensitive paths
1369
+ * (declared by the provider) are not compared on raw values; instead the
1370
+ * before/after are replaced with a placeholder so callers never get a chance
1371
+ * to print a secret. The presence/absence of a value at a sensitive path is
1372
+ * still reported as a change so users see *something* moved.
1373
+ */
1374
+ function diffSpecs(before, after, sensitivePaths = []) {
1375
+ return microdiff(before ?? {}, after ?? {}).map((d) => toChange(d, sensitivePaths));
1376
+ }
1377
+ function toChange(d, sensitivePaths) {
1378
+ const dotted = d.path.join(".");
1379
+ const isSensitive = sensitivePaths.some((p) => dotted === p || dotted.startsWith(`${p}.`));
1380
+ switch (d.type) {
1381
+ case "CREATE": return {
1382
+ kind: "add",
1383
+ path: d.path,
1384
+ after: isSensitive ? REDACTED : d.value,
1385
+ sensitive: isSensitive
1386
+ };
1387
+ case "REMOVE": return {
1388
+ kind: "remove",
1389
+ path: d.path,
1390
+ before: isSensitive ? REDACTED : d.oldValue,
1391
+ sensitive: isSensitive
1392
+ };
1393
+ case "CHANGE": return {
1394
+ kind: "change",
1395
+ path: d.path,
1396
+ before: isSensitive ? REDACTED : d.oldValue,
1397
+ after: isSensitive ? REDACTED : d.value,
1398
+ sensitive: isSensitive
1399
+ };
1400
+ }
1401
+ }
1402
+ /**
1403
+ * Render a single diff entry as a human-readable line. Used by both
1404
+ * `revos apply --dry-run` and `revos diff`.
1405
+ */
1406
+ function formatDiffLine(c) {
1407
+ const path = c.path.map(String).join(".");
1408
+ switch (c.kind) {
1409
+ case "add": return chalk.green(` + ${path}: ${formatValue(c.after)}`);
1410
+ case "remove": return chalk.red(` - ${path}: ${formatValue(c.before)}`);
1411
+ case "change": return chalk.yellow(` ~ ${path}: ${formatValue(c.before)} → ${formatValue(c.after)}`);
1412
+ }
1413
+ }
1414
+ function formatValue(v) {
1415
+ if (v === REDACTED) return chalk.dim(REDACTED);
1416
+ if (typeof v === "string") return JSON.stringify(v);
1417
+ if (v === null || v === void 0) return String(v);
1418
+ if (typeof v === "object") return JSON.stringify(v);
1419
+ return String(v);
1420
+ }
1421
+ //#endregion
1422
+ //#region src/core/iac/refs/graph.ts
1423
+ const { Graph, alg } = graphlib;
1424
+ /**
1425
+ * Build the dependency DAG from a flat list of resources.
1426
+ *
1427
+ * Edges point from dependency → dependent: if Connection/c references
1428
+ * Source/s, we add an edge `Source/s → Connection/c`. That means once
1429
+ * `Source/s` is applied, `Connection/c` becomes eligible.
1430
+ *
1431
+ * Errors:
1432
+ * - A resource references a (kind, name) that does not exist locally.
1433
+ * Reported with `file:line:col` of the referencing resource.
1434
+ * - The dependency graph contains a cycle. Reported with the addresses
1435
+ * involved.
1436
+ */
1437
+ function buildDependencyGraph(resources, registry) {
1438
+ const errors = [];
1439
+ const byAddress = /* @__PURE__ */ new Map();
1440
+ for (const r of resources) byAddress.set(address(r.kind, r.metadata.name), r);
1441
+ const g = new Graph({ directed: true });
1442
+ for (const r of resources) g.setNode(address(r.kind, r.metadata.name));
1443
+ for (const r of resources) {
1444
+ const provider = registry.get(r.kind);
1445
+ if (!provider) continue;
1446
+ const refs = provider.extractRefs(r.spec);
1447
+ for (const ref of refs) {
1448
+ const dep = address(ref.kind, ref.name);
1449
+ if (!byAddress.has(dep)) {
1450
+ errors.push({
1451
+ code: "ref",
1452
+ message: `${address(r.kind, r.metadata.name)} references ${dep} but no local resource with that name exists`,
1453
+ source: r.source,
1454
+ hint: `Either define ${dep} in this project or remove the reference.`
1455
+ });
1456
+ continue;
1457
+ }
1458
+ g.setEdge(dep, address(r.kind, r.metadata.name));
1459
+ }
1460
+ }
1461
+ if (errors.length > 0) return {
1462
+ levels: [],
1463
+ errors
1464
+ };
1465
+ const cycles = alg.findCycles(g);
1466
+ if (cycles.length > 0) {
1467
+ for (const cycle of cycles) errors.push({
1468
+ code: "cycle",
1469
+ message: `dependency cycle: ${cycle.join(" → ")} → ${cycle[0]}`,
1470
+ hint: "Break the cycle by removing one of the references."
1471
+ });
1472
+ return {
1473
+ levels: [],
1474
+ errors
1475
+ };
1476
+ }
1477
+ return {
1478
+ levels: kahnLevels(g, byAddress),
1479
+ errors: []
1480
+ };
1481
+ }
1482
+ /**
1483
+ * Kahn's algorithm, leveled. Repeatedly extract every node whose remaining
1484
+ * predecessors are zero — those form the next level. Within a level, order
1485
+ * is alphabetical for deterministic output.
1486
+ */
1487
+ function kahnLevels(g, byAddress) {
1488
+ const inDegree = /* @__PURE__ */ new Map();
1489
+ for (const node of g.nodes()) inDegree.set(node, (g.predecessors(node) ?? []).length);
1490
+ const levels = [];
1491
+ while (inDegree.size > 0) {
1492
+ const ready = [...inDegree.entries()].filter(([, d]) => d === 0).map(([n]) => n).sort();
1493
+ if (ready.length === 0) throw new Error("internal: kahnLevels could not make progress but no cycle was reported");
1494
+ const level = [];
1495
+ for (const n of ready) {
1496
+ const r = byAddress.get(n);
1497
+ if (r) level.push(r);
1498
+ inDegree.delete(n);
1499
+ for (const succ of g.successors(n) ?? []) inDegree.set(succ, (inDegree.get(succ) ?? 0) - 1);
1500
+ }
1501
+ levels.push(level);
1502
+ }
1503
+ return levels;
1504
+ }
1505
+ //#endregion
1506
+ //#region src/core/iac/apply/apply.ts
1507
+ const DEFAULT_PARALLELISM = 4;
1508
+ /**
1509
+ * Reconcile local YAML resources against the API.
1510
+ *
1511
+ * - No `metadata.id` → POST create, then AST-rewrite the file in place to
1512
+ * record the returned id.
1513
+ * - Has `metadata.id` → GET remote, normalize, diff against local. Equal →
1514
+ * `unchanged`. Different → PATCH update, report `update` with the drift
1515
+ * list. In `dryRun` mode the GET still happens (so drift can be reported)
1516
+ * but the PATCH/POST and the YAML rewrite are skipped — this is what
1517
+ * powers `revos diff`.
1518
+ *
1519
+ * Resources are applied in dependency order: providers declare which other
1520
+ * resources they reference via `extractRefs(spec)`, and we apply level by
1521
+ * level. Within a level, resources are applied concurrently up to
1522
+ * `parallelism`.
1523
+ *
1524
+ * Stop-on-first-error: any failure halts further mutations. Anything done
1525
+ * before the failure has already been persisted (each resource is committed
1526
+ * as soon as its remote write returns), so re-running picks up where it
1527
+ * left off.
1528
+ */
1529
+ async function apply(opts) {
1530
+ const load = loadResources({
1531
+ project: opts.project,
1532
+ registry: opts.registry,
1533
+ startPath: opts.startPath,
1534
+ env: opts.env
1535
+ });
1536
+ if (load.errors.length > 0) return {
1537
+ applied: [],
1538
+ errors: load.errors
1539
+ };
1540
+ const dag = buildDependencyGraph(load.index.all, opts.registry);
1541
+ if (dag.errors.length > 0) return {
1542
+ applied: [],
1543
+ errors: dag.errors
1544
+ };
1545
+ const result = {
1546
+ applied: [],
1547
+ errors: []
1548
+ };
1549
+ const dryRun = opts.dryRun ?? false;
1550
+ const parallelism = Math.max(1, opts.parallelism ?? DEFAULT_PARALLELISM);
1551
+ const resolver = makeResolver(load.index.all);
1552
+ const idResolver = makeIdResolver(load.index.all);
1553
+ for (const level of dag.levels) {
1554
+ if (result.errors.length > 0) break;
1555
+ const summaries = await runLevel(level, opts.registry, opts.project.metadata.orgId, resolver, idResolver, dryRun, parallelism, result);
1556
+ result.applied.push(...summaries);
1557
+ }
1558
+ return result;
1559
+ }
1560
+ /**
1561
+ * Apply every resource in the level concurrently, capped at `parallelism`.
1562
+ * The first failure inside the level halts further work in this level
1563
+ * (other in-flight tasks finish, but no new tasks start), and the outer
1564
+ * loop will not advance to the next level. Resources whose work completed
1565
+ * before the failure are still persisted on disk and reported in the
1566
+ * result.
1567
+ */
1568
+ async function runLevel(level, registry, orgId, resolver, idResolver, dryRun, parallelism, result) {
1569
+ const summaries = [];
1570
+ let cursor = 0;
1571
+ let aborted = false;
1572
+ async function worker() {
1573
+ while (!aborted) {
1574
+ const i = cursor++;
1575
+ if (i >= level.length) return;
1576
+ const resource = level[i];
1577
+ const provider = registry.get(resource.kind);
1578
+ if (!provider) continue;
1579
+ const ctx = {
1580
+ orgId,
1581
+ resolveRef: resolver,
1582
+ resolveById: idResolver
1583
+ };
1584
+ try {
1585
+ const summary = await applyOne(provider, ctx, resource, dryRun);
1586
+ summaries.push(summary);
1587
+ } catch (err) {
1588
+ aborted = true;
1589
+ result.errors.push({
1590
+ code: "apply",
1591
+ message: err instanceof Error ? `${address(resource.kind, resource.metadata.name)}: ${err.message}` : String(err),
1592
+ source: resource.source
1593
+ });
1594
+ }
1595
+ }
1596
+ }
1597
+ const workers = Array.from({ length: Math.min(parallelism, level.length) }, () => worker());
1598
+ await Promise.all(workers);
1599
+ return summaries;
1600
+ }
1601
+ async function applyOne(provider, ctx, resource, dryRun) {
1602
+ const base = {
1603
+ address: address(resource.kind, resource.metadata.name),
1604
+ kind: resource.kind,
1605
+ name: resource.metadata.name,
1606
+ file: resource.source.file
1607
+ };
1608
+ if (!resource.metadata.id) {
1609
+ const createDiff = diffSpecs({}, resource.spec, provider.sensitivePaths);
1610
+ if (dryRun) return {
1611
+ ...base,
1612
+ action: "create",
1613
+ diff: createDiff
1614
+ };
1615
+ const created = await provider.create(ctx, resource.spec);
1616
+ rewriteYamlFile(resource.source.file, [{
1617
+ docIndex: resource.docIndex,
1618
+ mutate: (doc) => {
1619
+ doc.setIn(["metadata", "id"], created.id);
1620
+ for (const path of provider.computedPaths ?? []) {
1621
+ const segs = path.split(".");
1622
+ if (hasIn$1(resource.rawSpec, segs)) continue;
1623
+ const value = getIn$1(created.spec, segs);
1624
+ if (value === void 0) continue;
1625
+ doc.setIn(["spec", ...segs], value);
1626
+ }
1627
+ }
1628
+ }]);
1629
+ return {
1630
+ ...base,
1631
+ id: created.id,
1632
+ action: "create",
1633
+ diff: createDiff
1634
+ };
1635
+ }
1636
+ const id = resource.metadata.id;
1637
+ const remote = await provider.read(ctx, id);
1638
+ if (!remote) throw new Error(`remote resource ${id} not found — local file references an id that no longer exists. Either remove metadata.id to recreate, or pull a fresh state.`);
1639
+ const localNorm = provider.normalize(resource.spec);
1640
+ const drift = diffSpecs(provider.normalize(remote.spec), localNorm, provider.sensitivePaths);
1641
+ if (drift.length === 0) return {
1642
+ ...base,
1643
+ id,
1644
+ action: "unchanged"
1645
+ };
1646
+ if (dryRun) return {
1647
+ ...base,
1648
+ id,
1649
+ action: "update",
1650
+ diff: drift
1651
+ };
1652
+ await provider.update(ctx, id, resource.spec);
1653
+ return {
1654
+ ...base,
1655
+ id,
1656
+ action: "update",
1657
+ diff: drift
1658
+ };
1659
+ }
1660
+ /**
1661
+ * Resolve `(kind, name)` against the local index to the resource's current
1662
+ * `metadata.id`. The DAG guarantees dependencies are applied first, but if
1663
+ * we get here for a resource whose dependency was never applied (e.g. a
1664
+ * skipped kind without a registered provider), `metadata.id` will be
1665
+ * undefined and the provider's create/update body construction will fail
1666
+ * with a clear error.
1667
+ */
1668
+ function makeResolver(resources) {
1669
+ const byAddress = /* @__PURE__ */ new Map();
1670
+ for (const r of resources) byAddress.set(address(r.kind, r.metadata.name), r);
1671
+ return (ref) => byAddress.get(address(ref.kind, ref.name))?.metadata.id;
1672
+ }
1673
+ function hasIn$1(obj, path) {
1674
+ let cur = obj;
1675
+ for (const seg of path) {
1676
+ if (typeof cur !== "object" || cur === null || !(seg in cur)) return false;
1677
+ cur = cur[seg];
1678
+ }
1679
+ return true;
1680
+ }
1681
+ function getIn$1(obj, path) {
1682
+ let cur = obj;
1683
+ for (const seg of path) {
1684
+ if (typeof cur !== "object" || cur === null) return void 0;
1685
+ cur = cur[seg];
1686
+ }
1687
+ return cur;
1688
+ }
1689
+ function makeIdResolver(resources) {
1690
+ const byKindId = /* @__PURE__ */ new Map();
1691
+ for (const r of resources) if (r.metadata.id) byKindId.set(`${r.kind}/${r.metadata.id}`, r.metadata.name);
1692
+ return (kind, id) => byKindId.get(`${kind}/${id}`);
1693
+ }
1694
+ //#endregion
1695
+ //#region src/core/iac/util/yaml.ts
1696
+ /**
1697
+ * Indent a YAML string by `baseIndent` levels (2-space indent per level).
1698
+ */
1699
+ function stringifySpec(spec, baseIndent) {
1700
+ const indent = " ".repeat(baseIndent);
1701
+ return stringify(spec ?? {}, { indent: 2 }).trimEnd().split("\n").map((line) => line.length === 0 ? line : indent + line).join("\n");
1702
+ }
1703
+ //#endregion
1704
+ //#region src/core/iac/pull/pull.ts
1705
+ /**
1706
+ * Reconcile local YAML with the API in the OPPOSITE direction of `apply`.
1707
+ *
1708
+ * For each registered kind (in topological order over `Provider.dependsOn`):
1709
+ * 1. List every remote resource of that kind.
1710
+ * 2. Match each remote against the local index by `metadata.id`. If the
1711
+ * local exists, reconcile it (rewrite spec when drifted, leave alone
1712
+ * when unchanged). If no local has that id, *discover* the remote: pick
1713
+ * a default `metadata.name` and write a new YAML file under
1714
+ * `<kind-lowercase>s/<name>.yaml`.
1715
+ * 3. Local resources whose `metadata.id` was not in the remote list get
1716
+ * `skipped-not-found`. Local resources without `metadata.id` are
1717
+ * unmapped — they get `skipped-no-id`.
1718
+ *
1719
+ * Refuses to overwrite files with uncommitted changes unless `--force`. New
1720
+ * files (discoveries) are written even when --force is unset, since they
1721
+ * can't conflict with anything on disk.
1722
+ */
1723
+ async function pull(opts) {
1724
+ const load = loadResources({
1725
+ project: opts.project,
1726
+ registry: opts.registry,
1727
+ startPath: opts.startPath,
1728
+ env: opts.env,
1729
+ validateSpecs: false
1730
+ });
1731
+ if (load.errors.length > 0) return {
1732
+ pulled: [],
1733
+ errors: load.errors
1734
+ };
1735
+ const isDirty = opts.isDirty ?? defaultIsDirty;
1736
+ const dryRun = opts.dryRun ?? false;
1737
+ const localById = /* @__PURE__ */ new Map();
1738
+ const localByAddress = /* @__PURE__ */ new Map();
1739
+ for (const r of load.index.all) {
1740
+ if (r.metadata.id) localById.set(`${r.kind}/${r.metadata.id}`, r);
1741
+ localByAddress.set(address(r.kind, r.metadata.name), r);
1742
+ }
1743
+ const byId = /* @__PURE__ */ new Map();
1744
+ for (const r of load.index.all) if (r.metadata.id) byId.set(`${r.kind}/${r.metadata.id}`, r.metadata.name);
1745
+ const idResolver = (kind, id) => byId.get(`${kind}/${id}`);
1746
+ const ctx = {
1747
+ orgId: opts.project.metadata.orgId,
1748
+ resolveRef: () => void 0,
1749
+ resolveById: idResolver
1750
+ };
1751
+ const result = {
1752
+ pulled: [],
1753
+ errors: []
1754
+ };
1755
+ const matchedLocal = /* @__PURE__ */ new Set();
1756
+ const editsByFile = /* @__PURE__ */ new Map();
1757
+ const providers = orderedProviders(opts.registry);
1758
+ const listed = await Promise.all(providers.map(async (provider) => {
1759
+ try {
1760
+ return {
1761
+ provider,
1762
+ remotes: await provider.listRemote(ctx),
1763
+ error: null
1764
+ };
1765
+ } catch (err) {
1766
+ return {
1767
+ provider,
1768
+ remotes: [],
1769
+ error: {
1770
+ code: "pull",
1771
+ message: `${provider.kind}: ${err instanceof Error ? err.message : String(err)}`
1772
+ }
1773
+ };
1774
+ }
1775
+ }));
1776
+ for (const { provider, remotes, error } of listed) {
1777
+ if (error) {
1778
+ result.errors.push(error);
1779
+ return result;
1780
+ }
1781
+ for (const r of remotes) {
1782
+ const local = localById.get(`${provider.kind}/${r.id}`);
1783
+ if (local) {
1784
+ matchedLocal.add(local);
1785
+ const summary = await reconcileExisting({
1786
+ provider,
1787
+ local,
1788
+ remote: r,
1789
+ ctx,
1790
+ isDirty,
1791
+ force: opts.force ?? false,
1792
+ editsByFile
1793
+ });
1794
+ result.pulled.push(summary);
1795
+ } else {
1796
+ const summary = discoverNew({
1797
+ provider,
1798
+ remote: r,
1799
+ ctx,
1800
+ project: opts.project,
1801
+ localByAddress,
1802
+ dryRun
1803
+ });
1804
+ if (summary.action === "discovered") {
1805
+ byId.set(`${provider.kind}/${r.id}`, summary.name);
1806
+ localByAddress.set(summary.address, {
1807
+ kind: provider.kind,
1808
+ metadata: {
1809
+ name: summary.name,
1810
+ id: r.id
1811
+ },
1812
+ spec: {},
1813
+ rawSpec: {},
1814
+ source: { file: summary.file },
1815
+ docIndex: 0,
1816
+ document: void 0
1817
+ });
1818
+ }
1819
+ result.pulled.push(summary);
1820
+ }
1821
+ }
1822
+ }
1823
+ if (!dryRun) {
1824
+ for (const [file, edits] of editsByFile) if (edits.length > 0) rewriteYamlFile(file, edits);
1825
+ }
1826
+ for (const r of load.index.all) {
1827
+ if (matchedLocal.has(r)) continue;
1828
+ const base = {
1829
+ address: address(r.kind, r.metadata.name),
1830
+ kind: r.kind,
1831
+ name: r.metadata.name,
1832
+ file: r.source.file
1833
+ };
1834
+ if (!r.metadata.id) result.pulled.push({
1835
+ ...base,
1836
+ action: "skipped-no-id"
1837
+ });
1838
+ else result.pulled.push({
1839
+ ...base,
1840
+ id: r.metadata.id,
1841
+ action: "skipped-not-found"
1842
+ });
1843
+ }
1844
+ return result;
1845
+ }
1846
+ async function reconcileExisting(args) {
1847
+ const { provider, local, remote, ctx, isDirty, force, editsByFile } = args;
1848
+ const file = local.source.file;
1849
+ const base = {
1850
+ address: address(provider.kind, local.metadata.name),
1851
+ kind: provider.kind,
1852
+ name: local.metadata.name,
1853
+ id: remote.id,
1854
+ file
1855
+ };
1856
+ if (!force && isDirty(file)) return {
1857
+ ...base,
1858
+ action: "skipped-dirty"
1859
+ };
1860
+ const remoteSpec = provider.inflateRemote(remote.remote, ctx);
1861
+ const writeSpec = mergeForPull(local.rawSpec, remoteSpec, provider);
1862
+ if (deepEqual(local.rawSpec, writeSpec)) return {
1863
+ ...base,
1864
+ action: "unchanged"
1865
+ };
1866
+ const edits = editsByFile.get(file) ?? [];
1867
+ edits.push({
1868
+ docIndex: local.docIndex,
1869
+ mutate: (doc) => doc.setIn(["spec"], writeSpec)
1870
+ });
1871
+ editsByFile.set(file, edits);
1872
+ return {
1873
+ ...base,
1874
+ action: "pulled"
1875
+ };
1876
+ }
1877
+ function discoverNew(args) {
1878
+ const { provider, remote, ctx, project, localByAddress, dryRun } = args;
1879
+ const root = projectRoot(project);
1880
+ const folder = `${provider.kind.toLowerCase()}s`;
1881
+ const name = dedupeName(provider.deriveName(remote.remote), provider.kind, localByAddress);
1882
+ const addr = address(provider.kind, name);
1883
+ const file = path.join(root, folder, `${name}.yaml`);
1884
+ const spec = stripSensitivePaths(provider.inflateRemote(remote.remote, ctx), provider.sensitivePaths);
1885
+ const yaml = renderResourceYaml({
1886
+ kind: provider.kind,
1887
+ name,
1888
+ id: remote.id,
1889
+ spec
1890
+ });
1891
+ if (!dryRun) {
1892
+ fs.mkdirSync(path.dirname(file), { recursive: true });
1893
+ fs.writeFileSync(file, yaml, { flag: "wx" });
1894
+ }
1895
+ return {
1896
+ address: addr,
1897
+ kind: provider.kind,
1898
+ name,
1899
+ id: remote.id,
1900
+ file,
1901
+ action: "discovered"
1902
+ };
1903
+ }
1904
+ /**
1905
+ * Topologically order providers so a kind that depends on another kind is
1906
+ * processed after it.
1907
+ */
1908
+ function orderedProviders(registry) {
1909
+ const all = [];
1910
+ for (const k of registry.kinds()) {
1911
+ const p = registry.get(k);
1912
+ if (p) all.push(p);
1913
+ }
1914
+ const ordered = [];
1915
+ const placed = /* @__PURE__ */ new Set();
1916
+ while (ordered.length < all.length) {
1917
+ const before = ordered.length;
1918
+ for (const p of all) {
1919
+ if (placed.has(p.kind)) continue;
1920
+ if (p.dependsOn.filter((d) => all.some((q) => q.kind === d)).every((d) => placed.has(d))) {
1921
+ ordered.push(p);
1922
+ placed.add(p.kind);
1923
+ }
1924
+ }
1925
+ if (ordered.length === before) {
1926
+ for (const p of all) if (!placed.has(p.kind)) ordered.push(p);
1927
+ break;
1928
+ }
1929
+ }
1930
+ return ordered;
1931
+ }
1932
+ function dedupeName(base, kind, taken) {
1933
+ if (!taken.has(address(kind, base))) return base;
1934
+ let i = 2;
1935
+ while (taken.has(address(kind, `${base}-${i}`))) i++;
1936
+ return `${base}-${i}`;
1937
+ }
1938
+ function renderResourceYaml(args) {
1939
+ const lines = [
1940
+ "apiVersion: revos/v1",
1941
+ `kind: ${args.kind}`,
1942
+ "metadata:",
1943
+ ` name: ${args.name}`,
1944
+ ` id: ${args.id}`,
1945
+ "spec:"
1946
+ ];
1947
+ const specYaml = stringifySpec(args.spec, 1);
1948
+ return lines.join("\n") + "\n" + specYaml + "\n";
1949
+ }
1950
+ function defaultIsDirty(filePath) {
1951
+ try {
1952
+ const dir = path.dirname(filePath);
1953
+ return execFileSync("git", [
1954
+ "status",
1955
+ "--porcelain",
1956
+ "--",
1957
+ filePath
1958
+ ], {
1959
+ cwd: dir,
1960
+ encoding: "utf-8",
1961
+ stdio: [
1962
+ "ignore",
1963
+ "pipe",
1964
+ "ignore"
1965
+ ]
1966
+ }).trim().length > 0;
1967
+ } catch {
1968
+ return false;
1969
+ }
1970
+ }
1971
+ function deepEqual(a, b) {
1972
+ if (a === b) return true;
1973
+ if (typeof a !== typeof b) return false;
1974
+ if (a === null || b === null) return a === b;
1975
+ if (Array.isArray(a)) {
1976
+ if (!Array.isArray(b) || a.length !== b.length) return false;
1977
+ return a.every((v, i) => deepEqual(v, b[i]));
1978
+ }
1979
+ if (typeof a === "object" && typeof b === "object") {
1980
+ const ao = a;
1981
+ const bo = b;
1982
+ const keys = Object.keys(ao);
1983
+ if (keys.length !== Object.keys(bo).length) return false;
1984
+ return keys.every((k) => k in bo && deepEqual(ao[k], bo[k]));
1985
+ }
1986
+ return false;
1987
+ }
1988
+ function stripSensitivePaths(spec, sensitivePaths) {
1989
+ const cloned = JSON.parse(JSON.stringify(spec ?? null));
1990
+ for (const dotted of sensitivePaths) deleteIn(cloned, dotted.split("."));
1991
+ return cloned;
1992
+ }
1993
+ /**
1994
+ * Build the spec we will write back to disk. Start from the inflated
1995
+ * remote, then merge in local values for the provider's `localOnlyPaths`
1996
+ * (refs the server doesn't echo back) and `sensitivePaths` (server-redacted
1997
+ * fields where the local file is the source of truth). For both lists,
1998
+ * absence in local also wins — i.e. we delete the path from the merged
1999
+ * spec rather than keep the server's redacted placeholder.
2000
+ */
2001
+ function mergeForPull(localSpec, remoteSpec, provider) {
2002
+ const merged = JSON.parse(JSON.stringify(remoteSpec ?? null));
2003
+ const paths = [...provider.localOnlyPaths, ...provider.sensitivePaths];
2004
+ for (const dotted of paths) {
2005
+ const segs = dotted.split(".");
2006
+ if (hasIn(localSpec, segs)) setIn(merged, segs, getIn(localSpec, segs));
2007
+ else deleteIn(merged, segs);
2008
+ }
2009
+ return merged;
2010
+ }
2011
+ function hasIn(obj, path) {
2012
+ let cur = obj;
2013
+ for (const seg of path) {
2014
+ if (typeof cur !== "object" || cur === null || !(seg in cur)) return false;
2015
+ cur = cur[seg];
2016
+ }
2017
+ return true;
2018
+ }
2019
+ function getIn(obj, path) {
2020
+ let cur = obj;
2021
+ for (const seg of path) {
2022
+ if (typeof cur !== "object" || cur === null) return void 0;
2023
+ cur = cur[seg];
2024
+ }
2025
+ return cur;
2026
+ }
2027
+ function setIn(obj, path, value) {
2028
+ let cur = obj;
2029
+ for (let i = 0; i < path.length - 1; i++) {
2030
+ const seg = path[i];
2031
+ const next = cur[seg];
2032
+ if (typeof next !== "object" || next === null) cur[seg] = {};
2033
+ cur = cur[seg];
2034
+ }
2035
+ cur[path[path.length - 1]] = value;
2036
+ }
2037
+ function deleteIn(obj, path) {
2038
+ let cur = obj;
2039
+ for (let i = 0; i < path.length - 1; i++) {
2040
+ const seg = path[i];
2041
+ const next = cur[seg];
2042
+ if (typeof next !== "object" || next === null) return;
2043
+ cur = next;
2044
+ }
2045
+ delete cur[path[path.length - 1]];
2046
+ }
2047
+ //#endregion
2048
+ //#region src/core/iac/status.ts
2049
+ /**
2050
+ * Phase 1: state is local-only. Resources without `metadata.id` are `pending`;
2051
+ * resources whose metadata failed validation are `tampered` and surface from
2052
+ * the loader's error list. Phase 3+ will add `ok` and `drifted` once the apply
2053
+ * + diff paths exist.
2054
+ */
2055
+ function resourceState(r) {
2056
+ if (!r.metadata.id) return "pending";
2057
+ return "ok";
2058
+ }
2059
+ function describeResources(load) {
2060
+ const out = [];
2061
+ const tamperedAddrs = new Set(load.errors.filter((e) => e.code === "tampered" && e.source).map((e) => e.message.split(":")[0]));
2062
+ for (const r of load.index.all) {
2063
+ const addr = address(r.kind, r.metadata.name);
2064
+ out.push({
2065
+ address: addr,
2066
+ kind: r.kind,
2067
+ name: r.metadata.name,
2068
+ id: r.metadata.id,
2069
+ state: tamperedAddrs.has(addr) ? "tampered" : resourceState(r),
2070
+ source: {
2071
+ file: r.source.file,
2072
+ line: r.source.line
2073
+ }
2074
+ });
2075
+ }
2076
+ return out.sort((a, b) => a.address.localeCompare(b.address));
2077
+ }
2078
+ //#endregion
2079
+ //#region src/core/iac/index.ts
2080
+ var iac_exports = /* @__PURE__ */ __exportAll({
2081
+ API_VERSION: () => API_VERSION,
2082
+ IacAggregateError: () => IacAggregateError,
2083
+ PROJECT_FILE: () => PROJECT_FILE,
2084
+ PROJECT_KIND: () => PROJECT_KIND,
2085
+ ProjectNotFoundError: () => ProjectNotFoundError,
2086
+ ProviderRegistry: () => ProviderRegistry,
2087
+ address: () => address,
2088
+ apply: () => apply,
2089
+ applyDocumentEdits: () => applyDocumentEdits,
2090
+ asResourceDoc: () => asResourceDoc,
2091
+ buildDependencyGraph: () => buildDependencyGraph,
2092
+ buildIacRegistry: () => buildIacRegistry,
2093
+ buildIndex: () => buildIndex,
2094
+ connectionSpecSchema: () => connectionSpecSchema,
2095
+ createConnectionProvider: () => createConnectionProvider,
2096
+ createCubeProvider: () => createCubeProvider,
2097
+ createSdkConnectionsClient: () => createSdkConnectionsClient,
2098
+ createSdkCubesClient: () => createSdkCubesClient,
2099
+ cubeSpecSchema: () => cubeSpecSchema,
2100
+ describeResources: () => describeResources,
2101
+ diffSpecs: () => diffSpecs,
2102
+ discoverProject: () => discoverProject,
2103
+ formatDiffLine: () => formatDiffLine,
2104
+ interpolateSpec: () => interpolateSpec,
2105
+ isResourceDocument: () => isResourceDocument,
2106
+ isValidName: () => isValidName,
2107
+ loadResources: () => loadResources,
2108
+ locationFromOffset: () => locationFromOffset,
2109
+ parseFile: () => parseFile,
2110
+ parseFiles: () => parseFiles,
2111
+ projectRoot: () => projectRoot,
2112
+ pull: () => pull,
2113
+ readResourceShape: () => readResourceShape,
2114
+ resourceState: () => resourceState,
2115
+ rewriteYamlFile: () => rewriteYamlFile,
2116
+ scanYamlFiles: () => scanYamlFiles,
2117
+ setMetadataId: () => setMetadataId,
2118
+ validateAll: () => validateAll,
2119
+ validateResource: () => validateResource,
2120
+ writeProjectFile: () => writeProjectFile
2121
+ });
2122
+ //#endregion
2123
+ //#region src/core/services/org-selector.ts
2124
+ async function selectOrganization(organizations) {
2125
+ const answer = await search({
2126
+ message: "Select an organization",
2127
+ source: (input) => {
2128
+ const term = (input ?? "").toLowerCase();
2129
+ return organizations.filter((org) => org.name.toLowerCase().includes(term)).map((org) => ({
2130
+ name: org.name,
2131
+ value: org.id
2132
+ }));
2133
+ }
2134
+ });
2135
+ const selected = organizations.find((o) => o.id === answer);
2136
+ if (!selected) throw new Error("Organization not found");
2137
+ return selected;
2138
+ }
2139
+ //#endregion
2140
+ //#region src/core/services/init.service.ts
2141
+ var InitService = class InitService {
2142
+ static PROJECT_DIRS = [
2143
+ ".devcontainer",
2144
+ ".claude/skills/explore-lakehouse",
2145
+ ".claude/skills/create-connections",
2146
+ ".claude/skills/create-connections/references",
2147
+ ".claude/skills/create-cubes",
2148
+ ".claude/skills/create-cubes/references",
2149
+ ".claude/skills/create-dbt-transformations",
2150
+ ".claude/skills/create-dbt-transformations/references",
2151
+ ".claude/skills/load-sample-data",
2152
+ ".claude/skills/visualize-semantic-model",
2153
+ ".claude/skills/visualize-semantic-model/scripts",
2154
+ "dbt/models/bronze",
2155
+ "dbt/models/silver",
2156
+ "dbt/models/gold",
2157
+ "cubes"
2158
+ ];
2159
+ static PROJECT_FILES = [
2160
+ "revos.yaml",
2161
+ ".devcontainer/devcontainer.json",
2162
+ ".devcontainer/Dockerfile",
2163
+ ".devcontainer/setup.sh",
2164
+ ".gitignore",
2165
+ "README.md",
2166
+ "dbt/profiles.yml",
2167
+ "dbt/dbt_project.yml",
2168
+ "CLAUDE.md",
2169
+ "AGENTS.md",
2170
+ ".claude/settings.json",
2171
+ ".claude/skills/explore-lakehouse/SKILL.md",
2172
+ ".claude/skills/create-connections/SKILL.md",
2173
+ ".claude/skills/create-connections/references/mappers.md",
2174
+ ".claude/skills/create-cubes/SKILL.md",
2175
+ ".claude/skills/create-cubes/references/cube-examples.md",
2176
+ ".claude/skills/create-cubes/references/key-patterns.md",
2177
+ ".claude/skills/create-cubes/references/validation-queries.md",
2178
+ ".claude/skills/create-cubes/references/hubspot-entities.md",
2179
+ ".claude/skills/create-cubes/references/jira-entities.md",
2180
+ ".claude/skills/create-cubes/references/stripe-entities.md",
2181
+ ".claude/skills/create-cubes/references/netsuite-entities.md",
2182
+ ".claude/skills/create-cubes/references/bq-pk-fk-conventions.md",
2183
+ ".claude/skills/create-dbt-transformations/SKILL.md",
2184
+ ".claude/skills/create-dbt-transformations/references/sql-templates.md",
2185
+ ".claude/skills/create-dbt-transformations/references/schema-conventions.md",
2186
+ ".claude/skills/create-dbt-transformations/references/edge-cases.md",
2187
+ ".claude/skills/load-sample-data/SKILL.md",
2188
+ ".claude/skills/visualize-semantic-model/SKILL.md",
2189
+ ".claude/skills/visualize-semantic-model/scripts/render_graph.py",
2190
+ "dbt/models/bronze/.gitkeep",
2191
+ "dbt/models/silver/.gitkeep",
2192
+ "dbt/models/gold/.gitkeep",
2193
+ "cubes/.gitkeep"
2194
+ ];
2195
+ constructor(templatesDir) {
2196
+ this.templatesDir = templatesDir;
2197
+ }
2198
+ async run(options) {
2199
+ const { projectDir, apiUrl, token, organizationId } = options;
2200
+ const org = options.organization ?? await this.resolveOrganization(apiUrl, token, organizationId);
2201
+ const projectName = org.name;
2202
+ const projectSlug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
2203
+ const gcpProjectId = await this.downloadGcpKey(apiUrl, token, org, projectSlug);
2204
+ const resolvedOrg = {
2205
+ ...org,
2206
+ bqProjectId: org.bqProjectId || gcpProjectId,
2207
+ bqLocation: org.bqLocation || "europe-west3"
2208
+ };
2209
+ return {
2210
+ projectDir,
2211
+ organization: resolvedOrg,
2212
+ createdFiles: this.scaffold(projectDir, projectName, projectSlug, resolvedOrg, options.allowExistingDir),
2213
+ pulledIac: options.pullIac === false ? {
2214
+ pulled: [],
2215
+ errors: []
2216
+ } : await this.pullIacResources({
2217
+ projectDir,
2218
+ apiUrl,
2219
+ token,
2220
+ orgId: org.id
2221
+ })
2222
+ };
2223
+ }
2224
+ /**
2225
+ * Discover all IaC resources (Connections, Cubes) currently in the org
2226
+ * and write them as YAML under `<projectDir>/<kind>s/`. Best-effort: any
2227
+ * error is captured in the returned result rather than aborting init,
2228
+ * since the project scaffolding has already succeeded.
2229
+ */
2230
+ async pullIacResources(args) {
2231
+ const project = discoverProject({ cwd: args.projectDir });
2232
+ const registry = buildIacRegistry(createApiClient({
2233
+ apiUrl: args.apiUrl,
2234
+ token: args.token,
2235
+ organizationId: args.orgId
2236
+ }));
2237
+ try {
2238
+ return await pull({
2239
+ project,
2240
+ registry
2241
+ });
2242
+ } catch (err) {
2243
+ return {
2244
+ pulled: [],
2245
+ errors: [{
2246
+ code: "init.pull",
2247
+ message: formatError(err)
2248
+ }]
2249
+ };
2250
+ }
2251
+ }
2252
+ async resolveOrganization(apiUrl, token, organizationId) {
2253
+ const api = createApiClient({
2254
+ apiUrl,
2255
+ token
2256
+ });
2257
+ if (organizationId) {
2258
+ const full = unwrap(await api.organizations.get({ id: organizationId }));
2259
+ if (!full.bqDataset) throw new Error("Organization is missing BigQuery dataset configuration. Contact support.");
2260
+ return full;
2261
+ }
2262
+ const orgs = unwrap(await api.organizations.list()) ?? [];
2263
+ if (orgs.length === 0) throw new Error("No organizations found for this account.");
2264
+ const selected = orgs.length === 1 ? orgs[0] : await selectOrganization(orgs);
2265
+ const full = unwrap(await api.organizations.get({ id: selected.id }));
2266
+ if (!full.bqDataset) throw new Error("Organization is missing BigQuery dataset configuration. Contact support.");
2267
+ return full;
2268
+ }
2269
+ async downloadGcpKey(apiUrl, token, org, projectSlug) {
2270
+ const api = createApiClient({
2271
+ apiUrl,
2272
+ token,
2273
+ organizationId: org.id
2274
+ });
2275
+ const keyId = unwrap(await api.gserviceAccounts.create({ body: { name: projectSlug } }))?.gServiceAccountKeys?.[0]?.id;
2276
+ if (!keyId) throw new Error("Service account has no keys");
2277
+ const keyJson = unwrap(await api.gserviceAccountKeys.reveal({ id: keyId }))?.key;
2278
+ if (!keyJson) throw new Error("Service account key is empty");
2279
+ const gcpKeyPath = path.join(os.homedir(), ".revos", `${projectSlug}-gsa-creds.json`);
2280
+ fs.mkdirSync(path.dirname(gcpKeyPath), { recursive: true });
2281
+ fs.writeFileSync(gcpKeyPath, keyJson, process.platform !== "win32" ? {
2282
+ encoding: "utf-8",
2283
+ mode: 384
2284
+ } : { encoding: "utf-8" });
2285
+ return JSON.parse(keyJson).project_id ?? "";
2286
+ }
2287
+ dryRun(projectDir) {
2288
+ return {
2289
+ projectDir,
2290
+ dirs: InitService.PROJECT_DIRS,
2291
+ files: InitService.PROJECT_FILES
2292
+ };
2293
+ }
2294
+ scaffold(projectDir, projectName, projectSlug, org, allowExistingDir) {
2295
+ if (fs.existsSync(projectDir) && !allowExistingDir) throw new Error(`Directory "${projectName}" already exists. Remove it or choose a different name.`);
2296
+ const created = [];
2297
+ const dirs = InitService.PROJECT_DIRS;
2298
+ for (const dir of dirs) fs.mkdirSync(path.join(projectDir, dir), { recursive: true });
2299
+ const dbtName = projectName.replace(/[^a-zA-Z0-9_]/g, "_");
2300
+ const files = {
2301
+ "revos.yaml": this.renderProjectMarker(projectSlug, org.id),
2302
+ ".devcontainer/devcontainer.json": this.renderTemplate(".devcontainer/devcontainer.json", {
2303
+ projectName,
2304
+ projectSlug,
2305
+ bqProjectId: org.bqProjectId || "",
2306
+ bqDataset: org.bqDataset,
2307
+ organizationId: org.id
2308
+ }),
2309
+ ".devcontainer/Dockerfile": this.renderTemplate(".devcontainer/Dockerfile", {}),
2310
+ ".devcontainer/setup.sh": this.renderTemplate(".devcontainer/setup.sh", {}),
2311
+ ".gitignore": this.renderTemplate("gitignore", {}),
2312
+ "README.md": this.renderTemplate("README.md", {
2313
+ projectName,
2314
+ orgName: org.name
2315
+ }),
2316
+ "dbt/profiles.yml": this.renderTemplate("dbt/profiles.yml", { bqLocation: org.bqLocation ?? "europe-west3" }),
2317
+ "dbt/dbt_project.yml": this.renderTemplate("dbt/dbt_project.yml", { dbtName }),
2318
+ "CLAUDE.md": this.renderTemplate("CLAUDE.md", {}),
2319
+ "AGENTS.md": this.renderTemplate("AGENTS.md", {
2320
+ projectName,
2321
+ orgName: org.name
2322
+ }),
2323
+ ".claude/settings.json": this.renderTemplate(".claude/settings.json", {}),
2324
+ ".claude/skills/explore-lakehouse/SKILL.md": this.renderTemplate("skills/explore-lakehouse/SKILL.md", {
2325
+ bqProjectId: org.bqProjectId || "",
2326
+ bqDataset: org.bqDataset
2327
+ }),
2328
+ ".claude/skills/create-connections/SKILL.md": this.renderTemplate("skills/create-connections/SKILL.md", {}),
2329
+ ".claude/skills/create-connections/references/mappers.md": this.renderTemplate("skills/create-connections/references/mappers.md", {}),
2330
+ ".claude/skills/create-cubes/SKILL.md": this.renderTemplate("skills/create-cubes/SKILL.md", {}),
2331
+ ".claude/skills/create-cubes/references/cube-examples.md": this.renderTemplate("skills/create-cubes/references/cube-examples.md", {}),
2332
+ ".claude/skills/create-cubes/references/key-patterns.md": this.renderTemplate("skills/create-cubes/references/key-patterns.md", {}),
2333
+ ".claude/skills/create-cubes/references/validation-queries.md": this.renderTemplate("skills/create-cubes/references/validation-queries.md", {}),
2334
+ ".claude/skills/create-cubes/references/hubspot-entities.md": this.renderTemplate("skills/create-cubes/references/hubspot-entities.md", {}),
2335
+ ".claude/skills/create-cubes/references/jira-entities.md": this.renderTemplate("skills/create-cubes/references/jira-entities.md", {}),
2336
+ ".claude/skills/create-cubes/references/stripe-entities.md": this.renderTemplate("skills/create-cubes/references/stripe-entities.md", {}),
2337
+ ".claude/skills/create-cubes/references/netsuite-entities.md": this.renderTemplate("skills/create-cubes/references/netsuite-entities.md", {}),
2338
+ ".claude/skills/create-cubes/references/bq-pk-fk-conventions.md": this.renderTemplate("skills/create-cubes/references/bq-pk-fk-conventions.md", {}),
2339
+ ".claude/skills/create-dbt-transformations/SKILL.md": this.renderTemplate("skills/create-dbt-transformations/SKILL.md", {}),
2340
+ ".claude/skills/create-dbt-transformations/references/sql-templates.md": this.renderTemplate("skills/create-dbt-transformations/references/sql-templates.md", {}),
2341
+ ".claude/skills/create-dbt-transformations/references/schema-conventions.md": this.renderTemplate("skills/create-dbt-transformations/references/schema-conventions.md", {}),
2342
+ ".claude/skills/create-dbt-transformations/references/edge-cases.md": this.renderTemplate("skills/create-dbt-transformations/references/edge-cases.md", {}),
2343
+ ".claude/skills/load-sample-data/SKILL.md": this.renderTemplate("skills/load-sample-data/SKILL.md", {}),
2344
+ ".claude/skills/visualize-semantic-model/SKILL.md": this.renderTemplate("skills/visualize-semantic-model/SKILL.md", {}),
2345
+ ".claude/skills/visualize-semantic-model/scripts/render_graph.py": this.renderTemplate("skills/visualize-semantic-model/scripts/render_graph.py", {}),
2346
+ "dbt/models/bronze/.gitkeep": "",
2347
+ "dbt/models/silver/.gitkeep": "",
2348
+ "dbt/models/gold/.gitkeep": "",
2349
+ "cubes/.gitkeep": ""
2350
+ };
2351
+ for (const [rel, content] of Object.entries(files)) {
2352
+ const full = path.join(projectDir, rel);
2353
+ fs.writeFileSync(full, content, "utf-8");
2354
+ if (rel.endsWith(".sh") && process.platform !== "win32") fs.chmodSync(full, 493);
2355
+ created.push(rel);
2356
+ }
2357
+ return created;
2358
+ }
2359
+ renderProjectMarker(name, orgId) {
2360
+ return [
2361
+ "apiVersion: revos/v1",
2362
+ "kind: Project",
2363
+ "metadata:",
2364
+ ` name: ${name}`,
2365
+ ` orgId: ${orgId}`,
2366
+ ""
2367
+ ].join("\n");
2368
+ }
2369
+ renderTemplate(relativePath, vars) {
2370
+ const filePath = path.join(this.templatesDir, relativePath);
2371
+ if (!fs.existsSync(filePath)) throw new Error(`Template "${relativePath}" not found — try reinstalling the revos CLI`);
2372
+ let content = fs.readFileSync(filePath, "utf-8");
2373
+ for (const [key, value] of Object.entries(vars)) content = content.replaceAll(`<%=${key}%>`, value);
2374
+ return content;
2375
+ }
2376
+ };
2377
+ //#endregion
2378
+ export { deleteCredentials as A, getActiveAuthConfig as C, setAuthEnv as D, setAuthConfig as E, ApiError as F, isTokenExpired as M, loadCredentials as N, tokenResponseToCredentials as O, saveCredentials as P, generatePKCEChallenge as S, refreshAccessToken as T, DEFAULT_API_URL as _, pull as a, buildAuthorizationUrl as b, loadResources as c, projectRoot as d, createApiClient as f, sanitizeFileName as g, formatError as h, describeResources as i, getCredentialsPath as j, startOAuthServer as k, buildIacRegistry as l, resolveAppUrl as m, selectOrganization as n, apply as o, unwrap as p, iac_exports as r, formatDiffLine as s, InitService as t, discoverProject as u, getConfig as v, getUserInfo as w, exchangeCodeForTokens as x, AUTH_ENVS as y };