openship 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command25 } from "commander";
4
+ import { Command as Command27 } from "commander";
5
5
 
6
6
  // src/lib/output.ts
7
7
  import chalk from "chalk";
@@ -905,10 +905,17 @@ var PLANS = {
905
905
  description: "Get started for free",
906
906
  price: { monthly: 0, annual: 0 },
907
907
  stripePriceId: { monthly: null, annual: null },
908
- // Paid tiers below are `null` (price + credits) until Openship Cloud pricing
909
- // is finalized the UI renders "coming soon". Self-hosted is free and never
910
- // surfaces any of these numbers.
911
- monthlyCredits: null,
908
+ // Public dollar `price` on the paid tiers is `null` the UI renders
909
+ // "coming soon" and the Subscribe CTA stays disabled. Cloud pricing is
910
+ // intentionally NOT published yet. `monthlyCredits` (the CPU-time/credit
911
+ // quota pushed to Oblien) is real so tiers still enforce — it's an internal
912
+ // number, never shown as a price. NOTE (tune before launch): these credit
913
+ // numbers are placeholders sized to Oblien's 10,000,000-credit ceiling; the
914
+ // true credit-per-$/per-cpu-minute rate is Oblien-configured. 1 openship
915
+ // credit = 1 Oblien credit = 1000 milli (the wrapper divides by 1000 at the
916
+ // Oblien boundary). Self-hosted is free and surfaces none of these numbers.
917
+ monthlyCredits: 5e5,
918
+ // 500 credits — free-tier allowance
912
919
  oblienLimits: {
913
920
  max_workspaces: 1,
914
921
  max_vcpus: 2,
@@ -927,12 +934,13 @@ var PLANS = {
927
934
  name: "Pro",
928
935
  description: "For solo builders shipping production workloads",
929
936
  price: { monthly: null, annual: null },
930
- // coming soon
937
+ // coming soon — pricing not published
931
938
  stripePriceId: {
932
939
  monthly: process.env.STRIPE_PRICE_PRO_MONTHLY ?? "price_pro_monthly_placeholder",
933
940
  annual: process.env.STRIPE_PRICE_PRO_ANNUAL ?? "price_pro_annual_placeholder"
934
941
  },
935
- monthlyCredits: null,
942
+ monthlyCredits: 1e7,
943
+ // 10,000 credits/mo (placeholder — tune before launch)
936
944
  oblienLimits: {
937
945
  max_workspaces: 10,
938
946
  max_vcpus: 16,
@@ -951,12 +959,13 @@ var PLANS = {
951
959
  name: "Team",
952
960
  description: "For teams collaborating on shared infra",
953
961
  price: { monthly: null, annual: null },
954
- // coming soon
962
+ // coming soon — pricing not published
955
963
  stripePriceId: {
956
964
  monthly: process.env.STRIPE_PRICE_TEAM_MONTHLY ?? "price_team_monthly_placeholder",
957
965
  annual: process.env.STRIPE_PRICE_TEAM_ANNUAL ?? "price_team_annual_placeholder"
958
966
  },
959
- monthlyCredits: null,
967
+ monthlyCredits: 6e7,
968
+ // 60,000 credits/mo (placeholder — tune before launch)
960
969
  oblienLimits: {
961
970
  max_workspaces: 50,
962
971
  max_vcpus: 64,
@@ -1798,6 +1807,425 @@ var LANGUAGE_MANIFEST_FILES = Array.from(
1798
1807
  new Set(LANGUAGE_DETECTORS.flatMap((d) => d.manifestFiles.map((f) => f.toLowerCase())))
1799
1808
  );
1800
1809
 
1810
+ // ../../packages/core/src/openship-config/schema.ts
1811
+ var OPENSHIP_RUNTIMES = ["bare", "docker"];
1812
+ var OPENSHIP_PRODUCTION_MODES = [
1813
+ "host",
1814
+ "static",
1815
+ "standalone"
1816
+ ];
1817
+ var OPENSHIP_DOMAIN_TYPES = ["free", "custom"];
1818
+ var OPENSHIP_RESTARTS = [
1819
+ "no",
1820
+ "always",
1821
+ "on-failure",
1822
+ "unless-stopped"
1823
+ ];
1824
+ var OPENSHIP_RESOURCE_TIERS = [
1825
+ "micro",
1826
+ "low",
1827
+ "medium",
1828
+ "high"
1829
+ ];
1830
+
1831
+ // ../../packages/core/src/openship-config/parse.ts
1832
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
1833
+ "$schema",
1834
+ "framework",
1835
+ "packageManager",
1836
+ "rootDirectory",
1837
+ "installCommand",
1838
+ "buildCommand",
1839
+ "startCommand",
1840
+ "outputDirectory",
1841
+ "buildImage",
1842
+ "productionPaths",
1843
+ "runtime",
1844
+ "productionMode",
1845
+ "port",
1846
+ "env",
1847
+ "domains",
1848
+ "routes",
1849
+ "resources",
1850
+ "services",
1851
+ "monorepo"
1852
+ ]);
1853
+ var Ctx = class {
1854
+ errors = [];
1855
+ warnings = [];
1856
+ err(path2, msg) {
1857
+ this.errors.push(`${path2}: ${msg}`);
1858
+ }
1859
+ isObj(v) {
1860
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1861
+ }
1862
+ str(v, path2) {
1863
+ if (v === void 0) return void 0;
1864
+ if (typeof v !== "string") {
1865
+ this.err(path2, "must be a string");
1866
+ return void 0;
1867
+ }
1868
+ return v;
1869
+ }
1870
+ bool(v, path2) {
1871
+ if (v === void 0) return void 0;
1872
+ if (typeof v !== "boolean") {
1873
+ this.err(path2, "must be a boolean");
1874
+ return void 0;
1875
+ }
1876
+ return v;
1877
+ }
1878
+ int(v, path2, min, max) {
1879
+ if (v === void 0) return void 0;
1880
+ const n = typeof v === "string" ? Number(v) : v;
1881
+ if (typeof n !== "number" || !Number.isFinite(n)) {
1882
+ this.err(path2, "must be a number");
1883
+ return void 0;
1884
+ }
1885
+ if (n < min || n > max) {
1886
+ this.err(path2, `must be between ${min} and ${max}`);
1887
+ return void 0;
1888
+ }
1889
+ return n;
1890
+ }
1891
+ strArray(v, path2) {
1892
+ if (v === void 0) return void 0;
1893
+ if (!Array.isArray(v)) {
1894
+ this.err(path2, "must be an array of strings");
1895
+ return void 0;
1896
+ }
1897
+ const out = [];
1898
+ v.forEach((item, i) => {
1899
+ if (typeof item !== "string") this.err(`${path2}[${i}]`, "must be a string");
1900
+ else out.push(item);
1901
+ });
1902
+ return out;
1903
+ }
1904
+ enumOf(v, path2, allowed) {
1905
+ if (v === void 0) return void 0;
1906
+ if (typeof v !== "string" || !allowed.includes(v)) {
1907
+ this.err(path2, `must be one of: ${allowed.join(", ")}`);
1908
+ return void 0;
1909
+ }
1910
+ return v;
1911
+ }
1912
+ };
1913
+ function parseEnv(ctx, v, path2) {
1914
+ if (v === void 0) return void 0;
1915
+ if (!ctx.isObj(v)) {
1916
+ ctx.err(path2, "must be an object of environment variables");
1917
+ return void 0;
1918
+ }
1919
+ const out = {};
1920
+ for (const [key, val] of Object.entries(v)) {
1921
+ if (typeof val === "string") {
1922
+ out[key] = val;
1923
+ } else if (ctx.isObj(val)) {
1924
+ if (val.value === void 0) ctx.err(`${path2}.${key}.value`, "is required");
1925
+ const value = ctx.str(val.value, `${path2}.${key}.value`);
1926
+ const secret = ctx.bool(val.secret, `${path2}.${key}.secret`);
1927
+ if (value !== void 0) out[key] = { value, ...secret !== void 0 ? { secret } : {} };
1928
+ } else {
1929
+ ctx.err(`${path2}.${key}`, 'must be a string or { "value", "secret" }');
1930
+ }
1931
+ }
1932
+ return out;
1933
+ }
1934
+ function parseDomains(ctx, v, path2) {
1935
+ if (v === void 0) return void 0;
1936
+ if (!Array.isArray(v)) {
1937
+ ctx.err(path2, "must be an array of hostnames or domain objects");
1938
+ return void 0;
1939
+ }
1940
+ const out = [];
1941
+ v.forEach((item, i) => {
1942
+ const p = `${path2}[${i}]`;
1943
+ if (typeof item === "string") {
1944
+ out.push({ domain: item });
1945
+ } else if (ctx.isObj(item)) {
1946
+ const domain = ctx.str(item.domain, `${p}.domain`);
1947
+ if (!domain) {
1948
+ ctx.err(p, "requires a `domain`");
1949
+ return;
1950
+ }
1951
+ out.push({
1952
+ domain,
1953
+ port: ctx.int(item.port, `${p}.port`, 1, 65535),
1954
+ targetPath: ctx.str(item.targetPath, `${p}.targetPath`),
1955
+ type: ctx.enumOf(item.type, `${p}.type`, OPENSHIP_DOMAIN_TYPES)
1956
+ });
1957
+ } else {
1958
+ ctx.err(p, "must be a hostname string or a domain object");
1959
+ }
1960
+ });
1961
+ return out;
1962
+ }
1963
+ function parseRoutes(ctx, v, path2) {
1964
+ if (v === void 0) return void 0;
1965
+ if (!ctx.isObj(v)) {
1966
+ ctx.err(path2, "must be an object");
1967
+ return void 0;
1968
+ }
1969
+ const routes = {};
1970
+ const rule = (item, p) => {
1971
+ if (!ctx.isObj(item)) {
1972
+ ctx.err(p, "must be an object with `source` and `destination`");
1973
+ return null;
1974
+ }
1975
+ const source = ctx.str(item.source, `${p}.source`);
1976
+ const destination = ctx.str(item.destination, `${p}.destination`);
1977
+ return source && destination ? { source, destination } : null;
1978
+ };
1979
+ if (Array.isArray(v.rewrites)) {
1980
+ routes.rewrites = v.rewrites.map((r, i) => rule(r, `${path2}.rewrites[${i}]`)).filter(Boolean);
1981
+ } else if (v.rewrites !== void 0) ctx.err(`${path2}.rewrites`, "must be an array");
1982
+ if (Array.isArray(v.redirects)) {
1983
+ routes.redirects = v.redirects.map((r, i) => {
1984
+ const base = rule(r, `${path2}.redirects[${i}]`);
1985
+ if (!base) return null;
1986
+ const o = r;
1987
+ return {
1988
+ ...base,
1989
+ permanent: ctx.bool(o.permanent, `${path2}.redirects[${i}].permanent`),
1990
+ statusCode: ctx.int(o.statusCode, `${path2}.redirects[${i}].statusCode`, 300, 399)
1991
+ };
1992
+ }).filter(Boolean);
1993
+ } else if (v.redirects !== void 0) ctx.err(`${path2}.redirects`, "must be an array");
1994
+ if (Array.isArray(v.headers)) {
1995
+ routes.headers = v.headers.map((h, i) => {
1996
+ const p = `${path2}.headers[${i}]`;
1997
+ if (!ctx.isObj(h)) {
1998
+ ctx.err(p, "must be an object");
1999
+ return null;
2000
+ }
2001
+ const source = ctx.str(h.source, `${p}.source`);
2002
+ const list2 = Array.isArray(h.headers) ? h.headers.map((kv, j) => {
2003
+ const key = ctx.str(kv?.key, `${p}.headers[${j}].key`);
2004
+ const value = ctx.str(kv?.value, `${p}.headers[${j}].value`);
2005
+ return key && value !== void 0 ? { key, value } : null;
2006
+ }).filter(Boolean) : [];
2007
+ return source ? { source, headers: list2 } : null;
2008
+ }).filter(Boolean);
2009
+ } else if (v.headers !== void 0) ctx.err(`${path2}.headers`, "must be an array");
2010
+ const cleanUrls = ctx.bool(v.cleanUrls, `${path2}.cleanUrls`);
2011
+ const trailingSlash = ctx.bool(v.trailingSlash, `${path2}.trailingSlash`);
2012
+ if (cleanUrls !== void 0) routes.cleanUrls = cleanUrls;
2013
+ if (trailingSlash !== void 0) routes.trailingSlash = trailingSlash;
2014
+ return routes;
2015
+ }
2016
+ function parseResources(ctx, v, path2) {
2017
+ if (v === void 0) return void 0;
2018
+ if (!ctx.isObj(v)) {
2019
+ ctx.err(path2, "must be an object");
2020
+ return void 0;
2021
+ }
2022
+ const r = {
2023
+ tier: ctx.enumOf(v.tier, `${path2}.tier`, OPENSHIP_RESOURCE_TIERS),
2024
+ cpuCores: ctx.int(v.cpuCores, `${path2}.cpuCores`, 0.25, 4),
2025
+ memoryMb: ctx.int(v.memoryMb, `${path2}.memoryMb`, 128, 8192),
2026
+ diskMb: ctx.int(v.diskMb, `${path2}.diskMb`, 64, 204800)
2027
+ };
2028
+ return r;
2029
+ }
2030
+ function parseHealthcheck(ctx, v, path2) {
2031
+ if (v === void 0) return void 0;
2032
+ if (!ctx.isObj(v)) {
2033
+ ctx.err(path2, "must be an object");
2034
+ return void 0;
2035
+ }
2036
+ const test = typeof v.test === "string" ? v.test : Array.isArray(v.test) ? ctx.strArray(v.test, `${path2}.test`) : v.test !== void 0 ? (ctx.err(`${path2}.test`, "must be a string or array of strings"), void 0) : void 0;
2037
+ return {
2038
+ test,
2039
+ interval: ctx.str(v.interval, `${path2}.interval`),
2040
+ timeout: ctx.str(v.timeout, `${path2}.timeout`),
2041
+ retries: ctx.int(v.retries, `${path2}.retries`, 0, 100),
2042
+ startPeriod: ctx.str(v.startPeriod, `${path2}.startPeriod`),
2043
+ disable: ctx.bool(v.disable, `${path2}.disable`)
2044
+ };
2045
+ }
2046
+ function parseServices(ctx, v, path2) {
2047
+ if (v === void 0) return void 0;
2048
+ if (!Array.isArray(v)) {
2049
+ ctx.err(path2, "must be an array of service objects");
2050
+ return void 0;
2051
+ }
2052
+ const out = [];
2053
+ v.forEach((item, i) => {
2054
+ const p = `${path2}[${i}]`;
2055
+ if (!ctx.isObj(item)) {
2056
+ ctx.err(p, "must be an object");
2057
+ return;
2058
+ }
2059
+ const name = ctx.str(item.name, `${p}.name`);
2060
+ if (!name) {
2061
+ ctx.err(p, "requires a `name`");
2062
+ return;
2063
+ }
2064
+ out.push({
2065
+ name,
2066
+ image: ctx.str(item.image, `${p}.image`),
2067
+ build: ctx.str(item.build, `${p}.build`),
2068
+ dockerfile: ctx.str(item.dockerfile, `${p}.dockerfile`),
2069
+ ports: ctx.strArray(item.ports, `${p}.ports`),
2070
+ volumes: ctx.strArray(item.volumes, `${p}.volumes`),
2071
+ dependsOn: ctx.strArray(item.dependsOn, `${p}.dependsOn`),
2072
+ env: parseEnv(ctx, item.env, `${p}.env`),
2073
+ command: ctx.str(item.command, `${p}.command`),
2074
+ restart: ctx.enumOf(item.restart, `${p}.restart`, OPENSHIP_RESTARTS),
2075
+ exposed: ctx.bool(item.exposed, `${p}.exposed`),
2076
+ exposedPort: ctx.str(item.exposedPort, `${p}.exposedPort`),
2077
+ domain: ctx.str(item.domain, `${p}.domain`),
2078
+ healthcheck: parseHealthcheck(ctx, item.healthcheck, `${p}.healthcheck`)
2079
+ });
2080
+ });
2081
+ return out;
2082
+ }
2083
+ function parseMonorepo(ctx, v, path2) {
2084
+ if (v === void 0) return void 0;
2085
+ if (!ctx.isObj(v)) {
2086
+ ctx.err(path2, "must be an object");
2087
+ return void 0;
2088
+ }
2089
+ const mono = {};
2090
+ if (v.workspace !== void 0) {
2091
+ if (!ctx.isObj(v.workspace)) ctx.err(`${path2}.workspace`, "must be an object");
2092
+ else {
2093
+ const pm = ctx.str(v.workspace.packageManager, `${path2}.workspace.packageManager`);
2094
+ if (pm) {
2095
+ mono.workspace = {
2096
+ packageManager: pm,
2097
+ prepareCommand: ctx.str(v.workspace.prepareCommand, `${path2}.workspace.prepareCommand`)
2098
+ };
2099
+ } else {
2100
+ ctx.err(`${path2}.workspace`, "requires a `packageManager`");
2101
+ }
2102
+ }
2103
+ }
2104
+ if (v.sharedPaths !== void 0) {
2105
+ ctx.warnings.push(`${path2}.sharedPaths is not applied yet (ignored)`);
2106
+ }
2107
+ if (v.apps !== void 0) {
2108
+ if (!Array.isArray(v.apps)) ctx.err(`${path2}.apps`, "must be an array");
2109
+ else {
2110
+ const apps = [];
2111
+ v.apps.forEach((a, i) => {
2112
+ const p = `${path2}.apps[${i}]`;
2113
+ if (!ctx.isObj(a)) {
2114
+ ctx.err(p, "must be an object");
2115
+ return;
2116
+ }
2117
+ const name = ctx.str(a.name, `${p}.name`);
2118
+ const rootDirectory = ctx.str(a.rootDirectory, `${p}.rootDirectory`);
2119
+ if (!name || !rootDirectory) {
2120
+ ctx.err(p, "requires `name` and `rootDirectory`");
2121
+ return;
2122
+ }
2123
+ apps.push({
2124
+ name,
2125
+ rootDirectory,
2126
+ framework: ctx.enumOf(a.framework, `${p}.framework`, STACK_IDS),
2127
+ packageManager: parsePackageManager(ctx, a.packageManager, `${p}.packageManager`),
2128
+ installCommand: ctx.str(a.installCommand, `${p}.installCommand`),
2129
+ buildCommand: ctx.str(a.buildCommand, `${p}.buildCommand`),
2130
+ startCommand: ctx.str(a.startCommand, `${p}.startCommand`),
2131
+ outputDirectory: ctx.str(a.outputDirectory, `${p}.outputDirectory`),
2132
+ buildImage: ctx.str(a.buildImage, `${p}.buildImage`),
2133
+ port: ctx.int(a.port, `${p}.port`, 1, 65535)
2134
+ });
2135
+ });
2136
+ mono.apps = apps;
2137
+ }
2138
+ }
2139
+ return mono;
2140
+ }
2141
+ function parsePackageManager(ctx, v, path2) {
2142
+ const s = ctx.str(v, path2);
2143
+ if (s === void 0) return void 0;
2144
+ if (!ALL_PACKAGE_MANAGERS.includes(s)) {
2145
+ ctx.err(path2, `must be one of: ${ALL_PACKAGE_MANAGERS.join(", ")}`);
2146
+ return void 0;
2147
+ }
2148
+ return s;
2149
+ }
2150
+ function parseOpenshipConfig(raw) {
2151
+ const ctx = new Ctx();
2152
+ if (!ctx.isObj(raw)) {
2153
+ return { config: null, errors: ["openship.json must be a JSON object"], warnings: [] };
2154
+ }
2155
+ for (const key of Object.keys(raw)) {
2156
+ if (!TOP_LEVEL_KEYS.has(key)) ctx.warnings.push(`Unknown field "${key}" (ignored)`);
2157
+ }
2158
+ const config = {
2159
+ framework: ctx.enumOf(raw.framework, "framework", STACK_IDS),
2160
+ packageManager: parsePackageManager(ctx, raw.packageManager, "packageManager"),
2161
+ rootDirectory: ctx.str(raw.rootDirectory, "rootDirectory"),
2162
+ installCommand: ctx.str(raw.installCommand, "installCommand"),
2163
+ buildCommand: ctx.str(raw.buildCommand, "buildCommand"),
2164
+ startCommand: ctx.str(raw.startCommand, "startCommand"),
2165
+ outputDirectory: ctx.str(raw.outputDirectory, "outputDirectory"),
2166
+ buildImage: ctx.str(raw.buildImage, "buildImage"),
2167
+ productionPaths: ctx.strArray(raw.productionPaths, "productionPaths"),
2168
+ runtime: ctx.enumOf(raw.runtime, "runtime", OPENSHIP_RUNTIMES),
2169
+ productionMode: ctx.enumOf(raw.productionMode, "productionMode", OPENSHIP_PRODUCTION_MODES),
2170
+ port: ctx.int(raw.port, "port", 1, 65535),
2171
+ env: parseEnv(ctx, raw.env, "env"),
2172
+ domains: parseDomains(ctx, raw.domains, "domains"),
2173
+ routes: parseRoutes(ctx, raw.routes, "routes"),
2174
+ resources: parseResources(ctx, raw.resources, "resources"),
2175
+ services: parseServices(ctx, raw.services, "services"),
2176
+ monorepo: parseMonorepo(ctx, raw.monorepo, "monorepo")
2177
+ };
2178
+ for (const k of Object.keys(config)) {
2179
+ if (config[k] === void 0) delete config[k];
2180
+ }
2181
+ return { config, errors: ctx.errors, warnings: ctx.warnings };
2182
+ }
2183
+ function parseOpenshipConfigJson(text2) {
2184
+ let raw;
2185
+ try {
2186
+ raw = JSON.parse(text2);
2187
+ } catch (err2) {
2188
+ return {
2189
+ config: null,
2190
+ errors: [`invalid JSON: ${err2 instanceof Error ? err2.message : String(err2)}`],
2191
+ warnings: []
2192
+ };
2193
+ }
2194
+ return parseOpenshipConfig(raw);
2195
+ }
2196
+
2197
+ // ../../packages/core/src/metadata/openship.ts
2198
+ var openshipMetadataParser = {
2199
+ source: "openship",
2200
+ files: ["openship.json"],
2201
+ parse(fileContents) {
2202
+ const raw = fileContents["openship.json"];
2203
+ if (!raw) return null;
2204
+ const { config } = parseOpenshipConfigJson(raw);
2205
+ if (!config) return null;
2206
+ const metadata = { source: "openship" };
2207
+ if (config.installCommand) metadata.installCommand = config.installCommand;
2208
+ if (config.buildCommand) metadata.buildCommand = config.buildCommand;
2209
+ if (config.outputDirectory) metadata.outputDirectory = config.outputDirectory;
2210
+ if (config.startCommand) metadata.startCommand = config.startCommand;
2211
+ if (config.framework) metadata.framework = config.framework;
2212
+ if (config.routes) metadata.routing = config.routes;
2213
+ const hasSignal = config.installCommand || config.buildCommand || config.outputDirectory || config.startCommand || config.framework || config.routes;
2214
+ return hasSignal ? metadata : null;
2215
+ }
2216
+ };
2217
+
2218
+ // ../../packages/core/src/metadata/text.ts
2219
+ function stripBom2(content) {
2220
+ return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
2221
+ }
2222
+ function trimmed(value) {
2223
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2224
+ }
2225
+ function splitLines(raw) {
2226
+ return stripBom2(raw).split(/\r?\n/);
2227
+ }
2228
+
1801
2229
  // ../../packages/core/src/metadata/vercel.ts
1802
2230
  var VERCEL_FRAMEWORK_TO_STACK = {
1803
2231
  nextjs: "nextjs",
@@ -1812,9 +2240,6 @@ var VERCEL_FRAMEWORK_TO_STACK = {
1812
2240
  angular: "angular",
1813
2241
  "create-react-app": "cra"
1814
2242
  };
1815
- function trimmed(value) {
1816
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1817
- }
1818
2243
  function isConditional(entry) {
1819
2244
  return "has" in entry || "missing" in entry;
1820
2245
  }
@@ -1933,10 +2358,152 @@ var vercelMetadataParser = {
1933
2358
  }
1934
2359
  };
1935
2360
 
1936
- // ../../packages/core/src/metadata/render.ts
1937
- function stripBom2(content) {
1938
- return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
2361
+ // ../../packages/core/src/metadata/railway.ts
2362
+ function tomlScalar(rest) {
2363
+ const s = rest.trim();
2364
+ const quote = s[0];
2365
+ if (quote === '"' || quote === "'") {
2366
+ let out = "";
2367
+ for (let i = 1; i < s.length; i++) {
2368
+ const ch = s[i];
2369
+ if (quote === '"' && ch === "\\" && i + 1 < s.length) {
2370
+ const next = s[++i];
2371
+ switch (next) {
2372
+ case "t":
2373
+ out += " ";
2374
+ break;
2375
+ case "n":
2376
+ out += "\n";
2377
+ break;
2378
+ case "r":
2379
+ out += "\r";
2380
+ break;
2381
+ case "b":
2382
+ out += "\b";
2383
+ break;
2384
+ case "f":
2385
+ out += "\f";
2386
+ break;
2387
+ case '"':
2388
+ out += '"';
2389
+ break;
2390
+ case "\\":
2391
+ out += "\\";
2392
+ break;
2393
+ case "/":
2394
+ out += "/";
2395
+ break;
2396
+ case "u":
2397
+ case "U": {
2398
+ const width = next === "u" ? 4 : 8;
2399
+ const hex = s.slice(i + 1, i + 1 + width);
2400
+ if (hex.length === width && /^[0-9a-fA-F]+$/.test(hex)) {
2401
+ out += String.fromCodePoint(parseInt(hex, 16));
2402
+ i += width;
2403
+ } else {
2404
+ out += next;
2405
+ }
2406
+ break;
2407
+ }
2408
+ default:
2409
+ out += next;
2410
+ }
2411
+ continue;
2412
+ }
2413
+ if (ch === quote) return out;
2414
+ out += ch;
2415
+ }
2416
+ return void 0;
2417
+ }
2418
+ const bare = s.split("#")[0].trim();
2419
+ return bare.length > 0 ? bare : void 0;
2420
+ }
2421
+ function parseRailwayToml(raw) {
2422
+ const cfg = {};
2423
+ let section = "";
2424
+ const lines = splitLines(raw);
2425
+ for (let i = 0; i < lines.length; i++) {
2426
+ const line = lines[i];
2427
+ const header = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
2428
+ if (header) {
2429
+ section = header[1].trim().toLowerCase();
2430
+ continue;
2431
+ }
2432
+ const kv = line.match(/^\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.+)$/);
2433
+ if (!kv) continue;
2434
+ const [, rawKey, rest] = kv;
2435
+ const dot = rawKey.lastIndexOf(".");
2436
+ const table = dot >= 0 ? rawKey.slice(0, dot).toLowerCase() : section;
2437
+ const field = dot >= 0 ? rawKey.slice(dot + 1) : rawKey;
2438
+ let value;
2439
+ const triple = rest.match(/^("""|''')/);
2440
+ if (triple) {
2441
+ const delim = triple[1];
2442
+ const afterOpen = rest.slice(3);
2443
+ const close = afterOpen.indexOf(delim);
2444
+ if (close >= 0) {
2445
+ value = afterOpen.slice(0, close).trim() || void 0;
2446
+ } else {
2447
+ while (++i < lines.length && !lines[i].includes(delim)) {
2448
+ }
2449
+ value = void 0;
2450
+ }
2451
+ } else {
2452
+ value = tomlScalar(rest);
2453
+ }
2454
+ if (!value) continue;
2455
+ if (table === "build") {
2456
+ if (field === "buildCommand") cfg.buildCommand ??= value;
2457
+ else if (field === "builder") cfg.builder ??= value;
2458
+ } else if (table === "deploy") {
2459
+ if (field === "startCommand") cfg.startCommand ??= value;
2460
+ }
2461
+ }
2462
+ return cfg;
2463
+ }
2464
+ function parseRailwayJson(raw) {
2465
+ let parsed;
2466
+ try {
2467
+ parsed = JSON.parse(raw);
2468
+ } catch {
2469
+ return null;
2470
+ }
2471
+ if (typeof parsed !== "object" || parsed === null) return null;
2472
+ const obj = parsed;
2473
+ const build = obj.build ?? {};
2474
+ const deploy = obj.deploy ?? {};
2475
+ return {
2476
+ builder: trimmed(build.builder),
2477
+ buildCommand: trimmed(build.buildCommand),
2478
+ startCommand: trimmed(deploy.startCommand)
2479
+ };
1939
2480
  }
2481
+ function toMetadata(cfg) {
2482
+ const buildCommand = trimmed(cfg.buildCommand);
2483
+ const startCommand = trimmed(cfg.startCommand);
2484
+ const framework = cfg.builder?.toUpperCase() === "DOCKERFILE" ? "docker" : void 0;
2485
+ if (!buildCommand && !startCommand && !framework) return null;
2486
+ const metadata = { source: "railway" };
2487
+ if (buildCommand) metadata.buildCommand = buildCommand;
2488
+ if (startCommand) metadata.startCommand = startCommand;
2489
+ if (framework) metadata.framework = framework;
2490
+ if (extractCdTargets(buildCommand).length > 0) metadata.nonLocal = true;
2491
+ return metadata;
2492
+ }
2493
+ var railwayMetadataParser = {
2494
+ source: "railway",
2495
+ files: ["railway.toml", "railway.json"],
2496
+ parse(fileContents) {
2497
+ const tomlRaw = fileContents["railway.toml"];
2498
+ const jsonRaw = fileContents["railway.json"];
2499
+ const fromToml = tomlRaw ? toMetadata(parseRailwayToml(tomlRaw)) : null;
2500
+ if (fromToml) return fromToml;
2501
+ const jsonCfg = jsonRaw ? parseRailwayJson(jsonRaw) : null;
2502
+ return jsonCfg ? toMetadata(jsonCfg) : null;
2503
+ }
2504
+ };
2505
+
2506
+ // ../../packages/core/src/metadata/render.ts
1940
2507
  function unquote(value) {
1941
2508
  const trimmed2 = value.trim();
1942
2509
  const m = trimmed2.match(/^(['"])(.*)\1$/);
@@ -1948,7 +2515,7 @@ var renderMetadataParser = {
1948
2515
  parse(fileContents) {
1949
2516
  const raw = fileContents["render.yaml"];
1950
2517
  if (!raw) return null;
1951
- const lines = stripBom2(raw).split("\n");
2518
+ const lines = splitLines(raw);
1952
2519
  let startCommand;
1953
2520
  let buildCommand;
1954
2521
  const env = {};
@@ -1982,7 +2549,9 @@ var renderMetadataParser = {
1982
2549
 
1983
2550
  // ../../packages/core/src/metadata/index.ts
1984
2551
  var METADATA_PARSERS = [
2552
+ openshipMetadataParser,
1985
2553
  vercelMetadataParser,
2554
+ railwayMetadataParser,
1986
2555
  renderMetadataParser
1987
2556
  ];
1988
2557
  var METADATA_FILES = new Set(
@@ -2369,11 +2938,11 @@ var openCommand = new Command3("open").description("Open the Openship dashboard
2369
2938
  import { Command as Command4 } from "commander";
2370
2939
  import chalk5 from "chalk";
2371
2940
  import ora from "ora";
2372
- import { spawn } from "child_process";
2941
+ import { spawn as spawn2 } from "child_process";
2373
2942
  import { randomBytes } from "crypto";
2374
- import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync4 } from "fs";
2375
- import { homedir as homedir4 } from "os";
2376
- import { dirname as dirname2, join as join5 } from "path";
2943
+ import { createWriteStream as createWriteStream2, existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
2944
+ import { homedir as homedir6 } from "os";
2945
+ import { dirname as dirname2, join as join7 } from "path";
2377
2946
  import { fileURLToPath } from "url";
2378
2947
 
2379
2948
  // src/lib/dashboard.ts
@@ -2423,8 +2992,8 @@ async function downloadToFile(url, dest, onProgress) {
2423
2992
  } finally {
2424
2993
  file.end();
2425
2994
  }
2426
- await new Promise((resolve2, reject2) => {
2427
- file.on("finish", () => resolve2());
2995
+ await new Promise((resolve3, reject2) => {
2996
+ file.on("finish", () => resolve3());
2428
2997
  file.on("error", reject2);
2429
2998
  });
2430
2999
  return { sha256: hash.digest("hex"), size: received };
@@ -2480,16 +3049,29 @@ function assetName(tag) {
2480
3049
  async function ensureDashboard(opts = {}) {
2481
3050
  const override = process.env.OPENSHIP_DASHBOARD_DIR?.trim();
2482
3051
  if (override) {
2483
- const cwd2 = join3(override, "apps", "dashboard");
2484
- const entry2 = join3(cwd2, "server.js");
2485
- if (!existsSync2(entry2)) {
3052
+ const cwd = join3(override, "apps", "dashboard");
3053
+ const entry = join3(cwd, "server.js");
3054
+ if (!existsSync2(entry)) {
2486
3055
  throw new Error(
2487
- `OPENSHIP_DASHBOARD_DIR=${override} but ${entry2} is missing \u2014 build the dashboard standalone first (see docs).`
3056
+ `OPENSHIP_DASHBOARD_DIR=${override} but ${entry} is missing \u2014 build the dashboard standalone first (see docs).`
2488
3057
  );
2489
3058
  }
2490
- return { tag: "local", entry: entry2, cwd: cwd2 };
3059
+ return { tag: "local", entry, cwd };
2491
3060
  }
2492
- const tag = opts.tag ?? await resolveLatestTag();
3061
+ const requested = opts.tag ?? await resolveLatestTag();
3062
+ try {
3063
+ return await fetchBundle(requested, opts.onProgress);
3064
+ } catch (err2) {
3065
+ if (opts.tag && /\b404\b/.test(err2?.message ?? "")) {
3066
+ const latest = await resolveLatestTag();
3067
+ if (latest && latest !== requested) {
3068
+ return await fetchBundle(latest, opts.onProgress);
3069
+ }
3070
+ }
3071
+ throw err2;
3072
+ }
3073
+ }
3074
+ async function fetchBundle(tag, onProgress) {
2493
3075
  const dir = join3(DASHBOARD_CACHE, tag);
2494
3076
  const cwd = join3(dir, "apps", "dashboard");
2495
3077
  const entry = join3(cwd, "server.js");
@@ -2501,7 +3083,7 @@ async function ensureDashboard(opts = {}) {
2501
3083
  mkdirSync3(dir, { recursive: true });
2502
3084
  const name = assetName(tag);
2503
3085
  const tarball = join3(dir, name);
2504
- const { sha256 } = await downloadToFile(assetUrl(tag, name), tarball, opts.onProgress);
3086
+ const { sha256 } = await downloadToFile(assetUrl(tag, name), tarball, onProgress);
2505
3087
  const expected = await expectedSha256(tag, name);
2506
3088
  if (!expected) {
2507
3089
  throw new Error(
@@ -2527,7 +3109,7 @@ async function ensureDashboard(opts = {}) {
2527
3109
 
2528
3110
  // src/lib/service.ts
2529
3111
  import { spawnSync as spawnSync2 } from "child_process";
2530
- import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, rmSync as rmSync2 } from "fs";
3112
+ import { existsSync as existsSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3, rmSync as rmSync2, readFileSync as readFileSync2 } from "fs";
2531
3113
  import { homedir as homedir3 } from "os";
2532
3114
  import { join as join4, resolve } from "path";
2533
3115
  var HOME = homedir3();
@@ -2563,6 +3145,19 @@ function run(cmd, args) {
2563
3145
  const r = spawnSync2(cmd, args, { encoding: "utf8" });
2564
3146
  return { ok: r.status === 0, out: `${r.stdout ?? ""}${r.stderr ?? ""}`.trim() };
2565
3147
  }
3148
+ function sleepSync(ms) {
3149
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
3150
+ }
3151
+ function launchdPid() {
3152
+ const r = run("launchctl", ["list", MAC_LABEL]);
3153
+ if (!r.ok) return null;
3154
+ const m = r.out.match(/"PID"\s*=\s*(\d+)/);
3155
+ return m ? Number(m[1]) : null;
3156
+ }
3157
+ function launchdLastExit() {
3158
+ const m = run("launchctl", ["list", MAC_LABEL]).out.match(/"LastExitStatus"\s*=\s*(-?\d+)/);
3159
+ return m ? Number(m[1]) : null;
3160
+ }
2566
3161
  function isRoot() {
2567
3162
  return typeof process.getuid === "function" && process.getuid() === 0;
2568
3163
  }
@@ -2683,7 +3278,16 @@ function restart() {
2683
3278
  if (!existsSync3(MAC_PLIST)) return { restarted: false, detail: "no launchd agent installed" };
2684
3279
  const uid = String(process.getuid?.() ?? "");
2685
3280
  const r = run("launchctl", ["kickstart", "-k", `gui/${uid}/${MAC_LABEL}`]);
2686
- return { restarted: r.ok, detail: r.ok ? `restarted ${MAC_LABEL}` : r.out };
3281
+ if (!r.ok) return { restarted: false, detail: r.out || "launchctl kickstart failed" };
3282
+ for (let i = 0; i < 6; i++) {
3283
+ sleepSync(500);
3284
+ if (launchdPid() != null) return { restarted: true, detail: `restarted ${MAC_LABEL}` };
3285
+ }
3286
+ const exit = launchdLastExit();
3287
+ return {
3288
+ restarted: false,
3289
+ detail: `${MAC_LABEL} was killed but didn't stay up${exit != null ? ` (last exit ${exit})` : ""} \u2014 check logs in ${LOG_DIR}`
3290
+ };
2687
3291
  }
2688
3292
  if (kind === "systemd-user" || kind === "systemd-system") {
2689
3293
  const sysArgs = kind === "systemd-user" ? ["--user"] : [];
@@ -2699,27 +3303,258 @@ function restart() {
2699
3303
  }
2700
3304
  return { restarted: false, detail: "no supported service manager" };
2701
3305
  }
3306
+ function serviceStatus() {
3307
+ const kind = detectKind();
3308
+ if (kind === "launchd") {
3309
+ return { kind, installed: existsSync3(MAC_PLIST), running: launchdPid() != null };
3310
+ }
3311
+ if (kind === "systemd-user" || kind === "systemd-system") {
3312
+ const sysArgs = kind === "systemd-user" ? ["--user"] : [];
3313
+ const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
3314
+ return {
3315
+ kind,
3316
+ installed: existsSync3(unitPath),
3317
+ running: run("systemctl", [...sysArgs, "is-active", SYSTEMD_NAME]).out.trim() === "active"
3318
+ };
3319
+ }
3320
+ if (kind === "schtasks") {
3321
+ const q = run("schtasks", ["/Query", "/TN", WIN_TASK]);
3322
+ return { kind, installed: q.ok, running: q.ok && /Running/i.test(q.out) };
3323
+ }
3324
+ return { kind, installed: false, running: false };
3325
+ }
3326
+ function sweepOrphanPorts() {
3327
+ if (process.platform === "win32") return;
3328
+ let ports = [];
3329
+ try {
3330
+ const raw = readFileSync2(join4(HOME, ".openship", "ports.json"), "utf8");
3331
+ const p = JSON.parse(raw);
3332
+ ports = [p.api, p.dashboard].filter((n) => typeof n === "number");
3333
+ } catch {
3334
+ return;
3335
+ }
3336
+ const self = process.pid;
3337
+ for (const port of ports) {
3338
+ const q = run("lsof", ["-ti", `tcp:${port}`]);
3339
+ if (!q.ok) continue;
3340
+ for (const token of q.out.split(/\s+/).filter(Boolean)) {
3341
+ const pid = Number(token);
3342
+ if (Number.isInteger(pid) && pid > 1 && pid !== self) {
3343
+ try {
3344
+ process.kill(pid, "SIGKILL");
3345
+ } catch {
3346
+ }
3347
+ }
3348
+ }
3349
+ }
3350
+ }
2702
3351
  function stop() {
2703
3352
  const kind = detectKind();
3353
+ let result;
2704
3354
  if (kind === "launchd") {
2705
3355
  run("launchctl", ["bootout", `gui/${process.getuid?.() ?? ""}/${MAC_LABEL}`]);
2706
3356
  if (existsSync3(MAC_PLIST)) rmSync2(MAC_PLIST, { force: true });
2707
- return { kind, detail: `launchd agent ${MAC_LABEL} stopped + removed` };
2708
- }
2709
- if (kind === "systemd-user" || kind === "systemd-system") {
3357
+ result = { kind, detail: `launchd agent ${MAC_LABEL} stopped + removed` };
3358
+ } else if (kind === "systemd-user" || kind === "systemd-system") {
2710
3359
  const sysArgs = kind === "systemd-user" ? ["--user"] : [];
2711
3360
  run("systemctl", [...sysArgs, "disable", "--now", SYSTEMD_NAME]);
2712
3361
  const unitPath = kind === "systemd-user" ? join4(HOME, ".config/systemd/user", `${SYSTEMD_NAME}.service`) : `/etc/systemd/system/${SYSTEMD_NAME}.service`;
2713
3362
  if (existsSync3(unitPath)) rmSync2(unitPath, { force: true });
2714
3363
  run("systemctl", [...sysArgs, "daemon-reload"]);
2715
- return { kind, detail: `systemd unit ${SYSTEMD_NAME} stopped + disabled` };
2716
- }
2717
- if (kind === "schtasks") {
3364
+ result = { kind, detail: `systemd unit ${SYSTEMD_NAME} stopped + disabled` };
3365
+ } else if (kind === "schtasks") {
2718
3366
  run("schtasks", ["/End", "/TN", WIN_TASK]);
2719
3367
  run("schtasks", ["/Delete", "/TN", WIN_TASK, "/F"]);
2720
- return { kind, detail: `Scheduled Task ${WIN_TASK} stopped + removed` };
3368
+ result = { kind, detail: `Scheduled Task ${WIN_TASK} stopped + removed` };
3369
+ } else {
3370
+ result = { kind, detail: "no supported service manager \u2014 nothing to stop" };
3371
+ }
3372
+ sweepOrphanPorts();
3373
+ return result;
3374
+ }
3375
+
3376
+ // src/lib/ports.ts
3377
+ import { createServer } from "net";
3378
+ import { existsSync as existsSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
3379
+ import { homedir as homedir4 } from "os";
3380
+ import { join as join5 } from "path";
3381
+ var OS_DIR2 = join5(homedir4(), ".openship");
3382
+ var PORTS_FILE = join5(OS_DIR2, "ports.json");
3383
+ var INSTANCE_FILE = join5(OS_DIR2, "instance.json");
3384
+ function saveInstanceUrl(publicUrl) {
3385
+ try {
3386
+ if (!existsSync4(OS_DIR2)) mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
3387
+ writeFileSync4(INSTANCE_FILE, JSON.stringify({ publicUrl: publicUrl ?? null }));
3388
+ } catch {
3389
+ }
3390
+ }
3391
+ function readInstanceUrl() {
3392
+ try {
3393
+ return JSON.parse(readFileSync3(INSTANCE_FILE, "utf8")).publicUrl ?? null;
3394
+ } catch {
3395
+ return null;
3396
+ }
3397
+ }
3398
+ var DEFAULT_API = 4e3;
3399
+ var DEFAULT_DASHBOARD = 3001;
3400
+ function isPortFree(port) {
3401
+ return new Promise((resolve3) => {
3402
+ const srv = createServer();
3403
+ srv.once("error", () => resolve3(false));
3404
+ srv.listen(port, "127.0.0.1", () => srv.close(() => resolve3(true)));
3405
+ });
3406
+ }
3407
+ async function waitPortFree(port, opts = {}) {
3408
+ const timeoutMs = opts.timeoutMs ?? 6e3;
3409
+ const intervalMs = opts.intervalMs ?? 250;
3410
+ const deadline = Date.now() + timeoutMs;
3411
+ for (; ; ) {
3412
+ if (await isPortFree(port)) return true;
3413
+ if (Date.now() >= deadline) return false;
3414
+ await new Promise((r) => setTimeout(r, intervalMs));
3415
+ }
3416
+ }
3417
+ function getFreePort() {
3418
+ return new Promise((resolve3, reject2) => {
3419
+ const srv = createServer();
3420
+ srv.once("error", reject2);
3421
+ srv.listen(0, "127.0.0.1", () => {
3422
+ const addr = srv.address();
3423
+ const port = addr && typeof addr === "object" ? addr.port : 0;
3424
+ srv.close(() => port ? resolve3(port) : reject2(new Error("no free port")));
3425
+ });
3426
+ });
3427
+ }
3428
+ function loadStoredPorts() {
3429
+ try {
3430
+ return JSON.parse(readFileSync3(PORTS_FILE, "utf-8"));
3431
+ } catch {
3432
+ return {};
3433
+ }
3434
+ }
3435
+ function saveStoredPorts(api, dashboard) {
3436
+ try {
3437
+ if (!existsSync4(OS_DIR2)) mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
3438
+ writeFileSync4(PORTS_FILE, JSON.stringify({ api, dashboard }));
3439
+ } catch {
3440
+ }
3441
+ }
3442
+ async function resolvePorts(prefs) {
3443
+ const stored = loadStoredPorts();
3444
+ const apiPref = prefs.api ?? stored.api ?? DEFAULT_API;
3445
+ const dashPref = prefs.dashboard ?? stored.dashboard ?? DEFAULT_DASHBOARD;
3446
+ const apiRemembered = prefs.api === void 0 && stored.api === apiPref;
3447
+ const dashRemembered = prefs.dashboard === void 0 && stored.dashboard === dashPref;
3448
+ let api;
3449
+ if (await isPortFree(apiPref)) api = apiPref;
3450
+ else if (apiRemembered && await waitPortFree(apiPref)) api = apiPref;
3451
+ else api = await getFreePort();
3452
+ let dashboard;
3453
+ if (dashPref !== api && await isPortFree(dashPref)) dashboard = dashPref;
3454
+ else if (dashPref !== api && dashRemembered && await waitPortFree(dashPref)) dashboard = dashPref;
3455
+ else dashboard = await getFreePort();
3456
+ if (dashboard === api) dashboard = await getFreePort();
3457
+ saveStoredPorts(api, dashboard);
3458
+ return {
3459
+ api,
3460
+ dashboard,
3461
+ switched: { api: api !== apiPref, dashboard: dashboard !== dashPref }
3462
+ };
3463
+ }
3464
+
3465
+ // src/lib/from-source.ts
3466
+ import { spawn, spawnSync as spawnSync3 } from "child_process";
3467
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6 } from "fs";
3468
+ import { homedir as homedir5 } from "os";
3469
+ import { join as join6, resolve as resolve2 } from "path";
3470
+ var OS_DIR3 = join6(homedir5(), ".openship");
3471
+ var DEFAULT_REPO = "https://github.com/oblien/openship.git";
3472
+ function has(cmd) {
3473
+ try {
3474
+ return spawnSync3(cmd, ["--version"], { stdio: "ignore" }).status === 0;
3475
+ } catch {
3476
+ return false;
3477
+ }
3478
+ }
3479
+ function run2(cmd, args, cwd, env) {
3480
+ return new Promise((res, rej) => {
3481
+ const child = spawn(cmd, args, {
3482
+ cwd,
3483
+ stdio: "inherit",
3484
+ env: env ? { ...process.env, ...env } : process.env
3485
+ });
3486
+ child.on("error", rej);
3487
+ child.on(
3488
+ "exit",
3489
+ (code) => code === 0 ? res() : rej(new Error(`\`${cmd} ${args.join(" ")}\` (cwd=${cwd}) exited ${code ?? "?"}`))
3490
+ );
3491
+ });
3492
+ }
3493
+ function shortSha(cwd) {
3494
+ const r = spawnSync3("git", ["rev-parse", "--short", "HEAD"], { cwd, encoding: "utf8" });
3495
+ return r.status === 0 ? (r.stdout ?? "").trim() || "unknown" : "unknown";
3496
+ }
3497
+ function isMonorepo(dir) {
3498
+ return existsSync5(join6(dir, "package.json")) && existsSync5(join6(dir, "apps/api/package.json")) && existsSync5(join6(dir, "apps/dashboard/package.json"));
3499
+ }
3500
+ async function prepareFromSource(opts) {
3501
+ if (!has("bun")) {
3502
+ throw new Error(
3503
+ "`bun` is required to build from source but wasn't found on PATH \u2014 install it: https://bun.sh"
3504
+ );
3505
+ }
3506
+ let sourceDir;
3507
+ let ref;
3508
+ if (opts.source) {
3509
+ sourceDir = resolve2(opts.source);
3510
+ if (!isMonorepo(sourceDir)) {
3511
+ throw new Error(
3512
+ `--source ${sourceDir} doesn't look like an Openship checkout (missing package.json / apps/api / apps/dashboard).`
3513
+ );
3514
+ }
3515
+ ref = "local";
3516
+ } else {
3517
+ if (!has("git")) {
3518
+ throw new Error("`git` is required to clone the source but wasn't found on PATH.");
3519
+ }
3520
+ ref = (opts.ref || "main").trim();
3521
+ const repoUrl = opts.repo || DEFAULT_REPO;
3522
+ sourceDir = join6(OS_DIR3, "src");
3523
+ mkdirSync6(OS_DIR3, { recursive: true });
3524
+ if (!existsSync5(join6(sourceDir, ".git"))) {
3525
+ console.log(` Cloning ${repoUrl} \u2192 ${sourceDir}`);
3526
+ await run2("git", ["clone", repoUrl, sourceDir], OS_DIR3);
3527
+ }
3528
+ console.log(` Fetching + checking out ${ref}`);
3529
+ await run2("git", ["fetch", "origin", ref, "--tags"], sourceDir);
3530
+ await run2("git", ["checkout", ref], sourceDir);
3531
+ await run2("git", ["pull", "--ff-only", "origin", ref], sourceDir).catch(() => {
3532
+ console.log(" (pinned ref \u2014 not fast-forwarding)");
3533
+ });
2721
3534
  }
2722
- return { kind, detail: "no supported service manager \u2014 nothing to stop" };
3535
+ const sha = shortSha(sourceDir);
3536
+ console.log(` Source: ${sourceDir} @ ${ref} (${sha})`);
3537
+ console.log(" Installing workspace dependencies (bun install)\u2026");
3538
+ await run2("bun", ["install"], sourceDir);
3539
+ const distDir = join6(OS_DIR3, "from-source-dist");
3540
+ console.log(" Building release dist (compiles the dashboard \u2014 needs RAM/CPU)\u2026");
3541
+ await run2(
3542
+ "bun",
3543
+ ["run", join6(sourceDir, "apps/api/scripts/build-release.ts")],
3544
+ sourceDir,
3545
+ { DIST_DIR: distDir, NODE_ENV: "production", CLOUD_MODE: "false", OPENSHIP_TARGET: "local" }
3546
+ );
3547
+ console.log(" Installing runtime dependencies in the dist\u2026");
3548
+ await run2("bun", ["install", "--production", "--frozen-lockfile"], distDir);
3549
+ const apiDir = join6(distDir, "api");
3550
+ const dashboardDir = join6(distDir, "dashboard");
3551
+ if (!existsSync5(join6(apiDir, "src/index.ts"))) {
3552
+ throw new Error(`Build produced no API at ${apiDir}/src/index.ts \u2014 build-release layout drift?`);
3553
+ }
3554
+ if (!existsSync5(join6(dashboardDir, "apps/dashboard/server.js"))) {
3555
+ throw new Error(`Build produced no dashboard at ${dashboardDir}/apps/dashboard/server.js.`);
3556
+ }
3557
+ return { apiDir, dashboardDir, ref, sha, sourceDir };
2723
3558
  }
2724
3559
 
2725
3560
  // src/commands/up.ts
@@ -2744,22 +3579,22 @@ function normalizePublicUrl(raw) {
2744
3579
  return url;
2745
3580
  }
2746
3581
  var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
2747
- var SERVER_DIR = join5(DIST_DIR, "server");
2748
- var OS_DIR2 = join5(homedir4(), ".openship");
3582
+ var SERVER_DIR = join7(DIST_DIR, "server");
3583
+ var OS_DIR4 = join7(homedir6(), ".openship");
2749
3584
  function ensureAuthSecret() {
2750
- const path2 = join5(OS_DIR2, "auth-secret");
2751
- if (existsSync4(path2)) return readFileSync2(path2, "utf8").trim();
2752
- mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
3585
+ const path2 = join7(OS_DIR4, "auth-secret");
3586
+ if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
3587
+ mkdirSync7(OS_DIR4, { recursive: true, mode: 448 });
2753
3588
  const secret = randomBytes(32).toString("hex");
2754
- writeFileSync4(path2, secret, { mode: 384 });
3589
+ writeFileSync5(path2, secret, { mode: 384 });
2755
3590
  return secret;
2756
3591
  }
2757
3592
  function ensureInternalToken() {
2758
- const path2 = join5(OS_DIR2, "internal-token");
2759
- if (existsSync4(path2)) return readFileSync2(path2, "utf8").trim();
2760
- mkdirSync5(OS_DIR2, { recursive: true, mode: 448 });
3593
+ const path2 = join7(OS_DIR4, "internal-token");
3594
+ if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
3595
+ mkdirSync7(OS_DIR4, { recursive: true, mode: 448 });
2761
3596
  const token = randomBytes(32).toString("hex");
2762
- writeFileSync4(path2, token, { mode: 384 });
3597
+ writeFileSync5(path2, token, { mode: 384 });
2763
3598
  return token;
2764
3599
  }
2765
3600
  var upCommand = new Command4("up").description("Start Openship as a persistent service (boot + auto-restart); --foreground to run attached").option("--port <port>", "API port to listen on", "4000").option("--data-dir <dir>", "Directory for the embedded database").option("--dashboard-port <port>", "Dashboard port", "3001").option("--no-ui", "Run the API only \u2014 don't download/serve the dashboard").option("--ui-version <tag>", "Dashboard release tag to run (default: this CLI's version)").option("-f, --foreground", "Run attached in this terminal instead of as a background service").option("--dry-run", "Print the service definition that would be installed, then exit").option(
@@ -2771,18 +3606,69 @@ var upCommand = new Command4("up").description("Start Openship as a persistent s
2771
3606
  ).option(
2772
3607
  "--managed-edge",
2773
3608
  "Managed edge: install OpenResty + a free Let's Encrypt cert on this box and route --public-url's domain to the dashboard (no reverse proxy needed)"
2774
- ).option("--acme-email <email>", "Contact email for Let's Encrypt certificates (managed edge)").action(async (opts) => {
3609
+ ).option("--acme-email <email>", "Contact email for Let's Encrypt certificates (managed edge)").option("--from-source", "Preview: build + run Openship from source (a branch) instead of a published release \u2014 runs attached").option("--ref <branch>", "Git branch/tag/sha to build with --from-source (default: main)").option("--source <path>", "Build from an existing local Openship checkout instead of cloning").option("--repo <url>", "Git remote to clone for --from-source (default: oblien/openship)").action(async (opts) => {
3610
+ if (opts.fromSource || opts.source) return runFromSource(opts);
2775
3611
  if (opts.foreground) return runForeground(opts);
2776
- startService(opts);
3612
+ await startService(opts);
2777
3613
  });
2778
- function startService(opts, runOpts = {}) {
3614
+ async function runFromSource(opts) {
3615
+ console.log(chalk5.cyan("\n Building Openship from source (preview mode)\u2026"));
3616
+ console.log(
3617
+ chalk5.dim(" Unverified dev build \u2014 for previewing a branch, not production self-hosting.\n")
3618
+ );
3619
+ let src;
3620
+ try {
3621
+ src = await prepareFromSource({ ref: opts.ref, source: opts.source, repo: opts.repo });
3622
+ } catch (e) {
3623
+ console.error(
3624
+ chalk5.red(`
3625
+ Build from source failed: ${e.message}
3626
+ `) + chalk5.dim(" Small boxes can OOM on the dashboard build \u2014 build on a bigger machine and pass --source, or use a published release.\n")
3627
+ );
3628
+ process.exit(1);
3629
+ }
3630
+ console.log(chalk5.green(`
3631
+ Built ${src.ref} (${src.sha}). Starting\u2026
3632
+ `));
3633
+ await runForeground(opts, src);
3634
+ }
3635
+ async function startService(opts, runOpts = {}) {
2779
3636
  const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
2780
- const port = String(opts.port || "4000");
2781
- const dashPort = String(opts.dashboardPort || "3001");
3637
+ if (opts.dryRun) {
3638
+ const p = preview({
3639
+ port: opts.port,
3640
+ dataDir: opts.dataDir,
3641
+ dashboardPort: opts.dashboardPort,
3642
+ ui: opts.ui,
3643
+ uiVersion: opts.uiVersion,
3644
+ publicUrl,
3645
+ trustProxy: opts.trustProxy || opts.managedEdge,
3646
+ managedEdge: opts.managedEdge,
3647
+ acmeEmail: opts.acmeEmail
3648
+ });
3649
+ console.log(
3650
+ chalk5.dim(`
3651
+ service manager: ${p.kind}
3652
+ path: ${p.path}
3653
+
3654
+ `) + p.content + "\n"
3655
+ );
3656
+ return {
3657
+ port: String(opts.port || "4000"),
3658
+ dashPort: String(opts.dashboardPort || "3001"),
3659
+ publicUrl
3660
+ };
3661
+ }
3662
+ const resolved = await resolvePorts({
3663
+ api: opts.port ? Number(opts.port) : void 0,
3664
+ dashboard: opts.dashboardPort ? Number(opts.dashboardPort) : void 0
3665
+ });
3666
+ const port = String(resolved.api);
3667
+ const dashPort = String(resolved.dashboard);
2782
3668
  const flags = {
2783
- port: opts.port,
3669
+ port,
2784
3670
  dataDir: opts.dataDir,
2785
- dashboardPort: opts.dashboardPort,
3671
+ dashboardPort: dashPort,
2786
3672
  ui: opts.ui,
2787
3673
  uiVersion: opts.uiVersion,
2788
3674
  publicUrl,
@@ -2791,20 +3677,15 @@ function startService(opts, runOpts = {}) {
2791
3677
  managedEdge: opts.managedEdge,
2792
3678
  acmeEmail: opts.acmeEmail
2793
3679
  };
2794
- if (opts.dryRun) {
2795
- const p = preview(flags);
2796
- console.log(
2797
- chalk5.dim(`
2798
- service manager: ${p.kind}
2799
- path: ${p.path}
2800
-
2801
- `) + p.content + "\n"
2802
- );
2803
- return { port, dashPort, publicUrl };
2804
- }
2805
3680
  try {
2806
3681
  const res = installAndStart(flags);
2807
3682
  if (!runOpts.quiet) {
3683
+ if (resolved.switched.api || resolved.switched.dashboard) {
3684
+ console.log(
3685
+ chalk5.yellow(`
3686
+ A preferred port was busy \u2014 using API ${port}, dashboard ${dashPort}.`)
3687
+ );
3688
+ }
2808
3689
  const dashboardLine = publicUrl ? chalk5.dim(` Dashboard: ${publicUrl} (login required)
2809
3690
  `) : chalk5.dim(` Dashboard: http://localhost:${dashPort} (login required)
2810
3691
  `);
@@ -2825,20 +3706,38 @@ function startService(opts, runOpts = {}) {
2825
3706
  process.exit(1);
2826
3707
  }
2827
3708
  }
2828
- async function runForeground(opts) {
2829
- const serverEntry = join5(SERVER_DIR, "index.js");
2830
- if (!existsSync4(serverEntry)) {
2831
- console.error(
2832
- chalk5.red("\n Bundled server not found in this install.") + chalk5.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
2833
- );
2834
- process.exit(1);
3709
+ async function runForeground(opts, source) {
3710
+ let apiCmd = process.execPath;
3711
+ let apiArgs;
3712
+ let apiCwd;
3713
+ if (source) {
3714
+ apiCmd = "bun";
3715
+ apiArgs = ["run", "src/index.ts"];
3716
+ apiCwd = source.apiDir;
3717
+ } else {
3718
+ const serverEntry = join7(SERVER_DIR, "index.js");
3719
+ if (!existsSync6(serverEntry)) {
3720
+ console.error(
3721
+ chalk5.red("\n Bundled server not found in this install.") + chalk5.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
3722
+ );
3723
+ process.exit(1);
3724
+ }
3725
+ apiArgs = [serverEntry];
2835
3726
  }
2836
- const port = String(opts.port || "4000");
2837
- const dashPort = String(opts.dashboardPort || "3001");
3727
+ const resolved = await resolvePorts({
3728
+ api: opts.port ? Number(opts.port) : void 0,
3729
+ dashboard: opts.dashboardPort ? Number(opts.dashboardPort) : void 0
3730
+ });
3731
+ const port = String(resolved.api);
3732
+ const dashPort = String(resolved.dashboard);
2838
3733
  const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
2839
3734
  const managedEdge = Boolean(opts.managedEdge && publicUrl);
2840
- const dataDir = opts.dataDir || join5(OS_DIR2, "data");
2841
- mkdirSync5(dataDir, { recursive: true });
3735
+ const dataDir = opts.dataDir || join7(OS_DIR4, "data");
3736
+ mkdirSync7(dataDir, { recursive: true });
3737
+ const logDir = join7(OS_DIR4, "logs");
3738
+ mkdirSync7(logDir, { recursive: true });
3739
+ const instanceLogPath = join7(logDir, "instance.log");
3740
+ const instanceLog = createWriteStream2(instanceLogPath, { flags: "w" });
2842
3741
  const env = {
2843
3742
  ...process.env,
2844
3743
  PORT: port,
@@ -2848,13 +3747,17 @@ async function runForeground(opts) {
2848
3747
  OPENSHIP_TARGET: "local",
2849
3748
  OPENSHIP_JOB_RUNNER: "in-process",
2850
3749
  PGLITE_DATA_DIR: dataDir,
2851
- OPENSHIP_MIGRATIONS_DIR: join5(SERVER_DIR, "migrations"),
2852
- OPENSHIP_PGLITE_ASSETS_DIR: join5(SERVER_DIR, "pglite"),
2853
3750
  BETTER_AUTH_SECRET: ensureAuthSecret()
2854
3751
  };
3752
+ if (!source) {
3753
+ env.OPENSHIP_MIGRATIONS_DIR = join7(SERVER_DIR, "migrations");
3754
+ env.OPENSHIP_PGLITE_ASSETS_DIR = join7(SERVER_DIR, "pglite");
3755
+ }
2855
3756
  env.OPENSHIP_REQUIRE_AUTH = "true";
2856
3757
  env.INTERNAL_TOKEN = ensureInternalToken();
2857
3758
  env.OPENSHIP_API_HOST = "127.0.0.1";
3759
+ env.OPENSHIP_DASHBOARD_PORT = dashPort;
3760
+ env.OPENSHIP_INSTANCE_LOG = instanceLogPath;
2858
3761
  delete env.OPENSHIP_ALLOW_ZERO_AUTH;
2859
3762
  if (publicUrl) {
2860
3763
  env.OPENSHIP_PUBLIC_URL = publicUrl;
@@ -2868,7 +3771,14 @@ async function runForeground(opts) {
2868
3771
  delete env.DATABASE_URL;
2869
3772
  delete env.POSTGRES_URL;
2870
3773
  const spinner3 = ora(`Starting Openship on http://localhost:${port} \u2026`).start();
2871
- const child = spawn(process.execPath, [serverEntry], { env, stdio: ["ignore", "pipe", "pipe"] });
3774
+ const child = spawn2(apiCmd, apiArgs, {
3775
+ cwd: apiCwd,
3776
+ env,
3777
+ stdio: ["ignore", "pipe", "pipe"],
3778
+ detached: process.platform !== "win32"
3779
+ });
3780
+ child.stdout.on("data", (d) => instanceLog.write(d));
3781
+ child.stderr.on("data", (d) => instanceLog.write(d));
2872
3782
  let buffered = "";
2873
3783
  const buffer = (d) => {
2874
3784
  buffered += d.toString();
@@ -2903,26 +3813,34 @@ async function runForeground(opts) {
2903
3813
  }
2904
3814
  spinner3.succeed(`Openship API running at http://localhost:${port}`);
2905
3815
  const children = [child];
2906
- const stopAll = () => {
2907
- for (const c of children) {
2908
- try {
2909
- c.kill("SIGTERM");
2910
- } catch {
2911
- }
2912
- setTimeout(() => {
2913
- try {
2914
- c.kill("SIGKILL");
2915
- } catch {
2916
- }
2917
- }, 5e3).unref?.();
3816
+ const killTree = (c, sig) => {
3817
+ try {
3818
+ if (c.pid && process.platform !== "win32") process.kill(-c.pid, sig);
3819
+ else c.kill(sig);
3820
+ } catch {
3821
+ }
3822
+ };
3823
+ let stopping = false;
3824
+ const stopAll = (exitCode = 0) => {
3825
+ if (stopping) return;
3826
+ stopping = true;
3827
+ try {
3828
+ instanceLog.end();
3829
+ } catch {
2918
3830
  }
3831
+ for (const c of children) killTree(c, "SIGTERM");
3832
+ setTimeout(() => {
3833
+ for (const c of children) killTree(c, "SIGKILL");
3834
+ process.exit(exitCode);
3835
+ }, 1500);
2919
3836
  };
2920
3837
  let dashboardUrl = null;
2921
3838
  if (opts.ui !== false) {
3839
+ if (source) process.env.OPENSHIP_DASHBOARD_DIR = source.dashboardDir;
2922
3840
  const uiSpinner = ora("Preparing the dashboard\u2026").start();
2923
3841
  try {
2924
3842
  const bundle = await ensureDashboard({
2925
- tag: opts.uiVersion || `v${"0.2.1"}`,
3843
+ tag: source ? "local" : opts.uiVersion || `v${"0.2.3"}`,
2926
3844
  onProgress: (received, total) => {
2927
3845
  if (total) {
2928
3846
  uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
@@ -2930,8 +3848,9 @@ async function runForeground(opts) {
2930
3848
  }
2931
3849
  });
2932
3850
  uiSpinner.text = "Starting the dashboard\u2026";
2933
- const dash = spawn(process.execPath, [bundle.entry], {
3851
+ const dash = spawn2(process.execPath, [bundle.entry], {
2934
3852
  cwd: bundle.cwd,
3853
+ detached: process.platform !== "win32",
2935
3854
  env: {
2936
3855
  ...process.env,
2937
3856
  NODE_ENV: "production",
@@ -2946,13 +3865,24 @@ async function runForeground(opts) {
2946
3865
  // the browser never needs to know where the API lives. Set in every
2947
3866
  // mode; loopback because the dashboard runs on the same box.
2948
3867
  INTERNAL_API_URL: `http://127.0.0.1:${port}`,
2949
- // Public URL feeds the SSR proxy-origin resolver; local mode keeps
2950
- // the window.__OPENSHIP_API_ORIGIN__ fallback for direct API calls.
2951
- ...publicUrl ? { OPENSHIP_PUBLIC_URL: publicUrl } : { OPENSHIP_LOCAL_API_URL: `http://127.0.0.1:${port}` }
3868
+ // ALWAYS tell the dashboard the real loopback API origin. The API port
3869
+ // is dynamic, so a browser opened on THIS box must learn it via
3870
+ // window.__OPENSHIP_API_ORIGIN__ (layout.tsx) otherwise it falls back
3871
+ // to the static default :4000 and every call 404s. Use `localhost` (NOT
3872
+ // 127.0.0.1) to MATCH the host the dashboard is opened on — a host-only
3873
+ // SameSite session cookie set on 127.0.0.1 is never sent to localhost
3874
+ // (they're different sites to a browser), which is the login-reload loop.
3875
+ // Older dashboards use this origin verbatim; newer ones align it anyway.
3876
+ // `localhost` still reaches the 127.0.0.1-bound API. In proxy mode this
3877
+ // is just a fallback (sameOriginProxyOrigin wins for remote browsers).
3878
+ OPENSHIP_LOCAL_API_URL: `http://localhost:${port}`,
3879
+ ...publicUrl ? { OPENSHIP_PUBLIC_URL: publicUrl } : {}
2952
3880
  },
2953
3881
  stdio: ["ignore", "pipe", "pipe"]
2954
3882
  });
2955
3883
  children.push(dash);
3884
+ dash.stdout.on("data", (d) => instanceLog.write(d));
3885
+ dash.stderr.on("data", (d) => instanceLog.write(d));
2956
3886
  let dashBuf = "";
2957
3887
  const onDash = (d) => {
2958
3888
  dashBuf += d.toString();
@@ -3009,12 +3939,9 @@ async function runForeground(opts) {
3009
3939
  child.stderr.off("data", buffer);
3010
3940
  child.stdout.on("data", (d) => process.stdout.write(d));
3011
3941
  child.stderr.on("data", (d) => process.stderr.write(d));
3012
- process.on("SIGINT", stopAll);
3013
- process.on("SIGTERM", stopAll);
3014
- child.on("exit", (code) => {
3015
- stopAll();
3016
- process.exit(code ?? 0);
3017
- });
3942
+ process.on("SIGINT", () => stopAll(0));
3943
+ process.on("SIGTERM", () => stopAll(0));
3944
+ child.on("exit", (code) => stopAll(code ?? 0));
3018
3945
  }
3019
3946
 
3020
3947
  // src/commands/stop.ts
@@ -3035,15 +3962,15 @@ var stopCommand = new Command5("stop").description("Stop the Openship service (s
3035
3962
 
3036
3963
  // src/commands/init.ts
3037
3964
  import { Command as Command6 } from "commander";
3038
- import { existsSync as existsSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync5 } from "fs";
3965
+ import { existsSync as existsSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync6 } from "fs";
3039
3966
  import { createInterface as createInterface2 } from "readline/promises";
3040
3967
  import { stdin as input2, stdout as output2 } from "process";
3041
- import { join as join6 } from "path";
3968
+ import { join as join8 } from "path";
3042
3969
  var initCommand = new Command6("init").description("Link the current directory to an Openship project (.openship/project.json)").option("--project <id>", "Project id to link (skips the picker)").option("--environment <name>", "Default deploy environment", "production").option("--dir <path>", "Directory to initialize", process.cwd()).option("--force", "Overwrite an existing project link").option("-y, --yes", "Non-interactive: fail instead of prompting").action(async (opts) => {
3043
3970
  const root = opts.dir || process.cwd();
3044
- const linkDir = join6(root, ".openship");
3045
- const linkPath = join6(linkDir, "project.json");
3046
- if (existsSync5(linkPath) && !opts.force) {
3971
+ const linkDir = join8(root, ".openship");
3972
+ const linkPath = join8(linkDir, "project.json");
3973
+ if (existsSync7(linkPath) && !opts.force) {
3047
3974
  err(`Already linked (${linkPath}). Re-run with --force to overwrite.`);
3048
3975
  process.exit(1);
3049
3976
  }
@@ -3096,19 +4023,97 @@ var initCommand = new Command6("init").description("Link the current directory t
3096
4023
  context: getActiveContext(),
3097
4024
  defaults: { environment: opts.environment || "production" }
3098
4025
  };
3099
- mkdirSync6(linkDir, { recursive: true });
3100
- writeFileSync5(linkPath, JSON.stringify(link, null, 2) + "\n");
4026
+ mkdirSync8(linkDir, { recursive: true });
4027
+ writeFileSync6(linkPath, JSON.stringify(link, null, 2) + "\n");
3101
4028
  if (isJsonMode()) {
3102
4029
  printJson({ path: linkPath, link });
3103
4030
  return;
3104
4031
  }
3105
4032
  ok(`
3106
- Linked ${link.name ?? link.projectId} \u2192 ${linkPath}
4033
+ Linked ${link.name ?? link.projectId} \u2192 ${linkPath}
4034
+ `);
4035
+ });
4036
+
4037
+ // src/commands/config.ts
4038
+ import { Command as Command7 } from "commander";
4039
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
4040
+ import { join as join9 } from "path";
4041
+ var SCHEMA_URL = "https://openship.io/openship.schema.json";
4042
+ var CONFIG_FILE = "openship.json";
4043
+ function detectHints(dir) {
4044
+ const hints = {};
4045
+ const lock = [
4046
+ ["pnpm-lock.yaml", "pnpm"],
4047
+ ["yarn.lock", "yarn"],
4048
+ ["bun.lockb", "bun"],
4049
+ ["package-lock.json", "npm"]
4050
+ ];
4051
+ for (const [file, pm] of lock) {
4052
+ if (existsSync8(join9(dir, file))) {
4053
+ hints.packageManager = pm;
4054
+ break;
4055
+ }
4056
+ }
4057
+ const pkgPath = join9(dir, "package.json");
4058
+ if (existsSync8(pkgPath)) {
4059
+ try {
4060
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
4061
+ const runner = hints.packageManager ?? "npm";
4062
+ const exec = runner === "npm" ? "npm run" : runner;
4063
+ if (pkg.scripts?.build) hints.buildCommand = `${exec} build`;
4064
+ if (pkg.scripts?.start) hints.startCommand = `${exec} start`;
4065
+ } catch {
4066
+ }
4067
+ }
4068
+ return hints;
4069
+ }
4070
+ var initCmd = new Command7("init").description("Scaffold an openship.json in the current directory").option("--dir <path>", "Directory to write into", process.cwd()).option("--force", "Overwrite an existing openship.json").action((opts) => {
4071
+ const dir = opts.dir || process.cwd();
4072
+ const path2 = join9(dir, CONFIG_FILE);
4073
+ if (existsSync8(path2) && !opts.force) {
4074
+ err(`${CONFIG_FILE} already exists. Re-run with --force to overwrite.`);
4075
+ process.exit(1);
4076
+ }
4077
+ const hints = detectHints(dir);
4078
+ const scaffold = { $schema: SCHEMA_URL, ...hints };
4079
+ const text2 = JSON.stringify(scaffold, null, 2) + "\n";
4080
+ writeFileSync7(path2, text2);
4081
+ if (isJsonMode()) {
4082
+ printJson({ path: path2, config: scaffold });
4083
+ return;
4084
+ }
4085
+ ok(`
4086
+ Wrote ${CONFIG_FILE} \u2192 ${path2}`);
4087
+ info(" Edit it to declare framework, env, domains, resources, services\u2026 then `openship config validate`.\n");
4088
+ });
4089
+ var validateCmd = new Command7("validate").description("Validate an openship.json against the deploy schema").argument("[file]", "Path to the config file", CONFIG_FILE).action((file) => {
4090
+ const path2 = file === CONFIG_FILE ? join9(process.cwd(), CONFIG_FILE) : file;
4091
+ if (!existsSync8(path2)) {
4092
+ if (isJsonMode()) printJson({ valid: false, errors: [`${path2} not found`], warnings: [] });
4093
+ else err(`Not found: ${path2}`);
4094
+ process.exit(1);
4095
+ }
4096
+ const { config, errors, warnings } = parseOpenshipConfigJson(readFileSync5(path2, "utf8"));
4097
+ const valid = errors.length === 0 && config !== null;
4098
+ if (isJsonMode()) {
4099
+ printJson({ valid, errors, warnings });
4100
+ process.exit(valid ? 0 : 1);
4101
+ }
4102
+ for (const w of warnings) info(` \u26A0 ${w}`);
4103
+ if (!valid) {
4104
+ err(`
4105
+ ${errors.length} error${errors.length === 1 ? "" : "s"} in ${CONFIG_FILE}:`);
4106
+ for (const e of errors) err(` \u2022 ${e}`);
4107
+ process.exit(1);
4108
+ }
4109
+ ok(`
4110
+ ${CONFIG_FILE} is valid${warnings.length ? ` (${warnings.length} warning${warnings.length === 1 ? "" : "s"})` : ""}.
3107
4111
  `);
3108
4112
  });
4113
+ var configCommand = new Command7("config").description("Author and validate openship.json (declarative deploy config)").addCommand(initCmd).addCommand(validateCmd);
3109
4114
 
3110
4115
  // src/commands/context.ts
3111
- import { Command as Command7 } from "commander";
4116
+ import { Command as Command8 } from "commander";
3112
4117
  function renderContexts() {
3113
4118
  const rows = listContexts().map((c) => ({
3114
4119
  current: c.current ? "*" : "",
@@ -3119,8 +4124,8 @@ function renderContexts() {
3119
4124
  }));
3120
4125
  printTable(rows, ["current", "name", "apiUrl", "dashboardUrl", "auth"]);
3121
4126
  }
3122
- var listCmd = new Command7("list").alias("ls").description("List configured contexts").action(renderContexts);
3123
- var useCmd = new Command7("use").description("Switch the active context").argument("<name>", "Context name").action((name) => {
4127
+ var listCmd = new Command8("list").alias("ls").description("List configured contexts").action(renderContexts);
4128
+ var useCmd = new Command8("use").description("Switch the active context").argument("<name>", "Context name").action((name) => {
3124
4129
  try {
3125
4130
  setActiveContext(name);
3126
4131
  ok(`
@@ -3131,7 +4136,7 @@ var useCmd = new Command7("use").description("Switch the active context").argume
3131
4136
  process.exit(1);
3132
4137
  }
3133
4138
  });
3134
- var addCmd = new Command7("add").description("Create or update a context's endpoints/token").argument("<name>", "Context name").option("--api-url <url>", "API base URL").option("--dashboard-url <url>", "Dashboard base URL").option("--token <token>", "Personal Access Token to store").option("--use", "Switch to this context after adding").action((name, opts) => {
4139
+ var addCmd = new Command8("add").description("Create or update a context's endpoints/token").argument("<name>", "Context name").option("--api-url <url>", "API base URL").option("--dashboard-url <url>", "Dashboard base URL").option("--token <token>", "Personal Access Token to store").option("--use", "Switch to this context after adding").action((name, opts) => {
3135
4140
  addContext(name, {
3136
4141
  apiUrl: opts.apiUrl,
3137
4142
  dashboardUrl: opts.dashboardUrl,
@@ -3142,7 +4147,7 @@ var addCmd = new Command7("add").description("Create or update a context's endpo
3142
4147
  Saved context "${name}"${opts.use ? " (now active)" : ""}.
3143
4148
  `);
3144
4149
  });
3145
- var rmCmd = new Command7("rm").alias("remove").description("Remove a context (cannot remove the active one)").argument("<name>", "Context name").action((name) => {
4150
+ var rmCmd = new Command8("rm").alias("remove").description("Remove a context (cannot remove the active one)").argument("<name>", "Context name").action((name) => {
3146
4151
  try {
3147
4152
  removeContext(name);
3148
4153
  ok(`
@@ -3153,47 +4158,61 @@ var rmCmd = new Command7("rm").alias("remove").description("Remove a context (ca
3153
4158
  process.exit(1);
3154
4159
  }
3155
4160
  });
3156
- var contextCommand = new Command7("context").alias("ctx").description("Manage connection contexts (list/use/add/rm)").action(() => {
4161
+ var contextCommand = new Command8("context").alias("ctx").description("Manage connection contexts (list/use/add/rm)").action(() => {
3157
4162
  ok(` Active context: ${getActiveContext()}`);
3158
4163
  renderContexts();
3159
4164
  }).addCommand(listCmd).addCommand(useCmd).addCommand(addCmd).addCommand(rmCmd);
3160
4165
 
3161
4166
  // src/commands/status.ts
3162
- import { Command as Command8 } from "commander";
4167
+ import { Command as Command9 } from "commander";
3163
4168
  import chalk7 from "chalk";
3164
- var statusCommand = new Command8("status").description("Show the active context's API health and deployment info").action(async () => {
4169
+ import { readFileSync as readFileSync6 } from "fs";
4170
+ import { homedir as homedir7 } from "os";
4171
+ import { join as join10 } from "path";
4172
+ function readPorts() {
4173
+ try {
4174
+ return JSON.parse(readFileSync6(join10(homedir7(), ".openship", "ports.json"), "utf8"));
4175
+ } catch {
4176
+ return {};
4177
+ }
4178
+ }
4179
+ var statusCommand = new Command9("status").description("Show the local Openship service (installed/running, ports) and the active context's API health").action(async () => {
3165
4180
  const context = getActiveContext();
3166
4181
  const apiUrl = getApiUrl2();
3167
- let health;
3168
- let envInfo;
4182
+ const svc = serviceStatus();
4183
+ const ports = readPorts();
4184
+ let health = null;
4185
+ let envInfo = null;
4186
+ let reachable = true;
4187
+ let unreachableMsg = "";
3169
4188
  try {
3170
4189
  health = await apiRequest("/health", { signal: AbortSignal.timeout(8e3) });
3171
4190
  envInfo = await apiRequest("/health/env", { signal: AbortSignal.timeout(8e3) });
3172
4191
  } catch (e) {
3173
- if (isJsonMode()) {
3174
- printJson({ context, apiUrl, reachable: false });
3175
- } else {
3176
- const msg = e instanceof ApiError ? e.message : e.message;
3177
- err(`
3178
- Cannot reach the API at ${apiUrl}: ${msg}
3179
- `);
3180
- }
3181
- process.exit(1);
4192
+ reachable = false;
4193
+ unreachableMsg = e instanceof ApiError ? e.message : e.message;
3182
4194
  }
3183
4195
  if (isJsonMode()) {
3184
- printJson({ context, apiUrl, reachable: true, health, env: envInfo });
3185
- return;
4196
+ printJson({ context, apiUrl, service: svc, ports, reachable, health, env: envInfo });
4197
+ process.exit(reachable ? 0 : 1);
3186
4198
  }
3187
4199
  const row = (label, value) => ` ${chalk7.dim(label.padEnd(14))}${value ?? chalk7.dim("-")}
3188
4200
  `;
3189
- process.stdout.write(
3190
- chalk7.bold("\n Openship status\n\n") + row("Context", context) + row("API", apiUrl) + row("Health", chalk7.green(health.status ?? "ok")) + row("Mode", envInfo.selfHosted ? "self-hosted" : "cloud") + row("Deploy", envInfo.deployMode) + row("Auth", envInfo.authMode) + row("Team", envInfo.teamMode) + (envInfo.hostDomain ? row("Host domain", envInfo.hostDomain) : "") + (envInfo.machineName ? row("Machine", envInfo.machineName) : "") + "\n"
3191
- );
4201
+ const serviceState = svc.running ? chalk7.green("running") : svc.installed ? chalk7.yellow("installed \xB7 stopped") : chalk7.dim("not installed");
4202
+ let out = chalk7.bold("\n Openship status\n\n") + row("Service", serviceState) + row("Manager", svc.kind === "unsupported" ? chalk7.dim("none") : svc.kind) + (ports.api ? row("API port", ports.api) : "") + (ports.dashboard ? row("Dashboard port", ports.dashboard) : "") + row("Context", context) + row("API", apiUrl);
4203
+ if (reachable && health && envInfo) {
4204
+ out += row("Health", chalk7.green(health.status ?? "ok")) + row("Mode", envInfo.selfHosted ? "self-hosted" : "cloud") + row("Deploy", envInfo.deployMode) + row("Auth", envInfo.authMode) + row("Team", envInfo.teamMode) + (envInfo.hostDomain ? row("Host domain", envInfo.hostDomain) : "") + (envInfo.machineName ? row("Machine", envInfo.machineName) : "");
4205
+ } else {
4206
+ out += row("Health", chalk7.red("not reachable")) + chalk7.dim(` ${unreachableMsg}
4207
+ `) + chalk7.dim(svc.running ? " (service is up \u2014 it may still be starting)\n" : " Start it with `openship up`.\n");
4208
+ }
4209
+ process.stdout.write(out + "\n");
4210
+ if (!reachable) process.exit(1);
3192
4211
  });
3193
4212
 
3194
4213
  // src/commands/doctor.ts
3195
- import { Command as Command9 } from "commander";
3196
- import { existsSync as existsSync6 } from "fs";
4214
+ import { Command as Command10 } from "commander";
4215
+ import { existsSync as existsSync9 } from "fs";
3197
4216
  import { execFileSync } from "child_process";
3198
4217
  import chalk8 from "chalk";
3199
4218
  function bunVersion() {
@@ -3205,9 +4224,9 @@ function bunVersion() {
3205
4224
  return null;
3206
4225
  }
3207
4226
  }
3208
- var doctorCommand = new Command9("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
4227
+ var doctorCommand = new Command10("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
3209
4228
  const checks = [];
3210
- const hasConfig = existsSync6(CONFIG_PATH);
4229
+ const hasConfig = existsSync9(CONFIG_PATH);
3211
4230
  checks.push({
3212
4231
  name: "config",
3213
4232
  status: hasConfig ? "pass" : "warn",
@@ -3255,20 +4274,20 @@ var doctorCommand = new Command9("doctor").description("Diagnose the CLI setup (
3255
4274
  });
3256
4275
 
3257
4276
  // src/commands/deploy.ts
3258
- import { Command as Command10 } from "commander";
4277
+ import { Command as Command11 } from "commander";
3259
4278
  import { execFileSync as execFileSync3 } from "child_process";
3260
4279
  import ora2 from "ora";
3261
4280
 
3262
4281
  // src/lib/project-link.ts
3263
- import { readFileSync as readFileSync3, existsSync as existsSync7 } from "fs";
3264
- import { join as join7, dirname as dirname3, parse } from "path";
3265
- var LINK_REL = join7(".openship", "project.json");
4282
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
4283
+ import { join as join11, dirname as dirname3, parse } from "path";
4284
+ var LINK_REL = join11(".openship", "project.json");
3266
4285
  function findProjectLinkPath(from = process.cwd()) {
3267
4286
  let dir = from;
3268
4287
  const root = parse(dir).root;
3269
4288
  for (; ; ) {
3270
- const candidate = join7(dir, LINK_REL);
3271
- if (existsSync7(candidate)) return candidate;
4289
+ const candidate = join11(dir, LINK_REL);
4290
+ if (existsSync10(candidate)) return candidate;
3272
4291
  if (dir === root) return null;
3273
4292
  dir = dirname3(dir);
3274
4293
  }
@@ -3277,7 +4296,7 @@ function readProjectLink(from) {
3277
4296
  const path2 = findProjectLinkPath(from);
3278
4297
  if (!path2) return null;
3279
4298
  try {
3280
- return JSON.parse(readFileSync3(path2, "utf8"));
4299
+ return JSON.parse(readFileSync7(path2, "utf8"));
3281
4300
  } catch {
3282
4301
  return null;
3283
4302
  }
@@ -3285,21 +4304,21 @@ function readProjectLink(from) {
3285
4304
 
3286
4305
  // src/lib/folder-deploy.ts
3287
4306
  import { execFileSync as execFileSync2 } from "child_process";
3288
- import { readFileSync as readFileSync4, existsSync as existsSync8, rmSync as rmSync3 } from "fs";
4307
+ import { readFileSync as readFileSync8, existsSync as existsSync11, rmSync as rmSync3 } from "fs";
3289
4308
  import { tmpdir } from "os";
3290
- import { join as join8, basename } from "path";
4309
+ import { join as join12, basename } from "path";
3291
4310
  function detectPackageManager(dir) {
3292
- if (existsSync8(join8(dir, "bun.lockb")) || existsSync8(join8(dir, "bun.lock"))) return "bun";
3293
- if (existsSync8(join8(dir, "pnpm-lock.yaml"))) return "pnpm";
3294
- if (existsSync8(join8(dir, "yarn.lock"))) return "yarn";
3295
- if (existsSync8(join8(dir, "package.json"))) return "npm";
4311
+ if (existsSync11(join12(dir, "bun.lockb")) || existsSync11(join12(dir, "bun.lock"))) return "bun";
4312
+ if (existsSync11(join12(dir, "pnpm-lock.yaml"))) return "pnpm";
4313
+ if (existsSync11(join12(dir, "yarn.lock"))) return "yarn";
4314
+ if (existsSync11(join12(dir, "package.json"))) return "npm";
3296
4315
  return void 0;
3297
4316
  }
3298
4317
  function detectStack(dir) {
3299
- if (existsSync8(join8(dir, "go.mod"))) return "go";
3300
- if (existsSync8(join8(dir, "Cargo.toml"))) return "rust";
3301
- if (existsSync8(join8(dir, "requirements.txt")) || existsSync8(join8(dir, "pyproject.toml"))) return "python";
3302
- if (existsSync8(join8(dir, "package.json"))) return "node";
4318
+ if (existsSync11(join12(dir, "go.mod"))) return "go";
4319
+ if (existsSync11(join12(dir, "Cargo.toml"))) return "rust";
4320
+ if (existsSync11(join12(dir, "requirements.txt")) || existsSync11(join12(dir, "pyproject.toml"))) return "python";
4321
+ if (existsSync11(join12(dir, "package.json"))) return "node";
3303
4322
  return void 0;
3304
4323
  }
3305
4324
  async function deployFolder(opts) {
@@ -3316,7 +4335,7 @@ async function deployFolder(opts) {
3316
4335
  throw new Error(session.error || "Failed to open upload session");
3317
4336
  }
3318
4337
  step("Packaging folder");
3319
- const tarball = join8(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
4338
+ const tarball = join12(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3320
4339
  execFileSync2(
3321
4340
  "tar",
3322
4341
  [
@@ -3335,7 +4354,7 @@ async function deployFolder(opts) {
3335
4354
  );
3336
4355
  step("Uploading source");
3337
4356
  try {
3338
- const body = readFileSync4(tarball);
4357
+ const body = readFileSync8(tarball);
3339
4358
  const up = session.upload;
3340
4359
  const method = up.method || "POST";
3341
4360
  const res = /^https?:\/\//i.test(up.url) ? await fetch(up.url, { method, headers: up.headers, body }) : await apiRaw(`/${up.url.replace(/^\/+/, "")}`, { method, headers: up.headers, body });
@@ -3516,7 +4535,7 @@ function git(args) {
3516
4535
  return void 0;
3517
4536
  }
3518
4537
  }
3519
- var deployCommand = new Command10("deploy").description("Trigger a deployment for the current project").option("--project <id>", "Project ID (defaults to the linked project in .openship/project.json)").option("--branch <name>", "Git branch to deploy (defaults to the current branch)").option("--commit <sha>", "Specific commit SHA (defaults to the latest commit on the branch)").option("--env <environment>", "Target environment: production | preview", "production").option("--force-all", "Rebuild every enabled service (skip smart per-service routing)").option("--service-ids <ids>", "Comma-separated service IDs to deploy (smart routing)").option("--smart-route", "Rebuild only services changed since the active deploy").option("--refresh", "Re-apply current env to the active deploy (no git pull, no rebuild)").option("--name <name>", "Project name for a folder (non-git) deploy (defaults to the directory name)").option("--watch", "Stream the deployment logs until it finishes").action(async (opts) => {
4538
+ var deployCommand = new Command11("deploy").description("Trigger a deployment for the current project").option("--project <id>", "Project ID (defaults to the linked project in .openship/project.json)").option("--branch <name>", "Git branch to deploy (defaults to the current branch)").option("--commit <sha>", "Specific commit SHA (defaults to the latest commit on the branch)").option("--env <environment>", "Target environment: production | preview", "production").option("--force-all", "Rebuild every enabled service (skip smart per-service routing)").option("--service-ids <ids>", "Comma-separated service IDs to deploy (smart routing)").option("--smart-route", "Rebuild only services changed since the active deploy").option("--refresh", "Re-apply current env to the active deploy (no git pull, no rebuild)").option("--name <name>", "Project name for a folder (non-git) deploy (defaults to the directory name)").option("--watch", "Stream the deployment logs until it finishes").action(async (opts) => {
3520
4539
  const link = readProjectLink();
3521
4540
  const env = opts.env;
3522
4541
  if (env !== "production" && env !== "preview") {
@@ -3598,9 +4617,9 @@ var deployCommand = new Command10("deploy").description("Trigger a deployment fo
3598
4617
  });
3599
4618
 
3600
4619
  // src/commands/deployment.ts
3601
- import { Command as Command11 } from "commander";
4620
+ import { Command as Command12 } from "commander";
3602
4621
  import { createInterface as createInterface3 } from "readline";
3603
- function run2(fn) {
4622
+ function run3(fn) {
3604
4623
  return async (...args) => {
3605
4624
  try {
3606
4625
  await fn(...args);
@@ -3614,18 +4633,18 @@ function report(res, message) {
3614
4633
  if (isJsonMode()) printJson(res);
3615
4634
  else ok(message);
3616
4635
  }
3617
- function shortSha(v) {
4636
+ function shortSha2(v) {
3618
4637
  return typeof v === "string" ? v.slice(0, 7) : "";
3619
4638
  }
3620
4639
  async function confirm(question) {
3621
4640
  if (!process.stdin.isTTY) return true;
3622
4641
  const rl = createInterface3({ input: process.stdin, output: process.stderr });
3623
- const answer = await new Promise((resolve2) => rl.question(`${question} [y/N] `, resolve2));
4642
+ const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
3624
4643
  rl.close();
3625
4644
  return /^y(es)?$/i.test(answer.trim());
3626
4645
  }
3627
- var list = new Command11("list").description("List deployments (org-wide, or scoped to a project)").option("--project <id>", "Scope to a project (defaults to the linked project)").option("--env <environment>", "Filter by environment: production | preview").option("--limit <n>", "Max rows to fetch", "50").action(
3628
- run2(async (opts) => {
4646
+ var list = new Command12("list").description("List deployments (org-wide, or scoped to a project)").option("--project <id>", "Scope to a project (defaults to the linked project)").option("--env <environment>", "Filter by environment: production | preview").option("--limit <n>", "Max rows to fetch", "50").action(
4647
+ run3(async (opts) => {
3629
4648
  const projectId = opts.project || readProjectLink()?.projectId;
3630
4649
  const params = new URLSearchParams();
3631
4650
  if (projectId) params.set("projectId", projectId);
@@ -3640,15 +4659,15 @@ var list = new Command11("list").description("List deployments (org-wide, or sco
3640
4659
  status: d.status,
3641
4660
  env: d.environment,
3642
4661
  branch: d.branch,
3643
- commit: shortSha(d.commitSha),
4662
+ commit: shortSha2(d.commitSha),
3644
4663
  active: d.isActive ? "*" : "",
3645
4664
  created: d.createdAt
3646
4665
  }));
3647
4666
  printTable(rows, ["id", "status", "env", "branch", "commit", "active", "created"]);
3648
4667
  })
3649
4668
  );
3650
- var get = new Command11("get").description("Show a single deployment").argument("<id>", "Deployment ID").action(
3651
- run2(async (id) => {
4669
+ var get = new Command12("get").description("Show a single deployment").argument("<id>", "Deployment ID").action(
4670
+ run3(async (id) => {
3652
4671
  const res = await apiRequest(`/deployments/${id}`);
3653
4672
  const d = res.data ?? {};
3654
4673
  if (isJsonMode()) return printJson(d);
@@ -3668,20 +4687,20 @@ var get = new Command11("get").description("Show a single deployment").argument(
3668
4687
  );
3669
4688
  })
3670
4689
  );
3671
- var info2 = new Command11("info").description("Show container info for a deployment").argument("<id>", "Deployment ID").action(
3672
- run2(async (id) => {
4690
+ var info2 = new Command12("info").description("Show container info for a deployment").argument("<id>", "Deployment ID").action(
4691
+ run3(async (id) => {
3673
4692
  const res = await apiRequest(`/deployments/${id}/info`);
3674
4693
  printJson(res.data ?? res);
3675
4694
  })
3676
4695
  );
3677
- var usage = new Command11("usage").description("Show container resource usage for a deployment").argument("<id>", "Deployment ID").action(
3678
- run2(async (id) => {
4696
+ var usage = new Command12("usage").description("Show container resource usage for a deployment").argument("<id>", "Deployment ID").action(
4697
+ run3(async (id) => {
3679
4698
  const res = await apiRequest(`/deployments/${id}/usage`);
3680
4699
  printJson(res.data ?? res);
3681
4700
  })
3682
4701
  );
3683
- var redeploy = new Command11("redeploy").description("Redeploy from an existing deployment").argument("<id>", "Deployment ID").option("--use-existing-commit", "Rebuild the same commit instead of the latest on the branch").action(
3684
- run2(async (id, opts) => {
4702
+ var redeploy = new Command12("redeploy").description("Redeploy from an existing deployment").argument("<id>", "Deployment ID").option("--use-existing-commit", "Rebuild the same commit instead of the latest on the branch").action(
4703
+ run3(async (id, opts) => {
3685
4704
  const res = await apiRequest(`/deployments/${id}/redeploy`, {
3686
4705
  method: "POST",
3687
4706
  body: JSON.stringify({ useExistingCommit: opts.useExistingCommit === true })
@@ -3689,14 +4708,14 @@ var redeploy = new Command11("redeploy").description("Redeploy from an existing
3689
4708
  report(res, `Redeploy triggered for ${id}`);
3690
4709
  })
3691
4710
  );
3692
- var rollback = new Command11("rollback").description("Roll back to a previous deployment").argument("<id>", "Deployment ID to roll back to").action(
3693
- run2(async (id) => {
4711
+ var rollback = new Command12("rollback").description("Roll back to a previous deployment").argument("<id>", "Deployment ID to roll back to").action(
4712
+ run3(async (id) => {
3694
4713
  const res = await apiRequest(`/deployments/${id}/rollback`, { method: "POST" });
3695
4714
  report(res, `Rolled back to ${id}`);
3696
4715
  })
3697
4716
  );
3698
- var pin = new Command11("pin").description("Pin (or unpin) a deployment's rollback artifact").argument("<id>", "Deployment ID").option("--off", "Unpin instead of pin").action(
3699
- run2(async (id, opts) => {
4717
+ var pin = new Command12("pin").description("Pin (or unpin) a deployment's rollback artifact").argument("<id>", "Deployment ID").option("--off", "Unpin instead of pin").action(
4718
+ run3(async (id, opts) => {
3700
4719
  const pinned = !opts.off;
3701
4720
  const res = await apiRequest(`/deployments/${id}/pin`, {
3702
4721
  method: "POST",
@@ -3705,32 +4724,32 @@ var pin = new Command11("pin").description("Pin (or unpin) a deployment's rollba
3705
4724
  report(res, `${pinned ? "Pinned" : "Unpinned"} ${id}`);
3706
4725
  })
3707
4726
  );
3708
- var cancel = new Command11("cancel").description("Cancel an in-progress deployment").argument("<id>", "Deployment ID").action(
3709
- run2(async (id) => {
4727
+ var cancel = new Command12("cancel").description("Cancel an in-progress deployment").argument("<id>", "Deployment ID").action(
4728
+ run3(async (id) => {
3710
4729
  const res = await apiRequest(`/deployments/${id}/cancel`, { method: "POST" });
3711
4730
  report(res, `Cancelled ${id}`);
3712
4731
  })
3713
4732
  );
3714
- var restart2 = new Command11("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
3715
- run2(async (id) => {
4733
+ var restart2 = new Command12("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
4734
+ run3(async (id) => {
3716
4735
  const res = await apiRequest(`/deployments/${id}/restart`, { method: "POST" });
3717
4736
  report(res, `Restarted ${id}`);
3718
4737
  })
3719
4738
  );
3720
- var reject = new Command11("reject").description("Reject a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
3721
- run2(async (id) => {
4739
+ var reject = new Command12("reject").description("Reject a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
4740
+ run3(async (id) => {
3722
4741
  const res = await apiRequest(`/deployments/${id}/reject`, { method: "POST" });
3723
4742
  report(res, `Rejected ${id}`);
3724
4743
  })
3725
4744
  );
3726
- var keep = new Command11("keep").description("Keep a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
3727
- run2(async (id) => {
4745
+ var keep = new Command12("keep").description("Keep a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
4746
+ run3(async (id) => {
3728
4747
  const res = await apiRequest(`/deployments/${id}/keep`, { method: "POST" });
3729
4748
  report(res, `Kept ${id}`);
3730
4749
  })
3731
4750
  );
3732
- var rm = new Command11("rm").description("Delete a deployment").argument("<id>", "Deployment ID").option("-y, --yes", "Skip the confirmation prompt").action(
3733
- run2(async (id, opts) => {
4751
+ var rm = new Command12("rm").description("Delete a deployment").argument("<id>", "Deployment ID").option("-y, --yes", "Skip the confirmation prompt").action(
4752
+ run3(async (id, opts) => {
3734
4753
  if (!opts.yes && !isJsonMode() && !await confirm(`Delete deployment ${id}?`)) {
3735
4754
  err("Aborted.");
3736
4755
  process.exit(1);
@@ -3739,8 +4758,8 @@ var rm = new Command11("rm").description("Delete a deployment").argument("<id>",
3739
4758
  report(res, `Deleted ${id}`);
3740
4759
  })
3741
4760
  );
3742
- var sslStatus = new Command11("status").description("Check SSL certificate status for a domain").argument("<domain>", "Domain to probe").action(
3743
- run2(async (domain) => {
4761
+ var sslStatus = new Command12("status").description("Check SSL certificate status for a domain").argument("<domain>", "Domain to probe").action(
4762
+ run3(async (domain) => {
3744
4763
  const res = await apiRequest("/deployments/ssl/status", {
3745
4764
  method: "POST",
3746
4765
  body: JSON.stringify({ domain })
@@ -3748,8 +4767,8 @@ var sslStatus = new Command11("status").description("Check SSL certificate statu
3748
4767
  printJson(res);
3749
4768
  })
3750
4769
  );
3751
- var sslRenew = new Command11("renew").description("Renew (issue) an SSL certificate for a domain").argument("<domain>", "Domain to renew").option("--www", "Also include the www subdomain").action(
3752
- run2(async (domain, opts) => {
4770
+ var sslRenew = new Command12("renew").description("Renew (issue) an SSL certificate for a domain").argument("<domain>", "Domain to renew").option("--www", "Also include the www subdomain").action(
4771
+ run3(async (domain, opts) => {
3753
4772
  const res = await apiRequest("/deployments/ssl/renew", {
3754
4773
  method: "POST",
3755
4774
  body: JSON.stringify({ domain, includeWww: opts.www === true })
@@ -3757,12 +4776,12 @@ var sslRenew = new Command11("renew").description("Renew (issue) an SSL certific
3757
4776
  report(res, `SSL renewal requested for ${domain}`);
3758
4777
  })
3759
4778
  );
3760
- var ssl = new Command11("ssl").description("SSL certificate operations").addCommand(sslStatus).addCommand(sslRenew);
3761
- var deploymentCommand = new Command11("deployment").alias("deployments").description("Manage deployments (list, inspect, redeploy, rollback, \u2026)").addCommand(list).addCommand(get).addCommand(info2).addCommand(usage).addCommand(redeploy).addCommand(rollback).addCommand(pin).addCommand(cancel).addCommand(restart2).addCommand(reject).addCommand(keep).addCommand(rm).addCommand(ssl);
4779
+ var ssl = new Command12("ssl").description("SSL certificate operations").addCommand(sslStatus).addCommand(sslRenew);
4780
+ var deploymentCommand = new Command12("deployment").alias("deployments").description("Manage deployments (list, inspect, redeploy, rollback, \u2026)").addCommand(list).addCommand(get).addCommand(info2).addCommand(usage).addCommand(redeploy).addCommand(rollback).addCommand(pin).addCommand(cancel).addCommand(restart2).addCommand(reject).addCommand(keep).addCommand(rm).addCommand(ssl);
3762
4781
 
3763
4782
  // src/commands/logs.ts
3764
- import { Command as Command12 } from "commander";
3765
- var logsCommand = new Command12("logs").description("View or stream a deployment's logs").argument("<deploymentId>", "Deployment ID").option("-f, --follow", "Stream live logs via SSE until the deployment finishes").option("--tail <n>", "Show only the last N log lines (snapshot mode)").action(async (deploymentId, opts) => {
4783
+ import { Command as Command13 } from "commander";
4784
+ var logsCommand = new Command13("logs").description("View or stream a deployment's logs").argument("<deploymentId>", "Deployment ID").option("-f, --follow", "Stream live logs via SSE until the deployment finishes").option("--tail <n>", "Show only the last N log lines (snapshot mode)").action(async (deploymentId, opts) => {
3766
4785
  if (opts.follow) {
3767
4786
  try {
3768
4787
  const result = await streamDeploymentLogs(deploymentId);
@@ -3794,7 +4813,7 @@ var logsCommand = new Command12("logs").description("View or stream a deployment
3794
4813
  });
3795
4814
 
3796
4815
  // src/commands/project.ts
3797
- import { Command as Command13 } from "commander";
4816
+ import { Command as Command14 } from "commander";
3798
4817
  import chalk9 from "chalk";
3799
4818
  import { createInterface as createInterface4 } from "readline/promises";
3800
4819
  import { stdin as input3, stdout as output3 } from "process";
@@ -3831,7 +4850,7 @@ function printProject(project) {
3831
4850
  }
3832
4851
  }
3833
4852
  var ENVIRONMENTS = ["production", "preview", "development"];
3834
- var listCmd2 = new Command13("list").alias("ls").description("List projects in the active organization").action(
4853
+ var listCmd2 = new Command14("list").alias("ls").description("List projects in the active organization").action(
3835
4854
  action(async () => {
3836
4855
  const rows = [];
3837
4856
  for await (const p of paginate("/projects")) {
@@ -3846,7 +4865,7 @@ var listCmd2 = new Command13("list").alias("ls").description("List projects in t
3846
4865
  printTable(rows, ["id", "name", "slug", "repo", "source"]);
3847
4866
  })
3848
4867
  );
3849
- var getCmd = new Command13("get").description("Show a single project").argument("<id>", "Project ID").action(
4868
+ var getCmd = new Command14("get").description("Show a single project").argument("<id>", "Project ID").action(
3850
4869
  action(async (id) => {
3851
4870
  const { data } = await apiRequest(
3852
4871
  `/projects/${encodeURIComponent(id)}`
@@ -3854,7 +4873,7 @@ var getCmd = new Command13("get").description("Show a single project").argument(
3854
4873
  printProject(data);
3855
4874
  })
3856
4875
  );
3857
- var createCmd = new Command13("create").description("Create a project").requiredOption("--name <name>", "Project name").option("--slug <slug>", "Free-subdomain slug (slug.opsh.io)").option("--git-owner <owner>", "GitHub owner/org").option("--git-repo <repo>", "GitHub repository name").option("--git-branch <branch>", "Git branch to deploy").option("--framework <framework>", "Stack/framework id").option("--local-path <path>", "Local source path").option("--port <port>", "Container port", (v) => Number(v)).option(
4876
+ var createCmd = new Command14("create").description("Create a project").requiredOption("--name <name>", "Project name").option("--slug <slug>", "Free-subdomain slug (slug.opsh.io)").option("--git-owner <owner>", "GitHub owner/org").option("--git-repo <repo>", "GitHub repository name").option("--git-branch <branch>", "Git branch to deploy").option("--framework <framework>", "Stack/framework id").option("--local-path <path>", "Local source path").option("--port <port>", "Container port", (v) => Number(v)).option(
3858
4877
  "--type <type>",
3859
4878
  "Project type: app | docker | services | monorepo"
3860
4879
  ).action(
@@ -3878,7 +4897,7 @@ var createCmd = new Command13("create").description("Create a project").required
3878
4897
  printProject(data);
3879
4898
  })
3880
4899
  );
3881
- var deleteCmd = new Command13("delete").alias("rm").description("Delete a project (tears down all resources)").argument("<id>", "Project ID").option("--force", "Cancel active work and delete anyway").option("--force-orphan", "Orphan resources that won't destroy, then drop the row").option("--wipe-volumes", "Also destroy persistent volumes").option("-y, --yes", "Skip the confirmation prompt").action(
4900
+ var deleteCmd = new Command14("delete").alias("rm").description("Delete a project (tears down all resources)").argument("<id>", "Project ID").option("--force", "Cancel active work and delete anyway").option("--force-orphan", "Orphan resources that won't destroy, then drop the row").option("--wipe-volumes", "Also destroy persistent volumes").option("-y, --yes", "Skip the confirmation prompt").action(
3882
4901
  action(async (id, opts) => {
3883
4902
  if (!opts.yes) {
3884
4903
  const rl = createInterface4({ input: input3, output: output3 });
@@ -3909,7 +4928,7 @@ var deleteCmd = new Command13("delete").alias("rm").description("Delete a projec
3909
4928
  `);
3910
4929
  })
3911
4930
  );
3912
- var envCmd = new Command13("env").description("Manage project environment variables");
4931
+ var envCmd = new Command14("env").description("Manage project environment variables");
3913
4932
  envCmd.command("get").description("List env vars (secret values are masked by the API)").argument("<id>", "Project ID").option("--environment <env>", "Filter by environment (production|preview|development)").action(
3914
4933
  action(async (id, opts) => {
3915
4934
  const qs = opts.environment ? `?environment=${encodeURIComponent(opts.environment)}` : "";
@@ -3983,7 +5002,7 @@ envCmd.command("set").description("Merge env vars: upsert KEY=VALUE pairs and/or
3983
5002
  );
3984
5003
  })
3985
5004
  );
3986
- var gitCmd = new Command13("git").description("Manage git linkage and auto-deploy");
5005
+ var gitCmd = new Command14("git").description("Manage git linkage and auto-deploy");
3987
5006
  gitCmd.command("link").description("Link a GitHub repository to a project").argument("<id>", "Project ID").requiredOption("--owner <owner>", "GitHub owner/org").requiredOption("--repo <repo>", "Repository name").option("--branch <branch>", "Branch (defaults to the repo's default branch)").option("--installation-id <id>", "GitHub App installation id", (v) => Number(v)).action(
3988
5007
  action(async (id, opts) => {
3989
5008
  const result = await apiRequest(
@@ -4071,7 +5090,7 @@ gitCmd.command("webhook-domain").description("Set or clear the domain that recei
4071
5090
  `);
4072
5091
  })
4073
5092
  );
4074
- var connectCmd = new Command13("connect").description("Connect a custom domain to a project").argument("<id>", "Project ID").argument("<domain>", "Custom domain hostname").option("--include-www", "Also connect the www. variant").action(
5093
+ var connectCmd = new Command14("connect").description("Connect a custom domain to a project").argument("<id>", "Project ID").argument("<domain>", "Custom domain hostname").option("--include-www", "Also connect the www. variant").action(
4075
5094
  action(async (id, domain, opts) => {
4076
5095
  const result = await apiRequest(
4077
5096
  `/projects/${encodeURIComponent(id)}/connect`,
@@ -4091,7 +5110,7 @@ var connectCmd = new Command13("connect").description("Connect a custom domain t
4091
5110
  printJson(result.records);
4092
5111
  })
4093
5112
  );
4094
- var enableCmd = new Command13("enable").description("Start a stopped project").argument("<id>", "Project ID").action(
5113
+ var enableCmd = new Command14("enable").description("Start a stopped project").argument("<id>", "Project ID").action(
4095
5114
  action(async (id) => {
4096
5115
  const result = await apiRequest(
4097
5116
  `/projects/${encodeURIComponent(id)}/enable`,
@@ -4103,7 +5122,7 @@ var enableCmd = new Command13("enable").description("Start a stopped project").a
4103
5122
  `);
4104
5123
  })
4105
5124
  );
4106
- var disableCmd = new Command13("disable").description("Stop a running project").argument("<id>", "Project ID").action(
5125
+ var disableCmd = new Command14("disable").description("Stop a running project").argument("<id>", "Project ID").action(
4107
5126
  action(async (id) => {
4108
5127
  const result = await apiRequest(
4109
5128
  `/projects/${encodeURIComponent(id)}/disable`,
@@ -4116,7 +5135,7 @@ var disableCmd = new Command13("disable").description("Stop a running project").
4116
5135
  })
4117
5136
  );
4118
5137
  var SLEEP_MODES = ["auto_sleep", "always_on"];
4119
- var sleepModeCmd = new Command13("sleep-mode").description("Set the project sleep mode").argument("<id>", "Project ID").argument("<mode>", `One of: ${SLEEP_MODES.join(", ")}`).action(
5138
+ var sleepModeCmd = new Command14("sleep-mode").description("Set the project sleep mode").argument("<id>", "Project ID").argument("<mode>", `One of: ${SLEEP_MODES.join(", ")}`).action(
4120
5139
  action(async (id, mode) => {
4121
5140
  if (!SLEEP_MODES.includes(mode)) {
4122
5141
  err(` mode must be one of: ${SLEEP_MODES.join(", ")}`);
@@ -4134,7 +5153,7 @@ var sleepModeCmd = new Command13("sleep-mode").description("Set the project slee
4134
5153
  })
4135
5154
  );
4136
5155
  var TRANSFER_DIRS = ["to-cloud", "to-self-hosted"];
4137
- var transferCmd = new Command13("transfer").description("Promote a project to Openship Cloud, or bring it back (self-hosted only)").argument("<id>", "Project ID").argument("<direction>", `One of: ${TRANSFER_DIRS.join(", ")}`).action(
5156
+ var transferCmd = new Command14("transfer").description("Promote a project to Openship Cloud, or bring it back (self-hosted only)").argument("<id>", "Project ID").argument("<direction>", `One of: ${TRANSFER_DIRS.join(", ")}`).action(
4138
5157
  action(async (id, direction) => {
4139
5158
  if (!TRANSFER_DIRS.includes(direction)) {
4140
5159
  err(` direction must be one of: ${TRANSFER_DIRS.join(", ")}`);
@@ -4157,7 +5176,7 @@ var transferCmd = new Command13("transfer").description("Promote a project to Op
4157
5176
  );
4158
5177
  })
4159
5178
  );
4160
- var logsCmd = new Command13("logs").description("Show or stream runtime (container) logs").argument("<id>", "Project ID").option("--tail <n>", "Number of recent lines", (v) => Number(v)).option("-f, --follow", "Stream logs until interrupted").action(
5179
+ var logsCmd = new Command14("logs").description("Show or stream runtime (container) logs").argument("<id>", "Project ID").option("--tail <n>", "Number of recent lines", (v) => Number(v)).option("-f, --follow", "Stream logs until interrupted").action(
4161
5180
  action(async (id, opts) => {
4162
5181
  const tailQs = opts.tail ? `?tail=${opts.tail}` : "";
4163
5182
  if (!opts.follow) {
@@ -4185,7 +5204,7 @@ var logsCmd = new Command13("logs").description("Show or stream runtime (contain
4185
5204
  }
4186
5205
  })
4187
5206
  );
4188
- var serverLogsCmd = new Command13("server-logs").description("Show or stream HTTP request logs (edge/OpenResty)").argument("<id>", "Project ID").option("--limit <n>", "Number of recent entries (max 200)", (v) => Number(v)).option("--domain <domain>", "Restrict to a specific domain").option("-f, --follow", "Stream request logs until interrupted").action(
5207
+ var serverLogsCmd = new Command14("server-logs").description("Show or stream HTTP request logs (edge/OpenResty)").argument("<id>", "Project ID").option("--limit <n>", "Number of recent entries (max 200)", (v) => Number(v)).option("--domain <domain>", "Restrict to a specific domain").option("-f, --follow", "Stream request logs until interrupted").action(
4189
5208
  action(async (id, opts) => {
4190
5209
  const base = `/projects/${encodeURIComponent(id)}/server-logs`;
4191
5210
  const domainQs = opts.domain ? `domain=${encodeURIComponent(opts.domain)}` : "";
@@ -4233,7 +5252,7 @@ function printLogEntry(entry) {
4233
5252
  process.stdout.write(` ${ts} ${color(level.padEnd(5))} ${String(msg)}
4234
5253
  `);
4235
5254
  }
4236
- var projectCommand = new Command13("project").alias("projects").description("Manage Openship projects");
5255
+ var projectCommand = new Command14("project").alias("projects").description("Manage Openship projects");
4237
5256
  projectCommand.addCommand(listCmd2);
4238
5257
  projectCommand.addCommand(getCmd);
4239
5258
  projectCommand.addCommand(createCmd);
@@ -4249,9 +5268,9 @@ projectCommand.addCommand(logsCmd);
4249
5268
  projectCommand.addCommand(serverLogsCmd);
4250
5269
 
4251
5270
  // src/commands/service.ts
4252
- import { Command as Command14 } from "commander";
5271
+ import { Command as Command15 } from "commander";
4253
5272
  import chalk10 from "chalk";
4254
- import { spawnSync as spawnSync3 } from "child_process";
5273
+ import { spawnSync as spawnSync4 } from "child_process";
4255
5274
  import path from "path";
4256
5275
  import { createInterface as createInterface5 } from "readline/promises";
4257
5276
  import { stdin as input4, stdout as output4 } from "process";
@@ -4270,7 +5289,7 @@ function fail(e) {
4270
5289
  process.exit(1);
4271
5290
  }
4272
5291
  function stackCommand(name) {
4273
- return new Command14(name).requiredOption(
5292
+ return new Command15(name).requiredOption(
4274
5293
  "-p, --project <id|slug|name>",
4275
5294
  "Stack (project) id, slug, or name"
4276
5295
  );
@@ -4518,7 +5537,7 @@ function mapComposeService(name, def, baseDir) {
4518
5537
  var syncCmd = stackCommand("sync").description("Sync a stack's services from a docker-compose file (services not in the file are removed)").argument("<compose-file>", "Path to docker-compose.yml / compose.yaml").option("-y, --yes", "Skip the confirmation prompt").action(async (composeFile, opts) => {
4519
5538
  requireAuth();
4520
5539
  const abs = path.resolve(composeFile);
4521
- const proc = spawnSync3(
5540
+ const proc = spawnSync4(
4522
5541
  "docker",
4523
5542
  ["compose", "-f", abs, "config", "--format", "json"],
4524
5543
  { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }
@@ -4612,7 +5631,7 @@ var containersCmd = stackCommand("containers").description("List the stack's act
4612
5631
  fail(e);
4613
5632
  }
4614
5633
  });
4615
- var driftCmd = new Command14("drift").description(
5634
+ var driftCmd = new Command15("drift").description(
4616
5635
  "Resolve compose drift on a service (upstream compose changed a value you edited)"
4617
5636
  );
4618
5637
  function driftActionCommand(action2) {
@@ -4640,7 +5659,7 @@ function driftActionCommand(action2) {
4640
5659
  }
4641
5660
  driftCmd.addCommand(driftActionCommand("accept"));
4642
5661
  driftCmd.addCommand(driftActionCommand("keep"));
4643
- var envCmd2 = new Command14("env").description("Read and write a service's environment variables");
5662
+ var envCmd2 = new Command15("env").description("Read and write a service's environment variables");
4644
5663
  var envGetCmd = stackCommand("get").description("List a service's environment variables (secrets masked)").argument("<service>", "Service name or id").option("-e, --env <environment>", "Environment: production | preview | development").action(async (service, opts) => {
4645
5664
  requireAuth();
4646
5665
  try {
@@ -4773,7 +5792,7 @@ var execCmd = stackCommand("exec").description("Open an interactive shell in a s
4773
5792
  );
4774
5793
  process.exit(1);
4775
5794
  });
4776
- var serviceCommand = new Command14("service").alias("services").description("Manage the services in a compose stack (a multi-service project)");
5795
+ var serviceCommand = new Command15("service").alias("services").description("Manage the services in a compose stack (a multi-service project)");
4777
5796
  serviceCommand.addCommand(listCmd3);
4778
5797
  serviceCommand.addCommand(getCmd2);
4779
5798
  serviceCommand.addCommand(createCmd2);
@@ -4789,7 +5808,7 @@ serviceCommand.addCommand(logsCmd2);
4789
5808
  serviceCommand.addCommand(execCmd);
4790
5809
 
4791
5810
  // src/commands/domain.ts
4792
- import { Command as Command15 } from "commander";
5811
+ import { Command as Command16 } from "commander";
4793
5812
  import chalk11 from "chalk";
4794
5813
  import ora3 from "ora";
4795
5814
  function spin(text2) {
@@ -4825,7 +5844,7 @@ function printRecords(result) {
4825
5844
  ["type", "host", "value"]
4826
5845
  );
4827
5846
  }
4828
- var listCmd4 = new Command15("list").description("List a project's custom domains").requiredOption("-p, --project <id>", "Project ID to list domains for").action(async (opts) => {
5847
+ var listCmd4 = new Command16("list").description("List a project's custom domains").requiredOption("-p, --project <id>", "Project ID to list domains for").action(async (opts) => {
4829
5848
  try {
4830
5849
  const res = await apiRequest(
4831
5850
  `/domains?projectId=${encodeURIComponent(opts.project)}`
@@ -4840,7 +5859,7 @@ var listCmd4 = new Command15("list").description("List a project's custom domain
4840
5859
  fail2(e);
4841
5860
  }
4842
5861
  });
4843
- var addCmd2 = new Command15("add").description("Add a custom domain to a project").argument("<hostname>", "Domain hostname (e.g. app.example.com)").requiredOption("-p, --project <id>", "Project ID to attach the domain to").option("--primary", "Mark this domain as the project's primary", false).action(async (hostname, opts) => {
5862
+ var addCmd2 = new Command16("add").description("Add a custom domain to a project").argument("<hostname>", "Domain hostname (e.g. app.example.com)").requiredOption("-p, --project <id>", "Project ID to attach the domain to").option("--primary", "Mark this domain as the project's primary", false).action(async (hostname, opts) => {
4844
5863
  const sp = spin(`Adding ${hostname}\u2026`);
4845
5864
  try {
4846
5865
  const res = await apiRequest("/domains", {
@@ -4859,7 +5878,7 @@ var addCmd2 = new Command15("add").description("Add a custom domain to a project
4859
5878
  fail2(e);
4860
5879
  }
4861
5880
  });
4862
- var previewCmd = new Command15("preview").description("Preview the DNS records a hostname would need (no changes saved)").argument("<hostname>", "Domain hostname to preview").action(async (hostname) => {
5881
+ var previewCmd = new Command16("preview").description("Preview the DNS records a hostname would need (no changes saved)").argument("<hostname>", "Domain hostname to preview").action(async (hostname) => {
4863
5882
  try {
4864
5883
  const res = await apiRequest("/domains/preview", {
4865
5884
  method: "POST",
@@ -4870,7 +5889,7 @@ var previewCmd = new Command15("preview").description("Preview the DNS records a
4870
5889
  fail2(e);
4871
5890
  }
4872
5891
  });
4873
- var verifyCmd = new Command15("verify").description("Run DNS verification for a domain").argument("<id>", "Domain ID").action(async (id) => {
5892
+ var verifyCmd = new Command16("verify").description("Run DNS verification for a domain").argument("<id>", "Domain ID").action(async (id) => {
4874
5893
  const sp = spin("Checking DNS records\u2026");
4875
5894
  try {
4876
5895
  const res = await apiRaw(`/domains/${encodeURIComponent(id)}/verify`, { method: "POST" });
@@ -4896,7 +5915,7 @@ var verifyCmd = new Command15("verify").description("Run DNS verification for a
4896
5915
  fail2(e);
4897
5916
  }
4898
5917
  });
4899
- var primaryCmd = new Command15("primary").description("Make a domain the project's primary hostname").argument("<id>", "Domain ID").action(async (id) => {
5918
+ var primaryCmd = new Command16("primary").description("Make a domain the project's primary hostname").argument("<id>", "Domain ID").action(async (id) => {
4900
5919
  const sp = spin("Setting primary\u2026");
4901
5920
  try {
4902
5921
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/primary`, {
@@ -4909,7 +5928,7 @@ var primaryCmd = new Command15("primary").description("Make a domain the project
4909
5928
  fail2(e);
4910
5929
  }
4911
5930
  });
4912
- var recordsCmd = new Command15("records").description("Show the DNS records for an existing domain").argument("<id>", "Domain ID").action(async (id) => {
5931
+ var recordsCmd = new Command16("records").description("Show the DNS records for an existing domain").argument("<id>", "Domain ID").action(async (id) => {
4913
5932
  try {
4914
5933
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/records`);
4915
5934
  printRecords(res.data);
@@ -4927,7 +5946,7 @@ function printSsl(data) {
4927
5946
  if (data.issuer) info(` issuer: ${data.issuer}`);
4928
5947
  if (data.expiresAt) info(` expires: ${data.expiresAt}`);
4929
5948
  }
4930
- var renewCmd = new Command15("renew").description("Renew the SSL certificate for a domain").argument("<id>", "Domain ID").action(async (id) => {
5949
+ var renewCmd = new Command16("renew").description("Renew the SSL certificate for a domain").argument("<id>", "Domain ID").action(async (id) => {
4931
5950
  const sp = spin("Renewing certificate\u2026");
4932
5951
  try {
4933
5952
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/renew`, {
@@ -4940,7 +5959,7 @@ var renewCmd = new Command15("renew").description("Renew the SSL certificate for
4940
5959
  fail2(e);
4941
5960
  }
4942
5961
  });
4943
- var verifySslCmd = new Command15("verify-ssl").description("Recheck that a domain's SSL certificate is issued and valid (no reissue)").argument("<id>", "Domain ID").action(async (id) => {
5962
+ var verifySslCmd = new Command16("verify-ssl").description("Recheck that a domain's SSL certificate is issued and valid (no reissue)").argument("<id>", "Domain ID").action(async (id) => {
4944
5963
  const sp = spin("Checking certificate\u2026");
4945
5964
  try {
4946
5965
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/verify-ssl`, {
@@ -4955,7 +5974,7 @@ var verifySslCmd = new Command15("verify-ssl").description("Recheck that a domai
4955
5974
  fail2(e);
4956
5975
  }
4957
5976
  });
4958
- var renewAllCmd = new Command15("renew-all").description("Renew SSL for every near-expiry domain in your organization").action(async () => {
5977
+ var renewAllCmd = new Command16("renew-all").description("Renew SSL for every near-expiry domain in your organization").action(async () => {
4959
5978
  const sp = spin("Renewing expiring certificates\u2026");
4960
5979
  try {
4961
5980
  const res = await apiRequest("/domains/renew-all", { method: "POST" });
@@ -4977,10 +5996,10 @@ var renewAllCmd = new Command15("renew-all").description("Renew SSL for every ne
4977
5996
  fail2(e);
4978
5997
  }
4979
5998
  });
4980
- var domainCommand = new Command15("domain").description("Manage custom domains, DNS verification, and SSL certificates").addCommand(listCmd4).addCommand(addCmd2).addCommand(previewCmd).addCommand(verifyCmd).addCommand(primaryCmd).addCommand(recordsCmd).addCommand(renewCmd).addCommand(verifySslCmd).addCommand(renewAllCmd);
5999
+ var domainCommand = new Command16("domain").description("Manage custom domains, DNS verification, and SSL certificates").addCommand(listCmd4).addCommand(addCmd2).addCommand(previewCmd).addCommand(verifyCmd).addCommand(primaryCmd).addCommand(recordsCmd).addCommand(renewCmd).addCommand(verifySslCmd).addCommand(renewAllCmd);
4981
6000
 
4982
6001
  // src/commands/server.ts
4983
- import { Command as Command16 } from "commander";
6002
+ import { Command as Command17 } from "commander";
4984
6003
  import chalk12 from "chalk";
4985
6004
  import ora4 from "ora";
4986
6005
  var INSTALLABLE = ["docker", "git", "openresty", "certbot", "rsync"];
@@ -5013,7 +6032,7 @@ function connBody(o) {
5013
6032
  sshArgs: o.sshArgs
5014
6033
  };
5015
6034
  }
5016
- var server = new Command16("server").description("Manage self-hosted SSH servers");
6035
+ var server = new Command17("server").description("Manage self-hosted SSH servers");
5017
6036
  server.command("list").alias("ls").description("List servers in the active organization").action(
5018
6037
  guard(async () => {
5019
6038
  const servers = await apiRequest("/system/servers");
@@ -5091,6 +6110,50 @@ server.command("check <serverId>").description("Run component health checks agai
5091
6110
  else err(` Missing required components: ${res.missing.join(", ") || "none"}`);
5092
6111
  })
5093
6112
  );
6113
+ server.command("update <serverId>").description("Check for and apply native-module migrations (OpenResty, \u2026)").option("-c, --component <name...>", "Limit to specific modules").option("--check", "Only report drift; don't apply").action(
6114
+ guard(async (serverId, o) => {
6115
+ const base = `/system/servers/${encodeURIComponent(serverId)}/modules`;
6116
+ await apiRequest(`${base}/scan`, { method: "POST", body: "{}" }).catch(() => {
6117
+ });
6118
+ let mods = await apiRequest(base);
6119
+ if (o.component?.length) mods = mods.filter((m) => o.component.includes(m.moduleName));
6120
+ if (o.check) {
6121
+ if (isJsonMode()) return printJson(mods);
6122
+ printTable(
6123
+ mods.map((m) => ({
6124
+ module: m.moduleName,
6125
+ installed: m.installedVersion ?? "-",
6126
+ current: m.migrationVersion ?? "-",
6127
+ available: m.availableVersion ?? "-",
6128
+ behind: m.behind ? "yes" : "no",
6129
+ consent: String(m.detail?.pendingConsent?.length ?? 0)
6130
+ })),
6131
+ ["module", "installed", "current", "available", "behind", "consent"]
6132
+ );
6133
+ return;
6134
+ }
6135
+ const behind = mods.filter((m) => m.behind);
6136
+ if (!behind.length) return ok(" All modules up to date.");
6137
+ for (const m of behind) {
6138
+ const consent = m.detail?.pendingConsent ?? [];
6139
+ if (consent.length && !isJsonMode()) {
6140
+ info(` ${m.moduleName}: includes consent migrations \u2014 ${consent.map((c) => c.warning ?? c.id).join("; ")}`);
6141
+ }
6142
+ const spinner3 = isJsonMode() ? null : ora4(`Updating ${m.moduleName}\u2026`).start();
6143
+ const res = await apiRequest(`${base}/${encodeURIComponent(m.moduleName)}/apply`, {
6144
+ method: "POST",
6145
+ body: "{}"
6146
+ });
6147
+ spinner3?.stop();
6148
+ if (isJsonMode()) {
6149
+ printJson(res);
6150
+ continue;
6151
+ }
6152
+ if (res.ok) ok(` ${m.moduleName}: ${res.fromVersion} \u2192 ${res.toVersion} (${res.appliedSteps.length} step(s))`);
6153
+ else err(` ${m.moduleName}: ${res.error ?? "update failed"}`);
6154
+ }
6155
+ })
6156
+ );
5094
6157
  server.command("install <serverId>").description("Install components on a server").requiredOption("-c, --component <name...>", `Components to install (${INSTALLABLE.join("|")})`).option("--follow", "Stream install logs live (SSE)").action(
5095
6158
  guard(async (serverId, o) => {
5096
6159
  const components = o.component;
@@ -5231,9 +6294,9 @@ function fmtUptime(seconds) {
5231
6294
  var serverCommand = server;
5232
6295
 
5233
6296
  // src/commands/system.ts
5234
- import { Command as Command17 } from "commander";
6297
+ import { Command as Command18 } from "commander";
5235
6298
  import ora5 from "ora";
5236
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync6 } from "fs";
6299
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
5237
6300
  import { createInterface as createInterface6 } from "readline/promises";
5238
6301
  import { stdin as input5, stdout as output5 } from "process";
5239
6302
  async function guarded(fn) {
@@ -5279,7 +6342,7 @@ async function promptHidden(query) {
5279
6342
  output5.write("\n");
5280
6343
  return answer;
5281
6344
  }
5282
- var settingsCommand = new Command17("settings").description("Read or update instance settings");
6345
+ var settingsCommand = new Command18("settings").description("Read or update instance settings");
5283
6346
  settingsCommand.command("get").description("Show current instance settings").action(async () => {
5284
6347
  await guarded(async () => {
5285
6348
  const s = await apiRequest("/system/settings");
@@ -5318,7 +6381,7 @@ settingsCommand.command("set").description("Update instance-level settings").opt
5318
6381
  `));
5319
6382
  });
5320
6383
  });
5321
- var onboardingCommand = new Command17("onboarding").description("First-run instance setup");
6384
+ var onboardingCommand = new Command18("onboarding").description("First-run instance setup");
5322
6385
  onboardingCommand.command("apply").description("Configure a fresh instance (fails once already configured)").option("--ssh-host <host>", "SSH host of the target server").option("--ssh-port <n>", "SSH port (default 22)").option("--ssh-user <user>", "SSH user (default root)").option("--ssh-auth-method <method>", "SSH auth method").option("--ssh-password <password>", "SSH password").option("--ssh-key-path <path>", "SSH private key path").option("--ssh-key-passphrase <pass>", "SSH key passphrase").option("--ssh-jump-host <host>", "SSH jump host").option("--ssh-args <args>", "Extra SSH args").option("--server-name <name>", "Display name for the server").option("--auth-mode <mode>", "Initial auth mode: none | local | cloud").option("--tunnel-provider <provider>", "Tunnel provider").option("--tunnel-token <token>", "Tunnel token").option("--default-build-mode <mode>", "Default build mode").option("--default-rollback-window <n>", "Default rollback window").action(async (opts) => {
5323
6386
  await guarded(async () => {
5324
6387
  const body = {
@@ -5352,7 +6415,7 @@ onboardingCommand.command("apply").description("Configure a fresh instance (fail
5352
6415
  }
5353
6416
  });
5354
6417
  });
5355
- var upgradeToAuthCommand = new Command17("upgrade-to-auth").description("Promote a zero-auth instance to email/password login").option("--name <name>", "Account display name").option("--email <email>", "Account email").option("--password <password>", "Account password (prompted if omitted)").option("--use-own-mail-server", "Warm the self-hosted mail server for auth emails").action(async (opts) => {
6418
+ var upgradeToAuthCommand = new Command18("upgrade-to-auth").description("Promote a zero-auth instance to email/password login").option("--name <name>", "Account display name").option("--email <email>", "Account email").option("--password <password>", "Account password (prompted if omitted)").option("--use-own-mail-server", "Warm the self-hosted mail server for auth emails").action(async (opts) => {
5356
6419
  await guarded(async () => {
5357
6420
  const name = opts.name;
5358
6421
  const email = opts.email;
@@ -5381,7 +6444,7 @@ var upgradeToAuthCommand = new Command17("upgrade-to-auth").description("Promote
5381
6444
  `));
5382
6445
  });
5383
6446
  });
5384
- var browseCommand = new Command17("browse").description("List directories on the instance host (defaults to home)").argument("[path]", "Directory to list").action(async (path2) => {
6447
+ var browseCommand = new Command18("browse").description("List directories on the instance host (defaults to home)").argument("[path]", "Directory to list").action(async (path2) => {
5385
6448
  await guarded(async () => {
5386
6449
  const qs = path2 ? `?path=${encodeURIComponent(path2)}` : "";
5387
6450
  const res = await apiRequest(`/system/browse${qs}`);
@@ -5410,7 +6473,7 @@ function buildDomain(opts) {
5410
6473
  err("\n A domain is required: pass --hostname <host> or --slug <slug>.\n");
5411
6474
  process.exit(1);
5412
6475
  }
5413
- var migrationCommand = new Command17("migration").description("Team-mode migration lifecycle");
6476
+ var migrationCommand = new Command18("migration").description("Team-mode migration lifecycle");
5414
6477
  migrationCommand.command("preflight").description("Read-only readiness check for the own-server migration").requiredOption("--server-id <id>", "Target server id").option("--hostname <host>", "Custom domain pointing at the server").option("--slug <slug>", "Free <slug>.opsh.io subdomain").action(async (opts) => {
5415
6478
  await guarded(async () => {
5416
6479
  const domain = buildDomain(opts);
@@ -5517,7 +6580,7 @@ migrationCommand.command("switch-back").description("Reverse migration back to s
5517
6580
  }
5518
6581
  });
5519
6582
  });
5520
- var dataTransferCommand = new Command17("data-transfer").description(
6583
+ var dataTransferCommand = new Command18("data-transfer").description(
5521
6584
  "Whole-instance export / import (owner-only)"
5522
6585
  );
5523
6586
  dataTransferCommand.command("export").description("Export the entire instance to a JSON file").option("--passphrase <passphrase>", "Seal secrets under this passphrase").option("--out <file>", "Write the export to this file instead of stdout").action(async (opts) => {
@@ -5533,7 +6596,7 @@ dataTransferCommand.command("export").description("Export the entire instance to
5533
6596
  );
5534
6597
  spin4?.succeed("Export ready.");
5535
6598
  if (opts.out) {
5536
- writeFileSync6(opts.out, JSON.stringify(file));
6599
+ writeFileSync8(opts.out, JSON.stringify(file));
5537
6600
  const tables = Object.keys(file.dump?.tables ?? {}).length;
5538
6601
  report2(
5539
6602
  { out: opts.out, tables },
@@ -5559,7 +6622,7 @@ dataTransferCommand.command("import").description("Import an instance export fil
5559
6622
  }
5560
6623
  let file;
5561
6624
  try {
5562
- file = JSON.parse(readFileSync5(opts.file, "utf8"));
6625
+ file = JSON.parse(readFileSync9(opts.file, "utf8"));
5563
6626
  } catch {
5564
6627
  err(`
5565
6628
  Could not read or parse ${opts.file}.
@@ -5587,10 +6650,10 @@ dataTransferCommand.command("import").description("Import an instance export fil
5587
6650
  }
5588
6651
  });
5589
6652
  });
5590
- var systemCommand = new Command17("system").description("Instance settings, onboarding, migration, and data transfer").addCommand(settingsCommand).addCommand(onboardingCommand).addCommand(upgradeToAuthCommand).addCommand(browseCommand).addCommand(migrationCommand).addCommand(dataTransferCommand);
6653
+ var systemCommand = new Command18("system").description("Instance settings, onboarding, migration, and data transfer").addCommand(settingsCommand).addCommand(onboardingCommand).addCommand(upgradeToAuthCommand).addCommand(browseCommand).addCommand(migrationCommand).addCommand(dataTransferCommand);
5591
6654
 
5592
6655
  // src/commands/mail.ts
5593
- import { Command as Command18 } from "commander";
6656
+ import { Command as Command19 } from "commander";
5594
6657
  import chalk13 from "chalk";
5595
6658
  import ora6 from "ora";
5596
6659
  import { createInterface as createInterface7 } from "readline/promises";
@@ -5632,7 +6695,7 @@ function printRecordsObject(records) {
5632
6695
  if (rows.length === 0) return info(" (no DNS records)");
5633
6696
  printTable(rows, ["key", "type", "host", "value"]);
5634
6697
  }
5635
- var stepsCmd = new Command18("steps").description("List the mail setup steps").action(
6698
+ var stepsCmd = new Command19("steps").description("List the mail setup steps").action(
5636
6699
  guard2(async () => {
5637
6700
  const res = await apiRequest(
5638
6701
  "/mail/steps"
@@ -5645,7 +6708,7 @@ var stepsCmd = new Command18("steps").description("List the mail setup steps").a
5645
6708
  info(` ${res.total} steps total.`);
5646
6709
  })
5647
6710
  );
5648
- var statusCmd = new Command18("status").description("Show the setup progress for a mail server").argument("[serverId]", "Mail server ID (omit for the empty welcome shell)").action(
6711
+ var statusCmd = new Command19("status").description("Show the setup progress for a mail server").argument("[serverId]", "Mail server ID (omit for the empty welcome shell)").action(
5649
6712
  guard2(async (serverId) => {
5650
6713
  const q = serverId ? `?serverId=${encodeURIComponent(serverId)}` : "";
5651
6714
  const res = await apiRequest(`/mail/status${q}`);
@@ -5666,7 +6729,7 @@ var statusCmd = new Command18("status").description("Show the setup progress for
5666
6729
  }
5667
6730
  })
5668
6731
  );
5669
- var serversCmd = new Command18("servers").description("List every server the mail stack is installed on").action(
6732
+ var serversCmd = new Command19("servers").description("List every server the mail stack is installed on").action(
5670
6733
  guard2(async () => {
5671
6734
  const res = await apiRequest(
5672
6735
  "/mail/servers"
@@ -5687,7 +6750,7 @@ var serversCmd = new Command18("servers").description("List every server the mai
5687
6750
  );
5688
6751
  })
5689
6752
  );
5690
- var scanCmd = new Command18("scan").description("Probe a server for an existing mail install (read-only)").argument("<serverId>", "Server ID to scan").action(
6753
+ var scanCmd = new Command19("scan").description("Probe a server for an existing mail install (read-only)").argument("<serverId>", "Server ID to scan").action(
5691
6754
  guard2(async (serverId) => {
5692
6755
  const sp = spin2("Scanning server\u2026");
5693
6756
  const res = await apiRequest("/mail/scan", { method: "POST", body: JSON.stringify({ serverId }) });
@@ -5702,7 +6765,7 @@ var scanCmd = new Command18("scan").description("Probe a server for an existing
5702
6765
  else info(" Nothing to adopt on this server.");
5703
6766
  })
5704
6767
  );
5705
- var adoptCmd = new Command18("adopt").description("Re-adopt an existing mail install whose orchestrator state was lost").argument("<serverId>", "Server ID to adopt").action(
6768
+ var adoptCmd = new Command19("adopt").description("Re-adopt an existing mail install whose orchestrator state was lost").argument("<serverId>", "Server ID to adopt").action(
5706
6769
  guard2(async (serverId) => {
5707
6770
  const sp = spin2("Adopting mail server\u2026");
5708
6771
  const res = await apiRequest(
@@ -5715,7 +6778,7 @@ var adoptCmd = new Command18("adopt").description("Re-adopt an existing mail ins
5715
6778
  info(` completed: ${res.completed ? "yes" : "no"}`);
5716
6779
  })
5717
6780
  );
5718
- var setupCmd = new Command18("setup").description("Start or resume the mail setup wizard (streams over SSE)").argument("<serverId>", "Server ID to install the mail stack on").requiredOption("-d, --domain <domain>", "Mail domain (e.g. example.com)").option("--start-step <n>", "Resume from a specific step (1-13)").option("--config <json>", "iRedMail config overrides as a JSON object").action(
6781
+ var setupCmd = new Command19("setup").description("Start or resume the mail setup wizard (streams over SSE)").argument("<serverId>", "Server ID to install the mail stack on").requiredOption("-d, --domain <domain>", "Mail domain (e.g. example.com)").option("--start-step <n>", "Resume from a specific step (1-13)").option("--config <json>", "iRedMail config overrides as a JSON object").action(
5719
6782
  guard2(async (serverId, opts) => {
5720
6783
  let config;
5721
6784
  if (opts.config) {
@@ -5784,7 +6847,7 @@ var setupCmd = new Command18("setup").description("Start or resume the mail setu
5784
6847
  if (failed) process.exit(1);
5785
6848
  })
5786
6849
  );
5787
- var cancelCmd = new Command18("cancel").description("Cancel the mail setup currently running").action(
6850
+ var cancelCmd = new Command19("cancel").description("Cancel the mail setup currently running").action(
5788
6851
  guard2(async () => {
5789
6852
  const res = await apiRequest("/mail/setup/cancel", {
5790
6853
  method: "POST"
@@ -5794,7 +6857,7 @@ var cancelCmd = new Command18("cancel").description("Cancel the mail setup curre
5794
6857
  })
5795
6858
  );
5796
6859
  function ackCommand(name, path2, description, successMsg) {
5797
- return new Command18(name).description(description).argument("<serverId>", "Mail server ID").action(
6860
+ return new Command19(name).description(description).argument("<serverId>", "Mail server ID").action(
5798
6861
  guard2(async (serverId) => {
5799
6862
  const res = await apiRequest(path2, {
5800
6863
  method: "POST",
@@ -5817,7 +6880,7 @@ var ptrAckCmd = ackCommand(
5817
6880
  "Acknowledge that reverse DNS (PTR) is configured",
5818
6881
  "PTR acknowledged. Re-run `mail setup` with --start-step to continue."
5819
6882
  );
5820
- var resetCmd = new Command18("reset").description("Wipe the on-server setup state file (does NOT touch installed daemons)").argument("<serverId>", "Mail server ID").option("-y, --yes", "Skip the confirmation prompt").action(
6883
+ var resetCmd = new Command19("reset").description("Wipe the on-server setup state file (does NOT touch installed daemons)").argument("<serverId>", "Mail server ID").option("-y, --yes", "Skip the confirmation prompt").action(
5821
6884
  guard2(async (serverId, opts) => {
5822
6885
  if (!opts.yes && !isJsonMode()) {
5823
6886
  const rl = createInterface7({ input: input6, output: output6 });
@@ -5833,7 +6896,7 @@ var resetCmd = new Command18("reset").description("Wipe the on-server setup stat
5833
6896
  ok(" Setup state reset.");
5834
6897
  })
5835
6898
  );
5836
- var forgetCmd = new Command18("forget").description("Stop managing a mail server (drops the DB row; leaves the stack + state intact)").argument("<serverId>", "Mail server ID").action(
6899
+ var forgetCmd = new Command19("forget").description("Stop managing a mail server (drops the DB row; leaves the stack + state intact)").argument("<serverId>", "Mail server ID").action(
5837
6900
  guard2(async (serverId) => {
5838
6901
  const res = await apiRequest(`/mail/servers/${encodeURIComponent(serverId)}`, {
5839
6902
  method: "DELETE"
@@ -5842,7 +6905,7 @@ var forgetCmd = new Command18("forget").description("Stop managing a mail server
5842
6905
  ok(` Forgot mail server ${serverId} (re-adopt with \`mail scan\` + \`mail adopt\`).`);
5843
6906
  })
5844
6907
  );
5845
- var healthCmd = new Command18("health").description("Show live status of every mail daemon").argument("<serverId>", "Mail server ID").action(
6908
+ var healthCmd = new Command19("health").description("Show live status of every mail daemon").argument("<serverId>", "Mail server ID").action(
5846
6909
  guard2(async (serverId) => {
5847
6910
  const sp = spin2("Checking mail daemons\u2026");
5848
6911
  const res = await apiRequest(`/mail/health/${encodeURIComponent(serverId)}`);
@@ -5859,7 +6922,7 @@ var healthCmd = new Command18("health").description("Show live status of every m
5859
6922
  );
5860
6923
  })
5861
6924
  );
5862
- var logsCmd3 = new Command18("logs").description("Tail a mail component's journal (snapshot)").argument("<serverId>", "Mail server ID").argument("<component>", "Component key (postfix|dovecot|amavis|clamav|iredapd|postgresql|\u2026)").option("-n, --lines <n>", "Number of lines (max 1000)", "200").action(
6925
+ var logsCmd3 = new Command19("logs").description("Tail a mail component's journal (snapshot)").argument("<serverId>", "Mail server ID").argument("<component>", "Component key (postfix|dovecot|amavis|clamav|iredapd|postgresql|\u2026)").option("-n, --lines <n>", "Number of lines (max 1000)", "200").action(
5863
6926
  guard2(async (serverId, component, opts) => {
5864
6927
  const res = await apiRequest(
5865
6928
  `/mail/admin/${encodeURIComponent(serverId)}/components/${encodeURIComponent(component)}/logs?lines=${encodeURIComponent(opts.lines)}`
@@ -5869,9 +6932,9 @@ var logsCmd3 = new Command18("logs").description("Tail a mail component's journa
5869
6932
  for (const line of res.lines) process.stdout.write(line + "\n");
5870
6933
  })
5871
6934
  );
5872
- var postmasterCmd = new Command18("postmaster").description("Manage the postmaster mailbox");
6935
+ var postmasterCmd = new Command19("postmaster").description("Manage the postmaster mailbox");
5873
6936
  postmasterCmd.addCommand(
5874
- new Command18("set-password").description("Rotate the postmaster password").argument("<serverId>", "Mail server ID").option("--password <password>", "New password (min 12 chars); prompted if omitted").action(
6937
+ new Command19("set-password").description("Rotate the postmaster password").argument("<serverId>", "Mail server ID").option("--password <password>", "New password (min 12 chars); prompted if omitted").action(
5875
6938
  guard2(async (serverId, opts) => {
5876
6939
  let password2 = opts.password;
5877
6940
  if (!password2) {
@@ -5897,12 +6960,12 @@ postmasterCmd.addCommand(
5897
6960
  })
5898
6961
  )
5899
6962
  );
5900
- var mailCommand = new Command18("mail").description("Self-hosted mail server (iRedMail) setup and admin [self-host]").addCommand(stepsCmd).addCommand(statusCmd).addCommand(serversCmd).addCommand(scanCmd).addCommand(adoptCmd).addCommand(setupCmd).addCommand(cancelCmd).addCommand(dnsAckCmd).addCommand(ptrAckCmd).addCommand(resetCmd).addCommand(forgetCmd).addCommand(healthCmd).addCommand(logsCmd3).addCommand(postmasterCmd);
6963
+ var mailCommand = new Command19("mail").description("Self-hosted mail server (iRedMail) setup and admin [self-host]").addCommand(stepsCmd).addCommand(statusCmd).addCommand(serversCmd).addCommand(scanCmd).addCommand(adoptCmd).addCommand(setupCmd).addCommand(cancelCmd).addCommand(dnsAckCmd).addCommand(ptrAckCmd).addCommand(resetCmd).addCommand(forgetCmd).addCommand(healthCmd).addCommand(logsCmd3).addCommand(postmasterCmd);
5901
6964
 
5902
6965
  // src/commands/backup.ts
5903
- import { Command as Command19 } from "commander";
6966
+ import { Command as Command20 } from "commander";
5904
6967
  import ora7 from "ora";
5905
- import { readFileSync as readFileSync6 } from "fs";
6968
+ import { readFileSync as readFileSync10 } from "fs";
5906
6969
  async function guard3(fn) {
5907
6970
  try {
5908
6971
  await fn();
@@ -5985,7 +7048,7 @@ async function followStream(path2, label) {
5985
7048
  spinner3?.stop();
5986
7049
  return status;
5987
7050
  }
5988
- var policyCmd = new Command19("policy").description("Backup policies (schedules) for a project");
7051
+ var policyCmd = new Command20("policy").description("Backup policies (schedules) for a project");
5989
7052
  policyCmd.command("list").description("List backup policies for a project").requiredOption("--project <id>", "Project ID").action(
5990
7053
  (opts) => guard3(async () => {
5991
7054
  const { data } = await apiRequest(
@@ -6055,7 +7118,7 @@ policyCmd.command("run").description("Trigger a policy's backup now").argument("
6055
7118
  }
6056
7119
  })
6057
7120
  );
6058
- var runCmd = new Command19("run").description("Backup runs (executions)");
7121
+ var runCmd = new Command20("run").description("Backup runs (executions)");
6059
7122
  runCmd.command("list").description("List backup runs for a project").requiredOption("--project <id>", "Project ID").option("--service <id>", "Filter to a single service").option("--limit <n>", "Max rows (default 50)").action(
6060
7123
  (opts) => guard3(async () => {
6061
7124
  const qs = new URLSearchParams();
@@ -6131,7 +7194,7 @@ runCmd.command("restore").description("Prepare a restore from a run (stages it;
6131
7194
  }
6132
7195
  })
6133
7196
  );
6134
- var restoreCmd = new Command19("restore").description("Manage staged restores");
7197
+ var restoreCmd = new Command20("restore").description("Manage staged restores");
6135
7198
  restoreCmd.command("apply").description("Apply a staged restore (destructive)").argument("<restoreId>", "Restore ID from `backup run restore`").requiredOption("--token <token>", "Confirmation token from prepare").option("--follow", "Stream the restore to completion").action(
6136
7199
  (restoreId, opts) => guard3(async () => {
6137
7200
  await apiRequest(
@@ -6174,7 +7237,7 @@ restoreCmd.command("get").description("Show one restore (optionally stream it)")
6174
7237
  show(data);
6175
7238
  })
6176
7239
  );
6177
- var destinationCmd = new Command19("destination").description("Backup destinations (storage targets)");
7240
+ var destinationCmd = new Command20("destination").description("Backup destinations (storage targets)");
6178
7241
  destinationCmd.command("list").description("List backup destinations").action(
6179
7242
  () => guard3(async () => {
6180
7243
  const { data } = await apiRequest("/backup-destinations");
@@ -6195,7 +7258,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
6195
7258
  let sftpPrivateKey = opts.sftpPrivateKey;
6196
7259
  if (opts.sftpPrivateKeyFile) {
6197
7260
  try {
6198
- sftpPrivateKey = readFileSync6(opts.sftpPrivateKeyFile, "utf8");
7261
+ sftpPrivateKey = readFileSync10(opts.sftpPrivateKeyFile, "utf8");
6199
7262
  } catch {
6200
7263
  throw new Error(`Cannot read key file: ${opts.sftpPrivateKeyFile}`);
6201
7264
  }
@@ -6246,10 +7309,10 @@ destinationCmd.command("preflight").description("Verify a destination (write + r
6246
7309
  }
6247
7310
  })
6248
7311
  );
6249
- var backupCommand = new Command19("backup").description("Manage backups: policies, runs, restores, destinations").addCommand(policyCmd).addCommand(runCmd).addCommand(restoreCmd).addCommand(destinationCmd);
7312
+ var backupCommand = new Command20("backup").description("Manage backups: policies, runs, restores, destinations").addCommand(policyCmd).addCommand(runCmd).addCommand(restoreCmd).addCommand(destinationCmd);
6250
7313
 
6251
7314
  // src/commands/token.ts
6252
- import { Command as Command20 } from "commander";
7315
+ import { Command as Command21 } from "commander";
6253
7316
  import chalk15 from "chalk";
6254
7317
 
6255
7318
  // src/lib/cmd-helpers.ts
@@ -6277,7 +7340,7 @@ function collectGrant(value, acc) {
6277
7340
  acc.push({ resourceType, resourceId, permissions });
6278
7341
  return acc;
6279
7342
  }
6280
- var listCmd5 = new Command20("list").description("List your personal access tokens").action(async () => {
7343
+ var listCmd5 = new Command21("list").description("List your personal access tokens").action(async () => {
6281
7344
  try {
6282
7345
  const res = await apiRequest("/tokens");
6283
7346
  const rows = res.data ?? [];
@@ -6302,7 +7365,7 @@ var listCmd5 = new Command20("list").description("List your personal access toke
6302
7365
  fail3(e);
6303
7366
  }
6304
7367
  });
6305
- var createCmd3 = new Command20("create").description("Mint a new personal access token (the secret is shown once)").argument("<name>", "Human-readable token name").option("--read-only", "Reject mutation methods (POST/PUT/PATCH/DELETE)", false).option("--expires <days>", "Expire after N days (1\u2013365); omit for non-expiring", (v) => parseInt(v, 10)).option(
7368
+ var createCmd3 = new Command21("create").description("Mint a new personal access token (the secret is shown once)").argument("<name>", "Human-readable token name").option("--read-only", "Reject mutation methods (POST/PUT/PATCH/DELETE)", false).option("--expires <days>", "Expire after N days (1\u2013365); omit for non-expiring", (v) => parseInt(v, 10)).option(
6306
7369
  "--grant <type:id:perms>",
6307
7370
  "Scope the token to a resource (repeatable), e.g. project:abc123:read,write",
6308
7371
  collectGrant,
@@ -6334,7 +7397,7 @@ var createCmd3 = new Command20("create").description("Mint a new personal access
6334
7397
  fail3(e);
6335
7398
  }
6336
7399
  });
6337
- var revokeCmd = new Command20("revoke").description("Revoke one of your tokens").argument("<id>", "Token ID").action(async (id) => {
7400
+ var revokeCmd = new Command21("revoke").description("Revoke one of your tokens").argument("<id>", "Token ID").action(async (id) => {
6338
7401
  const sp = spin3("Revoking token\u2026");
6339
7402
  try {
6340
7403
  await apiRequest(`/tokens/${encodeURIComponent(id)}`, { method: "DELETE" });
@@ -6346,11 +7409,11 @@ var revokeCmd = new Command20("revoke").description("Revoke one of your tokens")
6346
7409
  fail3(e);
6347
7410
  }
6348
7411
  });
6349
- var tokenCommand = new Command20("token").description("Manage personal access tokens").addCommand(listCmd5).addCommand(createCmd3).addCommand(revokeCmd);
7412
+ var tokenCommand = new Command21("token").description("Manage personal access tokens").addCommand(listCmd5).addCommand(createCmd3).addCommand(revokeCmd);
6350
7413
 
6351
7414
  // src/commands/api.ts
6352
- import { Command as Command21 } from "commander";
6353
- var apiCommand = new Command21("api").description("Make an authenticated request to any Openship API route (like `gh api`)").argument("<path>", "Path under /api, e.g. /projects or /deployments/<id>").option("-X, --method <method>", "HTTP method (defaults to GET, or POST when --data is given)").option("-d, --data <json>", "Request body as a JSON string").option("-q, --query <kv...>", "Query parameter key=value (repeatable)").action(async (path2, opts) => {
7415
+ import { Command as Command22 } from "commander";
7416
+ var apiCommand = new Command22("api").description("Make an authenticated request to any Openship API route (like `gh api`)").argument("<path>", "Path under /api, e.g. /projects or /deployments/<id>").option("-X, --method <method>", "HTTP method (defaults to GET, or POST when --data is given)").option("-d, --data <json>", "Request body as a JSON string").option("-q, --query <kv...>", "Query parameter key=value (repeatable)").action(async (path2, opts) => {
6354
7417
  const method = (opts.method || (opts.data ? "POST" : "GET")).toUpperCase();
6355
7418
  let url = path2.startsWith("/") ? path2 : `/${path2}`;
6356
7419
  if (opts.query?.length) {
@@ -6383,12 +7446,73 @@ var apiCommand = new Command21("api").description("Make an authenticated request
6383
7446
  }
6384
7447
  });
6385
7448
 
7449
+ // src/commands/reset-admin.ts
7450
+ import { Command as Command23 } from "commander";
7451
+ import chalk16 from "chalk";
7452
+ import { intro, outro, password as passwordPrompt, isCancel, cancel as cancel2, log } from "@clack/prompts";
7453
+ import { readFileSync as readFileSync11 } from "fs";
7454
+ import { homedir as homedir8 } from "os";
7455
+ import { join as join13 } from "path";
7456
+ function resolvedApiPort() {
7457
+ try {
7458
+ return JSON.parse(readFileSync11(join13(homedir8(), ".openship", "ports.json"), "utf8")).api;
7459
+ } catch {
7460
+ return void 0;
7461
+ }
7462
+ }
7463
+ var resetAdminCommand = new Command23("reset-admin-password").description("Reset the local admin login on THIS machine (no sign-in required)").option("--port <port>", "API port of the running service (default: the resolved port from ~/.openship/ports.json, else 4000)").option("--email <email>", "Also set the admin email").option("--name <name>", "Also set the admin display name").option("--password <password>", "New password (prompted if omitted)").action(async (opts) => {
7464
+ intro(chalk16.cyan("Reset Openship admin password"));
7465
+ let pw = opts.password;
7466
+ if (!pw) {
7467
+ if (!process.stdin.isTTY) {
7468
+ log.error("--password is required in non-interactive mode.");
7469
+ process.exit(1);
7470
+ }
7471
+ const entered = await passwordPrompt({
7472
+ message: "New admin password",
7473
+ validate: (v) => v && v.length >= 8 && v.length <= 128 ? void 0 : "8\u2013128 characters"
7474
+ });
7475
+ if (isCancel(entered)) {
7476
+ cancel2("Cancelled.");
7477
+ process.exit(0);
7478
+ }
7479
+ const confirm3 = await passwordPrompt({
7480
+ message: "Confirm password",
7481
+ validate: (v) => v === entered ? void 0 : "Passwords don't match"
7482
+ });
7483
+ if (isCancel(confirm3)) {
7484
+ cancel2("Cancelled.");
7485
+ process.exit(0);
7486
+ }
7487
+ pw = entered;
7488
+ }
7489
+ const port = String(opts.port || resolvedApiPort() || 4e3);
7490
+ let res;
7491
+ try {
7492
+ res = await fetch(`http://127.0.0.1:${port}/api/system/reset-admin-password`, {
7493
+ method: "POST",
7494
+ headers: { "Content-Type": "application/json", "X-Internal-Token": ensureInternalToken() },
7495
+ body: JSON.stringify({ password: pw, email: opts.email, name: opts.name })
7496
+ });
7497
+ } catch {
7498
+ log.error(`Couldn't reach the Openship API on port ${port}. Is it running? (openship status)`);
7499
+ log.info("If it's listening on another port, pass --port <n>.");
7500
+ process.exit(1);
7501
+ }
7502
+ const data = await res.json().catch(() => ({}));
7503
+ if (!res.ok || !data.ok) {
7504
+ log.error(`Reset failed: ${data.error || res.statusText}`);
7505
+ process.exit(1);
7506
+ }
7507
+ outro(chalk16.green(`Password reset. Log in as ${data.email} with your new password.`));
7508
+ });
7509
+
6386
7510
  // src/commands/install.ts
6387
- import { Command as Command22 } from "commander";
6388
- import { chmodSync as chmodSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync7 } from "fs";
6389
- import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
6390
- import { homedir as homedir5 } from "os";
6391
- import { join as join9 } from "path";
7511
+ import { Command as Command24 } from "commander";
7512
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
7513
+ import { spawn as spawn3, spawnSync as spawnSync5 } from "child_process";
7514
+ import { homedir as homedir9 } from "os";
7515
+ import { join as join14 } from "path";
6392
7516
  import ora9 from "ora";
6393
7517
  function assetForPlatform() {
6394
7518
  const { platform, arch } = process;
@@ -6400,33 +7524,33 @@ function assetForPlatform() {
6400
7524
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
6401
7525
  }
6402
7526
  function installDmg(dmg) {
6403
- const homeApps = join9(homedir5(), "Applications");
7527
+ const homeApps = join14(homedir9(), "Applications");
6404
7528
  let dest = homeApps;
6405
7529
  try {
6406
- mkdirSync7(homeApps, { recursive: true });
7530
+ mkdirSync9(homeApps, { recursive: true });
6407
7531
  } catch {
6408
7532
  dest = "/Applications";
6409
7533
  }
6410
- const attach = spawnSync4("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
7534
+ const attach = spawnSync5("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
6411
7535
  encoding: "utf8"
6412
7536
  });
6413
7537
  if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
6414
7538
  const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
6415
7539
  if (!mount) throw new Error("Could not determine the mounted volume");
6416
- let target = join9(dest, "Openship.app");
7540
+ let target = join14(dest, "Openship.app");
6417
7541
  try {
6418
- const appInDmg = join9(mount, "Openship.app");
6419
- if (!existsSync9(appInDmg)) throw new Error("Openship.app not found in the disk image");
6420
- spawnSync4("rm", ["-rf", target]);
6421
- let copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
7542
+ const appInDmg = join14(mount, "Openship.app");
7543
+ if (!existsSync12(appInDmg)) throw new Error("Openship.app not found in the disk image");
7544
+ spawnSync5("rm", ["-rf", target]);
7545
+ let copy = spawnSync5("ditto", [appInDmg, target], { encoding: "utf8" });
6422
7546
  if (copy.status !== 0 && dest === homeApps) {
6423
- target = join9("/Applications", "Openship.app");
6424
- spawnSync4("rm", ["-rf", target]);
6425
- copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
7547
+ target = join14("/Applications", "Openship.app");
7548
+ spawnSync5("rm", ["-rf", target]);
7549
+ copy = spawnSync5("ditto", [appInDmg, target], { encoding: "utf8" });
6426
7550
  }
6427
7551
  if (copy.status !== 0) throw new Error(`ditto copy failed: ${copy.stderr?.trim()}`);
6428
7552
  } finally {
6429
- spawnSync4("hdiutil", ["detach", mount, "-quiet"]);
7553
+ spawnSync5("hdiutil", ["detach", mount, "-quiet"]);
6430
7554
  }
6431
7555
  return target;
6432
7556
  }
@@ -6435,10 +7559,10 @@ function installAppImage(appImage) {
6435
7559
  return appImage;
6436
7560
  }
6437
7561
  function installZip(zip) {
6438
- const localAppData = process.env.LOCALAPPDATA || join9(homedir5(), "AppData", "Local");
6439
- const target = join9(localAppData, "Programs", "Openship");
6440
- mkdirSync7(target, { recursive: true });
6441
- const expand = spawnSync4(
7562
+ const localAppData = process.env.LOCALAPPDATA || join14(homedir9(), "AppData", "Local");
7563
+ const target = join14(localAppData, "Programs", "Openship");
7564
+ mkdirSync9(target, { recursive: true });
7565
+ const expand = spawnSync5(
6442
7566
  "powershell",
6443
7567
  [
6444
7568
  "-NoProfile",
@@ -6453,22 +7577,22 @@ function installZip(zip) {
6453
7577
  }
6454
7578
  function launch(kind, target) {
6455
7579
  if (kind === "dmg") {
6456
- spawnSync4("open", [target]);
7580
+ spawnSync5("open", [target]);
6457
7581
  return;
6458
7582
  }
6459
7583
  if (kind === "appimage") {
6460
- const child = spawn2(target, [], { detached: true, stdio: "ignore" });
7584
+ const child = spawn3(target, [], { detached: true, stdio: "ignore" });
6461
7585
  child.on("error", () => {
6462
- spawn2(target, ["--appimage-extract-and-run"], { detached: true, stdio: "ignore" }).unref();
7586
+ spawn3(target, ["--appimage-extract-and-run"], { detached: true, stdio: "ignore" }).unref();
6463
7587
  });
6464
7588
  child.unref();
6465
7589
  return;
6466
7590
  }
6467
- const exe = join9(target, "Openship.exe");
6468
- const path2 = existsSync9(exe) ? exe : target;
6469
- spawnSync4("cmd", ["/c", "start", "", path2]);
7591
+ const exe = join14(target, "Openship.exe");
7592
+ const path2 = existsSync12(exe) ? exe : target;
7593
+ spawnSync5("cmd", ["/c", "start", "", path2]);
6470
7594
  }
6471
- var installCommand = new Command22("install").description("Download and install the Openship desktop app for this OS").option("--version <tag>", "Release tag to install (e.g. v1.2.3)").option("--latest", "Install the latest release (default)").option("--force", "Re-download even if a verified copy is cached").option("--no-verify", "Skip SHA-256 verification (allowed when no sidecar exists)").option("--no-launch", "Install without launching the app").action(async (opts) => {
7595
+ var installCommand = new Command24("install").description("Download and install the Openship desktop app for this OS").option("--version <tag>", "Release tag to install (e.g. v1.2.3)").option("--latest", "Install the latest release (default)").option("--force", "Re-download even if a verified copy is cached").option("--no-verify", "Skip SHA-256 verification (allowed when no sidecar exists)").option("--no-launch", "Install without launching the app").action(async (opts) => {
6472
7596
  let asset;
6473
7597
  try {
6474
7598
  asset = assetForPlatform();
@@ -6491,13 +7615,13 @@ var installCommand = new Command22("install").description("Download and install
6491
7615
  process.exit(1);
6492
7616
  }
6493
7617
  const dir = releaseDir(tag);
6494
- const assetPath = join9(dir, asset.name);
7618
+ const assetPath = join14(dir, asset.name);
6495
7619
  const sidecarPath = `${assetPath}.sha256`;
6496
7620
  const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
6497
7621
  const sidecarUrl = `${assetUrl2}.sha256`;
6498
7622
  let downloaded = false;
6499
7623
  let sha;
6500
- const cachedUsable = !opts.force && existsSync9(assetPath) && (existsSync9(sidecarPath) || opts.verify === false);
7624
+ const cachedUsable = !opts.force && existsSync12(assetPath) && (existsSync12(sidecarPath) || opts.verify === false);
6501
7625
  if (cachedUsable) {
6502
7626
  info(` Using cached ${asset.name} (${tag}).`);
6503
7627
  } else {
@@ -6519,8 +7643,8 @@ var installCommand = new Command22("install").description("Download and install
6519
7643
  const s2 = spin4("Verifying checksum\u2026");
6520
7644
  try {
6521
7645
  let sidecarBody;
6522
- if (existsSync9(sidecarPath) && !downloaded) {
6523
- sidecarBody = readFileSync7(sidecarPath, "utf8");
7646
+ if (existsSync12(sidecarPath) && !downloaded) {
7647
+ sidecarBody = readFileSync12(sidecarPath, "utf8");
6524
7648
  } else {
6525
7649
  sidecarBody = await fetchSidecar(sidecarUrl);
6526
7650
  }
@@ -6543,8 +7667,8 @@ var installCommand = new Command22("install").description("Download and install
6543
7667
  err(`Expected ${expected}, got ${actual}. The download may be corrupt or tampered with.`);
6544
7668
  process.exit(1);
6545
7669
  }
6546
- mkdirSync7(dir, { recursive: true });
6547
- writeFileSync7(sidecarPath, sidecarBody);
7670
+ mkdirSync9(dir, { recursive: true });
7671
+ writeFileSync9(sidecarPath, sidecarBody);
6548
7672
  s2?.succeed("Checksum verified");
6549
7673
  } catch (e) {
6550
7674
  s2?.fail("Verification failed");
@@ -6583,15 +7707,15 @@ var installCommand = new Command22("install").description("Download and install
6583
7707
  });
6584
7708
 
6585
7709
  // src/commands/update.ts
6586
- import { Command as Command23 } from "commander";
6587
- import { spawnSync as spawnSync5 } from "child_process";
7710
+ import { Command as Command25 } from "commander";
7711
+ import { spawnSync as spawnSync6 } from "child_process";
6588
7712
  function detectPackageManager2(override) {
6589
7713
  if (override === "bun" || override === "npm") return override;
6590
- const hasBun = spawnSync5("bun", ["--version"], { stdio: "ignore" }).status === 0;
7714
+ const hasBun = spawnSync6("bun", ["--version"], { stdio: "ignore" }).status === 0;
6591
7715
  return hasBun ? "bun" : "npm";
6592
7716
  }
6593
- var updateCommand = new Command23("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").action(async (opts) => {
6594
- const current = "0.2.1";
7717
+ var updateCommand = new Command25("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").action(async (opts) => {
7718
+ const current = "0.2.3";
6595
7719
  let latest;
6596
7720
  try {
6597
7721
  latest = (await resolveLatestTag()).replace(/^v/, "");
@@ -6619,7 +7743,7 @@ var updateCommand = new Command23("update").description("Update the Openship CLI
6619
7743
  const ref = `openship@${latest}`;
6620
7744
  const argv = pm === "bun" ? ["add", "-g", ref] : ["install", "-g", ref];
6621
7745
  info(`Updating v${current} \u2192 v${latest} (${cliInstallCommand(pm, latest)})...`);
6622
- const res = spawnSync5(pm, argv, { stdio: "inherit" });
7746
+ const res = spawnSync6(pm, argv, { stdio: "inherit" });
6623
7747
  if (res.status !== 0) {
6624
7748
  err(`Update failed (${pm} exited ${res.status ?? "with a signal"}). Reinstall manually: ${cliInstallCommand(pm, latest)}`);
6625
7749
  process.exitCode = 1;
@@ -6636,18 +7760,18 @@ var updateCommand = new Command23("update").description("Update the Openship CLI
6636
7760
  });
6637
7761
 
6638
7762
  // src/commands/cache.ts
6639
- import { Command as Command24 } from "commander";
6640
- import { existsSync as existsSync10, readdirSync, readFileSync as readFileSync8, rmSync as rmSync4, statSync } from "fs";
6641
- import { join as join10 } from "path";
7763
+ import { Command as Command26 } from "commander";
7764
+ import { existsSync as existsSync13, readdirSync, readFileSync as readFileSync13, rmSync as rmSync4, statSync } from "fs";
7765
+ import { join as join15 } from "path";
6642
7766
  function listAssets() {
6643
- if (!existsSync10(RELEASES_DIR)) return [];
7767
+ if (!existsSync13(RELEASES_DIR)) return [];
6644
7768
  const out = [];
6645
7769
  for (const tag of readdirSync(RELEASES_DIR)) {
6646
7770
  const dir = releaseDir(tag);
6647
7771
  if (!statSync(dir).isDirectory()) continue;
6648
7772
  for (const name of readdirSync(dir)) {
6649
7773
  if (name.endsWith(".sha256")) continue;
6650
- const path2 = join10(dir, name);
7774
+ const path2 = join15(dir, name);
6651
7775
  const st = statSync(path2);
6652
7776
  if (!st.isFile()) continue;
6653
7777
  out.push({
@@ -6655,17 +7779,17 @@ function listAssets() {
6655
7779
  name,
6656
7780
  path: path2,
6657
7781
  size: st.size,
6658
- hasSidecar: existsSync10(`${path2}.sha256`)
7782
+ hasSidecar: existsSync13(`${path2}.sha256`)
6659
7783
  });
6660
7784
  }
6661
7785
  }
6662
7786
  return out;
6663
7787
  }
6664
- var pathCmd = new Command24("path").description("Print the cache directory path").action(() => {
7788
+ var pathCmd = new Command26("path").description("Print the cache directory path").action(() => {
6665
7789
  if (isJsonMode()) printJson({ path: CACHE_DIR });
6666
7790
  else process.stdout.write(CACHE_DIR + "\n");
6667
7791
  });
6668
- var listCmd6 = new Command24("list").alias("ls").description("List cached release assets").action(() => {
7792
+ var listCmd6 = new Command26("list").alias("ls").description("List cached release assets").action(() => {
6669
7793
  const assets = listAssets();
6670
7794
  printTable(
6671
7795
  assets.map((a) => ({
@@ -6677,7 +7801,7 @@ var listCmd6 = new Command24("list").alias("ls").description("List cached releas
6677
7801
  ["tag", "asset", "size", "sidecar"]
6678
7802
  );
6679
7803
  });
6680
- var verifyCmd2 = new Command24("verify").description("Re-hash cached assets and compare to their .sha256 sidecar").argument("[tag]", "Only verify assets under this release tag").action(async (tag) => {
7804
+ var verifyCmd2 = new Command26("verify").description("Re-hash cached assets and compare to their .sha256 sidecar").argument("[tag]", "Only verify assets under this release tag").action(async (tag) => {
6681
7805
  const assets = listAssets().filter((a) => !tag || a.tag === tag);
6682
7806
  const results = [];
6683
7807
  let bad = 0;
@@ -6686,7 +7810,7 @@ var verifyCmd2 = new Command24("verify").description("Re-hash cached assets and
6686
7810
  results.push({ tag: a.tag, asset: a.name, result: "no-sidecar" });
6687
7811
  continue;
6688
7812
  }
6689
- const expected = parseSha256(readFileSync8(`${a.path}.sha256`, "utf8"));
7813
+ const expected = parseSha256(readFileSync13(`${a.path}.sha256`, "utf8"));
6690
7814
  const actual = await hashFile(a.path);
6691
7815
  const okMatch = expected !== null && expected === actual;
6692
7816
  if (!okMatch) bad += 1;
@@ -6700,9 +7824,9 @@ var verifyCmd2 = new Command24("verify").description("Re-hash cached assets and
6700
7824
  }
6701
7825
  if (bad > 0) process.exit(1);
6702
7826
  });
6703
- var cleanCmd = new Command24("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
7827
+ var cleanCmd = new Command26("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
6704
7828
  const target = tag ? releaseDir(tag) : RELEASES_DIR;
6705
- if (!existsSync10(target)) {
7829
+ if (!existsSync13(target)) {
6706
7830
  if (isJsonMode()) printJson({ removed: false, path: target });
6707
7831
  else info(` Nothing to clean (${target}).`);
6708
7832
  return;
@@ -6713,32 +7837,33 @@ var cleanCmd = new Command24("clean").description("Delete cached release assets"
6713
7837
  Removed ${target}
6714
7838
  `);
6715
7839
  });
6716
- var cacheCommand = new Command24("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
7840
+ var cacheCommand = new Command26("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
6717
7841
  err("Specify a subcommand: path | list | verify | clean");
6718
7842
  process.exit(1);
6719
7843
  }).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
6720
7844
 
6721
7845
  // src/commands/wizard.ts
6722
- import chalk16 from "chalk";
7846
+ import chalk17 from "chalk";
6723
7847
  import open from "open";
6724
- import { createServer } from "http";
6725
7848
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
7849
+ import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
7850
+ import { homedir as homedir10 } from "os";
7851
+ import { join as join16 } from "path";
6726
7852
  import {
6727
- intro,
6728
- outro,
7853
+ intro as intro2,
7854
+ outro as outro2,
6729
7855
  text,
6730
7856
  password,
6731
7857
  select,
6732
- confirm as confirm3,
6733
7858
  spinner as spinner2,
6734
7859
  note,
6735
- log,
6736
- cancel as cancel2,
6737
- isCancel
7860
+ log as log2,
7861
+ cancel as cancel3,
7862
+ isCancel as isCancel2
6738
7863
  } from "@clack/prompts";
6739
7864
  function ensure(value) {
6740
- if (isCancel(value)) {
6741
- cancel2("Setup cancelled.");
7865
+ if (isCancel2(value)) {
7866
+ cancel3("Setup cancelled.");
6742
7867
  process.exit(0);
6743
7868
  }
6744
7869
  return value;
@@ -6776,6 +7901,19 @@ async function bootstrapAdmin(apiPort, admin) {
6776
7901
  if (data?.error === "An admin account already exists") return { ok: true, message: "already-exists" };
6777
7902
  return { ok: false, message: data?.error || "failed" };
6778
7903
  }
7904
+ function lastServiceError() {
7905
+ for (const name of ["up.err.log", "up.log"]) {
7906
+ const p = join16(homedir10(), ".openship", "logs", name);
7907
+ if (!existsSync14(p)) continue;
7908
+ try {
7909
+ const lines = readFileSync14(p, "utf8").trim().split("\n");
7910
+ const hit = [...lines].reverse().find((l) => /error|locked|EADDRINUSE|throw|cannot/i.test(l));
7911
+ if (hit) return hit.trim().slice(0, 200);
7912
+ } catch {
7913
+ }
7914
+ }
7915
+ return null;
7916
+ }
6779
7917
  async function waitHealthy(apiPort, seconds = 90) {
6780
7918
  for (let i = 0; i < seconds; i++) {
6781
7919
  await new Promise((r) => setTimeout(r, 1e3));
@@ -6787,6 +7925,20 @@ async function waitHealthy(apiPort, seconds = 90) {
6787
7925
  }
6788
7926
  return false;
6789
7927
  }
7928
+ async function waitDashboard(dashPort, seconds = 45) {
7929
+ for (let i = 0; i < seconds; i++) {
7930
+ await new Promise((r) => setTimeout(r, 1e3));
7931
+ try {
7932
+ const res = await fetch(`http://127.0.0.1:${dashPort}/`, {
7933
+ redirect: "manual",
7934
+ signal: AbortSignal.timeout(2e3)
7935
+ });
7936
+ if (res.status > 0) return true;
7937
+ } catch {
7938
+ }
7939
+ }
7940
+ return false;
7941
+ }
6790
7942
  async function detectPublicIp() {
6791
7943
  for (const url of ["https://api.ipify.org", "https://ifconfig.me/ip"]) {
6792
7944
  try {
@@ -6803,65 +7955,58 @@ var b64url = (buf) => buf.toString("base64").replace(/\+/g, "-").replace(/\//g,
6803
7955
  async function connectOpenshipCloud(port) {
6804
7956
  const already = await internalGet(port, "/api/system/cloud-status");
6805
7957
  if (already?.connected) {
6806
- log.success(`Already connected to Openship Cloud${already.user?.email ? ` as ${already.user.email}` : ""}.`);
6807
- return true;
7958
+ log2.success(`Already connected to Openship Cloud${already.user?.email ? ` as ${already.user.email}` : ""}.`);
7959
+ return { email: already.user?.email ?? null };
6808
7960
  }
6809
7961
  const capsEnv = await internalGet(port, "/api/health/env");
6810
7962
  const cloudApiUrl = capsEnv?.cloudApiUrl;
6811
7963
  if (!cloudApiUrl) {
6812
- log.error("Couldn't discover the Openship Cloud URL \u2014 free domain unavailable. Use a custom domain instead.");
6813
- return false;
7964
+ log2.error("Couldn't discover the Openship Cloud URL \u2014 free domain unavailable. Use a custom domain instead.");
7965
+ return null;
6814
7966
  }
6815
7967
  const verifier = b64url(randomBytes2(32));
6816
7968
  const challenge = b64url(createHash2("sha256").update(verifier).digest());
6817
- const state = b64url(randomBytes2(16));
6818
- const codePromise = new Promise((resolve2) => {
6819
- const server2 = createServer((req, res2) => {
6820
- const u = new URL(req.url || "/", "http://127.0.0.1");
6821
- if (!u.pathname.startsWith("/callback")) {
6822
- res2.writeHead(404).end();
6823
- return;
6824
- }
6825
- const code2 = u.searchParams.get("code");
6826
- const gotState = u.searchParams.get("state");
6827
- res2.writeHead(200, { "Content-Type": "text/html" }).end(
6828
- "<html><body style='font:16px system-ui;padding:3rem;text-align:center'><h2>Openship Cloud connected</h2><p>You can close this window and return to your terminal.</p></body></html>"
6829
- );
6830
- server2.close();
6831
- resolve2(code2 && gotState === state ? code2 : null);
6832
- });
6833
- server2.on("error", () => resolve2(null));
6834
- server2.listen(0, "127.0.0.1", () => {
6835
- const cbPort = server2.address().port;
6836
- const redirect = `http://127.0.0.1:${cbPort}/callback`;
6837
- const handoff = `${cloudApiUrl.replace(/\/$/, "")}/api/cloud/connect-handoff?redirect=${encodeURIComponent(redirect)}&state=${state}&code_challenge=${challenge}`;
6838
- note(handoff, "Open this URL to authorize (opening your browser\u2026)");
6839
- void open(handoff).catch(() => {
6840
- });
6841
- });
6842
- setTimeout(() => {
6843
- try {
6844
- server2.close();
6845
- } catch {
6846
- }
6847
- resolve2(null);
6848
- }, 3e5);
7969
+ const state = b64url(randomBytes2(24));
7970
+ const apiBase = cloudApiUrl.replace(/\/$/, "");
7971
+ const handoff = `${apiBase}/api/cloud/connect-handoff?redirect=${encodeURIComponent(apiBase)}&state=${encodeURIComponent(state)}&code_challenge=${challenge}&mode=device`;
7972
+ const overSsh = !!(process.env.SSH_CONNECTION || process.env.SSH_TTY || process.env.SSH_CLIENT);
7973
+ note(handoff, "Open this URL in your browser to authorize (then click Authorize)");
7974
+ if (!overSsh) void open(handoff).catch(() => {
6849
7975
  });
6850
7976
  const s = spinner2();
6851
- s.start("Waiting for Openship Cloud authorization in your browser");
6852
- const code = await codePromise;
7977
+ s.start("Waiting for you to authorize in the browser");
7978
+ let code = null;
7979
+ const deadline = Date.now() + 3e5;
7980
+ while (Date.now() < deadline) {
7981
+ await new Promise((r) => setTimeout(r, 2500));
7982
+ try {
7983
+ const res2 = await fetch(
7984
+ `${apiBase}/api/cloud/connect-poll?state=${encodeURIComponent(state)}`,
7985
+ { signal: AbortSignal.timeout(5e3) }
7986
+ );
7987
+ if (!res2.ok) continue;
7988
+ const data = await res2.json();
7989
+ if (data.status === "ready" && data.code) {
7990
+ code = data.code;
7991
+ break;
7992
+ }
7993
+ } catch {
7994
+ }
7995
+ }
6853
7996
  if (!code) {
6854
- s.stop("Openship Cloud wasn't authorized.", 1);
6855
- return false;
7997
+ s.stop("Openship Cloud wasn't authorized in time \u2014 re-run the connect step to try again.", 1);
7998
+ return null;
6856
7999
  }
6857
- s.message("Linking this instance to Openship Cloud");
8000
+ s.stop("Authorized.");
8001
+ const linking = spinner2();
8002
+ linking.start("Linking this instance to Openship Cloud");
6858
8003
  const res = await internalPost(port, "/api/system/cloud-connect", { code, codeVerifier: verifier });
6859
8004
  if (!res.ok) {
6860
- s.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
6861
- return false;
8005
+ linking.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
8006
+ return null;
6862
8007
  }
6863
- s.stop("Connected to Openship Cloud.");
6864
- return true;
8008
+ linking.stop(`Connected to Openship Cloud${res.data?.email ? ` as ${res.data.email}` : ""}.`);
8009
+ return { email: res.data?.email ?? null };
6865
8010
  }
6866
8011
  async function promptLocalAdmin() {
6867
8012
  const name = ensure(await text({ message: "Your name", validate: (v) => v?.trim() ? void 0 : "Required" })).trim();
@@ -6922,161 +8067,315 @@ async function streamProvision(port, sessionId, s) {
6922
8067
  return ok3;
6923
8068
  }
6924
8069
  async function runWizard() {
6925
- intro(`${chalk16.bgCyan(chalk16.black(" Openship "))}${chalk16.dim(" setup")}`);
6926
- log.message(
6927
- chalk16.dim(
8070
+ intro2(`${chalk17.bgCyan(chalk17.black(" Openship "))}${chalk17.dim(" setup")}`);
8071
+ log2.message(
8072
+ chalk17.dim(
6928
8073
  "Deploy Openship on this machine \u2014 a few questions, then it installs itself\nas a service, registers as an app, and prints the URL to log in."
6929
8074
  )
6930
8075
  );
8076
+ log2.message(chalk17.dim("First, your instance login (email + password) \u2014 this is how you sign in. Domain and Openship Cloud come next and never replace it."));
8077
+ const admin = await promptLocalAdmin();
8078
+ let cloudEmail = null;
6931
8079
  let publicUrl;
6932
8080
  let behindProxy = false;
6933
8081
  let managedEdge = false;
6934
8082
  let domainPlan = { type: "none" };
6935
- const reach = ensure(
6936
- await select({
6937
- message: "How should this instance be reachable?",
6938
- initialValue: "private",
6939
- options: [
6940
- { value: "private", label: "Private", hint: "this machine only (localhost)" },
6941
- { value: "public", label: "Public", hint: "a server / VPS, reachable from other machines" }
6942
- ]
6943
- })
6944
- );
6945
- if (reach === "public") {
6946
- const canManage = process.platform === "linux";
6947
- const domainType = ensure(
6948
- await select({
6949
- message: "How do you want a domain + HTTPS?",
6950
- initialValue: "free",
6951
- options: [
6952
- { value: "free", label: "Free domain", hint: "name.opsh.io via Openship Cloud \u2014 HTTPS handled for you" },
6953
- ...canManage ? [{ value: "custom", label: "Custom domain", hint: "your domain + free Let's Encrypt on this box" }] : [],
6954
- { value: "byo", label: "Bring your own", hint: "your domain, behind your own reverse proxy" }
6955
- ]
8083
+ const canManage = process.platform === "linux";
8084
+ const BACK = "__back__";
8085
+ let slug = "";
8086
+ let customDomainInput = "";
8087
+ let byoDomainInput = "";
8088
+ let publicHost = null;
8089
+ async function resolvePublicHost() {
8090
+ if (publicHost) return publicHost;
8091
+ const sp = spinner2();
8092
+ sp.start("Detecting this server's public IP");
8093
+ const detected = await detectPublicIp();
8094
+ if (detected) {
8095
+ sp.stop(`Public IP: ${chalk17.bold(detected)}`);
8096
+ publicHost = detected;
8097
+ return detected;
8098
+ }
8099
+ sp.stop("Couldn't detect the public IP automatically.", 1);
8100
+ publicHost = ensure(
8101
+ await text({
8102
+ message: "This server's public IP or hostname",
8103
+ placeholder: "203.0.113.10",
8104
+ validate: (v) => v?.trim() ? void 0 : "Required \u2014 the edge proxy routes traffic to this address"
6956
8105
  })
6957
- );
6958
- if (domainType === "free") {
6959
- const slug = ensure(
8106
+ ).trim();
8107
+ return publicHost;
8108
+ }
8109
+ let stage = "reach";
8110
+ log2.message(chalk17.dim("These are just starting choices \u2014 domain, Cloud, team, and the rest are all editable later in Settings."));
8111
+ planning: while (true) {
8112
+ if (stage === "reach") {
8113
+ const reach = ensure(
8114
+ await select({
8115
+ message: "How should this instance be reachable?",
8116
+ // Default to public — most people setting up on a server/VPS want a
8117
+ // domain + HTTPS; localhost-only is the deliberate opt-out.
8118
+ initialValue: "public",
8119
+ options: [
8120
+ { value: "public", label: "Public (server / VPS)", hint: "a domain + HTTPS, reachable from anywhere" },
8121
+ { value: "private", label: "This machine only", hint: "localhost \u2014 no domain, log in on this box" }
8122
+ ]
8123
+ })
8124
+ );
8125
+ if (reach === "private") {
8126
+ domainPlan = { type: "none" };
8127
+ publicUrl = void 0;
8128
+ behindProxy = false;
8129
+ managedEdge = false;
8130
+ break planning;
8131
+ }
8132
+ stage = "type";
8133
+ continue;
8134
+ }
8135
+ if (stage === "type") {
8136
+ const domainType = ensure(
8137
+ await select({
8138
+ message: "How do you want a domain + HTTPS?",
8139
+ initialValue: "free",
8140
+ options: [
8141
+ { value: "free", label: "Free domain", hint: "name.opsh.io via Openship Cloud \u2014 HTTPS handled for you" },
8142
+ ...canManage ? [{ value: "custom", label: "Custom domain", hint: "your domain + free Let's Encrypt on this box" }] : [],
8143
+ { value: "byo", label: "Bring your own", hint: "your domain, behind your own reverse proxy" },
8144
+ { value: BACK, label: "\u2190 Back" }
8145
+ ]
8146
+ })
8147
+ );
8148
+ if (domainType === BACK) {
8149
+ stage = "reach";
8150
+ continue;
8151
+ }
8152
+ stage = domainType;
8153
+ continue;
8154
+ }
8155
+ if (stage === "free") {
8156
+ slug = ensure(
6960
8157
  await text({
6961
8158
  message: "Choose your subdomain",
6962
8159
  placeholder: "my-openship",
8160
+ initialValue: slug || void 0,
6963
8161
  validate: (v) => v && SLUG_RE.test(v.trim().toLowerCase()) ? void 0 : "Lowercase letters, digits, hyphens"
6964
8162
  })
6965
8163
  ).trim().toLowerCase();
6966
- const s2 = spinner2();
6967
- s2.start("Detecting this server's public IP");
6968
- const publicHost = await detectPublicIp();
6969
- s2.stop(publicHost ? `Public IP: ${chalk16.bold(publicHost)}` : "Couldn't detect the public IP automatically.");
8164
+ const host = await resolvePublicHost();
8165
+ note(
8166
+ `${chalk17.cyan(`https://${slug}.opsh.io`)}
8167
+
8168
+ ${chalk17.dim("served via")} Openship Cloud edge ${chalk17.dim("\u2192")} ${chalk17.cyan(host)}
8169
+
8170
+ ` + chalk17.dim("Openship Cloud terminates HTTPS and forwards to this server."),
8171
+ "Confirm free domain"
8172
+ );
8173
+ const go2 = ensure(
8174
+ await select({
8175
+ message: "Create this free domain?",
8176
+ options: [
8177
+ { value: "go", label: "Create it" },
8178
+ { value: BACK, label: "\u2190 Back", hint: "change subdomain or IP" }
8179
+ ]
8180
+ })
8181
+ );
8182
+ if (go2 === BACK) {
8183
+ stage = "type";
8184
+ continue;
8185
+ }
6970
8186
  publicUrl = `https://${slug}.opsh.io`;
6971
8187
  behindProxy = true;
6972
- domainPlan = { type: "free", slug, publicHost };
6973
- } else if (domainType === "custom") {
6974
- const raw = ensure(
8188
+ domainPlan = { type: "free", slug, publicHost: host };
8189
+ break planning;
8190
+ }
8191
+ if (stage === "custom") {
8192
+ const raw2 = ensure(
6975
8193
  await text({
6976
8194
  message: "Your domain",
6977
8195
  placeholder: "ops.example.com",
8196
+ initialValue: customDomainInput || void 0,
6978
8197
  validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
6979
8198
  })
6980
8199
  );
6981
- publicUrl = normalizeUrl(raw).replace(/^http:/i, "https:");
6982
- const hostname = new URL(publicUrl).hostname;
6983
- managedEdge = true;
6984
- behindProxy = true;
8200
+ customDomainInput = raw2;
8201
+ const url2 = normalizeUrl(raw2).replace(/^http:/i, "https:");
8202
+ const hostname2 = new URL(url2).hostname;
6985
8203
  if (typeof process.getuid === "function" && process.getuid() !== 0) {
6986
- log.warn("Managed HTTPS installs OpenResty + certbot \u2014 that needs root. Re-run with sudo if it can't install.");
8204
+ log2.warn("Managed HTTPS installs OpenResty + certbot \u2014 that needs root. Re-run with sudo if it can't install.");
6987
8205
  }
6988
- const s2 = spinner2();
6989
- s2.start("Detecting this server's public IP");
6990
- const ip = await detectPublicIp();
6991
- s2.stop(ip ? `Public IP: ${chalk16.bold(ip)}` : "Couldn't detect the public IP automatically.");
8206
+ const host = await resolvePublicHost();
6992
8207
  note(
6993
- `Add a DNS ${chalk16.bold("A record")}:
8208
+ `Add a DNS ${chalk17.bold("A record")}:
6994
8209
 
6995
- ${chalk16.cyan(hostname)} \u2192 ${chalk16.cyan(ip ?? "<this server's public IP>")}
8210
+ ${chalk17.cyan(hostname2)} \u2192 ${chalk17.cyan(host)}
6996
8211
 
6997
- ` + chalk16.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
8212
+ ` + chalk17.dim("HTTPS is issued automatically once DNS resolves (it retries for a couple minutes)."),
6998
8213
  "DNS"
6999
8214
  );
7000
- ensure(await confirm3({ message: "A record set? (continue either way \u2014 it retries)", initialValue: true }));
7001
- domainPlan = { type: "custom", hostname };
7002
- } else {
7003
- const raw = ensure(
7004
- await text({
7005
- message: "Your domain (served behind your proxy)",
7006
- placeholder: "ops.example.com",
7007
- validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
8215
+ const go2 = ensure(
8216
+ await select({
8217
+ message: "A record added?",
8218
+ options: [
8219
+ { value: "go", label: "Continue", hint: "HTTPS provisions once DNS resolves \u2014 it retries" },
8220
+ { value: BACK, label: "\u2190 Back", hint: "change the domain" }
8221
+ ]
7008
8222
  })
7009
8223
  );
7010
- publicUrl = normalizeUrl(raw);
7011
- behindProxy = true;
7012
- if (publicUrl.startsWith("http://")) {
7013
- log.warn("Serving over plain HTTP sends passwords in cleartext \u2014 put HTTPS in front before real use.");
8224
+ if (go2 === BACK) {
8225
+ stage = "type";
8226
+ continue;
7014
8227
  }
7015
- domainPlan = { type: "byo", hostname: new URL(publicUrl).hostname };
8228
+ publicUrl = url2;
8229
+ managedEdge = true;
8230
+ behindProxy = true;
8231
+ domainPlan = { type: "custom", hostname: hostname2 };
8232
+ break planning;
8233
+ }
8234
+ const raw = ensure(
8235
+ await text({
8236
+ message: "Your domain (served behind your proxy)",
8237
+ placeholder: "ops.example.com",
8238
+ initialValue: byoDomainInput || void 0,
8239
+ validate: (v) => v && normalizeUrl(v) ? void 0 : "Enter a valid domain"
8240
+ })
8241
+ );
8242
+ byoDomainInput = raw;
8243
+ const url = normalizeUrl(raw);
8244
+ const hostname = new URL(url).hostname;
8245
+ if (url.startsWith("http://")) {
8246
+ log2.warn("Serving over plain HTTP sends passwords in cleartext \u2014 put HTTPS in front before real use.");
8247
+ }
8248
+ note(
8249
+ `${chalk17.cyan(url)}
8250
+
8251
+ ` + chalk17.dim("Point your reverse proxy at the dashboard port shown at the end."),
8252
+ "Confirm"
8253
+ );
8254
+ const go = ensure(
8255
+ await select({
8256
+ message: "Continue?",
8257
+ options: [
8258
+ { value: "go", label: "Continue" },
8259
+ { value: BACK, label: "\u2190 Back", hint: "change the domain" }
8260
+ ]
8261
+ })
8262
+ );
8263
+ if (go === BACK) {
8264
+ stage = "type";
8265
+ continue;
7016
8266
  }
8267
+ publicUrl = url;
8268
+ behindProxy = true;
8269
+ domainPlan = { type: "byo", hostname };
8270
+ break planning;
8271
+ }
8272
+ const uiTag = `v${"0.2.3"}`;
8273
+ const dl = spinner2();
8274
+ dl.start("Pulling the Openship dist from GitHub");
8275
+ try {
8276
+ await ensureDashboard({
8277
+ tag: uiTag,
8278
+ onProgress: (received, total) => {
8279
+ if (total) dl.message(`Pulling the Openship dist from GitHub \u2014 ${Math.round(received / total * 100)}%`);
8280
+ }
8281
+ });
8282
+ dl.stop("Openship dist ready.");
8283
+ } catch (e) {
8284
+ dl.stop(`Couldn't pull the Openship dist: ${e.message}`, 1);
8285
+ log2.info("Check your network / that this release published its dashboard asset, then re-run `openship`.");
8286
+ process.exit(1);
7017
8287
  }
7018
- const isCloudDomain = domainPlan.type === "free";
7019
- const admin = isCloudDomain ? null : await promptLocalAdmin();
7020
8288
  const s = spinner2();
7021
8289
  s.start("Installing Openship as a service");
7022
8290
  let started;
7023
8291
  try {
7024
- started = startService(
7025
- { publicUrl, trustProxy: behindProxy, managedEdge, acmeEmail: managedEdge ? admin?.email : void 0 },
8292
+ started = await startService(
8293
+ { publicUrl, trustProxy: behindProxy, managedEdge, acmeEmail: managedEdge ? admin.email : void 0, uiVersion: uiTag },
7026
8294
  { quiet: true }
7027
8295
  );
7028
8296
  } catch (e) {
7029
8297
  s.stop("Couldn't install the service.", 1);
7030
- log.error(e.message);
7031
- log.info("Run `openship up --foreground` to run it attached and see the error.");
8298
+ log2.error(e.message);
8299
+ log2.info("Run `openship up --foreground` to run it attached and see the error.");
7032
8300
  process.exit(1);
7033
8301
  }
7034
- s.message("Waiting for Openship to come up");
8302
+ s.message("Waiting for the Openship API");
7035
8303
  if (!await waitHealthy(started.port)) {
7036
8304
  s.stop("Openship didn't become healthy in time.", 1);
7037
- log.info("Check logs: `openship logs` (or `openship up --foreground`).");
8305
+ const reason = lastServiceError();
8306
+ if (reason) log2.error(reason);
8307
+ if (reason && /lock/i.test(reason)) {
8308
+ log2.info("The database is locked by another instance \u2014 run `openship stop`, then re-run `openship`.");
8309
+ } else {
8310
+ log2.info("Check logs: `openship logs` (or `openship up --foreground`).");
8311
+ }
8312
+ process.exit(1);
8313
+ }
8314
+ s.message("Creating your admin account");
8315
+ const adminRes = await bootstrapAdmin(started.port, admin);
8316
+ if (!adminRes.ok) {
8317
+ s.stop(`Couldn't create the admin account: ${adminRes.message}`, 1);
7038
8318
  process.exit(1);
7039
8319
  }
7040
- if (admin) {
7041
- s.message("Creating your admin account");
7042
- const adminRes = await bootstrapAdmin(started.port, admin);
7043
- if (!adminRes.ok) {
7044
- s.stop(`Couldn't create the admin account: ${adminRes.message}`, 1);
8320
+ if (adminRes.message === "already-exists") {
8321
+ s.message("Applying your admin login");
8322
+ const rr = await internalPost(started.port, "/api/system/reset-admin-password", {
8323
+ email: admin.email,
8324
+ name: admin.name,
8325
+ password: admin.password
8326
+ });
8327
+ if (!rr.ok) {
8328
+ s.stop(`Couldn't set your admin login: ${rr.data?.error || "failed"}`, 1);
7045
8329
  process.exit(1);
7046
8330
  }
7047
- s.stop(
7048
- adminRes.message === "already-exists" ? "An admin already exists \u2014 use your existing login." : `Admin account created for ${admin.email}.`
7049
- );
7050
- } else {
7051
- s.stop("Openship is up.");
7052
8331
  }
8332
+ s.message(`Admin ready for ${admin.email}`);
8333
+ s.message("Starting the Openship dashboard");
8334
+ await waitDashboard(started.dashPort);
8335
+ s.stop("Deployed.");
7053
8336
  let liveUrl = publicUrl ?? `http://localhost:${started.dashPort}`;
7054
8337
  const port = started.port;
7055
8338
  if (domainPlan.type === "free") {
7056
- const linked = await connectOpenshipCloud(port);
7057
- if (!linked) {
7058
- log.warn("Openship Cloud wasn't connected \u2014 set up a local admin instead. You can add the free domain later in Settings \u2192 Cloud.");
7059
- const fb = await promptLocalAdmin();
7060
- const abr = await bootstrapAdmin(port, fb);
7061
- if (!abr.ok) {
7062
- log.error(`Couldn't create the admin account: ${abr.message}`);
7063
- process.exit(1);
7064
- }
8339
+ const cloud = await connectOpenshipCloud(port);
8340
+ if (!cloud) {
8341
+ log2.warn("Openship Cloud wasn't connected \u2014 skipping the free domain. Your local admin login still works; add the domain later in Settings \u2192 Cloud.");
7065
8342
  await internalPost(port, "/api/system/self-register", { domainType: "byo" });
7066
8343
  } else {
7067
- const s2 = spinner2();
7068
- s2.start("Registering your free domain with Openship Cloud");
7069
- const res = await internalPost(port, "/api/system/self-register", {
7070
- domainType: "free",
7071
- slug: domainPlan.slug,
7072
- publicHost: domainPlan.publicHost,
7073
- dashPort: Number(started.dashPort)
7074
- });
7075
- if (res.ok && res.data?.url) {
7076
- liveUrl = res.data.url;
7077
- s2.stop(`Free domain live: ${res.data.url}`);
7078
- } else {
7079
- s2.stop(`Couldn't register the free domain: ${res.data?.error || "failed"}`, 1);
8344
+ cloudEmail = cloud.email;
8345
+ let regSlug = domainPlan.slug;
8346
+ while (true) {
8347
+ const s2 = spinner2();
8348
+ s2.start(`Registering ${chalk17.bold(`${regSlug}.opsh.io`)} with Openship Cloud`);
8349
+ const res = await internalPost(port, "/api/system/self-register", {
8350
+ domainType: "free",
8351
+ slug: regSlug,
8352
+ publicHost: domainPlan.publicHost,
8353
+ dashPort: Number(started.dashPort)
8354
+ });
8355
+ if (res.ok && res.data?.url) {
8356
+ liveUrl = res.data.url;
8357
+ s2.stop(`Free domain live: ${res.data.url}`);
8358
+ break;
8359
+ }
8360
+ s2.stop(`Couldn't register ${regSlug}.opsh.io: ${res.data?.error || "failed"}`, 1);
8361
+ const next = ensure(
8362
+ await select({
8363
+ message: "Try a different subdomain?",
8364
+ options: [
8365
+ { value: "retry", label: "Pick another subdomain" },
8366
+ { value: "skip", label: "Skip for now", hint: "log in on this server; add a domain later in Settings \u2192 Cloud" }
8367
+ ]
8368
+ })
8369
+ );
8370
+ if (next === "skip") break;
8371
+ regSlug = ensure(
8372
+ await text({
8373
+ message: "Choose your subdomain",
8374
+ placeholder: "my-openship",
8375
+ initialValue: regSlug,
8376
+ validate: (v) => v && SLUG_RE.test(v.trim().toLowerCase()) ? void 0 : "Lowercase letters, digits, hyphens"
8377
+ })
8378
+ ).trim().toLowerCase();
7080
8379
  }
7081
8380
  }
7082
8381
  } else if (domainPlan.type === "custom") {
@@ -7115,7 +8414,7 @@ async function runWizard() {
7115
8414
  else edgeTakeover = true;
7116
8415
  }
7117
8416
  if (!proceedCustom) {
7118
- log.warn(
8417
+ log2.warn(
7119
8418
  "Left the existing proxy on 80/443 running. Registering Openship without managed HTTPS \u2014 front it with your proxy, or re-run setup to take over."
7120
8419
  );
7121
8420
  await internalPost(port, "/api/system/self-register", {
@@ -7140,7 +8439,7 @@ async function runWizard() {
7140
8439
  if (done) s2.stop(`HTTPS ready: ${liveUrl}`);
7141
8440
  else s2.stop("HTTPS isn't ready yet \u2014 it retries on reboot; the site serves over HTTP meanwhile.", 1);
7142
8441
  } else {
7143
- log.warn(`Couldn't start domain provisioning: ${res.data?.error || "failed"}`);
8442
+ log2.warn(`Couldn't start domain provisioning: ${res.data?.error || "failed"}`);
7144
8443
  }
7145
8444
  }
7146
8445
  } else if (domainPlan.type === "byo") {
@@ -7152,23 +8451,105 @@ async function runWizard() {
7152
8451
  } else {
7153
8452
  await internalPost(port, "/api/system/self-register", { domainType: "byo" });
7154
8453
  }
8454
+ saveInstanceUrl(liveUrl);
8455
+ const pad = (label) => chalk17.dim(label.padEnd(11));
8456
+ log2.success(chalk17.bold("Openship is live"));
8457
+ log2.message(
8458
+ `${pad("URL")}${chalk17.bold(liveUrl)}
8459
+ ${pad("Dashboard")}http://localhost:${started.dashPort}
8460
+ ${pad("API")}http://localhost:${started.port}
8461
+ ${pad("Login")}${admin.email} ${chalk17.dim("(email + password you set)")}
8462
+ ` + (cloudEmail ? `${pad("Cloud")}${chalk17.dim("connected as ")}${cloudEmail}${chalk17.dim(" \u2014 free domain + mail only")}
8463
+ ` : "") + `${pad("Status")}${chalk17.green("running")} ${chalk17.dim("\xB7 service (restarts on boot)")}`
8464
+ );
8465
+ log2.message(
8466
+ chalk17.dim("Sign in with the email + password you just set. Openship appears under your Apps.\n") + chalk17.dim("Change the domain, Openship Cloud, team, and everything else anytime in Settings.\n") + chalk17.dim(`Locked out? Run ${chalk17.reset("openship reset-admin-password")}${chalk17.dim(" on this machine \u2014 resets your login without signing in.")}`)
8467
+ );
8468
+ outro2(
8469
+ domainPlan.type === "byo" ? chalk17.dim("Point your reverse proxy at the dashboard port above.") : chalk17.green("Happy shipping.")
8470
+ );
8471
+ }
8472
+ function storedPorts() {
8473
+ const p = join16(homedir10(), ".openship", "ports.json");
8474
+ try {
8475
+ return existsSync14(p) ? JSON.parse(readFileSync14(p, "utf8")) : {};
8476
+ } catch {
8477
+ return {};
8478
+ }
8479
+ }
8480
+ async function runControl() {
8481
+ const svc = serviceStatus();
8482
+ const ports = storedPorts();
8483
+ const apiPort = String(ports.api ?? 4e3);
8484
+ const dashUrl = `http://localhost:${ports.dashboard ?? 3001}`;
8485
+ const publicUrl = readInstanceUrl();
8486
+ const primaryUrl = publicUrl && !/^https?:\/\/localhost/i.test(publicUrl) ? publicUrl : dashUrl;
8487
+ intro2(`${chalk17.bgCyan(chalk17.black(" Openship "))}${chalk17.dim(" control")}`);
7155
8488
  note(
7156
- `${chalk16.bold(liveUrl)}
7157
-
7158
- ` + chalk16.dim(`${admin ? `Log in as ${admin.email}` : "Log in with Openship Cloud"}. Openship now appears under your Apps, and runs as a service (restarts on boot).`),
7159
- "Openship is live"
8489
+ `${chalk17.dim("URL".padEnd(11))}${chalk17.bold(primaryUrl)}
8490
+ ${chalk17.dim("Service".padEnd(11))}${svc.running ? chalk17.green("running") : chalk17.yellow("stopped")}
8491
+ ${chalk17.dim("Dashboard".padEnd(11))}${dashUrl}
8492
+ ` + (ports.api ? `${chalk17.dim("API".padEnd(11))}http://localhost:${ports.api}
8493
+ ` : "") + `${chalk17.dim("Manager".padEnd(11))}${svc.kind === "unsupported" ? "none" : svc.kind}`,
8494
+ "Openship is already set up"
7160
8495
  );
7161
- outro(
7162
- domainPlan.type === "byo" ? chalk16.dim("Point your reverse proxy at the dashboard port above.") : chalk16.green("Happy shipping.")
8496
+ const action2 = ensure(
8497
+ await select({
8498
+ message: "What would you like to do?",
8499
+ options: [
8500
+ { value: "open", label: "Open the dashboard" },
8501
+ svc.running ? { value: "restart", label: "Restart the service" } : { value: "start", label: "Start the service" },
8502
+ { value: "stop", label: "Stop the service", hint: "won't restart on boot" },
8503
+ { value: "reset", label: "Reset admin password", hint: "sets a local email + password login" },
8504
+ { value: "reconfigure", label: "Re-run setup", hint: "reconfigure domain / cloud / admin" },
8505
+ { value: "quit", label: "Quit" }
8506
+ ]
8507
+ })
7163
8508
  );
8509
+ switch (action2) {
8510
+ case "open":
8511
+ await open(primaryUrl).catch(() => {
8512
+ });
8513
+ outro2(chalk17.dim(`Opening ${primaryUrl}`));
8514
+ return;
8515
+ case "start":
8516
+ await startService({});
8517
+ return;
8518
+ case "restart": {
8519
+ const r = restart();
8520
+ outro2(r.restarted ? chalk17.green("Restarted.") : chalk17.yellow(r.detail));
8521
+ return;
8522
+ }
8523
+ case "stop": {
8524
+ const r = stop();
8525
+ outro2(chalk17.green(`Stopped. ${chalk17.dim(r.detail)}`));
8526
+ return;
8527
+ }
8528
+ case "reset": {
8529
+ const pw = ensure(
8530
+ await password({ message: "New admin password", validate: (v) => v && v.length >= 8 ? void 0 : "At least 8 characters" })
8531
+ );
8532
+ const rr = await internalPost(apiPort, "/api/system/reset-admin-password", { password: pw });
8533
+ outro2(
8534
+ rr.ok ? chalk17.green(`Password reset. Sign in at ${dashUrl} with your email + new password.`) : chalk17.red(`Couldn't reset: ${rr.data?.error || "failed"}`)
8535
+ );
8536
+ return;
8537
+ }
8538
+ case "reconfigure":
8539
+ await runWizard();
8540
+ return;
8541
+ default:
8542
+ outro2(chalk17.dim("Nothing changed."));
8543
+ }
7164
8544
  }
7165
8545
 
7166
8546
  // src/index.ts
7167
- var program = new Command25();
7168
- program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.2.1").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
8547
+ var program = new Command27();
8548
+ program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.2.3").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
7169
8549
  if (thisCommand.opts().json) setJsonMode(true);
7170
8550
  }).action(async () => {
7171
- await runWizard();
8551
+ if (serviceStatus().installed) await runControl();
8552
+ else await runWizard();
7172
8553
  });
7173
8554
  program.addCommand(upCommand);
7174
8555
  program.addCommand(stopCommand);
@@ -7178,6 +8559,7 @@ program.addCommand(openCommand);
7178
8559
  program.addCommand(loginCommand);
7179
8560
  program.addCommand(logoutCommand);
7180
8561
  program.addCommand(initCommand);
8562
+ program.addCommand(configCommand);
7181
8563
  program.addCommand(contextCommand);
7182
8564
  program.addCommand(statusCommand);
7183
8565
  program.addCommand(doctorCommand);
@@ -7193,5 +8575,6 @@ program.addCommand(mailCommand);
7193
8575
  program.addCommand(backupCommand);
7194
8576
  program.addCommand(tokenCommand);
7195
8577
  program.addCommand(apiCommand);
8578
+ program.addCommand(resetAdminCommand);
7196
8579
  installCommand.addCommand(cacheCommand);
7197
8580
  program.parse();