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/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();