@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.
- package/README.md +271 -71
- package/dist/adapters/oclif/commands/action-runs/get.mjs +1 -1
- package/dist/adapters/oclif/commands/action-runs/list.mjs +8 -2
- package/dist/adapters/oclif/commands/actions/get-input-schema.mjs +2 -2
- package/dist/adapters/oclif/commands/actions/get-params-schema.mjs +2 -2
- package/dist/adapters/oclif/commands/actions/get.mjs +1 -1
- package/dist/adapters/oclif/commands/actions/list.mjs +8 -4
- package/dist/adapters/oclif/commands/ai-instructions/create.mjs +1 -1
- package/dist/adapters/oclif/commands/ai-instructions/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/ai-instructions/get.mjs +1 -1
- package/dist/adapters/oclif/commands/ai-instructions/list.mjs +8 -2
- package/dist/adapters/oclif/commands/ai-instructions/update.mjs +1 -1
- package/dist/adapters/oclif/commands/api.d.mts +11 -0
- package/dist/adapters/oclif/commands/api.mjs +112 -0
- package/dist/adapters/oclif/commands/apply.d.mts +28 -0
- package/dist/adapters/oclif/commands/apply.mjs +77 -0
- package/dist/adapters/oclif/commands/auth/login.d.mts +5 -4
- package/dist/adapters/oclif/commands/auth/login.mjs +22 -11
- package/dist/adapters/oclif/commands/auth/logout.d.mts +1 -1
- package/dist/adapters/oclif/commands/auth/logout.mjs +2 -2
- package/dist/adapters/oclif/commands/auth/status.d.mts +2 -2
- package/dist/adapters/oclif/commands/auth/status.mjs +2 -2
- package/dist/adapters/oclif/commands/connections/create.d.mts +6 -0
- package/dist/adapters/oclif/commands/connections/create.mjs +8 -0
- package/dist/adapters/oclif/commands/connections/delete.d.mts +6 -0
- package/dist/adapters/oclif/commands/connections/delete.mjs +8 -0
- package/dist/adapters/oclif/commands/connections/get.d.mts +6 -0
- package/dist/adapters/oclif/commands/connections/get.mjs +8 -0
- package/dist/adapters/oclif/commands/connections/list.d.mts +6 -0
- package/dist/adapters/oclif/commands/connections/list.mjs +14 -0
- package/dist/adapters/oclif/commands/connections/update.d.mts +6 -0
- package/dist/adapters/oclif/commands/connections/update.mjs +8 -0
- package/dist/adapters/oclif/commands/cubes/create.d.mts +6 -0
- package/dist/adapters/oclif/commands/cubes/create.mjs +8 -0
- package/dist/adapters/oclif/commands/cubes/delete.d.mts +6 -0
- package/dist/adapters/oclif/commands/cubes/delete.mjs +8 -0
- package/dist/adapters/oclif/commands/cubes/get.d.mts +6 -0
- package/dist/adapters/oclif/commands/cubes/get.mjs +8 -0
- package/dist/adapters/oclif/commands/cubes/list.d.mts +6 -0
- package/dist/adapters/oclif/commands/cubes/list.mjs +13 -0
- package/dist/adapters/oclif/commands/cubes/update.d.mts +6 -0
- package/dist/adapters/oclif/commands/cubes/update.mjs +8 -0
- package/dist/adapters/oclif/commands/diff.d.mts +27 -0
- package/dist/adapters/oclif/commands/diff.mjs +66 -0
- package/dist/adapters/oclif/commands/gservice-account-keys/get.mjs +1 -1
- package/dist/adapters/oclif/commands/gservice-account-keys/reveal.mjs +2 -2
- package/dist/adapters/oclif/commands/gservice-accounts/create.mjs +1 -1
- package/dist/adapters/oclif/commands/gservice-accounts/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/gservice-accounts/get.mjs +1 -1
- package/dist/adapters/oclif/commands/gservice-accounts/list.mjs +7 -2
- package/dist/adapters/oclif/commands/init.d.mts +2 -1
- package/dist/adapters/oclif/commands/init.mjs +26 -23
- package/dist/adapters/oclif/commands/org/create.mjs +1 -1
- package/dist/adapters/oclif/commands/org/current.d.mts +2 -2
- package/dist/adapters/oclif/commands/org/current.mjs +2 -2
- package/dist/adapters/oclif/commands/org/get.mjs +1 -1
- package/dist/adapters/oclif/commands/org/list.d.mts +3 -11
- package/dist/adapters/oclif/commands/org/list.mjs +26 -26
- package/dist/adapters/oclif/commands/org/switch.d.mts +3 -2
- package/dist/adapters/oclif/commands/org/switch.mjs +10 -3
- package/dist/adapters/oclif/commands/pull.d.mts +28 -0
- package/dist/adapters/oclif/commands/pull.mjs +88 -0
- package/dist/adapters/oclif/commands/score-groups/create.mjs +3 -2
- package/dist/adapters/oclif/commands/score-groups/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/score-groups/get.mjs +1 -1
- package/dist/adapters/oclif/commands/score-groups/list.mjs +3 -2
- package/dist/adapters/oclif/commands/score-groups/update.mjs +1 -1
- package/dist/adapters/oclif/commands/scores/create.mjs +3 -2
- package/dist/adapters/oclif/commands/scores/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/scores/list.mjs +3 -2
- package/dist/adapters/oclif/commands/scores/update.mjs +1 -1
- package/dist/adapters/oclif/commands/segments/create.mjs +1 -1
- package/dist/adapters/oclif/commands/segments/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/segments/evaluate.mjs +2 -2
- package/dist/adapters/oclif/commands/segments/get-evaluation-history.mjs +2 -2
- package/dist/adapters/oclif/commands/segments/get-version.mjs +2 -2
- package/dist/adapters/oclif/commands/segments/get.mjs +1 -1
- package/dist/adapters/oclif/commands/segments/list-versions.mjs +16 -5
- package/dist/adapters/oclif/commands/segments/list.mjs +9 -2
- package/dist/adapters/oclif/commands/segments/restore-version.mjs +2 -2
- package/dist/adapters/oclif/commands/segments/update.mjs +1 -1
- package/dist/adapters/oclif/commands/sources/create.d.mts +11 -0
- package/dist/adapters/oclif/commands/sources/create.mjs +16 -0
- package/dist/adapters/oclif/commands/sources/delete.d.mts +6 -0
- package/dist/adapters/oclif/commands/sources/delete.mjs +8 -0
- package/dist/adapters/oclif/commands/sources/get.d.mts +6 -0
- package/dist/adapters/oclif/commands/sources/get.mjs +8 -0
- package/dist/adapters/oclif/commands/sources/list-streams.d.mts +6 -0
- package/dist/adapters/oclif/commands/sources/list-streams.mjs +31 -0
- package/dist/adapters/oclif/commands/sources/list.d.mts +6 -0
- package/dist/adapters/oclif/commands/sources/list.mjs +13 -0
- package/dist/adapters/oclif/commands/{integrations/get.d.mts → sources/update.d.mts} +4 -4
- package/dist/adapters/oclif/commands/sources/update.mjs +21 -0
- package/dist/adapters/oclif/commands/status.d.mts +26 -0
- package/dist/adapters/oclif/commands/status.mjs +77 -0
- package/dist/adapters/oclif/commands/table-views/create.mjs +3 -2
- package/dist/adapters/oclif/commands/table-views/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/table-views/list.mjs +3 -2
- package/dist/adapters/oclif/commands/table-views/update.mjs +1 -1
- package/dist/adapters/oclif/commands/tables/create.mjs +1 -1
- package/dist/adapters/oclif/commands/tables/delete.mjs +1 -1
- package/dist/adapters/oclif/commands/tables/get.mjs +1 -1
- package/dist/adapters/oclif/commands/tables/list.mjs +3 -2
- package/dist/adapters/oclif/commands/tables/update.mjs +1 -1
- package/dist/{base.command-d7VW6WTp.d.mts → base.command-D7X3ZNtY.d.mts} +0 -1
- package/dist/{base.command-YiwlGlKs.mjs → base.command-cV5d65r8.mjs} +15 -12
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/core-CMrP5BQS.mjs +2378 -0
- package/dist/{factory-BrFKT8t-.mjs → factory-C6XLqhT9.mjs} +44 -10
- package/dist/iac-render-BSZZEP0n.mjs +17 -0
- package/dist/index-BqKwXXAo.d.mts +598 -0
- package/dist/index.d.mts +3 -4
- package/dist/index.mjs +2 -2
- package/dist/{presets-D9b6IWKy.mjs → presets-CJbFbHlw.mjs} +35 -8
- package/dist/templates/.claude/settings.json +39 -0
- package/dist/templates/.devcontainer/setup.sh +3 -0
- package/dist/templates/AGENTS.md +33 -20
- package/dist/templates/dbt/dbt_project.yml +2 -2
- package/dist/templates/skills/create-connections/SKILL.md +210 -0
- package/dist/templates/skills/create-connections/references/mappers.md +152 -0
- package/dist/templates/skills/{create-semantic-model → create-cubes}/SKILL.md +20 -18
- package/dist/templates/skills/create-cubes/references/bq-pk-fk-conventions.md +183 -0
- package/dist/templates/skills/{create-semantic-model → create-cubes}/references/cube-examples.md +2 -2
- package/dist/templates/skills/create-cubes/references/hubspot-entities.md +289 -0
- package/dist/templates/skills/create-cubes/references/jira-entities.md +201 -0
- package/dist/templates/skills/create-cubes/references/netsuite-entities.md +121 -0
- package/dist/templates/skills/create-cubes/references/stripe-entities.md +114 -0
- package/dist/templates/skills/create-dbt-transformations/SKILL.md +43 -22
- package/dist/templates/skills/create-dbt-transformations/references/edge-cases.md +20 -2
- package/dist/templates/skills/create-dbt-transformations/references/schema-conventions.md +21 -7
- package/dist/templates/skills/create-dbt-transformations/references/sql-templates.md +34 -20
- package/dist/templates/skills/explore-lakehouse/SKILL.md +3 -3
- package/dist/templates/skills/load-sample-data/SKILL.md +1 -1
- package/dist/templates/skills/visualize-semantic-model/SKILL.md +159 -0
- package/dist/templates/skills/visualize-semantic-model/scripts/render_graph.py +186 -0
- package/dist/{types-Y_ht_ja5.d.mts → types-CGjxcj4L.d.mts} +3 -0
- package/package.json +44 -7
- package/dist/adapters/oclif/commands/integrations/create.d.mts +0 -11
- package/dist/adapters/oclif/commands/integrations/create.mjs +0 -16
- package/dist/adapters/oclif/commands/integrations/get.mjs +0 -21
- package/dist/adapters/oclif/commands/integrations/list.d.mts +0 -11
- package/dist/adapters/oclif/commands/integrations/list.mjs +0 -16
- package/dist/adapters/oclif/commands/integrations/update.d.mts +0 -15
- package/dist/adapters/oclif/commands/integrations/update.mjs +0 -21
- package/dist/adapters/oclif/commands/overlays/diff.d.mts +0 -19
- package/dist/adapters/oclif/commands/overlays/diff.mjs +0 -80
- package/dist/adapters/oclif/commands/overlays/pull.d.mts +0 -15
- package/dist/adapters/oclif/commands/overlays/pull.mjs +0 -45
- package/dist/adapters/oclif/commands/overlays/push.d.mts +0 -18
- package/dist/adapters/oclif/commands/overlays/push.mjs +0 -59
- package/dist/adapters/oclif/commands/overlays/status.d.mts +0 -18
- package/dist/adapters/oclif/commands/overlays/status.mjs +0 -53
- package/dist/core-jpFPylBb.mjs +0 -997
- package/dist/index-DD2Vr-pu.d.mts +0 -193
- package/dist/types-C_p_6rkj.d.mts +0 -69
- /package/dist/templates/skills/{create-semantic-model → create-cubes}/references/key-patterns.md +0 -0
- /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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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 };
|