openship 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1 -0
  2. package/dist/{bare-JY5Y5ELU.js → bare-VRGE5MXH.js} +7 -7
  3. package/dist/{chunk-TGIINPVM.js → chunk-6LKHXB6T.js} +22 -7
  4. package/dist/{chunk-2D3I5G6P.js → chunk-6PIYRG5I.js} +23 -3
  5. package/dist/{chunk-BBHX7T4R.js → chunk-BS2AXYZX.js} +401 -76
  6. package/dist/{chunk-SJIFLLX4.js → chunk-CUJXGK7R.js} +321 -85
  7. package/dist/{chunk-VFINNTUL.js → chunk-EAAGSQXN.js} +23 -8
  8. package/dist/{chunk-4Z5QO7N6.js → chunk-I6PSTQIR.js} +2 -2
  9. package/dist/{chunk-5NYUCONN.js → chunk-JLKKYUZA.js} +31 -49
  10. package/dist/{chunk-XPPZ7B2J.js → chunk-K2RPVGPN.js} +2 -2
  11. package/dist/{chunk-MZ2WEHHY.js → chunk-KNTJ7PSF.js} +1 -1
  12. package/dist/{chunk-L2VOGB2X.js → chunk-KZOBYJUW.js} +1 -1
  13. package/dist/{chunk-GKHUSZCD.js → chunk-NNPEG6HM.js} +1 -1
  14. package/dist/{chunk-SLTYVHU7.js → chunk-NXFIRK2Z.js} +381 -72
  15. package/dist/{chunk-FXTBHTWY.js → chunk-RB35YN3J.js} +271 -35
  16. package/dist/{chunk-6APULWSU.js → chunk-UMQP7SYT.js} +429 -66
  17. package/dist/{chunk-V6AN6XLZ.js → chunk-V7BZMGOT.js} +123 -45
  18. package/dist/{chunk-VDMQJHPI.js → chunk-YWMODKWL.js} +67 -4
  19. package/dist/{chunk-OXWDY22G.js → chunk-ZCOQJCZO.js} +1 -1
  20. package/dist/{cloud-KYWY3RP4.js → cloud-3M2WZ624.js} +12 -12
  21. package/dist/{detect-AZLOHUFX.js → detect-BAJMY6KM.js} +3 -3
  22. package/dist/{docker-LODK5Y2S.js → docker-2NGBRJFJ.js} +9 -7
  23. package/dist/{docker-edge-executor-VHW3JPJS.js → docker-edge-executor-5I4M5QT7.js} +1 -1
  24. package/dist/{edge-import-ABUDKKJI.js → edge-import-RIWNEIXQ.js} +21 -19
  25. package/dist/{ensure-container-edge-SW6HENSG.js → ensure-container-edge-3NSYKHRX.js} +6 -6
  26. package/dist/{executor-XJ6RP4Q2.js → executor-LOYMPU67.js} +3 -3
  27. package/dist/index.js +269 -331
  28. package/dist/{nginx-3L4SIBJA.js → nginx-QE4DOM5J.js} +4 -4
  29. package/dist/{openresty-lua-4WLGDSER.js → openresty-lua-BTAKECCF.js} +2 -2
  30. package/dist/server/index.js +51643 -45322
  31. package/dist/server/migrations/0073_project_volumes_object_storage.sql +3 -0
  32. package/dist/server/migrations/0074_project_compose_path.sql +1 -0
  33. package/dist/server/migrations/0075_service_command_argv.sql +1 -0
  34. package/dist/server/migrations/0076_rollback_retention.sql +17 -0
  35. package/dist/server/migrations/0077_domain_redirect.sql +13 -0
  36. package/dist/server/migrations/0078_grant_source_scope.sql +30 -0
  37. package/dist/server/migrations/0079_project_readiness.sql +18 -0
  38. package/dist/server/migrations/0080_deployment_error_classification.sql +26 -0
  39. package/dist/server/migrations/0081_project_readiness_repair.sql +34 -0
  40. package/dist/server/migrations/meta/_journal.json +63 -0
  41. package/dist/{setup-NU6T2PDU.js → setup-KELALK7H.js} +7 -7
  42. package/package.json +1 -1
@@ -52,8 +52,15 @@ var LANGUAGES = {
52
52
  },
53
53
  php: {
54
54
  name: "PHP",
55
- buildImage: "php:8.3-cli",
56
- runtimeImage: "php:8.3-fpm",
55
+ buildImage: "php:8.4-cli",
56
+ // FrankenPHP, not php:*-fpm. An fpm image is a FastCGI backend, not a
57
+ // process host: it ships no web server, and pairing it with an apt nginx
58
+ // means two processes under a shell that swallows SIGTERM — a container stop
59
+ // then kills in-flight work instead of draining it. FrankenPHP is ONE
60
+ // process that owns both the HTTP server and PHP, so signals land where they
61
+ // should with no supervision tree, it runs fine as a non-root user, and its
62
+ // docroot convention (`/app/public`) is already the layout our recipes emit.
63
+ runtimeImage: "dunglas/frankenphp:1-php8.4-bookworm",
57
64
  packageManagers: ["composer"],
58
65
  requiredTools: ["php", "composer"]
59
66
  },
@@ -515,18 +522,28 @@ var STACKS = {
515
522
  }
516
523
  },
517
524
  // ── PHP ────────────────────────────────────────────────────────────────────
518
- // PHP stacks run php-fpm behind nginx (docroot public/). The generated
519
- // Dockerfile (docker-build-plan.ts PHP branch) installs nginx + writes the
520
- // config template; this start command renders it for the injected $PORT and
521
- // launches both processes. Runtime image inherits php:8.3-fpm from the language.
525
+ // PHP stacks serve `public/` from FrankenPHP (see LANGUAGES.php). `exec` hands
526
+ // the container's PID to frankenphp so SIGTERM drains in-flight requests
527
+ // instead of being swallowed by the shell; SERVER_NAME carries the injected
528
+ // $PORT (a bare `:port` also keeps Caddy's automatic HTTPS out of the way —
529
+ // TLS terminates at the edge). No build command: `composer install` is the
530
+ // INSTALL step, and the build step is where a JS asset pipeline goes (the
531
+ // detector fills it in when the repo has one).
522
532
  laravel: {
523
533
  name: "Laravel",
524
534
  language: "php",
525
535
  category: "fullstack",
526
536
  outputDirectory: "public",
527
537
  defaultPort: 8e3,
528
- defaultBuildCommand: "composer install --no-dev --optimize-autoloader",
529
- defaultStartCommand: "envsubst '$PORT' < /etc/nginx/app.conf.template > /etc/nginx/conf.d/default.conf && php-fpm -D && nginx -g 'daemon off;'",
538
+ defaultBuildCommand: "",
539
+ defaultStartCommand: 'SERVER_NAME=":$PORT" exec frankenphp run --config /etc/frankenphp/Caddyfile --adapter caddyfile',
540
+ // Everything a stock Laravel app writes lives here: uploads
541
+ // (storage/app), sessions + cache when the drivers are `file`, and the
542
+ // framework's own scratch space. `database/` is deliberately NOT persisted —
543
+ // it holds migrations, so mounting over it would hide the ones a later
544
+ // release adds. A SQLite app should point DB_DATABASE at a persisted path or
545
+ // (better) use a database service.
546
+ persistentPaths: ["storage"],
530
547
  detection: {
531
548
  rootMarkers: ["artisan", "composer.json"],
532
549
  deps: ["laravel/framework"]
@@ -538,8 +555,10 @@ var STACKS = {
538
555
  category: "fullstack",
539
556
  outputDirectory: "public",
540
557
  defaultPort: 8e3,
541
- defaultBuildCommand: "composer install --no-dev --optimize-autoloader",
542
- defaultStartCommand: "envsubst '$PORT' < /etc/nginx/app.conf.template > /etc/nginx/conf.d/default.conf && php-fpm -D && nginx -g 'daemon off;'",
558
+ defaultBuildCommand: "",
559
+ defaultStartCommand: 'SERVER_NAME=":$PORT" exec frankenphp run --config /etc/frankenphp/Caddyfile --adapter caddyfile',
560
+ // No persistentPaths: Symfony's `var/` is cache + logs, both regenerated,
561
+ // and it has no convention for where user uploads land.
543
562
  detection: {
544
563
  rootMarkers: ["composer.json", "symfony.lock"],
545
564
  deps: ["symfony/framework-bundle"]
@@ -778,6 +797,9 @@ var STACK_ROOT_MARKERS = new Set(
778
797
  Object.values(STACKS).flatMap((stack) => stack.detection?.rootMarkers ?? []).map((marker) => marker.toLowerCase())
779
798
  );
780
799
  function packageManagerEnsureCommand(packageManager) {
800
+ if (packageManager === "bun") {
801
+ return `(command -v bun >/dev/null 2>&1 || npm i -g bun) >/dev/null 2>&1 || true`;
802
+ }
781
803
  if (packageManager !== "pnpm" && packageManager !== "yarn") return "";
782
804
  return `(corepack enable ${packageManager} || corepack enable || npm i -g ${packageManager}) >/dev/null 2>&1 || true`;
783
805
  }
@@ -841,6 +863,22 @@ var STACK_ICONS = {
841
863
  "docker-compose": `${DI}/docker/docker-original.svg`
842
864
  };
843
865
 
866
+ // ../../packages/core/src/volumes.ts
867
+ var APP_DIR = "/app";
868
+ var MODE_SUFFIX = /:(ro|rw|z|Z|nocopy)$/;
869
+ function appVolumeTargets(volumes, appDir = APP_DIR) {
870
+ const out = [];
871
+ for (const spec of volumes) {
872
+ const body = spec.replace(MODE_SUFFIX, "");
873
+ const idx = body.indexOf(":");
874
+ if (idx < 0) continue;
875
+ const target = body.slice(idx + 1);
876
+ if (!target.startsWith(`${appDir}/`)) continue;
877
+ out.push(target.slice(appDir.length + 1));
878
+ }
879
+ return out;
880
+ }
881
+
844
882
  // ../../packages/core/src/constants.ts
845
883
  var DEFAULT_IMAGE_REGISTRY = "ghcr.io/oblien";
846
884
  var PLANS = {
@@ -1017,7 +1055,35 @@ var SYSTEM = {
1017
1055
  /** Maximum length of the error message stored in DB */
1018
1056
  MAX_ERROR_MESSAGE_LENGTH: 512,
1019
1057
  /** Default restart policy for production containers */
1020
- DEFAULT_RESTART_POLICY: "always"
1058
+ DEFAULT_RESTART_POLICY: "always",
1059
+ /**
1060
+ * How long a just-started container is watched before a deploy may call
1061
+ * itself ready. Container-create succeeding proves nothing: a bad start
1062
+ * command exits within milliseconds and `restart: always` hides it behind a
1063
+ * bounce loop. Measured from each container's OWN start time, so a stack
1064
+ * whose earlier services have already been up this long waits only for the
1065
+ * last one.
1066
+ */
1067
+ STABILIZE_WINDOW_MS: 15e3,
1068
+ /** Inspect poll interval inside that window. */
1069
+ STABILIZE_POLL_MS: 1e3,
1070
+ /**
1071
+ * Restarts within the window that mean "crash loop" rather than "waited for
1072
+ * a dependency and recovered". Docker's restart backoff (100ms doubling)
1073
+ * takes an instantly-exiting process past this in ~2s, while a service that
1074
+ * times out waiting on a slow database manages one or two.
1075
+ */
1076
+ STABILIZE_CRASH_RESTARTS: 3,
1077
+ /** Log lines folded into the failure message, so diagnosing needs no SSH. */
1078
+ STABILIZE_LOG_TAIL_LINES: 20,
1079
+ /**
1080
+ * Readiness-probe budget when a project OPTS IN to the health check. Only
1081
+ * reached with `readiness.enabled` — an unconfigured deploy runs no probe
1082
+ * at all, so this never sits on the default critical path.
1083
+ */
1084
+ READINESS_TIMEOUT_MS: 45e3,
1085
+ /** Readiness-probe poll interval. */
1086
+ READINESS_INTERVAL_MS: 1e3
1021
1087
  },
1022
1088
  // ── SSE / Build Streaming ────────────────────────────────────────────
1023
1089
  SSE: {
@@ -1071,11 +1137,12 @@ var SYSTEM = {
1071
1137
  /** Port range */
1072
1138
  MIN_PORT: 1,
1073
1139
  MAX_PORT: 65535,
1074
- /** Resource limits */
1075
- MIN_CPU_CORES: 0.25,
1076
- MAX_CPU_CORES: 4,
1077
- MIN_MEMORY_MB: 128,
1078
- MAX_MEMORY_MB: 8192,
1140
+ // Resource bounds deliberately do NOT live here. They belong to
1141
+ // ./resources.ts, which is the single source of truth: the floors
1142
+ // (MIN_CPU_CORES / MIN_MEMORY_MB) plus the rule that the UPPER bound is the
1143
+ // target machine's probed capacity, not a constant. The unread
1144
+ // MAX_CPU_CORES: 4 / MAX_MEMORY_MB: 8192 pair that used to sit here encoded
1145
+ // exactly the fixed ceiling that made a large self-hosted box unusable.
1079
1146
  /** Pagination */
1080
1147
  DEFAULT_PAGE: 1,
1081
1148
  DEFAULT_PER_PAGE: 20,
@@ -1083,6 +1150,41 @@ var SYSTEM = {
1083
1150
  }
1084
1151
  };
1085
1152
 
1153
+ // ../../packages/core/src/utils.ts
1154
+ var REDIRECT_STATUSES = [301, 302, 307, 308];
1155
+ var DEFAULT_REDIRECT_STATUS = 301;
1156
+ function resolveRedirectStatus(status) {
1157
+ return REDIRECT_STATUSES.includes(status) ? status : DEFAULT_REDIRECT_STATUS;
1158
+ }
1159
+ function isValidEmail(value) {
1160
+ const email3 = value?.trim() ?? "";
1161
+ if (!email3 || email3.length > 254) return false;
1162
+ return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/.test(email3);
1163
+ }
1164
+ function formatBytes(bytes, decimals = 2) {
1165
+ if (bytes <= 0) return "0 B";
1166
+ const k = 1024;
1167
+ const sizes = ["B", "KB", "MB", "GB", "TB"];
1168
+ const i = Math.min(Math.max(Math.floor(Math.log(bytes) / Math.log(k)), 0), sizes.length - 1);
1169
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
1170
+ }
1171
+ function withTimeout(promise2, ms, message) {
1172
+ if (!ms) return promise2;
1173
+ return new Promise((resolve, reject) => {
1174
+ const timer = setTimeout(() => reject(new Error(message)), ms);
1175
+ promise2.then(
1176
+ (v) => {
1177
+ clearTimeout(timer);
1178
+ resolve(v);
1179
+ },
1180
+ (e) => {
1181
+ clearTimeout(timer);
1182
+ reject(e);
1183
+ }
1184
+ );
1185
+ });
1186
+ }
1187
+
1086
1188
  // ../../packages/core/src/errors.ts
1087
1189
  var AppError = class extends Error {
1088
1190
  constructor(message, statusCode = 500, code) {
@@ -1106,6 +1208,12 @@ function safeErrorMessage(err) {
1106
1208
  return String(err).slice(0, 2e3);
1107
1209
  }
1108
1210
 
1211
+ // ../../packages/core/src/types.ts
1212
+ var OPENSHIP_READINESS_FAILURE_ACTIONS = [
1213
+ "warn",
1214
+ "fail"
1215
+ ];
1216
+
1109
1217
  // ../../packages/core/src/openship-config/schema.ts
1110
1218
  var OPENSHIP_RUNTIMES = ["bare", "docker"];
1111
1219
  var OPENSHIP_PRODUCTION_MODES = [
@@ -1121,6 +1229,7 @@ var OPENSHIP_RESTARTS = [
1121
1229
  "unless-stopped"
1122
1230
  ];
1123
1231
  var OPENSHIP_RESOURCE_TIERS = [
1232
+ "unlimited",
1124
1233
  "micro",
1125
1234
  "low",
1126
1235
  "medium",
@@ -1133,12 +1242,14 @@ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
1133
1242
  "framework",
1134
1243
  "packageManager",
1135
1244
  "rootDirectory",
1245
+ "composePath",
1136
1246
  "installCommand",
1137
1247
  "buildCommand",
1138
1248
  "startCommand",
1139
1249
  "outputDirectory",
1140
1250
  "buildImage",
1141
1251
  "productionPaths",
1252
+ "volumes",
1142
1253
  "runtime",
1143
1254
  "productionMode",
1144
1255
  "port",
@@ -1146,6 +1257,7 @@ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
1146
1257
  "domains",
1147
1258
  "routes",
1148
1259
  "resources",
1260
+ "readiness",
1149
1261
  "services",
1150
1262
  "monorepo"
1151
1263
  ]);
@@ -1320,12 +1432,28 @@ function parseResources(ctx, v, path) {
1320
1432
  }
1321
1433
  const r = {
1322
1434
  tier: ctx.enumOf(v.tier, `${path}.tier`, OPENSHIP_RESOURCE_TIERS),
1323
- cpuCores: ctx.int(v.cpuCores, `${path}.cpuCores`, 0.25, 4),
1324
- memoryMb: ctx.int(v.memoryMb, `${path}.memoryMb`, 128, 8192),
1325
- diskMb: ctx.int(v.diskMb, `${path}.diskMb`, 64, 204800)
1435
+ cpuCores: ctx.int(v.cpuCores, `${path}.cpuCores`, 0, 1024),
1436
+ memoryMb: ctx.int(v.memoryMb, `${path}.memoryMb`, 0, 4194304),
1437
+ diskMb: ctx.int(v.diskMb, `${path}.diskMb`, 0, 204800)
1326
1438
  };
1327
1439
  return r;
1328
1440
  }
1441
+ function parseReadiness(ctx, v, path) {
1442
+ if (v === void 0) return void 0;
1443
+ if (!ctx.isObj(v)) {
1444
+ ctx.err(path, "must be an object");
1445
+ return void 0;
1446
+ }
1447
+ return {
1448
+ enabled: ctx.bool(v.enabled, `${path}.enabled`),
1449
+ path: ctx.str(v.path, `${path}.path`),
1450
+ port: ctx.int(v.port, `${path}.port`, 1, 65535),
1451
+ timeoutSeconds: ctx.int(v.timeoutSeconds, `${path}.timeoutSeconds`, 1, 600),
1452
+ stabilization: ctx.bool(v.stabilization, `${path}.stabilization`),
1453
+ stabilizationSeconds: ctx.int(v.stabilizationSeconds, `${path}.stabilizationSeconds`, 1, 600),
1454
+ onFailure: ctx.enumOf(v.onFailure, `${path}.onFailure`, OPENSHIP_READINESS_FAILURE_ACTIONS)
1455
+ };
1456
+ }
1329
1457
  function parseHealthcheck(ctx, v, path) {
1330
1458
  if (v === void 0) return void 0;
1331
1459
  if (!ctx.isObj(v)) {
@@ -1374,7 +1502,9 @@ function parseServices(ctx, v, path) {
1374
1502
  exposed: ctx.bool(item.exposed, `${p}.exposed`),
1375
1503
  exposedPort: ctx.str(item.exposedPort, `${p}.exposedPort`),
1376
1504
  domain: ctx.str(item.domain, `${p}.domain`),
1377
- healthcheck: parseHealthcheck(ctx, item.healthcheck, `${p}.healthcheck`)
1505
+ healthcheck: parseHealthcheck(ctx, item.healthcheck, `${p}.healthcheck`),
1506
+ readiness: parseReadiness(ctx, item.readiness, `${p}.readiness`),
1507
+ resources: parseResources(ctx, item.resources, `${p}.resources`)
1378
1508
  });
1379
1509
  });
1380
1510
  return out;
@@ -1458,12 +1588,14 @@ function parseOpenshipConfig(raw) {
1458
1588
  framework: ctx.enumOf(raw.framework, "framework", STACK_IDS),
1459
1589
  packageManager: parsePackageManager(ctx, raw.packageManager, "packageManager"),
1460
1590
  rootDirectory: ctx.str(raw.rootDirectory, "rootDirectory"),
1591
+ composePath: ctx.str(raw.composePath, "composePath"),
1461
1592
  installCommand: ctx.str(raw.installCommand, "installCommand"),
1462
1593
  buildCommand: ctx.str(raw.buildCommand, "buildCommand"),
1463
1594
  startCommand: ctx.str(raw.startCommand, "startCommand"),
1464
1595
  outputDirectory: ctx.str(raw.outputDirectory, "outputDirectory"),
1465
1596
  buildImage: ctx.str(raw.buildImage, "buildImage"),
1466
1597
  productionPaths: ctx.strArray(raw.productionPaths, "productionPaths"),
1598
+ volumes: ctx.strArray(raw.volumes, "volumes"),
1467
1599
  runtime: ctx.enumOf(raw.runtime, "runtime", OPENSHIP_RUNTIMES),
1468
1600
  productionMode: ctx.enumOf(raw.productionMode, "productionMode", OPENSHIP_PRODUCTION_MODES),
1469
1601
  port: ctx.int(raw.port, "port", 1, 65535),
@@ -1471,6 +1603,7 @@ function parseOpenshipConfig(raw) {
1471
1603
  domains: parseDomains(ctx, raw.domains, "domains"),
1472
1604
  routes: parseRoutes(ctx, raw.routes, "routes"),
1473
1605
  resources: parseResources(ctx, raw.resources, "resources"),
1606
+ readiness: parseReadiness(ctx, raw.readiness, "readiness"),
1474
1607
  services: parseServices(ctx, raw.services, "services"),
1475
1608
  monorepo: parseMonorepo(ctx, raw.monorepo, "monorepo")
1476
1609
  };
@@ -1888,34 +2021,57 @@ function sanitizeProxySettings(input) {
1888
2021
  return Object.keys(out).length > 0 ? out : void 0;
1889
2022
  }
1890
2023
 
1891
- // ../../packages/core/src/utils.ts
1892
- function isValidEmail(value) {
1893
- const email3 = value?.trim() ?? "";
1894
- if (!email3 || email3.length > 254) return false;
1895
- return /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/.test(email3);
1896
- }
1897
- function formatBytes(bytes, decimals = 2) {
1898
- if (bytes <= 0) return "0 B";
1899
- const k = 1024;
1900
- const sizes = ["B", "KB", "MB", "GB", "TB"];
1901
- const i = Math.min(Math.max(Math.floor(Math.log(bytes) / Math.log(k)), 0), sizes.length - 1);
1902
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(decimals))} ${sizes[i]}`;
2024
+ // ../../packages/core/src/object-storage.ts
2025
+ function isAwsS3Endpoint(endpoint2) {
2026
+ return !endpoint2 || endpoint2.includes(".amazonaws.com");
1903
2027
  }
1904
- function withTimeout(promise2, ms, message) {
1905
- if (!ms) return promise2;
1906
- return new Promise((resolve, reject) => {
1907
- const timer = setTimeout(() => reject(new Error(message)), ms);
1908
- promise2.then(
1909
- (v) => {
1910
- clearTimeout(timer);
1911
- resolve(v);
1912
- },
1913
- (e) => {
1914
- clearTimeout(timer);
1915
- reject(e);
2028
+
2029
+ // ../../packages/core/src/shell-split.ts
2030
+ function shellSplitWords(value) {
2031
+ const words = [];
2032
+ let current = "";
2033
+ let quoted = false;
2034
+ let inSingle = false;
2035
+ let inDouble = false;
2036
+ let escaped = false;
2037
+ for (const char of value.trim()) {
2038
+ if (escaped) {
2039
+ current += char;
2040
+ escaped = false;
2041
+ continue;
2042
+ }
2043
+ if (char === "\\" && !inSingle) {
2044
+ escaped = true;
2045
+ continue;
2046
+ }
2047
+ if (char === "'" && !inDouble) {
2048
+ inSingle = !inSingle;
2049
+ quoted = true;
2050
+ continue;
2051
+ }
2052
+ if (char === '"' && !inSingle) {
2053
+ inDouble = !inDouble;
2054
+ quoted = true;
2055
+ continue;
2056
+ }
2057
+ if (/\s/.test(char) && !inSingle && !inDouble) {
2058
+ if (current || quoted) {
2059
+ words.push(current);
2060
+ current = "";
2061
+ quoted = false;
1916
2062
  }
1917
- );
1918
- });
2063
+ continue;
2064
+ }
2065
+ current += char;
2066
+ }
2067
+ if (escaped) current += "\\";
2068
+ if (current || quoted) words.push(current);
2069
+ return words;
2070
+ }
2071
+ function commandToArgv(command) {
2072
+ if (command == null) return null;
2073
+ if (Array.isArray(command)) return command.map((part) => String(part));
2074
+ return shellSplitWords(command);
1919
2075
  }
1920
2076
 
1921
2077
  // ../../packages/core/src/runtime-config.ts
@@ -1977,6 +2133,36 @@ var dashboardRuntimeOrigins = Object.values(DASHBOARD_RUNTIME_TARGETS).flatMap(
1977
2133
  ({ dashboard, api }) => [dashboard, api]
1978
2134
  );
1979
2135
 
2136
+ // ../../packages/core/src/resources.ts
2137
+ var RESOURCE_TIER_ORDER = ["micro", "low", "medium", "high"];
2138
+ var ALL_RESOURCE_TIERS = [
2139
+ "unlimited",
2140
+ ...RESOURCE_TIER_ORDER,
2141
+ "custom"
2142
+ ];
2143
+ var CLOUD_RESOURCE_TIER_IDS = [
2144
+ ...RESOURCE_TIER_ORDER,
2145
+ "custom"
2146
+ ];
2147
+ function hasCpuLimit(r) {
2148
+ return typeof r?.cpuCores === "number" && r.cpuCores > 0;
2149
+ }
2150
+ function hasMemoryLimit(r) {
2151
+ return typeof r?.memoryMb === "number" && r.memoryMb > 0;
2152
+ }
2153
+ function formatMemoryMb(mb) {
2154
+ if (!mb || mb <= 0) return "no limit";
2155
+ if (mb >= 1024) return `${(mb / 1024).toFixed(mb % 1024 === 0 ? 0 : 1)} GB`;
2156
+ return `${mb} MB`;
2157
+ }
2158
+ function formatCpuCores(cores) {
2159
+ if (!cores || cores <= 0) return "no limit";
2160
+ return `${cores} vCPU`;
2161
+ }
2162
+
2163
+ // ../../packages/core/src/rollback-window.ts
2164
+ var ROLLBACK_DISK_BUDGET_CAP_BYTES = 20 * 1024 * 1024 * 1024;
2165
+
1980
2166
  // ../../packages/core/src/workspaces/toml-helpers.ts
1981
2167
  function extractStringArrayFromSection(content, sectionPath, key) {
1982
2168
  const sectionHeader = new RegExp(
@@ -2158,14 +2344,33 @@ var goWorkspaceDetector = {
2158
2344
  };
2159
2345
 
2160
2346
  // ../../packages/core/src/workspaces/gradle.ts
2347
+ function readIncludeArgs(content, start) {
2348
+ let i = start;
2349
+ while (content[i] === " " || content[i] === " ") i++;
2350
+ if (content[i] === "(") {
2351
+ let depth = 0;
2352
+ for (let j = i; j < content.length; j++) {
2353
+ if (content[j] === "(") depth++;
2354
+ else if (content[j] === ")" && --depth === 0) return content.slice(i + 1, j);
2355
+ }
2356
+ return content.slice(i + 1);
2357
+ }
2358
+ let args = "";
2359
+ for (; ; ) {
2360
+ const newline = content.indexOf("\n", i);
2361
+ const line = newline === -1 ? content.slice(i) : content.slice(i, newline);
2362
+ args += line;
2363
+ if (newline === -1 || !line.trimEnd().endsWith(",")) return args;
2364
+ i = newline + 1;
2365
+ }
2366
+ }
2161
2367
  function parseGradleSettings(content) {
2162
2368
  const cleaned = content.replace(/\/\*[\s\S]*?\*\//g, "").split("\n").map((line) => line.replace(/\/\/.*$/, "")).join("\n");
2163
2369
  const paths = [];
2164
- const includePattern = /include\s*(?:\(\s*)?((?:[^()\n]+))(?:\))?/g;
2370
+ const includePattern = /\binclude\b/g;
2165
2371
  let match;
2166
2372
  while ((match = includePattern.exec(cleaned)) !== null) {
2167
- const args = match[1];
2168
- if (/^Build\b/.test(args)) continue;
2373
+ const args = readIncludeArgs(cleaned, match.index + match[0].length);
2169
2374
  const stringPattern = /"([^"]+)"|'([^']+)'/g;
2170
2375
  let stringMatch;
2171
2376
  while ((stringMatch = stringPattern.exec(args)) !== null) {
@@ -2190,9 +2395,49 @@ var gradleWorkspaceDetector = {
2190
2395
  parseSubProjects: parseGradleSettings
2191
2396
  };
2192
2397
 
2398
+ // ../../packages/core/src/workspaces/comments.ts
2399
+ function stripJsonComments(content) {
2400
+ let out = "";
2401
+ let inString = false;
2402
+ for (let i = 0; i < content.length; i++) {
2403
+ const ch = content[i];
2404
+ if (inString) {
2405
+ out += ch;
2406
+ if (ch === "\\") {
2407
+ out += content[++i] ?? "";
2408
+ continue;
2409
+ }
2410
+ if (ch === '"') inString = false;
2411
+ continue;
2412
+ }
2413
+ if (ch === '"') {
2414
+ inString = true;
2415
+ out += ch;
2416
+ continue;
2417
+ }
2418
+ if (ch === "/" && content[i + 1] === "/") {
2419
+ const newline = content.indexOf("\n", i);
2420
+ if (newline === -1) break;
2421
+ i = newline - 1;
2422
+ continue;
2423
+ }
2424
+ if (ch === "/" && content[i + 1] === "*") {
2425
+ const end = content.indexOf("*/", i + 2);
2426
+ if (end === -1) break;
2427
+ i = end + 1;
2428
+ continue;
2429
+ }
2430
+ out += ch;
2431
+ }
2432
+ return out;
2433
+ }
2434
+ function stripXmlComments(content) {
2435
+ return content.replace(/<!--[\s\S]*?-->/g, "");
2436
+ }
2437
+
2193
2438
  // ../../packages/core/src/workspaces/maven.ts
2194
2439
  function parsePomXml(content) {
2195
- const modulesBlock = content.match(/<modules\b[^>]*>([\s\S]*?)<\/modules>/i);
2440
+ const modulesBlock = stripXmlComments(content).match(/<modules\b[^>]*>([\s\S]*?)<\/modules>/i);
2196
2441
  if (!modulesBlock) return [];
2197
2442
  const moduleEntries = modulesBlock[1].matchAll(/<module\b[^>]*>\s*([\s\S]*?)\s*<\/module>/gi);
2198
2443
  const paths = [];
@@ -2289,7 +2534,7 @@ var uvWorkspaceDetector = {
2289
2534
  function parseRushJson(content) {
2290
2535
  let parsed = null;
2291
2536
  try {
2292
- parsed = JSON.parse(content);
2537
+ parsed = JSON.parse(stripJsonComments(content));
2293
2538
  } catch {
2294
2539
  return [];
2295
2540
  }
@@ -2367,8 +2612,11 @@ var dockerLanguageDetector = {
2367
2612
  // ../../packages/core/src/languages/elixir.ts
2368
2613
  function parseMixExs2(content) {
2369
2614
  const deps = {};
2370
- for (const m of content.matchAll(/\{:([\w]+),/g)) {
2371
- deps[m[1]] = "*";
2615
+ for (const line of content.split("\n")) {
2616
+ if (line.trim().startsWith("#")) continue;
2617
+ for (const m of line.matchAll(/\{:([\w]+),/g)) {
2618
+ deps[m[1]] = "*";
2619
+ }
2372
2620
  }
2373
2621
  return deps;
2374
2622
  }
@@ -2416,6 +2664,34 @@ function clampPort(raw) {
2416
2664
  const port = parseInt(raw, 10);
2417
2665
  return port > 0 && port <= 65535 ? port : null;
2418
2666
  }
2667
+ var YAML_PORT_RE = /^port:\s*["']?(\d{2,5})/;
2668
+ function springYamlPort(yml) {
2669
+ const inline = yml.match(/^server:\s*\{[^}]*\bport:\s*["']?(\d{2,5})/m);
2670
+ if (inline) return inline[1];
2671
+ let inServer = false;
2672
+ let childIndent = null;
2673
+ for (const raw of yml.split("\n")) {
2674
+ const trimmed2 = raw.trim();
2675
+ if (!trimmed2 || trimmed2.startsWith("#")) continue;
2676
+ const indent = raw.length - raw.trimStart().length;
2677
+ if (inServer) {
2678
+ if (indent > 0) {
2679
+ if (childIndent === null) childIndent = indent;
2680
+ if (indent === childIndent) {
2681
+ const m = trimmed2.match(YAML_PORT_RE);
2682
+ if (m) return m[1];
2683
+ }
2684
+ continue;
2685
+ }
2686
+ inServer = false;
2687
+ }
2688
+ if (indent === 0 && /^server:$/.test(trimmed2)) {
2689
+ inServer = true;
2690
+ childIndent = null;
2691
+ }
2692
+ }
2693
+ return null;
2694
+ }
2419
2695
  function detectJavaPort(context) {
2420
2696
  const fc = context.fileContents;
2421
2697
  if (!fc) return null;
@@ -2427,8 +2703,8 @@ function detectJavaPort(context) {
2427
2703
  for (const name of ["application.yml", "application.yaml"]) {
2428
2704
  const yml = fc[name];
2429
2705
  if (!yml) continue;
2430
- const m = yml.match(/server:[\s\S]*?\bport:\s*["']?(\d{2,5})/) ?? yml.match(/^\s*port:\s*["']?(\d{2,5})/m);
2431
- if (m) return clampPort(m[1]);
2706
+ const port = springYamlPort(yml) ?? yml.match(/^port:\s*["']?(\d{2,5})/m)?.[1];
2707
+ if (port) return clampPort(port);
2432
2708
  }
2433
2709
  return null;
2434
2710
  }
@@ -2515,11 +2791,30 @@ function parseRequirementsTxt(content) {
2515
2791
  }
2516
2792
  return deps;
2517
2793
  }
2794
+ function sliceArrayValue(body, key) {
2795
+ const open = body.match(new RegExp(`(?:^|\\n)\\s*${key}\\s*=\\s*\\[`));
2796
+ if (!open || open.index === void 0) return null;
2797
+ const from = open.index + open[0].length;
2798
+ let depth = 1;
2799
+ let quote = null;
2800
+ for (let i = from; i < body.length; i++) {
2801
+ const char = body[i];
2802
+ if (quote) {
2803
+ if (char === quote) quote = null;
2804
+ continue;
2805
+ }
2806
+ if (char === '"' || char === "'") quote = char;
2807
+ else if (char === "[") depth++;
2808
+ else if (char === "]" && --depth === 0) return body.slice(from, i);
2809
+ }
2810
+ return null;
2811
+ }
2518
2812
  function parsePyprojectToml(content) {
2519
2813
  const deps = {};
2520
- const pep621 = content.match(/\[project\][^[]*?dependencies\s*=\s*\[([\s\S]*?)\]/);
2521
- if (pep621) {
2522
- const items = pep621[1].matchAll(/["']([^"']+)["']/g);
2814
+ const project = content.match(/\[project\]([\s\S]*?)(?=\n\[|$)/);
2815
+ const pep621 = project ? sliceArrayValue(project[1], "dependencies") : null;
2816
+ if (pep621 !== null) {
2817
+ const items = pep621.matchAll(/["']([^"']+)["']/g);
2523
2818
  for (const item of items) {
2524
2819
  const m = item[1].match(/^([A-Za-z0-9][A-Za-z0-9._-]*)/);
2525
2820
  if (m) deps[normalizeName(m[1])] = "*";
@@ -2576,8 +2871,11 @@ var pythonLanguageDetector = {
2576
2871
  // ../../packages/core/src/languages/ruby.ts
2577
2872
  function parseGemfile(content) {
2578
2873
  const deps = {};
2579
- for (const m of content.matchAll(/gem\s+['"]([^'"]+)['"]/g)) {
2580
- deps[m[1].toLowerCase()] = "*";
2874
+ for (const line of content.split("\n")) {
2875
+ if (line.trim().startsWith("#")) continue;
2876
+ for (const m of line.matchAll(/gem\s+['"]([^'"]+)['"]/g)) {
2877
+ deps[m[1].toLowerCase()] = "*";
2878
+ }
2581
2879
  }
2582
2880
  return deps;
2583
2881
  }
@@ -4060,7 +4358,7 @@ var catalog_default = {
4060
4358
  verified: true,
4061
4359
  id: "minio",
4062
4360
  name: "MinIO",
4063
- description: "S3-compatible object storage with a web console \u2014 point any S3 client (aws-cli, SDKs) at it.",
4361
+ description: "S3-compatible object storage with a web console \u2014 point any S3 client (aws-cli, SDKs) at it, or bind a bucket straight into another project's uploads.",
4064
4362
  kind: "template",
4065
4363
  logo: "minio",
4066
4364
  category: "database",
@@ -4119,6 +4417,29 @@ var catalog_default = {
4119
4417
  help: "Auto-generated. The S3 admin secret \u2014 shown once after install.",
4120
4418
  generate: "secret",
4121
4419
  secret: true
4420
+ },
4421
+ {
4422
+ key: "OPENSHIP_BUCKET",
4423
+ service: "minio",
4424
+ label: "First bucket",
4425
+ help: "Created on the first deploy so an app can use this storage immediately. More can be added from the console.",
4426
+ type: "text",
4427
+ default: "uploads",
4428
+ required: true
4429
+ }
4430
+ ],
4431
+ prepare: [
4432
+ {
4433
+ service: "minio",
4434
+ command: `mc alias set local http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" > /dev/null && mc mb --ignore-existing "local/$OPENSHIP_BUCKET" > /dev/null && printf '%s' "$OPENSHIP_BUCKET"`,
4435
+ capture: "bucket",
4436
+ phase: "post-ready",
4437
+ readiness: {
4438
+ test: 'mc alias set local http://127.0.0.1:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"',
4439
+ interval: 1e3,
4440
+ retries: 30
4441
+ },
4442
+ once: true
4122
4443
  }
4123
4444
  ],
4124
4445
  endpoints: [
@@ -4147,21 +4468,54 @@ var catalog_default = {
4147
4468
  {
4148
4469
  id: "endpoint",
4149
4470
  label: "S3 endpoint",
4150
- source: "publicUrl:minio:9000"
4471
+ source: "publicUrl:minio:9000",
4472
+ service: "minio",
4473
+ envKey: "S3_ENDPOINT",
4474
+ recommended: true
4151
4475
  },
4152
4476
  {
4153
4477
  id: "accessKey",
4154
4478
  label: "Access key",
4155
- source: "env:minio:MINIO_ROOT_USER"
4479
+ source: "env:minio:MINIO_ROOT_USER",
4480
+ service: "minio",
4481
+ envKey: "S3_ACCESS_KEY_ID",
4482
+ recommended: true
4156
4483
  },
4157
4484
  {
4158
4485
  id: "secretKey",
4159
4486
  label: "Secret key",
4160
4487
  source: "env:minio:MINIO_ROOT_PASSWORD",
4161
- secret: true
4488
+ service: "minio",
4489
+ envKey: "S3_SECRET_ACCESS_KEY",
4490
+ secret: true,
4491
+ recommended: true
4492
+ },
4493
+ {
4494
+ id: "bucket",
4495
+ label: "Bucket",
4496
+ source: "env:minio:OPENSHIP_BUCKET",
4497
+ service: "minio",
4498
+ envKey: "S3_BUCKET"
4162
4499
  }
4163
- ]
4164
- }
4500
+ ],
4501
+ guide: {
4502
+ intro: "Your app gets an S3-compatible bucket it can write uploads to.",
4503
+ useHint: "Bind it from the consuming project's Configuration \u2192 Object storage to get the framework's own env names (Laravel gets FILESYSTEM_DISK + AWS_*), or wire these values by hand.",
4504
+ defaultMode: "internal"
4505
+ }
4506
+ },
4507
+ provides: [
4508
+ {
4509
+ id: "s3",
4510
+ outputRefs: [
4511
+ "endpoint",
4512
+ "accessKey",
4513
+ "secretKey",
4514
+ "bucket"
4515
+ ],
4516
+ category: "database"
4517
+ }
4518
+ ]
4165
4519
  },
4166
4520
  {
4167
4521
  available: true,
@@ -19143,9 +19497,14 @@ export {
19143
19497
  buildOutputTransferExcludes,
19144
19498
  PACKAGE_ROOT_ONLY_EXCLUDES,
19145
19499
  packageManagerEnsureCommand,
19500
+ appVolumeTargets,
19501
+ isAwsS3Endpoint,
19146
19502
  DEFAULT_IMAGE_REGISTRY,
19503
+ shellSplitWords,
19504
+ commandToArgv,
19147
19505
  buildEdgeImageRef,
19148
19506
  SYSTEM,
19507
+ resolveRedirectStatus,
19149
19508
  isValidEmail,
19150
19509
  formatBytes,
19151
19510
  withTimeout,
@@ -19155,6 +19514,10 @@ export {
19155
19514
  LOCAL_DASHBOARD_URL,
19156
19515
  LOCAL_API_URL,
19157
19516
  CLOUD_DASHBOARD_URL,
19517
+ hasCpuLimit,
19518
+ hasMemoryLimit,
19519
+ formatMemoryMb,
19520
+ formatCpuCores,
19158
19521
  parseOpenshipConfigJson,
19159
19522
  missingOutputDirectoryMessage,
19160
19523
  compareSemver,