@overaihq/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +43 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/index.js +2428 -0
- package/dist/types-5J3QQFGH.js +15211 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2428 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-MLKGABMK.js";
|
|
3
|
+
|
|
4
|
+
// src/auth.ts
|
|
5
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
6
|
+
import { createServer } from "http";
|
|
7
|
+
import { hostname } from "os";
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
import {
|
|
11
|
+
chmodSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
writeFileSync
|
|
16
|
+
} from "fs";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
var configDir = join(homedir(), ".overai");
|
|
20
|
+
var configFilePath = join(configDir, "config.json");
|
|
21
|
+
var readConfig = () => {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(readFileSync(configFilePath, "utf8"));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var writeConfig = (config) => {
|
|
29
|
+
mkdirSync(configDir, { recursive: true });
|
|
30
|
+
chmodSync(configDir, 448);
|
|
31
|
+
writeFileSync(configFilePath, `${JSON.stringify(config, null, 2)}
|
|
32
|
+
`, {
|
|
33
|
+
mode: 384
|
|
34
|
+
});
|
|
35
|
+
chmodSync(configFilePath, 384);
|
|
36
|
+
};
|
|
37
|
+
var clearConfig = () => {
|
|
38
|
+
rmSync(configFilePath, { force: true });
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/env.ts
|
|
42
|
+
var trimSlash = (url) => url.replace(/\/+$/, "");
|
|
43
|
+
var DEFAULT_API_BASE_URL = "https://api.overai.com";
|
|
44
|
+
var apiBaseUrlOverride = process.env.OVERAI_API_URL != null ? trimSlash(process.env.OVERAI_API_URL) : void 0;
|
|
45
|
+
var appBaseUrl = trimSlash(
|
|
46
|
+
process.env.OVERAI_APP_URL ?? "https://app.overai.com"
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// src/api.ts
|
|
50
|
+
var apiBaseUrl = () => apiBaseUrlOverride ?? readConfig()?.apiBaseUrl ?? DEFAULT_API_BASE_URL;
|
|
51
|
+
var trpcMutation = async (procedure, input) => {
|
|
52
|
+
const res = await fetch(
|
|
53
|
+
`${apiBaseUrl()}/v1/utils/trpc/unauthenticated/${procedure}`,
|
|
54
|
+
{
|
|
55
|
+
method: "POST",
|
|
56
|
+
headers: { "Content-Type": "application/json" },
|
|
57
|
+
body: JSON.stringify(input)
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
const body = await res.json().catch(() => null);
|
|
61
|
+
if (body != null && "error" in body) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
body.error.message || `${procedure} failed (${String(res.status)})`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
if (!res.ok || body == null || !("result" in body)) {
|
|
67
|
+
throw new Error(`${procedure} failed (${String(res.status)})`);
|
|
68
|
+
}
|
|
69
|
+
return body.result.data;
|
|
70
|
+
};
|
|
71
|
+
var exchangeCliAuthCode = (code, codeVerifier) => trpcMutation("exchangeCliAuthCode", { code, codeVerifier });
|
|
72
|
+
var exchangeSessionToken = (refreshToken, destinationOrgId) => trpcMutation("exchangeSessionToken", {
|
|
73
|
+
refreshToken,
|
|
74
|
+
destinationOrgId
|
|
75
|
+
});
|
|
76
|
+
var signOutHeadless = (refreshToken) => trpcMutation("signOutHeadless", { refreshToken });
|
|
77
|
+
var decodeJwtClaims = (token) => {
|
|
78
|
+
const payload = token.split(".")[1];
|
|
79
|
+
if (payload == null) return {};
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
var orgScopeOverride = null;
|
|
87
|
+
var setOrgScope = (organizationId) => {
|
|
88
|
+
orgScopeOverride = organizationId;
|
|
89
|
+
};
|
|
90
|
+
var inFlightExchange = null;
|
|
91
|
+
var processToken = null;
|
|
92
|
+
var getAccessToken = async () => {
|
|
93
|
+
const config = readConfig();
|
|
94
|
+
if (config == null) {
|
|
95
|
+
throw new Error("Not signed in. Run `overai signin`.");
|
|
96
|
+
}
|
|
97
|
+
const targetOrgId = orgScopeOverride ?? config.organizationId;
|
|
98
|
+
const skewMs = 6e4;
|
|
99
|
+
if (targetOrgId === config.organizationId && config.accessToken != null && config.accessTokenExpiresAt != null && config.accessTokenExpiresAt - skewMs > Date.now()) {
|
|
100
|
+
return { token: config.accessToken, organizationId: config.organizationId };
|
|
101
|
+
}
|
|
102
|
+
if (processToken?.organizationId === targetOrgId && processToken.expiresAt - skewMs > Date.now()) {
|
|
103
|
+
return { token: processToken.token, organizationId: targetOrgId };
|
|
104
|
+
}
|
|
105
|
+
inFlightExchange ??= (async () => {
|
|
106
|
+
const result = await exchangeSessionToken(config.refreshToken, targetOrgId);
|
|
107
|
+
if (result.type === "redirect") {
|
|
108
|
+
throw new Error(
|
|
109
|
+
targetOrgId === config.organizationId ? "Your session expired. Run `overai signin`." : `Org ${targetOrgId} requires interactive sign-in (SSO). Run \`overai org switch ${targetOrgId}\`.`
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const forActiveOrg = targetOrgId === config.organizationId;
|
|
113
|
+
writeConfig({
|
|
114
|
+
...config,
|
|
115
|
+
email: decodeJwtClaims(result.token).email ?? config.email,
|
|
116
|
+
refreshToken: result.refreshToken,
|
|
117
|
+
accessToken: forActiveOrg ? result.token : config.accessToken,
|
|
118
|
+
accessTokenExpiresAt: forActiveOrg ? Date.now() + result.accessTokenValiditySeconds * 1e3 : config.accessTokenExpiresAt
|
|
119
|
+
});
|
|
120
|
+
processToken = {
|
|
121
|
+
organizationId: targetOrgId,
|
|
122
|
+
token: result.token,
|
|
123
|
+
expiresAt: Date.now() + result.accessTokenValiditySeconds * 1e3
|
|
124
|
+
};
|
|
125
|
+
return { token: result.token, organizationId: targetOrgId };
|
|
126
|
+
})();
|
|
127
|
+
try {
|
|
128
|
+
return await inFlightExchange;
|
|
129
|
+
} finally {
|
|
130
|
+
inFlightExchange = null;
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var v2Fetch = async (path, init) => {
|
|
134
|
+
const auth = await getAccessToken();
|
|
135
|
+
const headers = new Headers(init?.headers);
|
|
136
|
+
headers.set("Authorization", `Bearer ${auth.token}`);
|
|
137
|
+
return fetch(`${apiBaseUrl()}${path}`, { ...init, headers });
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/browser.ts
|
|
141
|
+
import { spawn } from "child_process";
|
|
142
|
+
import { platform } from "os";
|
|
143
|
+
var openBrowser = (url) => {
|
|
144
|
+
const [command, args] = platform() === "darwin" ? ["open", [url]] : platform() === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
145
|
+
try {
|
|
146
|
+
const child = spawn(command, args, { stdio: "ignore", detached: true });
|
|
147
|
+
child.on("error", () => {
|
|
148
|
+
});
|
|
149
|
+
child.unref();
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// src/pkce.ts
|
|
155
|
+
import { createHash, randomBytes } from "crypto";
|
|
156
|
+
var createPkcePair = () => {
|
|
157
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
158
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
159
|
+
return { verifier, challenge };
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// src/auth.ts
|
|
163
|
+
var CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
164
|
+
var startCallbackServer = (expectedState) => {
|
|
165
|
+
let resolveCode;
|
|
166
|
+
let rejectCode;
|
|
167
|
+
const codePromise = new Promise((resolve3, reject) => {
|
|
168
|
+
resolveCode = resolve3;
|
|
169
|
+
rejectCode = reject;
|
|
170
|
+
});
|
|
171
|
+
const server = createServer((req, res) => {
|
|
172
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
173
|
+
if (url.pathname !== "/") {
|
|
174
|
+
res.writeHead(204).end();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const respond = (message) => {
|
|
178
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
179
|
+
res.end(
|
|
180
|
+
`<html><body style="font-family:sans-serif;text-align:center;padding:3rem"><p>${message}</p><p>You can close this tab.</p></body></html>`
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
const error = url.searchParams.get("error");
|
|
184
|
+
const state = url.searchParams.get("state");
|
|
185
|
+
const code = url.searchParams.get("code");
|
|
186
|
+
if (error != null) {
|
|
187
|
+
respond("Authorization was denied.");
|
|
188
|
+
rejectCode?.(new Error(`Authorization denied: ${error}`));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (state !== expectedState) {
|
|
192
|
+
respond("Invalid authorization state.");
|
|
193
|
+
rejectCode?.(new Error("State mismatch \u2014 aborting."));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (code == null) {
|
|
197
|
+
respond("Missing authorization code.");
|
|
198
|
+
rejectCode?.(new Error("No authorization code returned."));
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
respond("Authorized! Return to your terminal.");
|
|
202
|
+
resolveCode?.(code);
|
|
203
|
+
});
|
|
204
|
+
let timeout;
|
|
205
|
+
const close = () => {
|
|
206
|
+
if (timeout != null) clearTimeout(timeout);
|
|
207
|
+
server.closeAllConnections();
|
|
208
|
+
server.close();
|
|
209
|
+
};
|
|
210
|
+
return new Promise((resolveServer, rejectServer) => {
|
|
211
|
+
server.on("error", rejectServer);
|
|
212
|
+
server.listen(0, "127.0.0.1", () => {
|
|
213
|
+
const address = server.address();
|
|
214
|
+
if (address == null || typeof address === "string") {
|
|
215
|
+
rejectServer(new Error("Failed to bind loopback callback server."));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
timeout = setTimeout(() => {
|
|
219
|
+
rejectCode?.(new Error("Timed out waiting for browser authorization."));
|
|
220
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
221
|
+
timeout.unref();
|
|
222
|
+
resolveServer({ port: address.port, codePromise, close });
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
var signIn = async () => {
|
|
227
|
+
const { verifier, challenge } = createPkcePair();
|
|
228
|
+
const state = randomBytes2(16).toString("base64url");
|
|
229
|
+
const { port, codePromise, close } = await startCallbackServer(state);
|
|
230
|
+
const authorizeUrl = `${appBaseUrl}/cli-auth?${new URLSearchParams({
|
|
231
|
+
redirect_uri: `http://127.0.0.1:${String(port)}`,
|
|
232
|
+
code_challenge: challenge,
|
|
233
|
+
state,
|
|
234
|
+
label: hostname()
|
|
235
|
+
}).toString()}`;
|
|
236
|
+
const autoOpen = process.env.OVERAI_NO_BROWSER == null;
|
|
237
|
+
process.stdout.write(
|
|
238
|
+
`${autoOpen ? "Opening your browser to authorize the CLI.\nIf it doesn't open, visit" : "Visit this URL to authorize the CLI"}:
|
|
239
|
+
${authorizeUrl}
|
|
240
|
+
`
|
|
241
|
+
);
|
|
242
|
+
if (autoOpen) openBrowser(authorizeUrl);
|
|
243
|
+
try {
|
|
244
|
+
const code = await codePromise;
|
|
245
|
+
const session = await exchangeCliAuthCode(code, verifier);
|
|
246
|
+
const email = decodeJwtClaims(session.token).email;
|
|
247
|
+
writeConfig({
|
|
248
|
+
refreshToken: session.refreshToken,
|
|
249
|
+
organizationId: session.organizationId,
|
|
250
|
+
apiBaseUrl: apiBaseUrlOverride ?? DEFAULT_API_BASE_URL,
|
|
251
|
+
email,
|
|
252
|
+
accessToken: session.token,
|
|
253
|
+
accessTokenExpiresAt: Date.now() + session.accessTokenValiditySeconds * 1e3
|
|
254
|
+
});
|
|
255
|
+
process.stdout.write(
|
|
256
|
+
`Signed in${email != null ? ` as ${email}` : ""} (org ${session.organizationId}).
|
|
257
|
+
`
|
|
258
|
+
);
|
|
259
|
+
} finally {
|
|
260
|
+
close();
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
var signOut = async () => {
|
|
264
|
+
const config = readConfig();
|
|
265
|
+
if (config == null) {
|
|
266
|
+
process.stdout.write("Not signed in.\n");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
await signOutHeadless(config.refreshToken).catch(() => void 0);
|
|
270
|
+
clearConfig();
|
|
271
|
+
process.stdout.write("Signed out.\n");
|
|
272
|
+
};
|
|
273
|
+
var whoami = () => {
|
|
274
|
+
const config = readConfig();
|
|
275
|
+
if (config == null) {
|
|
276
|
+
process.stderr.write("Not signed in. Run `overai signin`.\n");
|
|
277
|
+
process.exitCode = 1;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
process.stdout.write(
|
|
281
|
+
`Signed in${config.email != null ? ` as ${config.email}` : ""} (org ${config.organizationId}).
|
|
282
|
+
`
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// src/args.ts
|
|
287
|
+
var flagValue = (args, ...names) => {
|
|
288
|
+
for (const name of names) {
|
|
289
|
+
const i = args.indexOf(name);
|
|
290
|
+
if (i !== -1 && i + 1 < args.length) return args[i + 1];
|
|
291
|
+
}
|
|
292
|
+
return void 0;
|
|
293
|
+
};
|
|
294
|
+
var readStdin = async () => {
|
|
295
|
+
const chunks = [];
|
|
296
|
+
for await (const chunk of process.stdin) {
|
|
297
|
+
chunks.push(chunk);
|
|
298
|
+
}
|
|
299
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/gql.ts
|
|
303
|
+
var gqlRequest = async (query, variables) => {
|
|
304
|
+
const res = await v2Fetch("/v2/graphql", {
|
|
305
|
+
method: "POST",
|
|
306
|
+
headers: { "Content-Type": "application/json" },
|
|
307
|
+
body: JSON.stringify({ query, variables })
|
|
308
|
+
});
|
|
309
|
+
const body = await res.json().catch(() => null);
|
|
310
|
+
if (body?.errors != null && body.errors.length > 0) {
|
|
311
|
+
throw new Error(body.errors.map((e) => e.message).join("; "));
|
|
312
|
+
}
|
|
313
|
+
if (!res.ok || body?.data == null) {
|
|
314
|
+
throw new Error(`graphql failed (${String(res.status)})`);
|
|
315
|
+
}
|
|
316
|
+
return body.data;
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// src/output.ts
|
|
320
|
+
var resolveFormat = (explicit) => {
|
|
321
|
+
if (explicit === "json" || explicit === "csv" || explicit === "table") {
|
|
322
|
+
return explicit;
|
|
323
|
+
}
|
|
324
|
+
if (explicit != null) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`Unknown output format "${explicit}". Use json, table, or csv.`
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
return process.stdout.isTTY ? "table" : "json";
|
|
330
|
+
};
|
|
331
|
+
var cell = (value) => {
|
|
332
|
+
if (value == null) return "";
|
|
333
|
+
if (typeof value === "string") return value;
|
|
334
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
335
|
+
return String(value);
|
|
336
|
+
}
|
|
337
|
+
return JSON.stringify(value);
|
|
338
|
+
};
|
|
339
|
+
var formatResult = (data, format) => {
|
|
340
|
+
if (format === "json" || !Array.isArray(data)) {
|
|
341
|
+
return JSON.stringify(data, null, 2);
|
|
342
|
+
}
|
|
343
|
+
const records = data.map(
|
|
344
|
+
(row) => row != null && typeof row === "object" ? row : { value: row }
|
|
345
|
+
);
|
|
346
|
+
if (records.length === 0) return "(no rows)";
|
|
347
|
+
const columns = [...new Set(records.flatMap((r) => Object.keys(r)))];
|
|
348
|
+
if (format === "csv") {
|
|
349
|
+
const escape = (value) => {
|
|
350
|
+
const text = cell(value);
|
|
351
|
+
return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
352
|
+
};
|
|
353
|
+
return [
|
|
354
|
+
columns.join(","),
|
|
355
|
+
...records.map((r) => columns.map((c) => escape(r[c])).join(","))
|
|
356
|
+
].join("\n");
|
|
357
|
+
}
|
|
358
|
+
const widths = columns.map(
|
|
359
|
+
(c) => Math.max(c.length, ...records.map((r) => cell(r[c]).length))
|
|
360
|
+
);
|
|
361
|
+
const line = (cells) => cells.map((text, i) => text.padEnd(widths[i] ?? 0)).join(" ");
|
|
362
|
+
return [
|
|
363
|
+
line(columns),
|
|
364
|
+
line(widths.map((w) => "-".repeat(w))),
|
|
365
|
+
...records.map((r) => line(columns.map((c) => cell(r[c]))))
|
|
366
|
+
].join("\n");
|
|
367
|
+
};
|
|
368
|
+
var printResult = (data, format) => {
|
|
369
|
+
process.stdout.write(`${formatResult(data, format)}
|
|
370
|
+
`);
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// src/commands/automation.ts
|
|
374
|
+
var runRuns = async (automationId, args) => {
|
|
375
|
+
const limit = Number(flagValue(args, "--limit") ?? 20);
|
|
376
|
+
const data = await gqlRequest(
|
|
377
|
+
`query CliAutomationRuns($automationId: String!, $first: Int!) {
|
|
378
|
+
automationRuns(condition: { automationId: $automationId }, orderBy: [STARTED_AT_DESC], first: $first) {
|
|
379
|
+
nodes { id startedAt endedAt errored isCancelled manuallyTriggered version { num } }
|
|
380
|
+
}
|
|
381
|
+
}`,
|
|
382
|
+
// Over-fetch when status-filtering so --failed still fills the limit.
|
|
383
|
+
{
|
|
384
|
+
automationId,
|
|
385
|
+
first: args.includes("--failed") || args.includes("--running") ? limit * 5 : limit
|
|
386
|
+
}
|
|
387
|
+
);
|
|
388
|
+
let runs = data.automationRuns.nodes;
|
|
389
|
+
if (args.includes("--failed")) runs = runs.filter((r) => r.errored);
|
|
390
|
+
if (args.includes("--running")) {
|
|
391
|
+
runs = runs.filter((r) => r.endedAt == null && !r.isCancelled);
|
|
392
|
+
}
|
|
393
|
+
printResult(
|
|
394
|
+
runs.slice(0, limit).map((r) => ({
|
|
395
|
+
id: r.id,
|
|
396
|
+
version: r.version?.num,
|
|
397
|
+
startedAt: r.startedAt,
|
|
398
|
+
endedAt: r.endedAt,
|
|
399
|
+
status: r.errored ? "errored" : r.isCancelled ? "cancelled" : r.endedAt == null ? "running" : "ok",
|
|
400
|
+
manual: r.manuallyTriggered
|
|
401
|
+
})),
|
|
402
|
+
resolveFormat(flagValue(args, "-o", "--output"))
|
|
403
|
+
);
|
|
404
|
+
};
|
|
405
|
+
var runState = async (runId, args) => {
|
|
406
|
+
const data = await gqlRequest(
|
|
407
|
+
`query CliAutomationRunState($id: String!) {
|
|
408
|
+
automationRun(id: $id) { state }
|
|
409
|
+
}`,
|
|
410
|
+
{ id: runId }
|
|
411
|
+
);
|
|
412
|
+
if (data.automationRun == null) throw new Error(`Run ${runId} not found.`);
|
|
413
|
+
const node = flagValue(args, "--node");
|
|
414
|
+
const state = data.automationRun.state ?? {};
|
|
415
|
+
printResult(
|
|
416
|
+
node != null ? { [node]: state[node] ?? "(no state for this node)" } : state,
|
|
417
|
+
resolveFormat(flagValue(args, "-o", "--output"))
|
|
418
|
+
);
|
|
419
|
+
};
|
|
420
|
+
var runTrigger = async (automationId, args) => {
|
|
421
|
+
const { organizationId } = await getAccessToken();
|
|
422
|
+
const body = {};
|
|
423
|
+
if (args.includes("-")) {
|
|
424
|
+
Object.assign(
|
|
425
|
+
body,
|
|
426
|
+
JSON.parse(await readStdin())
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
for (let i = 0; i < args.length; i++) {
|
|
430
|
+
if (args[i] === "--input") {
|
|
431
|
+
const pair = args[i + 1] ?? "";
|
|
432
|
+
const eq = pair.indexOf("=");
|
|
433
|
+
if (eq > 0) body[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
const res = await v2Fetch(
|
|
437
|
+
`/v2/automations/${organizationId}/${automationId}/run`,
|
|
438
|
+
{
|
|
439
|
+
method: "POST",
|
|
440
|
+
headers: { "Content-Type": "application/json" },
|
|
441
|
+
body: JSON.stringify(body)
|
|
442
|
+
}
|
|
443
|
+
);
|
|
444
|
+
const responseBody = await res.json().catch(() => null);
|
|
445
|
+
if (!res.ok) {
|
|
446
|
+
throw new Error(
|
|
447
|
+
responseBody?.error ?? `run failed (${String(res.status)})`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
printResult(responseBody, resolveFormat(flagValue(args, "-o", "--output")));
|
|
451
|
+
};
|
|
452
|
+
var runAutomation = async (args) => {
|
|
453
|
+
const [sub, id, ...rest] = args;
|
|
454
|
+
if (sub === "runs" && id != null) return runRuns(id, rest);
|
|
455
|
+
if (sub === "state" && id != null) return runState(id, rest);
|
|
456
|
+
if (sub === "run" && id != null) return runTrigger(id, rest);
|
|
457
|
+
process.stderr.write(
|
|
458
|
+
"Usage: overai automation runs <automationId> [--limit N] [--failed] [--running]\n overai automation state <runId> [--node <variableId>]\n overai automation run <automationId> [--input k=v ... | -]\n"
|
|
459
|
+
);
|
|
460
|
+
process.exitCode = 1;
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
// src/commands/dev.ts
|
|
464
|
+
import { createHash as createHash2 } from "crypto";
|
|
465
|
+
import { watch } from "fs";
|
|
466
|
+
import { createServer as createServer2 } from "http";
|
|
467
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
468
|
+
|
|
469
|
+
// src/workspace/manifest.ts
|
|
470
|
+
import { existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
471
|
+
import { dirname, join as join2, resolve } from "path";
|
|
472
|
+
var MANIFEST_DIR = ".overai";
|
|
473
|
+
var MANIFEST_FILE = "manifest.json";
|
|
474
|
+
var BASE_DIR = "base";
|
|
475
|
+
var findWorkspaceRoot = (from = process.cwd()) => {
|
|
476
|
+
let dir = resolve(from);
|
|
477
|
+
for (; ; ) {
|
|
478
|
+
if (existsSync(join2(dir, MANIFEST_DIR, MANIFEST_FILE))) return dir;
|
|
479
|
+
const parent = dirname(dir);
|
|
480
|
+
if (parent === dir) {
|
|
481
|
+
throw new Error(
|
|
482
|
+
"Not inside a cloned workspace (no .overai/manifest.json found). Run `overai clone <workspaceId>` first."
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
dir = parent;
|
|
486
|
+
}
|
|
487
|
+
};
|
|
488
|
+
var readManifest = (root) => JSON.parse(
|
|
489
|
+
readFileSync2(join2(root, MANIFEST_DIR, MANIFEST_FILE), "utf8")
|
|
490
|
+
);
|
|
491
|
+
var openClone = () => {
|
|
492
|
+
const root = findWorkspaceRoot();
|
|
493
|
+
const manifest = readManifest(root);
|
|
494
|
+
setOrgScope(manifest.organizationId);
|
|
495
|
+
return { root, manifest };
|
|
496
|
+
};
|
|
497
|
+
var writeManifest = (root, manifest) => {
|
|
498
|
+
mkdirSync2(join2(root, MANIFEST_DIR), { recursive: true });
|
|
499
|
+
writeFileSync2(
|
|
500
|
+
join2(root, MANIFEST_DIR, MANIFEST_FILE),
|
|
501
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
502
|
+
`
|
|
503
|
+
);
|
|
504
|
+
};
|
|
505
|
+
var basePath = (root, relPath) => join2(root, MANIFEST_DIR, BASE_DIR, relPath);
|
|
506
|
+
var writeSynced = (root, relPath, content) => {
|
|
507
|
+
for (const abs of [join2(root, relPath), basePath(root, relPath)]) {
|
|
508
|
+
mkdirSync2(dirname(abs), { recursive: true });
|
|
509
|
+
writeFileSync2(abs, content);
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
var readWorkingFile = (root, relPath) => {
|
|
513
|
+
try {
|
|
514
|
+
return readFileSync2(join2(root, relPath), "utf8");
|
|
515
|
+
} catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
var readBaseFile = (root, relPath) => {
|
|
520
|
+
try {
|
|
521
|
+
return readFileSync2(basePath(root, relPath), "utf8");
|
|
522
|
+
} catch {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
var slugify = (name) => name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "untitled";
|
|
527
|
+
var localState = (root, resource) => {
|
|
528
|
+
const changedFiles = [];
|
|
529
|
+
let missing = false;
|
|
530
|
+
for (const relPath of resource.files) {
|
|
531
|
+
const working = readWorkingFile(root, relPath);
|
|
532
|
+
if (working == null) {
|
|
533
|
+
missing = true;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (working !== readBaseFile(root, relPath)) changedFiles.push(relPath);
|
|
537
|
+
}
|
|
538
|
+
return {
|
|
539
|
+
state: missing ? "missing" : changedFiles.length > 0 ? "modified" : "unchanged",
|
|
540
|
+
changedFiles
|
|
541
|
+
};
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
// src/workspace/validateElements.ts
|
|
545
|
+
var validateAutomationElements = async (elements) => {
|
|
546
|
+
const { automationNodesSchema } = await import("./types-5J3QQFGH.js");
|
|
547
|
+
const parsed = automationNodesSchema.safeParse(elements);
|
|
548
|
+
return parsed.success ? [] : parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`);
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
// src/workspace/adapters.ts
|
|
552
|
+
var metaJson = (value) => `${JSON.stringify(value, null, 2)}
|
|
553
|
+
`;
|
|
554
|
+
var fileBySuffix = (files, suffix) => {
|
|
555
|
+
const entry = Object.entries(files).find(([path]) => path.endsWith(suffix));
|
|
556
|
+
if (entry == null) throw new Error(`Missing local file (*${suffix}).`);
|
|
557
|
+
return entry[1];
|
|
558
|
+
};
|
|
559
|
+
var functionFiles = (id, name, version) => {
|
|
560
|
+
const stem = `functions/${slugify(name)}.${id}`;
|
|
561
|
+
return {
|
|
562
|
+
[`${stem}.ts`]: version.code,
|
|
563
|
+
[`${stem}.meta.json`]: metaJson({
|
|
564
|
+
inputSchema: version.inputSchema,
|
|
565
|
+
executionTimeout: version.executionTimeout,
|
|
566
|
+
executionMemory: version.executionMemory
|
|
567
|
+
})
|
|
568
|
+
};
|
|
569
|
+
};
|
|
570
|
+
var functionAdapter = {
|
|
571
|
+
type: "function",
|
|
572
|
+
fetchAll: async (workspaceId) => {
|
|
573
|
+
const data = await gqlRequest(
|
|
574
|
+
`query CliFunctions($workspaceId: String!) {
|
|
575
|
+
functions(condition: { workspaceId: $workspaceId }, includeArchived: NO) {
|
|
576
|
+
nodes {
|
|
577
|
+
id name
|
|
578
|
+
functionVersions(orderBy: [CREATED_AT_DESC], first: 1) {
|
|
579
|
+
nodes { versionNumber code inputSchema executionTimeout executionMemory }
|
|
580
|
+
}
|
|
581
|
+
functionDeployedVersion { versionNumber }
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}`,
|
|
585
|
+
{ workspaceId }
|
|
586
|
+
);
|
|
587
|
+
return data.functions.nodes.flatMap((fn) => {
|
|
588
|
+
const version = fn.functionVersions.nodes[0];
|
|
589
|
+
if (version == null) return [];
|
|
590
|
+
return [
|
|
591
|
+
{
|
|
592
|
+
id: fn.id,
|
|
593
|
+
name: fn.name,
|
|
594
|
+
latestVersion: version.versionNumber,
|
|
595
|
+
deployedVersion: fn.functionDeployedVersion?.versionNumber ?? null,
|
|
596
|
+
files: functionFiles(fn.id, fn.name, version)
|
|
597
|
+
}
|
|
598
|
+
];
|
|
599
|
+
});
|
|
600
|
+
},
|
|
601
|
+
head: async (id) => {
|
|
602
|
+
const data = await gqlRequest(
|
|
603
|
+
`query CliFunctionHead($id: String!) {
|
|
604
|
+
function(id: $id) {
|
|
605
|
+
functionVersions(orderBy: [CREATED_AT_DESC], first: 1) { nodes { versionNumber } }
|
|
606
|
+
functionDeployedVersion { versionNumber }
|
|
607
|
+
}
|
|
608
|
+
}`,
|
|
609
|
+
{ id }
|
|
610
|
+
);
|
|
611
|
+
if (data.function == null) throw new Error(`Function ${id} not found.`);
|
|
612
|
+
return {
|
|
613
|
+
latestVersion: data.function.functionVersions.nodes[0]?.versionNumber ?? 0,
|
|
614
|
+
deployedVersion: data.function.functionDeployedVersion?.versionNumber ?? null
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
fetchVersion: async (id, versionNumber, name) => {
|
|
618
|
+
const data = await gqlRequest(
|
|
619
|
+
`query CliFunctionVersion($functionId: String!, $versionNumber: Int!) {
|
|
620
|
+
functionVersion(functionId: $functionId, versionNumber: $versionNumber) {
|
|
621
|
+
versionNumber code inputSchema executionTimeout executionMemory
|
|
622
|
+
}
|
|
623
|
+
}`,
|
|
624
|
+
{ functionId: id, versionNumber }
|
|
625
|
+
);
|
|
626
|
+
if (data.functionVersion == null) {
|
|
627
|
+
throw new Error(
|
|
628
|
+
`Function ${id} has no version ${String(versionNumber)}.`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
return functionFiles(id, name, data.functionVersion);
|
|
632
|
+
},
|
|
633
|
+
push: async (id, files, message) => {
|
|
634
|
+
const meta = JSON.parse(fileBySuffix(files, ".meta.json"));
|
|
635
|
+
const data = await gqlRequest(
|
|
636
|
+
`mutation CliCreateFunctionVersion($functionId: String!, $code: String!, $message: String, $inputSchema: JSON, $executionTimeout: Int, $executionMemory: Int) {
|
|
637
|
+
createFunctionVersion(input: { functionVersion: {
|
|
638
|
+
functionId: $functionId, code: $code, message: $message,
|
|
639
|
+
inputSchema: $inputSchema, executionTimeout: $executionTimeout, executionMemory: $executionMemory
|
|
640
|
+
}}) { functionVersion { versionNumber } }
|
|
641
|
+
}`,
|
|
642
|
+
{
|
|
643
|
+
functionId: id,
|
|
644
|
+
code: fileBySuffix(files, ".ts"),
|
|
645
|
+
message,
|
|
646
|
+
inputSchema: meta.inputSchema,
|
|
647
|
+
// Match the webapp's save defaults so a missing meta doesn't zero out limits.
|
|
648
|
+
executionTimeout: meta.executionTimeout ?? 1e4,
|
|
649
|
+
executionMemory: meta.executionMemory ?? 512
|
|
650
|
+
}
|
|
651
|
+
);
|
|
652
|
+
return data.createFunctionVersion.functionVersion.versionNumber;
|
|
653
|
+
},
|
|
654
|
+
deploy: async (id, versionNumber) => {
|
|
655
|
+
await gqlRequest(
|
|
656
|
+
`mutation CliDeployFunction($functionId: String!, $versionNumber: Int!) {
|
|
657
|
+
updateFunctionDeployedVersion(input: { functionId: $functionId, patch: { versionNumber: $versionNumber } }) {
|
|
658
|
+
functionDeployedVersion { functionId }
|
|
659
|
+
}
|
|
660
|
+
}`,
|
|
661
|
+
{ functionId: id, versionNumber }
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
var pageFiles = (dir, id, name, config) => {
|
|
666
|
+
const { code, ...meta } = config;
|
|
667
|
+
const ext = config.language === "liquid" ? "liquid" : "tsx";
|
|
668
|
+
const stem = `${dir}/${slugify(name)}.${id}`;
|
|
669
|
+
return {
|
|
670
|
+
[`${stem}.${ext}`]: code ?? "",
|
|
671
|
+
[`${stem}.meta.json`]: metaJson(meta)
|
|
672
|
+
};
|
|
673
|
+
};
|
|
674
|
+
var pageConfigFromFiles = (files) => {
|
|
675
|
+
const meta = JSON.parse(fileBySuffix(files, ".meta.json"));
|
|
676
|
+
const codeEntry = Object.entries(files).find(
|
|
677
|
+
([path]) => !path.endsWith(".meta.json")
|
|
678
|
+
);
|
|
679
|
+
if (codeEntry == null) throw new Error("Missing local code file.");
|
|
680
|
+
return { ...meta, code: codeEntry[1] };
|
|
681
|
+
};
|
|
682
|
+
var pageAdapter = (opts) => {
|
|
683
|
+
const { gqlName } = opts;
|
|
684
|
+
const plural = `${gqlName}s`;
|
|
685
|
+
const versionField = `${gqlName}Versions`;
|
|
686
|
+
const deployedField = `${gqlName}DeployedVersion`;
|
|
687
|
+
const idArg = `${gqlName}Id`;
|
|
688
|
+
const capitalized = gqlName.charAt(0).toUpperCase() + gqlName.slice(1);
|
|
689
|
+
return {
|
|
690
|
+
type: opts.type,
|
|
691
|
+
fetchAll: async (workspaceId) => {
|
|
692
|
+
const data = await gqlRequest(
|
|
693
|
+
`query CliPages($workspaceId: String!) {
|
|
694
|
+
${plural}(condition: { workspaceId: $workspaceId }, includeArchived: NO) {
|
|
695
|
+
nodes {
|
|
696
|
+
id name
|
|
697
|
+
${versionField}(orderBy: PRIMARY_KEY_DESC, first: 1) { nodes { versionNumber config } }
|
|
698
|
+
${deployedField} { versionNumber }
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}`,
|
|
702
|
+
{ workspaceId }
|
|
703
|
+
);
|
|
704
|
+
const nodes = data[plural]?.nodes ?? [];
|
|
705
|
+
return nodes.flatMap((node) => {
|
|
706
|
+
const versions = node[versionField];
|
|
707
|
+
const version = versions.nodes[0];
|
|
708
|
+
if (version == null) return [];
|
|
709
|
+
return [
|
|
710
|
+
{
|
|
711
|
+
id: node.id,
|
|
712
|
+
name: node.name,
|
|
713
|
+
latestVersion: version.versionNumber,
|
|
714
|
+
deployedVersion: node[deployedField]?.versionNumber ?? null,
|
|
715
|
+
files: pageFiles(
|
|
716
|
+
opts.dir,
|
|
717
|
+
node.id,
|
|
718
|
+
node.name,
|
|
719
|
+
version.config
|
|
720
|
+
)
|
|
721
|
+
}
|
|
722
|
+
];
|
|
723
|
+
});
|
|
724
|
+
},
|
|
725
|
+
head: async (id) => {
|
|
726
|
+
const data = await gqlRequest(
|
|
727
|
+
`query CliPageHead($id: String!) {
|
|
728
|
+
${gqlName}(id: $id) {
|
|
729
|
+
${versionField}(orderBy: PRIMARY_KEY_DESC, first: 1) { nodes { versionNumber } }
|
|
730
|
+
${deployedField} { versionNumber }
|
|
731
|
+
}
|
|
732
|
+
}`,
|
|
733
|
+
{ id }
|
|
734
|
+
);
|
|
735
|
+
const node = data[gqlName];
|
|
736
|
+
if (node == null) throw new Error(`${capitalized} ${id} not found.`);
|
|
737
|
+
const versions = node[versionField];
|
|
738
|
+
return {
|
|
739
|
+
latestVersion: versions.nodes[0]?.versionNumber ?? 0,
|
|
740
|
+
deployedVersion: node[deployedField]?.versionNumber ?? null
|
|
741
|
+
};
|
|
742
|
+
},
|
|
743
|
+
fetchVersion: async (id, versionNumber, name) => {
|
|
744
|
+
const data = await gqlRequest(
|
|
745
|
+
`query CliPageVersion($id: String!, $versionNumber: Int!) {
|
|
746
|
+
${gqlName}Version(${idArg}: $id, versionNumber: $versionNumber) { config }
|
|
747
|
+
}`,
|
|
748
|
+
{ id, versionNumber }
|
|
749
|
+
);
|
|
750
|
+
const version = data[`${gqlName}Version`];
|
|
751
|
+
if (version == null) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
`${capitalized} ${id} has no version ${String(versionNumber)}.`
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
return pageFiles(opts.dir, id, name, version.config);
|
|
757
|
+
},
|
|
758
|
+
push: async (id, files, message) => {
|
|
759
|
+
const config = pageConfigFromFiles(files);
|
|
760
|
+
const data = await gqlRequest(
|
|
761
|
+
`mutation CliCreatePageVersion($id: String!, $message: String, $config: JSON!) {
|
|
762
|
+
create${capitalized}Version(input: { ${gqlName}Version: { ${idArg}: $id, message: $message, config: $config } }) {
|
|
763
|
+
${gqlName}Version { versionNumber }
|
|
764
|
+
}
|
|
765
|
+
}`,
|
|
766
|
+
{ id, message, config }
|
|
767
|
+
);
|
|
768
|
+
const versionNumber = data[`create${capitalized}Version`]?.[`${gqlName}Version`]?.versionNumber;
|
|
769
|
+
if (versionNumber == null) {
|
|
770
|
+
throw new Error(`create${capitalized}Version returned no version.`);
|
|
771
|
+
}
|
|
772
|
+
return versionNumber;
|
|
773
|
+
},
|
|
774
|
+
deploy: async (id, versionNumber) => {
|
|
775
|
+
await gqlRequest(
|
|
776
|
+
`mutation CliDeployPageVersion($id: String!, $versionNumber: Int!) {
|
|
777
|
+
update${capitalized}DeployedVersion(input: { ${idArg}: $id, patch: { versionNumber: $versionNumber } }) {
|
|
778
|
+
${gqlName}DeployedVersion { versionNumber }
|
|
779
|
+
}
|
|
780
|
+
}`,
|
|
781
|
+
{ id, versionNumber }
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
};
|
|
786
|
+
var automationAdapter = {
|
|
787
|
+
type: "automation",
|
|
788
|
+
fetchAll: async (workspaceId) => {
|
|
789
|
+
const data = await gqlRequest(
|
|
790
|
+
`query CliAutomations($workspaceId: String!) {
|
|
791
|
+
automations(condition: { workspaceId: $workspaceId }, includeArchived: NO) {
|
|
792
|
+
nodes {
|
|
793
|
+
id name
|
|
794
|
+
automationVersions(orderBy: [NUM_DESC], first: 1) {
|
|
795
|
+
nodes { num elements maxConcurrentRuns }
|
|
796
|
+
}
|
|
797
|
+
automationDeployedVersion { automationVersion { num } }
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}`,
|
|
801
|
+
{ workspaceId }
|
|
802
|
+
);
|
|
803
|
+
return data.automations.nodes.flatMap((automation) => {
|
|
804
|
+
const version = automation.automationVersions.nodes[0];
|
|
805
|
+
if (version == null) return [];
|
|
806
|
+
const path = `automations/${slugify(automation.name)}.${automation.id}.json`;
|
|
807
|
+
return [
|
|
808
|
+
{
|
|
809
|
+
id: automation.id,
|
|
810
|
+
name: automation.name,
|
|
811
|
+
latestVersion: version.num,
|
|
812
|
+
deployedVersion: automation.automationDeployedVersion?.automationVersion?.num ?? null,
|
|
813
|
+
files: {
|
|
814
|
+
[path]: metaJson({
|
|
815
|
+
maxConcurrentRuns: version.maxConcurrentRuns,
|
|
816
|
+
elements: version.elements
|
|
817
|
+
})
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
];
|
|
821
|
+
});
|
|
822
|
+
},
|
|
823
|
+
head: async (id) => {
|
|
824
|
+
const data = await gqlRequest(
|
|
825
|
+
`query CliAutomationHead($id: String!) {
|
|
826
|
+
automation(id: $id) {
|
|
827
|
+
automationVersions(orderBy: [NUM_DESC], first: 1) { nodes { num } }
|
|
828
|
+
automationDeployedVersion { automationVersion { num } }
|
|
829
|
+
}
|
|
830
|
+
}`,
|
|
831
|
+
{ id }
|
|
832
|
+
);
|
|
833
|
+
if (data.automation == null) throw new Error(`Automation ${id} not found.`);
|
|
834
|
+
return {
|
|
835
|
+
latestVersion: data.automation.automationVersions.nodes[0]?.num ?? 0,
|
|
836
|
+
deployedVersion: data.automation.automationDeployedVersion?.automationVersion?.num ?? null
|
|
837
|
+
};
|
|
838
|
+
},
|
|
839
|
+
fetchVersion: async (id, versionNumber, name) => {
|
|
840
|
+
const data = await gqlRequest(
|
|
841
|
+
`query CliAutomationVersion($automationId: String!, $num: Int!) {
|
|
842
|
+
automationVersionByAutomationIdAndNum(automationId: $automationId, num: $num) {
|
|
843
|
+
num elements maxConcurrentRuns
|
|
844
|
+
}
|
|
845
|
+
}`,
|
|
846
|
+
{ automationId: id, num: versionNumber }
|
|
847
|
+
);
|
|
848
|
+
const version = data.automationVersionByAutomationIdAndNum;
|
|
849
|
+
if (version == null) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`Automation ${id} has no version ${String(versionNumber)}.`
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
return {
|
|
855
|
+
[`automations/${slugify(name)}.${id}.json`]: metaJson({
|
|
856
|
+
maxConcurrentRuns: version.maxConcurrentRuns,
|
|
857
|
+
elements: version.elements
|
|
858
|
+
})
|
|
859
|
+
};
|
|
860
|
+
},
|
|
861
|
+
push: async (id, files, message) => {
|
|
862
|
+
const doc = JSON.parse(fileBySuffix(files, ".json"));
|
|
863
|
+
const findings = await validateAutomationElements(doc.elements);
|
|
864
|
+
if (findings.length > 0) {
|
|
865
|
+
throw new Error(
|
|
866
|
+
`Invalid automation elements:
|
|
867
|
+
${findings.map((f) => ` ${f}`).join("\n")}`
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
const data = await gqlRequest(
|
|
871
|
+
`mutation CliCreateAutomationVersion($automationId: String!, $elements: JSON!, $message: String, $maxConcurrentRuns: Int) {
|
|
872
|
+
createAutomationVersion(input: { automationVersion: {
|
|
873
|
+
automationId: $automationId, elements: $elements, message: $message, maxConcurrentRuns: $maxConcurrentRuns
|
|
874
|
+
}}) { automationVersion { num } }
|
|
875
|
+
}`,
|
|
876
|
+
{
|
|
877
|
+
automationId: id,
|
|
878
|
+
elements: doc.elements,
|
|
879
|
+
message,
|
|
880
|
+
maxConcurrentRuns: doc.maxConcurrentRuns
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
return data.createAutomationVersion.automationVersion.num;
|
|
884
|
+
},
|
|
885
|
+
deploy: async (id, versionNumber) => {
|
|
886
|
+
const data = await gqlRequest(
|
|
887
|
+
`query CliAutomationVersionId($automationId: String!, $num: Int!) {
|
|
888
|
+
automationVersionByAutomationIdAndNum(automationId: $automationId, num: $num) { id }
|
|
889
|
+
}`,
|
|
890
|
+
{ automationId: id, num: versionNumber }
|
|
891
|
+
);
|
|
892
|
+
const versionId = data.automationVersionByAutomationIdAndNum?.id;
|
|
893
|
+
if (versionId == null) {
|
|
894
|
+
throw new Error(
|
|
895
|
+
`Automation ${id} has no version ${String(versionNumber)}.`
|
|
896
|
+
);
|
|
897
|
+
}
|
|
898
|
+
await gqlRequest(
|
|
899
|
+
`mutation CliDeployAutomation($automationId: String!, $automationVersionId: String!) {
|
|
900
|
+
updateAutomationDeployedVersion(input: { automationId: $automationId, patch: { automationVersionId: $automationVersionId } }) {
|
|
901
|
+
automationDeployedVersion { automationId }
|
|
902
|
+
}
|
|
903
|
+
}`,
|
|
904
|
+
{ automationId: id, automationVersionId: versionId }
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
};
|
|
908
|
+
var webPageAdapter = pageAdapter({
|
|
909
|
+
type: "webPage",
|
|
910
|
+
dir: "web-pages",
|
|
911
|
+
gqlName: "webPage"
|
|
912
|
+
});
|
|
913
|
+
var printablePageAdapter = pageAdapter({
|
|
914
|
+
type: "printablePage",
|
|
915
|
+
dir: "printable-pages",
|
|
916
|
+
gqlName: "printablePage"
|
|
917
|
+
});
|
|
918
|
+
var allAdapters = [
|
|
919
|
+
automationAdapter,
|
|
920
|
+
functionAdapter,
|
|
921
|
+
webPageAdapter,
|
|
922
|
+
printablePageAdapter
|
|
923
|
+
];
|
|
924
|
+
var adapterFor = (type) => {
|
|
925
|
+
const adapter = allAdapters.find((a) => a.type === type);
|
|
926
|
+
if (adapter == null) throw new Error(`No adapter for resource type ${type}.`);
|
|
927
|
+
return adapter;
|
|
928
|
+
};
|
|
929
|
+
|
|
930
|
+
// src/commands/sync.ts
|
|
931
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
932
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
933
|
+
var toManifestResource = (type, remote) => ({
|
|
934
|
+
type,
|
|
935
|
+
name: remote.name,
|
|
936
|
+
files: Object.keys(remote.files),
|
|
937
|
+
baseVersion: remote.latestVersion,
|
|
938
|
+
deployedVersion: remote.deployedVersion
|
|
939
|
+
});
|
|
940
|
+
var fetchWorkspace = async (workspaceId) => {
|
|
941
|
+
const data = await gqlRequest(
|
|
942
|
+
`query CliWorkspace($id: String!) { workspace(id: $id) { id name organizationId } }`,
|
|
943
|
+
{ id: workspaceId }
|
|
944
|
+
);
|
|
945
|
+
if (data.workspace == null) {
|
|
946
|
+
throw new Error(
|
|
947
|
+
`Workspace ${workspaceId} not found (or not visible to you).`
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
return data.workspace;
|
|
951
|
+
};
|
|
952
|
+
var runClone = async (args) => {
|
|
953
|
+
const [workspaceId, dirArg] = args.filter((a) => !a.startsWith("-"));
|
|
954
|
+
if (workspaceId == null) {
|
|
955
|
+
throw new Error("Usage: overai clone <workspaceId> [dir]");
|
|
956
|
+
}
|
|
957
|
+
await getAccessToken();
|
|
958
|
+
const workspace = await fetchWorkspace(workspaceId);
|
|
959
|
+
const root = resolve2(dirArg ?? slugify(workspace.name));
|
|
960
|
+
if (existsSync2(join3(root, ".overai", "manifest.json"))) {
|
|
961
|
+
throw new Error(
|
|
962
|
+
`${root} is already a cloned workspace. Use \`overai pull\` to refresh.`
|
|
963
|
+
);
|
|
964
|
+
}
|
|
965
|
+
mkdirSync3(root, { recursive: true });
|
|
966
|
+
const manifest = {
|
|
967
|
+
workspaceId: workspace.id,
|
|
968
|
+
workspaceName: workspace.name,
|
|
969
|
+
organizationId: workspace.organizationId,
|
|
970
|
+
resources: {}
|
|
971
|
+
};
|
|
972
|
+
for (const adapter of allAdapters) {
|
|
973
|
+
const resources = await adapter.fetchAll(workspaceId);
|
|
974
|
+
for (const remote of resources) {
|
|
975
|
+
for (const [relPath, content] of Object.entries(remote.files)) {
|
|
976
|
+
writeSynced(root, relPath, content);
|
|
977
|
+
}
|
|
978
|
+
manifest.resources[remote.id] = toManifestResource(adapter.type, remote);
|
|
979
|
+
}
|
|
980
|
+
process.stdout.write(
|
|
981
|
+
`${adapter.type}: ${String(resources.length)} cloned
|
|
982
|
+
`
|
|
983
|
+
);
|
|
984
|
+
}
|
|
985
|
+
writeManifest(root, manifest);
|
|
986
|
+
process.stdout.write(`
|
|
987
|
+
Cloned "${workspace.name}" into ${root}
|
|
988
|
+
`);
|
|
989
|
+
};
|
|
990
|
+
var collectStatus = async (root, manifest) => {
|
|
991
|
+
const rows = [];
|
|
992
|
+
const untracked = [];
|
|
993
|
+
for (const adapter of allAdapters) {
|
|
994
|
+
const remotes = await adapter.fetchAll(manifest.workspaceId);
|
|
995
|
+
const remoteById = new Map(remotes.map((r) => [r.id, r]));
|
|
996
|
+
for (const [id, resource] of Object.entries(manifest.resources)) {
|
|
997
|
+
if (resource.type !== adapter.type) continue;
|
|
998
|
+
const remote = remoteById.get(id);
|
|
999
|
+
rows.push({
|
|
1000
|
+
id,
|
|
1001
|
+
resource,
|
|
1002
|
+
local: localState(root, resource),
|
|
1003
|
+
remoteLatest: remote?.latestVersion ?? resource.baseVersion,
|
|
1004
|
+
behind: (remote?.latestVersion ?? resource.baseVersion) > resource.baseVersion
|
|
1005
|
+
});
|
|
1006
|
+
}
|
|
1007
|
+
for (const remote of remotes) {
|
|
1008
|
+
if (manifest.resources[remote.id] == null) untracked.push(remote);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
return { rows, untracked };
|
|
1012
|
+
};
|
|
1013
|
+
var runStatus = async () => {
|
|
1014
|
+
const { root, manifest } = openClone();
|
|
1015
|
+
const { rows, untracked } = await collectStatus(root, manifest);
|
|
1016
|
+
const lines = [];
|
|
1017
|
+
let dirty = false;
|
|
1018
|
+
for (const row of rows) {
|
|
1019
|
+
const flags = [];
|
|
1020
|
+
if (row.local.state === "modified") flags.push("modified");
|
|
1021
|
+
if (row.local.state === "missing") flags.push("missing files");
|
|
1022
|
+
if (row.behind) {
|
|
1023
|
+
flags.push(`behind (remote v${String(row.remoteLatest)})`);
|
|
1024
|
+
}
|
|
1025
|
+
if (row.resource.baseVersion !== (row.resource.deployedVersion ?? -1)) {
|
|
1026
|
+
flags.push("undeployed");
|
|
1027
|
+
}
|
|
1028
|
+
if (flags.length > 0) dirty = true;
|
|
1029
|
+
const versions = `base v${String(row.resource.baseVersion)}, deployed ${row.resource.deployedVersion == null ? "none" : `v${String(row.resource.deployedVersion)}`}`;
|
|
1030
|
+
lines.push(
|
|
1031
|
+
` ${row.resource.type} ${row.resource.name} (${row.id}) [${versions}]${flags.length > 0 ? `: ${flags.join(", ")}` : ""}`
|
|
1032
|
+
);
|
|
1033
|
+
for (const file of row.local.changedFiles) lines.push(` ${file}`);
|
|
1034
|
+
}
|
|
1035
|
+
for (const remote of untracked) {
|
|
1036
|
+
dirty = true;
|
|
1037
|
+
lines.push(` new remote ${remote.name} \u2014 run \`overai pull\` to fetch`);
|
|
1038
|
+
}
|
|
1039
|
+
process.stdout.write(
|
|
1040
|
+
`Workspace "${manifest.workspaceName}" (${manifest.workspaceId})${dirty ? "" : " \u2014 clean, everything in sync"}
|
|
1041
|
+
${lines.join("\n")}
|
|
1042
|
+
`
|
|
1043
|
+
);
|
|
1044
|
+
};
|
|
1045
|
+
var runRestore = (args) => {
|
|
1046
|
+
const { root, manifest } = openClone();
|
|
1047
|
+
const all = args.includes("--all");
|
|
1048
|
+
const filter = positionals(args)[0];
|
|
1049
|
+
if (filter == null && !all) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
"Usage: overai restore <filter> (resource id, name, or path substring), or --all to discard every local edit."
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
let restored = 0;
|
|
1055
|
+
for (const [, resource] of resourcesMatching(
|
|
1056
|
+
manifest,
|
|
1057
|
+
all ? void 0 : filter
|
|
1058
|
+
)) {
|
|
1059
|
+
for (const relPath of resource.files) {
|
|
1060
|
+
const base = readBaseFile(root, relPath);
|
|
1061
|
+
if (base == null || readWorkingFile(root, relPath) === base) continue;
|
|
1062
|
+
writeSynced(root, relPath, base);
|
|
1063
|
+
restored += 1;
|
|
1064
|
+
process.stdout.write(`restored ${relPath}
|
|
1065
|
+
`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
process.stdout.write(
|
|
1069
|
+
restored === 0 ? "Nothing to restore (no modified files matched).\n" : `Restored ${String(restored)} file(s) to their base versions.
|
|
1070
|
+
`
|
|
1071
|
+
);
|
|
1072
|
+
};
|
|
1073
|
+
var runPull = async () => {
|
|
1074
|
+
const { root, manifest } = openClone();
|
|
1075
|
+
for (const adapter of allAdapters) {
|
|
1076
|
+
const remotes = await adapter.fetchAll(manifest.workspaceId);
|
|
1077
|
+
const remoteById = new Map(remotes.map((r) => [r.id, r]));
|
|
1078
|
+
for (const [id, resource] of Object.entries(manifest.resources)) {
|
|
1079
|
+
if (resource.type !== adapter.type) continue;
|
|
1080
|
+
const remote = remoteById.get(id);
|
|
1081
|
+
if (remote == null) {
|
|
1082
|
+
process.stdout.write(
|
|
1083
|
+
`gone remotely (archived/deleted): ${resource.name} \u2014 local files kept
|
|
1084
|
+
`
|
|
1085
|
+
);
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
const { state } = localState(root, resource);
|
|
1089
|
+
if (remote.latestVersion === resource.baseVersion && state !== "missing") {
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
if (state === "modified" && remote.latestVersion > resource.baseVersion) {
|
|
1093
|
+
process.stdout.write(
|
|
1094
|
+
`CONFLICT ${resource.name}: local edits + remote v${String(remote.latestVersion)} (base v${String(resource.baseVersion)}). Kept local; copy your edits aside, re-run pull, re-apply.
|
|
1095
|
+
`
|
|
1096
|
+
);
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
for (const [relPath, content] of Object.entries(remote.files)) {
|
|
1100
|
+
writeSynced(root, relPath, content);
|
|
1101
|
+
}
|
|
1102
|
+
manifest.resources[id] = toManifestResource(adapter.type, remote);
|
|
1103
|
+
process.stdout.write(
|
|
1104
|
+
`updated ${resource.name} \u2192 v${String(remote.latestVersion)}
|
|
1105
|
+
`
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
for (const remote of remotes) {
|
|
1109
|
+
if (manifest.resources[remote.id] != null) continue;
|
|
1110
|
+
for (const [relPath, content] of Object.entries(remote.files)) {
|
|
1111
|
+
writeSynced(root, relPath, content);
|
|
1112
|
+
}
|
|
1113
|
+
manifest.resources[remote.id] = toManifestResource(adapter.type, remote);
|
|
1114
|
+
process.stdout.write(`added ${remote.name}
|
|
1115
|
+
`);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
writeManifest(root, manifest);
|
|
1119
|
+
process.stdout.write("Pull complete.\n");
|
|
1120
|
+
};
|
|
1121
|
+
var resourcesMatching = (manifest, filter) => Object.entries(manifest.resources).filter(
|
|
1122
|
+
([id, resource]) => filter == null || id === filter || resource.name.toLowerCase().includes(filter.toLowerCase()) || resource.files.some((f) => f.includes(filter))
|
|
1123
|
+
);
|
|
1124
|
+
var positionals = (args) => {
|
|
1125
|
+
const valueFlags = /* @__PURE__ */ new Set(["-m", "--message", "-o", "--output"]);
|
|
1126
|
+
const out = [];
|
|
1127
|
+
for (let i = 0; i < args.length; i++) {
|
|
1128
|
+
const arg = args[i];
|
|
1129
|
+
if (arg == null) continue;
|
|
1130
|
+
if (valueFlags.has(arg)) {
|
|
1131
|
+
i++;
|
|
1132
|
+
continue;
|
|
1133
|
+
}
|
|
1134
|
+
if (arg.startsWith("-")) continue;
|
|
1135
|
+
out.push(arg);
|
|
1136
|
+
}
|
|
1137
|
+
return out;
|
|
1138
|
+
};
|
|
1139
|
+
var runPush = async (args) => {
|
|
1140
|
+
const { root, manifest } = openClone();
|
|
1141
|
+
const message = flagValue(args, "-m", "--message") ?? "Pushed via overai CLI";
|
|
1142
|
+
const deploy = args.includes("--deploy");
|
|
1143
|
+
const force = args.includes("--force");
|
|
1144
|
+
const pathFilter = positionals(args)[0];
|
|
1145
|
+
let pushed = 0;
|
|
1146
|
+
for (const [id, resource] of resourcesMatching(manifest, pathFilter)) {
|
|
1147
|
+
const { state, changedFiles } = localState(root, resource);
|
|
1148
|
+
if (state === "missing") {
|
|
1149
|
+
const missing = resource.files.filter(
|
|
1150
|
+
(f) => readWorkingFile(root, f) == null
|
|
1151
|
+
).length;
|
|
1152
|
+
process.stdout.write(
|
|
1153
|
+
`SKIP ${resource.name}: ${String(missing)} file(s) missing locally \u2014 run \`overai restore ${id}\` to recover.
|
|
1154
|
+
`
|
|
1155
|
+
);
|
|
1156
|
+
continue;
|
|
1157
|
+
}
|
|
1158
|
+
if (state !== "modified") continue;
|
|
1159
|
+
const adapter = adapterFor(resource.type);
|
|
1160
|
+
const head = await adapter.head(id);
|
|
1161
|
+
if (head.latestVersion !== resource.baseVersion && !force) {
|
|
1162
|
+
process.stdout.write(
|
|
1163
|
+
`SKIP ${resource.name}: remote is at v${String(head.latestVersion)}, you branched from v${String(resource.baseVersion)}. \`overai pull\` first, or --force to push anyway.
|
|
1164
|
+
`
|
|
1165
|
+
);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
const files = {};
|
|
1169
|
+
for (const relPath of resource.files) {
|
|
1170
|
+
const content = readWorkingFile(root, relPath);
|
|
1171
|
+
if (content == null) throw new Error(`${relPath} is missing locally.`);
|
|
1172
|
+
files[relPath] = content;
|
|
1173
|
+
}
|
|
1174
|
+
const versionNumber = await adapter.push(id, files, message);
|
|
1175
|
+
for (const relPath of changedFiles) {
|
|
1176
|
+
const content = files[relPath];
|
|
1177
|
+
if (content == null) continue;
|
|
1178
|
+
writeSynced(root, relPath, content);
|
|
1179
|
+
}
|
|
1180
|
+
resource.baseVersion = versionNumber;
|
|
1181
|
+
writeManifest(root, manifest);
|
|
1182
|
+
pushed += 1;
|
|
1183
|
+
process.stdout.write(
|
|
1184
|
+
`pushed ${resource.name} \u2192 v${String(versionNumber)}${deploy ? "" : " (not deployed)"}
|
|
1185
|
+
`
|
|
1186
|
+
);
|
|
1187
|
+
if (deploy) {
|
|
1188
|
+
await adapter.deploy(id, versionNumber);
|
|
1189
|
+
resource.deployedVersion = versionNumber;
|
|
1190
|
+
writeManifest(root, manifest);
|
|
1191
|
+
process.stdout.write(
|
|
1192
|
+
`deployed ${resource.name} v${String(versionNumber)}
|
|
1193
|
+
`
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
process.stdout.write(
|
|
1198
|
+
pushed === 0 ? "Nothing to push (no local modifications matched).\n" : `Pushed ${String(pushed)} resource(s).
|
|
1199
|
+
`
|
|
1200
|
+
);
|
|
1201
|
+
};
|
|
1202
|
+
var runDeploy = async (args) => {
|
|
1203
|
+
const { root, manifest } = openClone();
|
|
1204
|
+
const pathFilter = positionals(args)[0];
|
|
1205
|
+
const pending = resourcesMatching(manifest, pathFilter).filter(
|
|
1206
|
+
([, r]) => r.deployedVersion !== r.baseVersion
|
|
1207
|
+
);
|
|
1208
|
+
if (pathFilter == null && !args.includes("--all") && pending.length > 1) {
|
|
1209
|
+
throw new Error(
|
|
1210
|
+
`${String(pending.length)} resources have undeployed versions:
|
|
1211
|
+
${pending.map(([id, r]) => ` ${r.name} (${id}) v${String(r.baseVersion)}`).join(
|
|
1212
|
+
"\n"
|
|
1213
|
+
)}
|
|
1214
|
+
Pass a filter to deploy one, or --all to deploy everything.`
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
let deployed = 0;
|
|
1218
|
+
for (const [id, resource] of resourcesMatching(manifest, pathFilter)) {
|
|
1219
|
+
if (resource.deployedVersion === resource.baseVersion) continue;
|
|
1220
|
+
const { state } = localState(root, resource);
|
|
1221
|
+
if (state === "modified") {
|
|
1222
|
+
process.stdout.write(
|
|
1223
|
+
`SKIP ${resource.name}: local edits not pushed \u2014 push first so you deploy what you see.
|
|
1224
|
+
`
|
|
1225
|
+
);
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
await adapterFor(resource.type).deploy(id, resource.baseVersion);
|
|
1229
|
+
resource.deployedVersion = resource.baseVersion;
|
|
1230
|
+
writeManifest(root, manifest);
|
|
1231
|
+
deployed += 1;
|
|
1232
|
+
process.stdout.write(
|
|
1233
|
+
`deployed ${resource.name} v${String(resource.baseVersion)}
|
|
1234
|
+
`
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
process.stdout.write(
|
|
1238
|
+
deployed === 0 ? "Nothing to deploy.\n" : `Deployed ${String(deployed)} resource(s).
|
|
1239
|
+
`
|
|
1240
|
+
);
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1243
|
+
// src/commands/dev.ts
|
|
1244
|
+
var LONG_POLL_MS = 25e3;
|
|
1245
|
+
var runDev = async (args) => {
|
|
1246
|
+
const { root, manifest } = openClone();
|
|
1247
|
+
const filter = args.find(
|
|
1248
|
+
(a, i) => !a.startsWith("-") && args[i - 1] !== "--port"
|
|
1249
|
+
);
|
|
1250
|
+
const pages2 = resourcesMatching(manifest, filter).filter(
|
|
1251
|
+
([, r]) => r.type === "webPage"
|
|
1252
|
+
);
|
|
1253
|
+
const match = pages2.length === 1 ? pages2[0] : null;
|
|
1254
|
+
if (match == null) {
|
|
1255
|
+
throw new Error(
|
|
1256
|
+
pages2.length === 0 ? `No web page matched "${filter ?? ""}" (dev preview supports web pages for now).` : `Ambiguous \u2014 matched ${pages2.map(([, r]) => r.name).join(", ")}. Narrow the filter.`
|
|
1257
|
+
);
|
|
1258
|
+
}
|
|
1259
|
+
const [id, resource] = match;
|
|
1260
|
+
const snapshot = () => {
|
|
1261
|
+
const base = {
|
|
1262
|
+
id,
|
|
1263
|
+
name: resource.name,
|
|
1264
|
+
workspaceId: manifest.workspaceId
|
|
1265
|
+
};
|
|
1266
|
+
try {
|
|
1267
|
+
const files = {};
|
|
1268
|
+
for (const relPath of resource.files) {
|
|
1269
|
+
const content = readWorkingFile(root, relPath);
|
|
1270
|
+
if (content == null) throw new Error(`Missing local file ${relPath}.`);
|
|
1271
|
+
files[relPath] = content;
|
|
1272
|
+
}
|
|
1273
|
+
const config = pageConfigFromFiles(files);
|
|
1274
|
+
const revision = createHash2("sha1").update(JSON.stringify(config)).digest("hex");
|
|
1275
|
+
return { ...base, revision, config };
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1278
|
+
return {
|
|
1279
|
+
...base,
|
|
1280
|
+
revision: `error-${createHash2("sha1").update(message).digest("hex")}`,
|
|
1281
|
+
error: message
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
};
|
|
1285
|
+
let waiters = [];
|
|
1286
|
+
let debounce = null;
|
|
1287
|
+
const notifyChange = () => {
|
|
1288
|
+
if (debounce != null) clearTimeout(debounce);
|
|
1289
|
+
debounce = setTimeout(() => {
|
|
1290
|
+
const pending = waiters;
|
|
1291
|
+
waiters = [];
|
|
1292
|
+
pending.forEach((wake) => {
|
|
1293
|
+
wake();
|
|
1294
|
+
});
|
|
1295
|
+
}, 100);
|
|
1296
|
+
};
|
|
1297
|
+
const watchedDirs = [
|
|
1298
|
+
...new Set(resource.files.map((f) => join4(root, dirname2(f))))
|
|
1299
|
+
];
|
|
1300
|
+
watchedDirs.forEach((dir) => watch(dir, notifyChange));
|
|
1301
|
+
const appOrigin = new URL(appBaseUrl).origin;
|
|
1302
|
+
const server = createServer2((req, res) => {
|
|
1303
|
+
const headers = {
|
|
1304
|
+
// The webapp page is a different origin; a secure page may also send a
|
|
1305
|
+
// private-network preflight before touching loopback — allow both.
|
|
1306
|
+
"Access-Control-Allow-Origin": appOrigin,
|
|
1307
|
+
"Access-Control-Allow-Private-Network": "true",
|
|
1308
|
+
"Cache-Control": "no-store"
|
|
1309
|
+
};
|
|
1310
|
+
if (req.method === "OPTIONS") {
|
|
1311
|
+
res.writeHead(204, headers);
|
|
1312
|
+
res.end();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
const url2 = new URL(req.url ?? "/", "http://localhost");
|
|
1316
|
+
if (url2.pathname !== "/page") {
|
|
1317
|
+
res.writeHead(404, headers);
|
|
1318
|
+
res.end();
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const respond = () => {
|
|
1322
|
+
res.writeHead(200, { ...headers, "Content-Type": "application/json" });
|
|
1323
|
+
res.end(JSON.stringify(snapshot()));
|
|
1324
|
+
};
|
|
1325
|
+
const since = url2.searchParams.get("since");
|
|
1326
|
+
if (since == null || since !== snapshot().revision) {
|
|
1327
|
+
respond();
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
let settled = false;
|
|
1331
|
+
const finish = () => {
|
|
1332
|
+
if (settled) return;
|
|
1333
|
+
settled = true;
|
|
1334
|
+
clearTimeout(timer);
|
|
1335
|
+
respond();
|
|
1336
|
+
};
|
|
1337
|
+
const timer = setTimeout(finish, LONG_POLL_MS);
|
|
1338
|
+
waiters.push(finish);
|
|
1339
|
+
req.on("close", () => {
|
|
1340
|
+
settled = true;
|
|
1341
|
+
clearTimeout(timer);
|
|
1342
|
+
});
|
|
1343
|
+
});
|
|
1344
|
+
const port = Number(flagValue(args, "--port") ?? 4820);
|
|
1345
|
+
await new Promise((resolve3, reject) => {
|
|
1346
|
+
server.once("error", reject);
|
|
1347
|
+
server.listen(port, "127.0.0.1", resolve3);
|
|
1348
|
+
}).catch((error) => {
|
|
1349
|
+
throw new Error(
|
|
1350
|
+
error instanceof Error && "code" in error && error.code === "EADDRINUSE" ? `Port ${String(port)} is in use \u2014 pass --port <n>.` : String(error)
|
|
1351
|
+
);
|
|
1352
|
+
});
|
|
1353
|
+
const url = `${appBaseUrl}/${manifest.organizationId}/dev?src=${encodeURIComponent(`http://127.0.0.1:${String(port)}`)}`;
|
|
1354
|
+
process.stdout.write(
|
|
1355
|
+
`Serving ${resource.name} (${id}) from local files.
|
|
1356
|
+
${url}
|
|
1357
|
+
Watching for changes \u2014 Ctrl+C to stop.
|
|
1358
|
+
`
|
|
1359
|
+
);
|
|
1360
|
+
if (args.includes("--open")) openBrowser(url);
|
|
1361
|
+
await new Promise(() => void 0);
|
|
1362
|
+
};
|
|
1363
|
+
|
|
1364
|
+
// src/commands/fn.ts
|
|
1365
|
+
var collectInput = async (args) => {
|
|
1366
|
+
if (args.includes("-")) {
|
|
1367
|
+
const raw = (await readStdin()).trim();
|
|
1368
|
+
try {
|
|
1369
|
+
return JSON.parse(raw);
|
|
1370
|
+
} catch {
|
|
1371
|
+
throw new Error(`stdin must be valid JSON. Got: ${raw.slice(0, 120)}`);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
const input = {};
|
|
1375
|
+
args.forEach((arg, i) => {
|
|
1376
|
+
if (arg === "--input" && i + 1 < args.length) {
|
|
1377
|
+
const pair = args[i + 1] ?? "";
|
|
1378
|
+
const eq = pair.indexOf("=");
|
|
1379
|
+
if (eq > 0) input[pair.slice(0, eq)] = pair.slice(eq + 1);
|
|
1380
|
+
}
|
|
1381
|
+
});
|
|
1382
|
+
return input;
|
|
1383
|
+
};
|
|
1384
|
+
var runLogs = async (args) => {
|
|
1385
|
+
const functionId = args[0];
|
|
1386
|
+
if (functionId == null) {
|
|
1387
|
+
throw new Error(
|
|
1388
|
+
"Usage: overai fn logs <functionId> [--limit N] [--execution <id>]"
|
|
1389
|
+
);
|
|
1390
|
+
}
|
|
1391
|
+
const executionId = flagValue(args, "--execution");
|
|
1392
|
+
if (executionId == null) {
|
|
1393
|
+
const limit = Number(flagValue(args, "--limit") ?? 20);
|
|
1394
|
+
const data2 = await gqlRequest(
|
|
1395
|
+
`query CliFunctionExecutions($functionId: String!, $first: Int!) {
|
|
1396
|
+
functionExecutions(condition: { functionId: $functionId }, orderBy: [CREATED_AT_DESC], first: $first) {
|
|
1397
|
+
nodes { id createdAt httpStatusCode executionTime requestMethod requestPath versionNumber }
|
|
1398
|
+
}
|
|
1399
|
+
}`,
|
|
1400
|
+
{ functionId, first: limit }
|
|
1401
|
+
);
|
|
1402
|
+
printResult(
|
|
1403
|
+
data2.functionExecutions.nodes,
|
|
1404
|
+
resolveFormat(flagValue(args, "-o", "--output"))
|
|
1405
|
+
);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const data = await gqlRequest(
|
|
1409
|
+
`query CliFunctionExecution($functionId: String!, $id: String!) {
|
|
1410
|
+
functionExecutions(condition: { functionId: $functionId, id: $id }, first: 1) {
|
|
1411
|
+
nodes { id summary createdAt httpStatusCode executionTime versionNumber logsUrl inputBodyUrl outputBodyUrl }
|
|
1412
|
+
}
|
|
1413
|
+
}`,
|
|
1414
|
+
{ functionId, id: executionId }
|
|
1415
|
+
);
|
|
1416
|
+
const execution = data.functionExecutions.nodes[0];
|
|
1417
|
+
if (execution == null) throw new Error(`Execution ${executionId} not found.`);
|
|
1418
|
+
const fetchText = async (url) => url == null ? null : await fetch(url).then((r) => r.ok ? r.text() : null);
|
|
1419
|
+
const [logs, input, output] = await Promise.all([
|
|
1420
|
+
fetchText(execution.logsUrl),
|
|
1421
|
+
fetchText(execution.inputBodyUrl),
|
|
1422
|
+
fetchText(execution.outputBodyUrl)
|
|
1423
|
+
]);
|
|
1424
|
+
const fmt = resolveFormat(flagValue(args, "-o", "--output"));
|
|
1425
|
+
printResult(
|
|
1426
|
+
{
|
|
1427
|
+
id: execution.id,
|
|
1428
|
+
createdAt: execution.createdAt,
|
|
1429
|
+
httpStatusCode: execution.httpStatusCode,
|
|
1430
|
+
executionTime: execution.executionTime,
|
|
1431
|
+
versionNumber: execution.versionNumber,
|
|
1432
|
+
summary: execution.summary,
|
|
1433
|
+
input,
|
|
1434
|
+
output,
|
|
1435
|
+
logs
|
|
1436
|
+
},
|
|
1437
|
+
// A single nested record reads better as JSON than a 1-row table.
|
|
1438
|
+
fmt === "table" ? "json" : fmt
|
|
1439
|
+
);
|
|
1440
|
+
};
|
|
1441
|
+
var runFn = async (args) => {
|
|
1442
|
+
if (args[0] === "logs") return runLogs(args.slice(1));
|
|
1443
|
+
const functionId = args[0];
|
|
1444
|
+
if (functionId == null || functionId.startsWith("-")) {
|
|
1445
|
+
throw new Error(
|
|
1446
|
+
"Usage: overai fn <functionId> [--input key=value ... | -]\n overai fn logs <functionId> [--limit N] [--execution <id>]"
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
const input = await collectInput(args);
|
|
1450
|
+
const { organizationId } = await getAccessToken();
|
|
1451
|
+
const res = await v2Fetch(`/v2/functions/${organizationId}/${functionId}`, {
|
|
1452
|
+
method: "POST",
|
|
1453
|
+
headers: { "Content-Type": "application/json" },
|
|
1454
|
+
body: JSON.stringify(input)
|
|
1455
|
+
});
|
|
1456
|
+
const executionId = res.headers.get("x-execution-id");
|
|
1457
|
+
if (executionId != null) {
|
|
1458
|
+
process.stderr.write(`execution: ${executionId}
|
|
1459
|
+
`);
|
|
1460
|
+
}
|
|
1461
|
+
const body = await res.text();
|
|
1462
|
+
if (!res.ok) {
|
|
1463
|
+
const structured = (() => {
|
|
1464
|
+
try {
|
|
1465
|
+
return JSON.parse(body);
|
|
1466
|
+
} catch {
|
|
1467
|
+
return null;
|
|
1468
|
+
}
|
|
1469
|
+
})();
|
|
1470
|
+
throw new Error(
|
|
1471
|
+
structured?.error ?? (body.trim() || `fn failed (${String(res.status)})`)
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
process.stdout.write(`${body}
|
|
1475
|
+
`);
|
|
1476
|
+
};
|
|
1477
|
+
|
|
1478
|
+
// src/commands/graphql.ts
|
|
1479
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
1480
|
+
var runGraphql = async (args) => {
|
|
1481
|
+
const inline = flagValue(args, "-q", "--query");
|
|
1482
|
+
const file = flagValue(args, "-f", "--file");
|
|
1483
|
+
const query = inline ?? (file != null ? readFileSync3(file, "utf8") : process.stdin.isTTY ? "" : (await readStdin()).trim());
|
|
1484
|
+
if (query.length === 0) {
|
|
1485
|
+
throw new Error("Provide a query with -q '<query>', -f <file>, or stdin.");
|
|
1486
|
+
}
|
|
1487
|
+
const variablesJson = flagValue(args, "--variables");
|
|
1488
|
+
const variables = (() => {
|
|
1489
|
+
if (variablesJson == null) return void 0;
|
|
1490
|
+
try {
|
|
1491
|
+
return JSON.parse(variablesJson);
|
|
1492
|
+
} catch {
|
|
1493
|
+
throw new Error(`--variables must be valid JSON. Got: ${variablesJson}`);
|
|
1494
|
+
}
|
|
1495
|
+
})();
|
|
1496
|
+
const res = await v2Fetch(
|
|
1497
|
+
args.includes("--v1") ? "/v1/graphql" : "/v2/graphql",
|
|
1498
|
+
{
|
|
1499
|
+
method: "POST",
|
|
1500
|
+
headers: { "Content-Type": "application/json" },
|
|
1501
|
+
body: JSON.stringify({ query, variables })
|
|
1502
|
+
}
|
|
1503
|
+
);
|
|
1504
|
+
const body = await res.json().catch(() => null);
|
|
1505
|
+
if (body?.errors != null && body.errors.length > 0) {
|
|
1506
|
+
throw new Error(body.errors.map((e) => e.message).join("; "));
|
|
1507
|
+
}
|
|
1508
|
+
if (!res.ok || body == null) {
|
|
1509
|
+
throw new Error(`graphql failed (${String(res.status)})`);
|
|
1510
|
+
}
|
|
1511
|
+
printResult(body.data, resolveFormat(flagValue(args, "-o", "--output")));
|
|
1512
|
+
};
|
|
1513
|
+
|
|
1514
|
+
// src/commands/open.ts
|
|
1515
|
+
var ROUTE_BY_TYPE = {
|
|
1516
|
+
automation: "automations",
|
|
1517
|
+
function: "functions",
|
|
1518
|
+
webPage: "web-pages",
|
|
1519
|
+
printablePage: "printable-pages"
|
|
1520
|
+
};
|
|
1521
|
+
var ROUTE_BY_PREFIX = [
|
|
1522
|
+
["WS", "workspaces"],
|
|
1523
|
+
["WP", "web-pages"],
|
|
1524
|
+
["PP", "printable-pages"],
|
|
1525
|
+
["FN", "functions"],
|
|
1526
|
+
["AT", "automations"],
|
|
1527
|
+
["TBS", "table-spaces"],
|
|
1528
|
+
["DF", "data-files"],
|
|
1529
|
+
["AG", "agents"]
|
|
1530
|
+
];
|
|
1531
|
+
var runOpen = async (args) => {
|
|
1532
|
+
const filter = args.find((a) => !a.startsWith("-"));
|
|
1533
|
+
const { organizationId } = await getAccessToken();
|
|
1534
|
+
const url = (() => {
|
|
1535
|
+
const manifest = (() => {
|
|
1536
|
+
try {
|
|
1537
|
+
return readManifest(findWorkspaceRoot());
|
|
1538
|
+
} catch {
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1541
|
+
})();
|
|
1542
|
+
if (manifest != null) {
|
|
1543
|
+
if (filter == null) {
|
|
1544
|
+
return `${appBaseUrl}/${manifest.organizationId}/workspaces/${manifest.workspaceId}`;
|
|
1545
|
+
}
|
|
1546
|
+
const match = Object.entries(manifest.resources).find(
|
|
1547
|
+
([id, r]) => id === filter || r.name.toLowerCase().includes(filter.toLowerCase()) || r.files.some((f) => f.includes(filter))
|
|
1548
|
+
);
|
|
1549
|
+
if (match != null) {
|
|
1550
|
+
const [id, resource] = match;
|
|
1551
|
+
return `${appBaseUrl}/${manifest.organizationId}/${ROUTE_BY_TYPE[resource.type]}/${id}`;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
if (filter == null) {
|
|
1555
|
+
throw new Error(
|
|
1556
|
+
"Not inside a cloned workspace \u2014 pass a resource id (WP..., FN..., WS..., AT...)."
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
const prefixed = ROUTE_BY_PREFIX.find(
|
|
1560
|
+
([prefix]) => filter.startsWith(prefix)
|
|
1561
|
+
);
|
|
1562
|
+
if (prefixed == null) {
|
|
1563
|
+
throw new Error(
|
|
1564
|
+
`Nothing matched "${filter}" (not a tracked resource or a recognized id prefix).`
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
return `${appBaseUrl}/${organizationId}/${prefixed[1]}/${filter}`;
|
|
1568
|
+
})();
|
|
1569
|
+
process.stdout.write(`${url}
|
|
1570
|
+
`);
|
|
1571
|
+
openBrowser(url);
|
|
1572
|
+
};
|
|
1573
|
+
|
|
1574
|
+
// src/commands/org.ts
|
|
1575
|
+
var USER_ORGS_QUERY = `
|
|
1576
|
+
query UserOrganizations($identityId: String!) {
|
|
1577
|
+
identityOrganizations(
|
|
1578
|
+
where: { identityId: { _eq: $identityId }, disabled: { _eq: false } }
|
|
1579
|
+
) {
|
|
1580
|
+
organization { id name }
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
`;
|
|
1584
|
+
var listOrgs = async (args) => {
|
|
1585
|
+
const auth = await getAccessToken();
|
|
1586
|
+
const identityId = decodeJwtClaims(auth.token).user_id;
|
|
1587
|
+
if (identityId == null) {
|
|
1588
|
+
throw new Error("Could not read identity from the current session.");
|
|
1589
|
+
}
|
|
1590
|
+
const res = await v2Fetch("/v1/graphql", {
|
|
1591
|
+
method: "POST",
|
|
1592
|
+
headers: { "Content-Type": "application/json" },
|
|
1593
|
+
body: JSON.stringify({ query: USER_ORGS_QUERY, variables: { identityId } })
|
|
1594
|
+
});
|
|
1595
|
+
const body = await res.json().catch(() => null);
|
|
1596
|
+
if (body?.errors != null && body.errors.length > 0) {
|
|
1597
|
+
throw new Error(body.errors.map((e) => e.message).join("; "));
|
|
1598
|
+
}
|
|
1599
|
+
if (!res.ok || body == null) {
|
|
1600
|
+
throw new Error(`org list failed (${String(res.status)})`);
|
|
1601
|
+
}
|
|
1602
|
+
const orgs = (body.data?.identityOrganizations ?? []).map((edge) => edge.organization).filter((org) => org != null).map((org) => ({
|
|
1603
|
+
id: org.id,
|
|
1604
|
+
name: org.name ?? "",
|
|
1605
|
+
active: org.id === auth.organizationId
|
|
1606
|
+
}));
|
|
1607
|
+
printResult(orgs, resolveFormat(flagValue(args, "-o", "--output")));
|
|
1608
|
+
};
|
|
1609
|
+
var switchOrg = async (orgId) => {
|
|
1610
|
+
const config = readConfig();
|
|
1611
|
+
if (config == null) {
|
|
1612
|
+
process.stderr.write("Not signed in. Run `overai signin`.\n");
|
|
1613
|
+
process.exitCode = 1;
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
const result = await exchangeSessionToken(config.refreshToken, orgId);
|
|
1617
|
+
if (result.type === "redirect") {
|
|
1618
|
+
process.stdout.write(
|
|
1619
|
+
`This organization requires SSO. Complete sign-in in your browser, then run \`overai signin\`.
|
|
1620
|
+
${result.redirectUrl}
|
|
1621
|
+
`
|
|
1622
|
+
);
|
|
1623
|
+
openBrowser(result.redirectUrl);
|
|
1624
|
+
process.exitCode = 1;
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
writeConfig({
|
|
1628
|
+
...config,
|
|
1629
|
+
organizationId: orgId,
|
|
1630
|
+
email: decodeJwtClaims(result.token).email ?? config.email,
|
|
1631
|
+
accessToken: result.token,
|
|
1632
|
+
accessTokenExpiresAt: Date.now() + result.accessTokenValiditySeconds * 1e3,
|
|
1633
|
+
refreshToken: result.refreshToken
|
|
1634
|
+
});
|
|
1635
|
+
process.stdout.write(`Switched to org ${orgId}.
|
|
1636
|
+
`);
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
// src/docs/bestPractices.ts
|
|
1640
|
+
var platform2 = `# OverAI platform primer
|
|
1641
|
+
|
|
1642
|
+
Resources live in Workspaces. The runtime always executes the DEPLOYED version
|
|
1643
|
+
of a resource, never the latest \u2014 \`overai push\` creates a version, \`overai
|
|
1644
|
+
deploy\` makes it live. Pick primitives by trigger:
|
|
1645
|
+
|
|
1646
|
+
- User-initiated work (clicks, forms, reviewing data) \u2192 a **Web Page**.
|
|
1647
|
+
- Schedule / webhook / file upload / inbound email \u2192 an **Automation**.
|
|
1648
|
+
- Awaitable server-side logic, callable from pages/automations/agents \u2192 a
|
|
1649
|
+
**Function** (Deno sandbox, can import from esm.sh, full env SDK).
|
|
1650
|
+
- Storage: **TableSpaces** (Postgres, via env.TBSxxx) for structured data;
|
|
1651
|
+
**Data Files** (env.DFxxx) for versioned single-file datasets. Prefer
|
|
1652
|
+
first-class platform features over third-party integrations.
|
|
1653
|
+
|
|
1654
|
+
The env SDK is the same in pages, functions, and automation code nodes:
|
|
1655
|
+
env.TBSxxx.sql\`...\` (postgres.js tagged template \u2014 interpolations are
|
|
1656
|
+
auto-parameterized, never string-concatenate user input), env.FNxxx.call(),
|
|
1657
|
+
env.ATxxx.start(inputs?) (fire-and-forget), env.DFxxx.read.json()/.saveVersion(),
|
|
1658
|
+
env.STORAGE, env.AI.generateText/generateObject, env.GRAPHQL.request().`;
|
|
1659
|
+
var automations = `# Automation best practices
|
|
1660
|
+
|
|
1661
|
+
An automation's elements document is the node graph: an array of nodes, each
|
|
1662
|
+
{ id, label, variableId, nodeTypeId, inputs, parents?, position?, elements? }.
|
|
1663
|
+
Edges are expressed as parents: [{ id, handle }] with handle \u2208
|
|
1664
|
+
success|failure|loop. The trigger node has id "event", variableId "EVENT".
|
|
1665
|
+
Loop children nest inside the loop node's own \`elements\` array.
|
|
1666
|
+
|
|
1667
|
+
Rules that keep graphs healthy:
|
|
1668
|
+
- Never create cycles; a node must not be the child of two different outputs
|
|
1669
|
+
of the same parent; never connect across loop boundaries.
|
|
1670
|
+
- Keep TYPESCRIPT_CODE nodes small and focused \u2014 split logic across nodes and
|
|
1671
|
+
let the graph orchestrate, rather than one big code block.
|
|
1672
|
+
- Node inputs reference upstream state via variableIds (e.g. STATE.S1, Liquid
|
|
1673
|
+
{{S1.field}}). After renaming/removing a node, grep the document for its
|
|
1674
|
+
variableId \u2014 dangling references fail at runtime, not at push.
|
|
1675
|
+
- Bring IF_ELSE branches back together with an ALWAYS node.
|
|
1676
|
+
- Activation requires a deployed version with an EVENT trigger node \u2014
|
|
1677
|
+
activating without one silently no-ops.
|
|
1678
|
+
|
|
1679
|
+
Debugging without the canvas:
|
|
1680
|
+
- \`overai automation runs <id> --failed\` \u2014 recent errored runs.
|
|
1681
|
+
- \`overai automation state <runId>\` \u2014 per-node { inputs, outputs, outcome },
|
|
1682
|
+
keyed by variableId; loop iterations nest under L*.iterations[i].state.
|
|
1683
|
+
Large/old runs may have partial state here (authoritative copy lives in
|
|
1684
|
+
object storage) \u2014 treat as best-effort.
|
|
1685
|
+
- \`overai automation run <id> --input k=v\` starts a real run of the DEPLOYED
|
|
1686
|
+
version (fire-and-forget, returns automationRunId). Run read-only paths
|
|
1687
|
+
first; verify before deploying mutating changes.`;
|
|
1688
|
+
var pages = `# Page best practices (web + printable)
|
|
1689
|
+
|
|
1690
|
+
A page version's config JSON carries the code; the CLI splits it into
|
|
1691
|
+
<slug>.<id>.tsx (the code) and <slug>.<id>.meta.json (config minus code \u2014
|
|
1692
|
+
language, customTitle, hideTopBar, ...). Push reassembles them.
|
|
1693
|
+
|
|
1694
|
+
- Web pages are React (TSX). Printable pages are React or Liquid.
|
|
1695
|
+
- Web page code is concatenated with an SDK that already provides React and
|
|
1696
|
+
its hooks, then appends the <App /> render call. Declare a plain
|
|
1697
|
+
\`function App() { ... }\` \u2014 no \`export default\`, no
|
|
1698
|
+
\`import ... from "react"\` (redeclaring hooks is a SyntaxError \u2192 blank
|
|
1699
|
+
page). Imports must be https URLs (\`https://esm.sh/...\`) \u2014 the sandbox
|
|
1700
|
+
cannot resolve bare or relative specifiers.
|
|
1701
|
+
- Pages run sandboxed in an iframe with the env SDK plus hooks: useCurrentUser,
|
|
1702
|
+
useSearchParam, useLLMText/Object/Chat, useAgentChat, useDataFile (legacy \u2014
|
|
1703
|
+
prefer env.DFxxx), navigateToPage, openPrintablePage.
|
|
1704
|
+
- Call Functions for server-side logic \u2014 they're awaitable; automations are
|
|
1705
|
+
fire-and-forget.
|
|
1706
|
+
- SQL from pages: env.TBSxxx.sql tagged templates only; compose dynamic SQL by
|
|
1707
|
+
nesting sql fragments, sql([...]) for IN lists. sql.unsafe only for dynamic
|
|
1708
|
+
identifiers.
|
|
1709
|
+
- For guest-facing workspaces, add page-to-page navigation (guests land on the
|
|
1710
|
+
workspace homepage and can only follow links). "Hide Top Bar" gives an
|
|
1711
|
+
app-like experience.
|
|
1712
|
+
- Console logs are visible in the webapp editor; use them to verify behavior
|
|
1713
|
+
after deploying.`;
|
|
1714
|
+
var functions = `# Function best practices
|
|
1715
|
+
|
|
1716
|
+
Functions are TypeScript HTTP handlers in a Deno sandbox: full env SDK,
|
|
1717
|
+
esm.sh imports allowed. The CLI materializes <slug>.<id>.ts (code) and
|
|
1718
|
+
<slug>.<id>.meta.json (inputSchema, executionTimeout ms, executionMemory MB).
|
|
1719
|
+
|
|
1720
|
+
- Functions are full HTTP apps: method + sub-path are forwarded to your code,
|
|
1721
|
+
and each execution records requestMethod/requestPath.
|
|
1722
|
+
- Keep the inputSchema in sync with what the code actually reads \u2014 callers
|
|
1723
|
+
(agents, pages, automations) rely on it.
|
|
1724
|
+
- Verify with \`overai fn <id> --input k=v\` (invokes the DEPLOYED version) and
|
|
1725
|
+
\`overai fn logs <id>\` / \`--execution <FEX id>\` for status, timing, logs,
|
|
1726
|
+
and input/output bodies.
|
|
1727
|
+
- Version discipline: function_execution.versionNumber tells you exactly which
|
|
1728
|
+
version served a request \u2014 check it before blaming current code for an old
|
|
1729
|
+
failure.`;
|
|
1730
|
+
var workflow = `# CLI editing workflow
|
|
1731
|
+
|
|
1732
|
+
1. \`overai clone <workspaceId>\` (or \`overai pull\` in an existing clone).
|
|
1733
|
+
2. Edit files. The id in each filename is authoritative; slugs are cosmetic.
|
|
1734
|
+
3. \`overai status\` / \`overai diff\` \u2014 review what changed.
|
|
1735
|
+
4. \`overai review\` \u2014 validation + these guides; fix findings first.
|
|
1736
|
+
5. \`overai push -m "why"\` \u2014 creates NEW versions (not live). Fast-forward
|
|
1737
|
+
only: if the resource moved remotely, pull, re-apply, retry. Write commit
|
|
1738
|
+
messages like PR titles \u2014 they're the version history.
|
|
1739
|
+
6. Verify behavior (fn invoke, automation run, page preview in the webapp).
|
|
1740
|
+
7. \`overai deploy [filter]\` \u2014 move the deployed pointer. Deploy is the only
|
|
1741
|
+
step users can feel; push freely, deploy deliberately.
|
|
1742
|
+
|
|
1743
|
+
Never edit .overai/ by hand \u2014 it holds the manifest and pristine base copies
|
|
1744
|
+
that make status/diff/fast-forward work.`;
|
|
1745
|
+
var BEST_PRACTICES = {
|
|
1746
|
+
platform: platform2,
|
|
1747
|
+
automations,
|
|
1748
|
+
pages,
|
|
1749
|
+
functions,
|
|
1750
|
+
workflow
|
|
1751
|
+
};
|
|
1752
|
+
|
|
1753
|
+
// src/commands/review.ts
|
|
1754
|
+
var runBestPractices = (args) => {
|
|
1755
|
+
const topic = args[0];
|
|
1756
|
+
if (topic != null) {
|
|
1757
|
+
const doc = BEST_PRACTICES[topic];
|
|
1758
|
+
if (doc == null) {
|
|
1759
|
+
throw new Error(
|
|
1760
|
+
`Unknown topic "${topic}". Topics: ${Object.keys(BEST_PRACTICES).join(", ")}`
|
|
1761
|
+
);
|
|
1762
|
+
}
|
|
1763
|
+
process.stdout.write(`${doc}
|
|
1764
|
+
`);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
process.stdout.write(
|
|
1768
|
+
`${Object.values(BEST_PRACTICES).join("\n\n---\n\n")}
|
|
1769
|
+
`
|
|
1770
|
+
);
|
|
1771
|
+
};
|
|
1772
|
+
var runReview = async () => {
|
|
1773
|
+
const root = findWorkspaceRoot();
|
|
1774
|
+
const manifest = readManifest(root);
|
|
1775
|
+
const findings = [];
|
|
1776
|
+
const touchedTypes = /* @__PURE__ */ new Set();
|
|
1777
|
+
const changed = [];
|
|
1778
|
+
for (const [id, resource] of Object.entries(manifest.resources)) {
|
|
1779
|
+
const { state, changedFiles } = localState(root, resource);
|
|
1780
|
+
if (state !== "modified") continue;
|
|
1781
|
+
touchedTypes.add(resource.type);
|
|
1782
|
+
changed.push(
|
|
1783
|
+
`${resource.type} "${resource.name}" (${id}): ${changedFiles.join(", ")}`
|
|
1784
|
+
);
|
|
1785
|
+
for (const relPath of changedFiles) {
|
|
1786
|
+
const content = readWorkingFile(root, relPath);
|
|
1787
|
+
if (content == null) continue;
|
|
1788
|
+
if (relPath.endsWith(".json")) {
|
|
1789
|
+
try {
|
|
1790
|
+
JSON.parse(content);
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
findings.push(
|
|
1793
|
+
`${relPath}: invalid JSON \u2014 ${error instanceof Error ? error.message : String(error)}`
|
|
1794
|
+
);
|
|
1795
|
+
continue;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
if (!relPath.endsWith(".meta.json") && content.trim().length === 0) {
|
|
1799
|
+
findings.push(
|
|
1800
|
+
`${relPath}: file is empty \u2014 push would blank the resource.`
|
|
1801
|
+
);
|
|
1802
|
+
}
|
|
1803
|
+
if (resource.type === "webPage" && relPath.endsWith(".tsx")) {
|
|
1804
|
+
const importRe = /^\s*import\s+(?:[^'"\n]*?\bfrom\s+)?["']([^"']+)["']/gm;
|
|
1805
|
+
for (const match of content.matchAll(importRe)) {
|
|
1806
|
+
const specifier = match[1];
|
|
1807
|
+
if (specifier == null || specifier.startsWith("https://")) continue;
|
|
1808
|
+
findings.push(
|
|
1809
|
+
/^react(-dom)?(\/|$)/.test(specifier) ? `${relPath}: imports "${specifier}" \u2014 React and its hooks are already in scope; remove the import or the page renders blank.` : `${relPath}: imports "${specifier}" \u2014 the page sandbox can only import https URLs (use https://esm.sh/...).`
|
|
1810
|
+
);
|
|
1811
|
+
}
|
|
1812
|
+
if (/^\s*export\s+default\b/m.test(content)) {
|
|
1813
|
+
findings.push(
|
|
1814
|
+
`${relPath}: uses \`export default\` \u2014 declare a plain \`function App() {}\`; the runtime appends the render call.`
|
|
1815
|
+
);
|
|
1816
|
+
}
|
|
1817
|
+
if (!/\b(function|const|let)\s+App\b/.test(content)) {
|
|
1818
|
+
findings.push(
|
|
1819
|
+
`${relPath}: no App component found \u2014 page code must declare \`function App() {}\`.`
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (resource.type === "automation") {
|
|
1825
|
+
const docFile = resource.files.find((f) => f.endsWith(".json"));
|
|
1826
|
+
const raw = docFile == null ? null : readWorkingFile(root, docFile);
|
|
1827
|
+
if (docFile != null && raw != null) {
|
|
1828
|
+
try {
|
|
1829
|
+
const doc = JSON.parse(raw);
|
|
1830
|
+
findings.push(
|
|
1831
|
+
...(await validateAutomationElements(doc.elements)).map(
|
|
1832
|
+
(f) => `${docFile} elements.${f}`
|
|
1833
|
+
)
|
|
1834
|
+
);
|
|
1835
|
+
} catch {
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
if (changed.length === 0) {
|
|
1841
|
+
process.stdout.write("Nothing modified locally \u2014 nothing to review.\n");
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
const guides = ["workflow", ...touchedTypes].map((t) => t === "webPage" || t === "printablePage" ? "pages" : t).map((t) => t === "automation" ? "automations" : t).map((t) => t === "function" ? "functions" : t);
|
|
1845
|
+
const guideDocs = [...new Set(guides)].map((t) => BEST_PRACTICES[t]).filter((d) => d != null);
|
|
1846
|
+
process.stdout.write(
|
|
1847
|
+
[
|
|
1848
|
+
"## Changed resources",
|
|
1849
|
+
...changed.map((c) => `- ${c}`),
|
|
1850
|
+
"",
|
|
1851
|
+
"## Validation findings",
|
|
1852
|
+
findings.length > 0 ? findings.map((f) => `- ${f}`).join("\n") : "- none (mechanical checks passed \u2014 still review the diff against the guides below)",
|
|
1853
|
+
"",
|
|
1854
|
+
"## Review guides",
|
|
1855
|
+
"Review `overai diff` output against these before pushing:",
|
|
1856
|
+
"",
|
|
1857
|
+
guideDocs.join("\n\n---\n\n"),
|
|
1858
|
+
""
|
|
1859
|
+
].join("\n")
|
|
1860
|
+
);
|
|
1861
|
+
};
|
|
1862
|
+
|
|
1863
|
+
// src/commands/sql.ts
|
|
1864
|
+
import { createInterface } from "readline";
|
|
1865
|
+
var runQuery = async (organizationId, tableSpaceId, query, params) => {
|
|
1866
|
+
const res = await v2Fetch(
|
|
1867
|
+
`/v2/table-spaces/${organizationId}/${tableSpaceId}/run`,
|
|
1868
|
+
{
|
|
1869
|
+
method: "POST",
|
|
1870
|
+
headers: { "Content-Type": "application/json" },
|
|
1871
|
+
body: JSON.stringify(
|
|
1872
|
+
params == null ? { query, ignoreCamelCase: true } : { unsafe: { query, params } }
|
|
1873
|
+
)
|
|
1874
|
+
}
|
|
1875
|
+
);
|
|
1876
|
+
const raw = await res.text();
|
|
1877
|
+
const body = (() => {
|
|
1878
|
+
try {
|
|
1879
|
+
return JSON.parse(raw);
|
|
1880
|
+
} catch {
|
|
1881
|
+
return null;
|
|
1882
|
+
}
|
|
1883
|
+
})();
|
|
1884
|
+
if (!res.ok || body?.ok === false) {
|
|
1885
|
+
throw new Error(
|
|
1886
|
+
body?.error ?? (raw.trim().length > 0 ? raw.trim() : res.status === 404 ? "table space not found or you lack access to it" : `HTTP ${String(res.status)}`)
|
|
1887
|
+
);
|
|
1888
|
+
}
|
|
1889
|
+
return {
|
|
1890
|
+
rows: body?.rows ?? [],
|
|
1891
|
+
rowCount: body?.rowCount ?? 0,
|
|
1892
|
+
resultSetCount: body?.resultSetCount ?? 1,
|
|
1893
|
+
durationMs: body?.durationMs ?? 0
|
|
1894
|
+
};
|
|
1895
|
+
};
|
|
1896
|
+
var summarize = (result) => `(${String(result.rowCount)} row${result.rowCount === 1 ? "" : "s"}, ${String(result.durationMs)}ms)`;
|
|
1897
|
+
var REPL_HELP = ` \\dt list tables
|
|
1898
|
+
\\d <table> describe a table's columns (schema.table to disambiguate)
|
|
1899
|
+
\\? this help
|
|
1900
|
+
\\q quit
|
|
1901
|
+
Statements run when a line ends with ";". Each statement runs in its own
|
|
1902
|
+
transaction \u2014 BEGIN/SET/temp tables don't carry across statements.
|
|
1903
|
+
`;
|
|
1904
|
+
var runRepl = async (organizationId, tableSpaceId, format) => {
|
|
1905
|
+
const interactive = process.stdin.isTTY;
|
|
1906
|
+
const rl = createInterface({
|
|
1907
|
+
input: process.stdin,
|
|
1908
|
+
...interactive ? { output: process.stdout, historySize: 1e3, terminal: true } : {}
|
|
1909
|
+
});
|
|
1910
|
+
let buffer = "";
|
|
1911
|
+
const prompt = () => {
|
|
1912
|
+
if (!interactive) return;
|
|
1913
|
+
rl.setPrompt(
|
|
1914
|
+
buffer.length === 0 ? `${tableSpaceId}=> ` : `${tableSpaceId}-> `
|
|
1915
|
+
);
|
|
1916
|
+
rl.prompt();
|
|
1917
|
+
};
|
|
1918
|
+
const execute = async (sql, params) => {
|
|
1919
|
+
try {
|
|
1920
|
+
const result = await runQuery(organizationId, tableSpaceId, sql, params);
|
|
1921
|
+
if (result.rows.length > 0) {
|
|
1922
|
+
process.stdout.write(`${formatResult(result.rows, format)}
|
|
1923
|
+
`);
|
|
1924
|
+
}
|
|
1925
|
+
if (result.resultSetCount > 1) {
|
|
1926
|
+
process.stderr.write(
|
|
1927
|
+
`(${String(result.resultSetCount)} result sets, showing the last)
|
|
1928
|
+
`
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
process.stderr.write(`${summarize(result)}
|
|
1932
|
+
`);
|
|
1933
|
+
return result;
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
process.stderr.write(
|
|
1936
|
+
`ERROR: ${error instanceof Error ? error.message : String(error)}
|
|
1937
|
+
`
|
|
1938
|
+
);
|
|
1939
|
+
if (!interactive) process.exitCode = 1;
|
|
1940
|
+
return null;
|
|
1941
|
+
}
|
|
1942
|
+
};
|
|
1943
|
+
const handleMeta = async (line) => {
|
|
1944
|
+
const [cmd, arg] = line.split(/\s+/);
|
|
1945
|
+
if (cmd === "\\q") {
|
|
1946
|
+
rl.close();
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
if (cmd === "\\?") {
|
|
1950
|
+
process.stdout.write(REPL_HELP);
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
if (cmd === "\\dt") {
|
|
1954
|
+
await execute(
|
|
1955
|
+
"SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema NOT IN ('pg_catalog', 'information_schema') ORDER BY 1, 2"
|
|
1956
|
+
);
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (cmd === "\\d" && arg != null) {
|
|
1960
|
+
const dot = arg.indexOf(".");
|
|
1961
|
+
const result = await execute(
|
|
1962
|
+
dot > 0 ? "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2 ORDER BY ordinal_position" : "SELECT table_schema, column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = $1 ORDER BY table_schema, ordinal_position",
|
|
1963
|
+
dot > 0 ? [arg.slice(0, dot), arg.slice(dot + 1)] : [arg]
|
|
1964
|
+
);
|
|
1965
|
+
if (result?.rows.length === 0) {
|
|
1966
|
+
process.stderr.write(`Did not find any relation named "${arg}".
|
|
1967
|
+
`);
|
|
1968
|
+
}
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
process.stderr.write(`Unknown command ${line}. Try \\?
|
|
1972
|
+
`);
|
|
1973
|
+
};
|
|
1974
|
+
const handleLine = async (line) => {
|
|
1975
|
+
const trimmed = line.trim();
|
|
1976
|
+
if (buffer.length === 0 && trimmed.startsWith("\\")) {
|
|
1977
|
+
await handleMeta(trimmed);
|
|
1978
|
+
} else if (buffer.length > 0 || trimmed.length > 0) {
|
|
1979
|
+
buffer += (buffer.length > 0 ? "\n" : "") + line;
|
|
1980
|
+
if (trimmed.endsWith(";")) {
|
|
1981
|
+
const sql = buffer;
|
|
1982
|
+
buffer = "";
|
|
1983
|
+
await execute(sql);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
prompt();
|
|
1987
|
+
};
|
|
1988
|
+
if (interactive) {
|
|
1989
|
+
process.stdout.write(
|
|
1990
|
+
`Connected to table space ${tableSpaceId}. \\? for help, \\q to quit.
|
|
1991
|
+
`
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
await new Promise((resolve3) => {
|
|
1995
|
+
let queue = Promise.resolve();
|
|
1996
|
+
rl.on("line", (line) => {
|
|
1997
|
+
queue = queue.then(() => handleLine(line));
|
|
1998
|
+
});
|
|
1999
|
+
rl.on("SIGINT", () => {
|
|
2000
|
+
buffer = "";
|
|
2001
|
+
process.stdout.write("\n");
|
|
2002
|
+
prompt();
|
|
2003
|
+
});
|
|
2004
|
+
rl.on("close", () => {
|
|
2005
|
+
queue = queue.then(async () => {
|
|
2006
|
+
if (buffer.trim().length > 0) await execute(buffer);
|
|
2007
|
+
});
|
|
2008
|
+
queue.then(resolve3).catch(resolve3);
|
|
2009
|
+
});
|
|
2010
|
+
prompt();
|
|
2011
|
+
});
|
|
2012
|
+
};
|
|
2013
|
+
var runSql = async (args) => {
|
|
2014
|
+
const tableSpaceId = args[0];
|
|
2015
|
+
if (tableSpaceId == null || tableSpaceId.startsWith("-")) {
|
|
2016
|
+
throw new Error(
|
|
2017
|
+
'Usage: overai sql <tableSpaceId> ["SELECT ..."] (no query \u2192 interactive session)'
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
const format = resolveFormat(flagValue(args, "-o", "--output"));
|
|
2021
|
+
const { organizationId } = await getAccessToken();
|
|
2022
|
+
const query = (() => {
|
|
2023
|
+
const rest = args.slice(1);
|
|
2024
|
+
for (let i = 0; i < rest.length; i++) {
|
|
2025
|
+
const arg = rest[i];
|
|
2026
|
+
if (arg === "-o" || arg === "--output") {
|
|
2027
|
+
i++;
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
if (arg != null && !arg.startsWith("-")) return arg;
|
|
2031
|
+
}
|
|
2032
|
+
return void 0;
|
|
2033
|
+
})();
|
|
2034
|
+
if (query == null) return runRepl(organizationId, tableSpaceId, format);
|
|
2035
|
+
const result = await runQuery(organizationId, tableSpaceId, query);
|
|
2036
|
+
printResult(result.rows, format);
|
|
2037
|
+
if (process.stderr.isTTY) {
|
|
2038
|
+
process.stderr.write(`${summarize(result)}
|
|
2039
|
+
`);
|
|
2040
|
+
}
|
|
2041
|
+
};
|
|
2042
|
+
|
|
2043
|
+
// src/commands/update.ts
|
|
2044
|
+
import { spawnSync } from "child_process";
|
|
2045
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
2046
|
+
import { fileURLToPath } from "url";
|
|
2047
|
+
var PACKAGE_NAME = "@overaihq/cli";
|
|
2048
|
+
var packageRootUrl = () => {
|
|
2049
|
+
for (const up of ["..", "../..", "../../.."]) {
|
|
2050
|
+
if (existsSync3(new URL(`${up}/package.json`, import.meta.url))) {
|
|
2051
|
+
return new URL(`${up}/`, import.meta.url);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
throw new Error("package.json not found relative to the CLI bundle");
|
|
2055
|
+
};
|
|
2056
|
+
var cliVersion = () => {
|
|
2057
|
+
const pkg = JSON.parse(
|
|
2058
|
+
readFileSync4(new URL("package.json", packageRootUrl()), "utf8")
|
|
2059
|
+
);
|
|
2060
|
+
return pkg.version;
|
|
2061
|
+
};
|
|
2062
|
+
var detectInstaller = () => {
|
|
2063
|
+
const path = fileURLToPath(import.meta.url);
|
|
2064
|
+
if (path.includes("_npx") || path.includes("dlx-")) return "ephemeral";
|
|
2065
|
+
if (path.includes(".pnpm") || /[\\/]pnpm[\\/]/.test(path)) return "pnpm";
|
|
2066
|
+
if (path.includes(".bun")) return "bun";
|
|
2067
|
+
return "npm";
|
|
2068
|
+
};
|
|
2069
|
+
var runUpdate = async () => {
|
|
2070
|
+
const current = cliVersion();
|
|
2071
|
+
if (existsSync3(new URL("src", packageRootUrl()))) {
|
|
2072
|
+
process.stdout.write(
|
|
2073
|
+
"Development build \u2014 rebuild from the repo instead of updating from npm.\n"
|
|
2074
|
+
);
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const res = await fetch(
|
|
2078
|
+
`https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`,
|
|
2079
|
+
// Bound the lookup so a slow/unreachable registry can't hang the command.
|
|
2080
|
+
{ signal: AbortSignal.timeout(1e4) }
|
|
2081
|
+
);
|
|
2082
|
+
if (!res.ok) {
|
|
2083
|
+
throw new Error(`npm registry lookup failed (HTTP ${String(res.status)})`);
|
|
2084
|
+
}
|
|
2085
|
+
const body = await res.json();
|
|
2086
|
+
const latest = typeof body.version === "string" ? body.version : null;
|
|
2087
|
+
if (latest == null) {
|
|
2088
|
+
throw new Error("npm registry returned an unexpected response");
|
|
2089
|
+
}
|
|
2090
|
+
if (latest === current) {
|
|
2091
|
+
process.stdout.write(`overai ${current} is up to date.
|
|
2092
|
+
`);
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
const cur = current.split(".").map(Number);
|
|
2096
|
+
const lat = latest.split(".").map(Number);
|
|
2097
|
+
const cmp = cur.map((n, i) => n - (lat[i] ?? 0)).find((d) => d !== 0) ?? 0;
|
|
2098
|
+
if (cmp > 0) {
|
|
2099
|
+
process.stdout.write(
|
|
2100
|
+
`overai ${current} is ahead of latest (${latest}) \u2014 likely a @next install; nothing to do.
|
|
2101
|
+
`
|
|
2102
|
+
);
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
const installer = detectInstaller();
|
|
2106
|
+
if (installer === "ephemeral") {
|
|
2107
|
+
process.stdout.write(
|
|
2108
|
+
`Running from an npx/dlx cache (current ${current}, latest ${latest}) \u2014 invoke as \`npx ${PACKAGE_NAME}@latest\` to always get the newest release.
|
|
2109
|
+
`
|
|
2110
|
+
);
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
const spec = `${PACKAGE_NAME}@${latest}`;
|
|
2114
|
+
const argsByInstaller = {
|
|
2115
|
+
npm: ["npm", "install", "-g", spec],
|
|
2116
|
+
pnpm: ["pnpm", "add", "-g", spec],
|
|
2117
|
+
bun: ["bun", "add", "-g", spec]
|
|
2118
|
+
};
|
|
2119
|
+
const [cmd, ...args] = argsByInstaller[installer];
|
|
2120
|
+
process.stdout.write(
|
|
2121
|
+
`Updating overai ${current} \u2192 ${latest} via ${installer}\u2026
|
|
2122
|
+
`
|
|
2123
|
+
);
|
|
2124
|
+
const result = spawnSync(cmd, args, {
|
|
2125
|
+
stdio: "inherit",
|
|
2126
|
+
// npm/pnpm are .cmd shims on Windows; a shell is required to resolve them.
|
|
2127
|
+
shell: process.platform === "win32"
|
|
2128
|
+
});
|
|
2129
|
+
if (result.status !== 0) {
|
|
2130
|
+
throw new Error(
|
|
2131
|
+
`${[cmd, ...args].join(" ")} failed (exit ${String(result.status ?? "?")})`
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
process.stdout.write(`Updated to ${latest}.
|
|
2135
|
+
`);
|
|
2136
|
+
};
|
|
2137
|
+
|
|
2138
|
+
// src/commands/versions.ts
|
|
2139
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2140
|
+
import { mkdirSync as mkdirSync4, mkdtempSync, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2141
|
+
import { tmpdir } from "os";
|
|
2142
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
2143
|
+
var parseRef = (raw) => {
|
|
2144
|
+
if (raw === "local" || raw === "base" || raw === "deployed" || raw === "latest") {
|
|
2145
|
+
return raw;
|
|
2146
|
+
}
|
|
2147
|
+
const n = Number(raw.replace(/^v/, ""));
|
|
2148
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
2149
|
+
throw new Error(
|
|
2150
|
+
`Bad version ref "${raw}" \u2014 use a number, local, base, deployed, or latest.`
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
return n;
|
|
2154
|
+
};
|
|
2155
|
+
var findResource = (manifest, filter) => {
|
|
2156
|
+
const matches = Object.entries(manifest.resources).filter(
|
|
2157
|
+
([id, r]) => id === filter || r.name.toLowerCase().includes(filter.toLowerCase()) || r.files.some((f) => f.includes(filter))
|
|
2158
|
+
);
|
|
2159
|
+
if (matches.length === 0) throw new Error(`Nothing matched "${filter}".`);
|
|
2160
|
+
const match = matches[0];
|
|
2161
|
+
if (match == null || matches.length > 1) {
|
|
2162
|
+
throw new Error(
|
|
2163
|
+
`"${filter}" is ambiguous:
|
|
2164
|
+
${matches.map(([id, r]) => ` ${r.name} (${id})`).join("\n")}`
|
|
2165
|
+
);
|
|
2166
|
+
}
|
|
2167
|
+
return match;
|
|
2168
|
+
};
|
|
2169
|
+
var filesAtRef = async (root, id, resource, ref) => {
|
|
2170
|
+
const local = (reader) => Object.fromEntries(
|
|
2171
|
+
resource.files.flatMap((p) => {
|
|
2172
|
+
const content = reader(root, p);
|
|
2173
|
+
return content == null ? [] : [[p, content]];
|
|
2174
|
+
})
|
|
2175
|
+
);
|
|
2176
|
+
if (ref === "local") return local(readWorkingFile);
|
|
2177
|
+
if (ref === "base") return local(readBaseFile);
|
|
2178
|
+
const adapter = adapterFor(resource.type);
|
|
2179
|
+
const versionNumber = await (async () => {
|
|
2180
|
+
if (typeof ref === "number") return ref;
|
|
2181
|
+
const head = await adapter.head(id);
|
|
2182
|
+
if (ref === "latest") return head.latestVersion;
|
|
2183
|
+
if (head.deployedVersion == null) {
|
|
2184
|
+
throw new Error(`${resource.name} has no deployed version.`);
|
|
2185
|
+
}
|
|
2186
|
+
return head.deployedVersion;
|
|
2187
|
+
})();
|
|
2188
|
+
return adapter.fetchVersion(id, versionNumber, resource.name);
|
|
2189
|
+
};
|
|
2190
|
+
var pickFile = (files, meta) => {
|
|
2191
|
+
const entry = Object.entries(files).find(
|
|
2192
|
+
([p]) => meta ? p.endsWith(".meta.json") : !p.endsWith(".meta.json")
|
|
2193
|
+
);
|
|
2194
|
+
if (entry == null) {
|
|
2195
|
+
throw new Error(meta ? "No meta file for this resource." : "No code file.");
|
|
2196
|
+
}
|
|
2197
|
+
return entry[1];
|
|
2198
|
+
};
|
|
2199
|
+
var runCat = async (args) => {
|
|
2200
|
+
const target = args.find((a) => !a.startsWith("-"));
|
|
2201
|
+
if (target == null) {
|
|
2202
|
+
throw new Error(
|
|
2203
|
+
"Usage: overai cat <filter>[@version|@deployed|@latest|@base] [--meta]"
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
const { root, manifest } = openClone();
|
|
2207
|
+
const at = target.lastIndexOf("@");
|
|
2208
|
+
const filter = at > 0 ? target.slice(0, at) : target;
|
|
2209
|
+
const ref = at > 0 ? parseRef(target.slice(at + 1)) : "local";
|
|
2210
|
+
const [id, resource] = findResource(manifest, filter);
|
|
2211
|
+
const files = await filesAtRef(root, id, resource, ref);
|
|
2212
|
+
process.stdout.write(pickFile(files, args.includes("--meta")));
|
|
2213
|
+
};
|
|
2214
|
+
var runDiff = async (args) => {
|
|
2215
|
+
const { root, manifest } = openClone();
|
|
2216
|
+
const filter = args.find((a) => !a.startsWith("-"));
|
|
2217
|
+
const from = parseRef(flagValue(args, "--from") ?? "base");
|
|
2218
|
+
const to = parseRef(flagValue(args, "--to") ?? "local");
|
|
2219
|
+
const resources = filter != null ? [findResource(manifest, filter)] : Object.entries(manifest.resources);
|
|
2220
|
+
for (const [id, resource] of resources) {
|
|
2221
|
+
if (from === "base" && to === "local") {
|
|
2222
|
+
for (const relPath of resource.files) {
|
|
2223
|
+
const missing = readWorkingFile(root, relPath) == null;
|
|
2224
|
+
spawnSync2(
|
|
2225
|
+
"git",
|
|
2226
|
+
[
|
|
2227
|
+
"diff",
|
|
2228
|
+
"--no-index",
|
|
2229
|
+
"--src-prefix=base/",
|
|
2230
|
+
"--dst-prefix=local/",
|
|
2231
|
+
join5(".overai", "base", relPath),
|
|
2232
|
+
missing ? "/dev/null" : relPath
|
|
2233
|
+
],
|
|
2234
|
+
{ stdio: "inherit", cwd: root }
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
const [fromFiles, toFiles] = await Promise.all([
|
|
2240
|
+
filesAtRef(root, id, resource, from),
|
|
2241
|
+
filesAtRef(root, id, resource, to)
|
|
2242
|
+
]);
|
|
2243
|
+
const tmp = mkdtempSync(join5(tmpdir(), "overai-diff-"));
|
|
2244
|
+
try {
|
|
2245
|
+
for (const [side, files] of [
|
|
2246
|
+
["from", fromFiles],
|
|
2247
|
+
["to", toFiles]
|
|
2248
|
+
]) {
|
|
2249
|
+
for (const [relPath, content] of Object.entries(files)) {
|
|
2250
|
+
const abs = join5(tmp, side, relPath);
|
|
2251
|
+
mkdirSync4(dirname3(abs), { recursive: true });
|
|
2252
|
+
writeFileSync3(abs, content);
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
spawnSync2(
|
|
2256
|
+
"git",
|
|
2257
|
+
[
|
|
2258
|
+
"diff",
|
|
2259
|
+
"--no-index",
|
|
2260
|
+
`--src-prefix=${String(from)}/`,
|
|
2261
|
+
`--dst-prefix=${String(to)}/`,
|
|
2262
|
+
"from",
|
|
2263
|
+
"to"
|
|
2264
|
+
],
|
|
2265
|
+
{ stdio: "inherit", cwd: tmp }
|
|
2266
|
+
);
|
|
2267
|
+
} finally {
|
|
2268
|
+
rmSync2(tmp, { recursive: true, force: true });
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
|
|
2273
|
+
// src/index.ts
|
|
2274
|
+
var HELP = `overai \u2014 OverAI command-line interface
|
|
2275
|
+
|
|
2276
|
+
Usage:
|
|
2277
|
+
overai signin Sign in via your browser
|
|
2278
|
+
overai signout Sign out and revoke this session
|
|
2279
|
+
overai whoami Show the current identity and org
|
|
2280
|
+
overai org list List your organizations
|
|
2281
|
+
overai org switch <orgId> Switch the active org
|
|
2282
|
+
overai graphql -q '<query>' | -f <file> [--v1]
|
|
2283
|
+
Run a GraphQL operation (/v2; --v1 = legacy Hasura)
|
|
2284
|
+
overai fn <functionId> [--input k=v ... | -]
|
|
2285
|
+
Invoke a function
|
|
2286
|
+
overai fn logs <functionId> [--limit N] [--execution <id>]
|
|
2287
|
+
Execution history / logs
|
|
2288
|
+
overai sql <tableSpaceId> ["<SQL>"] Run SQL against a tablespace; no query
|
|
2289
|
+
opens a psql-style session (stdin scripts work)
|
|
2290
|
+
|
|
2291
|
+
Workspace sync (edit resources as local files):
|
|
2292
|
+
overai clone <workspaceId> [dir] Materialize a workspace as files
|
|
2293
|
+
overai status Local edits + remote drift
|
|
2294
|
+
overai diff [filter] [--from <ref>] [--to <ref>]
|
|
2295
|
+
Diff versions (refs: N, base, local, deployed, latest;
|
|
2296
|
+
default base \u2192 local, i.e. your edits)
|
|
2297
|
+
overai cat <filter>[@ref] [--meta] Print a version's code to stdout
|
|
2298
|
+
(composes: diff <(overai cat x@3) <(overai cat x@5))
|
|
2299
|
+
overai pull Fast-forward clean resources to remote
|
|
2300
|
+
overai push [filter] -m <msg> [--deploy] [--force]
|
|
2301
|
+
Create new versions from local edits
|
|
2302
|
+
overai deploy [filter | --all] Point deployed versions at pushed ones
|
|
2303
|
+
overai restore <filter | --all> Discard local edits (back to base)
|
|
2304
|
+
overai open [filter | id] Open a resource (or the workspace) in the webapp
|
|
2305
|
+
overai dev <filter> [--port N] [--open]
|
|
2306
|
+
Live-preview a web page from local files
|
|
2307
|
+
overai review Validation + best-practice review packet
|
|
2308
|
+
overai best-practices [topic] Print bundled guides
|
|
2309
|
+
overai update Update the global install to the latest release
|
|
2310
|
+
overai version Print the CLI version
|
|
2311
|
+
|
|
2312
|
+
Automations:
|
|
2313
|
+
overai automation runs <automationId> [--limit N] [--failed] [--running]
|
|
2314
|
+
overai automation state <runId> [--node <variableId>]
|
|
2315
|
+
overai automation run <automationId> [--input k=v ... | -]
|
|
2316
|
+
|
|
2317
|
+
Output: table on a TTY, JSON when piped; -o json|table|csv to force.
|
|
2318
|
+
Aliases: login = signin, logout = signout.
|
|
2319
|
+
Env: OVERAI_API_URL, OVERAI_APP_URL override the target deployment.
|
|
2320
|
+
`;
|
|
2321
|
+
process.stdout.on("error", (error) => {
|
|
2322
|
+
if (error.code === "EPIPE") process.exit(0);
|
|
2323
|
+
throw error;
|
|
2324
|
+
});
|
|
2325
|
+
var run = async () => {
|
|
2326
|
+
const [command, ...rest] = process.argv.slice(2);
|
|
2327
|
+
switch (command) {
|
|
2328
|
+
case "signin":
|
|
2329
|
+
case "login":
|
|
2330
|
+
await signIn();
|
|
2331
|
+
return;
|
|
2332
|
+
case "signout":
|
|
2333
|
+
case "logout":
|
|
2334
|
+
await signOut();
|
|
2335
|
+
return;
|
|
2336
|
+
case "whoami":
|
|
2337
|
+
whoami();
|
|
2338
|
+
return;
|
|
2339
|
+
case "org":
|
|
2340
|
+
if (rest[0] === "list") {
|
|
2341
|
+
await listOrgs(rest.slice(1));
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
if (rest[0] === "switch" && rest[1] != null) {
|
|
2345
|
+
await switchOrg(rest[1]);
|
|
2346
|
+
return;
|
|
2347
|
+
}
|
|
2348
|
+
process.stderr.write("Usage: overai org list | org switch <orgId>\n");
|
|
2349
|
+
process.exitCode = 1;
|
|
2350
|
+
return;
|
|
2351
|
+
case "graphql":
|
|
2352
|
+
await runGraphql(rest);
|
|
2353
|
+
return;
|
|
2354
|
+
case "fn":
|
|
2355
|
+
await runFn(rest);
|
|
2356
|
+
return;
|
|
2357
|
+
case "sql":
|
|
2358
|
+
await runSql(rest);
|
|
2359
|
+
return;
|
|
2360
|
+
case "clone":
|
|
2361
|
+
await runClone(rest);
|
|
2362
|
+
return;
|
|
2363
|
+
case "status":
|
|
2364
|
+
await runStatus();
|
|
2365
|
+
return;
|
|
2366
|
+
case "diff":
|
|
2367
|
+
await runDiff(rest);
|
|
2368
|
+
return;
|
|
2369
|
+
case "cat":
|
|
2370
|
+
await runCat(rest);
|
|
2371
|
+
return;
|
|
2372
|
+
case "pull":
|
|
2373
|
+
await runPull();
|
|
2374
|
+
return;
|
|
2375
|
+
case "push":
|
|
2376
|
+
await runPush(rest);
|
|
2377
|
+
return;
|
|
2378
|
+
case "deploy":
|
|
2379
|
+
await runDeploy(rest);
|
|
2380
|
+
return;
|
|
2381
|
+
case "restore":
|
|
2382
|
+
runRestore(rest);
|
|
2383
|
+
return;
|
|
2384
|
+
case "open":
|
|
2385
|
+
await runOpen(rest);
|
|
2386
|
+
return;
|
|
2387
|
+
case "dev":
|
|
2388
|
+
await runDev(rest);
|
|
2389
|
+
return;
|
|
2390
|
+
case "review":
|
|
2391
|
+
await runReview();
|
|
2392
|
+
return;
|
|
2393
|
+
case "best-practices":
|
|
2394
|
+
runBestPractices(rest);
|
|
2395
|
+
return;
|
|
2396
|
+
case "automation":
|
|
2397
|
+
await runAutomation(rest);
|
|
2398
|
+
return;
|
|
2399
|
+
case "update":
|
|
2400
|
+
case "upgrade":
|
|
2401
|
+
await runUpdate();
|
|
2402
|
+
return;
|
|
2403
|
+
case "version":
|
|
2404
|
+
case "--version":
|
|
2405
|
+
case "-v":
|
|
2406
|
+
process.stdout.write(`${cliVersion()}
|
|
2407
|
+
`);
|
|
2408
|
+
return;
|
|
2409
|
+
case void 0:
|
|
2410
|
+
case "help":
|
|
2411
|
+
case "--help":
|
|
2412
|
+
case "-h":
|
|
2413
|
+
process.stdout.write(HELP);
|
|
2414
|
+
return;
|
|
2415
|
+
default:
|
|
2416
|
+
process.stderr.write(`Unknown command: ${command}
|
|
2417
|
+
|
|
2418
|
+
${HELP}`);
|
|
2419
|
+
process.exitCode = 1;
|
|
2420
|
+
}
|
|
2421
|
+
};
|
|
2422
|
+
run().catch((error) => {
|
|
2423
|
+
process.stderr.write(
|
|
2424
|
+
`${error instanceof Error ? error.message : String(error)}
|
|
2425
|
+
`
|
|
2426
|
+
);
|
|
2427
|
+
process.exitCode = 1;
|
|
2428
|
+
});
|