freestyle-sandboxes 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (7) hide show
  1. package/README.md +17 -0
  2. package/cli.mjs +453 -31
  3. package/index.cjs +1662 -1328
  4. package/index.d.cts +1835 -1043
  5. package/index.d.mts +1835 -1043
  6. package/index.mjs +1662 -1328
  7. package/package.json +3 -2
package/README.md CHANGED
@@ -108,6 +108,23 @@ const { repoId } = await freestyle.git.repos.create({
108
108
  },
109
109
  });
110
110
 
111
+ // Create a new branch from the default branch
112
+ const repo = freestyle.git.repos.ref({ repoId });
113
+ const { name, sha } = await repo.branches.create({
114
+ name: "feature/something",
115
+ });
116
+
117
+ // Create commits with files (text and binary)
118
+ const { commit } = await repo.commits.create({
119
+ message: "Add new feature",
120
+ branch: "feature/something",
121
+ files: [
122
+ { path: "README.md", content: "# My Project" },
123
+ { path: "logo.png", content: base64Image, encoding: "base64" }
124
+ ],
125
+ author: { name: "John Doe", email: "john@example.com" }
126
+ });
127
+
111
128
  // Develop code with VMs.
112
129
  const { vm } = await freestyle.vms.create({
113
130
  gitRepos: [{ repo: repoId, path: "/repo" }],
package/cli.mjs CHANGED
@@ -6,17 +6,382 @@ import { Freestyle, VmSpec } from './index.mjs';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import * as dotenv from 'dotenv';
9
+ import * as os from 'os';
9
10
  import { spawn } from 'child_process';
10
11
 
11
- function getFreestyleClient() {
12
+ const DEFAULT_STACK_API_URL = "https://api.stack-auth.com";
13
+ const DEFAULT_STACK_APP_URL = "https://dash.freestyle.sh";
14
+ const DEFAULT_STACK_PROJECT_ID = "0edf478c-f123-46fb-818f-34c0024a9f35";
15
+ const DEFAULT_STACK_PUBLISHABLE_CLIENT_KEY = "pck_h2aft7g9pqjzrkdnzs199h1may5wjtdtdxeex7m2wzp1r";
16
+ const CLI_AUTH_TIMEOUT_MILLIS = 10 * 60 * 1e3;
17
+ const POLL_INTERVAL_MILLIS = 2e3;
18
+ const STACK_REFRESH_TOKEN_ENV_KEY = "FREESTYLE_STACK_REFRESH_TOKEN";
19
+ const STACK_SAVE_TO_DOTENV_ENV_KEY = "FREESTYLE_STACK_SAVE_TO_DOTENV";
20
+ function isTruthy(value) {
21
+ if (!value) {
22
+ return false;
23
+ }
24
+ const normalized = value.trim().toLowerCase();
25
+ return normalized === "1" || normalized === "true" || normalized === "yes";
26
+ }
27
+ function loadRefreshTokenFromDotenv() {
28
+ const refreshToken = process.env[STACK_REFRESH_TOKEN_ENV_KEY];
29
+ if (!refreshToken || typeof refreshToken !== "string") {
30
+ return null;
31
+ }
32
+ const trimmed = refreshToken.trim();
33
+ return trimmed.length > 0 ? trimmed : null;
34
+ }
35
+ function shouldSaveToDotenv(options) {
36
+ if (typeof options?.saveToDotenv === "boolean") {
37
+ return options.saveToDotenv;
38
+ }
39
+ return isTruthy(process.env[STACK_SAVE_TO_DOTENV_ENV_KEY]);
40
+ }
41
+ function persistRefreshTokenToDotenv(refreshToken, options) {
42
+ if (!shouldSaveToDotenv(options)) {
43
+ return;
44
+ }
45
+ const envPath = path.join(process.cwd(), ".env");
46
+ const line = `${STACK_REFRESH_TOKEN_ENV_KEY}=${refreshToken}`;
47
+ let existing = "";
48
+ if (fs.existsSync(envPath)) {
49
+ existing = fs.readFileSync(envPath, "utf-8");
50
+ }
51
+ const pattern = new RegExp(`^${STACK_REFRESH_TOKEN_ENV_KEY}=.*$`, "m");
52
+ let next;
53
+ if (pattern.test(existing)) {
54
+ next = existing.replace(pattern, line);
55
+ } else if (existing.length === 0) {
56
+ next = `${line}
57
+ `;
58
+ } else if (existing.endsWith("\n")) {
59
+ next = `${existing}${line}
60
+ `;
61
+ } else {
62
+ next = `${existing}
63
+ ${line}
64
+ `;
65
+ }
66
+ fs.writeFileSync(envPath, next, { encoding: "utf-8" });
67
+ }
68
+ function walkUpDirectories(startDir) {
69
+ const result = [];
70
+ let current = path.resolve(startDir);
71
+ while (true) {
72
+ result.push(current);
73
+ const parent = path.dirname(current);
74
+ if (parent === current) {
75
+ break;
76
+ }
77
+ current = parent;
78
+ }
79
+ return result;
80
+ }
81
+ function readEnvFileValue(filePath, key) {
82
+ if (!fs.existsSync(filePath)) {
83
+ return void 0;
84
+ }
85
+ const content = fs.readFileSync(filePath, "utf-8");
86
+ const pattern = new RegExp(`^${key}=(.*)$`, "m");
87
+ const match = content.match(pattern);
88
+ if (!match?.[1]) {
89
+ return void 0;
90
+ }
91
+ return match[1].trim().replace(/^['\"]|['\"]$/g, "");
92
+ }
93
+ function readYamlEnvValue(filePath, envName) {
94
+ if (!fs.existsSync(filePath)) {
95
+ return void 0;
96
+ }
97
+ const content = fs.readFileSync(filePath, "utf-8");
98
+ const escapedEnv = envName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
99
+ const pattern = new RegExp(
100
+ `-\\s+name:\\s+${escapedEnv}\\s*[\\r\\n]+\\s*value:\\s*([^\\r\\n#]+)`,
101
+ "m"
102
+ );
103
+ const match = content.match(pattern);
104
+ if (!match?.[1]) {
105
+ return void 0;
106
+ }
107
+ return match[1].trim().replace(/^['\"]|['\"]$/g, "");
108
+ }
109
+ function discoverStackConfigFromWorkspace() {
110
+ const discovered = {};
111
+ const roots = walkUpDirectories(process.cwd());
112
+ for (const root of roots) {
113
+ if (!discovered.projectId || !discovered.publishableClientKey) {
114
+ const dashboardEnv = path.join(root, "freestyle-dashboard", ".env.local");
115
+ discovered.projectId ||= readEnvFileValue(
116
+ dashboardEnv,
117
+ "VITE_STACK_PROJECT_ID"
118
+ );
119
+ discovered.publishableClientKey ||= readEnvFileValue(
120
+ dashboardEnv,
121
+ "VITE_STACK_PUBLISHABLE_CLIENT_KEY"
122
+ );
123
+ }
124
+ if (!discovered.projectId || !discovered.publishableClientKey) {
125
+ const adminEnv = path.join(root, "freestyle-sandbox-admin", ".env.local");
126
+ discovered.projectId ||= readEnvFileValue(
127
+ adminEnv,
128
+ "NEXT_PUBLIC_STACK_PROJECT_ID"
129
+ );
130
+ discovered.publishableClientKey ||= readEnvFileValue(
131
+ adminEnv,
132
+ "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY"
133
+ );
134
+ }
135
+ if (!discovered.projectId || !discovered.publishableClientKey) {
136
+ const dashK8s = path.join(root, "k8s", "freestyle-dash.yml");
137
+ discovered.projectId ||= readYamlEnvValue(
138
+ dashK8s,
139
+ "VITE_STACK_PROJECT_ID"
140
+ );
141
+ discovered.publishableClientKey ||= readYamlEnvValue(
142
+ dashK8s,
143
+ "VITE_STACK_PUBLISHABLE_CLIENT_KEY"
144
+ );
145
+ }
146
+ if (discovered.projectId && discovered.publishableClientKey) {
147
+ break;
148
+ }
149
+ }
150
+ return discovered;
151
+ }
152
+ function resolveStackConfig() {
153
+ const discovered = discoverStackConfigFromWorkspace();
154
+ 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;
155
+ 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;
156
+ if (!projectId || !publishableClientKey) {
157
+ return null;
158
+ }
159
+ const stackApiUrl = (process.env.FREESTYLE_STACK_API_URL ?? DEFAULT_STACK_API_URL).replace(/\/+$/, "");
160
+ const appUrl = (process.env.FREESTYLE_STACK_APP_URL ?? DEFAULT_STACK_APP_URL).replace(/\/+$/, "");
161
+ const authFilePath = process.env.FREESTYLE_STACK_AUTH_FILE ?? path.join(os.homedir(), ".freestyle", "stack-auth.json");
162
+ return {
163
+ stackApiUrl,
164
+ appUrl,
165
+ projectId,
166
+ publishableClientKey,
167
+ authFilePath
168
+ };
169
+ }
170
+ function clientHeaders(config) {
171
+ return {
172
+ "Content-Type": "application/json",
173
+ "x-stack-project-id": config.projectId,
174
+ "x-stack-access-type": "client",
175
+ "x-stack-publishable-client-key": config.publishableClientKey
176
+ };
177
+ }
178
+ function loadStoredAuth(config) {
179
+ try {
180
+ if (!fs.existsSync(config.authFilePath)) {
181
+ return null;
182
+ }
183
+ const auth = JSON.parse(fs.readFileSync(config.authFilePath, "utf-8"));
184
+ if (!auth.refreshToken || typeof auth.refreshToken !== "string") {
185
+ return null;
186
+ }
187
+ return {
188
+ refreshToken: auth.refreshToken,
189
+ updatedAt: typeof auth.updatedAt === "number" ? auth.updatedAt : Date.now()
190
+ };
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+ function persistAuth(config, refreshToken) {
196
+ const dirPath = path.dirname(config.authFilePath);
197
+ fs.mkdirSync(dirPath, { recursive: true });
198
+ fs.writeFileSync(
199
+ config.authFilePath,
200
+ JSON.stringify(
201
+ {
202
+ refreshToken,
203
+ updatedAt: Date.now()
204
+ },
205
+ null,
206
+ 2
207
+ ),
208
+ { encoding: "utf-8", mode: 384 }
209
+ );
210
+ }
211
+ function clearStoredAuth(config) {
212
+ try {
213
+ if (fs.existsSync(config.authFilePath)) {
214
+ fs.unlinkSync(config.authFilePath);
215
+ }
216
+ } catch {
217
+ }
218
+ }
219
+ function tryOpenBrowser(url) {
220
+ try {
221
+ if (process.platform === "darwin") {
222
+ const child2 = spawn("open", [url], {
223
+ stdio: "ignore",
224
+ detached: true
225
+ });
226
+ child2.unref();
227
+ return true;
228
+ }
229
+ if (process.platform === "win32") {
230
+ const child2 = spawn("cmd", ["/c", "start", "", url], {
231
+ stdio: "ignore",
232
+ detached: true
233
+ });
234
+ child2.unref();
235
+ return true;
236
+ }
237
+ const child = spawn("xdg-open", [url], {
238
+ stdio: "ignore",
239
+ detached: true
240
+ });
241
+ child.unref();
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+ async function startCliLogin(config) {
248
+ const initResponse = await fetch(`${config.stackApiUrl}/api/v1/auth/cli`, {
249
+ method: "POST",
250
+ headers: clientHeaders(config),
251
+ body: JSON.stringify({
252
+ expires_in_millis: CLI_AUTH_TIMEOUT_MILLIS
253
+ })
254
+ });
255
+ if (!initResponse.ok) {
256
+ const errorText = await initResponse.text();
257
+ throw new Error(
258
+ `Failed to start Stack Auth CLI login (${initResponse.status}). ${errorText || "Check Stack project ID and publishable client key."}`
259
+ );
260
+ }
261
+ const initData = await initResponse.json();
262
+ if (!initData.polling_code || !initData.login_code) {
263
+ throw new Error("Stack Auth CLI login did not return polling/login codes.");
264
+ }
265
+ const loginUrl = `${config.appUrl}/handler/cli-auth-confirm?login_code=${encodeURIComponent(initData.login_code)}`;
266
+ console.log("\nStack authentication is required.");
267
+ console.log(`Open this URL to continue:
268
+ ${loginUrl}
269
+ `);
270
+ const opened = tryOpenBrowser(loginUrl);
271
+ if (opened) {
272
+ console.log("Opened your browser for authentication...");
273
+ } else {
274
+ console.log("Could not open browser automatically. Open the URL manually.");
275
+ }
276
+ const deadline = Date.now() + CLI_AUTH_TIMEOUT_MILLIS;
277
+ while (Date.now() < deadline) {
278
+ const pollResponse = await fetch(
279
+ `${config.stackApiUrl}/api/v1/auth/cli/poll`,
280
+ {
281
+ method: "POST",
282
+ headers: clientHeaders(config),
283
+ body: JSON.stringify({
284
+ polling_code: initData.polling_code
285
+ })
286
+ }
287
+ );
288
+ if (![200, 201].includes(pollResponse.status)) {
289
+ throw new Error(
290
+ `Failed while polling Stack Auth CLI login (${pollResponse.status}).`
291
+ );
292
+ }
293
+ const pollData = await pollResponse.json();
294
+ if (pollData.status === "success") {
295
+ if (!pollData.refresh_token) {
296
+ throw new Error(
297
+ "Stack Auth login completed without a refresh token response."
298
+ );
299
+ }
300
+ return pollData.refresh_token;
301
+ }
302
+ if (pollData.status === "cancelled" || pollData.status === "expired" || pollData.status === "error") {
303
+ throw new Error(
304
+ pollData.error || `Stack Auth login ${pollData.status}. Please retry.`
305
+ );
306
+ }
307
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MILLIS));
308
+ }
309
+ throw new Error("Timed out waiting for Stack Auth CLI authentication.");
310
+ }
311
+ async function refreshStackAccessToken(config, refreshToken) {
312
+ const response = await fetch(
313
+ `${config.stackApiUrl}/api/v1/auth/sessions/current/refresh`,
314
+ {
315
+ method: "POST",
316
+ headers: {
317
+ ...clientHeaders(config),
318
+ "x-stack-refresh-token": refreshToken
319
+ }
320
+ }
321
+ );
322
+ if (!response.ok) {
323
+ return null;
324
+ }
325
+ const data = await response.json();
326
+ if (!data.access_token) {
327
+ return null;
328
+ }
329
+ return {
330
+ accessToken: data.access_token,
331
+ refreshToken: data.refresh_token
332
+ };
333
+ }
334
+ async function getStackAccessTokenForCli(options) {
335
+ const config = resolveStackConfig();
336
+ if (!config) {
337
+ return null;
338
+ }
339
+ let refreshTokenFromEnv = loadRefreshTokenFromDotenv();
340
+ const stored = loadStoredAuth(config);
341
+ if (options?.forceRelogin) {
342
+ refreshTokenFromEnv = null;
343
+ clearStoredAuth(config);
344
+ }
345
+ let refreshToken = refreshTokenFromEnv ?? stored?.refreshToken;
346
+ if (!refreshToken) {
347
+ refreshToken = await startCliLogin(config);
348
+ persistAuth(config, refreshToken);
349
+ persistRefreshTokenToDotenv(refreshToken, options);
350
+ }
351
+ let refreshed = await refreshStackAccessToken(config, refreshToken);
352
+ if (!refreshed) {
353
+ if (!refreshTokenFromEnv) {
354
+ clearStoredAuth(config);
355
+ }
356
+ refreshToken = await startCliLogin(config);
357
+ persistAuth(config, refreshToken);
358
+ persistRefreshTokenToDotenv(refreshToken, options);
359
+ refreshed = await refreshStackAccessToken(config, refreshToken);
360
+ }
361
+ if (!refreshed) {
362
+ throw new Error("Failed to authenticate with Stack Auth.");
363
+ }
364
+ if (refreshed.refreshToken && refreshed.refreshToken !== refreshToken) {
365
+ persistAuth(config, refreshed.refreshToken);
366
+ persistRefreshTokenToDotenv(refreshed.refreshToken, options);
367
+ }
368
+ return refreshed.accessToken;
369
+ }
370
+
371
+ async function getFreestyleClient() {
12
372
  const apiKey = process.env.FREESTYLE_API_KEY;
13
373
  const baseUrl = process.env.FREESTYLE_API_URL;
14
- if (!apiKey) {
15
- console.error("Error: FREESTYLE_API_KEY environment variable is not set.");
16
- console.error('Run "freestyle init" to set it up, or set it manually.');
17
- process.exit(1);
374
+ if (apiKey) {
375
+ return new Freestyle({ apiKey, baseUrl });
376
+ }
377
+ const accessToken = await getStackAccessTokenForCli();
378
+ if (accessToken) {
379
+ return new Freestyle({ accessToken, baseUrl });
18
380
  }
19
- return new Freestyle({ apiKey, baseUrl });
381
+ console.error(
382
+ "Error: authentication is required. Set FREESTYLE_API_KEY, or configure Stack Auth via FREESTYLE_STACK_PROJECT_ID and FREESTYLE_STACK_PUBLISHABLE_CLIENT_KEY."
383
+ );
384
+ process.exit(1);
20
385
  }
21
386
  function handleError(error) {
22
387
  if (error.response) {
@@ -44,12 +409,14 @@ function formatTable(headers, rows) {
44
409
  console.log(headerRow);
45
410
  console.log(separator);
46
411
  rows.forEach((row) => {
47
- console.log(row.map((cell, i) => (cell || "").padEnd(colWidths[i])).join(" "));
412
+ console.log(
413
+ row.map((cell, i) => (cell || "").padEnd(colWidths[i])).join(" ")
414
+ );
48
415
  });
49
416
  }
50
417
 
51
418
  async function sshIntoVm(vmId, options = {}) {
52
- const freestyle = getFreestyleClient();
419
+ const freestyle = await getFreestyleClient();
53
420
  console.log("Setting up SSH connection...");
54
421
  const { identity, identityId } = await freestyle.identities.create();
55
422
  console.log(`Created identity: ${identityId}`);
@@ -139,7 +506,7 @@ const vmCommand = {
139
506
  loadEnv();
140
507
  const args = argv;
141
508
  try {
142
- const freestyle = getFreestyleClient();
509
+ const freestyle = await getFreestyleClient();
143
510
  let createOptions = {};
144
511
  if (args.snapshot) {
145
512
  createOptions.snapshotId = args.snapshot;
@@ -229,7 +596,7 @@ Exec exit code: ${execResult.statusCode || 0}`);
229
596
  loadEnv();
230
597
  const args = argv;
231
598
  try {
232
- const freestyle = getFreestyleClient();
599
+ const freestyle = await getFreestyleClient();
233
600
  const vms = await freestyle.vms.list();
234
601
  if (args.json) {
235
602
  console.log(JSON.stringify(vms, null, 2));
@@ -296,7 +663,7 @@ Exec exit code: ${execResult.statusCode || 0}`);
296
663
  loadEnv();
297
664
  const args = argv;
298
665
  try {
299
- const freestyle = getFreestyleClient();
666
+ const freestyle = await getFreestyleClient();
300
667
  const vm = freestyle.vms.ref({ vmId: args.vmId });
301
668
  console.log(`Executing command on VM ${args.vmId}...`);
302
669
  const result = await vm.exec({
@@ -334,7 +701,7 @@ Exit code: ${result.statusCode || 0}`);
334
701
  loadEnv();
335
702
  const args = argv;
336
703
  try {
337
- const freestyle = getFreestyleClient();
704
+ const freestyle = await getFreestyleClient();
338
705
  console.log(`Deleting VM ${args.vmId}...`);
339
706
  await freestyle.vms.delete({ vmId: args.vmId });
340
707
  console.log("\u2713 VM deleted successfully!");
@@ -378,9 +745,7 @@ const deployCommand = {
378
745
  const hasFile = !!argv.file;
379
746
  const hasRepo = !!argv.repo;
380
747
  if (!hasCode && !hasFile && !hasRepo) {
381
- throw new Error(
382
- "You must specify one of --code, --file, or --repo"
383
- );
748
+ throw new Error("You must specify one of --code, --file, or --repo");
384
749
  }
385
750
  if ([hasCode, hasFile, hasRepo].filter(Boolean).length > 1) {
386
751
  throw new Error(
@@ -394,7 +759,7 @@ const deployCommand = {
394
759
  loadEnv();
395
760
  const args = argv;
396
761
  try {
397
- const freestyle = getFreestyleClient();
762
+ const freestyle = await getFreestyleClient();
398
763
  let code;
399
764
  let repo;
400
765
  if (args.code) {
@@ -471,7 +836,7 @@ const runCommand = {
471
836
  loadEnv();
472
837
  const args = argv;
473
838
  try {
474
- const freestyle = getFreestyleClient();
839
+ const freestyle = await getFreestyleClient();
475
840
  let code;
476
841
  if (args.code) {
477
842
  code = args.code;
@@ -544,7 +909,7 @@ const gitCommand = {
544
909
  loadEnv();
545
910
  const args = argv;
546
911
  try {
547
- const freestyle = getFreestyleClient();
912
+ const freestyle = await getFreestyleClient();
548
913
  const body = {
549
914
  public: args.public
550
915
  };
@@ -592,7 +957,7 @@ const gitCommand = {
592
957
  loadEnv();
593
958
  const args = argv;
594
959
  try {
595
- const freestyle = getFreestyleClient();
960
+ const freestyle = await getFreestyleClient();
596
961
  const repos = await freestyle.git.repos.list({
597
962
  limit: args.limit,
598
963
  cursor: args.cursor
@@ -642,7 +1007,7 @@ Total: ${repos.total}`);
642
1007
  loadEnv();
643
1008
  const args = argv;
644
1009
  try {
645
- const freestyle = getFreestyleClient();
1010
+ const freestyle = await getFreestyleClient();
646
1011
  console.log(`Deleting repository ${args.repoId}...`);
647
1012
  await freestyle.git.repos.delete({ repoId: args.repoId });
648
1013
  console.log("\u2713 Repository deleted");
@@ -681,7 +1046,7 @@ const domainsCommand = {
681
1046
  loadEnv();
682
1047
  const args = argv;
683
1048
  try {
684
- const freestyle = getFreestyleClient();
1049
+ const freestyle = await getFreestyleClient();
685
1050
  const domains = await freestyle.domains.list({
686
1051
  limit: args.limit,
687
1052
  cursor: args.cursor
@@ -722,7 +1087,7 @@ const domainsCommand = {
722
1087
  loadEnv();
723
1088
  const args = argv;
724
1089
  try {
725
- const freestyle = getFreestyleClient();
1090
+ const freestyle = await getFreestyleClient();
726
1091
  const result = await freestyle.domains.verifications.create({
727
1092
  domain: args.domain
728
1093
  });
@@ -772,7 +1137,7 @@ const domainsCommand = {
772
1137
  loadEnv();
773
1138
  const args = argv;
774
1139
  try {
775
- const freestyle = getFreestyleClient();
1140
+ const freestyle = await getFreestyleClient();
776
1141
  const result = args.verificationId ? await freestyle.domains.verifications.complete({
777
1142
  verificationId: args.verificationId
778
1143
  }) : await freestyle.domains.verifications.complete({
@@ -802,7 +1167,7 @@ const domainsCommand = {
802
1167
  loadEnv();
803
1168
  const args = argv;
804
1169
  try {
805
- const freestyle = getFreestyleClient();
1170
+ const freestyle = await getFreestyleClient();
806
1171
  const verifications = await freestyle.domains.verifications.list();
807
1172
  if (args.json) {
808
1173
  console.log(JSON.stringify(verifications, null, 2));
@@ -865,7 +1230,7 @@ const domainsCommand = {
865
1230
  loadEnv();
866
1231
  const args = argv;
867
1232
  try {
868
- const freestyle = getFreestyleClient();
1233
+ const freestyle = await getFreestyleClient();
869
1234
  const result = args.deploymentId ? await freestyle.domains.mappings.create({
870
1235
  domain: args.domain,
871
1236
  deploymentId: args.deploymentId
@@ -907,7 +1272,7 @@ const domainsCommand = {
907
1272
  loadEnv();
908
1273
  const args = argv;
909
1274
  try {
910
- const freestyle = getFreestyleClient();
1275
+ const freestyle = await getFreestyleClient();
911
1276
  await freestyle.domains.mappings.delete({ domain: args.domain });
912
1277
  console.log("\u2713 Domain mapping deleted");
913
1278
  } catch (error) {
@@ -938,7 +1303,7 @@ const domainsCommand = {
938
1303
  loadEnv();
939
1304
  const args = argv;
940
1305
  try {
941
- const freestyle = getFreestyleClient();
1306
+ const freestyle = await getFreestyleClient();
942
1307
  const { mappings } = await freestyle.domains.mappings.list({
943
1308
  domain: args.domain,
944
1309
  limit: args.limit,
@@ -974,7 +1339,7 @@ const domainsCommand = {
974
1339
  };
975
1340
 
976
1341
  async function getCronJobById(scheduleId) {
977
- const freestyle = getFreestyleClient();
1342
+ const freestyle = await getFreestyleClient();
978
1343
  const { jobs } = await freestyle.cron.list();
979
1344
  const job = jobs.find((candidate) => candidate.schedule.id === scheduleId);
980
1345
  if (!job) {
@@ -1018,7 +1383,7 @@ const cronCommand = {
1018
1383
  loadEnv();
1019
1384
  const args = argv;
1020
1385
  try {
1021
- const freestyle = getFreestyleClient();
1386
+ const freestyle = await getFreestyleClient();
1022
1387
  let parsedPayload = {};
1023
1388
  if (args.payload) {
1024
1389
  try {
@@ -1065,7 +1430,7 @@ const cronCommand = {
1065
1430
  loadEnv();
1066
1431
  const args = argv;
1067
1432
  try {
1068
- const freestyle = getFreestyleClient();
1433
+ const freestyle = await getFreestyleClient();
1069
1434
  const { jobs } = await freestyle.cron.list({
1070
1435
  deploymentId: args.deploymentId
1071
1436
  });
@@ -1258,4 +1623,61 @@ const cronCommand = {
1258
1623
  }
1259
1624
  };
1260
1625
 
1261
- yargs(hideBin(process.argv)).scriptName("freestyle").usage("$0 <command> [options]").command(vmCommand).command(gitCommand).command(domainsCommand).command(cronCommand).command(deployCommand).command(runCommand).demandCommand(1, "You need to specify a command").help().alias("help", "h").version().alias("version", "v").strict().parse();
1626
+ const loginCommand = {
1627
+ command: "login",
1628
+ describe: "Authenticate the CLI with Stack Auth",
1629
+ builder: (yargs) => {
1630
+ return yargs.option("save-to-dotenv", {
1631
+ type: "boolean",
1632
+ description: "Save the Stack refresh token into the current folder's .env as FREESTYLE_STACK_REFRESH_TOKEN",
1633
+ default: false
1634
+ }).option("force", {
1635
+ type: "boolean",
1636
+ description: "Force a fresh login flow even if a stored token exists",
1637
+ default: false
1638
+ }).option("stack-project-id", {
1639
+ type: "string",
1640
+ description: "Stack project ID (overrides environment for this run)"
1641
+ }).option("stack-publishable-client-key", {
1642
+ type: "string",
1643
+ description: "Stack publishable client key (overrides environment for this run)"
1644
+ }).option("stack-app-url", {
1645
+ type: "string",
1646
+ description: "Stack app URL for browser confirmation (default: https://freestyle.sh)"
1647
+ });
1648
+ },
1649
+ handler: async (argv) => {
1650
+ loadEnv();
1651
+ const args = argv;
1652
+ if (args.stackProjectId) {
1653
+ process.env.FREESTYLE_STACK_PROJECT_ID = args.stackProjectId;
1654
+ }
1655
+ if (args.stackPublishableClientKey) {
1656
+ process.env.FREESTYLE_STACK_PUBLISHABLE_CLIENT_KEY = args.stackPublishableClientKey;
1657
+ }
1658
+ if (args.stackAppUrl) {
1659
+ process.env.FREESTYLE_STACK_APP_URL = args.stackAppUrl;
1660
+ }
1661
+ try {
1662
+ const accessToken = await getStackAccessTokenForCli({
1663
+ saveToDotenv: args.saveToDotenv,
1664
+ forceRelogin: args.force
1665
+ });
1666
+ if (!accessToken) {
1667
+ throw new Error(
1668
+ "Stack Auth is not configured. Set FREESTYLE_STACK_PROJECT_ID and FREESTYLE_STACK_PUBLISHABLE_CLIENT_KEY."
1669
+ );
1670
+ }
1671
+ console.log("\u2713 Authenticated with Stack Auth");
1672
+ if (args.saveToDotenv) {
1673
+ console.log("\u2713 Saved refresh token to .env in current directory");
1674
+ } else {
1675
+ console.log("\u2713 Saved refresh token to global CLI auth store");
1676
+ }
1677
+ } catch (error) {
1678
+ handleError(error);
1679
+ }
1680
+ }
1681
+ };
1682
+
1683
+ yargs(hideBin(process.argv)).scriptName("freestyle").usage("$0 <command> [options]").command(vmCommand).command(gitCommand).command(domainsCommand).command(cronCommand).command(loginCommand).command(deployCommand).command(runCommand).demandCommand(1, "You need to specify a command").help().alias("help", "h").version().alias("version", "v").strict().parse();