freestyle 0.1.49 → 0.1.51

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 +9 -0
  2. package/cli.mjs +830 -12
  3. package/index.cjs +2059 -1496
  4. package/index.d.cts +1965 -1233
  5. package/index.d.mts +1965 -1233
  6. package/index.mjs +2057 -1497
  7. package/package.json +1 -1
package/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import * as dotenv from 'dotenv';
3
3
  import yargs from 'yargs';
4
4
  import { hideBin } from 'yargs/helpers';
5
- import { Freestyle, VmSpec } from './index.mjs';
5
+ import { Freestyle, VmSpec, readFiles } from './index.mjs';
6
6
  import * as fs from 'fs';
7
7
  import * as path from 'path';
8
8
  import * as os from 'os';
@@ -686,6 +686,94 @@ async function sshIntoVm(vmId, options = {}) {
686
686
  });
687
687
  });
688
688
  }
689
+ function buildSubcommands(yargs, resolveBuild, idName) {
690
+ return yargs.command(
691
+ `get <${idName}>`,
692
+ "Show the build record",
693
+ (y) => y.positional(idName, { type: "string", demandOption: true }).option("json", { type: "boolean", default: false }),
694
+ async (argv) => {
695
+ loadEnv();
696
+ try {
697
+ const build = await resolveBuild(argv[idName]);
698
+ const record = await build.get();
699
+ console.log(JSON.stringify(record, null, 2));
700
+ } catch (e) {
701
+ handleError(e);
702
+ }
703
+ }
704
+ ).command(
705
+ `phases <${idName}>`,
706
+ "List build phases",
707
+ (y) => y.positional(idName, { type: "string", demandOption: true }).option("json", { type: "boolean", default: false }),
708
+ async (argv) => {
709
+ loadEnv();
710
+ try {
711
+ const build = await resolveBuild(argv[idName]);
712
+ const phases = await build.phases();
713
+ if (argv.json) {
714
+ console.log(JSON.stringify(phases, null, 2));
715
+ return;
716
+ }
717
+ console.log(
718
+ formatTable(
719
+ ["Phase ID", "Name", "Snapshot", "Started"],
720
+ phases.map((p) => [
721
+ p.phaseId,
722
+ p.name,
723
+ p.snapshotId ?? "\u2014",
724
+ p.startedAt
725
+ ])
726
+ )
727
+ );
728
+ } catch (e) {
729
+ handleError(e);
730
+ }
731
+ }
732
+ ).command(
733
+ `debug <${idName}>`,
734
+ "Boot a debug VM from the failed phase's snapshot and SSH in",
735
+ (y) => y.positional(idName, { type: "string", demandOption: true }),
736
+ async (argv) => {
737
+ loadEnv();
738
+ try {
739
+ const fs = await getFreestyleClient();
740
+ const build = await resolveBuild(argv[idName]);
741
+ const failed = await build.failedPhase();
742
+ if (!failed) {
743
+ console.error(
744
+ `No bookable failed-phase snapshot for build ${build.buildId} \u2014 nothing to debug.`
745
+ );
746
+ process.exit(1);
747
+ }
748
+ console.log(
749
+ `Failed phase: ${failed.name} (snapshot ${failed.snapshotId})`
750
+ );
751
+ console.log("Booting debug VM\u2026");
752
+ const result = await fs.vms.create({
753
+ snapshotId: failed.snapshotId
754
+ });
755
+ console.log(`\u2713 VM ${result.vmId} running`);
756
+ await sshIntoVm(result.vmId);
757
+ } catch (e) {
758
+ handleError(e);
759
+ }
760
+ }
761
+ ).command(
762
+ `wait <${idName}>`,
763
+ "Wait until the build reaches a terminal state",
764
+ (y) => y.positional(idName, { type: "string", demandOption: true }),
765
+ async (argv) => {
766
+ loadEnv();
767
+ try {
768
+ const build = await resolveBuild(argv[idName]);
769
+ const record = await build.wait();
770
+ console.log(JSON.stringify(record, null, 2));
771
+ } catch (e) {
772
+ handleError(e);
773
+ }
774
+ }
775
+ ).demandCommand(1, "Specify a build action");
776
+ }
689
777
  const vmCommand = {
690
778
  command: "vm <action>",
691
779
  describe: "Manage Virtual Machines",
@@ -940,12 +1028,330 @@ Exit code: ${result.statusCode || 0}`);
940
1028
  handleError(error);
941
1029
  }
942
1030
  }
1031
+ ).command(
1032
+ "build <action>",
1033
+ "Inspect the build that produced a VM",
1034
+ (yargs2) => buildSubcommands(yargs2, async (vmId) => {
1035
+ const fs = await getFreestyleClient();
1036
+ return fs.vms.ref({ vmId }).getBuild();
1037
+ }, "vmId"),
1038
+ () => {
1039
+ }
1040
+ ).command(
1041
+ "snapshot <action>",
1042
+ "Manage snapshots",
1043
+ (yargs2) => yargs2.command(
1044
+ "list",
1045
+ "List snapshots",
1046
+ (y) => y.option("show-failed", { type: "boolean", default: true }).option("show-deleted", { type: "boolean", default: false }).option("show-cancelled", {
1047
+ type: "boolean",
1048
+ default: false
1049
+ }).option("show-lost", { type: "boolean", default: false }).option("json", { type: "boolean", default: false }),
1050
+ async (argv) => {
1051
+ loadEnv();
1052
+ try {
1053
+ const fs = await getFreestyleClient();
1054
+ const result = await fs.vms.snapshots.list({
1055
+ includeFailed: argv["show-failed"],
1056
+ includeDeleted: argv["show-deleted"],
1057
+ includeCancelled: argv["show-cancelled"],
1058
+ includeLost: argv["show-lost"]
1059
+ });
1060
+ if (argv.json) {
1061
+ console.log(JSON.stringify(result, null, 2));
1062
+ return;
1063
+ }
1064
+ console.log(
1065
+ formatTable(
1066
+ ["Snapshot ID", "Name", "Source VM", "State", "Created"],
1067
+ result.snapshots.map((s) => [
1068
+ s.snapshotId,
1069
+ s.name ?? "\u2014",
1070
+ s.sourceVmId ?? "\u2014",
1071
+ s.state ?? (s.failed ? "failed" : "ready"),
1072
+ s.createdAt
1073
+ ])
1074
+ )
1075
+ );
1076
+ } catch (e) {
1077
+ handleError(e);
1078
+ }
1079
+ }
1080
+ ).command(
1081
+ "get <snapshotId>",
1082
+ "Show a snapshot record",
1083
+ (y) => y.positional("snapshotId", {
1084
+ type: "string",
1085
+ demandOption: true
1086
+ }).option("json", { type: "boolean", default: false }),
1087
+ async (argv) => {
1088
+ loadEnv();
1089
+ try {
1090
+ const fs = await getFreestyleClient();
1091
+ const info = await fs.vms.snapshots.ref({ snapshotId: argv.snapshotId }).get();
1092
+ console.log(JSON.stringify(info, null, 2));
1093
+ } catch (e) {
1094
+ handleError(e);
1095
+ }
1096
+ }
1097
+ ).command(
1098
+ "delete <snapshotId>",
1099
+ "Delete a snapshot",
1100
+ (y) => y.positional("snapshotId", {
1101
+ type: "string",
1102
+ demandOption: true
1103
+ }),
1104
+ async (argv) => {
1105
+ loadEnv();
1106
+ try {
1107
+ const fs = await getFreestyleClient();
1108
+ await fs.vms.snapshots.ref({ snapshotId: argv.snapshotId }).delete();
1109
+ console.log("\u2713 Snapshot deleted");
1110
+ } catch (e) {
1111
+ handleError(e);
1112
+ }
1113
+ }
1114
+ ).command(
1115
+ "rename <snapshotId>",
1116
+ "Rename a snapshot",
1117
+ (y) => y.positional("snapshotId", {
1118
+ type: "string",
1119
+ demandOption: true
1120
+ }).option("name", {
1121
+ type: "string",
1122
+ demandOption: true,
1123
+ description: "New name"
1124
+ }),
1125
+ async (argv) => {
1126
+ loadEnv();
1127
+ try {
1128
+ const fs = await getFreestyleClient();
1129
+ await fs.vms.snapshots.ref({ snapshotId: argv.snapshotId }).update({ name: argv.name });
1130
+ console.log("\u2713 Snapshot renamed");
1131
+ } catch (e) {
1132
+ handleError(e);
1133
+ }
1134
+ }
1135
+ ).command(
1136
+ "boot <snapshotId>",
1137
+ "Boot a VM from a snapshot",
1138
+ (y) => y.positional("snapshotId", {
1139
+ type: "string",
1140
+ demandOption: true
1141
+ }).option("ssh", { type: "boolean", default: false }),
1142
+ async (argv) => {
1143
+ loadEnv();
1144
+ try {
1145
+ const fs = await getFreestyleClient();
1146
+ console.log(
1147
+ `Booting VM from snapshot ${argv.snapshotId}...`
1148
+ );
1149
+ const result = await fs.vms.create({
1150
+ snapshotId: argv.snapshotId
1151
+ });
1152
+ console.log(`\u2713 VM ${result.vmId} created`);
1153
+ if (argv.ssh) {
1154
+ await sshIntoVm(result.vmId);
1155
+ }
1156
+ } catch (e) {
1157
+ handleError(e);
1158
+ }
1159
+ }
1160
+ ).command(
1161
+ "debug <snapshotId>",
1162
+ "Boot a debug VM from a (possibly failed) snapshot and SSH in",
1163
+ (y) => y.positional("snapshotId", {
1164
+ type: "string",
1165
+ demandOption: true
1166
+ }),
1167
+ async (argv) => {
1168
+ loadEnv();
1169
+ try {
1170
+ const fs = await getFreestyleClient();
1171
+ console.log(
1172
+ `Booting debug VM from snapshot ${argv.snapshotId}...`
1173
+ );
1174
+ const result = await fs.vms.create({
1175
+ snapshotId: argv.snapshotId
1176
+ });
1177
+ console.log(`\u2713 VM ${result.vmId} running`);
1178
+ await sshIntoVm(result.vmId);
1179
+ } catch (e) {
1180
+ handleError(e);
1181
+ }
1182
+ }
1183
+ ).command(
1184
+ "build <action>",
1185
+ "Inspect the build that produced a snapshot",
1186
+ (yy) => buildSubcommands(
1187
+ yy,
1188
+ async (snapshotId) => {
1189
+ const fs = await getFreestyleClient();
1190
+ return fs.vms.snapshots.ref({ snapshotId }).getBuild();
1191
+ },
1192
+ "snapshotId"
1193
+ ),
1194
+ () => {
1195
+ }
1196
+ ).demandCommand(1, "Specify a snapshot action"),
1197
+ () => {
1198
+ }
943
1199
  ).demandCommand(1, "You need to specify a vm action");
944
1200
  },
945
1201
  handler: () => {
946
1202
  }
947
1203
  };
948
1204
 
1205
+ const ALWAYS_IGNORED_DIRS = /* @__PURE__ */ new Set([
1206
+ ".git",
1207
+ ".hg",
1208
+ ".svn",
1209
+ ".idea",
1210
+ ".vscode",
1211
+ "node_modules"
1212
+ ]);
1213
+ const ALWAYS_IGNORED_FILES = /* @__PURE__ */ new Set([
1214
+ ".DS_Store"
1215
+ ]);
1216
+ function joinPosix(...parts) {
1217
+ return path.posix.join(...parts.map((part) => part.replace(/\\/g, "/")));
1218
+ }
1219
+ function shouldIgnorePath(filePath, options) {
1220
+ const normalizedPath = filePath.replace(/\\/g, "/");
1221
+ const segments = normalizedPath.split("/").filter(Boolean);
1222
+ const basename = segments[segments.length - 1] || "";
1223
+ for (const segment of segments.slice(0, -1)) {
1224
+ if (ALWAYS_IGNORED_DIRS.has(segment)) {
1225
+ return `ignored directory '${segment}'`;
1226
+ }
1227
+ if (options?.excludeNextArtifacts && segment === ".next") {
1228
+ return "ignored build artifact directory '.next'";
1229
+ }
1230
+ }
1231
+ if (ALWAYS_IGNORED_FILES.has(basename)) {
1232
+ return `ignored file '${basename}'`;
1233
+ }
1234
+ if (basename === ".env" || basename.startsWith(".env.")) {
1235
+ return "ignored sensitive env file";
1236
+ }
1237
+ if (options?.excludeNextArtifacts && basename === ".next") {
1238
+ return "ignored build artifact directory '.next'";
1239
+ }
1240
+ return null;
1241
+ }
1242
+ function filterDeploymentFiles(files, options) {
1243
+ const ignoredSummary = {};
1244
+ const filtered = [];
1245
+ for (const file of files) {
1246
+ const reason = shouldIgnorePath(file.path, options);
1247
+ if (reason) {
1248
+ ignoredSummary[reason] = (ignoredSummary[reason] || 0) + 1;
1249
+ continue;
1250
+ }
1251
+ filtered.push(file);
1252
+ }
1253
+ return {
1254
+ files: filtered,
1255
+ ignoredSummary
1256
+ };
1257
+ }
1258
+ async function readFilesWithPrefix(dir, prefix) {
1259
+ const files = await readFiles(dir);
1260
+ return files.map((file) => ({
1261
+ ...file,
1262
+ path: joinPosix(prefix, file.path)
1263
+ }));
1264
+ }
1265
+ function detectLockfile(projectRoot) {
1266
+ const lockfiles = [
1267
+ "package-lock.json",
1268
+ "yarn.lock",
1269
+ "pnpm-lock.yaml",
1270
+ "bun.lock",
1271
+ "bun.lockb"
1272
+ ];
1273
+ return lockfiles.find(
1274
+ (lockfile) => fs.existsSync(path.join(projectRoot, lockfile))
1275
+ );
1276
+ }
1277
+ function detectNextJsProject(projectRoot) {
1278
+ const nextConfigCandidates = [
1279
+ "next.config.js",
1280
+ "next.config.mjs",
1281
+ "next.config.ts",
1282
+ "next.config.cjs"
1283
+ ];
1284
+ if (nextConfigCandidates.some(
1285
+ (fileName) => fs.existsSync(path.join(projectRoot, fileName))
1286
+ )) {
1287
+ return true;
1288
+ }
1289
+ const packageJsonPath = path.join(projectRoot, "package.json");
1290
+ if (!fs.existsSync(packageJsonPath)) {
1291
+ return false;
1292
+ }
1293
+ try {
1294
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
1295
+ return Boolean(
1296
+ packageJson.dependencies?.next || packageJson.devDependencies?.next
1297
+ );
1298
+ } catch {
1299
+ return false;
1300
+ }
1301
+ }
1302
+ async function prepareNextJsBuiltFiles(projectRoot) {
1303
+ const standaloneDir = path.join(projectRoot, ".next", "standalone");
1304
+ const standaloneEntrypoint = path.join(standaloneDir, "server.js");
1305
+ if (!fs.existsSync(standaloneDir) || !fs.statSync(standaloneDir).isDirectory() || !fs.existsSync(standaloneEntrypoint)) {
1306
+ return null;
1307
+ }
1308
+ const files = await readFiles(standaloneDir);
1309
+ const existingPaths = new Set(files.map((file) => file.path));
1310
+ const projectPublicDir = path.join(projectRoot, "public");
1311
+ if (fs.existsSync(projectPublicDir) && fs.statSync(projectPublicDir).isDirectory()) {
1312
+ const publicFiles = await readFilesWithPrefix(projectPublicDir, "public");
1313
+ for (const file of publicFiles) {
1314
+ if (!existingPaths.has(file.path)) {
1315
+ files.push(file);
1316
+ existingPaths.add(file.path);
1317
+ }
1318
+ }
1319
+ }
1320
+ const projectStaticDir = path.join(projectRoot, ".next", "static");
1321
+ if (fs.existsSync(projectStaticDir) && fs.statSync(projectStaticDir).isDirectory()) {
1322
+ const staticFiles = await readFilesWithPrefix(
1323
+ projectStaticDir,
1324
+ ".next/static"
1325
+ );
1326
+ for (const file of staticFiles) {
1327
+ if (!existingPaths.has(file.path)) {
1328
+ files.push(file);
1329
+ existingPaths.add(file.path);
1330
+ }
1331
+ }
1332
+ }
1333
+ const lockfile = detectLockfile(projectRoot);
1334
+ if (lockfile && !existingPaths.has(lockfile)) {
1335
+ const lockfileContent = fs.readFileSync(path.join(projectRoot, lockfile), "base64");
1336
+ files.push({
1337
+ path: lockfile,
1338
+ content: lockfileContent,
1339
+ encoding: "base64"
1340
+ });
1341
+ }
1342
+ const freestyleJsonPath = path.join(projectRoot, "freestyle.json");
1343
+ if (fs.existsSync(freestyleJsonPath) && !existingPaths.has("freestyle.json")) {
1344
+ files.push({
1345
+ path: "freestyle.json",
1346
+ content: fs.readFileSync(freestyleJsonPath, "utf-8"),
1347
+ encoding: "utf-8"
1348
+ });
1349
+ }
1350
+ return {
1351
+ files,
1352
+ entrypointPath: "server.js"
1353
+ };
1354
+ }
949
1355
  const deployCommand = {
950
1356
  command: "deploy",
951
1357
  describe: "Deploy a serverless function",
@@ -958,15 +1364,36 @@ const deployCommand = {
958
1364
  alias: "f",
959
1365
  type: "string",
960
1366
  description: "File path containing code to deploy"
1367
+ }).option("dir", {
1368
+ alias: "d",
1369
+ type: "string",
1370
+ description: "Directory path to deploy (prebuilt files, or source files when used with --build)"
961
1371
  }).option("repo", {
962
1372
  alias: "r",
963
1373
  type: "string",
964
1374
  description: "Git repository ID to deploy"
1375
+ }).option("domain", {
1376
+ type: "array",
1377
+ description: "Domains to assign to the deployment (can be specified multiple times)",
1378
+ default: []
965
1379
  }).option("env", {
966
1380
  alias: "e",
967
1381
  type: "array",
968
1382
  description: "Environment variables (KEY=VALUE)",
969
1383
  default: []
1384
+ }).option("build", {
1385
+ type: "boolean",
1386
+ description: "Enable server-side build (use with --repo or --dir source deployments)"
1387
+ }).option("build-command", {
1388
+ type: "string",
1389
+ description: "Custom build command (for example: npm run build)"
1390
+ }).option("build-out-dir", {
1391
+ type: "string",
1392
+ description: "Build output directory (for example: dist or .next/standalone)"
1393
+ }).option("build-env", {
1394
+ type: "array",
1395
+ description: "Build environment variables (KEY=VALUE)",
1396
+ default: []
970
1397
  }).option("json", {
971
1398
  type: "boolean",
972
1399
  description: "Output as JSON",
@@ -974,15 +1401,41 @@ const deployCommand = {
974
1401
  }).check((argv) => {
975
1402
  const hasCode = !!argv.code;
976
1403
  const hasFile = !!argv.file;
1404
+ const hasDir = !!argv.dir;
977
1405
  const hasRepo = !!argv.repo;
978
- if (!hasCode && !hasFile && !hasRepo) {
979
- throw new Error("You must specify one of --code, --file, or --repo");
1406
+ const hasBuildConfig = !!argv.build || !!argv.buildCommand || !!argv.buildOutDir;
1407
+ if (!hasCode && !hasFile && !hasDir && !hasRepo) {
1408
+ throw new Error(
1409
+ "You must specify one of --code, --file, --dir, or --repo"
1410
+ );
1411
+ }
1412
+ if ([hasCode, hasFile, hasDir, hasRepo].filter(Boolean).length > 1) {
1413
+ throw new Error(
1414
+ "You can only specify one of --code, --file, --dir, or --repo"
1415
+ );
1416
+ }
1417
+ if (hasBuildConfig && (hasCode || hasFile)) {
1418
+ throw new Error(
1419
+ "--build options are only supported with --repo or --dir"
1420
+ );
980
1421
  }
981
- if ([hasCode, hasFile, hasRepo].filter(Boolean).length > 1) {
1422
+ if (argv.buildEnv && argv.buildEnv.length > 0 && !hasBuildConfig) {
982
1423
  throw new Error(
983
- "You can only specify one of --code, --file, or --repo"
1424
+ "--build-env requires --build, --build-command, or --build-out-dir"
984
1425
  );
985
1426
  }
1427
+ if (!!argv.buildCommand && !argv.build) {
1428
+ throw new Error("--build-command requires --build");
1429
+ }
1430
+ if (!!argv.buildOutDir && !argv.build) {
1431
+ throw new Error("--build-out-dir requires --build");
1432
+ }
1433
+ if (!!argv.buildOutDir && !argv.buildCommand) {
1434
+ throw new Error("--build-out-dir requires --build-command");
1435
+ }
1436
+ if (argv.buildEnv && argv.buildEnv.length > 0 && !argv.buildCommand) {
1437
+ throw new Error("--build-env requires --build-command");
1438
+ }
986
1439
  return true;
987
1440
  });
988
1441
  },
@@ -992,11 +1445,55 @@ const deployCommand = {
992
1445
  try {
993
1446
  const freestyle = await getFreestyleClient();
994
1447
  let code;
1448
+ let files;
995
1449
  let repo;
1450
+ let entrypointPath;
1451
+ let nextjsOptimization;
996
1452
  if (args.code) {
997
1453
  code = args.code;
998
1454
  } else if (args.file) {
999
1455
  code = fs.readFileSync(args.file, "utf-8");
1456
+ } else if (args.dir) {
1457
+ if (!fs.existsSync(args.dir)) {
1458
+ throw new Error(`Directory not found: ${args.dir}`);
1459
+ }
1460
+ if (!fs.statSync(args.dir).isDirectory()) {
1461
+ throw new Error(`Path is not a directory: ${args.dir}`);
1462
+ }
1463
+ const nextJsBuiltFiles = await prepareNextJsBuiltFiles(args.dir);
1464
+ let ignoredSummary = {};
1465
+ if (nextJsBuiltFiles) {
1466
+ const filteredBuiltFiles = filterDeploymentFiles(nextJsBuiltFiles.files, {
1467
+ excludeNextArtifacts: false
1468
+ });
1469
+ files = filteredBuiltFiles.files;
1470
+ ignoredSummary = filteredBuiltFiles.ignoredSummary;
1471
+ entrypointPath = nextJsBuiltFiles.entrypointPath;
1472
+ nextjsOptimization = true;
1473
+ } else {
1474
+ const rawFiles = await readFiles(args.dir);
1475
+ const filteredSourceFiles = filterDeploymentFiles(rawFiles, {
1476
+ // When uploading source for server-side build, ignore stale local Next.js build output.
1477
+ excludeNextArtifacts: !!args.build
1478
+ });
1479
+ files = filteredSourceFiles.files;
1480
+ ignoredSummary = filteredSourceFiles.ignoredSummary;
1481
+ if (detectNextJsProject(args.dir)) {
1482
+ nextjsOptimization = true;
1483
+ }
1484
+ }
1485
+ const ignoredEntries = Object.entries(ignoredSummary);
1486
+ if (ignoredEntries.length > 0 && !args.json) {
1487
+ console.log("Ignoring files not meant for deployment:");
1488
+ for (const [reason, count] of ignoredEntries) {
1489
+ console.log(` - ${reason}: ${count}`);
1490
+ }
1491
+ }
1492
+ if (files.length === 0) {
1493
+ throw new Error(
1494
+ `No deployable files found in directory: ${args.dir}. Check your path and ignore rules.`
1495
+ );
1496
+ }
1000
1497
  } else if (args.repo) {
1001
1498
  repo = args.repo;
1002
1499
  }
@@ -1009,19 +1506,53 @@ const deployCommand = {
1009
1506
  }
1010
1507
  }
1011
1508
  }
1509
+ const buildEnvVars = {};
1510
+ if (args.buildEnv) {
1511
+ for (const envVar of args.buildEnv) {
1512
+ const [key, ...valueParts] = envVar.split("=");
1513
+ if (key) {
1514
+ buildEnvVars[key] = valueParts.join("=");
1515
+ }
1516
+ }
1517
+ }
1518
+ let build;
1519
+ if (args.build) {
1520
+ if (args.buildCommand) {
1521
+ build = {
1522
+ command: args.buildCommand,
1523
+ ...args.buildOutDir ? { outDir: args.buildOutDir } : {},
1524
+ ...Object.keys(buildEnvVars).length > 0 ? { envVars: buildEnvVars } : {}
1525
+ };
1526
+ } else {
1527
+ build = true;
1528
+ }
1529
+ }
1012
1530
  console.log("Creating deployment...");
1013
- const result = await freestyle.serverless.deployments.create({
1531
+ const domains = args.domain?.filter(Boolean);
1532
+ const createDeploymentRequest = {
1014
1533
  ...code ? { code } : {},
1534
+ ...files ? { files } : {},
1015
1535
  ...repo ? { repo } : {},
1016
- env: Object.keys(env).length > 0 ? env : void 0
1017
- });
1536
+ ...build ? { build } : {},
1537
+ ...entrypointPath ? { entrypointPath } : {},
1538
+ ...nextjsOptimization ? {
1539
+ experimental: {
1540
+ nextjsOptimization: true
1541
+ }
1542
+ } : {},
1543
+ ...domains && domains.length > 0 ? { domains } : {},
1544
+ envVars: Object.keys(env).length > 0 ? env : void 0
1545
+ };
1546
+ const result = await freestyle.serverless.deployments.create(
1547
+ createDeploymentRequest
1548
+ );
1018
1549
  if (args.json) {
1019
1550
  console.log(JSON.stringify(result, null, 2));
1020
1551
  } else {
1021
1552
  console.log("\n\u2713 Deployment created successfully!");
1022
1553
  console.log(` Deployment ID: ${result.deploymentId}`);
1023
- if (result.url) {
1024
- console.log(` URL: ${result.url}`);
1554
+ if (result.domains.length > 0) {
1555
+ console.log(` Domains: ${result.domains.join(", ")}`);
1025
1556
  }
1026
1557
  }
1027
1558
  } catch (error) {
@@ -1708,6 +2239,9 @@ const cronCommand = {
1708
2239
  type: "string",
1709
2240
  description: "Cron expression",
1710
2241
  demandOption: true
2242
+ }).option("name", {
2243
+ type: "string",
2244
+ description: "Optional display name for the cron schedule"
1711
2245
  }).option("timezone", {
1712
2246
  type: "string",
1713
2247
  description: "Timezone (default: UTC)",
@@ -1739,6 +2273,7 @@ const cronCommand = {
1739
2273
  }
1740
2274
  const { job } = await freestyle.cron.schedule({
1741
2275
  deploymentId: args.deploymentId,
2276
+ name: args.name,
1742
2277
  cron: args.cron,
1743
2278
  timezone: args.timezone,
1744
2279
  payload: parsedPayload,
@@ -1750,6 +2285,7 @@ const cronCommand = {
1750
2285
  console.log("\u2713 Cron schedule created");
1751
2286
  console.log(` Schedule ID: ${job.schedule.id}`);
1752
2287
  console.log(` Deployment ID: ${job.schedule.deploymentId}`);
2288
+ console.log(` Name: ${job.schedule.name ?? "-"}`);
1753
2289
  console.log(` Cron: ${job.schedule.cron}`);
1754
2290
  console.log(` Timezone: ${job.schedule.timezone}`);
1755
2291
  console.log(` Active: ${job.schedule.active ? "yes" : "no"}`);
@@ -1795,13 +2331,14 @@ const cronCommand = {
1795
2331
  }
1796
2332
  const rows = jobs.map((job) => [
1797
2333
  job.schedule.id,
2334
+ job.schedule.name ?? "-",
1798
2335
  job.schedule.deploymentId,
1799
2336
  job.schedule.cron,
1800
2337
  job.schedule.timezone,
1801
2338
  job.schedule.active ? "active" : "disabled"
1802
2339
  ]);
1803
2340
  formatTable(
1804
- ["Schedule ID", "Deployment", "Cron", "Timezone", "Status"],
2341
+ ["Schedule ID", "Name", "Deployment", "Cron", "Timezone", "Status"],
1805
2342
  rows
1806
2343
  );
1807
2344
  } catch (error) {
@@ -2096,6 +2633,287 @@ const whoamiCommand = {
2096
2633
  }
2097
2634
  };
2098
2635
 
2636
+ const FILTER_OPTIONS = [
2637
+ "vm",
2638
+ "deployment",
2639
+ "domain",
2640
+ "run",
2641
+ "request",
2642
+ "build"
2643
+ ];
2644
+ function normalizeTimeOption(value) {
2645
+ if (!value) {
2646
+ return void 0;
2647
+ }
2648
+ const match = value.match(/^(\d+)(ms|s|m|h|d)$/);
2649
+ if (!match) {
2650
+ return value;
2651
+ }
2652
+ const amount = Number(match[1]);
2653
+ const unit = match[2];
2654
+ const multiplier = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
2655
+ return new Date(Date.now() - amount * multiplier).toISOString();
2656
+ }
2657
+ function parseDurationMs(value) {
2658
+ if (!value) {
2659
+ return void 0;
2660
+ }
2661
+ const numericValue = Number(value);
2662
+ if (Number.isFinite(numericValue)) {
2663
+ return numericValue;
2664
+ }
2665
+ const match = value.match(/^(\d+)(ms|s|m|h|d)$/);
2666
+ if (!match) {
2667
+ throw new Error(
2668
+ `Invalid --timeout value '${value}'. Use milliseconds or a duration like 30s, 5m, or 1h.`
2669
+ );
2670
+ }
2671
+ const amount = Number(match[1]);
2672
+ const unit = match[2];
2673
+ const multiplier = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
2674
+ return amount * multiplier;
2675
+ }
2676
+ function formatDurationMs(ms) {
2677
+ if (ms % 864e5 === 0) {
2678
+ return `${ms / 864e5}d`;
2679
+ }
2680
+ if (ms % 36e5 === 0) {
2681
+ return `${ms / 36e5}h`;
2682
+ }
2683
+ if (ms % 6e4 === 0) {
2684
+ return `${ms / 6e4}m`;
2685
+ }
2686
+ if (ms % 1e3 === 0) {
2687
+ return `${ms / 1e3}s`;
2688
+ }
2689
+ return `${ms}ms`;
2690
+ }
2691
+ function buildLogsQuery(args) {
2692
+ const query = {
2693
+ pageSize: args.limit,
2694
+ startTime: normalizeTimeOption(args.since),
2695
+ endTime: normalizeTimeOption(args.until),
2696
+ search: args.search,
2697
+ instanceId: args.instance,
2698
+ vmService: args.service,
2699
+ resourceType: args.resourceType
2700
+ };
2701
+ if (args.vm) {
2702
+ query.vmId = args.vm;
2703
+ } else if (args.deployment) {
2704
+ query.deploymentId = args.deployment;
2705
+ } else if (args.domain) {
2706
+ query.domain = args.domain;
2707
+ } else if (args.run) {
2708
+ query.runId = args.run;
2709
+ } else if (args.request) {
2710
+ query.requestId = args.request;
2711
+ } else if (args.build) {
2712
+ query.buildId = args.build;
2713
+ }
2714
+ return query;
2715
+ }
2716
+ function logEntryKey(entry) {
2717
+ return [
2718
+ entry.timestamp,
2719
+ entry.resourceType ?? "",
2720
+ entry.resourceId ?? "",
2721
+ entry.instanceId ?? "",
2722
+ entry.vmService ?? "",
2723
+ entry.source ?? "",
2724
+ entry.message
2725
+ ].join(" ");
2726
+ }
2727
+ function sortLogsAscending(logs) {
2728
+ return [...logs].sort((left, right) => {
2729
+ const leftTime = Date.parse(left.timestamp);
2730
+ const rightTime = Date.parse(right.timestamp);
2731
+ if (!Number.isNaN(leftTime) && !Number.isNaN(rightTime)) {
2732
+ return leftTime - rightTime;
2733
+ }
2734
+ return left.timestamp.localeCompare(right.timestamp);
2735
+ });
2736
+ }
2737
+ function formatLogEntry(entry) {
2738
+ const labels = [
2739
+ entry.resourceType && entry.resourceId ? `${entry.resourceType}:${entry.resourceId}` : entry.resourceId,
2740
+ entry.instanceId ? `instance:${entry.instanceId}` : void 0,
2741
+ entry.vmService ? `service:${entry.vmService}` : void 0,
2742
+ entry.buildId ? `build:${entry.buildId}` : void 0,
2743
+ entry.level,
2744
+ entry.source
2745
+ ].filter((value) => Boolean(value));
2746
+ const prefix = labels.length > 0 ? ` [${labels.join(" ")}]` : "";
2747
+ const message = entry.message.replace(/\n$/, "");
2748
+ return `${entry.timestamp}${prefix} ${message}`;
2749
+ }
2750
+ function printLogEntry(entry, json) {
2751
+ if (json) {
2752
+ console.log(JSON.stringify(entry));
2753
+ return;
2754
+ }
2755
+ console.log(formatLogEntry(entry));
2756
+ }
2757
+ const logsCommand = {
2758
+ command: "logs",
2759
+ describe: "Read observability logs",
2760
+ builder: (yargs) => {
2761
+ return yargs.option("vm", {
2762
+ type: "string",
2763
+ description: "VM ID to read logs for"
2764
+ }).option("deployment", {
2765
+ type: "string",
2766
+ description: "Deployment ID to read logs for"
2767
+ }).option("domain", {
2768
+ type: "string",
2769
+ description: "Domain to read deployment logs for"
2770
+ }).option("run", {
2771
+ type: "string",
2772
+ description: "Run ID to read logs for"
2773
+ }).option("request", {
2774
+ type: "string",
2775
+ description: "Background request ID to read logs for"
2776
+ }).option("build", {
2777
+ type: "string",
2778
+ description: "Build ID to read logs for"
2779
+ }).option("instance", {
2780
+ type: "string",
2781
+ description: "VM or deployment instance ID filter"
2782
+ }).option("service", {
2783
+ type: "string",
2784
+ description: "VM systemd service name filter"
2785
+ }).option("search", {
2786
+ type: "string",
2787
+ description: "Case-insensitive substring search"
2788
+ }).option("since", {
2789
+ type: "string",
2790
+ description: "Start time as RFC3339 or duration like 5m, 2h, 1d"
2791
+ }).option("until", {
2792
+ type: "string",
2793
+ description: "End time as RFC3339 or duration like 5m, 2h, 1d"
2794
+ }).option("limit", {
2795
+ type: "number",
2796
+ description: "Maximum logs per poll",
2797
+ default: 100
2798
+ }).option("stream", {
2799
+ type: "boolean",
2800
+ description: "Poll for new logs until interrupted",
2801
+ default: false
2802
+ }).option("interval", {
2803
+ type: "number",
2804
+ description: "Polling interval in milliseconds when streaming",
2805
+ default: 2e3
2806
+ }).option("timeout", {
2807
+ type: "string",
2808
+ description: "Stop streaming after this long without new logs, like 30s, 5m, or milliseconds"
2809
+ }).option("json", {
2810
+ type: "boolean",
2811
+ description: "Output JSON",
2812
+ default: false
2813
+ }).option("resource-type", {
2814
+ choices: ["deployment", "run", "vm"],
2815
+ description: "Account-wide resource type filter"
2816
+ }).check((argv) => {
2817
+ const filters = FILTER_OPTIONS.filter((option) => Boolean(argv[option]));
2818
+ if (filters.length > 1) {
2819
+ throw new Error(
2820
+ `Use only one log target filter: ${filters.map((filter) => `--${filter}`).join(", ")}`
2821
+ );
2822
+ }
2823
+ if (argv.service && !argv.vm) {
2824
+ throw new Error("--service can only be used with --vm");
2825
+ }
2826
+ if (argv.until && argv.stream) {
2827
+ throw new Error("--until cannot be used with --stream");
2828
+ }
2829
+ if (argv.timeout && !argv.stream) {
2830
+ throw new Error("--timeout can only be used with --stream");
2831
+ }
2832
+ const timeoutMs = parseDurationMs(
2833
+ typeof argv.timeout === "string" ? argv.timeout : void 0
2834
+ );
2835
+ if (timeoutMs !== void 0 && timeoutMs <= 0) {
2836
+ throw new Error("--timeout must be greater than 0");
2837
+ }
2838
+ if (typeof argv.interval === "number" && argv.interval <= 0) {
2839
+ throw new Error("--interval must be greater than 0");
2840
+ }
2841
+ if (typeof argv.limit === "number" && argv.limit <= 0) {
2842
+ throw new Error("--limit must be greater than 0");
2843
+ }
2844
+ return true;
2845
+ });
2846
+ },
2847
+ handler: async (argv) => {
2848
+ loadEnv();
2849
+ const args = argv;
2850
+ try {
2851
+ const freestyle = await getFreestyleClient();
2852
+ const query = buildLogsQuery(args);
2853
+ if (!args.stream) {
2854
+ const response = await freestyle.observability.getLogs(query);
2855
+ if (args.json) {
2856
+ console.log(JSON.stringify(response, null, 2));
2857
+ return;
2858
+ }
2859
+ for (const entry of sortLogsAscending(response.logs ?? [])) {
2860
+ printLogEntry(entry, false);
2861
+ }
2862
+ return;
2863
+ }
2864
+ const abortController = new AbortController();
2865
+ const timeoutMs = parseDurationMs(args.timeout);
2866
+ const seenLogs = /* @__PURE__ */ new Set();
2867
+ let timeout;
2868
+ let stoppedAfterTimeout = false;
2869
+ const resetTimeout = () => {
2870
+ if (timeoutMs === void 0) {
2871
+ return;
2872
+ }
2873
+ if (timeout) {
2874
+ clearTimeout(timeout);
2875
+ }
2876
+ timeout = setTimeout(() => {
2877
+ stoppedAfterTimeout = true;
2878
+ abortController.abort();
2879
+ }, timeoutMs);
2880
+ };
2881
+ const handleSigint = () => abortController.abort();
2882
+ process.once("SIGINT", handleSigint);
2883
+ if (!args.json) {
2884
+ console.error("Streaming logs. Press Ctrl-C to stop.");
2885
+ }
2886
+ try {
2887
+ resetTimeout();
2888
+ for await (const entry of freestyle.observability.streamLogs({
2889
+ ...query,
2890
+ intervalMs: args.interval,
2891
+ signal: abortController.signal,
2892
+ includeExisting: Boolean(query.startTime)
2893
+ })) {
2894
+ const key = logEntryKey(entry);
2895
+ if (seenLogs.has(key)) {
2896
+ continue;
2897
+ }
2898
+ seenLogs.add(key);
2899
+ printLogEntry(entry, Boolean(args.json));
2900
+ resetTimeout();
2901
+ }
2902
+ } finally {
2903
+ if (timeout) {
2904
+ clearTimeout(timeout);
2905
+ }
2906
+ process.off("SIGINT", handleSigint);
2907
+ }
2908
+ if (stoppedAfterTimeout && !args.json && timeoutMs !== void 0) {
2909
+ console.error(`No logs received for ${formatDurationMs(timeoutMs)}; stopped streaming.`);
2910
+ }
2911
+ } catch (error) {
2912
+ handleError(error);
2913
+ }
2914
+ }
2915
+ };
2916
+
2099
2917
  dotenv.config({ quiet: true });
2100
2918
  yargs(hideBin(process.argv)).scriptName("freestyle").usage("$0 <command> [options]").option("team", {
2101
2919
  type: "string",
@@ -2105,4 +2923,4 @@ yargs(hideBin(process.argv)).scriptName("freestyle").usage("$0 <command> [option
2105
2923
  if (argv.team && typeof argv.team === "string") {
2106
2924
  process.env.FREESTYLE_TEAM_ID = argv.team;
2107
2925
  }
2108
- }).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();
2926
+ }).command(vmCommand).command(gitCommand).command(domainsCommand).command(cronCommand).command(loginCommand).command(logoutCommand).command(whoamiCommand).command(logsCommand).command(deployCommand).command(runCommand).demandCommand(1, "You need to specify a command").help().alias("help", "h").version().alias("version", "v").strict().parse();