openship 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 Command26 } from "commander";
4
+ import { Command as Command27 } from "commander";
5
5
 
6
6
  // src/lib/output.ts
7
7
  import chalk from "chalk";
@@ -1216,7 +1216,13 @@ function findClosingBracket(body, start) {
1216
1216
  const ch = body[i];
1217
1217
  const prev = body[i - 1];
1218
1218
  if (inDouble) {
1219
- if (ch === '"' && prev !== "\\") inDouble = false;
1219
+ if (ch === '"') {
1220
+ let backslashRun = 0;
1221
+ for (let j = i - 1; j >= 0 && body[j] === "\\"; j--) {
1222
+ backslashRun++;
1223
+ }
1224
+ if (backslashRun % 2 === 0) inDouble = false;
1225
+ }
1220
1226
  continue;
1221
1227
  }
1222
1228
  if (inSingle) {
@@ -1807,6 +1813,425 @@ var LANGUAGE_MANIFEST_FILES = Array.from(
1807
1813
  new Set(LANGUAGE_DETECTORS.flatMap((d) => d.manifestFiles.map((f) => f.toLowerCase())))
1808
1814
  );
1809
1815
 
1816
+ // ../../packages/core/src/openship-config/schema.ts
1817
+ var OPENSHIP_RUNTIMES = ["bare", "docker"];
1818
+ var OPENSHIP_PRODUCTION_MODES = [
1819
+ "host",
1820
+ "static",
1821
+ "standalone"
1822
+ ];
1823
+ var OPENSHIP_DOMAIN_TYPES = ["free", "custom"];
1824
+ var OPENSHIP_RESTARTS = [
1825
+ "no",
1826
+ "always",
1827
+ "on-failure",
1828
+ "unless-stopped"
1829
+ ];
1830
+ var OPENSHIP_RESOURCE_TIERS = [
1831
+ "micro",
1832
+ "low",
1833
+ "medium",
1834
+ "high"
1835
+ ];
1836
+
1837
+ // ../../packages/core/src/openship-config/parse.ts
1838
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
1839
+ "$schema",
1840
+ "framework",
1841
+ "packageManager",
1842
+ "rootDirectory",
1843
+ "installCommand",
1844
+ "buildCommand",
1845
+ "startCommand",
1846
+ "outputDirectory",
1847
+ "buildImage",
1848
+ "productionPaths",
1849
+ "runtime",
1850
+ "productionMode",
1851
+ "port",
1852
+ "env",
1853
+ "domains",
1854
+ "routes",
1855
+ "resources",
1856
+ "services",
1857
+ "monorepo"
1858
+ ]);
1859
+ var Ctx = class {
1860
+ errors = [];
1861
+ warnings = [];
1862
+ err(path2, msg) {
1863
+ this.errors.push(`${path2}: ${msg}`);
1864
+ }
1865
+ isObj(v) {
1866
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1867
+ }
1868
+ str(v, path2) {
1869
+ if (v === void 0) return void 0;
1870
+ if (typeof v !== "string") {
1871
+ this.err(path2, "must be a string");
1872
+ return void 0;
1873
+ }
1874
+ return v;
1875
+ }
1876
+ bool(v, path2) {
1877
+ if (v === void 0) return void 0;
1878
+ if (typeof v !== "boolean") {
1879
+ this.err(path2, "must be a boolean");
1880
+ return void 0;
1881
+ }
1882
+ return v;
1883
+ }
1884
+ int(v, path2, min, max) {
1885
+ if (v === void 0) return void 0;
1886
+ const n = typeof v === "string" ? Number(v) : v;
1887
+ if (typeof n !== "number" || !Number.isFinite(n)) {
1888
+ this.err(path2, "must be a number");
1889
+ return void 0;
1890
+ }
1891
+ if (n < min || n > max) {
1892
+ this.err(path2, `must be between ${min} and ${max}`);
1893
+ return void 0;
1894
+ }
1895
+ return n;
1896
+ }
1897
+ strArray(v, path2) {
1898
+ if (v === void 0) return void 0;
1899
+ if (!Array.isArray(v)) {
1900
+ this.err(path2, "must be an array of strings");
1901
+ return void 0;
1902
+ }
1903
+ const out = [];
1904
+ v.forEach((item, i) => {
1905
+ if (typeof item !== "string") this.err(`${path2}[${i}]`, "must be a string");
1906
+ else out.push(item);
1907
+ });
1908
+ return out;
1909
+ }
1910
+ enumOf(v, path2, allowed) {
1911
+ if (v === void 0) return void 0;
1912
+ if (typeof v !== "string" || !allowed.includes(v)) {
1913
+ this.err(path2, `must be one of: ${allowed.join(", ")}`);
1914
+ return void 0;
1915
+ }
1916
+ return v;
1917
+ }
1918
+ };
1919
+ function parseEnv(ctx, v, path2) {
1920
+ if (v === void 0) return void 0;
1921
+ if (!ctx.isObj(v)) {
1922
+ ctx.err(path2, "must be an object of environment variables");
1923
+ return void 0;
1924
+ }
1925
+ const out = {};
1926
+ for (const [key, val] of Object.entries(v)) {
1927
+ if (typeof val === "string") {
1928
+ out[key] = val;
1929
+ } else if (ctx.isObj(val)) {
1930
+ if (val.value === void 0) ctx.err(`${path2}.${key}.value`, "is required");
1931
+ const value = ctx.str(val.value, `${path2}.${key}.value`);
1932
+ const secret = ctx.bool(val.secret, `${path2}.${key}.secret`);
1933
+ if (value !== void 0) out[key] = { value, ...secret !== void 0 ? { secret } : {} };
1934
+ } else {
1935
+ ctx.err(`${path2}.${key}`, 'must be a string or { "value", "secret" }');
1936
+ }
1937
+ }
1938
+ return out;
1939
+ }
1940
+ function parseDomains(ctx, v, path2) {
1941
+ if (v === void 0) return void 0;
1942
+ if (!Array.isArray(v)) {
1943
+ ctx.err(path2, "must be an array of hostnames or domain objects");
1944
+ return void 0;
1945
+ }
1946
+ const out = [];
1947
+ v.forEach((item, i) => {
1948
+ const p = `${path2}[${i}]`;
1949
+ if (typeof item === "string") {
1950
+ out.push({ domain: item });
1951
+ } else if (ctx.isObj(item)) {
1952
+ const domain = ctx.str(item.domain, `${p}.domain`);
1953
+ if (!domain) {
1954
+ ctx.err(p, "requires a `domain`");
1955
+ return;
1956
+ }
1957
+ out.push({
1958
+ domain,
1959
+ port: ctx.int(item.port, `${p}.port`, 1, 65535),
1960
+ targetPath: ctx.str(item.targetPath, `${p}.targetPath`),
1961
+ type: ctx.enumOf(item.type, `${p}.type`, OPENSHIP_DOMAIN_TYPES)
1962
+ });
1963
+ } else {
1964
+ ctx.err(p, "must be a hostname string or a domain object");
1965
+ }
1966
+ });
1967
+ return out;
1968
+ }
1969
+ function parseRoutes(ctx, v, path2) {
1970
+ if (v === void 0) return void 0;
1971
+ if (!ctx.isObj(v)) {
1972
+ ctx.err(path2, "must be an object");
1973
+ return void 0;
1974
+ }
1975
+ const routes = {};
1976
+ const rule = (item, p) => {
1977
+ if (!ctx.isObj(item)) {
1978
+ ctx.err(p, "must be an object with `source` and `destination`");
1979
+ return null;
1980
+ }
1981
+ const source = ctx.str(item.source, `${p}.source`);
1982
+ const destination = ctx.str(item.destination, `${p}.destination`);
1983
+ return source && destination ? { source, destination } : null;
1984
+ };
1985
+ if (Array.isArray(v.rewrites)) {
1986
+ routes.rewrites = v.rewrites.map((r, i) => rule(r, `${path2}.rewrites[${i}]`)).filter(Boolean);
1987
+ } else if (v.rewrites !== void 0) ctx.err(`${path2}.rewrites`, "must be an array");
1988
+ if (Array.isArray(v.redirects)) {
1989
+ routes.redirects = v.redirects.map((r, i) => {
1990
+ const base = rule(r, `${path2}.redirects[${i}]`);
1991
+ if (!base) return null;
1992
+ const o = r;
1993
+ return {
1994
+ ...base,
1995
+ permanent: ctx.bool(o.permanent, `${path2}.redirects[${i}].permanent`),
1996
+ statusCode: ctx.int(o.statusCode, `${path2}.redirects[${i}].statusCode`, 300, 399)
1997
+ };
1998
+ }).filter(Boolean);
1999
+ } else if (v.redirects !== void 0) ctx.err(`${path2}.redirects`, "must be an array");
2000
+ if (Array.isArray(v.headers)) {
2001
+ routes.headers = v.headers.map((h, i) => {
2002
+ const p = `${path2}.headers[${i}]`;
2003
+ if (!ctx.isObj(h)) {
2004
+ ctx.err(p, "must be an object");
2005
+ return null;
2006
+ }
2007
+ const source = ctx.str(h.source, `${p}.source`);
2008
+ const list2 = Array.isArray(h.headers) ? h.headers.map((kv, j) => {
2009
+ const key = ctx.str(kv?.key, `${p}.headers[${j}].key`);
2010
+ const value = ctx.str(kv?.value, `${p}.headers[${j}].value`);
2011
+ return key && value !== void 0 ? { key, value } : null;
2012
+ }).filter(Boolean) : [];
2013
+ return source ? { source, headers: list2 } : null;
2014
+ }).filter(Boolean);
2015
+ } else if (v.headers !== void 0) ctx.err(`${path2}.headers`, "must be an array");
2016
+ const cleanUrls = ctx.bool(v.cleanUrls, `${path2}.cleanUrls`);
2017
+ const trailingSlash = ctx.bool(v.trailingSlash, `${path2}.trailingSlash`);
2018
+ if (cleanUrls !== void 0) routes.cleanUrls = cleanUrls;
2019
+ if (trailingSlash !== void 0) routes.trailingSlash = trailingSlash;
2020
+ return routes;
2021
+ }
2022
+ function parseResources(ctx, v, path2) {
2023
+ if (v === void 0) return void 0;
2024
+ if (!ctx.isObj(v)) {
2025
+ ctx.err(path2, "must be an object");
2026
+ return void 0;
2027
+ }
2028
+ const r = {
2029
+ tier: ctx.enumOf(v.tier, `${path2}.tier`, OPENSHIP_RESOURCE_TIERS),
2030
+ cpuCores: ctx.int(v.cpuCores, `${path2}.cpuCores`, 0.25, 4),
2031
+ memoryMb: ctx.int(v.memoryMb, `${path2}.memoryMb`, 128, 8192),
2032
+ diskMb: ctx.int(v.diskMb, `${path2}.diskMb`, 64, 204800)
2033
+ };
2034
+ return r;
2035
+ }
2036
+ function parseHealthcheck(ctx, v, path2) {
2037
+ if (v === void 0) return void 0;
2038
+ if (!ctx.isObj(v)) {
2039
+ ctx.err(path2, "must be an object");
2040
+ return void 0;
2041
+ }
2042
+ 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;
2043
+ return {
2044
+ test,
2045
+ interval: ctx.str(v.interval, `${path2}.interval`),
2046
+ timeout: ctx.str(v.timeout, `${path2}.timeout`),
2047
+ retries: ctx.int(v.retries, `${path2}.retries`, 0, 100),
2048
+ startPeriod: ctx.str(v.startPeriod, `${path2}.startPeriod`),
2049
+ disable: ctx.bool(v.disable, `${path2}.disable`)
2050
+ };
2051
+ }
2052
+ function parseServices(ctx, v, path2) {
2053
+ if (v === void 0) return void 0;
2054
+ if (!Array.isArray(v)) {
2055
+ ctx.err(path2, "must be an array of service objects");
2056
+ return void 0;
2057
+ }
2058
+ const out = [];
2059
+ v.forEach((item, i) => {
2060
+ const p = `${path2}[${i}]`;
2061
+ if (!ctx.isObj(item)) {
2062
+ ctx.err(p, "must be an object");
2063
+ return;
2064
+ }
2065
+ const name = ctx.str(item.name, `${p}.name`);
2066
+ if (!name) {
2067
+ ctx.err(p, "requires a `name`");
2068
+ return;
2069
+ }
2070
+ out.push({
2071
+ name,
2072
+ image: ctx.str(item.image, `${p}.image`),
2073
+ build: ctx.str(item.build, `${p}.build`),
2074
+ dockerfile: ctx.str(item.dockerfile, `${p}.dockerfile`),
2075
+ ports: ctx.strArray(item.ports, `${p}.ports`),
2076
+ volumes: ctx.strArray(item.volumes, `${p}.volumes`),
2077
+ dependsOn: ctx.strArray(item.dependsOn, `${p}.dependsOn`),
2078
+ env: parseEnv(ctx, item.env, `${p}.env`),
2079
+ command: ctx.str(item.command, `${p}.command`),
2080
+ restart: ctx.enumOf(item.restart, `${p}.restart`, OPENSHIP_RESTARTS),
2081
+ exposed: ctx.bool(item.exposed, `${p}.exposed`),
2082
+ exposedPort: ctx.str(item.exposedPort, `${p}.exposedPort`),
2083
+ domain: ctx.str(item.domain, `${p}.domain`),
2084
+ healthcheck: parseHealthcheck(ctx, item.healthcheck, `${p}.healthcheck`)
2085
+ });
2086
+ });
2087
+ return out;
2088
+ }
2089
+ function parseMonorepo(ctx, v, path2) {
2090
+ if (v === void 0) return void 0;
2091
+ if (!ctx.isObj(v)) {
2092
+ ctx.err(path2, "must be an object");
2093
+ return void 0;
2094
+ }
2095
+ const mono = {};
2096
+ if (v.workspace !== void 0) {
2097
+ if (!ctx.isObj(v.workspace)) ctx.err(`${path2}.workspace`, "must be an object");
2098
+ else {
2099
+ const pm = ctx.str(v.workspace.packageManager, `${path2}.workspace.packageManager`);
2100
+ if (pm) {
2101
+ mono.workspace = {
2102
+ packageManager: pm,
2103
+ prepareCommand: ctx.str(v.workspace.prepareCommand, `${path2}.workspace.prepareCommand`)
2104
+ };
2105
+ } else {
2106
+ ctx.err(`${path2}.workspace`, "requires a `packageManager`");
2107
+ }
2108
+ }
2109
+ }
2110
+ if (v.sharedPaths !== void 0) {
2111
+ ctx.warnings.push(`${path2}.sharedPaths is not applied yet (ignored)`);
2112
+ }
2113
+ if (v.apps !== void 0) {
2114
+ if (!Array.isArray(v.apps)) ctx.err(`${path2}.apps`, "must be an array");
2115
+ else {
2116
+ const apps = [];
2117
+ v.apps.forEach((a, i) => {
2118
+ const p = `${path2}.apps[${i}]`;
2119
+ if (!ctx.isObj(a)) {
2120
+ ctx.err(p, "must be an object");
2121
+ return;
2122
+ }
2123
+ const name = ctx.str(a.name, `${p}.name`);
2124
+ const rootDirectory = ctx.str(a.rootDirectory, `${p}.rootDirectory`);
2125
+ if (!name || !rootDirectory) {
2126
+ ctx.err(p, "requires `name` and `rootDirectory`");
2127
+ return;
2128
+ }
2129
+ apps.push({
2130
+ name,
2131
+ rootDirectory,
2132
+ framework: ctx.enumOf(a.framework, `${p}.framework`, STACK_IDS),
2133
+ packageManager: parsePackageManager(ctx, a.packageManager, `${p}.packageManager`),
2134
+ installCommand: ctx.str(a.installCommand, `${p}.installCommand`),
2135
+ buildCommand: ctx.str(a.buildCommand, `${p}.buildCommand`),
2136
+ startCommand: ctx.str(a.startCommand, `${p}.startCommand`),
2137
+ outputDirectory: ctx.str(a.outputDirectory, `${p}.outputDirectory`),
2138
+ buildImage: ctx.str(a.buildImage, `${p}.buildImage`),
2139
+ port: ctx.int(a.port, `${p}.port`, 1, 65535)
2140
+ });
2141
+ });
2142
+ mono.apps = apps;
2143
+ }
2144
+ }
2145
+ return mono;
2146
+ }
2147
+ function parsePackageManager(ctx, v, path2) {
2148
+ const s = ctx.str(v, path2);
2149
+ if (s === void 0) return void 0;
2150
+ if (!ALL_PACKAGE_MANAGERS.includes(s)) {
2151
+ ctx.err(path2, `must be one of: ${ALL_PACKAGE_MANAGERS.join(", ")}`);
2152
+ return void 0;
2153
+ }
2154
+ return s;
2155
+ }
2156
+ function parseOpenshipConfig(raw) {
2157
+ const ctx = new Ctx();
2158
+ if (!ctx.isObj(raw)) {
2159
+ return { config: null, errors: ["openship.json must be a JSON object"], warnings: [] };
2160
+ }
2161
+ for (const key of Object.keys(raw)) {
2162
+ if (!TOP_LEVEL_KEYS.has(key)) ctx.warnings.push(`Unknown field "${key}" (ignored)`);
2163
+ }
2164
+ const config = {
2165
+ framework: ctx.enumOf(raw.framework, "framework", STACK_IDS),
2166
+ packageManager: parsePackageManager(ctx, raw.packageManager, "packageManager"),
2167
+ rootDirectory: ctx.str(raw.rootDirectory, "rootDirectory"),
2168
+ installCommand: ctx.str(raw.installCommand, "installCommand"),
2169
+ buildCommand: ctx.str(raw.buildCommand, "buildCommand"),
2170
+ startCommand: ctx.str(raw.startCommand, "startCommand"),
2171
+ outputDirectory: ctx.str(raw.outputDirectory, "outputDirectory"),
2172
+ buildImage: ctx.str(raw.buildImage, "buildImage"),
2173
+ productionPaths: ctx.strArray(raw.productionPaths, "productionPaths"),
2174
+ runtime: ctx.enumOf(raw.runtime, "runtime", OPENSHIP_RUNTIMES),
2175
+ productionMode: ctx.enumOf(raw.productionMode, "productionMode", OPENSHIP_PRODUCTION_MODES),
2176
+ port: ctx.int(raw.port, "port", 1, 65535),
2177
+ env: parseEnv(ctx, raw.env, "env"),
2178
+ domains: parseDomains(ctx, raw.domains, "domains"),
2179
+ routes: parseRoutes(ctx, raw.routes, "routes"),
2180
+ resources: parseResources(ctx, raw.resources, "resources"),
2181
+ services: parseServices(ctx, raw.services, "services"),
2182
+ monorepo: parseMonorepo(ctx, raw.monorepo, "monorepo")
2183
+ };
2184
+ for (const k of Object.keys(config)) {
2185
+ if (config[k] === void 0) delete config[k];
2186
+ }
2187
+ return { config, errors: ctx.errors, warnings: ctx.warnings };
2188
+ }
2189
+ function parseOpenshipConfigJson(text2) {
2190
+ let raw;
2191
+ try {
2192
+ raw = JSON.parse(text2);
2193
+ } catch (err2) {
2194
+ return {
2195
+ config: null,
2196
+ errors: [`invalid JSON: ${err2 instanceof Error ? err2.message : String(err2)}`],
2197
+ warnings: []
2198
+ };
2199
+ }
2200
+ return parseOpenshipConfig(raw);
2201
+ }
2202
+
2203
+ // ../../packages/core/src/metadata/openship.ts
2204
+ var openshipMetadataParser = {
2205
+ source: "openship",
2206
+ files: ["openship.json"],
2207
+ parse(fileContents) {
2208
+ const raw = fileContents["openship.json"];
2209
+ if (!raw) return null;
2210
+ const { config } = parseOpenshipConfigJson(raw);
2211
+ if (!config) return null;
2212
+ const metadata = { source: "openship" };
2213
+ if (config.installCommand) metadata.installCommand = config.installCommand;
2214
+ if (config.buildCommand) metadata.buildCommand = config.buildCommand;
2215
+ if (config.outputDirectory) metadata.outputDirectory = config.outputDirectory;
2216
+ if (config.startCommand) metadata.startCommand = config.startCommand;
2217
+ if (config.framework) metadata.framework = config.framework;
2218
+ if (config.routes) metadata.routing = config.routes;
2219
+ const hasSignal = config.installCommand || config.buildCommand || config.outputDirectory || config.startCommand || config.framework || config.routes;
2220
+ return hasSignal ? metadata : null;
2221
+ }
2222
+ };
2223
+
2224
+ // ../../packages/core/src/metadata/text.ts
2225
+ function stripBom2(content) {
2226
+ return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
2227
+ }
2228
+ function trimmed(value) {
2229
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2230
+ }
2231
+ function splitLines(raw) {
2232
+ return stripBom2(raw).split(/\r?\n/);
2233
+ }
2234
+
1810
2235
  // ../../packages/core/src/metadata/vercel.ts
1811
2236
  var VERCEL_FRAMEWORK_TO_STACK = {
1812
2237
  nextjs: "nextjs",
@@ -1821,9 +2246,6 @@ var VERCEL_FRAMEWORK_TO_STACK = {
1821
2246
  angular: "angular",
1822
2247
  "create-react-app": "cra"
1823
2248
  };
1824
- function trimmed(value) {
1825
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1826
- }
1827
2249
  function isConditional(entry) {
1828
2250
  return "has" in entry || "missing" in entry;
1829
2251
  }
@@ -1942,10 +2364,152 @@ var vercelMetadataParser = {
1942
2364
  }
1943
2365
  };
1944
2366
 
1945
- // ../../packages/core/src/metadata/render.ts
1946
- function stripBom2(content) {
1947
- return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
2367
+ // ../../packages/core/src/metadata/railway.ts
2368
+ function tomlScalar(rest) {
2369
+ const s = rest.trim();
2370
+ const quote = s[0];
2371
+ if (quote === '"' || quote === "'") {
2372
+ let out = "";
2373
+ for (let i = 1; i < s.length; i++) {
2374
+ const ch = s[i];
2375
+ if (quote === '"' && ch === "\\" && i + 1 < s.length) {
2376
+ const next = s[++i];
2377
+ switch (next) {
2378
+ case "t":
2379
+ out += " ";
2380
+ break;
2381
+ case "n":
2382
+ out += "\n";
2383
+ break;
2384
+ case "r":
2385
+ out += "\r";
2386
+ break;
2387
+ case "b":
2388
+ out += "\b";
2389
+ break;
2390
+ case "f":
2391
+ out += "\f";
2392
+ break;
2393
+ case '"':
2394
+ out += '"';
2395
+ break;
2396
+ case "\\":
2397
+ out += "\\";
2398
+ break;
2399
+ case "/":
2400
+ out += "/";
2401
+ break;
2402
+ case "u":
2403
+ case "U": {
2404
+ const width = next === "u" ? 4 : 8;
2405
+ const hex = s.slice(i + 1, i + 1 + width);
2406
+ if (hex.length === width && /^[0-9a-fA-F]+$/.test(hex)) {
2407
+ out += String.fromCodePoint(parseInt(hex, 16));
2408
+ i += width;
2409
+ } else {
2410
+ out += next;
2411
+ }
2412
+ break;
2413
+ }
2414
+ default:
2415
+ out += next;
2416
+ }
2417
+ continue;
2418
+ }
2419
+ if (ch === quote) return out;
2420
+ out += ch;
2421
+ }
2422
+ return void 0;
2423
+ }
2424
+ const bare = s.split("#")[0].trim();
2425
+ return bare.length > 0 ? bare : void 0;
1948
2426
  }
2427
+ function parseRailwayToml(raw) {
2428
+ const cfg = {};
2429
+ let section = "";
2430
+ const lines = splitLines(raw);
2431
+ for (let i = 0; i < lines.length; i++) {
2432
+ const line = lines[i];
2433
+ const header = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
2434
+ if (header) {
2435
+ section = header[1].trim().toLowerCase();
2436
+ continue;
2437
+ }
2438
+ const kv = line.match(/^\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.+)$/);
2439
+ if (!kv) continue;
2440
+ const [, rawKey, rest] = kv;
2441
+ const dot = rawKey.lastIndexOf(".");
2442
+ const table = dot >= 0 ? rawKey.slice(0, dot).toLowerCase() : section;
2443
+ const field = dot >= 0 ? rawKey.slice(dot + 1) : rawKey;
2444
+ let value;
2445
+ const triple = rest.match(/^("""|''')/);
2446
+ if (triple) {
2447
+ const delim = triple[1];
2448
+ const afterOpen = rest.slice(3);
2449
+ const close = afterOpen.indexOf(delim);
2450
+ if (close >= 0) {
2451
+ value = afterOpen.slice(0, close).trim() || void 0;
2452
+ } else {
2453
+ while (++i < lines.length && !lines[i].includes(delim)) {
2454
+ }
2455
+ value = void 0;
2456
+ }
2457
+ } else {
2458
+ value = tomlScalar(rest);
2459
+ }
2460
+ if (!value) continue;
2461
+ if (table === "build") {
2462
+ if (field === "buildCommand") cfg.buildCommand ??= value;
2463
+ else if (field === "builder") cfg.builder ??= value;
2464
+ } else if (table === "deploy") {
2465
+ if (field === "startCommand") cfg.startCommand ??= value;
2466
+ }
2467
+ }
2468
+ return cfg;
2469
+ }
2470
+ function parseRailwayJson(raw) {
2471
+ let parsed;
2472
+ try {
2473
+ parsed = JSON.parse(raw);
2474
+ } catch {
2475
+ return null;
2476
+ }
2477
+ if (typeof parsed !== "object" || parsed === null) return null;
2478
+ const obj = parsed;
2479
+ const build = obj.build ?? {};
2480
+ const deploy = obj.deploy ?? {};
2481
+ return {
2482
+ builder: trimmed(build.builder),
2483
+ buildCommand: trimmed(build.buildCommand),
2484
+ startCommand: trimmed(deploy.startCommand)
2485
+ };
2486
+ }
2487
+ function toMetadata(cfg) {
2488
+ const buildCommand = trimmed(cfg.buildCommand);
2489
+ const startCommand = trimmed(cfg.startCommand);
2490
+ const framework = cfg.builder?.toUpperCase() === "DOCKERFILE" ? "docker" : void 0;
2491
+ if (!buildCommand && !startCommand && !framework) return null;
2492
+ const metadata = { source: "railway" };
2493
+ if (buildCommand) metadata.buildCommand = buildCommand;
2494
+ if (startCommand) metadata.startCommand = startCommand;
2495
+ if (framework) metadata.framework = framework;
2496
+ if (extractCdTargets(buildCommand).length > 0) metadata.nonLocal = true;
2497
+ return metadata;
2498
+ }
2499
+ var railwayMetadataParser = {
2500
+ source: "railway",
2501
+ files: ["railway.toml", "railway.json"],
2502
+ parse(fileContents) {
2503
+ const tomlRaw = fileContents["railway.toml"];
2504
+ const jsonRaw = fileContents["railway.json"];
2505
+ const fromToml = tomlRaw ? toMetadata(parseRailwayToml(tomlRaw)) : null;
2506
+ if (fromToml) return fromToml;
2507
+ const jsonCfg = jsonRaw ? parseRailwayJson(jsonRaw) : null;
2508
+ return jsonCfg ? toMetadata(jsonCfg) : null;
2509
+ }
2510
+ };
2511
+
2512
+ // ../../packages/core/src/metadata/render.ts
1949
2513
  function unquote(value) {
1950
2514
  const trimmed2 = value.trim();
1951
2515
  const m = trimmed2.match(/^(['"])(.*)\1$/);
@@ -1957,7 +2521,7 @@ var renderMetadataParser = {
1957
2521
  parse(fileContents) {
1958
2522
  const raw = fileContents["render.yaml"];
1959
2523
  if (!raw) return null;
1960
- const lines = stripBom2(raw).split("\n");
2524
+ const lines = splitLines(raw);
1961
2525
  let startCommand;
1962
2526
  let buildCommand;
1963
2527
  const env = {};
@@ -1991,7 +2555,9 @@ var renderMetadataParser = {
1991
2555
 
1992
2556
  // ../../packages/core/src/metadata/index.ts
1993
2557
  var METADATA_PARSERS = [
2558
+ openshipMetadataParser,
1994
2559
  vercelMetadataParser,
2560
+ railwayMetadataParser,
1995
2561
  renderMetadataParser
1996
2562
  ];
1997
2563
  var METADATA_FILES = new Set(
@@ -2378,11 +2944,11 @@ var openCommand = new Command3("open").description("Open the Openship dashboard
2378
2944
  import { Command as Command4 } from "commander";
2379
2945
  import chalk5 from "chalk";
2380
2946
  import ora from "ora";
2381
- import { spawn } from "child_process";
2947
+ import { spawn as spawn2 } from "child_process";
2382
2948
  import { randomBytes } from "crypto";
2383
- import { createWriteStream as createWriteStream2, existsSync as existsSync5, mkdirSync as mkdirSync6, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
2384
- import { homedir as homedir5 } from "os";
2385
- import { dirname as dirname2, join as join6 } from "path";
2949
+ import { createWriteStream as createWriteStream2, existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
2950
+ import { homedir as homedir6 } from "os";
2951
+ import { dirname as dirname2, join as join7 } from "path";
2386
2952
  import { fileURLToPath } from "url";
2387
2953
 
2388
2954
  // src/lib/dashboard.ts
@@ -2432,8 +2998,8 @@ async function downloadToFile(url, dest, onProgress) {
2432
2998
  } finally {
2433
2999
  file.end();
2434
3000
  }
2435
- await new Promise((resolve2, reject2) => {
2436
- file.on("finish", () => resolve2());
3001
+ await new Promise((resolve3, reject2) => {
3002
+ file.on("finish", () => resolve3());
2437
3003
  file.on("error", reject2);
2438
3004
  });
2439
3005
  return { sha256: hash.digest("hex"), size: received };
@@ -2838,10 +3404,10 @@ function readInstanceUrl() {
2838
3404
  var DEFAULT_API = 4e3;
2839
3405
  var DEFAULT_DASHBOARD = 3001;
2840
3406
  function isPortFree(port) {
2841
- return new Promise((resolve2) => {
3407
+ return new Promise((resolve3) => {
2842
3408
  const srv = createServer();
2843
- srv.once("error", () => resolve2(false));
2844
- srv.listen(port, "127.0.0.1", () => srv.close(() => resolve2(true)));
3409
+ srv.once("error", () => resolve3(false));
3410
+ srv.listen(port, "127.0.0.1", () => srv.close(() => resolve3(true)));
2845
3411
  });
2846
3412
  }
2847
3413
  async function waitPortFree(port, opts = {}) {
@@ -2855,13 +3421,13 @@ async function waitPortFree(port, opts = {}) {
2855
3421
  }
2856
3422
  }
2857
3423
  function getFreePort() {
2858
- return new Promise((resolve2, reject2) => {
3424
+ return new Promise((resolve3, reject2) => {
2859
3425
  const srv = createServer();
2860
3426
  srv.once("error", reject2);
2861
3427
  srv.listen(0, "127.0.0.1", () => {
2862
3428
  const addr = srv.address();
2863
3429
  const port = addr && typeof addr === "object" ? addr.port : 0;
2864
- srv.close(() => port ? resolve2(port) : reject2(new Error("no free port")));
3430
+ srv.close(() => port ? resolve3(port) : reject2(new Error("no free port")));
2865
3431
  });
2866
3432
  });
2867
3433
  }
@@ -2902,6 +3468,101 @@ async function resolvePorts(prefs) {
2902
3468
  };
2903
3469
  }
2904
3470
 
3471
+ // src/lib/from-source.ts
3472
+ import { spawn, spawnSync as spawnSync3 } from "child_process";
3473
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6 } from "fs";
3474
+ import { homedir as homedir5 } from "os";
3475
+ import { join as join6, resolve as resolve2 } from "path";
3476
+ var OS_DIR3 = join6(homedir5(), ".openship");
3477
+ var DEFAULT_REPO = "https://github.com/oblien/openship.git";
3478
+ function has(cmd) {
3479
+ try {
3480
+ return spawnSync3(cmd, ["--version"], { stdio: "ignore" }).status === 0;
3481
+ } catch {
3482
+ return false;
3483
+ }
3484
+ }
3485
+ function run2(cmd, args, cwd, env) {
3486
+ return new Promise((res, rej) => {
3487
+ const child = spawn(cmd, args, {
3488
+ cwd,
3489
+ stdio: "inherit",
3490
+ env: env ? { ...process.env, ...env } : process.env
3491
+ });
3492
+ child.on("error", rej);
3493
+ child.on(
3494
+ "exit",
3495
+ (code) => code === 0 ? res() : rej(new Error(`\`${cmd} ${args.join(" ")}\` (cwd=${cwd}) exited ${code ?? "?"}`))
3496
+ );
3497
+ });
3498
+ }
3499
+ function shortSha(cwd) {
3500
+ const r = spawnSync3("git", ["rev-parse", "--short", "HEAD"], { cwd, encoding: "utf8" });
3501
+ return r.status === 0 ? (r.stdout ?? "").trim() || "unknown" : "unknown";
3502
+ }
3503
+ function isMonorepo(dir) {
3504
+ return existsSync5(join6(dir, "package.json")) && existsSync5(join6(dir, "apps/api/package.json")) && existsSync5(join6(dir, "apps/dashboard/package.json"));
3505
+ }
3506
+ async function prepareFromSource(opts) {
3507
+ if (!has("bun")) {
3508
+ throw new Error(
3509
+ "`bun` is required to build from source but wasn't found on PATH \u2014 install it: https://bun.sh"
3510
+ );
3511
+ }
3512
+ let sourceDir;
3513
+ let ref;
3514
+ if (opts.source) {
3515
+ sourceDir = resolve2(opts.source);
3516
+ if (!isMonorepo(sourceDir)) {
3517
+ throw new Error(
3518
+ `--source ${sourceDir} doesn't look like an Openship checkout (missing package.json / apps/api / apps/dashboard).`
3519
+ );
3520
+ }
3521
+ ref = "local";
3522
+ } else {
3523
+ if (!has("git")) {
3524
+ throw new Error("`git` is required to clone the source but wasn't found on PATH.");
3525
+ }
3526
+ ref = (opts.ref || "main").trim();
3527
+ const repoUrl = opts.repo || DEFAULT_REPO;
3528
+ sourceDir = join6(OS_DIR3, "src");
3529
+ mkdirSync6(OS_DIR3, { recursive: true });
3530
+ if (!existsSync5(join6(sourceDir, ".git"))) {
3531
+ console.log(` Cloning ${repoUrl} \u2192 ${sourceDir}`);
3532
+ await run2("git", ["clone", repoUrl, sourceDir], OS_DIR3);
3533
+ }
3534
+ console.log(` Fetching + checking out ${ref}`);
3535
+ await run2("git", ["fetch", "origin", ref, "--tags"], sourceDir);
3536
+ await run2("git", ["checkout", ref], sourceDir);
3537
+ await run2("git", ["pull", "--ff-only", "origin", ref], sourceDir).catch(() => {
3538
+ console.log(" (pinned ref \u2014 not fast-forwarding)");
3539
+ });
3540
+ }
3541
+ const sha = shortSha(sourceDir);
3542
+ console.log(` Source: ${sourceDir} @ ${ref} (${sha})`);
3543
+ console.log(" Installing workspace dependencies (bun install)\u2026");
3544
+ await run2("bun", ["install"], sourceDir);
3545
+ const distDir = join6(OS_DIR3, "from-source-dist");
3546
+ console.log(" Building release dist (compiles the dashboard \u2014 needs RAM/CPU)\u2026");
3547
+ await run2(
3548
+ "bun",
3549
+ ["run", join6(sourceDir, "apps/api/scripts/build-release.ts")],
3550
+ sourceDir,
3551
+ { DIST_DIR: distDir, NODE_ENV: "production", CLOUD_MODE: "false", OPENSHIP_TARGET: "local" }
3552
+ );
3553
+ console.log(" Installing runtime dependencies in the dist\u2026");
3554
+ await run2("bun", ["install", "--production", "--frozen-lockfile"], distDir);
3555
+ const apiDir = join6(distDir, "api");
3556
+ const dashboardDir = join6(distDir, "dashboard");
3557
+ if (!existsSync5(join6(apiDir, "src/index.ts"))) {
3558
+ throw new Error(`Build produced no API at ${apiDir}/src/index.ts \u2014 build-release layout drift?`);
3559
+ }
3560
+ if (!existsSync5(join6(dashboardDir, "apps/dashboard/server.js"))) {
3561
+ throw new Error(`Build produced no dashboard at ${dashboardDir}/apps/dashboard/server.js.`);
3562
+ }
3563
+ return { apiDir, dashboardDir, ref, sha, sourceDir };
3564
+ }
3565
+
2905
3566
  // src/commands/up.ts
2906
3567
  function normalizeUrl(raw) {
2907
3568
  const value = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
@@ -2924,20 +3585,20 @@ function normalizePublicUrl(raw) {
2924
3585
  return url;
2925
3586
  }
2926
3587
  var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
2927
- var SERVER_DIR = join6(DIST_DIR, "server");
2928
- var OS_DIR3 = join6(homedir5(), ".openship");
3588
+ var SERVER_DIR = join7(DIST_DIR, "server");
3589
+ var OS_DIR4 = join7(homedir6(), ".openship");
2929
3590
  function ensureAuthSecret() {
2930
- const path2 = join6(OS_DIR3, "auth-secret");
2931
- if (existsSync5(path2)) return readFileSync4(path2, "utf8").trim();
2932
- mkdirSync6(OS_DIR3, { recursive: true, mode: 448 });
3591
+ const path2 = join7(OS_DIR4, "auth-secret");
3592
+ if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
3593
+ mkdirSync7(OS_DIR4, { recursive: true, mode: 448 });
2933
3594
  const secret = randomBytes(32).toString("hex");
2934
3595
  writeFileSync5(path2, secret, { mode: 384 });
2935
3596
  return secret;
2936
3597
  }
2937
3598
  function ensureInternalToken() {
2938
- const path2 = join6(OS_DIR3, "internal-token");
2939
- if (existsSync5(path2)) return readFileSync4(path2, "utf8").trim();
2940
- mkdirSync6(OS_DIR3, { recursive: true, mode: 448 });
3599
+ const path2 = join7(OS_DIR4, "internal-token");
3600
+ if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
3601
+ mkdirSync7(OS_DIR4, { recursive: true, mode: 448 });
2941
3602
  const token = randomBytes(32).toString("hex");
2942
3603
  writeFileSync5(path2, token, { mode: 384 });
2943
3604
  return token;
@@ -2951,10 +3612,32 @@ var upCommand = new Command4("up").description("Start Openship as a persistent s
2951
3612
  ).option(
2952
3613
  "--managed-edge",
2953
3614
  "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)"
2954
- ).option("--acme-email <email>", "Contact email for Let's Encrypt certificates (managed edge)").action(async (opts) => {
3615
+ ).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) => {
3616
+ if (opts.fromSource || opts.source) return runFromSource(opts);
2955
3617
  if (opts.foreground) return runForeground(opts);
2956
3618
  await startService(opts);
2957
3619
  });
3620
+ async function runFromSource(opts) {
3621
+ console.log(chalk5.cyan("\n Building Openship from source (preview mode)\u2026"));
3622
+ console.log(
3623
+ chalk5.dim(" Unverified dev build \u2014 for previewing a branch, not production self-hosting.\n")
3624
+ );
3625
+ let src;
3626
+ try {
3627
+ src = await prepareFromSource({ ref: opts.ref, source: opts.source, repo: opts.repo });
3628
+ } catch (e) {
3629
+ console.error(
3630
+ chalk5.red(`
3631
+ Build from source failed: ${e.message}
3632
+ `) + 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")
3633
+ );
3634
+ process.exit(1);
3635
+ }
3636
+ console.log(chalk5.green(`
3637
+ Built ${src.ref} (${src.sha}). Starting\u2026
3638
+ `));
3639
+ await runForeground(opts, src);
3640
+ }
2958
3641
  async function startService(opts, runOpts = {}) {
2959
3642
  const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
2960
3643
  if (opts.dryRun) {
@@ -3029,13 +3712,23 @@ async function startService(opts, runOpts = {}) {
3029
3712
  process.exit(1);
3030
3713
  }
3031
3714
  }
3032
- async function runForeground(opts) {
3033
- const serverEntry = join6(SERVER_DIR, "index.js");
3034
- if (!existsSync5(serverEntry)) {
3035
- console.error(
3036
- chalk5.red("\n Bundled server not found in this install.") + chalk5.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
3037
- );
3038
- process.exit(1);
3715
+ async function runForeground(opts, source) {
3716
+ let apiCmd = process.execPath;
3717
+ let apiArgs;
3718
+ let apiCwd;
3719
+ if (source) {
3720
+ apiCmd = "bun";
3721
+ apiArgs = ["run", "src/index.ts"];
3722
+ apiCwd = source.apiDir;
3723
+ } else {
3724
+ const serverEntry = join7(SERVER_DIR, "index.js");
3725
+ if (!existsSync6(serverEntry)) {
3726
+ console.error(
3727
+ chalk5.red("\n Bundled server not found in this install.") + chalk5.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
3728
+ );
3729
+ process.exit(1);
3730
+ }
3731
+ apiArgs = [serverEntry];
3039
3732
  }
3040
3733
  const resolved = await resolvePorts({
3041
3734
  api: opts.port ? Number(opts.port) : void 0,
@@ -3045,11 +3738,11 @@ async function runForeground(opts) {
3045
3738
  const dashPort = String(resolved.dashboard);
3046
3739
  const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
3047
3740
  const managedEdge = Boolean(opts.managedEdge && publicUrl);
3048
- const dataDir = opts.dataDir || join6(OS_DIR3, "data");
3049
- mkdirSync6(dataDir, { recursive: true });
3050
- const logDir = join6(OS_DIR3, "logs");
3051
- mkdirSync6(logDir, { recursive: true });
3052
- const instanceLogPath = join6(logDir, "instance.log");
3741
+ const dataDir = opts.dataDir || join7(OS_DIR4, "data");
3742
+ mkdirSync7(dataDir, { recursive: true });
3743
+ const logDir = join7(OS_DIR4, "logs");
3744
+ mkdirSync7(logDir, { recursive: true });
3745
+ const instanceLogPath = join7(logDir, "instance.log");
3053
3746
  const instanceLog = createWriteStream2(instanceLogPath, { flags: "w" });
3054
3747
  const env = {
3055
3748
  ...process.env,
@@ -3060,10 +3753,12 @@ async function runForeground(opts) {
3060
3753
  OPENSHIP_TARGET: "local",
3061
3754
  OPENSHIP_JOB_RUNNER: "in-process",
3062
3755
  PGLITE_DATA_DIR: dataDir,
3063
- OPENSHIP_MIGRATIONS_DIR: join6(SERVER_DIR, "migrations"),
3064
- OPENSHIP_PGLITE_ASSETS_DIR: join6(SERVER_DIR, "pglite"),
3065
3756
  BETTER_AUTH_SECRET: ensureAuthSecret()
3066
3757
  };
3758
+ if (!source) {
3759
+ env.OPENSHIP_MIGRATIONS_DIR = join7(SERVER_DIR, "migrations");
3760
+ env.OPENSHIP_PGLITE_ASSETS_DIR = join7(SERVER_DIR, "pglite");
3761
+ }
3067
3762
  env.OPENSHIP_REQUIRE_AUTH = "true";
3068
3763
  env.INTERNAL_TOKEN = ensureInternalToken();
3069
3764
  env.OPENSHIP_API_HOST = "127.0.0.1";
@@ -3082,7 +3777,8 @@ async function runForeground(opts) {
3082
3777
  delete env.DATABASE_URL;
3083
3778
  delete env.POSTGRES_URL;
3084
3779
  const spinner3 = ora(`Starting Openship on http://localhost:${port} \u2026`).start();
3085
- const child = spawn(process.execPath, [serverEntry], {
3780
+ const child = spawn2(apiCmd, apiArgs, {
3781
+ cwd: apiCwd,
3086
3782
  env,
3087
3783
  stdio: ["ignore", "pipe", "pipe"],
3088
3784
  detached: process.platform !== "win32"
@@ -3146,10 +3842,11 @@ async function runForeground(opts) {
3146
3842
  };
3147
3843
  let dashboardUrl = null;
3148
3844
  if (opts.ui !== false) {
3845
+ if (source) process.env.OPENSHIP_DASHBOARD_DIR = source.dashboardDir;
3149
3846
  const uiSpinner = ora("Preparing the dashboard\u2026").start();
3150
3847
  try {
3151
3848
  const bundle = await ensureDashboard({
3152
- tag: opts.uiVersion || `v${"0.2.2"}`,
3849
+ tag: source ? "local" : opts.uiVersion || `v${"0.3.0"}`,
3153
3850
  onProgress: (received, total) => {
3154
3851
  if (total) {
3155
3852
  uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
@@ -3157,7 +3854,7 @@ async function runForeground(opts) {
3157
3854
  }
3158
3855
  });
3159
3856
  uiSpinner.text = "Starting the dashboard\u2026";
3160
- const dash = spawn(process.execPath, [bundle.entry], {
3857
+ const dash = spawn2(process.execPath, [bundle.entry], {
3161
3858
  cwd: bundle.cwd,
3162
3859
  detached: process.platform !== "win32",
3163
3860
  env: {
@@ -3271,15 +3968,15 @@ var stopCommand = new Command5("stop").description("Stop the Openship service (s
3271
3968
 
3272
3969
  // src/commands/init.ts
3273
3970
  import { Command as Command6 } from "commander";
3274
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
3971
+ import { existsSync as existsSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync6 } from "fs";
3275
3972
  import { createInterface as createInterface2 } from "readline/promises";
3276
3973
  import { stdin as input2, stdout as output2 } from "process";
3277
- import { join as join7 } from "path";
3974
+ import { join as join8 } from "path";
3278
3975
  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) => {
3279
3976
  const root = opts.dir || process.cwd();
3280
- const linkDir = join7(root, ".openship");
3281
- const linkPath = join7(linkDir, "project.json");
3282
- if (existsSync6(linkPath) && !opts.force) {
3977
+ const linkDir = join8(root, ".openship");
3978
+ const linkPath = join8(linkDir, "project.json");
3979
+ if (existsSync7(linkPath) && !opts.force) {
3283
3980
  err(`Already linked (${linkPath}). Re-run with --force to overwrite.`);
3284
3981
  process.exit(1);
3285
3982
  }
@@ -3332,7 +4029,7 @@ var initCommand = new Command6("init").description("Link the current directory t
3332
4029
  context: getActiveContext(),
3333
4030
  defaults: { environment: opts.environment || "production" }
3334
4031
  };
3335
- mkdirSync7(linkDir, { recursive: true });
4032
+ mkdirSync8(linkDir, { recursive: true });
3336
4033
  writeFileSync6(linkPath, JSON.stringify(link, null, 2) + "\n");
3337
4034
  if (isJsonMode()) {
3338
4035
  printJson({ path: linkPath, link });
@@ -3343,8 +4040,86 @@ var initCommand = new Command6("init").description("Link the current directory t
3343
4040
  `);
3344
4041
  });
3345
4042
 
3346
- // src/commands/context.ts
4043
+ // src/commands/config.ts
3347
4044
  import { Command as Command7 } from "commander";
4045
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
4046
+ import { join as join9 } from "path";
4047
+ var SCHEMA_URL = "https://openship.io/openship.schema.json";
4048
+ var CONFIG_FILE = "openship.json";
4049
+ function detectHints(dir) {
4050
+ const hints = {};
4051
+ const lock = [
4052
+ ["pnpm-lock.yaml", "pnpm"],
4053
+ ["yarn.lock", "yarn"],
4054
+ ["bun.lockb", "bun"],
4055
+ ["package-lock.json", "npm"]
4056
+ ];
4057
+ for (const [file, pm] of lock) {
4058
+ if (existsSync8(join9(dir, file))) {
4059
+ hints.packageManager = pm;
4060
+ break;
4061
+ }
4062
+ }
4063
+ const pkgPath = join9(dir, "package.json");
4064
+ if (existsSync8(pkgPath)) {
4065
+ try {
4066
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
4067
+ const runner = hints.packageManager ?? "npm";
4068
+ const exec = runner === "npm" ? "npm run" : runner;
4069
+ if (pkg.scripts?.build) hints.buildCommand = `${exec} build`;
4070
+ if (pkg.scripts?.start) hints.startCommand = `${exec} start`;
4071
+ } catch {
4072
+ }
4073
+ }
4074
+ return hints;
4075
+ }
4076
+ 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) => {
4077
+ const dir = opts.dir || process.cwd();
4078
+ const path2 = join9(dir, CONFIG_FILE);
4079
+ if (existsSync8(path2) && !opts.force) {
4080
+ err(`${CONFIG_FILE} already exists. Re-run with --force to overwrite.`);
4081
+ process.exit(1);
4082
+ }
4083
+ const hints = detectHints(dir);
4084
+ const scaffold = { $schema: SCHEMA_URL, ...hints };
4085
+ const text2 = JSON.stringify(scaffold, null, 2) + "\n";
4086
+ writeFileSync7(path2, text2);
4087
+ if (isJsonMode()) {
4088
+ printJson({ path: path2, config: scaffold });
4089
+ return;
4090
+ }
4091
+ ok(`
4092
+ Wrote ${CONFIG_FILE} \u2192 ${path2}`);
4093
+ info(" Edit it to declare framework, env, domains, resources, services\u2026 then `openship config validate`.\n");
4094
+ });
4095
+ 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) => {
4096
+ const path2 = file === CONFIG_FILE ? join9(process.cwd(), CONFIG_FILE) : file;
4097
+ if (!existsSync8(path2)) {
4098
+ if (isJsonMode()) printJson({ valid: false, errors: [`${path2} not found`], warnings: [] });
4099
+ else err(`Not found: ${path2}`);
4100
+ process.exit(1);
4101
+ }
4102
+ const { config, errors, warnings } = parseOpenshipConfigJson(readFileSync5(path2, "utf8"));
4103
+ const valid = errors.length === 0 && config !== null;
4104
+ if (isJsonMode()) {
4105
+ printJson({ valid, errors, warnings });
4106
+ process.exit(valid ? 0 : 1);
4107
+ }
4108
+ for (const w of warnings) info(` \u26A0 ${w}`);
4109
+ if (!valid) {
4110
+ err(`
4111
+ ${errors.length} error${errors.length === 1 ? "" : "s"} in ${CONFIG_FILE}:`);
4112
+ for (const e of errors) err(` \u2022 ${e}`);
4113
+ process.exit(1);
4114
+ }
4115
+ ok(`
4116
+ ${CONFIG_FILE} is valid${warnings.length ? ` (${warnings.length} warning${warnings.length === 1 ? "" : "s"})` : ""}.
4117
+ `);
4118
+ });
4119
+ var configCommand = new Command7("config").description("Author and validate openship.json (declarative deploy config)").addCommand(initCmd).addCommand(validateCmd);
4120
+
4121
+ // src/commands/context.ts
4122
+ import { Command as Command8 } from "commander";
3348
4123
  function renderContexts() {
3349
4124
  const rows = listContexts().map((c) => ({
3350
4125
  current: c.current ? "*" : "",
@@ -3355,8 +4130,8 @@ function renderContexts() {
3355
4130
  }));
3356
4131
  printTable(rows, ["current", "name", "apiUrl", "dashboardUrl", "auth"]);
3357
4132
  }
3358
- var listCmd = new Command7("list").alias("ls").description("List configured contexts").action(renderContexts);
3359
- var useCmd = new Command7("use").description("Switch the active context").argument("<name>", "Context name").action((name) => {
4133
+ var listCmd = new Command8("list").alias("ls").description("List configured contexts").action(renderContexts);
4134
+ var useCmd = new Command8("use").description("Switch the active context").argument("<name>", "Context name").action((name) => {
3360
4135
  try {
3361
4136
  setActiveContext(name);
3362
4137
  ok(`
@@ -3367,7 +4142,7 @@ var useCmd = new Command7("use").description("Switch the active context").argume
3367
4142
  process.exit(1);
3368
4143
  }
3369
4144
  });
3370
- 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) => {
4145
+ 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) => {
3371
4146
  addContext(name, {
3372
4147
  apiUrl: opts.apiUrl,
3373
4148
  dashboardUrl: opts.dashboardUrl,
@@ -3378,7 +4153,7 @@ var addCmd = new Command7("add").description("Create or update a context's endpo
3378
4153
  Saved context "${name}"${opts.use ? " (now active)" : ""}.
3379
4154
  `);
3380
4155
  });
3381
- var rmCmd = new Command7("rm").alias("remove").description("Remove a context (cannot remove the active one)").argument("<name>", "Context name").action((name) => {
4156
+ var rmCmd = new Command8("rm").alias("remove").description("Remove a context (cannot remove the active one)").argument("<name>", "Context name").action((name) => {
3382
4157
  try {
3383
4158
  removeContext(name);
3384
4159
  ok(`
@@ -3389,25 +4164,25 @@ var rmCmd = new Command7("rm").alias("remove").description("Remove a context (ca
3389
4164
  process.exit(1);
3390
4165
  }
3391
4166
  });
3392
- var contextCommand = new Command7("context").alias("ctx").description("Manage connection contexts (list/use/add/rm)").action(() => {
4167
+ var contextCommand = new Command8("context").alias("ctx").description("Manage connection contexts (list/use/add/rm)").action(() => {
3393
4168
  ok(` Active context: ${getActiveContext()}`);
3394
4169
  renderContexts();
3395
4170
  }).addCommand(listCmd).addCommand(useCmd).addCommand(addCmd).addCommand(rmCmd);
3396
4171
 
3397
4172
  // src/commands/status.ts
3398
- import { Command as Command8 } from "commander";
4173
+ import { Command as Command9 } from "commander";
3399
4174
  import chalk7 from "chalk";
3400
- import { readFileSync as readFileSync5 } from "fs";
3401
- import { homedir as homedir6 } from "os";
3402
- import { join as join8 } from "path";
4175
+ import { readFileSync as readFileSync6 } from "fs";
4176
+ import { homedir as homedir7 } from "os";
4177
+ import { join as join10 } from "path";
3403
4178
  function readPorts() {
3404
4179
  try {
3405
- return JSON.parse(readFileSync5(join8(homedir6(), ".openship", "ports.json"), "utf8"));
4180
+ return JSON.parse(readFileSync6(join10(homedir7(), ".openship", "ports.json"), "utf8"));
3406
4181
  } catch {
3407
4182
  return {};
3408
4183
  }
3409
4184
  }
3410
- var statusCommand = new Command8("status").description("Show the local Openship service (installed/running, ports) and the active context's API health").action(async () => {
4185
+ var statusCommand = new Command9("status").description("Show the local Openship service (installed/running, ports) and the active context's API health").action(async () => {
3411
4186
  const context = getActiveContext();
3412
4187
  const apiUrl = getApiUrl2();
3413
4188
  const svc = serviceStatus();
@@ -3442,8 +4217,8 @@ var statusCommand = new Command8("status").description("Show the local Openship
3442
4217
  });
3443
4218
 
3444
4219
  // src/commands/doctor.ts
3445
- import { Command as Command9 } from "commander";
3446
- import { existsSync as existsSync7 } from "fs";
4220
+ import { Command as Command10 } from "commander";
4221
+ import { existsSync as existsSync9 } from "fs";
3447
4222
  import { execFileSync } from "child_process";
3448
4223
  import chalk8 from "chalk";
3449
4224
  function bunVersion() {
@@ -3455,9 +4230,9 @@ function bunVersion() {
3455
4230
  return null;
3456
4231
  }
3457
4232
  }
3458
- var doctorCommand = new Command9("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
4233
+ var doctorCommand = new Command10("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
3459
4234
  const checks = [];
3460
- const hasConfig = existsSync7(CONFIG_PATH);
4235
+ const hasConfig = existsSync9(CONFIG_PATH);
3461
4236
  checks.push({
3462
4237
  name: "config",
3463
4238
  status: hasConfig ? "pass" : "warn",
@@ -3505,20 +4280,20 @@ var doctorCommand = new Command9("doctor").description("Diagnose the CLI setup (
3505
4280
  });
3506
4281
 
3507
4282
  // src/commands/deploy.ts
3508
- import { Command as Command10 } from "commander";
4283
+ import { Command as Command11 } from "commander";
3509
4284
  import { execFileSync as execFileSync3 } from "child_process";
3510
4285
  import ora2 from "ora";
3511
4286
 
3512
4287
  // src/lib/project-link.ts
3513
- import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
3514
- import { join as join9, dirname as dirname3, parse } from "path";
3515
- var LINK_REL = join9(".openship", "project.json");
4288
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
4289
+ import { join as join11, dirname as dirname3, parse } from "path";
4290
+ var LINK_REL = join11(".openship", "project.json");
3516
4291
  function findProjectLinkPath(from = process.cwd()) {
3517
4292
  let dir = from;
3518
4293
  const root = parse(dir).root;
3519
4294
  for (; ; ) {
3520
- const candidate = join9(dir, LINK_REL);
3521
- if (existsSync8(candidate)) return candidate;
4295
+ const candidate = join11(dir, LINK_REL);
4296
+ if (existsSync10(candidate)) return candidate;
3522
4297
  if (dir === root) return null;
3523
4298
  dir = dirname3(dir);
3524
4299
  }
@@ -3527,7 +4302,7 @@ function readProjectLink(from) {
3527
4302
  const path2 = findProjectLinkPath(from);
3528
4303
  if (!path2) return null;
3529
4304
  try {
3530
- return JSON.parse(readFileSync6(path2, "utf8"));
4305
+ return JSON.parse(readFileSync7(path2, "utf8"));
3531
4306
  } catch {
3532
4307
  return null;
3533
4308
  }
@@ -3535,21 +4310,21 @@ function readProjectLink(from) {
3535
4310
 
3536
4311
  // src/lib/folder-deploy.ts
3537
4312
  import { execFileSync as execFileSync2 } from "child_process";
3538
- import { readFileSync as readFileSync7, existsSync as existsSync9, rmSync as rmSync3 } from "fs";
4313
+ import { readFileSync as readFileSync8, existsSync as existsSync11, rmSync as rmSync3 } from "fs";
3539
4314
  import { tmpdir } from "os";
3540
- import { join as join10, basename } from "path";
4315
+ import { join as join12, basename } from "path";
3541
4316
  function detectPackageManager(dir) {
3542
- if (existsSync9(join10(dir, "bun.lockb")) || existsSync9(join10(dir, "bun.lock"))) return "bun";
3543
- if (existsSync9(join10(dir, "pnpm-lock.yaml"))) return "pnpm";
3544
- if (existsSync9(join10(dir, "yarn.lock"))) return "yarn";
3545
- if (existsSync9(join10(dir, "package.json"))) return "npm";
4317
+ if (existsSync11(join12(dir, "bun.lockb")) || existsSync11(join12(dir, "bun.lock"))) return "bun";
4318
+ if (existsSync11(join12(dir, "pnpm-lock.yaml"))) return "pnpm";
4319
+ if (existsSync11(join12(dir, "yarn.lock"))) return "yarn";
4320
+ if (existsSync11(join12(dir, "package.json"))) return "npm";
3546
4321
  return void 0;
3547
4322
  }
3548
4323
  function detectStack(dir) {
3549
- if (existsSync9(join10(dir, "go.mod"))) return "go";
3550
- if (existsSync9(join10(dir, "Cargo.toml"))) return "rust";
3551
- if (existsSync9(join10(dir, "requirements.txt")) || existsSync9(join10(dir, "pyproject.toml"))) return "python";
3552
- if (existsSync9(join10(dir, "package.json"))) return "node";
4324
+ if (existsSync11(join12(dir, "go.mod"))) return "go";
4325
+ if (existsSync11(join12(dir, "Cargo.toml"))) return "rust";
4326
+ if (existsSync11(join12(dir, "requirements.txt")) || existsSync11(join12(dir, "pyproject.toml"))) return "python";
4327
+ if (existsSync11(join12(dir, "package.json"))) return "node";
3553
4328
  return void 0;
3554
4329
  }
3555
4330
  async function deployFolder(opts) {
@@ -3566,7 +4341,7 @@ async function deployFolder(opts) {
3566
4341
  throw new Error(session.error || "Failed to open upload session");
3567
4342
  }
3568
4343
  step("Packaging folder");
3569
- const tarball = join10(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
4344
+ const tarball = join12(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3570
4345
  execFileSync2(
3571
4346
  "tar",
3572
4347
  [
@@ -3585,7 +4360,7 @@ async function deployFolder(opts) {
3585
4360
  );
3586
4361
  step("Uploading source");
3587
4362
  try {
3588
- const body = readFileSync7(tarball);
4363
+ const body = readFileSync8(tarball);
3589
4364
  const up = session.upload;
3590
4365
  const method = up.method || "POST";
3591
4366
  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 });
@@ -3766,7 +4541,7 @@ function git(args) {
3766
4541
  return void 0;
3767
4542
  }
3768
4543
  }
3769
- 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) => {
4544
+ 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) => {
3770
4545
  const link = readProjectLink();
3771
4546
  const env = opts.env;
3772
4547
  if (env !== "production" && env !== "preview") {
@@ -3848,9 +4623,9 @@ var deployCommand = new Command10("deploy").description("Trigger a deployment fo
3848
4623
  });
3849
4624
 
3850
4625
  // src/commands/deployment.ts
3851
- import { Command as Command11 } from "commander";
4626
+ import { Command as Command12 } from "commander";
3852
4627
  import { createInterface as createInterface3 } from "readline";
3853
- function run2(fn) {
4628
+ function run3(fn) {
3854
4629
  return async (...args) => {
3855
4630
  try {
3856
4631
  await fn(...args);
@@ -3864,18 +4639,18 @@ function report(res, message) {
3864
4639
  if (isJsonMode()) printJson(res);
3865
4640
  else ok(message);
3866
4641
  }
3867
- function shortSha(v) {
4642
+ function shortSha2(v) {
3868
4643
  return typeof v === "string" ? v.slice(0, 7) : "";
3869
4644
  }
3870
4645
  async function confirm(question) {
3871
4646
  if (!process.stdin.isTTY) return true;
3872
4647
  const rl = createInterface3({ input: process.stdin, output: process.stderr });
3873
- const answer = await new Promise((resolve2) => rl.question(`${question} [y/N] `, resolve2));
4648
+ const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
3874
4649
  rl.close();
3875
4650
  return /^y(es)?$/i.test(answer.trim());
3876
4651
  }
3877
- 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(
3878
- run2(async (opts) => {
4652
+ 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(
4653
+ run3(async (opts) => {
3879
4654
  const projectId = opts.project || readProjectLink()?.projectId;
3880
4655
  const params = new URLSearchParams();
3881
4656
  if (projectId) params.set("projectId", projectId);
@@ -3890,15 +4665,15 @@ var list = new Command11("list").description("List deployments (org-wide, or sco
3890
4665
  status: d.status,
3891
4666
  env: d.environment,
3892
4667
  branch: d.branch,
3893
- commit: shortSha(d.commitSha),
4668
+ commit: shortSha2(d.commitSha),
3894
4669
  active: d.isActive ? "*" : "",
3895
4670
  created: d.createdAt
3896
4671
  }));
3897
4672
  printTable(rows, ["id", "status", "env", "branch", "commit", "active", "created"]);
3898
4673
  })
3899
4674
  );
3900
- var get = new Command11("get").description("Show a single deployment").argument("<id>", "Deployment ID").action(
3901
- run2(async (id) => {
4675
+ var get = new Command12("get").description("Show a single deployment").argument("<id>", "Deployment ID").action(
4676
+ run3(async (id) => {
3902
4677
  const res = await apiRequest(`/deployments/${id}`);
3903
4678
  const d = res.data ?? {};
3904
4679
  if (isJsonMode()) return printJson(d);
@@ -3918,20 +4693,20 @@ var get = new Command11("get").description("Show a single deployment").argument(
3918
4693
  );
3919
4694
  })
3920
4695
  );
3921
- var info2 = new Command11("info").description("Show container info for a deployment").argument("<id>", "Deployment ID").action(
3922
- run2(async (id) => {
4696
+ var info2 = new Command12("info").description("Show container info for a deployment").argument("<id>", "Deployment ID").action(
4697
+ run3(async (id) => {
3923
4698
  const res = await apiRequest(`/deployments/${id}/info`);
3924
4699
  printJson(res.data ?? res);
3925
4700
  })
3926
4701
  );
3927
- var usage = new Command11("usage").description("Show container resource usage for a deployment").argument("<id>", "Deployment ID").action(
3928
- run2(async (id) => {
4702
+ var usage = new Command12("usage").description("Show container resource usage for a deployment").argument("<id>", "Deployment ID").action(
4703
+ run3(async (id) => {
3929
4704
  const res = await apiRequest(`/deployments/${id}/usage`);
3930
4705
  printJson(res.data ?? res);
3931
4706
  })
3932
4707
  );
3933
- 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(
3934
- run2(async (id, opts) => {
4708
+ 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(
4709
+ run3(async (id, opts) => {
3935
4710
  const res = await apiRequest(`/deployments/${id}/redeploy`, {
3936
4711
  method: "POST",
3937
4712
  body: JSON.stringify({ useExistingCommit: opts.useExistingCommit === true })
@@ -3939,14 +4714,14 @@ var redeploy = new Command11("redeploy").description("Redeploy from an existing
3939
4714
  report(res, `Redeploy triggered for ${id}`);
3940
4715
  })
3941
4716
  );
3942
- var rollback = new Command11("rollback").description("Roll back to a previous deployment").argument("<id>", "Deployment ID to roll back to").action(
3943
- run2(async (id) => {
4717
+ var rollback = new Command12("rollback").description("Roll back to a previous deployment").argument("<id>", "Deployment ID to roll back to").action(
4718
+ run3(async (id) => {
3944
4719
  const res = await apiRequest(`/deployments/${id}/rollback`, { method: "POST" });
3945
4720
  report(res, `Rolled back to ${id}`);
3946
4721
  })
3947
4722
  );
3948
- 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(
3949
- run2(async (id, opts) => {
4723
+ 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(
4724
+ run3(async (id, opts) => {
3950
4725
  const pinned = !opts.off;
3951
4726
  const res = await apiRequest(`/deployments/${id}/pin`, {
3952
4727
  method: "POST",
@@ -3955,32 +4730,32 @@ var pin = new Command11("pin").description("Pin (or unpin) a deployment's rollba
3955
4730
  report(res, `${pinned ? "Pinned" : "Unpinned"} ${id}`);
3956
4731
  })
3957
4732
  );
3958
- var cancel = new Command11("cancel").description("Cancel an in-progress deployment").argument("<id>", "Deployment ID").action(
3959
- run2(async (id) => {
4733
+ var cancel = new Command12("cancel").description("Cancel an in-progress deployment").argument("<id>", "Deployment ID").action(
4734
+ run3(async (id) => {
3960
4735
  const res = await apiRequest(`/deployments/${id}/cancel`, { method: "POST" });
3961
4736
  report(res, `Cancelled ${id}`);
3962
4737
  })
3963
4738
  );
3964
- var restart2 = new Command11("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
3965
- run2(async (id) => {
4739
+ var restart2 = new Command12("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
4740
+ run3(async (id) => {
3966
4741
  const res = await apiRequest(`/deployments/${id}/restart`, { method: "POST" });
3967
4742
  report(res, `Restarted ${id}`);
3968
4743
  })
3969
4744
  );
3970
- var reject = new Command11("reject").description("Reject a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
3971
- run2(async (id) => {
4745
+ var reject = new Command12("reject").description("Reject a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
4746
+ run3(async (id) => {
3972
4747
  const res = await apiRequest(`/deployments/${id}/reject`, { method: "POST" });
3973
4748
  report(res, `Rejected ${id}`);
3974
4749
  })
3975
4750
  );
3976
- var keep = new Command11("keep").description("Keep a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
3977
- run2(async (id) => {
4751
+ var keep = new Command12("keep").description("Keep a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
4752
+ run3(async (id) => {
3978
4753
  const res = await apiRequest(`/deployments/${id}/keep`, { method: "POST" });
3979
4754
  report(res, `Kept ${id}`);
3980
4755
  })
3981
4756
  );
3982
- var rm = new Command11("rm").description("Delete a deployment").argument("<id>", "Deployment ID").option("-y, --yes", "Skip the confirmation prompt").action(
3983
- run2(async (id, opts) => {
4757
+ var rm = new Command12("rm").description("Delete a deployment").argument("<id>", "Deployment ID").option("-y, --yes", "Skip the confirmation prompt").action(
4758
+ run3(async (id, opts) => {
3984
4759
  if (!opts.yes && !isJsonMode() && !await confirm(`Delete deployment ${id}?`)) {
3985
4760
  err("Aborted.");
3986
4761
  process.exit(1);
@@ -3989,8 +4764,8 @@ var rm = new Command11("rm").description("Delete a deployment").argument("<id>",
3989
4764
  report(res, `Deleted ${id}`);
3990
4765
  })
3991
4766
  );
3992
- var sslStatus = new Command11("status").description("Check SSL certificate status for a domain").argument("<domain>", "Domain to probe").action(
3993
- run2(async (domain) => {
4767
+ var sslStatus = new Command12("status").description("Check SSL certificate status for a domain").argument("<domain>", "Domain to probe").action(
4768
+ run3(async (domain) => {
3994
4769
  const res = await apiRequest("/deployments/ssl/status", {
3995
4770
  method: "POST",
3996
4771
  body: JSON.stringify({ domain })
@@ -3998,8 +4773,8 @@ var sslStatus = new Command11("status").description("Check SSL certificate statu
3998
4773
  printJson(res);
3999
4774
  })
4000
4775
  );
4001
- 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(
4002
- run2(async (domain, opts) => {
4776
+ 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(
4777
+ run3(async (domain, opts) => {
4003
4778
  const res = await apiRequest("/deployments/ssl/renew", {
4004
4779
  method: "POST",
4005
4780
  body: JSON.stringify({ domain, includeWww: opts.www === true })
@@ -4007,12 +4782,12 @@ var sslRenew = new Command11("renew").description("Renew (issue) an SSL certific
4007
4782
  report(res, `SSL renewal requested for ${domain}`);
4008
4783
  })
4009
4784
  );
4010
- var ssl = new Command11("ssl").description("SSL certificate operations").addCommand(sslStatus).addCommand(sslRenew);
4011
- 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);
4785
+ var ssl = new Command12("ssl").description("SSL certificate operations").addCommand(sslStatus).addCommand(sslRenew);
4786
+ 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);
4012
4787
 
4013
4788
  // src/commands/logs.ts
4014
- import { Command as Command12 } from "commander";
4015
- 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) => {
4789
+ import { Command as Command13 } from "commander";
4790
+ 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) => {
4016
4791
  if (opts.follow) {
4017
4792
  try {
4018
4793
  const result = await streamDeploymentLogs(deploymentId);
@@ -4044,7 +4819,7 @@ var logsCommand = new Command12("logs").description("View or stream a deployment
4044
4819
  });
4045
4820
 
4046
4821
  // src/commands/project.ts
4047
- import { Command as Command13 } from "commander";
4822
+ import { Command as Command14 } from "commander";
4048
4823
  import chalk9 from "chalk";
4049
4824
  import { createInterface as createInterface4 } from "readline/promises";
4050
4825
  import { stdin as input3, stdout as output3 } from "process";
@@ -4081,7 +4856,7 @@ function printProject(project) {
4081
4856
  }
4082
4857
  }
4083
4858
  var ENVIRONMENTS = ["production", "preview", "development"];
4084
- var listCmd2 = new Command13("list").alias("ls").description("List projects in the active organization").action(
4859
+ var listCmd2 = new Command14("list").alias("ls").description("List projects in the active organization").action(
4085
4860
  action(async () => {
4086
4861
  const rows = [];
4087
4862
  for await (const p of paginate("/projects")) {
@@ -4096,7 +4871,7 @@ var listCmd2 = new Command13("list").alias("ls").description("List projects in t
4096
4871
  printTable(rows, ["id", "name", "slug", "repo", "source"]);
4097
4872
  })
4098
4873
  );
4099
- var getCmd = new Command13("get").description("Show a single project").argument("<id>", "Project ID").action(
4874
+ var getCmd = new Command14("get").description("Show a single project").argument("<id>", "Project ID").action(
4100
4875
  action(async (id) => {
4101
4876
  const { data } = await apiRequest(
4102
4877
  `/projects/${encodeURIComponent(id)}`
@@ -4104,7 +4879,7 @@ var getCmd = new Command13("get").description("Show a single project").argument(
4104
4879
  printProject(data);
4105
4880
  })
4106
4881
  );
4107
- 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(
4882
+ 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(
4108
4883
  "--type <type>",
4109
4884
  "Project type: app | docker | services | monorepo"
4110
4885
  ).action(
@@ -4128,7 +4903,7 @@ var createCmd = new Command13("create").description("Create a project").required
4128
4903
  printProject(data);
4129
4904
  })
4130
4905
  );
4131
- 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(
4906
+ 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(
4132
4907
  action(async (id, opts) => {
4133
4908
  if (!opts.yes) {
4134
4909
  const rl = createInterface4({ input: input3, output: output3 });
@@ -4159,7 +4934,7 @@ var deleteCmd = new Command13("delete").alias("rm").description("Delete a projec
4159
4934
  `);
4160
4935
  })
4161
4936
  );
4162
- var envCmd = new Command13("env").description("Manage project environment variables");
4937
+ var envCmd = new Command14("env").description("Manage project environment variables");
4163
4938
  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(
4164
4939
  action(async (id, opts) => {
4165
4940
  const qs = opts.environment ? `?environment=${encodeURIComponent(opts.environment)}` : "";
@@ -4233,7 +5008,7 @@ envCmd.command("set").description("Merge env vars: upsert KEY=VALUE pairs and/or
4233
5008
  );
4234
5009
  })
4235
5010
  );
4236
- var gitCmd = new Command13("git").description("Manage git linkage and auto-deploy");
5011
+ var gitCmd = new Command14("git").description("Manage git linkage and auto-deploy");
4237
5012
  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(
4238
5013
  action(async (id, opts) => {
4239
5014
  const result = await apiRequest(
@@ -4321,7 +5096,7 @@ gitCmd.command("webhook-domain").description("Set or clear the domain that recei
4321
5096
  `);
4322
5097
  })
4323
5098
  );
4324
- 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(
5099
+ 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(
4325
5100
  action(async (id, domain, opts) => {
4326
5101
  const result = await apiRequest(
4327
5102
  `/projects/${encodeURIComponent(id)}/connect`,
@@ -4341,7 +5116,7 @@ var connectCmd = new Command13("connect").description("Connect a custom domain t
4341
5116
  printJson(result.records);
4342
5117
  })
4343
5118
  );
4344
- var enableCmd = new Command13("enable").description("Start a stopped project").argument("<id>", "Project ID").action(
5119
+ var enableCmd = new Command14("enable").description("Start a stopped project").argument("<id>", "Project ID").action(
4345
5120
  action(async (id) => {
4346
5121
  const result = await apiRequest(
4347
5122
  `/projects/${encodeURIComponent(id)}/enable`,
@@ -4353,7 +5128,7 @@ var enableCmd = new Command13("enable").description("Start a stopped project").a
4353
5128
  `);
4354
5129
  })
4355
5130
  );
4356
- var disableCmd = new Command13("disable").description("Stop a running project").argument("<id>", "Project ID").action(
5131
+ var disableCmd = new Command14("disable").description("Stop a running project").argument("<id>", "Project ID").action(
4357
5132
  action(async (id) => {
4358
5133
  const result = await apiRequest(
4359
5134
  `/projects/${encodeURIComponent(id)}/disable`,
@@ -4366,7 +5141,7 @@ var disableCmd = new Command13("disable").description("Stop a running project").
4366
5141
  })
4367
5142
  );
4368
5143
  var SLEEP_MODES = ["auto_sleep", "always_on"];
4369
- var sleepModeCmd = new Command13("sleep-mode").description("Set the project sleep mode").argument("<id>", "Project ID").argument("<mode>", `One of: ${SLEEP_MODES.join(", ")}`).action(
5144
+ var sleepModeCmd = new Command14("sleep-mode").description("Set the project sleep mode").argument("<id>", "Project ID").argument("<mode>", `One of: ${SLEEP_MODES.join(", ")}`).action(
4370
5145
  action(async (id, mode) => {
4371
5146
  if (!SLEEP_MODES.includes(mode)) {
4372
5147
  err(` mode must be one of: ${SLEEP_MODES.join(", ")}`);
@@ -4384,7 +5159,7 @@ var sleepModeCmd = new Command13("sleep-mode").description("Set the project slee
4384
5159
  })
4385
5160
  );
4386
5161
  var TRANSFER_DIRS = ["to-cloud", "to-self-hosted"];
4387
- 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(
5162
+ 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(
4388
5163
  action(async (id, direction) => {
4389
5164
  if (!TRANSFER_DIRS.includes(direction)) {
4390
5165
  err(` direction must be one of: ${TRANSFER_DIRS.join(", ")}`);
@@ -4407,7 +5182,7 @@ var transferCmd = new Command13("transfer").description("Promote a project to Op
4407
5182
  );
4408
5183
  })
4409
5184
  );
4410
- 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(
5185
+ 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(
4411
5186
  action(async (id, opts) => {
4412
5187
  const tailQs = opts.tail ? `?tail=${opts.tail}` : "";
4413
5188
  if (!opts.follow) {
@@ -4435,7 +5210,7 @@ var logsCmd = new Command13("logs").description("Show or stream runtime (contain
4435
5210
  }
4436
5211
  })
4437
5212
  );
4438
- 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(
5213
+ 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(
4439
5214
  action(async (id, opts) => {
4440
5215
  const base = `/projects/${encodeURIComponent(id)}/server-logs`;
4441
5216
  const domainQs = opts.domain ? `domain=${encodeURIComponent(opts.domain)}` : "";
@@ -4483,7 +5258,7 @@ function printLogEntry(entry) {
4483
5258
  process.stdout.write(` ${ts} ${color(level.padEnd(5))} ${String(msg)}
4484
5259
  `);
4485
5260
  }
4486
- var projectCommand = new Command13("project").alias("projects").description("Manage Openship projects");
5261
+ var projectCommand = new Command14("project").alias("projects").description("Manage Openship projects");
4487
5262
  projectCommand.addCommand(listCmd2);
4488
5263
  projectCommand.addCommand(getCmd);
4489
5264
  projectCommand.addCommand(createCmd);
@@ -4499,9 +5274,9 @@ projectCommand.addCommand(logsCmd);
4499
5274
  projectCommand.addCommand(serverLogsCmd);
4500
5275
 
4501
5276
  // src/commands/service.ts
4502
- import { Command as Command14 } from "commander";
5277
+ import { Command as Command15 } from "commander";
4503
5278
  import chalk10 from "chalk";
4504
- import { spawnSync as spawnSync3 } from "child_process";
5279
+ import { spawnSync as spawnSync4 } from "child_process";
4505
5280
  import path from "path";
4506
5281
  import { createInterface as createInterface5 } from "readline/promises";
4507
5282
  import { stdin as input4, stdout as output4 } from "process";
@@ -4520,7 +5295,7 @@ function fail(e) {
4520
5295
  process.exit(1);
4521
5296
  }
4522
5297
  function stackCommand(name) {
4523
- return new Command14(name).requiredOption(
5298
+ return new Command15(name).requiredOption(
4524
5299
  "-p, --project <id|slug|name>",
4525
5300
  "Stack (project) id, slug, or name"
4526
5301
  );
@@ -4768,7 +5543,7 @@ function mapComposeService(name, def, baseDir) {
4768
5543
  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) => {
4769
5544
  requireAuth();
4770
5545
  const abs = path.resolve(composeFile);
4771
- const proc = spawnSync3(
5546
+ const proc = spawnSync4(
4772
5547
  "docker",
4773
5548
  ["compose", "-f", abs, "config", "--format", "json"],
4774
5549
  { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }
@@ -4862,7 +5637,7 @@ var containersCmd = stackCommand("containers").description("List the stack's act
4862
5637
  fail(e);
4863
5638
  }
4864
5639
  });
4865
- var driftCmd = new Command14("drift").description(
5640
+ var driftCmd = new Command15("drift").description(
4866
5641
  "Resolve compose drift on a service (upstream compose changed a value you edited)"
4867
5642
  );
4868
5643
  function driftActionCommand(action2) {
@@ -4890,7 +5665,7 @@ function driftActionCommand(action2) {
4890
5665
  }
4891
5666
  driftCmd.addCommand(driftActionCommand("accept"));
4892
5667
  driftCmd.addCommand(driftActionCommand("keep"));
4893
- var envCmd2 = new Command14("env").description("Read and write a service's environment variables");
5668
+ var envCmd2 = new Command15("env").description("Read and write a service's environment variables");
4894
5669
  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) => {
4895
5670
  requireAuth();
4896
5671
  try {
@@ -5023,7 +5798,7 @@ var execCmd = stackCommand("exec").description("Open an interactive shell in a s
5023
5798
  );
5024
5799
  process.exit(1);
5025
5800
  });
5026
- var serviceCommand = new Command14("service").alias("services").description("Manage the services in a compose stack (a multi-service project)");
5801
+ var serviceCommand = new Command15("service").alias("services").description("Manage the services in a compose stack (a multi-service project)");
5027
5802
  serviceCommand.addCommand(listCmd3);
5028
5803
  serviceCommand.addCommand(getCmd2);
5029
5804
  serviceCommand.addCommand(createCmd2);
@@ -5039,7 +5814,7 @@ serviceCommand.addCommand(logsCmd2);
5039
5814
  serviceCommand.addCommand(execCmd);
5040
5815
 
5041
5816
  // src/commands/domain.ts
5042
- import { Command as Command15 } from "commander";
5817
+ import { Command as Command16 } from "commander";
5043
5818
  import chalk11 from "chalk";
5044
5819
  import ora3 from "ora";
5045
5820
  function spin(text2) {
@@ -5075,7 +5850,7 @@ function printRecords(result) {
5075
5850
  ["type", "host", "value"]
5076
5851
  );
5077
5852
  }
5078
- 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) => {
5853
+ 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) => {
5079
5854
  try {
5080
5855
  const res = await apiRequest(
5081
5856
  `/domains?projectId=${encodeURIComponent(opts.project)}`
@@ -5090,7 +5865,7 @@ var listCmd4 = new Command15("list").description("List a project's custom domain
5090
5865
  fail2(e);
5091
5866
  }
5092
5867
  });
5093
- 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) => {
5868
+ 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) => {
5094
5869
  const sp = spin(`Adding ${hostname}\u2026`);
5095
5870
  try {
5096
5871
  const res = await apiRequest("/domains", {
@@ -5109,7 +5884,7 @@ var addCmd2 = new Command15("add").description("Add a custom domain to a project
5109
5884
  fail2(e);
5110
5885
  }
5111
5886
  });
5112
- 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) => {
5887
+ 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) => {
5113
5888
  try {
5114
5889
  const res = await apiRequest("/domains/preview", {
5115
5890
  method: "POST",
@@ -5120,7 +5895,7 @@ var previewCmd = new Command15("preview").description("Preview the DNS records a
5120
5895
  fail2(e);
5121
5896
  }
5122
5897
  });
5123
- var verifyCmd = new Command15("verify").description("Run DNS verification for a domain").argument("<id>", "Domain ID").action(async (id) => {
5898
+ var verifyCmd = new Command16("verify").description("Run DNS verification for a domain").argument("<id>", "Domain ID").action(async (id) => {
5124
5899
  const sp = spin("Checking DNS records\u2026");
5125
5900
  try {
5126
5901
  const res = await apiRaw(`/domains/${encodeURIComponent(id)}/verify`, { method: "POST" });
@@ -5146,7 +5921,7 @@ var verifyCmd = new Command15("verify").description("Run DNS verification for a
5146
5921
  fail2(e);
5147
5922
  }
5148
5923
  });
5149
- var primaryCmd = new Command15("primary").description("Make a domain the project's primary hostname").argument("<id>", "Domain ID").action(async (id) => {
5924
+ var primaryCmd = new Command16("primary").description("Make a domain the project's primary hostname").argument("<id>", "Domain ID").action(async (id) => {
5150
5925
  const sp = spin("Setting primary\u2026");
5151
5926
  try {
5152
5927
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/primary`, {
@@ -5159,7 +5934,7 @@ var primaryCmd = new Command15("primary").description("Make a domain the project
5159
5934
  fail2(e);
5160
5935
  }
5161
5936
  });
5162
- var recordsCmd = new Command15("records").description("Show the DNS records for an existing domain").argument("<id>", "Domain ID").action(async (id) => {
5937
+ var recordsCmd = new Command16("records").description("Show the DNS records for an existing domain").argument("<id>", "Domain ID").action(async (id) => {
5163
5938
  try {
5164
5939
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/records`);
5165
5940
  printRecords(res.data);
@@ -5177,7 +5952,7 @@ function printSsl(data) {
5177
5952
  if (data.issuer) info(` issuer: ${data.issuer}`);
5178
5953
  if (data.expiresAt) info(` expires: ${data.expiresAt}`);
5179
5954
  }
5180
- var renewCmd = new Command15("renew").description("Renew the SSL certificate for a domain").argument("<id>", "Domain ID").action(async (id) => {
5955
+ var renewCmd = new Command16("renew").description("Renew the SSL certificate for a domain").argument("<id>", "Domain ID").action(async (id) => {
5181
5956
  const sp = spin("Renewing certificate\u2026");
5182
5957
  try {
5183
5958
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/renew`, {
@@ -5190,7 +5965,7 @@ var renewCmd = new Command15("renew").description("Renew the SSL certificate for
5190
5965
  fail2(e);
5191
5966
  }
5192
5967
  });
5193
- 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) => {
5968
+ 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) => {
5194
5969
  const sp = spin("Checking certificate\u2026");
5195
5970
  try {
5196
5971
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/verify-ssl`, {
@@ -5205,7 +5980,7 @@ var verifySslCmd = new Command15("verify-ssl").description("Recheck that a domai
5205
5980
  fail2(e);
5206
5981
  }
5207
5982
  });
5208
- var renewAllCmd = new Command15("renew-all").description("Renew SSL for every near-expiry domain in your organization").action(async () => {
5983
+ var renewAllCmd = new Command16("renew-all").description("Renew SSL for every near-expiry domain in your organization").action(async () => {
5209
5984
  const sp = spin("Renewing expiring certificates\u2026");
5210
5985
  try {
5211
5986
  const res = await apiRequest("/domains/renew-all", { method: "POST" });
@@ -5227,10 +6002,10 @@ var renewAllCmd = new Command15("renew-all").description("Renew SSL for every ne
5227
6002
  fail2(e);
5228
6003
  }
5229
6004
  });
5230
- 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);
6005
+ 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);
5231
6006
 
5232
6007
  // src/commands/server.ts
5233
- import { Command as Command16 } from "commander";
6008
+ import { Command as Command17 } from "commander";
5234
6009
  import chalk12 from "chalk";
5235
6010
  import ora4 from "ora";
5236
6011
  var INSTALLABLE = ["docker", "git", "openresty", "certbot", "rsync"];
@@ -5263,7 +6038,7 @@ function connBody(o) {
5263
6038
  sshArgs: o.sshArgs
5264
6039
  };
5265
6040
  }
5266
- var server = new Command16("server").description("Manage self-hosted SSH servers");
6041
+ var server = new Command17("server").description("Manage self-hosted SSH servers");
5267
6042
  server.command("list").alias("ls").description("List servers in the active organization").action(
5268
6043
  guard(async () => {
5269
6044
  const servers = await apiRequest("/system/servers");
@@ -5525,9 +6300,9 @@ function fmtUptime(seconds) {
5525
6300
  var serverCommand = server;
5526
6301
 
5527
6302
  // src/commands/system.ts
5528
- import { Command as Command17 } from "commander";
6303
+ import { Command as Command18 } from "commander";
5529
6304
  import ora5 from "ora";
5530
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
6305
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
5531
6306
  import { createInterface as createInterface6 } from "readline/promises";
5532
6307
  import { stdin as input5, stdout as output5 } from "process";
5533
6308
  async function guarded(fn) {
@@ -5573,7 +6348,7 @@ async function promptHidden(query) {
5573
6348
  output5.write("\n");
5574
6349
  return answer;
5575
6350
  }
5576
- var settingsCommand = new Command17("settings").description("Read or update instance settings");
6351
+ var settingsCommand = new Command18("settings").description("Read or update instance settings");
5577
6352
  settingsCommand.command("get").description("Show current instance settings").action(async () => {
5578
6353
  await guarded(async () => {
5579
6354
  const s = await apiRequest("/system/settings");
@@ -5612,7 +6387,7 @@ settingsCommand.command("set").description("Update instance-level settings").opt
5612
6387
  `));
5613
6388
  });
5614
6389
  });
5615
- var onboardingCommand = new Command17("onboarding").description("First-run instance setup");
6390
+ var onboardingCommand = new Command18("onboarding").description("First-run instance setup");
5616
6391
  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) => {
5617
6392
  await guarded(async () => {
5618
6393
  const body = {
@@ -5646,7 +6421,7 @@ onboardingCommand.command("apply").description("Configure a fresh instance (fail
5646
6421
  }
5647
6422
  });
5648
6423
  });
5649
- 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) => {
6424
+ 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) => {
5650
6425
  await guarded(async () => {
5651
6426
  const name = opts.name;
5652
6427
  const email = opts.email;
@@ -5675,7 +6450,7 @@ var upgradeToAuthCommand = new Command17("upgrade-to-auth").description("Promote
5675
6450
  `));
5676
6451
  });
5677
6452
  });
5678
- var browseCommand = new Command17("browse").description("List directories on the instance host (defaults to home)").argument("[path]", "Directory to list").action(async (path2) => {
6453
+ var browseCommand = new Command18("browse").description("List directories on the instance host (defaults to home)").argument("[path]", "Directory to list").action(async (path2) => {
5679
6454
  await guarded(async () => {
5680
6455
  const qs = path2 ? `?path=${encodeURIComponent(path2)}` : "";
5681
6456
  const res = await apiRequest(`/system/browse${qs}`);
@@ -5704,7 +6479,7 @@ function buildDomain(opts) {
5704
6479
  err("\n A domain is required: pass --hostname <host> or --slug <slug>.\n");
5705
6480
  process.exit(1);
5706
6481
  }
5707
- var migrationCommand = new Command17("migration").description("Team-mode migration lifecycle");
6482
+ var migrationCommand = new Command18("migration").description("Team-mode migration lifecycle");
5708
6483
  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) => {
5709
6484
  await guarded(async () => {
5710
6485
  const domain = buildDomain(opts);
@@ -5811,7 +6586,7 @@ migrationCommand.command("switch-back").description("Reverse migration back to s
5811
6586
  }
5812
6587
  });
5813
6588
  });
5814
- var dataTransferCommand = new Command17("data-transfer").description(
6589
+ var dataTransferCommand = new Command18("data-transfer").description(
5815
6590
  "Whole-instance export / import (owner-only)"
5816
6591
  );
5817
6592
  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) => {
@@ -5827,7 +6602,7 @@ dataTransferCommand.command("export").description("Export the entire instance to
5827
6602
  );
5828
6603
  spin4?.succeed("Export ready.");
5829
6604
  if (opts.out) {
5830
- writeFileSync7(opts.out, JSON.stringify(file));
6605
+ writeFileSync8(opts.out, JSON.stringify(file));
5831
6606
  const tables = Object.keys(file.dump?.tables ?? {}).length;
5832
6607
  report2(
5833
6608
  { out: opts.out, tables },
@@ -5853,7 +6628,7 @@ dataTransferCommand.command("import").description("Import an instance export fil
5853
6628
  }
5854
6629
  let file;
5855
6630
  try {
5856
- file = JSON.parse(readFileSync8(opts.file, "utf8"));
6631
+ file = JSON.parse(readFileSync9(opts.file, "utf8"));
5857
6632
  } catch {
5858
6633
  err(`
5859
6634
  Could not read or parse ${opts.file}.
@@ -5881,10 +6656,10 @@ dataTransferCommand.command("import").description("Import an instance export fil
5881
6656
  }
5882
6657
  });
5883
6658
  });
5884
- 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);
6659
+ 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);
5885
6660
 
5886
6661
  // src/commands/mail.ts
5887
- import { Command as Command18 } from "commander";
6662
+ import { Command as Command19 } from "commander";
5888
6663
  import chalk13 from "chalk";
5889
6664
  import ora6 from "ora";
5890
6665
  import { createInterface as createInterface7 } from "readline/promises";
@@ -5926,7 +6701,7 @@ function printRecordsObject(records) {
5926
6701
  if (rows.length === 0) return info(" (no DNS records)");
5927
6702
  printTable(rows, ["key", "type", "host", "value"]);
5928
6703
  }
5929
- var stepsCmd = new Command18("steps").description("List the mail setup steps").action(
6704
+ var stepsCmd = new Command19("steps").description("List the mail setup steps").action(
5930
6705
  guard2(async () => {
5931
6706
  const res = await apiRequest(
5932
6707
  "/mail/steps"
@@ -5939,7 +6714,7 @@ var stepsCmd = new Command18("steps").description("List the mail setup steps").a
5939
6714
  info(` ${res.total} steps total.`);
5940
6715
  })
5941
6716
  );
5942
- 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(
6717
+ 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(
5943
6718
  guard2(async (serverId) => {
5944
6719
  const q = serverId ? `?serverId=${encodeURIComponent(serverId)}` : "";
5945
6720
  const res = await apiRequest(`/mail/status${q}`);
@@ -5960,7 +6735,7 @@ var statusCmd = new Command18("status").description("Show the setup progress for
5960
6735
  }
5961
6736
  })
5962
6737
  );
5963
- var serversCmd = new Command18("servers").description("List every server the mail stack is installed on").action(
6738
+ var serversCmd = new Command19("servers").description("List every server the mail stack is installed on").action(
5964
6739
  guard2(async () => {
5965
6740
  const res = await apiRequest(
5966
6741
  "/mail/servers"
@@ -5981,7 +6756,7 @@ var serversCmd = new Command18("servers").description("List every server the mai
5981
6756
  );
5982
6757
  })
5983
6758
  );
5984
- var scanCmd = new Command18("scan").description("Probe a server for an existing mail install (read-only)").argument("<serverId>", "Server ID to scan").action(
6759
+ var scanCmd = new Command19("scan").description("Probe a server for an existing mail install (read-only)").argument("<serverId>", "Server ID to scan").action(
5985
6760
  guard2(async (serverId) => {
5986
6761
  const sp = spin2("Scanning server\u2026");
5987
6762
  const res = await apiRequest("/mail/scan", { method: "POST", body: JSON.stringify({ serverId }) });
@@ -5996,7 +6771,7 @@ var scanCmd = new Command18("scan").description("Probe a server for an existing
5996
6771
  else info(" Nothing to adopt on this server.");
5997
6772
  })
5998
6773
  );
5999
- var adoptCmd = new Command18("adopt").description("Re-adopt an existing mail install whose orchestrator state was lost").argument("<serverId>", "Server ID to adopt").action(
6774
+ var adoptCmd = new Command19("adopt").description("Re-adopt an existing mail install whose orchestrator state was lost").argument("<serverId>", "Server ID to adopt").action(
6000
6775
  guard2(async (serverId) => {
6001
6776
  const sp = spin2("Adopting mail server\u2026");
6002
6777
  const res = await apiRequest(
@@ -6009,7 +6784,7 @@ var adoptCmd = new Command18("adopt").description("Re-adopt an existing mail ins
6009
6784
  info(` completed: ${res.completed ? "yes" : "no"}`);
6010
6785
  })
6011
6786
  );
6012
- 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(
6787
+ 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(
6013
6788
  guard2(async (serverId, opts) => {
6014
6789
  let config;
6015
6790
  if (opts.config) {
@@ -6078,7 +6853,7 @@ var setupCmd = new Command18("setup").description("Start or resume the mail setu
6078
6853
  if (failed) process.exit(1);
6079
6854
  })
6080
6855
  );
6081
- var cancelCmd = new Command18("cancel").description("Cancel the mail setup currently running").action(
6856
+ var cancelCmd = new Command19("cancel").description("Cancel the mail setup currently running").action(
6082
6857
  guard2(async () => {
6083
6858
  const res = await apiRequest("/mail/setup/cancel", {
6084
6859
  method: "POST"
@@ -6088,7 +6863,7 @@ var cancelCmd = new Command18("cancel").description("Cancel the mail setup curre
6088
6863
  })
6089
6864
  );
6090
6865
  function ackCommand(name, path2, description, successMsg) {
6091
- return new Command18(name).description(description).argument("<serverId>", "Mail server ID").action(
6866
+ return new Command19(name).description(description).argument("<serverId>", "Mail server ID").action(
6092
6867
  guard2(async (serverId) => {
6093
6868
  const res = await apiRequest(path2, {
6094
6869
  method: "POST",
@@ -6111,7 +6886,7 @@ var ptrAckCmd = ackCommand(
6111
6886
  "Acknowledge that reverse DNS (PTR) is configured",
6112
6887
  "PTR acknowledged. Re-run `mail setup` with --start-step to continue."
6113
6888
  );
6114
- 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(
6889
+ 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(
6115
6890
  guard2(async (serverId, opts) => {
6116
6891
  if (!opts.yes && !isJsonMode()) {
6117
6892
  const rl = createInterface7({ input: input6, output: output6 });
@@ -6127,7 +6902,7 @@ var resetCmd = new Command18("reset").description("Wipe the on-server setup stat
6127
6902
  ok(" Setup state reset.");
6128
6903
  })
6129
6904
  );
6130
- 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(
6905
+ 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(
6131
6906
  guard2(async (serverId) => {
6132
6907
  const res = await apiRequest(`/mail/servers/${encodeURIComponent(serverId)}`, {
6133
6908
  method: "DELETE"
@@ -6136,7 +6911,7 @@ var forgetCmd = new Command18("forget").description("Stop managing a mail server
6136
6911
  ok(` Forgot mail server ${serverId} (re-adopt with \`mail scan\` + \`mail adopt\`).`);
6137
6912
  })
6138
6913
  );
6139
- var healthCmd = new Command18("health").description("Show live status of every mail daemon").argument("<serverId>", "Mail server ID").action(
6914
+ var healthCmd = new Command19("health").description("Show live status of every mail daemon").argument("<serverId>", "Mail server ID").action(
6140
6915
  guard2(async (serverId) => {
6141
6916
  const sp = spin2("Checking mail daemons\u2026");
6142
6917
  const res = await apiRequest(`/mail/health/${encodeURIComponent(serverId)}`);
@@ -6153,7 +6928,7 @@ var healthCmd = new Command18("health").description("Show live status of every m
6153
6928
  );
6154
6929
  })
6155
6930
  );
6156
- 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(
6931
+ 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(
6157
6932
  guard2(async (serverId, component, opts) => {
6158
6933
  const res = await apiRequest(
6159
6934
  `/mail/admin/${encodeURIComponent(serverId)}/components/${encodeURIComponent(component)}/logs?lines=${encodeURIComponent(opts.lines)}`
@@ -6163,9 +6938,9 @@ var logsCmd3 = new Command18("logs").description("Tail a mail component's journa
6163
6938
  for (const line of res.lines) process.stdout.write(line + "\n");
6164
6939
  })
6165
6940
  );
6166
- var postmasterCmd = new Command18("postmaster").description("Manage the postmaster mailbox");
6941
+ var postmasterCmd = new Command19("postmaster").description("Manage the postmaster mailbox");
6167
6942
  postmasterCmd.addCommand(
6168
- 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(
6943
+ 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(
6169
6944
  guard2(async (serverId, opts) => {
6170
6945
  let password2 = opts.password;
6171
6946
  if (!password2) {
@@ -6191,12 +6966,12 @@ postmasterCmd.addCommand(
6191
6966
  })
6192
6967
  )
6193
6968
  );
6194
- 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);
6969
+ 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);
6195
6970
 
6196
6971
  // src/commands/backup.ts
6197
- import { Command as Command19 } from "commander";
6972
+ import { Command as Command20 } from "commander";
6198
6973
  import ora7 from "ora";
6199
- import { readFileSync as readFileSync9 } from "fs";
6974
+ import { readFileSync as readFileSync10 } from "fs";
6200
6975
  async function guard3(fn) {
6201
6976
  try {
6202
6977
  await fn();
@@ -6279,7 +7054,7 @@ async function followStream(path2, label) {
6279
7054
  spinner3?.stop();
6280
7055
  return status;
6281
7056
  }
6282
- var policyCmd = new Command19("policy").description("Backup policies (schedules) for a project");
7057
+ var policyCmd = new Command20("policy").description("Backup policies (schedules) for a project");
6283
7058
  policyCmd.command("list").description("List backup policies for a project").requiredOption("--project <id>", "Project ID").action(
6284
7059
  (opts) => guard3(async () => {
6285
7060
  const { data } = await apiRequest(
@@ -6349,7 +7124,7 @@ policyCmd.command("run").description("Trigger a policy's backup now").argument("
6349
7124
  }
6350
7125
  })
6351
7126
  );
6352
- var runCmd = new Command19("run").description("Backup runs (executions)");
7127
+ var runCmd = new Command20("run").description("Backup runs (executions)");
6353
7128
  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(
6354
7129
  (opts) => guard3(async () => {
6355
7130
  const qs = new URLSearchParams();
@@ -6425,7 +7200,7 @@ runCmd.command("restore").description("Prepare a restore from a run (stages it;
6425
7200
  }
6426
7201
  })
6427
7202
  );
6428
- var restoreCmd = new Command19("restore").description("Manage staged restores");
7203
+ var restoreCmd = new Command20("restore").description("Manage staged restores");
6429
7204
  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(
6430
7205
  (restoreId, opts) => guard3(async () => {
6431
7206
  await apiRequest(
@@ -6468,7 +7243,7 @@ restoreCmd.command("get").description("Show one restore (optionally stream it)")
6468
7243
  show(data);
6469
7244
  })
6470
7245
  );
6471
- var destinationCmd = new Command19("destination").description("Backup destinations (storage targets)");
7246
+ var destinationCmd = new Command20("destination").description("Backup destinations (storage targets)");
6472
7247
  destinationCmd.command("list").description("List backup destinations").action(
6473
7248
  () => guard3(async () => {
6474
7249
  const { data } = await apiRequest("/backup-destinations");
@@ -6489,7 +7264,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
6489
7264
  let sftpPrivateKey = opts.sftpPrivateKey;
6490
7265
  if (opts.sftpPrivateKeyFile) {
6491
7266
  try {
6492
- sftpPrivateKey = readFileSync9(opts.sftpPrivateKeyFile, "utf8");
7267
+ sftpPrivateKey = readFileSync10(opts.sftpPrivateKeyFile, "utf8");
6493
7268
  } catch {
6494
7269
  throw new Error(`Cannot read key file: ${opts.sftpPrivateKeyFile}`);
6495
7270
  }
@@ -6540,10 +7315,10 @@ destinationCmd.command("preflight").description("Verify a destination (write + r
6540
7315
  }
6541
7316
  })
6542
7317
  );
6543
- var backupCommand = new Command19("backup").description("Manage backups: policies, runs, restores, destinations").addCommand(policyCmd).addCommand(runCmd).addCommand(restoreCmd).addCommand(destinationCmd);
7318
+ var backupCommand = new Command20("backup").description("Manage backups: policies, runs, restores, destinations").addCommand(policyCmd).addCommand(runCmd).addCommand(restoreCmd).addCommand(destinationCmd);
6544
7319
 
6545
7320
  // src/commands/token.ts
6546
- import { Command as Command20 } from "commander";
7321
+ import { Command as Command21 } from "commander";
6547
7322
  import chalk15 from "chalk";
6548
7323
 
6549
7324
  // src/lib/cmd-helpers.ts
@@ -6571,7 +7346,7 @@ function collectGrant(value, acc) {
6571
7346
  acc.push({ resourceType, resourceId, permissions });
6572
7347
  return acc;
6573
7348
  }
6574
- var listCmd5 = new Command20("list").description("List your personal access tokens").action(async () => {
7349
+ var listCmd5 = new Command21("list").description("List your personal access tokens").action(async () => {
6575
7350
  try {
6576
7351
  const res = await apiRequest("/tokens");
6577
7352
  const rows = res.data ?? [];
@@ -6596,7 +7371,7 @@ var listCmd5 = new Command20("list").description("List your personal access toke
6596
7371
  fail3(e);
6597
7372
  }
6598
7373
  });
6599
- 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(
7374
+ 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(
6600
7375
  "--grant <type:id:perms>",
6601
7376
  "Scope the token to a resource (repeatable), e.g. project:abc123:read,write",
6602
7377
  collectGrant,
@@ -6628,7 +7403,7 @@ var createCmd3 = new Command20("create").description("Mint a new personal access
6628
7403
  fail3(e);
6629
7404
  }
6630
7405
  });
6631
- var revokeCmd = new Command20("revoke").description("Revoke one of your tokens").argument("<id>", "Token ID").action(async (id) => {
7406
+ var revokeCmd = new Command21("revoke").description("Revoke one of your tokens").argument("<id>", "Token ID").action(async (id) => {
6632
7407
  const sp = spin3("Revoking token\u2026");
6633
7408
  try {
6634
7409
  await apiRequest(`/tokens/${encodeURIComponent(id)}`, { method: "DELETE" });
@@ -6640,11 +7415,11 @@ var revokeCmd = new Command20("revoke").description("Revoke one of your tokens")
6640
7415
  fail3(e);
6641
7416
  }
6642
7417
  });
6643
- var tokenCommand = new Command20("token").description("Manage personal access tokens").addCommand(listCmd5).addCommand(createCmd3).addCommand(revokeCmd);
7418
+ var tokenCommand = new Command21("token").description("Manage personal access tokens").addCommand(listCmd5).addCommand(createCmd3).addCommand(revokeCmd);
6644
7419
 
6645
7420
  // src/commands/api.ts
6646
- import { Command as Command21 } from "commander";
6647
- 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) => {
7421
+ import { Command as Command22 } from "commander";
7422
+ 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) => {
6648
7423
  const method = (opts.method || (opts.data ? "POST" : "GET")).toUpperCase();
6649
7424
  let url = path2.startsWith("/") ? path2 : `/${path2}`;
6650
7425
  if (opts.query?.length) {
@@ -6678,20 +7453,20 @@ var apiCommand = new Command21("api").description("Make an authenticated request
6678
7453
  });
6679
7454
 
6680
7455
  // src/commands/reset-admin.ts
6681
- import { Command as Command22 } from "commander";
7456
+ import { Command as Command23 } from "commander";
6682
7457
  import chalk16 from "chalk";
6683
7458
  import { intro, outro, password as passwordPrompt, isCancel, cancel as cancel2, log } from "@clack/prompts";
6684
- import { readFileSync as readFileSync10 } from "fs";
6685
- import { homedir as homedir7 } from "os";
6686
- import { join as join11 } from "path";
7459
+ import { readFileSync as readFileSync11 } from "fs";
7460
+ import { homedir as homedir8 } from "os";
7461
+ import { join as join13 } from "path";
6687
7462
  function resolvedApiPort() {
6688
7463
  try {
6689
- return JSON.parse(readFileSync10(join11(homedir7(), ".openship", "ports.json"), "utf8")).api;
7464
+ return JSON.parse(readFileSync11(join13(homedir8(), ".openship", "ports.json"), "utf8")).api;
6690
7465
  } catch {
6691
7466
  return void 0;
6692
7467
  }
6693
7468
  }
6694
- var resetAdminCommand = new Command22("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) => {
7469
+ 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) => {
6695
7470
  intro(chalk16.cyan("Reset Openship admin password"));
6696
7471
  let pw = opts.password;
6697
7472
  if (!pw) {
@@ -6739,11 +7514,11 @@ var resetAdminCommand = new Command22("reset-admin-password").description("Reset
6739
7514
  });
6740
7515
 
6741
7516
  // src/commands/install.ts
6742
- import { Command as Command23 } from "commander";
6743
- import { chmodSync as chmodSync2, existsSync as existsSync10, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync8 } from "fs";
6744
- import { spawn as spawn2, spawnSync as spawnSync4 } from "child_process";
6745
- import { homedir as homedir8 } from "os";
6746
- import { join as join12 } from "path";
7517
+ import { Command as Command24 } from "commander";
7518
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
7519
+ import { spawn as spawn3, spawnSync as spawnSync5 } from "child_process";
7520
+ import { homedir as homedir9 } from "os";
7521
+ import { join as join14 } from "path";
6747
7522
  import ora9 from "ora";
6748
7523
  function assetForPlatform() {
6749
7524
  const { platform, arch } = process;
@@ -6751,37 +7526,39 @@ function assetForPlatform() {
6751
7526
  return { name: arch === "arm64" ? "Openship-arm64.dmg" : "Openship-x64.dmg", kind: "dmg" };
6752
7527
  }
6753
7528
  if (platform === "win32") return { name: "Openship-win32-x64.zip", kind: "zip" };
6754
- if (platform === "linux") return { name: "Openship.AppImage", kind: "appimage" };
7529
+ if (platform === "linux") {
7530
+ return { name: arch === "arm64" ? "Openship-arm64.AppImage" : "Openship.AppImage", kind: "appimage" };
7531
+ }
6755
7532
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
6756
7533
  }
6757
7534
  function installDmg(dmg) {
6758
- const homeApps = join12(homedir8(), "Applications");
7535
+ const homeApps = join14(homedir9(), "Applications");
6759
7536
  let dest = homeApps;
6760
7537
  try {
6761
- mkdirSync8(homeApps, { recursive: true });
7538
+ mkdirSync9(homeApps, { recursive: true });
6762
7539
  } catch {
6763
7540
  dest = "/Applications";
6764
7541
  }
6765
- const attach = spawnSync4("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
7542
+ const attach = spawnSync5("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
6766
7543
  encoding: "utf8"
6767
7544
  });
6768
7545
  if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
6769
7546
  const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
6770
7547
  if (!mount) throw new Error("Could not determine the mounted volume");
6771
- let target = join12(dest, "Openship.app");
7548
+ let target = join14(dest, "Openship.app");
6772
7549
  try {
6773
- const appInDmg = join12(mount, "Openship.app");
6774
- if (!existsSync10(appInDmg)) throw new Error("Openship.app not found in the disk image");
6775
- spawnSync4("rm", ["-rf", target]);
6776
- let copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
7550
+ const appInDmg = join14(mount, "Openship.app");
7551
+ if (!existsSync12(appInDmg)) throw new Error("Openship.app not found in the disk image");
7552
+ spawnSync5("rm", ["-rf", target]);
7553
+ let copy = spawnSync5("ditto", [appInDmg, target], { encoding: "utf8" });
6777
7554
  if (copy.status !== 0 && dest === homeApps) {
6778
- target = join12("/Applications", "Openship.app");
6779
- spawnSync4("rm", ["-rf", target]);
6780
- copy = spawnSync4("ditto", [appInDmg, target], { encoding: "utf8" });
7555
+ target = join14("/Applications", "Openship.app");
7556
+ spawnSync5("rm", ["-rf", target]);
7557
+ copy = spawnSync5("ditto", [appInDmg, target], { encoding: "utf8" });
6781
7558
  }
6782
7559
  if (copy.status !== 0) throw new Error(`ditto copy failed: ${copy.stderr?.trim()}`);
6783
7560
  } finally {
6784
- spawnSync4("hdiutil", ["detach", mount, "-quiet"]);
7561
+ spawnSync5("hdiutil", ["detach", mount, "-quiet"]);
6785
7562
  }
6786
7563
  return target;
6787
7564
  }
@@ -6790,10 +7567,10 @@ function installAppImage(appImage) {
6790
7567
  return appImage;
6791
7568
  }
6792
7569
  function installZip(zip) {
6793
- const localAppData = process.env.LOCALAPPDATA || join12(homedir8(), "AppData", "Local");
6794
- const target = join12(localAppData, "Programs", "Openship");
6795
- mkdirSync8(target, { recursive: true });
6796
- const expand = spawnSync4(
7570
+ const localAppData = process.env.LOCALAPPDATA || join14(homedir9(), "AppData", "Local");
7571
+ const target = join14(localAppData, "Programs", "Openship");
7572
+ mkdirSync9(target, { recursive: true });
7573
+ const expand = spawnSync5(
6797
7574
  "powershell",
6798
7575
  [
6799
7576
  "-NoProfile",
@@ -6808,22 +7585,22 @@ function installZip(zip) {
6808
7585
  }
6809
7586
  function launch(kind, target) {
6810
7587
  if (kind === "dmg") {
6811
- spawnSync4("open", [target]);
7588
+ spawnSync5("open", [target]);
6812
7589
  return;
6813
7590
  }
6814
7591
  if (kind === "appimage") {
6815
- const child = spawn2(target, [], { detached: true, stdio: "ignore" });
7592
+ const child = spawn3(target, [], { detached: true, stdio: "ignore" });
6816
7593
  child.on("error", () => {
6817
- spawn2(target, ["--appimage-extract-and-run"], { detached: true, stdio: "ignore" }).unref();
7594
+ spawn3(target, ["--appimage-extract-and-run"], { detached: true, stdio: "ignore" }).unref();
6818
7595
  });
6819
7596
  child.unref();
6820
7597
  return;
6821
7598
  }
6822
- const exe = join12(target, "Openship.exe");
6823
- const path2 = existsSync10(exe) ? exe : target;
6824
- spawnSync4("cmd", ["/c", "start", "", path2]);
7599
+ const exe = join14(target, "Openship.exe");
7600
+ const path2 = existsSync12(exe) ? exe : target;
7601
+ spawnSync5("cmd", ["/c", "start", "", path2]);
6825
7602
  }
6826
- var installCommand = new Command23("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) => {
7603
+ 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) => {
6827
7604
  let asset;
6828
7605
  try {
6829
7606
  asset = assetForPlatform();
@@ -6846,13 +7623,13 @@ var installCommand = new Command23("install").description("Download and install
6846
7623
  process.exit(1);
6847
7624
  }
6848
7625
  const dir = releaseDir(tag);
6849
- const assetPath = join12(dir, asset.name);
7626
+ const assetPath = join14(dir, asset.name);
6850
7627
  const sidecarPath = `${assetPath}.sha256`;
6851
7628
  const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
6852
7629
  const sidecarUrl = `${assetUrl2}.sha256`;
6853
7630
  let downloaded = false;
6854
7631
  let sha;
6855
- const cachedUsable = !opts.force && existsSync10(assetPath) && (existsSync10(sidecarPath) || opts.verify === false);
7632
+ const cachedUsable = !opts.force && existsSync12(assetPath) && (existsSync12(sidecarPath) || opts.verify === false);
6856
7633
  if (cachedUsable) {
6857
7634
  info(` Using cached ${asset.name} (${tag}).`);
6858
7635
  } else {
@@ -6874,8 +7651,8 @@ var installCommand = new Command23("install").description("Download and install
6874
7651
  const s2 = spin4("Verifying checksum\u2026");
6875
7652
  try {
6876
7653
  let sidecarBody;
6877
- if (existsSync10(sidecarPath) && !downloaded) {
6878
- sidecarBody = readFileSync11(sidecarPath, "utf8");
7654
+ if (existsSync12(sidecarPath) && !downloaded) {
7655
+ sidecarBody = readFileSync12(sidecarPath, "utf8");
6879
7656
  } else {
6880
7657
  sidecarBody = await fetchSidecar(sidecarUrl);
6881
7658
  }
@@ -6898,8 +7675,8 @@ var installCommand = new Command23("install").description("Download and install
6898
7675
  err(`Expected ${expected}, got ${actual}. The download may be corrupt or tampered with.`);
6899
7676
  process.exit(1);
6900
7677
  }
6901
- mkdirSync8(dir, { recursive: true });
6902
- writeFileSync8(sidecarPath, sidecarBody);
7678
+ mkdirSync9(dir, { recursive: true });
7679
+ writeFileSync9(sidecarPath, sidecarBody);
6903
7680
  s2?.succeed("Checksum verified");
6904
7681
  } catch (e) {
6905
7682
  s2?.fail("Verification failed");
@@ -6938,15 +7715,15 @@ var installCommand = new Command23("install").description("Download and install
6938
7715
  });
6939
7716
 
6940
7717
  // src/commands/update.ts
6941
- import { Command as Command24 } from "commander";
6942
- import { spawnSync as spawnSync5 } from "child_process";
7718
+ import { Command as Command25 } from "commander";
7719
+ import { spawnSync as spawnSync6 } from "child_process";
6943
7720
  function detectPackageManager2(override) {
6944
7721
  if (override === "bun" || override === "npm") return override;
6945
- const hasBun = spawnSync5("bun", ["--version"], { stdio: "ignore" }).status === 0;
7722
+ const hasBun = spawnSync6("bun", ["--version"], { stdio: "ignore" }).status === 0;
6946
7723
  return hasBun ? "bun" : "npm";
6947
7724
  }
6948
- var updateCommand = new Command24("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) => {
6949
- const current = "0.2.2";
7725
+ 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) => {
7726
+ const current = "0.3.0";
6950
7727
  let latest;
6951
7728
  try {
6952
7729
  latest = (await resolveLatestTag()).replace(/^v/, "");
@@ -6974,7 +7751,7 @@ var updateCommand = new Command24("update").description("Update the Openship CLI
6974
7751
  const ref = `openship@${latest}`;
6975
7752
  const argv = pm === "bun" ? ["add", "-g", ref] : ["install", "-g", ref];
6976
7753
  info(`Updating v${current} \u2192 v${latest} (${cliInstallCommand(pm, latest)})...`);
6977
- const res = spawnSync5(pm, argv, { stdio: "inherit" });
7754
+ const res = spawnSync6(pm, argv, { stdio: "inherit", shell: process.platform === "win32" });
6978
7755
  if (res.status !== 0) {
6979
7756
  err(`Update failed (${pm} exited ${res.status ?? "with a signal"}). Reinstall manually: ${cliInstallCommand(pm, latest)}`);
6980
7757
  process.exitCode = 1;
@@ -6991,18 +7768,18 @@ var updateCommand = new Command24("update").description("Update the Openship CLI
6991
7768
  });
6992
7769
 
6993
7770
  // src/commands/cache.ts
6994
- import { Command as Command25 } from "commander";
6995
- import { existsSync as existsSync11, readdirSync, readFileSync as readFileSync12, rmSync as rmSync4, statSync } from "fs";
6996
- import { join as join13 } from "path";
7771
+ import { Command as Command26 } from "commander";
7772
+ import { existsSync as existsSync13, readdirSync, readFileSync as readFileSync13, rmSync as rmSync4, statSync } from "fs";
7773
+ import { join as join15 } from "path";
6997
7774
  function listAssets() {
6998
- if (!existsSync11(RELEASES_DIR)) return [];
7775
+ if (!existsSync13(RELEASES_DIR)) return [];
6999
7776
  const out = [];
7000
7777
  for (const tag of readdirSync(RELEASES_DIR)) {
7001
7778
  const dir = releaseDir(tag);
7002
7779
  if (!statSync(dir).isDirectory()) continue;
7003
7780
  for (const name of readdirSync(dir)) {
7004
7781
  if (name.endsWith(".sha256")) continue;
7005
- const path2 = join13(dir, name);
7782
+ const path2 = join15(dir, name);
7006
7783
  const st = statSync(path2);
7007
7784
  if (!st.isFile()) continue;
7008
7785
  out.push({
@@ -7010,17 +7787,17 @@ function listAssets() {
7010
7787
  name,
7011
7788
  path: path2,
7012
7789
  size: st.size,
7013
- hasSidecar: existsSync11(`${path2}.sha256`)
7790
+ hasSidecar: existsSync13(`${path2}.sha256`)
7014
7791
  });
7015
7792
  }
7016
7793
  }
7017
7794
  return out;
7018
7795
  }
7019
- var pathCmd = new Command25("path").description("Print the cache directory path").action(() => {
7796
+ var pathCmd = new Command26("path").description("Print the cache directory path").action(() => {
7020
7797
  if (isJsonMode()) printJson({ path: CACHE_DIR });
7021
7798
  else process.stdout.write(CACHE_DIR + "\n");
7022
7799
  });
7023
- var listCmd6 = new Command25("list").alias("ls").description("List cached release assets").action(() => {
7800
+ var listCmd6 = new Command26("list").alias("ls").description("List cached release assets").action(() => {
7024
7801
  const assets = listAssets();
7025
7802
  printTable(
7026
7803
  assets.map((a) => ({
@@ -7032,7 +7809,7 @@ var listCmd6 = new Command25("list").alias("ls").description("List cached releas
7032
7809
  ["tag", "asset", "size", "sidecar"]
7033
7810
  );
7034
7811
  });
7035
- var verifyCmd2 = new Command25("verify").description("Re-hash cached assets and compare to their .sha256 sidecar").argument("[tag]", "Only verify assets under this release tag").action(async (tag) => {
7812
+ 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) => {
7036
7813
  const assets = listAssets().filter((a) => !tag || a.tag === tag);
7037
7814
  const results = [];
7038
7815
  let bad = 0;
@@ -7041,7 +7818,7 @@ var verifyCmd2 = new Command25("verify").description("Re-hash cached assets and
7041
7818
  results.push({ tag: a.tag, asset: a.name, result: "no-sidecar" });
7042
7819
  continue;
7043
7820
  }
7044
- const expected = parseSha256(readFileSync12(`${a.path}.sha256`, "utf8"));
7821
+ const expected = parseSha256(readFileSync13(`${a.path}.sha256`, "utf8"));
7045
7822
  const actual = await hashFile(a.path);
7046
7823
  const okMatch = expected !== null && expected === actual;
7047
7824
  if (!okMatch) bad += 1;
@@ -7055,9 +7832,9 @@ var verifyCmd2 = new Command25("verify").description("Re-hash cached assets and
7055
7832
  }
7056
7833
  if (bad > 0) process.exit(1);
7057
7834
  });
7058
- var cleanCmd = new Command25("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
7835
+ var cleanCmd = new Command26("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
7059
7836
  const target = tag ? releaseDir(tag) : RELEASES_DIR;
7060
- if (!existsSync11(target)) {
7837
+ if (!existsSync13(target)) {
7061
7838
  if (isJsonMode()) printJson({ removed: false, path: target });
7062
7839
  else info(` Nothing to clean (${target}).`);
7063
7840
  return;
@@ -7068,7 +7845,7 @@ var cleanCmd = new Command25("clean").description("Delete cached release assets"
7068
7845
  Removed ${target}
7069
7846
  `);
7070
7847
  });
7071
- var cacheCommand = new Command25("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
7848
+ var cacheCommand = new Command26("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
7072
7849
  err("Specify a subcommand: path | list | verify | clean");
7073
7850
  process.exit(1);
7074
7851
  }).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
@@ -7076,11 +7853,10 @@ var cacheCommand = new Command25("cache").description("Manage the local download
7076
7853
  // src/commands/wizard.ts
7077
7854
  import chalk17 from "chalk";
7078
7855
  import open from "open";
7079
- import { createServer as createServer2 } from "http";
7080
7856
  import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
7081
- import { existsSync as existsSync12, readFileSync as readFileSync13 } from "fs";
7082
- import { homedir as homedir9 } from "os";
7083
- import { join as join14 } from "path";
7857
+ import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
7858
+ import { homedir as homedir10 } from "os";
7859
+ import { join as join16 } from "path";
7084
7860
  import {
7085
7861
  intro as intro2,
7086
7862
  outro as outro2,
@@ -7135,10 +7911,10 @@ async function bootstrapAdmin(apiPort, admin) {
7135
7911
  }
7136
7912
  function lastServiceError() {
7137
7913
  for (const name of ["up.err.log", "up.log"]) {
7138
- const p = join14(homedir9(), ".openship", "logs", name);
7139
- if (!existsSync12(p)) continue;
7914
+ const p = join16(homedir10(), ".openship", "logs", name);
7915
+ if (!existsSync14(p)) continue;
7140
7916
  try {
7141
- const lines = readFileSync13(p, "utf8").trim().split("\n");
7917
+ const lines = readFileSync14(p, "utf8").trim().split("\n");
7142
7918
  const hit = [...lines].reverse().find((l) => /error|locked|EADDRINUSE|throw|cannot/i.test(l));
7143
7919
  if (hit) return hit.trim().slice(0, 200);
7144
7920
  } catch {
@@ -7198,58 +7974,46 @@ async function connectOpenshipCloud(port) {
7198
7974
  }
7199
7975
  const verifier = b64url(randomBytes2(32));
7200
7976
  const challenge = b64url(createHash2("sha256").update(verifier).digest());
7201
- const state = b64url(randomBytes2(16));
7202
- const codePromise = new Promise((resolve2) => {
7203
- const page = (ok3) => {
7204
- const icon = ok3 ? '<path d="M20 6 9 17l-5-5"/>' : '<path d="M18 6 6 18M6 6l12 12"/>';
7205
- const title = ok3 ? "Connected to Openship Cloud" : "Connection didn\u2019t complete";
7206
- const msg = ok3 ? "Your instance is now linked to your Openship Cloud account." : "Something went wrong. Return to your terminal and run the connect step again.";
7207
- return `<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Openship</title><style>:root{color-scheme:dark}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;background:#09090b;color:#e7e7ea;font:15px/1.55 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}.card{width:min(92vw,420px);padding:40px 36px;text-align:center}.badge{margin:0 auto 20px;display:grid;place-items:center}svg{width:40px;height:40px}h1{margin:0 0 8px;font-size:19px;font-weight:600;letter-spacing:-.2px}p{margin:0;color:#9a9aa2;font-size:14px}.hint{margin-top:22px;font-size:12.5px;color:#6a6a72}</style></head><body><div class="card"><div class="badge"><svg viewBox="0 0 24 24" fill="none" stroke="#e7e7ea" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">${icon}</svg></div><h1>${title}</h1><p>${msg}</p><div class="hint">You can close this tab and return to your terminal.</div></div><script>setTimeout(function(){try{window.close()}catch(e){}},1200)</script></body></html>`;
7208
- };
7209
- const server2 = createServer2((req, res2) => {
7210
- const u = new URL(req.url || "/", "http://127.0.0.1");
7211
- if (!u.pathname.startsWith("/callback")) {
7212
- res2.writeHead(404).end();
7213
- return;
7214
- }
7215
- const code2 = u.searchParams.get("code");
7216
- const gotState = u.searchParams.get("state");
7217
- const ok3 = !!(code2 && gotState === state);
7218
- res2.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }).end(page(ok3));
7219
- server2.close();
7220
- resolve2(ok3 ? code2 : null);
7221
- });
7222
- server2.on("error", () => resolve2(null));
7223
- server2.listen(0, "127.0.0.1", () => {
7224
- const cbPort = server2.address().port;
7225
- const redirect = `http://127.0.0.1:${cbPort}/callback`;
7226
- const handoff = `${cloudApiUrl.replace(/\/$/, "")}/api/cloud/connect-handoff?redirect=${encodeURIComponent(redirect)}&state=${state}&code_challenge=${challenge}`;
7227
- note(handoff, "Open this URL to authorize (opening your browser\u2026)");
7228
- void open(handoff).catch(() => {
7229
- });
7230
- });
7231
- setTimeout(() => {
7232
- try {
7233
- server2.close();
7234
- } catch {
7235
- }
7236
- resolve2(null);
7237
- }, 3e5);
7977
+ const state = b64url(randomBytes2(24));
7978
+ const apiBase = cloudApiUrl.replace(/\/$/, "");
7979
+ const handoff = `${apiBase}/api/cloud/connect-handoff?redirect=${encodeURIComponent(apiBase)}&state=${encodeURIComponent(state)}&code_challenge=${challenge}&mode=device`;
7980
+ const overSsh = !!(process.env.SSH_CONNECTION || process.env.SSH_TTY || process.env.SSH_CLIENT);
7981
+ note(handoff, "Open this URL in your browser to authorize (then click Authorize)");
7982
+ if (!overSsh) void open(handoff).catch(() => {
7238
7983
  });
7239
7984
  const s = spinner2();
7240
- s.start("Waiting for Openship Cloud authorization in your browser");
7241
- const code = await codePromise;
7985
+ s.start("Waiting for you to authorize in the browser");
7986
+ let code = null;
7987
+ const deadline = Date.now() + 3e5;
7988
+ while (Date.now() < deadline) {
7989
+ await new Promise((r) => setTimeout(r, 2500));
7990
+ try {
7991
+ const res2 = await fetch(
7992
+ `${apiBase}/api/cloud/connect-poll?state=${encodeURIComponent(state)}`,
7993
+ { signal: AbortSignal.timeout(5e3) }
7994
+ );
7995
+ if (!res2.ok) continue;
7996
+ const data = await res2.json();
7997
+ if (data.status === "ready" && data.code) {
7998
+ code = data.code;
7999
+ break;
8000
+ }
8001
+ } catch {
8002
+ }
8003
+ }
7242
8004
  if (!code) {
7243
- s.stop("Openship Cloud wasn't authorized.", 1);
8005
+ s.stop("Openship Cloud wasn't authorized in time \u2014 re-run the connect step to try again.", 1);
7244
8006
  return null;
7245
8007
  }
7246
- s.message("Linking this instance to Openship Cloud");
8008
+ s.stop("Authorized.");
8009
+ const linking = spinner2();
8010
+ linking.start("Linking this instance to Openship Cloud");
7247
8011
  const res = await internalPost(port, "/api/system/cloud-connect", { code, codeVerifier: verifier });
7248
8012
  if (!res.ok) {
7249
- s.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
8013
+ linking.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
7250
8014
  return null;
7251
8015
  }
7252
- s.stop(`Connected to Openship Cloud${res.data?.email ? ` as ${res.data.email}` : ""}.`);
8016
+ linking.stop(`Connected to Openship Cloud${res.data?.email ? ` as ${res.data.email}` : ""}.`);
7253
8017
  return { email: res.data?.email ?? null };
7254
8018
  }
7255
8019
  async function promptLocalAdmin() {
@@ -7269,12 +8033,13 @@ async function promptLocalAdmin() {
7269
8033
  }
7270
8034
  async function streamProvision(port, sessionId, s) {
7271
8035
  let ok3 = false;
8036
+ let detail;
7272
8037
  try {
7273
8038
  const res = await fetch(`http://127.0.0.1:${port}/api/system/self-register/stream?id=${sessionId}`, {
7274
8039
  headers: { "X-Internal-Token": ensureInternalToken() },
7275
8040
  signal: AbortSignal.timeout(3e5)
7276
8041
  });
7277
- if (!res.ok || !res.body) return false;
8042
+ if (!res.ok || !res.body) return { ok: false };
7278
8043
  const reader = res.body.getReader();
7279
8044
  const decoder = new TextDecoder();
7280
8045
  let buffer = "";
@@ -7292,23 +8057,29 @@ async function streamProvision(port, sessionId, s) {
7292
8057
  if (event === "log" && dataRaw) {
7293
8058
  try {
7294
8059
  const d = JSON.parse(dataRaw);
7295
- if (d.message) s.message(String(d.message).replace(/\s+/g, " ").slice(0, 68));
8060
+ if (d.message) {
8061
+ const msg = String(d.message).replace(/\s+/g, " ");
8062
+ s.message(msg.slice(0, 68));
8063
+ if (d.level === "warn" || d.level === "error") detail = msg;
8064
+ }
7296
8065
  } catch {
7297
8066
  }
7298
8067
  } else if (event === "complete" && dataRaw) {
7299
8068
  try {
7300
- ok3 = JSON.parse(dataRaw).status === "completed";
8069
+ const d = JSON.parse(dataRaw);
8070
+ ok3 = d.status === "completed";
8071
+ if (!ok3 && typeof d.error === "string") detail = d.error;
7301
8072
  } catch {
7302
8073
  }
7303
8074
  } else if (event === "end") {
7304
- return ok3;
8075
+ return { ok: ok3, detail };
7305
8076
  }
7306
8077
  }
7307
8078
  }
7308
8079
  } catch {
7309
- return ok3;
8080
+ return { ok: ok3, detail };
7310
8081
  }
7311
- return ok3;
8082
+ return { ok: ok3, detail };
7312
8083
  }
7313
8084
  async function runWizard() {
7314
8085
  intro2(`${chalk17.bgCyan(chalk17.black(" Openship "))}${chalk17.dim(" setup")}`);
@@ -7513,7 +8284,7 @@ async function runWizard() {
7513
8284
  domainPlan = { type: "byo", hostname };
7514
8285
  break planning;
7515
8286
  }
7516
- const uiTag = `v${"0.2.2"}`;
8287
+ const uiTag = `v${"0.3.0"}`;
7517
8288
  const dl = spinner2();
7518
8289
  dl.start("Pulling the Openship dist from GitHub");
7519
8290
  try {
@@ -7632,6 +8403,20 @@ async function runWizard() {
7632
8403
  if (status && !status.canProceedClean && status.occupants?.length) {
7633
8404
  const owner = status.occupants.map((o) => o.command ?? `port ${o.port}`).join(", ");
7634
8405
  const known = status.classification === "known";
8406
+ const sites = pf.ok && Array.isArray(pf.data?.sites) ? pf.data.sites : [];
8407
+ if (sites.length > 0) {
8408
+ const lines = sites.map((st) => {
8409
+ const host = (st.serverNames ?? []).join(", ") || "(no server_name)";
8410
+ const dest = st.target?.kind === "static" ? `static: ${st.target?.root ?? ""}` : st.target?.url ?? "";
8411
+ return `${chalk17.bold(host)} \u2192 ${chalk17.dim(dest)}${st.ssl ? chalk17.green(" [TLS]") : ""}`;
8412
+ });
8413
+ note(lines.join("\n"), `Detected ${sites.length} site${sites.length === 1 ? "" : "s"} on ${owner}`);
8414
+ }
8415
+ const warns = pf.ok && Array.isArray(pf.data?.warnings) ? pf.data.warnings : [];
8416
+ if (warns.length > 0) {
8417
+ log2.warn(`${warns.length} config item${warns.length === 1 ? "" : "s"} won't migrate automatically:`);
8418
+ for (const w of warns.slice(0, 8)) log2.message(chalk17.dim(`\u2022 ${w}`));
8419
+ }
7635
8420
  const choice = ensure(
7636
8421
  await select({
7637
8422
  message: known ? `An existing reverse proxy (${owner}) is serving ports 80/443.` : `Ports 80/443 are in use by ${owner}, which we couldn't identify.`,
@@ -7678,10 +8463,13 @@ async function runWizard() {
7678
8463
  if (res.ok && res.data?.sessionId) {
7679
8464
  const s2 = spinner2();
7680
8465
  s2.start("Issuing HTTPS certificate (OpenResty + Let's Encrypt)");
7681
- const done = await streamProvision(port, res.data.sessionId, s2);
8466
+ const { ok: done, detail } = await streamProvision(port, res.data.sessionId, s2);
7682
8467
  liveUrl = res.data.url ?? liveUrl;
7683
8468
  if (done) s2.stop(`HTTPS ready: ${liveUrl}`);
7684
- else s2.stop("HTTPS isn't ready yet \u2014 it retries on reboot; the site serves over HTTP meanwhile.", 1);
8469
+ else {
8470
+ s2.stop("HTTPS isn't ready yet \u2014 it retries on reboot; the site serves over HTTP meanwhile.", 1);
8471
+ if (detail) log2.warn(detail);
8472
+ }
7685
8473
  } else {
7686
8474
  log2.warn(`Couldn't start domain provisioning: ${res.data?.error || "failed"}`);
7687
8475
  }
@@ -7714,9 +8502,9 @@ ${pad("Login")}${admin.email} ${chalk17.dim("(email + password you set)")}
7714
8502
  );
7715
8503
  }
7716
8504
  function storedPorts() {
7717
- const p = join14(homedir9(), ".openship", "ports.json");
8505
+ const p = join16(homedir10(), ".openship", "ports.json");
7718
8506
  try {
7719
- return existsSync12(p) ? JSON.parse(readFileSync13(p, "utf8")) : {};
8507
+ return existsSync14(p) ? JSON.parse(readFileSync14(p, "utf8")) : {};
7720
8508
  } catch {
7721
8509
  return {};
7722
8510
  }
@@ -7788,8 +8576,8 @@ ${chalk17.dim("Dashboard".padEnd(11))}${dashUrl}
7788
8576
  }
7789
8577
 
7790
8578
  // src/index.ts
7791
- var program = new Command26();
7792
- program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.2.2").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
8579
+ var program = new Command27();
8580
+ program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.3.0").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
7793
8581
  if (thisCommand.opts().json) setJsonMode(true);
7794
8582
  }).action(async () => {
7795
8583
  if (serviceStatus().installed) await runControl();
@@ -7803,6 +8591,7 @@ program.addCommand(openCommand);
7803
8591
  program.addCommand(loginCommand);
7804
8592
  program.addCommand(logoutCommand);
7805
8593
  program.addCommand(initCommand);
8594
+ program.addCommand(configCommand);
7806
8595
  program.addCommand(contextCommand);
7807
8596
  program.addCommand(statusCommand);
7808
8597
  program.addCommand(doctorCommand);