freestyle 0.0.3 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -0
- package/cli.mjs +1994 -0
- package/index.cjs +8200 -0
- package/index.d.cts +13846 -0
- package/index.d.mts +13846 -0
- package/index.mjs +8179 -0
- package/package.json +31 -37
- package/README.markdown +0 -57
- package/example/les_miserables.txt +0 -67492
- package/example/midsummer_nights_dream.txt +0 -3425
- package/example/pentameter.js +0 -15
- package/example/prose.js +0 -9
- package/example/qwantz.txt +0 -50
- package/example/rap.js +0 -13
- package/index.js +0 -86
package/cli.mjs
ADDED
|
@@ -0,0 +1,1994 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as dotenv from 'dotenv';
|
|
3
|
+
import yargs from 'yargs';
|
|
4
|
+
import { hideBin } from 'yargs/helpers';
|
|
5
|
+
import { Freestyle, VmSpec } from './index.mjs';
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_STACK_API_URL = "https://api.stack-auth.com";
|
|
12
|
+
const DEFAULT_STACK_APP_URL = "https://dash.freestyle.sh";
|
|
13
|
+
const DEFAULT_STACK_PROJECT_ID = "0edf478c-f123-46fb-818f-34c0024a9f35";
|
|
14
|
+
const DEFAULT_STACK_PUBLISHABLE_CLIENT_KEY = "pck_h2aft7g9pqjzrkdnzs199h1may5wjtdtdxeex7m2wzp1r";
|
|
15
|
+
const CLI_AUTH_TIMEOUT_MILLIS = 10 * 60 * 1e3;
|
|
16
|
+
const POLL_INTERVAL_MILLIS = 2e3;
|
|
17
|
+
const STACK_REFRESH_TOKEN_ENV_KEY = "FREESTYLE_STACK_REFRESH_TOKEN";
|
|
18
|
+
const STACK_SAVE_TO_DOTENV_ENV_KEY = "FREESTYLE_STACK_SAVE_TO_DOTENV";
|
|
19
|
+
function isTruthy(value) {
|
|
20
|
+
if (!value) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const normalized = value.trim().toLowerCase();
|
|
24
|
+
return normalized === "1" || normalized === "true" || normalized === "yes";
|
|
25
|
+
}
|
|
26
|
+
function loadRefreshTokenFromDotenv() {
|
|
27
|
+
const refreshToken = process.env[STACK_REFRESH_TOKEN_ENV_KEY];
|
|
28
|
+
if (!refreshToken || typeof refreshToken !== "string") {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const trimmed = refreshToken.trim();
|
|
32
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
33
|
+
}
|
|
34
|
+
function shouldSaveToDotenv(options) {
|
|
35
|
+
if (typeof options?.saveToDotenv === "boolean") {
|
|
36
|
+
return options.saveToDotenv;
|
|
37
|
+
}
|
|
38
|
+
return isTruthy(process.env[STACK_SAVE_TO_DOTENV_ENV_KEY]);
|
|
39
|
+
}
|
|
40
|
+
function persistRefreshTokenToDotenv(refreshToken, options) {
|
|
41
|
+
if (!shouldSaveToDotenv(options)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
45
|
+
const line = `${STACK_REFRESH_TOKEN_ENV_KEY}=${refreshToken}`;
|
|
46
|
+
let existing = "";
|
|
47
|
+
if (fs.existsSync(envPath)) {
|
|
48
|
+
existing = fs.readFileSync(envPath, "utf-8");
|
|
49
|
+
}
|
|
50
|
+
const pattern = new RegExp(`^${STACK_REFRESH_TOKEN_ENV_KEY}=.*$`, "m");
|
|
51
|
+
let next;
|
|
52
|
+
if (pattern.test(existing)) {
|
|
53
|
+
next = existing.replace(pattern, line);
|
|
54
|
+
} else if (existing.length === 0) {
|
|
55
|
+
next = `${line}
|
|
56
|
+
`;
|
|
57
|
+
} else if (existing.endsWith("\n")) {
|
|
58
|
+
next = `${existing}${line}
|
|
59
|
+
`;
|
|
60
|
+
} else {
|
|
61
|
+
next = `${existing}
|
|
62
|
+
${line}
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
fs.writeFileSync(envPath, next, { encoding: "utf-8" });
|
|
66
|
+
}
|
|
67
|
+
function removeRefreshTokenFromDotenv() {
|
|
68
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
69
|
+
if (!fs.existsSync(envPath)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const existing = fs.readFileSync(envPath, "utf-8");
|
|
73
|
+
const lines = existing.split(/\r?\n/);
|
|
74
|
+
const nextLines = lines.filter(
|
|
75
|
+
(line) => !line.startsWith(`${STACK_REFRESH_TOKEN_ENV_KEY}=`)
|
|
76
|
+
);
|
|
77
|
+
if (nextLines.length === lines.length) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
const next = nextLines.filter((line) => line.length > 0).join("\n");
|
|
81
|
+
fs.writeFileSync(envPath, next.length > 0 ? `${next}
|
|
82
|
+
` : "", {
|
|
83
|
+
encoding: "utf-8"
|
|
84
|
+
});
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
function walkUpDirectories(startDir) {
|
|
88
|
+
const result = [];
|
|
89
|
+
let current = path.resolve(startDir);
|
|
90
|
+
while (true) {
|
|
91
|
+
result.push(current);
|
|
92
|
+
const parent = path.dirname(current);
|
|
93
|
+
if (parent === current) {
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
current = parent;
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
function readEnvFileValue(filePath, key) {
|
|
101
|
+
if (!fs.existsSync(filePath)) {
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
105
|
+
const pattern = new RegExp(`^${key}=(.*)$`, "m");
|
|
106
|
+
const match = content.match(pattern);
|
|
107
|
+
if (!match?.[1]) {
|
|
108
|
+
return void 0;
|
|
109
|
+
}
|
|
110
|
+
return match[1].trim().replace(/^['\"]|['\"]$/g, "");
|
|
111
|
+
}
|
|
112
|
+
function readYamlEnvValue(filePath, envName) {
|
|
113
|
+
if (!fs.existsSync(filePath)) {
|
|
114
|
+
return void 0;
|
|
115
|
+
}
|
|
116
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
117
|
+
const escapedEnv = envName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
118
|
+
const pattern = new RegExp(
|
|
119
|
+
`-\\s+name:\\s+${escapedEnv}\\s*[\\r\\n]+\\s*value:\\s*([^\\r\\n#]+)`,
|
|
120
|
+
"m"
|
|
121
|
+
);
|
|
122
|
+
const match = content.match(pattern);
|
|
123
|
+
if (!match?.[1]) {
|
|
124
|
+
return void 0;
|
|
125
|
+
}
|
|
126
|
+
return match[1].trim().replace(/^['\"]|['\"]$/g, "");
|
|
127
|
+
}
|
|
128
|
+
function discoverStackConfigFromWorkspace() {
|
|
129
|
+
const discovered = {};
|
|
130
|
+
const roots = walkUpDirectories(process.cwd());
|
|
131
|
+
for (const root of roots) {
|
|
132
|
+
if (!discovered.projectId || !discovered.publishableClientKey) {
|
|
133
|
+
const dashboardEnv = path.join(root, "freestyle-dashboard", ".env.local");
|
|
134
|
+
discovered.projectId ||= readEnvFileValue(
|
|
135
|
+
dashboardEnv,
|
|
136
|
+
"VITE_STACK_PROJECT_ID"
|
|
137
|
+
);
|
|
138
|
+
discovered.publishableClientKey ||= readEnvFileValue(
|
|
139
|
+
dashboardEnv,
|
|
140
|
+
"VITE_STACK_PUBLISHABLE_CLIENT_KEY"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
if (!discovered.projectId || !discovered.publishableClientKey) {
|
|
144
|
+
const adminEnv = path.join(root, "freestyle-sandbox-admin", ".env.local");
|
|
145
|
+
discovered.projectId ||= readEnvFileValue(
|
|
146
|
+
adminEnv,
|
|
147
|
+
"NEXT_PUBLIC_STACK_PROJECT_ID"
|
|
148
|
+
);
|
|
149
|
+
discovered.publishableClientKey ||= readEnvFileValue(
|
|
150
|
+
adminEnv,
|
|
151
|
+
"NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY"
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
if (!discovered.projectId || !discovered.publishableClientKey) {
|
|
155
|
+
const dashK8s = path.join(root, "k8s", "freestyle-dash.yml");
|
|
156
|
+
discovered.projectId ||= readYamlEnvValue(
|
|
157
|
+
dashK8s,
|
|
158
|
+
"VITE_STACK_PROJECT_ID"
|
|
159
|
+
);
|
|
160
|
+
discovered.publishableClientKey ||= readYamlEnvValue(
|
|
161
|
+
dashK8s,
|
|
162
|
+
"VITE_STACK_PUBLISHABLE_CLIENT_KEY"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (discovered.projectId && discovered.publishableClientKey) {
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return discovered;
|
|
170
|
+
}
|
|
171
|
+
function resolveAuthFilePath() {
|
|
172
|
+
return process.env.FREESTYLE_STACK_AUTH_FILE ?? path.join(os.homedir(), ".freestyle", "stack-auth.json");
|
|
173
|
+
}
|
|
174
|
+
function resolveStackConfig() {
|
|
175
|
+
const discovered = discoverStackConfigFromWorkspace();
|
|
176
|
+
const projectId = process.env.FREESTYLE_STACK_PROJECT_ID ?? process.env.NEXT_PUBLIC_STACK_PROJECT_ID ?? process.env.VITE_STACK_PROJECT_ID ?? discovered.projectId ?? DEFAULT_STACK_PROJECT_ID;
|
|
177
|
+
const publishableClientKey = process.env.FREESTYLE_STACK_PUBLISHABLE_CLIENT_KEY ?? process.env.NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY ?? process.env.VITE_STACK_PUBLISHABLE_CLIENT_KEY ?? discovered.publishableClientKey ?? DEFAULT_STACK_PUBLISHABLE_CLIENT_KEY;
|
|
178
|
+
if (!projectId || !publishableClientKey) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
const stackApiUrl = (process.env.FREESTYLE_STACK_API_URL ?? DEFAULT_STACK_API_URL).replace(/\/+$/, "");
|
|
182
|
+
const appUrl = (process.env.FREESTYLE_STACK_APP_URL ?? process.env.FREESTYLE_DASHBOARD_URL ?? DEFAULT_STACK_APP_URL).replace(/\/+$/, "");
|
|
183
|
+
const authFilePath = process.env.FREESTYLE_STACK_AUTH_FILE ?? resolveAuthFilePath();
|
|
184
|
+
return {
|
|
185
|
+
stackApiUrl,
|
|
186
|
+
appUrl,
|
|
187
|
+
projectId,
|
|
188
|
+
publishableClientKey,
|
|
189
|
+
authFilePath
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function clientHeaders(config) {
|
|
193
|
+
return {
|
|
194
|
+
"Content-Type": "application/json",
|
|
195
|
+
"x-stack-project-id": config.projectId,
|
|
196
|
+
"x-stack-access-type": "client",
|
|
197
|
+
"x-stack-publishable-client-key": config.publishableClientKey
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function loadStoredAuth(config) {
|
|
201
|
+
try {
|
|
202
|
+
if (!fs.existsSync(config.authFilePath)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const auth = JSON.parse(fs.readFileSync(config.authFilePath, "utf-8"));
|
|
206
|
+
if (!auth.refreshToken || typeof auth.refreshToken !== "string") {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
refreshToken: auth.refreshToken,
|
|
211
|
+
updatedAt: typeof auth.updatedAt === "number" ? auth.updatedAt : Date.now(),
|
|
212
|
+
defaultTeamId: typeof auth.defaultTeamId === "string" ? auth.defaultTeamId : void 0
|
|
213
|
+
};
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function persistAuth(config, auth) {
|
|
219
|
+
const dirPath = path.dirname(config.authFilePath);
|
|
220
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
221
|
+
fs.writeFileSync(
|
|
222
|
+
config.authFilePath,
|
|
223
|
+
JSON.stringify(
|
|
224
|
+
{
|
|
225
|
+
refreshToken: auth.refreshToken,
|
|
226
|
+
updatedAt: auth.updatedAt,
|
|
227
|
+
defaultTeamId: auth.defaultTeamId
|
|
228
|
+
},
|
|
229
|
+
null,
|
|
230
|
+
2
|
|
231
|
+
),
|
|
232
|
+
{ encoding: "utf-8", mode: 384 }
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
function clearStoredAuth(config) {
|
|
236
|
+
try {
|
|
237
|
+
if (fs.existsSync(config.authFilePath)) {
|
|
238
|
+
fs.unlinkSync(config.authFilePath);
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
function logoutCliAuth(options) {
|
|
244
|
+
const authFilePath = resolveAuthFilePath();
|
|
245
|
+
let clearedStored = false;
|
|
246
|
+
try {
|
|
247
|
+
if (fs.existsSync(authFilePath)) {
|
|
248
|
+
fs.unlinkSync(authFilePath);
|
|
249
|
+
clearedStored = true;
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
let clearedDotenv = false;
|
|
254
|
+
if (options?.removeFromDotenv) {
|
|
255
|
+
clearedDotenv = removeRefreshTokenFromDotenv();
|
|
256
|
+
}
|
|
257
|
+
delete process.env[STACK_REFRESH_TOKEN_ENV_KEY];
|
|
258
|
+
return { clearedStored, clearedDotenv };
|
|
259
|
+
}
|
|
260
|
+
function tryOpenBrowser(url) {
|
|
261
|
+
try {
|
|
262
|
+
if (process.platform === "darwin") {
|
|
263
|
+
const child2 = spawn("open", [url], {
|
|
264
|
+
stdio: "ignore",
|
|
265
|
+
detached: true
|
|
266
|
+
});
|
|
267
|
+
child2.unref();
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
if (process.platform === "win32") {
|
|
271
|
+
const child2 = spawn("cmd", ["/c", "start", "", url], {
|
|
272
|
+
stdio: "ignore",
|
|
273
|
+
detached: true
|
|
274
|
+
});
|
|
275
|
+
child2.unref();
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
const child = spawn("xdg-open", [url], {
|
|
279
|
+
stdio: "ignore",
|
|
280
|
+
detached: true
|
|
281
|
+
});
|
|
282
|
+
child.unref();
|
|
283
|
+
return true;
|
|
284
|
+
} catch {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
async function startCliLogin(config) {
|
|
289
|
+
const initResponse = await fetch(`${config.stackApiUrl}/api/v1/auth/cli`, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
headers: clientHeaders(config),
|
|
292
|
+
body: JSON.stringify({
|
|
293
|
+
expires_in_millis: CLI_AUTH_TIMEOUT_MILLIS
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
if (!initResponse.ok) {
|
|
297
|
+
const errorText = await initResponse.text();
|
|
298
|
+
throw new Error(
|
|
299
|
+
`Failed to start authentication login (${initResponse.status}). ${errorText || "Check project ID and client key configuration."}`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const initData = await initResponse.json();
|
|
303
|
+
if (!initData.polling_code || !initData.login_code) {
|
|
304
|
+
throw new Error("Authentication login did not return polling/login codes.");
|
|
305
|
+
}
|
|
306
|
+
const loginUrl = `${config.appUrl}/handler/cli-auth-confirm?login_code=${encodeURIComponent(initData.login_code)}`;
|
|
307
|
+
console.log("\nAuthentication is required.");
|
|
308
|
+
console.log(`Open this URL to continue:
|
|
309
|
+
${loginUrl}
|
|
310
|
+
`);
|
|
311
|
+
const opened = tryOpenBrowser(loginUrl);
|
|
312
|
+
if (opened) {
|
|
313
|
+
console.log("Opened your browser for authentication...");
|
|
314
|
+
} else {
|
|
315
|
+
console.log("Could not open browser automatically. Open the URL manually.");
|
|
316
|
+
}
|
|
317
|
+
const deadline = Date.now() + CLI_AUTH_TIMEOUT_MILLIS;
|
|
318
|
+
while (Date.now() < deadline) {
|
|
319
|
+
const pollResponse = await fetch(
|
|
320
|
+
`${config.stackApiUrl}/api/v1/auth/cli/poll`,
|
|
321
|
+
{
|
|
322
|
+
method: "POST",
|
|
323
|
+
headers: clientHeaders(config),
|
|
324
|
+
body: JSON.stringify({
|
|
325
|
+
polling_code: initData.polling_code
|
|
326
|
+
})
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
if (![200, 201].includes(pollResponse.status)) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Failed while polling authentication login (${pollResponse.status}).`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
const pollData = await pollResponse.json();
|
|
335
|
+
if (pollData.status && pollData.status !== "pending") {
|
|
336
|
+
console.log("Auth poll status:", pollData.status);
|
|
337
|
+
}
|
|
338
|
+
if (pollData.status === "completed" || pollData.status === "success") {
|
|
339
|
+
if (!pollData.refresh_token) {
|
|
340
|
+
throw new Error("Login completed without a refresh token response.");
|
|
341
|
+
}
|
|
342
|
+
return pollData.refresh_token;
|
|
343
|
+
}
|
|
344
|
+
if (pollData.status && pollData.status !== "pending" && pollData.status !== "waiting") {
|
|
345
|
+
throw new Error(
|
|
346
|
+
pollData.error || `Authentication ${pollData.status}. Please retry.`
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MILLIS));
|
|
350
|
+
}
|
|
351
|
+
throw new Error("Timed out waiting for authentication.");
|
|
352
|
+
}
|
|
353
|
+
async function refreshStackAccessToken(config, refreshToken) {
|
|
354
|
+
const response = await fetch(
|
|
355
|
+
`${config.stackApiUrl}/api/v1/auth/sessions/current/refresh`,
|
|
356
|
+
{
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: {
|
|
359
|
+
...clientHeaders(config),
|
|
360
|
+
"x-stack-refresh-token": refreshToken
|
|
361
|
+
},
|
|
362
|
+
body: "{}"
|
|
363
|
+
}
|
|
364
|
+
);
|
|
365
|
+
if (!response.ok) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
const data = await response.json();
|
|
369
|
+
if (!data.access_token) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
accessToken: data.access_token,
|
|
374
|
+
refreshToken: data.refresh_token
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
async function getStackAccessTokenForCli(options) {
|
|
378
|
+
const config = resolveStackConfig();
|
|
379
|
+
if (!config) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
let refreshTokenFromEnv = loadRefreshTokenFromDotenv();
|
|
383
|
+
const stored = loadStoredAuth(config);
|
|
384
|
+
if (options?.forceRelogin) {
|
|
385
|
+
refreshTokenFromEnv = null;
|
|
386
|
+
clearStoredAuth(config);
|
|
387
|
+
}
|
|
388
|
+
let refreshToken = refreshTokenFromEnv ?? stored?.refreshToken;
|
|
389
|
+
if (!refreshToken) {
|
|
390
|
+
refreshToken = await startCliLogin(config);
|
|
391
|
+
const auth = {
|
|
392
|
+
refreshToken,
|
|
393
|
+
updatedAt: Date.now()
|
|
394
|
+
};
|
|
395
|
+
persistAuth(config, auth);
|
|
396
|
+
persistRefreshTokenToDotenv(refreshToken, options);
|
|
397
|
+
}
|
|
398
|
+
let refreshed = await refreshStackAccessToken(config, refreshToken);
|
|
399
|
+
if (!refreshed) {
|
|
400
|
+
if (!refreshTokenFromEnv) {
|
|
401
|
+
clearStoredAuth(config);
|
|
402
|
+
}
|
|
403
|
+
refreshToken = await startCliLogin(config);
|
|
404
|
+
const auth = {
|
|
405
|
+
refreshToken,
|
|
406
|
+
updatedAt: Date.now(),
|
|
407
|
+
defaultTeamId: stored?.defaultTeamId
|
|
408
|
+
};
|
|
409
|
+
persistAuth(config, auth);
|
|
410
|
+
persistRefreshTokenToDotenv(refreshToken, options);
|
|
411
|
+
refreshed = await refreshStackAccessToken(config, refreshToken);
|
|
412
|
+
}
|
|
413
|
+
if (!refreshed) {
|
|
414
|
+
throw new Error("Failed to authenticate.");
|
|
415
|
+
}
|
|
416
|
+
if (refreshed.refreshToken && refreshed.refreshToken !== refreshToken) {
|
|
417
|
+
const auth = {
|
|
418
|
+
refreshToken: refreshed.refreshToken,
|
|
419
|
+
updatedAt: Date.now(),
|
|
420
|
+
defaultTeamId: stored?.defaultTeamId
|
|
421
|
+
};
|
|
422
|
+
persistAuth(config, auth);
|
|
423
|
+
persistRefreshTokenToDotenv(refreshed.refreshToken, options);
|
|
424
|
+
}
|
|
425
|
+
return refreshed.accessToken;
|
|
426
|
+
}
|
|
427
|
+
function getDashboardApiUrl() {
|
|
428
|
+
return process.env.FREESTYLE_DASHBOARD_URL || "https://dash.freestyle.sh";
|
|
429
|
+
}
|
|
430
|
+
async function callDashboardApi(endpoint, accessToken, body) {
|
|
431
|
+
const response = await fetch(`${getDashboardApiUrl()}${endpoint}`, {
|
|
432
|
+
method: "POST",
|
|
433
|
+
headers: {
|
|
434
|
+
"Content-Type": "application/json"
|
|
435
|
+
},
|
|
436
|
+
body: JSON.stringify({
|
|
437
|
+
data: {
|
|
438
|
+
accessToken,
|
|
439
|
+
...body
|
|
440
|
+
}
|
|
441
|
+
})
|
|
442
|
+
});
|
|
443
|
+
if (!response.ok) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
`Dashboard API call failed: ${response.status} ${response.statusText}`
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
return response.json();
|
|
449
|
+
}
|
|
450
|
+
async function getTeamsForCli() {
|
|
451
|
+
const config = resolveStackConfig();
|
|
452
|
+
if (!config) {
|
|
453
|
+
throw new Error(
|
|
454
|
+
"Stack Auth is not configured. Please check your environment variables."
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
const stored = loadStoredAuth(config);
|
|
458
|
+
if (!stored?.refreshToken) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
"No authentication found. Please run 'npx freestyle-sandboxes@latest login' first."
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
const tokenResponse = await refreshStackAccessToken(
|
|
464
|
+
config,
|
|
465
|
+
stored.refreshToken
|
|
466
|
+
);
|
|
467
|
+
if (!tokenResponse) {
|
|
468
|
+
throw new Error("Failed to refresh access token.");
|
|
469
|
+
}
|
|
470
|
+
const teams = await callDashboardApi("/api/cli/teams", tokenResponse.accessToken);
|
|
471
|
+
return teams;
|
|
472
|
+
}
|
|
473
|
+
async function setDefaultTeam(teamId) {
|
|
474
|
+
const config = resolveStackConfig();
|
|
475
|
+
if (!config) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
"Stack Auth is not configured. Please check your environment variables."
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
const stored = loadStoredAuth(config);
|
|
481
|
+
if (!stored?.refreshToken) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
"No authentication found. Please run 'npx freestyle-sandboxes@latest login' first."
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
const auth = {
|
|
487
|
+
refreshToken: stored.refreshToken,
|
|
488
|
+
updatedAt: Date.now(),
|
|
489
|
+
defaultTeamId: teamId
|
|
490
|
+
};
|
|
491
|
+
persistAuth(config, auth);
|
|
492
|
+
}
|
|
493
|
+
function getDefaultTeamId() {
|
|
494
|
+
const config = resolveStackConfig();
|
|
495
|
+
if (!config) {
|
|
496
|
+
return void 0;
|
|
497
|
+
}
|
|
498
|
+
const stored = loadStoredAuth(config);
|
|
499
|
+
return stored?.defaultTeamId;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function normalizeCliProxyErrorWithStatus(errorText, status) {
|
|
503
|
+
const fallbackCode = status === 400 ? "BAD_REQUEST" : status === 401 ? "UNAUTHORIZED_ERROR" : status === 403 ? "FORBIDDEN" : "INTERNAL_ERROR";
|
|
504
|
+
try {
|
|
505
|
+
const parsed = JSON.parse(errorText);
|
|
506
|
+
if (typeof parsed.code === "string" && typeof parsed.message === "string") {
|
|
507
|
+
return {
|
|
508
|
+
body: JSON.stringify(parsed),
|
|
509
|
+
contentType: "application/json"
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
const message2 = [parsed.error, parsed.message, parsed.reason].find(
|
|
513
|
+
(value) => typeof value === "string" && value.length > 0
|
|
514
|
+
);
|
|
515
|
+
if (message2) {
|
|
516
|
+
const normalized2 = fallbackCode === "UNAUTHORIZED_ERROR" ? {
|
|
517
|
+
code: fallbackCode,
|
|
518
|
+
message: message2,
|
|
519
|
+
route: "/api/proxy/request",
|
|
520
|
+
reason: message2
|
|
521
|
+
} : {
|
|
522
|
+
code: fallbackCode,
|
|
523
|
+
message: message2
|
|
524
|
+
};
|
|
525
|
+
return {
|
|
526
|
+
body: JSON.stringify(normalized2),
|
|
527
|
+
contentType: "application/json"
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
}
|
|
532
|
+
const message = errorText || "Request failed";
|
|
533
|
+
const normalized = fallbackCode === "UNAUTHORIZED_ERROR" ? {
|
|
534
|
+
code: fallbackCode,
|
|
535
|
+
message,
|
|
536
|
+
route: "/api/proxy/request",
|
|
537
|
+
reason: message
|
|
538
|
+
} : {
|
|
539
|
+
code: fallbackCode,
|
|
540
|
+
message
|
|
541
|
+
};
|
|
542
|
+
return {
|
|
543
|
+
body: JSON.stringify(normalized),
|
|
544
|
+
contentType: "application/json"
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function createProxyFetch(accessToken, teamId) {
|
|
548
|
+
const dashboardApiUrl = process.env.FREESTYLE_DASHBOARD_URL || "https://dash.freestyle.sh";
|
|
549
|
+
return async (url, init) => {
|
|
550
|
+
const urlObj = typeof url === "string" ? new URL(url) : url instanceof URL ? url : new URL(url.url);
|
|
551
|
+
const path2 = urlObj.pathname + urlObj.search;
|
|
552
|
+
const proxyResponse = await fetch(`${dashboardApiUrl}/api/proxy/request`, {
|
|
553
|
+
method: "POST",
|
|
554
|
+
headers: {
|
|
555
|
+
"Content-Type": "application/json"
|
|
556
|
+
},
|
|
557
|
+
body: JSON.stringify({
|
|
558
|
+
data: {
|
|
559
|
+
accessToken,
|
|
560
|
+
teamId,
|
|
561
|
+
path: path2.startsWith("/") ? path2.substring(1) : path2,
|
|
562
|
+
method: init?.method || "GET",
|
|
563
|
+
headers: init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : {},
|
|
564
|
+
body: init?.body ? init.body.toString() : void 0
|
|
565
|
+
}
|
|
566
|
+
})
|
|
567
|
+
});
|
|
568
|
+
if (!proxyResponse.ok) {
|
|
569
|
+
const errorText = await proxyResponse.text();
|
|
570
|
+
const normalizedError = normalizeCliProxyErrorWithStatus(
|
|
571
|
+
errorText,
|
|
572
|
+
proxyResponse.status
|
|
573
|
+
);
|
|
574
|
+
return new Response(normalizedError.body, {
|
|
575
|
+
status: proxyResponse.status,
|
|
576
|
+
statusText: proxyResponse.statusText,
|
|
577
|
+
headers: {
|
|
578
|
+
"Content-Type": normalizedError.contentType
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
const data = await proxyResponse.json();
|
|
583
|
+
return new Response(JSON.stringify(data), {
|
|
584
|
+
status: 200,
|
|
585
|
+
headers: { "Content-Type": "application/json" }
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
async function getFreestyleClient(teamId) {
|
|
590
|
+
const directApiKey = process.env.FREESTYLE_API_KEY;
|
|
591
|
+
if (directApiKey) {
|
|
592
|
+
const baseUrl2 = process.env.FREESTYLE_API_URL;
|
|
593
|
+
return new Freestyle({ apiKey: directApiKey, baseUrl: baseUrl2 });
|
|
594
|
+
}
|
|
595
|
+
const accessToken = await getStackAccessTokenForCli();
|
|
596
|
+
if (!accessToken) {
|
|
597
|
+
console.error(
|
|
598
|
+
"Error: No API key found. Please run 'npx freestyle-sandboxes@latest login' or set FREESTYLE_API_KEY in your .env file."
|
|
599
|
+
);
|
|
600
|
+
process.exit(1);
|
|
601
|
+
}
|
|
602
|
+
const resolvedTeamId = process.env.FREESTYLE_TEAM_ID ?? getDefaultTeamId();
|
|
603
|
+
if (!resolvedTeamId) {
|
|
604
|
+
console.error(
|
|
605
|
+
"Error: No team selected. Please run 'npx freestyle-sandboxes@latest login' to set a default team."
|
|
606
|
+
);
|
|
607
|
+
process.exit(1);
|
|
608
|
+
}
|
|
609
|
+
const baseUrl = process.env.FREESTYLE_API_URL || "https://api.freestyle.sh";
|
|
610
|
+
return new Freestyle({
|
|
611
|
+
apiKey: "placeholder",
|
|
612
|
+
// Need something to pass validation
|
|
613
|
+
baseUrl,
|
|
614
|
+
fetch: createProxyFetch(accessToken, resolvedTeamId)
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
function handleError(error) {
|
|
618
|
+
if (error.response) {
|
|
619
|
+
console.error("API Error:", error.response.data);
|
|
620
|
+
} else if (error.message) {
|
|
621
|
+
console.error("Error:", error.message);
|
|
622
|
+
} else {
|
|
623
|
+
console.error("Error:", error);
|
|
624
|
+
}
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
function loadEnv() {
|
|
628
|
+
const envPath = path.join(process.cwd(), ".env");
|
|
629
|
+
if (fs.existsSync(envPath)) {
|
|
630
|
+
dotenv.config({ path: envPath, quiet: true });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function formatTable(headers, rows) {
|
|
634
|
+
const colWidths = headers.map((h, i) => {
|
|
635
|
+
const maxRowWidth = Math.max(...rows.map((r) => (r[i] || "").length));
|
|
636
|
+
return Math.max(h.length, maxRowWidth);
|
|
637
|
+
});
|
|
638
|
+
const headerRow = headers.map((h, i) => h.padEnd(colWidths[i] || 0)).join(" ");
|
|
639
|
+
const separator = colWidths.map((w) => "-".repeat(w)).join(" ");
|
|
640
|
+
console.log(headerRow);
|
|
641
|
+
console.log(separator);
|
|
642
|
+
rows.forEach((row) => {
|
|
643
|
+
console.log(
|
|
644
|
+
row.map((cell, i) => (cell || "").padEnd(colWidths[i] || 0)).join(" ")
|
|
645
|
+
);
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async function sshIntoVm(vmId, options = {}) {
|
|
650
|
+
const freestyle = await getFreestyleClient();
|
|
651
|
+
console.log("Setting up SSH connection...");
|
|
652
|
+
const { identity, identityId } = await freestyle.identities.create();
|
|
653
|
+
console.log(`Created identity: ${identityId}`);
|
|
654
|
+
await identity.permissions.vms.grant({ vmId });
|
|
655
|
+
const { token, tokenId } = await identity.tokens.create();
|
|
656
|
+
const sshCommand = `ssh ${vmId}:${token}@vm-ssh.freestyle.sh -p 22`;
|
|
657
|
+
console.log(`Connecting to VM ${vmId}...`);
|
|
658
|
+
console.log(`Command: ${sshCommand}
|
|
659
|
+
`);
|
|
660
|
+
return new Promise((resolve, reject) => {
|
|
661
|
+
const sshProcess = spawn(sshCommand, {
|
|
662
|
+
shell: true,
|
|
663
|
+
stdio: "inherit"
|
|
664
|
+
});
|
|
665
|
+
sshProcess.on("close", async (code) => {
|
|
666
|
+
console.log("\nSSH session ended.");
|
|
667
|
+
try {
|
|
668
|
+
console.log("Cleaning up identity and token...");
|
|
669
|
+
await identity.tokens.revoke({ tokenId });
|
|
670
|
+
await freestyle.identities.delete({ identityId });
|
|
671
|
+
console.log("\u2713 Cleanup complete");
|
|
672
|
+
if (options.deleteOnExit) {
|
|
673
|
+
console.log(`Deleting VM ${vmId}...`);
|
|
674
|
+
await freestyle.vms.delete({ vmId });
|
|
675
|
+
console.log("\u2713 VM deleted");
|
|
676
|
+
}
|
|
677
|
+
resolve();
|
|
678
|
+
} catch (error) {
|
|
679
|
+
console.error("Error during cleanup:", error);
|
|
680
|
+
reject(error);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
sshProcess.on("error", (error) => {
|
|
684
|
+
console.error("Error starting SSH:", error);
|
|
685
|
+
reject(error);
|
|
686
|
+
});
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
const vmCommand = {
|
|
690
|
+
command: "vm <action>",
|
|
691
|
+
describe: "Manage Virtual Machines",
|
|
692
|
+
builder: (yargs) => {
|
|
693
|
+
return yargs.command(
|
|
694
|
+
"create",
|
|
695
|
+
"Create a new VM",
|
|
696
|
+
(yargs2) => {
|
|
697
|
+
return yargs2.option("name", {
|
|
698
|
+
alias: "n",
|
|
699
|
+
type: "string",
|
|
700
|
+
description: "VM name/discriminator"
|
|
701
|
+
}).option("domain", {
|
|
702
|
+
alias: "d",
|
|
703
|
+
type: "string",
|
|
704
|
+
description: "Custom domain to attach"
|
|
705
|
+
}).option("port", {
|
|
706
|
+
alias: "p",
|
|
707
|
+
type: "number",
|
|
708
|
+
description: "VM port to expose (default: 3000)",
|
|
709
|
+
default: 3e3
|
|
710
|
+
}).option("apt", {
|
|
711
|
+
type: "array",
|
|
712
|
+
description: "APT packages to install",
|
|
713
|
+
default: []
|
|
714
|
+
}).option("snapshot", {
|
|
715
|
+
alias: "s",
|
|
716
|
+
type: "string",
|
|
717
|
+
description: "Snapshot ID to create VM from"
|
|
718
|
+
}).option("exec", {
|
|
719
|
+
alias: "e",
|
|
720
|
+
type: "string",
|
|
721
|
+
description: "Execute a command on the VM after creation"
|
|
722
|
+
}).option("ssh", {
|
|
723
|
+
type: "boolean",
|
|
724
|
+
description: "SSH into VM after creation and delete VM on exit (for debugging)",
|
|
725
|
+
default: false
|
|
726
|
+
}).option("delete", {
|
|
727
|
+
type: "boolean",
|
|
728
|
+
description: "Delete VM after exec completes or when SSH session ends",
|
|
729
|
+
default: false
|
|
730
|
+
}).option("json", {
|
|
731
|
+
type: "boolean",
|
|
732
|
+
description: "Output as JSON",
|
|
733
|
+
default: false
|
|
734
|
+
});
|
|
735
|
+
},
|
|
736
|
+
async (argv) => {
|
|
737
|
+
loadEnv();
|
|
738
|
+
const args = argv;
|
|
739
|
+
try {
|
|
740
|
+
const freestyle = await getFreestyleClient();
|
|
741
|
+
let createOptions = {};
|
|
742
|
+
if (args.snapshot) {
|
|
743
|
+
createOptions.snapshotId = args.snapshot;
|
|
744
|
+
} else {
|
|
745
|
+
const spec = new VmSpec({
|
|
746
|
+
discriminator: args.name,
|
|
747
|
+
aptDeps: args.apt
|
|
748
|
+
});
|
|
749
|
+
createOptions.snapshot = spec;
|
|
750
|
+
}
|
|
751
|
+
if (args.domain) {
|
|
752
|
+
createOptions.domains = [
|
|
753
|
+
{
|
|
754
|
+
domain: args.domain,
|
|
755
|
+
vmPort: args.port
|
|
756
|
+
}
|
|
757
|
+
];
|
|
758
|
+
}
|
|
759
|
+
console.log("Creating VM...");
|
|
760
|
+
const result = await freestyle.vms.create(createOptions);
|
|
761
|
+
let execResult;
|
|
762
|
+
if (args.exec) {
|
|
763
|
+
const vm = freestyle.vms.ref({ vmId: result.vmId });
|
|
764
|
+
console.log(`Executing command on VM ${result.vmId}...`);
|
|
765
|
+
execResult = await vm.exec({
|
|
766
|
+
command: args.exec
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
if (args.json && !args.ssh) {
|
|
770
|
+
if (execResult) {
|
|
771
|
+
console.log(
|
|
772
|
+
JSON.stringify(
|
|
773
|
+
{
|
|
774
|
+
vm: result,
|
|
775
|
+
exec: execResult
|
|
776
|
+
},
|
|
777
|
+
null,
|
|
778
|
+
2
|
|
779
|
+
)
|
|
780
|
+
);
|
|
781
|
+
} else {
|
|
782
|
+
console.log(JSON.stringify(result, null, 2));
|
|
783
|
+
}
|
|
784
|
+
} else {
|
|
785
|
+
console.log("\n\u2713 VM created successfully!");
|
|
786
|
+
console.log(` VM ID: ${result.vmId}`);
|
|
787
|
+
const domainStr = result.domains?.[0];
|
|
788
|
+
if (domainStr) {
|
|
789
|
+
console.log(` Domain: https://${domainStr}`);
|
|
790
|
+
}
|
|
791
|
+
if (execResult) {
|
|
792
|
+
if (execResult.stdout) {
|
|
793
|
+
console.log("\nExec output:");
|
|
794
|
+
console.log(execResult.stdout);
|
|
795
|
+
}
|
|
796
|
+
if (execResult.stderr) {
|
|
797
|
+
console.error("\nExec errors:");
|
|
798
|
+
console.error(execResult.stderr);
|
|
799
|
+
}
|
|
800
|
+
console.log(`
|
|
801
|
+
Exec exit code: ${execResult.statusCode || 0}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
if (args.ssh) {
|
|
805
|
+
console.log("");
|
|
806
|
+
await sshIntoVm(result.vmId, { deleteOnExit: args.delete });
|
|
807
|
+
} else if (args.delete) {
|
|
808
|
+
console.log(`Deleting VM ${result.vmId}...`);
|
|
809
|
+
await freestyle.vms.delete({ vmId: result.vmId });
|
|
810
|
+
console.log("\u2713 VM deleted");
|
|
811
|
+
}
|
|
812
|
+
} catch (error) {
|
|
813
|
+
handleError(error);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
).command(
|
|
817
|
+
"list",
|
|
818
|
+
"List all VMs",
|
|
819
|
+
(yargs2) => {
|
|
820
|
+
return yargs2.option("json", {
|
|
821
|
+
type: "boolean",
|
|
822
|
+
description: "Output as JSON",
|
|
823
|
+
default: false
|
|
824
|
+
});
|
|
825
|
+
},
|
|
826
|
+
async (argv) => {
|
|
827
|
+
loadEnv();
|
|
828
|
+
const args = argv;
|
|
829
|
+
try {
|
|
830
|
+
const freestyle = await getFreestyleClient();
|
|
831
|
+
const vms = await freestyle.vms.list();
|
|
832
|
+
if (args.json) {
|
|
833
|
+
console.log(JSON.stringify(vms, null, 2));
|
|
834
|
+
} else {
|
|
835
|
+
if (vms.vms.length === 0) {
|
|
836
|
+
console.log("No VMs found.");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const rows = vms.vms.map((vm) => [
|
|
840
|
+
vm.id,
|
|
841
|
+
vm.state || "unknown",
|
|
842
|
+
vm.createdAt ? new Date(vm.createdAt).toLocaleString() : "N/A"
|
|
843
|
+
]);
|
|
844
|
+
formatTable(["VM ID", "Status", "Created"], rows);
|
|
845
|
+
}
|
|
846
|
+
} catch (error) {
|
|
847
|
+
handleError(error);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
).command(
|
|
851
|
+
"ssh <vmId>",
|
|
852
|
+
"SSH into a VM",
|
|
853
|
+
(yargs2) => {
|
|
854
|
+
return yargs2.positional("vmId", {
|
|
855
|
+
type: "string",
|
|
856
|
+
description: "VM ID to SSH into",
|
|
857
|
+
demandOption: true
|
|
858
|
+
}).option("delete", {
|
|
859
|
+
type: "boolean",
|
|
860
|
+
description: "Delete VM when SSH session ends",
|
|
861
|
+
default: false
|
|
862
|
+
});
|
|
863
|
+
},
|
|
864
|
+
async (argv) => {
|
|
865
|
+
loadEnv();
|
|
866
|
+
const args = argv;
|
|
867
|
+
try {
|
|
868
|
+
await sshIntoVm(args.vmId, {
|
|
869
|
+
deleteOnExit: args.delete
|
|
870
|
+
});
|
|
871
|
+
} catch (error) {
|
|
872
|
+
handleError(error);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
).command(
|
|
876
|
+
"exec <vmId> <command>",
|
|
877
|
+
"Execute a command on a VM",
|
|
878
|
+
(yargs2) => {
|
|
879
|
+
return yargs2.positional("vmId", {
|
|
880
|
+
type: "string",
|
|
881
|
+
description: "VM ID",
|
|
882
|
+
demandOption: true
|
|
883
|
+
}).positional("command", {
|
|
884
|
+
type: "string",
|
|
885
|
+
description: "Command to execute",
|
|
886
|
+
demandOption: true
|
|
887
|
+
}).option("json", {
|
|
888
|
+
type: "boolean",
|
|
889
|
+
description: "Output as JSON",
|
|
890
|
+
default: false
|
|
891
|
+
});
|
|
892
|
+
},
|
|
893
|
+
async (argv) => {
|
|
894
|
+
loadEnv();
|
|
895
|
+
const args = argv;
|
|
896
|
+
try {
|
|
897
|
+
const freestyle = await getFreestyleClient();
|
|
898
|
+
const vm = freestyle.vms.ref({ vmId: args.vmId });
|
|
899
|
+
console.log(`Executing command on VM ${args.vmId}...`);
|
|
900
|
+
const result = await vm.exec({
|
|
901
|
+
command: args.command
|
|
902
|
+
});
|
|
903
|
+
if (args.json) {
|
|
904
|
+
console.log(JSON.stringify(result, null, 2));
|
|
905
|
+
} else {
|
|
906
|
+
if (result.stdout) {
|
|
907
|
+
console.log("\nOutput:");
|
|
908
|
+
console.log(result.stdout);
|
|
909
|
+
}
|
|
910
|
+
if (result.stderr) {
|
|
911
|
+
console.error("\nErrors:");
|
|
912
|
+
console.error(result.stderr);
|
|
913
|
+
}
|
|
914
|
+
console.log(`
|
|
915
|
+
Exit code: ${result.statusCode || 0}`);
|
|
916
|
+
}
|
|
917
|
+
} catch (error) {
|
|
918
|
+
handleError(error);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
).command(
|
|
922
|
+
"delete <vmId>",
|
|
923
|
+
"Delete a VM",
|
|
924
|
+
(yargs2) => {
|
|
925
|
+
return yargs2.positional("vmId", {
|
|
926
|
+
type: "string",
|
|
927
|
+
description: "VM ID to delete",
|
|
928
|
+
demandOption: true
|
|
929
|
+
});
|
|
930
|
+
},
|
|
931
|
+
async (argv) => {
|
|
932
|
+
loadEnv();
|
|
933
|
+
const args = argv;
|
|
934
|
+
try {
|
|
935
|
+
const freestyle = await getFreestyleClient();
|
|
936
|
+
console.log(`Deleting VM ${args.vmId}...`);
|
|
937
|
+
await freestyle.vms.delete({ vmId: args.vmId });
|
|
938
|
+
console.log("\u2713 VM deleted successfully!");
|
|
939
|
+
} catch (error) {
|
|
940
|
+
handleError(error);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
).demandCommand(1, "You need to specify a vm action");
|
|
944
|
+
},
|
|
945
|
+
handler: () => {
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
const deployCommand = {
|
|
950
|
+
command: "deploy",
|
|
951
|
+
describe: "Deploy a serverless function",
|
|
952
|
+
builder: (yargs) => {
|
|
953
|
+
return yargs.option("code", {
|
|
954
|
+
alias: "c",
|
|
955
|
+
type: "string",
|
|
956
|
+
description: "Inline code to deploy"
|
|
957
|
+
}).option("file", {
|
|
958
|
+
alias: "f",
|
|
959
|
+
type: "string",
|
|
960
|
+
description: "File path containing code to deploy"
|
|
961
|
+
}).option("repo", {
|
|
962
|
+
alias: "r",
|
|
963
|
+
type: "string",
|
|
964
|
+
description: "Git repository ID to deploy"
|
|
965
|
+
}).option("env", {
|
|
966
|
+
alias: "e",
|
|
967
|
+
type: "array",
|
|
968
|
+
description: "Environment variables (KEY=VALUE)",
|
|
969
|
+
default: []
|
|
970
|
+
}).option("json", {
|
|
971
|
+
type: "boolean",
|
|
972
|
+
description: "Output as JSON",
|
|
973
|
+
default: false
|
|
974
|
+
}).check((argv) => {
|
|
975
|
+
const hasCode = !!argv.code;
|
|
976
|
+
const hasFile = !!argv.file;
|
|
977
|
+
const hasRepo = !!argv.repo;
|
|
978
|
+
if (!hasCode && !hasFile && !hasRepo) {
|
|
979
|
+
throw new Error("You must specify one of --code, --file, or --repo");
|
|
980
|
+
}
|
|
981
|
+
if ([hasCode, hasFile, hasRepo].filter(Boolean).length > 1) {
|
|
982
|
+
throw new Error(
|
|
983
|
+
"You can only specify one of --code, --file, or --repo"
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
return true;
|
|
987
|
+
});
|
|
988
|
+
},
|
|
989
|
+
handler: async (argv) => {
|
|
990
|
+
loadEnv();
|
|
991
|
+
const args = argv;
|
|
992
|
+
try {
|
|
993
|
+
const freestyle = await getFreestyleClient();
|
|
994
|
+
let code;
|
|
995
|
+
let repo;
|
|
996
|
+
if (args.code) {
|
|
997
|
+
code = args.code;
|
|
998
|
+
} else if (args.file) {
|
|
999
|
+
code = fs.readFileSync(args.file, "utf-8");
|
|
1000
|
+
} else if (args.repo) {
|
|
1001
|
+
repo = args.repo;
|
|
1002
|
+
}
|
|
1003
|
+
const env = {};
|
|
1004
|
+
if (args.env) {
|
|
1005
|
+
for (const envVar of args.env) {
|
|
1006
|
+
const [key, ...valueParts] = envVar.split("=");
|
|
1007
|
+
if (key) {
|
|
1008
|
+
env[key] = valueParts.join("=");
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
console.log("Creating deployment...");
|
|
1013
|
+
const result = await freestyle.serverless.deployments.create({
|
|
1014
|
+
...code ? { code } : {},
|
|
1015
|
+
...repo ? { repo } : {},
|
|
1016
|
+
env: Object.keys(env).length > 0 ? env : void 0
|
|
1017
|
+
});
|
|
1018
|
+
if (args.json) {
|
|
1019
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1020
|
+
} else {
|
|
1021
|
+
console.log("\n\u2713 Deployment created successfully!");
|
|
1022
|
+
console.log(` Deployment ID: ${result.deploymentId}`);
|
|
1023
|
+
if (result.url) {
|
|
1024
|
+
console.log(` URL: ${result.url}`);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
handleError(error);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const runCommand = {
|
|
1034
|
+
command: "run",
|
|
1035
|
+
describe: "Execute a one-off serverless function",
|
|
1036
|
+
builder: (yargs) => {
|
|
1037
|
+
return yargs.option("code", {
|
|
1038
|
+
alias: "c",
|
|
1039
|
+
type: "string",
|
|
1040
|
+
description: "Inline code to execute"
|
|
1041
|
+
}).option("file", {
|
|
1042
|
+
alias: "f",
|
|
1043
|
+
type: "string",
|
|
1044
|
+
description: "File path containing code to execute"
|
|
1045
|
+
}).option("env", {
|
|
1046
|
+
alias: "e",
|
|
1047
|
+
type: "array",
|
|
1048
|
+
description: "Environment variables (KEY=VALUE)",
|
|
1049
|
+
default: []
|
|
1050
|
+
}).option("json", {
|
|
1051
|
+
type: "boolean",
|
|
1052
|
+
description: "Output as JSON",
|
|
1053
|
+
default: false
|
|
1054
|
+
}).check((argv) => {
|
|
1055
|
+
const hasCode = !!argv.code;
|
|
1056
|
+
const hasFile = !!argv.file;
|
|
1057
|
+
if (!hasCode && !hasFile) {
|
|
1058
|
+
throw new Error("You must specify either --code or --file");
|
|
1059
|
+
}
|
|
1060
|
+
if (hasCode && hasFile) {
|
|
1061
|
+
throw new Error("You can only specify one of --code or --file");
|
|
1062
|
+
}
|
|
1063
|
+
return true;
|
|
1064
|
+
});
|
|
1065
|
+
},
|
|
1066
|
+
handler: async (argv) => {
|
|
1067
|
+
loadEnv();
|
|
1068
|
+
const args = argv;
|
|
1069
|
+
try {
|
|
1070
|
+
const freestyle = await getFreestyleClient();
|
|
1071
|
+
let code;
|
|
1072
|
+
if (args.code) {
|
|
1073
|
+
code = args.code;
|
|
1074
|
+
} else if (args.file) {
|
|
1075
|
+
code = fs.readFileSync(args.file, "utf-8");
|
|
1076
|
+
} else {
|
|
1077
|
+
throw new Error("Code is required");
|
|
1078
|
+
}
|
|
1079
|
+
const env = {};
|
|
1080
|
+
if (args.env) {
|
|
1081
|
+
for (const envVar of args.env) {
|
|
1082
|
+
const [key, ...valueParts] = envVar.split("=");
|
|
1083
|
+
if (key) {
|
|
1084
|
+
env[key] = valueParts.join("=");
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
console.log("Executing serverless function...");
|
|
1089
|
+
const result = await freestyle.serverless.runs.create({
|
|
1090
|
+
code,
|
|
1091
|
+
env: Object.keys(env).length > 0 ? env : void 0
|
|
1092
|
+
});
|
|
1093
|
+
if (args.json) {
|
|
1094
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1095
|
+
} else {
|
|
1096
|
+
console.log("\n\u2713 Function executed successfully!");
|
|
1097
|
+
console.log(` Run ID: ${result.runId}`);
|
|
1098
|
+
if (result.output) {
|
|
1099
|
+
console.log(` Output: ${result.output}`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
} catch (error) {
|
|
1103
|
+
handleError(error);
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
const gitCommand = {
|
|
1109
|
+
command: "git <action>",
|
|
1110
|
+
describe: "Manage Git repositories",
|
|
1111
|
+
builder: (yargs) => {
|
|
1112
|
+
return yargs.command(
|
|
1113
|
+
"create",
|
|
1114
|
+
"Create a git repository",
|
|
1115
|
+
(yargs2) => {
|
|
1116
|
+
return yargs2.option("name", {
|
|
1117
|
+
alias: "n",
|
|
1118
|
+
type: "string",
|
|
1119
|
+
description: "Internal repository name"
|
|
1120
|
+
}).option("public", {
|
|
1121
|
+
type: "boolean",
|
|
1122
|
+
description: "Create as public repository",
|
|
1123
|
+
default: false
|
|
1124
|
+
}).option("default-branch", {
|
|
1125
|
+
type: "string",
|
|
1126
|
+
description: "Default branch name"
|
|
1127
|
+
}).option("source-url", {
|
|
1128
|
+
type: "string",
|
|
1129
|
+
description: "Fork/import from existing git URL"
|
|
1130
|
+
}).option("source-rev", {
|
|
1131
|
+
type: "string",
|
|
1132
|
+
description: "Revision (branch/tag/sha) for source URL"
|
|
1133
|
+
}).option("json", {
|
|
1134
|
+
type: "boolean",
|
|
1135
|
+
description: "Output as JSON",
|
|
1136
|
+
default: false
|
|
1137
|
+
});
|
|
1138
|
+
},
|
|
1139
|
+
async (argv) => {
|
|
1140
|
+
loadEnv();
|
|
1141
|
+
const args = argv;
|
|
1142
|
+
try {
|
|
1143
|
+
const freestyle = await getFreestyleClient();
|
|
1144
|
+
const body = {
|
|
1145
|
+
public: args.public
|
|
1146
|
+
};
|
|
1147
|
+
if (args.name) body.name = args.name;
|
|
1148
|
+
if (args.defaultBranch) body.defaultBranch = args.defaultBranch;
|
|
1149
|
+
if (args.sourceUrl) {
|
|
1150
|
+
body.source = {
|
|
1151
|
+
url: args.sourceUrl,
|
|
1152
|
+
...args.sourceRev ? { rev: args.sourceRev } : {}
|
|
1153
|
+
};
|
|
1154
|
+
}
|
|
1155
|
+
console.log("Creating git repository...");
|
|
1156
|
+
const result = await freestyle.git.repos.create(body);
|
|
1157
|
+
if (args.json) {
|
|
1158
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1159
|
+
} else {
|
|
1160
|
+
console.log("\n\u2713 Repository created successfully!");
|
|
1161
|
+
console.log(` Repo ID: ${result.repoId}`);
|
|
1162
|
+
console.log(
|
|
1163
|
+
` Clone URL: https://git.freestyle.sh/${result.repoId}`
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
} catch (error) {
|
|
1167
|
+
handleError(error);
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
).command(
|
|
1171
|
+
"list",
|
|
1172
|
+
"List git repositories",
|
|
1173
|
+
(yargs2) => {
|
|
1174
|
+
return yargs2.option("limit", {
|
|
1175
|
+
type: "number",
|
|
1176
|
+
description: "Maximum repositories to return",
|
|
1177
|
+
default: 20
|
|
1178
|
+
}).option("cursor", {
|
|
1179
|
+
type: "string",
|
|
1180
|
+
description: "Offset cursor"
|
|
1181
|
+
}).option("json", {
|
|
1182
|
+
type: "boolean",
|
|
1183
|
+
description: "Output as JSON",
|
|
1184
|
+
default: false
|
|
1185
|
+
});
|
|
1186
|
+
},
|
|
1187
|
+
async (argv) => {
|
|
1188
|
+
loadEnv();
|
|
1189
|
+
const args = argv;
|
|
1190
|
+
try {
|
|
1191
|
+
const freestyle = await getFreestyleClient();
|
|
1192
|
+
const repos = await freestyle.git.repos.list({
|
|
1193
|
+
limit: args.limit,
|
|
1194
|
+
cursor: args.cursor
|
|
1195
|
+
});
|
|
1196
|
+
if (args.json) {
|
|
1197
|
+
console.log(JSON.stringify(repos, null, 2));
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
if (!repos.repositories || repos.repositories.length === 0) {
|
|
1201
|
+
console.log("No repositories found.");
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const rows = repos.repositories.map((repo, idx) => {
|
|
1205
|
+
const repoId = repo.repoId || repo.id || "N/A";
|
|
1206
|
+
const branchCount = Object.keys(repo.branches || {}).length;
|
|
1207
|
+
const tagCount = Object.keys(repo.tags || {}).length;
|
|
1208
|
+
return [
|
|
1209
|
+
String(idx + 1),
|
|
1210
|
+
repoId,
|
|
1211
|
+
repo.defaultBranch || "main",
|
|
1212
|
+
String(branchCount),
|
|
1213
|
+
String(tagCount)
|
|
1214
|
+
];
|
|
1215
|
+
});
|
|
1216
|
+
formatTable(
|
|
1217
|
+
["#", "Repo ID", "Default Branch", "Branches", "Tags"],
|
|
1218
|
+
rows
|
|
1219
|
+
);
|
|
1220
|
+
console.log(`
|
|
1221
|
+
Total: ${repos.total}`);
|
|
1222
|
+
console.log(`Offset: ${repos.offset}`);
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
handleError(error);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
).command(
|
|
1228
|
+
"delete <repoId>",
|
|
1229
|
+
"Delete a git repository",
|
|
1230
|
+
(yargs2) => {
|
|
1231
|
+
return yargs2.positional("repoId", {
|
|
1232
|
+
type: "string",
|
|
1233
|
+
description: "Repository ID",
|
|
1234
|
+
demandOption: true
|
|
1235
|
+
});
|
|
1236
|
+
},
|
|
1237
|
+
async (argv) => {
|
|
1238
|
+
loadEnv();
|
|
1239
|
+
const args = argv;
|
|
1240
|
+
try {
|
|
1241
|
+
const freestyle = await getFreestyleClient();
|
|
1242
|
+
console.log(`Deleting repository ${args.repoId}...`);
|
|
1243
|
+
await freestyle.git.repos.delete({ repoId: args.repoId });
|
|
1244
|
+
console.log("\u2713 Repository deleted");
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
handleError(error);
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
).demandCommand(1, "You need to specify a git action");
|
|
1250
|
+
},
|
|
1251
|
+
handler: () => {
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
|
|
1255
|
+
const domainsCommand = {
|
|
1256
|
+
command: "domains <action>",
|
|
1257
|
+
describe: "Manage domains, verifications, and mappings",
|
|
1258
|
+
builder: (yargs) => {
|
|
1259
|
+
return yargs.command(
|
|
1260
|
+
"list",
|
|
1261
|
+
"List verified domains",
|
|
1262
|
+
(yargs2) => {
|
|
1263
|
+
return yargs2.option("limit", {
|
|
1264
|
+
type: "number",
|
|
1265
|
+
description: "Maximum domains to return",
|
|
1266
|
+
default: 50
|
|
1267
|
+
}).option("cursor", {
|
|
1268
|
+
type: "string",
|
|
1269
|
+
description: "Offset cursor"
|
|
1270
|
+
}).option("json", {
|
|
1271
|
+
type: "boolean",
|
|
1272
|
+
description: "Output as JSON",
|
|
1273
|
+
default: false
|
|
1274
|
+
});
|
|
1275
|
+
},
|
|
1276
|
+
async (argv) => {
|
|
1277
|
+
loadEnv();
|
|
1278
|
+
const args = argv;
|
|
1279
|
+
try {
|
|
1280
|
+
const freestyle = await getFreestyleClient();
|
|
1281
|
+
const domains = await freestyle.domains.list({
|
|
1282
|
+
limit: args.limit,
|
|
1283
|
+
cursor: args.cursor
|
|
1284
|
+
});
|
|
1285
|
+
if (args.json) {
|
|
1286
|
+
console.log(JSON.stringify(domains, null, 2));
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
if (domains.length === 0) {
|
|
1290
|
+
console.log("No verified domains found.");
|
|
1291
|
+
return;
|
|
1292
|
+
}
|
|
1293
|
+
const rows = domains.map((domain) => [
|
|
1294
|
+
domain.domain,
|
|
1295
|
+
domain.verifiedDns ? "yes" : "no",
|
|
1296
|
+
domain.createdAt ? new Date(domain.createdAt).toLocaleString() : "N/A"
|
|
1297
|
+
]);
|
|
1298
|
+
formatTable(["Domain", "DNS Verified", "Created"], rows);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
handleError(error);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
).command(
|
|
1304
|
+
"verify <domain>",
|
|
1305
|
+
"Create a domain verification request",
|
|
1306
|
+
(yargs2) => {
|
|
1307
|
+
return yargs2.positional("domain", {
|
|
1308
|
+
type: "string",
|
|
1309
|
+
description: "Domain to verify",
|
|
1310
|
+
demandOption: true
|
|
1311
|
+
}).option("json", {
|
|
1312
|
+
type: "boolean",
|
|
1313
|
+
description: "Output as JSON",
|
|
1314
|
+
default: false
|
|
1315
|
+
});
|
|
1316
|
+
},
|
|
1317
|
+
async (argv) => {
|
|
1318
|
+
loadEnv();
|
|
1319
|
+
const args = argv;
|
|
1320
|
+
try {
|
|
1321
|
+
const freestyle = await getFreestyleClient();
|
|
1322
|
+
const result = await freestyle.domains.verifications.create({
|
|
1323
|
+
domain: args.domain
|
|
1324
|
+
});
|
|
1325
|
+
if (args.json) {
|
|
1326
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
console.log("\n\u2713 Verification created!");
|
|
1330
|
+
console.log(` Verification ID: ${result.verificationId}`);
|
|
1331
|
+
console.log("\nAdd this DNS record:");
|
|
1332
|
+
console.log(` Type: ${result.record.type}`);
|
|
1333
|
+
console.log(` Name: ${result.record.name}`);
|
|
1334
|
+
console.log(` Value: ${result.record.value}`);
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
handleError(error);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
).command(
|
|
1340
|
+
"complete",
|
|
1341
|
+
"Complete a domain verification",
|
|
1342
|
+
(yargs2) => {
|
|
1343
|
+
return yargs2.option("domain", {
|
|
1344
|
+
type: "string",
|
|
1345
|
+
description: "Domain to complete verification for"
|
|
1346
|
+
}).option("verification-id", {
|
|
1347
|
+
type: "string",
|
|
1348
|
+
description: "Verification ID to complete"
|
|
1349
|
+
}).option("json", {
|
|
1350
|
+
type: "boolean",
|
|
1351
|
+
description: "Output as JSON",
|
|
1352
|
+
default: false
|
|
1353
|
+
}).check((argv) => {
|
|
1354
|
+
const hasDomain = !!argv.domain;
|
|
1355
|
+
const hasVerificationId = !!argv["verification-id"];
|
|
1356
|
+
if (!hasDomain && !hasVerificationId) {
|
|
1357
|
+
throw new Error("Specify one of --domain or --verification-id");
|
|
1358
|
+
}
|
|
1359
|
+
if (hasDomain && hasVerificationId) {
|
|
1360
|
+
throw new Error(
|
|
1361
|
+
"Specify only one of --domain or --verification-id"
|
|
1362
|
+
);
|
|
1363
|
+
}
|
|
1364
|
+
return true;
|
|
1365
|
+
});
|
|
1366
|
+
},
|
|
1367
|
+
async (argv) => {
|
|
1368
|
+
loadEnv();
|
|
1369
|
+
const args = argv;
|
|
1370
|
+
try {
|
|
1371
|
+
const freestyle = await getFreestyleClient();
|
|
1372
|
+
const result = args.verificationId ? await freestyle.domains.verifications.complete({
|
|
1373
|
+
verificationId: args.verificationId
|
|
1374
|
+
}) : await freestyle.domains.verifications.complete({
|
|
1375
|
+
domain: args.domain
|
|
1376
|
+
});
|
|
1377
|
+
if (args.json) {
|
|
1378
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1379
|
+
} else {
|
|
1380
|
+
console.log("\u2713 Domain verification completed");
|
|
1381
|
+
console.log(` Domain: ${result.domain}`);
|
|
1382
|
+
}
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
handleError(error);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
).command(
|
|
1388
|
+
"verifications",
|
|
1389
|
+
"List pending/verifiable domain verifications",
|
|
1390
|
+
(yargs2) => {
|
|
1391
|
+
return yargs2.option("json", {
|
|
1392
|
+
type: "boolean",
|
|
1393
|
+
description: "Output as JSON",
|
|
1394
|
+
default: false
|
|
1395
|
+
});
|
|
1396
|
+
},
|
|
1397
|
+
async (argv) => {
|
|
1398
|
+
loadEnv();
|
|
1399
|
+
const args = argv;
|
|
1400
|
+
try {
|
|
1401
|
+
const freestyle = await getFreestyleClient();
|
|
1402
|
+
const verifications = await freestyle.domains.verifications.list();
|
|
1403
|
+
if (args.json) {
|
|
1404
|
+
console.log(JSON.stringify(verifications, null, 2));
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (verifications.length === 0) {
|
|
1408
|
+
console.log("No verifications found.");
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
const rows = verifications.map((verification) => [
|
|
1412
|
+
verification.domain,
|
|
1413
|
+
verification.verificationCode,
|
|
1414
|
+
new Date(verification.createdAt).toLocaleString()
|
|
1415
|
+
]);
|
|
1416
|
+
formatTable(["Domain", "Verification Code", "Created"], rows);
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
handleError(error);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
).command(
|
|
1422
|
+
"map <domain>",
|
|
1423
|
+
"Create a domain mapping",
|
|
1424
|
+
(yargs2) => {
|
|
1425
|
+
return yargs2.positional("domain", {
|
|
1426
|
+
type: "string",
|
|
1427
|
+
description: "Domain to map",
|
|
1428
|
+
demandOption: true
|
|
1429
|
+
}).option("deployment-id", {
|
|
1430
|
+
type: "string",
|
|
1431
|
+
description: "Deployment ID target"
|
|
1432
|
+
}).option("vm-id", {
|
|
1433
|
+
type: "string",
|
|
1434
|
+
description: "VM ID target"
|
|
1435
|
+
}).option("vm-port", {
|
|
1436
|
+
type: "number",
|
|
1437
|
+
description: "VM port target (required with --vm-id)"
|
|
1438
|
+
}).option("json", {
|
|
1439
|
+
type: "boolean",
|
|
1440
|
+
description: "Output as JSON",
|
|
1441
|
+
default: false
|
|
1442
|
+
}).check((argv) => {
|
|
1443
|
+
const hasDeploymentId = !!argv["deployment-id"];
|
|
1444
|
+
const hasVmId = !!argv["vm-id"];
|
|
1445
|
+
const hasVmPort = typeof argv["vm-port"] === "number";
|
|
1446
|
+
if (hasDeploymentId && hasVmId) {
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
"Specify either --deployment-id or --vm-id (not both)"
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
if (!hasDeploymentId && !hasVmId) {
|
|
1452
|
+
throw new Error("Specify one of --deployment-id or --vm-id");
|
|
1453
|
+
}
|
|
1454
|
+
if (hasVmId && !hasVmPort) {
|
|
1455
|
+
throw new Error("--vm-port is required when using --vm-id");
|
|
1456
|
+
}
|
|
1457
|
+
return true;
|
|
1458
|
+
});
|
|
1459
|
+
},
|
|
1460
|
+
async (argv) => {
|
|
1461
|
+
loadEnv();
|
|
1462
|
+
const args = argv;
|
|
1463
|
+
try {
|
|
1464
|
+
const freestyle = await getFreestyleClient();
|
|
1465
|
+
const result = args.deploymentId ? await freestyle.domains.mappings.create({
|
|
1466
|
+
domain: args.domain,
|
|
1467
|
+
deploymentId: args.deploymentId
|
|
1468
|
+
}) : await freestyle.domains.mappings.create({
|
|
1469
|
+
domain: args.domain,
|
|
1470
|
+
vmId: args.vmId,
|
|
1471
|
+
vmPort: args.vmPort
|
|
1472
|
+
});
|
|
1473
|
+
if (args.json) {
|
|
1474
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1475
|
+
} else {
|
|
1476
|
+
console.log("\u2713 Domain mapping created");
|
|
1477
|
+
console.log(` Domain: ${result.domain}`);
|
|
1478
|
+
if (result.deploymentId) {
|
|
1479
|
+
console.log(` Deployment ID: ${result.deploymentId}`);
|
|
1480
|
+
}
|
|
1481
|
+
if (result.vmId) {
|
|
1482
|
+
console.log(` VM ID: ${result.vmId}`);
|
|
1483
|
+
}
|
|
1484
|
+
if (result.vmPort) {
|
|
1485
|
+
console.log(` VM Port: ${result.vmPort}`);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
} catch (error) {
|
|
1489
|
+
handleError(error);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
).command(
|
|
1493
|
+
"unmap <domain>",
|
|
1494
|
+
"Delete a domain mapping",
|
|
1495
|
+
(yargs2) => {
|
|
1496
|
+
return yargs2.positional("domain", {
|
|
1497
|
+
type: "string",
|
|
1498
|
+
description: "Domain to unmap",
|
|
1499
|
+
demandOption: true
|
|
1500
|
+
});
|
|
1501
|
+
},
|
|
1502
|
+
async (argv) => {
|
|
1503
|
+
loadEnv();
|
|
1504
|
+
const args = argv;
|
|
1505
|
+
try {
|
|
1506
|
+
const freestyle = await getFreestyleClient();
|
|
1507
|
+
await freestyle.domains.mappings.delete({ domain: args.domain });
|
|
1508
|
+
console.log("\u2713 Domain mapping deleted");
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
handleError(error);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
).command(
|
|
1514
|
+
"mappings",
|
|
1515
|
+
"List domain mappings",
|
|
1516
|
+
(yargs2) => {
|
|
1517
|
+
return yargs2.option("domain", {
|
|
1518
|
+
type: "string",
|
|
1519
|
+
description: "Filter by domain"
|
|
1520
|
+
}).option("limit", {
|
|
1521
|
+
type: "number",
|
|
1522
|
+
description: "Maximum mappings to return",
|
|
1523
|
+
default: 50
|
|
1524
|
+
}).option("cursor", {
|
|
1525
|
+
type: "string",
|
|
1526
|
+
description: "Offset cursor"
|
|
1527
|
+
}).option("json", {
|
|
1528
|
+
type: "boolean",
|
|
1529
|
+
description: "Output as JSON",
|
|
1530
|
+
default: false
|
|
1531
|
+
});
|
|
1532
|
+
},
|
|
1533
|
+
async (argv) => {
|
|
1534
|
+
loadEnv();
|
|
1535
|
+
const args = argv;
|
|
1536
|
+
try {
|
|
1537
|
+
const freestyle = await getFreestyleClient();
|
|
1538
|
+
const { mappings } = await freestyle.domains.mappings.list({
|
|
1539
|
+
domain: args.domain,
|
|
1540
|
+
limit: args.limit,
|
|
1541
|
+
cursor: args.cursor
|
|
1542
|
+
});
|
|
1543
|
+
if (args.json) {
|
|
1544
|
+
console.log(JSON.stringify(mappings, null, 2));
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
if (mappings.length === 0) {
|
|
1548
|
+
console.log("No domain mappings found.");
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
const rows = mappings.map((mapping) => [
|
|
1552
|
+
mapping.domain,
|
|
1553
|
+
mapping.deploymentId || "-",
|
|
1554
|
+
mapping.vmId || "-",
|
|
1555
|
+
mapping.vmPort != null ? String(mapping.vmPort) : "-",
|
|
1556
|
+
new Date(mapping.createdAt).toLocaleString()
|
|
1557
|
+
]);
|
|
1558
|
+
formatTable(
|
|
1559
|
+
["Domain", "Deployment", "VM", "VM Port", "Created"],
|
|
1560
|
+
rows
|
|
1561
|
+
);
|
|
1562
|
+
} catch (error) {
|
|
1563
|
+
handleError(error);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
).demandCommand(1, "You need to specify a domains action");
|
|
1567
|
+
},
|
|
1568
|
+
handler: () => {
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
|
|
1572
|
+
async function getCronJobById(scheduleId) {
|
|
1573
|
+
const freestyle = await getFreestyleClient();
|
|
1574
|
+
const { jobs } = await freestyle.cron.list();
|
|
1575
|
+
const job = jobs.find((candidate) => candidate.schedule.id === scheduleId);
|
|
1576
|
+
if (!job) {
|
|
1577
|
+
throw new Error(`Cron schedule not found: ${scheduleId}`);
|
|
1578
|
+
}
|
|
1579
|
+
return job;
|
|
1580
|
+
}
|
|
1581
|
+
const cronCommand = {
|
|
1582
|
+
command: "cron <action>",
|
|
1583
|
+
describe: "Manage cron schedules for deployments",
|
|
1584
|
+
builder: (yargs) => {
|
|
1585
|
+
return yargs.command(
|
|
1586
|
+
"schedule",
|
|
1587
|
+
"Create a cron schedule",
|
|
1588
|
+
(yargs2) => {
|
|
1589
|
+
return yargs2.option("deployment-id", {
|
|
1590
|
+
type: "string",
|
|
1591
|
+
description: "Deployment ID to schedule",
|
|
1592
|
+
demandOption: true
|
|
1593
|
+
}).option("cron", {
|
|
1594
|
+
type: "string",
|
|
1595
|
+
description: "Cron expression",
|
|
1596
|
+
demandOption: true
|
|
1597
|
+
}).option("timezone", {
|
|
1598
|
+
type: "string",
|
|
1599
|
+
description: "Timezone (default: UTC)",
|
|
1600
|
+
default: "UTC"
|
|
1601
|
+
}).option("payload", {
|
|
1602
|
+
type: "string",
|
|
1603
|
+
description: "JSON payload string passed to scheduled handler"
|
|
1604
|
+
}).option("path", {
|
|
1605
|
+
type: "string",
|
|
1606
|
+
description: "Optional path for scheduled trigger"
|
|
1607
|
+
}).option("json", {
|
|
1608
|
+
type: "boolean",
|
|
1609
|
+
description: "Output as JSON",
|
|
1610
|
+
default: false
|
|
1611
|
+
});
|
|
1612
|
+
},
|
|
1613
|
+
async (argv) => {
|
|
1614
|
+
loadEnv();
|
|
1615
|
+
const args = argv;
|
|
1616
|
+
try {
|
|
1617
|
+
const freestyle = await getFreestyleClient();
|
|
1618
|
+
let parsedPayload = {};
|
|
1619
|
+
if (args.payload) {
|
|
1620
|
+
try {
|
|
1621
|
+
parsedPayload = JSON.parse(args.payload);
|
|
1622
|
+
} catch {
|
|
1623
|
+
throw new Error("--payload must be valid JSON");
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
const { job } = await freestyle.cron.schedule({
|
|
1627
|
+
deploymentId: args.deploymentId,
|
|
1628
|
+
cron: args.cron,
|
|
1629
|
+
timezone: args.timezone,
|
|
1630
|
+
payload: parsedPayload,
|
|
1631
|
+
path: args.path
|
|
1632
|
+
});
|
|
1633
|
+
if (args.json) {
|
|
1634
|
+
console.log(JSON.stringify(job.schedule, null, 2));
|
|
1635
|
+
} else {
|
|
1636
|
+
console.log("\u2713 Cron schedule created");
|
|
1637
|
+
console.log(` Schedule ID: ${job.schedule.id}`);
|
|
1638
|
+
console.log(` Deployment ID: ${job.schedule.deploymentId}`);
|
|
1639
|
+
console.log(` Cron: ${job.schedule.cron}`);
|
|
1640
|
+
console.log(` Timezone: ${job.schedule.timezone}`);
|
|
1641
|
+
console.log(` Active: ${job.schedule.active ? "yes" : "no"}`);
|
|
1642
|
+
}
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
handleError(error);
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
).command(
|
|
1648
|
+
"list",
|
|
1649
|
+
"List cron schedules",
|
|
1650
|
+
(yargs2) => {
|
|
1651
|
+
return yargs2.option("deployment-id", {
|
|
1652
|
+
type: "string",
|
|
1653
|
+
description: "Filter by deployment ID"
|
|
1654
|
+
}).option("json", {
|
|
1655
|
+
type: "boolean",
|
|
1656
|
+
description: "Output as JSON",
|
|
1657
|
+
default: false
|
|
1658
|
+
});
|
|
1659
|
+
},
|
|
1660
|
+
async (argv) => {
|
|
1661
|
+
loadEnv();
|
|
1662
|
+
const args = argv;
|
|
1663
|
+
try {
|
|
1664
|
+
const freestyle = await getFreestyleClient();
|
|
1665
|
+
const { jobs } = await freestyle.cron.list({
|
|
1666
|
+
deploymentId: args.deploymentId
|
|
1667
|
+
});
|
|
1668
|
+
if (args.json) {
|
|
1669
|
+
console.log(
|
|
1670
|
+
JSON.stringify(
|
|
1671
|
+
jobs.map((job) => job.schedule),
|
|
1672
|
+
null,
|
|
1673
|
+
2
|
|
1674
|
+
)
|
|
1675
|
+
);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
if (jobs.length === 0) {
|
|
1679
|
+
console.log("No cron schedules found.");
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const rows = jobs.map((job) => [
|
|
1683
|
+
job.schedule.id,
|
|
1684
|
+
job.schedule.deploymentId,
|
|
1685
|
+
job.schedule.cron,
|
|
1686
|
+
job.schedule.timezone,
|
|
1687
|
+
job.schedule.active ? "active" : "disabled"
|
|
1688
|
+
]);
|
|
1689
|
+
formatTable(
|
|
1690
|
+
["Schedule ID", "Deployment", "Cron", "Timezone", "Status"],
|
|
1691
|
+
rows
|
|
1692
|
+
);
|
|
1693
|
+
} catch (error) {
|
|
1694
|
+
handleError(error);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
).command(
|
|
1698
|
+
"enable <scheduleId>",
|
|
1699
|
+
"Enable a cron schedule",
|
|
1700
|
+
(yargs2) => {
|
|
1701
|
+
return yargs2.positional("scheduleId", {
|
|
1702
|
+
type: "string",
|
|
1703
|
+
description: "Schedule ID",
|
|
1704
|
+
demandOption: true
|
|
1705
|
+
});
|
|
1706
|
+
},
|
|
1707
|
+
async (argv) => {
|
|
1708
|
+
loadEnv();
|
|
1709
|
+
const args = argv;
|
|
1710
|
+
try {
|
|
1711
|
+
const job = await getCronJobById(args.scheduleId);
|
|
1712
|
+
await job.enable();
|
|
1713
|
+
console.log(`\u2713 Enabled schedule ${args.scheduleId}`);
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
handleError(error);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
).command(
|
|
1719
|
+
"disable <scheduleId>",
|
|
1720
|
+
"Disable a cron schedule",
|
|
1721
|
+
(yargs2) => {
|
|
1722
|
+
return yargs2.positional("scheduleId", {
|
|
1723
|
+
type: "string",
|
|
1724
|
+
description: "Schedule ID",
|
|
1725
|
+
demandOption: true
|
|
1726
|
+
});
|
|
1727
|
+
},
|
|
1728
|
+
async (argv) => {
|
|
1729
|
+
loadEnv();
|
|
1730
|
+
const args = argv;
|
|
1731
|
+
try {
|
|
1732
|
+
const job = await getCronJobById(args.scheduleId);
|
|
1733
|
+
await job.disable();
|
|
1734
|
+
console.log(`\u2713 Disabled schedule ${args.scheduleId}`);
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
handleError(error);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
).command(
|
|
1740
|
+
"executions <scheduleId>",
|
|
1741
|
+
"List executions for a cron schedule",
|
|
1742
|
+
(yargs2) => {
|
|
1743
|
+
return yargs2.positional("scheduleId", {
|
|
1744
|
+
type: "string",
|
|
1745
|
+
description: "Schedule ID",
|
|
1746
|
+
demandOption: true
|
|
1747
|
+
}).option("limit", {
|
|
1748
|
+
type: "number",
|
|
1749
|
+
description: "Maximum executions to return",
|
|
1750
|
+
default: 20
|
|
1751
|
+
}).option("cursor", {
|
|
1752
|
+
type: "string",
|
|
1753
|
+
description: "Offset cursor"
|
|
1754
|
+
}).option("json", {
|
|
1755
|
+
type: "boolean",
|
|
1756
|
+
description: "Output as JSON",
|
|
1757
|
+
default: false
|
|
1758
|
+
});
|
|
1759
|
+
},
|
|
1760
|
+
async (argv) => {
|
|
1761
|
+
loadEnv();
|
|
1762
|
+
const args = argv;
|
|
1763
|
+
try {
|
|
1764
|
+
const job = await getCronJobById(args.scheduleId);
|
|
1765
|
+
const result = await job.executions({
|
|
1766
|
+
limit: args.limit,
|
|
1767
|
+
cursor: args.cursor
|
|
1768
|
+
});
|
|
1769
|
+
if (args.json) {
|
|
1770
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1771
|
+
return;
|
|
1772
|
+
}
|
|
1773
|
+
if (result.executions.length === 0) {
|
|
1774
|
+
console.log("No executions found.");
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
const rows = result.executions.map((execution) => [
|
|
1778
|
+
execution.id,
|
|
1779
|
+
execution.status,
|
|
1780
|
+
execution.attempts.toString(),
|
|
1781
|
+
execution.runAt,
|
|
1782
|
+
execution.lastError || "-"
|
|
1783
|
+
]);
|
|
1784
|
+
formatTable(
|
|
1785
|
+
["Execution ID", "Status", "Attempts", "Run At", "Last Error"],
|
|
1786
|
+
rows
|
|
1787
|
+
);
|
|
1788
|
+
const byStatus = result.executions.reduce(
|
|
1789
|
+
(acc, execution) => {
|
|
1790
|
+
acc[execution.status] = (acc[execution.status] || 0) + 1;
|
|
1791
|
+
return acc;
|
|
1792
|
+
},
|
|
1793
|
+
{}
|
|
1794
|
+
);
|
|
1795
|
+
console.log("\nSummary:");
|
|
1796
|
+
console.log(` queued: ${byStatus.queued || 0}`);
|
|
1797
|
+
console.log(` running: ${byStatus.running || 0}`);
|
|
1798
|
+
console.log(` succeeded: ${byStatus.succeeded || 0}`);
|
|
1799
|
+
console.log(` failed: ${byStatus.failed || 0}`);
|
|
1800
|
+
console.log(` retry: ${byStatus.retry || 0}`);
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
handleError(error);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
).command(
|
|
1806
|
+
"success-rate <scheduleId>",
|
|
1807
|
+
"Get success rate for a cron schedule over a time range",
|
|
1808
|
+
(yargs2) => {
|
|
1809
|
+
return yargs2.positional("scheduleId", {
|
|
1810
|
+
type: "string",
|
|
1811
|
+
description: "Schedule ID",
|
|
1812
|
+
demandOption: true
|
|
1813
|
+
}).option("start", {
|
|
1814
|
+
type: "string",
|
|
1815
|
+
description: "Range start (ISO datetime)",
|
|
1816
|
+
demandOption: true
|
|
1817
|
+
}).option("end", {
|
|
1818
|
+
type: "string",
|
|
1819
|
+
description: "Range end (ISO datetime)",
|
|
1820
|
+
demandOption: true
|
|
1821
|
+
}).option("json", {
|
|
1822
|
+
type: "boolean",
|
|
1823
|
+
description: "Output as JSON",
|
|
1824
|
+
default: false
|
|
1825
|
+
});
|
|
1826
|
+
},
|
|
1827
|
+
async (argv) => {
|
|
1828
|
+
loadEnv();
|
|
1829
|
+
const args = argv;
|
|
1830
|
+
try {
|
|
1831
|
+
const job = await getCronJobById(args.scheduleId);
|
|
1832
|
+
const result = await job.successRate({
|
|
1833
|
+
start: args.start,
|
|
1834
|
+
end: args.end
|
|
1835
|
+
});
|
|
1836
|
+
if (args.json) {
|
|
1837
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1838
|
+
} else {
|
|
1839
|
+
console.log("Cron success rate");
|
|
1840
|
+
console.log(` Schedule ID: ${args.scheduleId}`);
|
|
1841
|
+
console.log(` Range: ${result.start} -> ${result.end}`);
|
|
1842
|
+
console.log(` Total: ${result.total}`);
|
|
1843
|
+
console.log(` Succeeded: ${result.succeeded}`);
|
|
1844
|
+
console.log(` Failed: ${result.failed}`);
|
|
1845
|
+
console.log(` Success Rate: ${result.successRate}`);
|
|
1846
|
+
}
|
|
1847
|
+
} catch (error) {
|
|
1848
|
+
handleError(error);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
).demandCommand(1, "You need to specify a cron action");
|
|
1852
|
+
},
|
|
1853
|
+
handler: () => {
|
|
1854
|
+
}
|
|
1855
|
+
};
|
|
1856
|
+
|
|
1857
|
+
const loginCommand = {
|
|
1858
|
+
command: "login",
|
|
1859
|
+
describe: "Authenticate the CLI",
|
|
1860
|
+
builder: (yargs) => {
|
|
1861
|
+
return yargs.option("save-to-dotenv", {
|
|
1862
|
+
type: "boolean",
|
|
1863
|
+
description: "Save the refresh token into the current folder's .env as FREESTYLE_STACK_REFRESH_TOKEN",
|
|
1864
|
+
default: false
|
|
1865
|
+
}).option("force", {
|
|
1866
|
+
type: "boolean",
|
|
1867
|
+
description: "Force a fresh login flow even if a stored token exists",
|
|
1868
|
+
default: false
|
|
1869
|
+
}).option("stack-project-id", {
|
|
1870
|
+
type: "string",
|
|
1871
|
+
description: "Project ID (overrides environment for this run)"
|
|
1872
|
+
}).option("stack-publishable-client-key", {
|
|
1873
|
+
type: "string",
|
|
1874
|
+
description: "Publishable client key (overrides environment for this run)"
|
|
1875
|
+
}).option("stack-app-url", {
|
|
1876
|
+
type: "string",
|
|
1877
|
+
description: "App URL for browser confirmation (default: https://freestyle.sh)"
|
|
1878
|
+
});
|
|
1879
|
+
},
|
|
1880
|
+
handler: async (argv) => {
|
|
1881
|
+
loadEnv();
|
|
1882
|
+
const args = argv;
|
|
1883
|
+
if (args.stackProjectId) {
|
|
1884
|
+
process.env.FREESTYLE_STACK_PROJECT_ID = args.stackProjectId;
|
|
1885
|
+
}
|
|
1886
|
+
if (args.stackPublishableClientKey) {
|
|
1887
|
+
process.env.FREESTYLE_STACK_PUBLISHABLE_CLIENT_KEY = args.stackPublishableClientKey;
|
|
1888
|
+
}
|
|
1889
|
+
if (args.stackAppUrl) {
|
|
1890
|
+
process.env.FREESTYLE_STACK_APP_URL = args.stackAppUrl;
|
|
1891
|
+
}
|
|
1892
|
+
try {
|
|
1893
|
+
const accessToken = await getStackAccessTokenForCli({
|
|
1894
|
+
saveToDotenv: args.saveToDotenv,
|
|
1895
|
+
forceRelogin: args.force
|
|
1896
|
+
});
|
|
1897
|
+
if (!accessToken) {
|
|
1898
|
+
throw new Error(
|
|
1899
|
+
"Authentication is not configured. Set FREESTYLE_STACK_PROJECT_ID and FREESTYLE_STACK_PUBLISHABLE_CLIENT_KEY."
|
|
1900
|
+
);
|
|
1901
|
+
}
|
|
1902
|
+
console.log("\u2713 Authenticated");
|
|
1903
|
+
if (args.saveToDotenv) {
|
|
1904
|
+
console.log("\u2713 Saved refresh token to .env in current directory");
|
|
1905
|
+
} else {
|
|
1906
|
+
console.log("\u2713 Saved refresh token to global CLI auth store");
|
|
1907
|
+
}
|
|
1908
|
+
console.log("Fetching teams...");
|
|
1909
|
+
const teams = await getTeamsForCli();
|
|
1910
|
+
if (teams.length === 0) {
|
|
1911
|
+
console.log(
|
|
1912
|
+
"\u26A0\uFE0F No teams found. You may need to create a team in the dashboard."
|
|
1913
|
+
);
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
const defaultTeam = teams[0];
|
|
1917
|
+
console.log(`Setting up default team: ${defaultTeam.displayName}`);
|
|
1918
|
+
await setDefaultTeam(defaultTeam.id);
|
|
1919
|
+
console.log(
|
|
1920
|
+
`\u2713 Default team configured: ${defaultTeam.displayName} (${defaultTeam.id})`
|
|
1921
|
+
);
|
|
1922
|
+
console.log("You can now use the CLI to manage resources.");
|
|
1923
|
+
if (teams.length > 1) {
|
|
1924
|
+
console.log(
|
|
1925
|
+
`
|
|
1926
|
+
\u2139\uFE0F You have ${teams.length} teams. Use 'freestyle team switch' to change teams.`
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
handleError(error);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
|
|
1935
|
+
const logoutCommand = {
|
|
1936
|
+
command: "logout",
|
|
1937
|
+
describe: "Sign out the CLI",
|
|
1938
|
+
builder: (yargs) => {
|
|
1939
|
+
return yargs.option("dotenv", {
|
|
1940
|
+
type: "boolean",
|
|
1941
|
+
description: "Remove the refresh token from the current folder's .env",
|
|
1942
|
+
default: false
|
|
1943
|
+
});
|
|
1944
|
+
},
|
|
1945
|
+
handler: async (argv) => {
|
|
1946
|
+
loadEnv();
|
|
1947
|
+
const args = argv;
|
|
1948
|
+
try {
|
|
1949
|
+
const result = logoutCliAuth({ removeFromDotenv: args.dotenv });
|
|
1950
|
+
if (!result.clearedStored && !result.clearedDotenv) {
|
|
1951
|
+
console.log("No stored credentials found.");
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
if (result.clearedStored) {
|
|
1955
|
+
console.log("\u2713 Cleared CLI credentials");
|
|
1956
|
+
}
|
|
1957
|
+
if (result.clearedDotenv) {
|
|
1958
|
+
console.log("\u2713 Removed refresh token from .env");
|
|
1959
|
+
}
|
|
1960
|
+
} catch (error) {
|
|
1961
|
+
handleError(error);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
|
|
1966
|
+
const whoamiCommand = {
|
|
1967
|
+
command: "whoami",
|
|
1968
|
+
describe: "Display information about the currently authenticated user",
|
|
1969
|
+
builder: (yargs) => yargs,
|
|
1970
|
+
handler: async (_argv) => {
|
|
1971
|
+
loadEnv();
|
|
1972
|
+
try {
|
|
1973
|
+
const client = await getFreestyleClient();
|
|
1974
|
+
const info = await client.whoami();
|
|
1975
|
+
console.log(`Account ID: ${info.accountId}`);
|
|
1976
|
+
if (info.identityId) {
|
|
1977
|
+
console.log(`Identity ID: ${info.identityId}`);
|
|
1978
|
+
}
|
|
1979
|
+
} catch (e) {
|
|
1980
|
+
handleError(e);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
};
|
|
1984
|
+
|
|
1985
|
+
dotenv.config({ quiet: true });
|
|
1986
|
+
yargs(hideBin(process.argv)).scriptName("freestyle").usage("$0 <command> [options]").option("team", {
|
|
1987
|
+
type: "string",
|
|
1988
|
+
describe: "Override team ID for this command",
|
|
1989
|
+
global: true
|
|
1990
|
+
}).middleware((argv) => {
|
|
1991
|
+
if (argv.team && typeof argv.team === "string") {
|
|
1992
|
+
process.env.FREESTYLE_TEAM_ID = argv.team;
|
|
1993
|
+
}
|
|
1994
|
+
}).command(vmCommand).command(gitCommand).command(domainsCommand).command(cronCommand).command(loginCommand).command(logoutCommand).command(whoamiCommand).command(deployCommand).command(runCommand).demandCommand(1, "You need to specify a command").help().alias("help", "h").version().alias("version", "v").strict().parse();
|