openship 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { Command as Command26 } from "commander";
4
+ import { Command as Command27 } from "commander";
5
5
 
6
6
  // src/lib/output.ts
7
7
  import chalk from "chalk";
@@ -1807,6 +1807,425 @@ var LANGUAGE_MANIFEST_FILES = Array.from(
1807
1807
  new Set(LANGUAGE_DETECTORS.flatMap((d) => d.manifestFiles.map((f) => f.toLowerCase())))
1808
1808
  );
1809
1809
 
1810
+ // ../../packages/core/src/openship-config/schema.ts
1811
+ var OPENSHIP_RUNTIMES = ["bare", "docker"];
1812
+ var OPENSHIP_PRODUCTION_MODES = [
1813
+ "host",
1814
+ "static",
1815
+ "standalone"
1816
+ ];
1817
+ var OPENSHIP_DOMAIN_TYPES = ["free", "custom"];
1818
+ var OPENSHIP_RESTARTS = [
1819
+ "no",
1820
+ "always",
1821
+ "on-failure",
1822
+ "unless-stopped"
1823
+ ];
1824
+ var OPENSHIP_RESOURCE_TIERS = [
1825
+ "micro",
1826
+ "low",
1827
+ "medium",
1828
+ "high"
1829
+ ];
1830
+
1831
+ // ../../packages/core/src/openship-config/parse.ts
1832
+ var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
1833
+ "$schema",
1834
+ "framework",
1835
+ "packageManager",
1836
+ "rootDirectory",
1837
+ "installCommand",
1838
+ "buildCommand",
1839
+ "startCommand",
1840
+ "outputDirectory",
1841
+ "buildImage",
1842
+ "productionPaths",
1843
+ "runtime",
1844
+ "productionMode",
1845
+ "port",
1846
+ "env",
1847
+ "domains",
1848
+ "routes",
1849
+ "resources",
1850
+ "services",
1851
+ "monorepo"
1852
+ ]);
1853
+ var Ctx = class {
1854
+ errors = [];
1855
+ warnings = [];
1856
+ err(path2, msg) {
1857
+ this.errors.push(`${path2}: ${msg}`);
1858
+ }
1859
+ isObj(v) {
1860
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1861
+ }
1862
+ str(v, path2) {
1863
+ if (v === void 0) return void 0;
1864
+ if (typeof v !== "string") {
1865
+ this.err(path2, "must be a string");
1866
+ return void 0;
1867
+ }
1868
+ return v;
1869
+ }
1870
+ bool(v, path2) {
1871
+ if (v === void 0) return void 0;
1872
+ if (typeof v !== "boolean") {
1873
+ this.err(path2, "must be a boolean");
1874
+ return void 0;
1875
+ }
1876
+ return v;
1877
+ }
1878
+ int(v, path2, min, max) {
1879
+ if (v === void 0) return void 0;
1880
+ const n = typeof v === "string" ? Number(v) : v;
1881
+ if (typeof n !== "number" || !Number.isFinite(n)) {
1882
+ this.err(path2, "must be a number");
1883
+ return void 0;
1884
+ }
1885
+ if (n < min || n > max) {
1886
+ this.err(path2, `must be between ${min} and ${max}`);
1887
+ return void 0;
1888
+ }
1889
+ return n;
1890
+ }
1891
+ strArray(v, path2) {
1892
+ if (v === void 0) return void 0;
1893
+ if (!Array.isArray(v)) {
1894
+ this.err(path2, "must be an array of strings");
1895
+ return void 0;
1896
+ }
1897
+ const out = [];
1898
+ v.forEach((item, i) => {
1899
+ if (typeof item !== "string") this.err(`${path2}[${i}]`, "must be a string");
1900
+ else out.push(item);
1901
+ });
1902
+ return out;
1903
+ }
1904
+ enumOf(v, path2, allowed) {
1905
+ if (v === void 0) return void 0;
1906
+ if (typeof v !== "string" || !allowed.includes(v)) {
1907
+ this.err(path2, `must be one of: ${allowed.join(", ")}`);
1908
+ return void 0;
1909
+ }
1910
+ return v;
1911
+ }
1912
+ };
1913
+ function parseEnv(ctx, v, path2) {
1914
+ if (v === void 0) return void 0;
1915
+ if (!ctx.isObj(v)) {
1916
+ ctx.err(path2, "must be an object of environment variables");
1917
+ return void 0;
1918
+ }
1919
+ const out = {};
1920
+ for (const [key, val] of Object.entries(v)) {
1921
+ if (typeof val === "string") {
1922
+ out[key] = val;
1923
+ } else if (ctx.isObj(val)) {
1924
+ if (val.value === void 0) ctx.err(`${path2}.${key}.value`, "is required");
1925
+ const value = ctx.str(val.value, `${path2}.${key}.value`);
1926
+ const secret = ctx.bool(val.secret, `${path2}.${key}.secret`);
1927
+ if (value !== void 0) out[key] = { value, ...secret !== void 0 ? { secret } : {} };
1928
+ } else {
1929
+ ctx.err(`${path2}.${key}`, 'must be a string or { "value", "secret" }');
1930
+ }
1931
+ }
1932
+ return out;
1933
+ }
1934
+ function parseDomains(ctx, v, path2) {
1935
+ if (v === void 0) return void 0;
1936
+ if (!Array.isArray(v)) {
1937
+ ctx.err(path2, "must be an array of hostnames or domain objects");
1938
+ return void 0;
1939
+ }
1940
+ const out = [];
1941
+ v.forEach((item, i) => {
1942
+ const p = `${path2}[${i}]`;
1943
+ if (typeof item === "string") {
1944
+ out.push({ domain: item });
1945
+ } else if (ctx.isObj(item)) {
1946
+ const domain = ctx.str(item.domain, `${p}.domain`);
1947
+ if (!domain) {
1948
+ ctx.err(p, "requires a `domain`");
1949
+ return;
1950
+ }
1951
+ out.push({
1952
+ domain,
1953
+ port: ctx.int(item.port, `${p}.port`, 1, 65535),
1954
+ targetPath: ctx.str(item.targetPath, `${p}.targetPath`),
1955
+ type: ctx.enumOf(item.type, `${p}.type`, OPENSHIP_DOMAIN_TYPES)
1956
+ });
1957
+ } else {
1958
+ ctx.err(p, "must be a hostname string or a domain object");
1959
+ }
1960
+ });
1961
+ return out;
1962
+ }
1963
+ function parseRoutes(ctx, v, path2) {
1964
+ if (v === void 0) return void 0;
1965
+ if (!ctx.isObj(v)) {
1966
+ ctx.err(path2, "must be an object");
1967
+ return void 0;
1968
+ }
1969
+ const routes = {};
1970
+ const rule = (item, p) => {
1971
+ if (!ctx.isObj(item)) {
1972
+ ctx.err(p, "must be an object with `source` and `destination`");
1973
+ return null;
1974
+ }
1975
+ const source = ctx.str(item.source, `${p}.source`);
1976
+ const destination = ctx.str(item.destination, `${p}.destination`);
1977
+ return source && destination ? { source, destination } : null;
1978
+ };
1979
+ if (Array.isArray(v.rewrites)) {
1980
+ routes.rewrites = v.rewrites.map((r, i) => rule(r, `${path2}.rewrites[${i}]`)).filter(Boolean);
1981
+ } else if (v.rewrites !== void 0) ctx.err(`${path2}.rewrites`, "must be an array");
1982
+ if (Array.isArray(v.redirects)) {
1983
+ routes.redirects = v.redirects.map((r, i) => {
1984
+ const base = rule(r, `${path2}.redirects[${i}]`);
1985
+ if (!base) return null;
1986
+ const o = r;
1987
+ return {
1988
+ ...base,
1989
+ permanent: ctx.bool(o.permanent, `${path2}.redirects[${i}].permanent`),
1990
+ statusCode: ctx.int(o.statusCode, `${path2}.redirects[${i}].statusCode`, 300, 399)
1991
+ };
1992
+ }).filter(Boolean);
1993
+ } else if (v.redirects !== void 0) ctx.err(`${path2}.redirects`, "must be an array");
1994
+ if (Array.isArray(v.headers)) {
1995
+ routes.headers = v.headers.map((h, i) => {
1996
+ const p = `${path2}.headers[${i}]`;
1997
+ if (!ctx.isObj(h)) {
1998
+ ctx.err(p, "must be an object");
1999
+ return null;
2000
+ }
2001
+ const source = ctx.str(h.source, `${p}.source`);
2002
+ const list2 = Array.isArray(h.headers) ? h.headers.map((kv, j) => {
2003
+ const key = ctx.str(kv?.key, `${p}.headers[${j}].key`);
2004
+ const value = ctx.str(kv?.value, `${p}.headers[${j}].value`);
2005
+ return key && value !== void 0 ? { key, value } : null;
2006
+ }).filter(Boolean) : [];
2007
+ return source ? { source, headers: list2 } : null;
2008
+ }).filter(Boolean);
2009
+ } else if (v.headers !== void 0) ctx.err(`${path2}.headers`, "must be an array");
2010
+ const cleanUrls = ctx.bool(v.cleanUrls, `${path2}.cleanUrls`);
2011
+ const trailingSlash = ctx.bool(v.trailingSlash, `${path2}.trailingSlash`);
2012
+ if (cleanUrls !== void 0) routes.cleanUrls = cleanUrls;
2013
+ if (trailingSlash !== void 0) routes.trailingSlash = trailingSlash;
2014
+ return routes;
2015
+ }
2016
+ function parseResources(ctx, v, path2) {
2017
+ if (v === void 0) return void 0;
2018
+ if (!ctx.isObj(v)) {
2019
+ ctx.err(path2, "must be an object");
2020
+ return void 0;
2021
+ }
2022
+ const r = {
2023
+ tier: ctx.enumOf(v.tier, `${path2}.tier`, OPENSHIP_RESOURCE_TIERS),
2024
+ cpuCores: ctx.int(v.cpuCores, `${path2}.cpuCores`, 0.25, 4),
2025
+ memoryMb: ctx.int(v.memoryMb, `${path2}.memoryMb`, 128, 8192),
2026
+ diskMb: ctx.int(v.diskMb, `${path2}.diskMb`, 64, 204800)
2027
+ };
2028
+ return r;
2029
+ }
2030
+ function parseHealthcheck(ctx, v, path2) {
2031
+ if (v === void 0) return void 0;
2032
+ if (!ctx.isObj(v)) {
2033
+ ctx.err(path2, "must be an object");
2034
+ return void 0;
2035
+ }
2036
+ const test = typeof v.test === "string" ? v.test : Array.isArray(v.test) ? ctx.strArray(v.test, `${path2}.test`) : v.test !== void 0 ? (ctx.err(`${path2}.test`, "must be a string or array of strings"), void 0) : void 0;
2037
+ return {
2038
+ test,
2039
+ interval: ctx.str(v.interval, `${path2}.interval`),
2040
+ timeout: ctx.str(v.timeout, `${path2}.timeout`),
2041
+ retries: ctx.int(v.retries, `${path2}.retries`, 0, 100),
2042
+ startPeriod: ctx.str(v.startPeriod, `${path2}.startPeriod`),
2043
+ disable: ctx.bool(v.disable, `${path2}.disable`)
2044
+ };
2045
+ }
2046
+ function parseServices(ctx, v, path2) {
2047
+ if (v === void 0) return void 0;
2048
+ if (!Array.isArray(v)) {
2049
+ ctx.err(path2, "must be an array of service objects");
2050
+ return void 0;
2051
+ }
2052
+ const out = [];
2053
+ v.forEach((item, i) => {
2054
+ const p = `${path2}[${i}]`;
2055
+ if (!ctx.isObj(item)) {
2056
+ ctx.err(p, "must be an object");
2057
+ return;
2058
+ }
2059
+ const name = ctx.str(item.name, `${p}.name`);
2060
+ if (!name) {
2061
+ ctx.err(p, "requires a `name`");
2062
+ return;
2063
+ }
2064
+ out.push({
2065
+ name,
2066
+ image: ctx.str(item.image, `${p}.image`),
2067
+ build: ctx.str(item.build, `${p}.build`),
2068
+ dockerfile: ctx.str(item.dockerfile, `${p}.dockerfile`),
2069
+ ports: ctx.strArray(item.ports, `${p}.ports`),
2070
+ volumes: ctx.strArray(item.volumes, `${p}.volumes`),
2071
+ dependsOn: ctx.strArray(item.dependsOn, `${p}.dependsOn`),
2072
+ env: parseEnv(ctx, item.env, `${p}.env`),
2073
+ command: ctx.str(item.command, `${p}.command`),
2074
+ restart: ctx.enumOf(item.restart, `${p}.restart`, OPENSHIP_RESTARTS),
2075
+ exposed: ctx.bool(item.exposed, `${p}.exposed`),
2076
+ exposedPort: ctx.str(item.exposedPort, `${p}.exposedPort`),
2077
+ domain: ctx.str(item.domain, `${p}.domain`),
2078
+ healthcheck: parseHealthcheck(ctx, item.healthcheck, `${p}.healthcheck`)
2079
+ });
2080
+ });
2081
+ return out;
2082
+ }
2083
+ function parseMonorepo(ctx, v, path2) {
2084
+ if (v === void 0) return void 0;
2085
+ if (!ctx.isObj(v)) {
2086
+ ctx.err(path2, "must be an object");
2087
+ return void 0;
2088
+ }
2089
+ const mono = {};
2090
+ if (v.workspace !== void 0) {
2091
+ if (!ctx.isObj(v.workspace)) ctx.err(`${path2}.workspace`, "must be an object");
2092
+ else {
2093
+ const pm = ctx.str(v.workspace.packageManager, `${path2}.workspace.packageManager`);
2094
+ if (pm) {
2095
+ mono.workspace = {
2096
+ packageManager: pm,
2097
+ prepareCommand: ctx.str(v.workspace.prepareCommand, `${path2}.workspace.prepareCommand`)
2098
+ };
2099
+ } else {
2100
+ ctx.err(`${path2}.workspace`, "requires a `packageManager`");
2101
+ }
2102
+ }
2103
+ }
2104
+ if (v.sharedPaths !== void 0) {
2105
+ ctx.warnings.push(`${path2}.sharedPaths is not applied yet (ignored)`);
2106
+ }
2107
+ if (v.apps !== void 0) {
2108
+ if (!Array.isArray(v.apps)) ctx.err(`${path2}.apps`, "must be an array");
2109
+ else {
2110
+ const apps = [];
2111
+ v.apps.forEach((a, i) => {
2112
+ const p = `${path2}.apps[${i}]`;
2113
+ if (!ctx.isObj(a)) {
2114
+ ctx.err(p, "must be an object");
2115
+ return;
2116
+ }
2117
+ const name = ctx.str(a.name, `${p}.name`);
2118
+ const rootDirectory = ctx.str(a.rootDirectory, `${p}.rootDirectory`);
2119
+ if (!name || !rootDirectory) {
2120
+ ctx.err(p, "requires `name` and `rootDirectory`");
2121
+ return;
2122
+ }
2123
+ apps.push({
2124
+ name,
2125
+ rootDirectory,
2126
+ framework: ctx.enumOf(a.framework, `${p}.framework`, STACK_IDS),
2127
+ packageManager: parsePackageManager(ctx, a.packageManager, `${p}.packageManager`),
2128
+ installCommand: ctx.str(a.installCommand, `${p}.installCommand`),
2129
+ buildCommand: ctx.str(a.buildCommand, `${p}.buildCommand`),
2130
+ startCommand: ctx.str(a.startCommand, `${p}.startCommand`),
2131
+ outputDirectory: ctx.str(a.outputDirectory, `${p}.outputDirectory`),
2132
+ buildImage: ctx.str(a.buildImage, `${p}.buildImage`),
2133
+ port: ctx.int(a.port, `${p}.port`, 1, 65535)
2134
+ });
2135
+ });
2136
+ mono.apps = apps;
2137
+ }
2138
+ }
2139
+ return mono;
2140
+ }
2141
+ function parsePackageManager(ctx, v, path2) {
2142
+ const s = ctx.str(v, path2);
2143
+ if (s === void 0) return void 0;
2144
+ if (!ALL_PACKAGE_MANAGERS.includes(s)) {
2145
+ ctx.err(path2, `must be one of: ${ALL_PACKAGE_MANAGERS.join(", ")}`);
2146
+ return void 0;
2147
+ }
2148
+ return s;
2149
+ }
2150
+ function parseOpenshipConfig(raw) {
2151
+ const ctx = new Ctx();
2152
+ if (!ctx.isObj(raw)) {
2153
+ return { config: null, errors: ["openship.json must be a JSON object"], warnings: [] };
2154
+ }
2155
+ for (const key of Object.keys(raw)) {
2156
+ if (!TOP_LEVEL_KEYS.has(key)) ctx.warnings.push(`Unknown field "${key}" (ignored)`);
2157
+ }
2158
+ const config = {
2159
+ framework: ctx.enumOf(raw.framework, "framework", STACK_IDS),
2160
+ packageManager: parsePackageManager(ctx, raw.packageManager, "packageManager"),
2161
+ rootDirectory: ctx.str(raw.rootDirectory, "rootDirectory"),
2162
+ installCommand: ctx.str(raw.installCommand, "installCommand"),
2163
+ buildCommand: ctx.str(raw.buildCommand, "buildCommand"),
2164
+ startCommand: ctx.str(raw.startCommand, "startCommand"),
2165
+ outputDirectory: ctx.str(raw.outputDirectory, "outputDirectory"),
2166
+ buildImage: ctx.str(raw.buildImage, "buildImage"),
2167
+ productionPaths: ctx.strArray(raw.productionPaths, "productionPaths"),
2168
+ runtime: ctx.enumOf(raw.runtime, "runtime", OPENSHIP_RUNTIMES),
2169
+ productionMode: ctx.enumOf(raw.productionMode, "productionMode", OPENSHIP_PRODUCTION_MODES),
2170
+ port: ctx.int(raw.port, "port", 1, 65535),
2171
+ env: parseEnv(ctx, raw.env, "env"),
2172
+ domains: parseDomains(ctx, raw.domains, "domains"),
2173
+ routes: parseRoutes(ctx, raw.routes, "routes"),
2174
+ resources: parseResources(ctx, raw.resources, "resources"),
2175
+ services: parseServices(ctx, raw.services, "services"),
2176
+ monorepo: parseMonorepo(ctx, raw.monorepo, "monorepo")
2177
+ };
2178
+ for (const k of Object.keys(config)) {
2179
+ if (config[k] === void 0) delete config[k];
2180
+ }
2181
+ return { config, errors: ctx.errors, warnings: ctx.warnings };
2182
+ }
2183
+ function parseOpenshipConfigJson(text2) {
2184
+ let raw;
2185
+ try {
2186
+ raw = JSON.parse(text2);
2187
+ } catch (err2) {
2188
+ return {
2189
+ config: null,
2190
+ errors: [`invalid JSON: ${err2 instanceof Error ? err2.message : String(err2)}`],
2191
+ warnings: []
2192
+ };
2193
+ }
2194
+ return parseOpenshipConfig(raw);
2195
+ }
2196
+
2197
+ // ../../packages/core/src/metadata/openship.ts
2198
+ var openshipMetadataParser = {
2199
+ source: "openship",
2200
+ files: ["openship.json"],
2201
+ parse(fileContents) {
2202
+ const raw = fileContents["openship.json"];
2203
+ if (!raw) return null;
2204
+ const { config } = parseOpenshipConfigJson(raw);
2205
+ if (!config) return null;
2206
+ const metadata = { source: "openship" };
2207
+ if (config.installCommand) metadata.installCommand = config.installCommand;
2208
+ if (config.buildCommand) metadata.buildCommand = config.buildCommand;
2209
+ if (config.outputDirectory) metadata.outputDirectory = config.outputDirectory;
2210
+ if (config.startCommand) metadata.startCommand = config.startCommand;
2211
+ if (config.framework) metadata.framework = config.framework;
2212
+ if (config.routes) metadata.routing = config.routes;
2213
+ const hasSignal = config.installCommand || config.buildCommand || config.outputDirectory || config.startCommand || config.framework || config.routes;
2214
+ return hasSignal ? metadata : null;
2215
+ }
2216
+ };
2217
+
2218
+ // ../../packages/core/src/metadata/text.ts
2219
+ function stripBom2(content) {
2220
+ return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
2221
+ }
2222
+ function trimmed(value) {
2223
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2224
+ }
2225
+ function splitLines(raw) {
2226
+ return stripBom2(raw).split(/\r?\n/);
2227
+ }
2228
+
1810
2229
  // ../../packages/core/src/metadata/vercel.ts
1811
2230
  var VERCEL_FRAMEWORK_TO_STACK = {
1812
2231
  nextjs: "nextjs",
@@ -1821,9 +2240,6 @@ var VERCEL_FRAMEWORK_TO_STACK = {
1821
2240
  angular: "angular",
1822
2241
  "create-react-app": "cra"
1823
2242
  };
1824
- function trimmed(value) {
1825
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1826
- }
1827
2243
  function isConditional(entry) {
1828
2244
  return "has" in entry || "missing" in entry;
1829
2245
  }
@@ -1942,10 +2358,152 @@ var vercelMetadataParser = {
1942
2358
  }
1943
2359
  };
1944
2360
 
1945
- // ../../packages/core/src/metadata/render.ts
1946
- function stripBom2(content) {
1947
- return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
2361
+ // ../../packages/core/src/metadata/railway.ts
2362
+ function tomlScalar(rest) {
2363
+ const s = rest.trim();
2364
+ const quote = s[0];
2365
+ if (quote === '"' || quote === "'") {
2366
+ let out = "";
2367
+ for (let i = 1; i < s.length; i++) {
2368
+ const ch = s[i];
2369
+ if (quote === '"' && ch === "\\" && i + 1 < s.length) {
2370
+ const next = s[++i];
2371
+ switch (next) {
2372
+ case "t":
2373
+ out += " ";
2374
+ break;
2375
+ case "n":
2376
+ out += "\n";
2377
+ break;
2378
+ case "r":
2379
+ out += "\r";
2380
+ break;
2381
+ case "b":
2382
+ out += "\b";
2383
+ break;
2384
+ case "f":
2385
+ out += "\f";
2386
+ break;
2387
+ case '"':
2388
+ out += '"';
2389
+ break;
2390
+ case "\\":
2391
+ out += "\\";
2392
+ break;
2393
+ case "/":
2394
+ out += "/";
2395
+ break;
2396
+ case "u":
2397
+ case "U": {
2398
+ const width = next === "u" ? 4 : 8;
2399
+ const hex = s.slice(i + 1, i + 1 + width);
2400
+ if (hex.length === width && /^[0-9a-fA-F]+$/.test(hex)) {
2401
+ out += String.fromCodePoint(parseInt(hex, 16));
2402
+ i += width;
2403
+ } else {
2404
+ out += next;
2405
+ }
2406
+ break;
2407
+ }
2408
+ default:
2409
+ out += next;
2410
+ }
2411
+ continue;
2412
+ }
2413
+ if (ch === quote) return out;
2414
+ out += ch;
2415
+ }
2416
+ return void 0;
2417
+ }
2418
+ const bare = s.split("#")[0].trim();
2419
+ return bare.length > 0 ? bare : void 0;
2420
+ }
2421
+ function parseRailwayToml(raw) {
2422
+ const cfg = {};
2423
+ let section = "";
2424
+ const lines = splitLines(raw);
2425
+ for (let i = 0; i < lines.length; i++) {
2426
+ const line = lines[i];
2427
+ const header = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
2428
+ if (header) {
2429
+ section = header[1].trim().toLowerCase();
2430
+ continue;
2431
+ }
2432
+ const kv = line.match(/^\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.+)$/);
2433
+ if (!kv) continue;
2434
+ const [, rawKey, rest] = kv;
2435
+ const dot = rawKey.lastIndexOf(".");
2436
+ const table = dot >= 0 ? rawKey.slice(0, dot).toLowerCase() : section;
2437
+ const field = dot >= 0 ? rawKey.slice(dot + 1) : rawKey;
2438
+ let value;
2439
+ const triple = rest.match(/^("""|''')/);
2440
+ if (triple) {
2441
+ const delim = triple[1];
2442
+ const afterOpen = rest.slice(3);
2443
+ const close = afterOpen.indexOf(delim);
2444
+ if (close >= 0) {
2445
+ value = afterOpen.slice(0, close).trim() || void 0;
2446
+ } else {
2447
+ while (++i < lines.length && !lines[i].includes(delim)) {
2448
+ }
2449
+ value = void 0;
2450
+ }
2451
+ } else {
2452
+ value = tomlScalar(rest);
2453
+ }
2454
+ if (!value) continue;
2455
+ if (table === "build") {
2456
+ if (field === "buildCommand") cfg.buildCommand ??= value;
2457
+ else if (field === "builder") cfg.builder ??= value;
2458
+ } else if (table === "deploy") {
2459
+ if (field === "startCommand") cfg.startCommand ??= value;
2460
+ }
2461
+ }
2462
+ return cfg;
2463
+ }
2464
+ function parseRailwayJson(raw) {
2465
+ let parsed;
2466
+ try {
2467
+ parsed = JSON.parse(raw);
2468
+ } catch {
2469
+ return null;
2470
+ }
2471
+ if (typeof parsed !== "object" || parsed === null) return null;
2472
+ const obj = parsed;
2473
+ const build = obj.build ?? {};
2474
+ const deploy = obj.deploy ?? {};
2475
+ return {
2476
+ builder: trimmed(build.builder),
2477
+ buildCommand: trimmed(build.buildCommand),
2478
+ startCommand: trimmed(deploy.startCommand)
2479
+ };
1948
2480
  }
2481
+ function toMetadata(cfg) {
2482
+ const buildCommand = trimmed(cfg.buildCommand);
2483
+ const startCommand = trimmed(cfg.startCommand);
2484
+ const framework = cfg.builder?.toUpperCase() === "DOCKERFILE" ? "docker" : void 0;
2485
+ if (!buildCommand && !startCommand && !framework) return null;
2486
+ const metadata = { source: "railway" };
2487
+ if (buildCommand) metadata.buildCommand = buildCommand;
2488
+ if (startCommand) metadata.startCommand = startCommand;
2489
+ if (framework) metadata.framework = framework;
2490
+ if (extractCdTargets(buildCommand).length > 0) metadata.nonLocal = true;
2491
+ return metadata;
2492
+ }
2493
+ var railwayMetadataParser = {
2494
+ source: "railway",
2495
+ files: ["railway.toml", "railway.json"],
2496
+ parse(fileContents) {
2497
+ const tomlRaw = fileContents["railway.toml"];
2498
+ const jsonRaw = fileContents["railway.json"];
2499
+ const fromToml = tomlRaw ? toMetadata(parseRailwayToml(tomlRaw)) : null;
2500
+ if (fromToml) return fromToml;
2501
+ const jsonCfg = jsonRaw ? parseRailwayJson(jsonRaw) : null;
2502
+ return jsonCfg ? toMetadata(jsonCfg) : null;
2503
+ }
2504
+ };
2505
+
2506
+ // ../../packages/core/src/metadata/render.ts
1949
2507
  function unquote(value) {
1950
2508
  const trimmed2 = value.trim();
1951
2509
  const m = trimmed2.match(/^(['"])(.*)\1$/);
@@ -1957,7 +2515,7 @@ var renderMetadataParser = {
1957
2515
  parse(fileContents) {
1958
2516
  const raw = fileContents["render.yaml"];
1959
2517
  if (!raw) return null;
1960
- const lines = stripBom2(raw).split("\n");
2518
+ const lines = splitLines(raw);
1961
2519
  let startCommand;
1962
2520
  let buildCommand;
1963
2521
  const env = {};
@@ -1991,7 +2549,9 @@ var renderMetadataParser = {
1991
2549
 
1992
2550
  // ../../packages/core/src/metadata/index.ts
1993
2551
  var METADATA_PARSERS = [
2552
+ openshipMetadataParser,
1994
2553
  vercelMetadataParser,
2554
+ railwayMetadataParser,
1995
2555
  renderMetadataParser
1996
2556
  ];
1997
2557
  var METADATA_FILES = new Set(
@@ -2378,11 +2938,11 @@ var openCommand = new Command3("open").description("Open the Openship dashboard
2378
2938
  import { Command as Command4 } from "commander";
2379
2939
  import chalk5 from "chalk";
2380
2940
  import ora from "ora";
2381
- import { spawn } from "child_process";
2941
+ import { spawn as spawn2 } from "child_process";
2382
2942
  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";
2943
+ import { createWriteStream as createWriteStream2, existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
2944
+ import { homedir as homedir6 } from "os";
2945
+ import { dirname as dirname2, join as join7 } from "path";
2386
2946
  import { fileURLToPath } from "url";
2387
2947
 
2388
2948
  // src/lib/dashboard.ts
@@ -2432,8 +2992,8 @@ async function downloadToFile(url, dest, onProgress) {
2432
2992
  } finally {
2433
2993
  file.end();
2434
2994
  }
2435
- await new Promise((resolve2, reject2) => {
2436
- file.on("finish", () => resolve2());
2995
+ await new Promise((resolve3, reject2) => {
2996
+ file.on("finish", () => resolve3());
2437
2997
  file.on("error", reject2);
2438
2998
  });
2439
2999
  return { sha256: hash.digest("hex"), size: received };
@@ -2838,10 +3398,10 @@ function readInstanceUrl() {
2838
3398
  var DEFAULT_API = 4e3;
2839
3399
  var DEFAULT_DASHBOARD = 3001;
2840
3400
  function isPortFree(port) {
2841
- return new Promise((resolve2) => {
3401
+ return new Promise((resolve3) => {
2842
3402
  const srv = createServer();
2843
- srv.once("error", () => resolve2(false));
2844
- srv.listen(port, "127.0.0.1", () => srv.close(() => resolve2(true)));
3403
+ srv.once("error", () => resolve3(false));
3404
+ srv.listen(port, "127.0.0.1", () => srv.close(() => resolve3(true)));
2845
3405
  });
2846
3406
  }
2847
3407
  async function waitPortFree(port, opts = {}) {
@@ -2855,13 +3415,13 @@ async function waitPortFree(port, opts = {}) {
2855
3415
  }
2856
3416
  }
2857
3417
  function getFreePort() {
2858
- return new Promise((resolve2, reject2) => {
3418
+ return new Promise((resolve3, reject2) => {
2859
3419
  const srv = createServer();
2860
3420
  srv.once("error", reject2);
2861
3421
  srv.listen(0, "127.0.0.1", () => {
2862
3422
  const addr = srv.address();
2863
3423
  const port = addr && typeof addr === "object" ? addr.port : 0;
2864
- srv.close(() => port ? resolve2(port) : reject2(new Error("no free port")));
3424
+ srv.close(() => port ? resolve3(port) : reject2(new Error("no free port")));
2865
3425
  });
2866
3426
  });
2867
3427
  }
@@ -2902,6 +3462,101 @@ async function resolvePorts(prefs) {
2902
3462
  };
2903
3463
  }
2904
3464
 
3465
+ // src/lib/from-source.ts
3466
+ import { spawn, spawnSync as spawnSync3 } from "child_process";
3467
+ import { existsSync as existsSync5, mkdirSync as mkdirSync6 } from "fs";
3468
+ import { homedir as homedir5 } from "os";
3469
+ import { join as join6, resolve as resolve2 } from "path";
3470
+ var OS_DIR3 = join6(homedir5(), ".openship");
3471
+ var DEFAULT_REPO = "https://github.com/oblien/openship.git";
3472
+ function has(cmd) {
3473
+ try {
3474
+ return spawnSync3(cmd, ["--version"], { stdio: "ignore" }).status === 0;
3475
+ } catch {
3476
+ return false;
3477
+ }
3478
+ }
3479
+ function run2(cmd, args, cwd, env) {
3480
+ return new Promise((res, rej) => {
3481
+ const child = spawn(cmd, args, {
3482
+ cwd,
3483
+ stdio: "inherit",
3484
+ env: env ? { ...process.env, ...env } : process.env
3485
+ });
3486
+ child.on("error", rej);
3487
+ child.on(
3488
+ "exit",
3489
+ (code) => code === 0 ? res() : rej(new Error(`\`${cmd} ${args.join(" ")}\` (cwd=${cwd}) exited ${code ?? "?"}`))
3490
+ );
3491
+ });
3492
+ }
3493
+ function shortSha(cwd) {
3494
+ const r = spawnSync3("git", ["rev-parse", "--short", "HEAD"], { cwd, encoding: "utf8" });
3495
+ return r.status === 0 ? (r.stdout ?? "").trim() || "unknown" : "unknown";
3496
+ }
3497
+ function isMonorepo(dir) {
3498
+ return existsSync5(join6(dir, "package.json")) && existsSync5(join6(dir, "apps/api/package.json")) && existsSync5(join6(dir, "apps/dashboard/package.json"));
3499
+ }
3500
+ async function prepareFromSource(opts) {
3501
+ if (!has("bun")) {
3502
+ throw new Error(
3503
+ "`bun` is required to build from source but wasn't found on PATH \u2014 install it: https://bun.sh"
3504
+ );
3505
+ }
3506
+ let sourceDir;
3507
+ let ref;
3508
+ if (opts.source) {
3509
+ sourceDir = resolve2(opts.source);
3510
+ if (!isMonorepo(sourceDir)) {
3511
+ throw new Error(
3512
+ `--source ${sourceDir} doesn't look like an Openship checkout (missing package.json / apps/api / apps/dashboard).`
3513
+ );
3514
+ }
3515
+ ref = "local";
3516
+ } else {
3517
+ if (!has("git")) {
3518
+ throw new Error("`git` is required to clone the source but wasn't found on PATH.");
3519
+ }
3520
+ ref = (opts.ref || "main").trim();
3521
+ const repoUrl = opts.repo || DEFAULT_REPO;
3522
+ sourceDir = join6(OS_DIR3, "src");
3523
+ mkdirSync6(OS_DIR3, { recursive: true });
3524
+ if (!existsSync5(join6(sourceDir, ".git"))) {
3525
+ console.log(` Cloning ${repoUrl} \u2192 ${sourceDir}`);
3526
+ await run2("git", ["clone", repoUrl, sourceDir], OS_DIR3);
3527
+ }
3528
+ console.log(` Fetching + checking out ${ref}`);
3529
+ await run2("git", ["fetch", "origin", ref, "--tags"], sourceDir);
3530
+ await run2("git", ["checkout", ref], sourceDir);
3531
+ await run2("git", ["pull", "--ff-only", "origin", ref], sourceDir).catch(() => {
3532
+ console.log(" (pinned ref \u2014 not fast-forwarding)");
3533
+ });
3534
+ }
3535
+ const sha = shortSha(sourceDir);
3536
+ console.log(` Source: ${sourceDir} @ ${ref} (${sha})`);
3537
+ console.log(" Installing workspace dependencies (bun install)\u2026");
3538
+ await run2("bun", ["install"], sourceDir);
3539
+ const distDir = join6(OS_DIR3, "from-source-dist");
3540
+ console.log(" Building release dist (compiles the dashboard \u2014 needs RAM/CPU)\u2026");
3541
+ await run2(
3542
+ "bun",
3543
+ ["run", join6(sourceDir, "apps/api/scripts/build-release.ts")],
3544
+ sourceDir,
3545
+ { DIST_DIR: distDir, NODE_ENV: "production", CLOUD_MODE: "false", OPENSHIP_TARGET: "local" }
3546
+ );
3547
+ console.log(" Installing runtime dependencies in the dist\u2026");
3548
+ await run2("bun", ["install", "--production", "--frozen-lockfile"], distDir);
3549
+ const apiDir = join6(distDir, "api");
3550
+ const dashboardDir = join6(distDir, "dashboard");
3551
+ if (!existsSync5(join6(apiDir, "src/index.ts"))) {
3552
+ throw new Error(`Build produced no API at ${apiDir}/src/index.ts \u2014 build-release layout drift?`);
3553
+ }
3554
+ if (!existsSync5(join6(dashboardDir, "apps/dashboard/server.js"))) {
3555
+ throw new Error(`Build produced no dashboard at ${dashboardDir}/apps/dashboard/server.js.`);
3556
+ }
3557
+ return { apiDir, dashboardDir, ref, sha, sourceDir };
3558
+ }
3559
+
2905
3560
  // src/commands/up.ts
2906
3561
  function normalizeUrl(raw) {
2907
3562
  const value = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
@@ -2924,20 +3579,20 @@ function normalizePublicUrl(raw) {
2924
3579
  return url;
2925
3580
  }
2926
3581
  var DIST_DIR = dirname2(fileURLToPath(import.meta.url));
2927
- var SERVER_DIR = join6(DIST_DIR, "server");
2928
- var OS_DIR3 = join6(homedir5(), ".openship");
3582
+ var SERVER_DIR = join7(DIST_DIR, "server");
3583
+ var OS_DIR4 = join7(homedir6(), ".openship");
2929
3584
  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 });
3585
+ const path2 = join7(OS_DIR4, "auth-secret");
3586
+ if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
3587
+ mkdirSync7(OS_DIR4, { recursive: true, mode: 448 });
2933
3588
  const secret = randomBytes(32).toString("hex");
2934
3589
  writeFileSync5(path2, secret, { mode: 384 });
2935
3590
  return secret;
2936
3591
  }
2937
3592
  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 });
3593
+ const path2 = join7(OS_DIR4, "internal-token");
3594
+ if (existsSync6(path2)) return readFileSync4(path2, "utf8").trim();
3595
+ mkdirSync7(OS_DIR4, { recursive: true, mode: 448 });
2941
3596
  const token = randomBytes(32).toString("hex");
2942
3597
  writeFileSync5(path2, token, { mode: 384 });
2943
3598
  return token;
@@ -2951,10 +3606,32 @@ var upCommand = new Command4("up").description("Start Openship as a persistent s
2951
3606
  ).option(
2952
3607
  "--managed-edge",
2953
3608
  "Managed edge: install OpenResty + a free Let's Encrypt cert on this box and route --public-url's domain to the dashboard (no reverse proxy needed)"
2954
- ).option("--acme-email <email>", "Contact email for Let's Encrypt certificates (managed edge)").action(async (opts) => {
3609
+ ).option("--acme-email <email>", "Contact email for Let's Encrypt certificates (managed edge)").option("--from-source", "Preview: build + run Openship from source (a branch) instead of a published release \u2014 runs attached").option("--ref <branch>", "Git branch/tag/sha to build with --from-source (default: main)").option("--source <path>", "Build from an existing local Openship checkout instead of cloning").option("--repo <url>", "Git remote to clone for --from-source (default: oblien/openship)").action(async (opts) => {
3610
+ if (opts.fromSource || opts.source) return runFromSource(opts);
2955
3611
  if (opts.foreground) return runForeground(opts);
2956
3612
  await startService(opts);
2957
3613
  });
3614
+ async function runFromSource(opts) {
3615
+ console.log(chalk5.cyan("\n Building Openship from source (preview mode)\u2026"));
3616
+ console.log(
3617
+ chalk5.dim(" Unverified dev build \u2014 for previewing a branch, not production self-hosting.\n")
3618
+ );
3619
+ let src;
3620
+ try {
3621
+ src = await prepareFromSource({ ref: opts.ref, source: opts.source, repo: opts.repo });
3622
+ } catch (e) {
3623
+ console.error(
3624
+ chalk5.red(`
3625
+ Build from source failed: ${e.message}
3626
+ `) + chalk5.dim(" Small boxes can OOM on the dashboard build \u2014 build on a bigger machine and pass --source, or use a published release.\n")
3627
+ );
3628
+ process.exit(1);
3629
+ }
3630
+ console.log(chalk5.green(`
3631
+ Built ${src.ref} (${src.sha}). Starting\u2026
3632
+ `));
3633
+ await runForeground(opts, src);
3634
+ }
2958
3635
  async function startService(opts, runOpts = {}) {
2959
3636
  const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
2960
3637
  if (opts.dryRun) {
@@ -3029,13 +3706,23 @@ async function startService(opts, runOpts = {}) {
3029
3706
  process.exit(1);
3030
3707
  }
3031
3708
  }
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);
3709
+ async function runForeground(opts, source) {
3710
+ let apiCmd = process.execPath;
3711
+ let apiArgs;
3712
+ let apiCwd;
3713
+ if (source) {
3714
+ apiCmd = "bun";
3715
+ apiArgs = ["run", "src/index.ts"];
3716
+ apiCwd = source.apiDir;
3717
+ } else {
3718
+ const serverEntry = join7(SERVER_DIR, "index.js");
3719
+ if (!existsSync6(serverEntry)) {
3720
+ console.error(
3721
+ chalk5.red("\n Bundled server not found in this install.") + chalk5.dim("\n Reinstall with `openship update` (or `npm i -g openship`).\n")
3722
+ );
3723
+ process.exit(1);
3724
+ }
3725
+ apiArgs = [serverEntry];
3039
3726
  }
3040
3727
  const resolved = await resolvePorts({
3041
3728
  api: opts.port ? Number(opts.port) : void 0,
@@ -3045,11 +3732,11 @@ async function runForeground(opts) {
3045
3732
  const dashPort = String(resolved.dashboard);
3046
3733
  const publicUrl = opts.publicUrl ? normalizePublicUrl(opts.publicUrl) : void 0;
3047
3734
  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");
3735
+ const dataDir = opts.dataDir || join7(OS_DIR4, "data");
3736
+ mkdirSync7(dataDir, { recursive: true });
3737
+ const logDir = join7(OS_DIR4, "logs");
3738
+ mkdirSync7(logDir, { recursive: true });
3739
+ const instanceLogPath = join7(logDir, "instance.log");
3053
3740
  const instanceLog = createWriteStream2(instanceLogPath, { flags: "w" });
3054
3741
  const env = {
3055
3742
  ...process.env,
@@ -3060,10 +3747,12 @@ async function runForeground(opts) {
3060
3747
  OPENSHIP_TARGET: "local",
3061
3748
  OPENSHIP_JOB_RUNNER: "in-process",
3062
3749
  PGLITE_DATA_DIR: dataDir,
3063
- OPENSHIP_MIGRATIONS_DIR: join6(SERVER_DIR, "migrations"),
3064
- OPENSHIP_PGLITE_ASSETS_DIR: join6(SERVER_DIR, "pglite"),
3065
3750
  BETTER_AUTH_SECRET: ensureAuthSecret()
3066
3751
  };
3752
+ if (!source) {
3753
+ env.OPENSHIP_MIGRATIONS_DIR = join7(SERVER_DIR, "migrations");
3754
+ env.OPENSHIP_PGLITE_ASSETS_DIR = join7(SERVER_DIR, "pglite");
3755
+ }
3067
3756
  env.OPENSHIP_REQUIRE_AUTH = "true";
3068
3757
  env.INTERNAL_TOKEN = ensureInternalToken();
3069
3758
  env.OPENSHIP_API_HOST = "127.0.0.1";
@@ -3082,7 +3771,8 @@ async function runForeground(opts) {
3082
3771
  delete env.DATABASE_URL;
3083
3772
  delete env.POSTGRES_URL;
3084
3773
  const spinner3 = ora(`Starting Openship on http://localhost:${port} \u2026`).start();
3085
- const child = spawn(process.execPath, [serverEntry], {
3774
+ const child = spawn2(apiCmd, apiArgs, {
3775
+ cwd: apiCwd,
3086
3776
  env,
3087
3777
  stdio: ["ignore", "pipe", "pipe"],
3088
3778
  detached: process.platform !== "win32"
@@ -3146,10 +3836,11 @@ async function runForeground(opts) {
3146
3836
  };
3147
3837
  let dashboardUrl = null;
3148
3838
  if (opts.ui !== false) {
3839
+ if (source) process.env.OPENSHIP_DASHBOARD_DIR = source.dashboardDir;
3149
3840
  const uiSpinner = ora("Preparing the dashboard\u2026").start();
3150
3841
  try {
3151
3842
  const bundle = await ensureDashboard({
3152
- tag: opts.uiVersion || `v${"0.2.2"}`,
3843
+ tag: source ? "local" : opts.uiVersion || `v${"0.2.3"}`,
3153
3844
  onProgress: (received, total) => {
3154
3845
  if (total) {
3155
3846
  uiSpinner.text = `Downloading dashboard\u2026 ${Math.round(received / total * 100)}%`;
@@ -3157,7 +3848,7 @@ async function runForeground(opts) {
3157
3848
  }
3158
3849
  });
3159
3850
  uiSpinner.text = "Starting the dashboard\u2026";
3160
- const dash = spawn(process.execPath, [bundle.entry], {
3851
+ const dash = spawn2(process.execPath, [bundle.entry], {
3161
3852
  cwd: bundle.cwd,
3162
3853
  detached: process.platform !== "win32",
3163
3854
  env: {
@@ -3271,15 +3962,15 @@ var stopCommand = new Command5("stop").description("Stop the Openship service (s
3271
3962
 
3272
3963
  // src/commands/init.ts
3273
3964
  import { Command as Command6 } from "commander";
3274
- import { existsSync as existsSync6, mkdirSync as mkdirSync7, writeFileSync as writeFileSync6 } from "fs";
3965
+ import { existsSync as existsSync7, mkdirSync as mkdirSync8, writeFileSync as writeFileSync6 } from "fs";
3275
3966
  import { createInterface as createInterface2 } from "readline/promises";
3276
3967
  import { stdin as input2, stdout as output2 } from "process";
3277
- import { join as join7 } from "path";
3968
+ import { join as join8 } from "path";
3278
3969
  var initCommand = new Command6("init").description("Link the current directory to an Openship project (.openship/project.json)").option("--project <id>", "Project id to link (skips the picker)").option("--environment <name>", "Default deploy environment", "production").option("--dir <path>", "Directory to initialize", process.cwd()).option("--force", "Overwrite an existing project link").option("-y, --yes", "Non-interactive: fail instead of prompting").action(async (opts) => {
3279
3970
  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) {
3971
+ const linkDir = join8(root, ".openship");
3972
+ const linkPath = join8(linkDir, "project.json");
3973
+ if (existsSync7(linkPath) && !opts.force) {
3283
3974
  err(`Already linked (${linkPath}). Re-run with --force to overwrite.`);
3284
3975
  process.exit(1);
3285
3976
  }
@@ -3332,7 +4023,7 @@ var initCommand = new Command6("init").description("Link the current directory t
3332
4023
  context: getActiveContext(),
3333
4024
  defaults: { environment: opts.environment || "production" }
3334
4025
  };
3335
- mkdirSync7(linkDir, { recursive: true });
4026
+ mkdirSync8(linkDir, { recursive: true });
3336
4027
  writeFileSync6(linkPath, JSON.stringify(link, null, 2) + "\n");
3337
4028
  if (isJsonMode()) {
3338
4029
  printJson({ path: linkPath, link });
@@ -3343,8 +4034,86 @@ var initCommand = new Command6("init").description("Link the current directory t
3343
4034
  `);
3344
4035
  });
3345
4036
 
3346
- // src/commands/context.ts
4037
+ // src/commands/config.ts
3347
4038
  import { Command as Command7 } from "commander";
4039
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync7 } from "fs";
4040
+ import { join as join9 } from "path";
4041
+ var SCHEMA_URL = "https://openship.io/openship.schema.json";
4042
+ var CONFIG_FILE = "openship.json";
4043
+ function detectHints(dir) {
4044
+ const hints = {};
4045
+ const lock = [
4046
+ ["pnpm-lock.yaml", "pnpm"],
4047
+ ["yarn.lock", "yarn"],
4048
+ ["bun.lockb", "bun"],
4049
+ ["package-lock.json", "npm"]
4050
+ ];
4051
+ for (const [file, pm] of lock) {
4052
+ if (existsSync8(join9(dir, file))) {
4053
+ hints.packageManager = pm;
4054
+ break;
4055
+ }
4056
+ }
4057
+ const pkgPath = join9(dir, "package.json");
4058
+ if (existsSync8(pkgPath)) {
4059
+ try {
4060
+ const pkg = JSON.parse(readFileSync5(pkgPath, "utf8"));
4061
+ const runner = hints.packageManager ?? "npm";
4062
+ const exec = runner === "npm" ? "npm run" : runner;
4063
+ if (pkg.scripts?.build) hints.buildCommand = `${exec} build`;
4064
+ if (pkg.scripts?.start) hints.startCommand = `${exec} start`;
4065
+ } catch {
4066
+ }
4067
+ }
4068
+ return hints;
4069
+ }
4070
+ var initCmd = new Command7("init").description("Scaffold an openship.json in the current directory").option("--dir <path>", "Directory to write into", process.cwd()).option("--force", "Overwrite an existing openship.json").action((opts) => {
4071
+ const dir = opts.dir || process.cwd();
4072
+ const path2 = join9(dir, CONFIG_FILE);
4073
+ if (existsSync8(path2) && !opts.force) {
4074
+ err(`${CONFIG_FILE} already exists. Re-run with --force to overwrite.`);
4075
+ process.exit(1);
4076
+ }
4077
+ const hints = detectHints(dir);
4078
+ const scaffold = { $schema: SCHEMA_URL, ...hints };
4079
+ const text2 = JSON.stringify(scaffold, null, 2) + "\n";
4080
+ writeFileSync7(path2, text2);
4081
+ if (isJsonMode()) {
4082
+ printJson({ path: path2, config: scaffold });
4083
+ return;
4084
+ }
4085
+ ok(`
4086
+ Wrote ${CONFIG_FILE} \u2192 ${path2}`);
4087
+ info(" Edit it to declare framework, env, domains, resources, services\u2026 then `openship config validate`.\n");
4088
+ });
4089
+ var validateCmd = new Command7("validate").description("Validate an openship.json against the deploy schema").argument("[file]", "Path to the config file", CONFIG_FILE).action((file) => {
4090
+ const path2 = file === CONFIG_FILE ? join9(process.cwd(), CONFIG_FILE) : file;
4091
+ if (!existsSync8(path2)) {
4092
+ if (isJsonMode()) printJson({ valid: false, errors: [`${path2} not found`], warnings: [] });
4093
+ else err(`Not found: ${path2}`);
4094
+ process.exit(1);
4095
+ }
4096
+ const { config, errors, warnings } = parseOpenshipConfigJson(readFileSync5(path2, "utf8"));
4097
+ const valid = errors.length === 0 && config !== null;
4098
+ if (isJsonMode()) {
4099
+ printJson({ valid, errors, warnings });
4100
+ process.exit(valid ? 0 : 1);
4101
+ }
4102
+ for (const w of warnings) info(` \u26A0 ${w}`);
4103
+ if (!valid) {
4104
+ err(`
4105
+ ${errors.length} error${errors.length === 1 ? "" : "s"} in ${CONFIG_FILE}:`);
4106
+ for (const e of errors) err(` \u2022 ${e}`);
4107
+ process.exit(1);
4108
+ }
4109
+ ok(`
4110
+ ${CONFIG_FILE} is valid${warnings.length ? ` (${warnings.length} warning${warnings.length === 1 ? "" : "s"})` : ""}.
4111
+ `);
4112
+ });
4113
+ var configCommand = new Command7("config").description("Author and validate openship.json (declarative deploy config)").addCommand(initCmd).addCommand(validateCmd);
4114
+
4115
+ // src/commands/context.ts
4116
+ import { Command as Command8 } from "commander";
3348
4117
  function renderContexts() {
3349
4118
  const rows = listContexts().map((c) => ({
3350
4119
  current: c.current ? "*" : "",
@@ -3355,8 +4124,8 @@ function renderContexts() {
3355
4124
  }));
3356
4125
  printTable(rows, ["current", "name", "apiUrl", "dashboardUrl", "auth"]);
3357
4126
  }
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) => {
4127
+ var listCmd = new Command8("list").alias("ls").description("List configured contexts").action(renderContexts);
4128
+ var useCmd = new Command8("use").description("Switch the active context").argument("<name>", "Context name").action((name) => {
3360
4129
  try {
3361
4130
  setActiveContext(name);
3362
4131
  ok(`
@@ -3367,7 +4136,7 @@ var useCmd = new Command7("use").description("Switch the active context").argume
3367
4136
  process.exit(1);
3368
4137
  }
3369
4138
  });
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) => {
4139
+ var addCmd = new Command8("add").description("Create or update a context's endpoints/token").argument("<name>", "Context name").option("--api-url <url>", "API base URL").option("--dashboard-url <url>", "Dashboard base URL").option("--token <token>", "Personal Access Token to store").option("--use", "Switch to this context after adding").action((name, opts) => {
3371
4140
  addContext(name, {
3372
4141
  apiUrl: opts.apiUrl,
3373
4142
  dashboardUrl: opts.dashboardUrl,
@@ -3378,7 +4147,7 @@ var addCmd = new Command7("add").description("Create or update a context's endpo
3378
4147
  Saved context "${name}"${opts.use ? " (now active)" : ""}.
3379
4148
  `);
3380
4149
  });
3381
- var rmCmd = new Command7("rm").alias("remove").description("Remove a context (cannot remove the active one)").argument("<name>", "Context name").action((name) => {
4150
+ var rmCmd = new Command8("rm").alias("remove").description("Remove a context (cannot remove the active one)").argument("<name>", "Context name").action((name) => {
3382
4151
  try {
3383
4152
  removeContext(name);
3384
4153
  ok(`
@@ -3389,25 +4158,25 @@ var rmCmd = new Command7("rm").alias("remove").description("Remove a context (ca
3389
4158
  process.exit(1);
3390
4159
  }
3391
4160
  });
3392
- var contextCommand = new Command7("context").alias("ctx").description("Manage connection contexts (list/use/add/rm)").action(() => {
4161
+ var contextCommand = new Command8("context").alias("ctx").description("Manage connection contexts (list/use/add/rm)").action(() => {
3393
4162
  ok(` Active context: ${getActiveContext()}`);
3394
4163
  renderContexts();
3395
4164
  }).addCommand(listCmd).addCommand(useCmd).addCommand(addCmd).addCommand(rmCmd);
3396
4165
 
3397
4166
  // src/commands/status.ts
3398
- import { Command as Command8 } from "commander";
4167
+ import { Command as Command9 } from "commander";
3399
4168
  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";
4169
+ import { readFileSync as readFileSync6 } from "fs";
4170
+ import { homedir as homedir7 } from "os";
4171
+ import { join as join10 } from "path";
3403
4172
  function readPorts() {
3404
4173
  try {
3405
- return JSON.parse(readFileSync5(join8(homedir6(), ".openship", "ports.json"), "utf8"));
4174
+ return JSON.parse(readFileSync6(join10(homedir7(), ".openship", "ports.json"), "utf8"));
3406
4175
  } catch {
3407
4176
  return {};
3408
4177
  }
3409
4178
  }
3410
- var statusCommand = new Command8("status").description("Show the local Openship service (installed/running, ports) and the active context's API health").action(async () => {
4179
+ var statusCommand = new Command9("status").description("Show the local Openship service (installed/running, ports) and the active context's API health").action(async () => {
3411
4180
  const context = getActiveContext();
3412
4181
  const apiUrl = getApiUrl2();
3413
4182
  const svc = serviceStatus();
@@ -3442,8 +4211,8 @@ var statusCommand = new Command8("status").description("Show the local Openship
3442
4211
  });
3443
4212
 
3444
4213
  // src/commands/doctor.ts
3445
- import { Command as Command9 } from "commander";
3446
- import { existsSync as existsSync7 } from "fs";
4214
+ import { Command as Command10 } from "commander";
4215
+ import { existsSync as existsSync9 } from "fs";
3447
4216
  import { execFileSync } from "child_process";
3448
4217
  import chalk8 from "chalk";
3449
4218
  function bunVersion() {
@@ -3455,9 +4224,9 @@ function bunVersion() {
3455
4224
  return null;
3456
4225
  }
3457
4226
  }
3458
- var doctorCommand = new Command9("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
4227
+ var doctorCommand = new Command10("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
3459
4228
  const checks = [];
3460
- const hasConfig = existsSync7(CONFIG_PATH);
4229
+ const hasConfig = existsSync9(CONFIG_PATH);
3461
4230
  checks.push({
3462
4231
  name: "config",
3463
4232
  status: hasConfig ? "pass" : "warn",
@@ -3505,20 +4274,20 @@ var doctorCommand = new Command9("doctor").description("Diagnose the CLI setup (
3505
4274
  });
3506
4275
 
3507
4276
  // src/commands/deploy.ts
3508
- import { Command as Command10 } from "commander";
4277
+ import { Command as Command11 } from "commander";
3509
4278
  import { execFileSync as execFileSync3 } from "child_process";
3510
4279
  import ora2 from "ora";
3511
4280
 
3512
4281
  // 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");
4282
+ import { readFileSync as readFileSync7, existsSync as existsSync10 } from "fs";
4283
+ import { join as join11, dirname as dirname3, parse } from "path";
4284
+ var LINK_REL = join11(".openship", "project.json");
3516
4285
  function findProjectLinkPath(from = process.cwd()) {
3517
4286
  let dir = from;
3518
4287
  const root = parse(dir).root;
3519
4288
  for (; ; ) {
3520
- const candidate = join9(dir, LINK_REL);
3521
- if (existsSync8(candidate)) return candidate;
4289
+ const candidate = join11(dir, LINK_REL);
4290
+ if (existsSync10(candidate)) return candidate;
3522
4291
  if (dir === root) return null;
3523
4292
  dir = dirname3(dir);
3524
4293
  }
@@ -3527,7 +4296,7 @@ function readProjectLink(from) {
3527
4296
  const path2 = findProjectLinkPath(from);
3528
4297
  if (!path2) return null;
3529
4298
  try {
3530
- return JSON.parse(readFileSync6(path2, "utf8"));
4299
+ return JSON.parse(readFileSync7(path2, "utf8"));
3531
4300
  } catch {
3532
4301
  return null;
3533
4302
  }
@@ -3535,21 +4304,21 @@ function readProjectLink(from) {
3535
4304
 
3536
4305
  // src/lib/folder-deploy.ts
3537
4306
  import { execFileSync as execFileSync2 } from "child_process";
3538
- import { readFileSync as readFileSync7, existsSync as existsSync9, rmSync as rmSync3 } from "fs";
4307
+ import { readFileSync as readFileSync8, existsSync as existsSync11, rmSync as rmSync3 } from "fs";
3539
4308
  import { tmpdir } from "os";
3540
- import { join as join10, basename } from "path";
4309
+ import { join as join12, basename } from "path";
3541
4310
  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";
4311
+ if (existsSync11(join12(dir, "bun.lockb")) || existsSync11(join12(dir, "bun.lock"))) return "bun";
4312
+ if (existsSync11(join12(dir, "pnpm-lock.yaml"))) return "pnpm";
4313
+ if (existsSync11(join12(dir, "yarn.lock"))) return "yarn";
4314
+ if (existsSync11(join12(dir, "package.json"))) return "npm";
3546
4315
  return void 0;
3547
4316
  }
3548
4317
  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";
4318
+ if (existsSync11(join12(dir, "go.mod"))) return "go";
4319
+ if (existsSync11(join12(dir, "Cargo.toml"))) return "rust";
4320
+ if (existsSync11(join12(dir, "requirements.txt")) || existsSync11(join12(dir, "pyproject.toml"))) return "python";
4321
+ if (existsSync11(join12(dir, "package.json"))) return "node";
3553
4322
  return void 0;
3554
4323
  }
3555
4324
  async function deployFolder(opts) {
@@ -3566,7 +4335,7 @@ async function deployFolder(opts) {
3566
4335
  throw new Error(session.error || "Failed to open upload session");
3567
4336
  }
3568
4337
  step("Packaging folder");
3569
- const tarball = join10(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
4338
+ const tarball = join12(tmpdir(), `openship-upload-${session.sessionId}.tar.gz`);
3570
4339
  execFileSync2(
3571
4340
  "tar",
3572
4341
  [
@@ -3585,7 +4354,7 @@ async function deployFolder(opts) {
3585
4354
  );
3586
4355
  step("Uploading source");
3587
4356
  try {
3588
- const body = readFileSync7(tarball);
4357
+ const body = readFileSync8(tarball);
3589
4358
  const up = session.upload;
3590
4359
  const method = up.method || "POST";
3591
4360
  const res = /^https?:\/\//i.test(up.url) ? await fetch(up.url, { method, headers: up.headers, body }) : await apiRaw(`/${up.url.replace(/^\/+/, "")}`, { method, headers: up.headers, body });
@@ -3766,7 +4535,7 @@ function git(args) {
3766
4535
  return void 0;
3767
4536
  }
3768
4537
  }
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) => {
4538
+ var deployCommand = new Command11("deploy").description("Trigger a deployment for the current project").option("--project <id>", "Project ID (defaults to the linked project in .openship/project.json)").option("--branch <name>", "Git branch to deploy (defaults to the current branch)").option("--commit <sha>", "Specific commit SHA (defaults to the latest commit on the branch)").option("--env <environment>", "Target environment: production | preview", "production").option("--force-all", "Rebuild every enabled service (skip smart per-service routing)").option("--service-ids <ids>", "Comma-separated service IDs to deploy (smart routing)").option("--smart-route", "Rebuild only services changed since the active deploy").option("--refresh", "Re-apply current env to the active deploy (no git pull, no rebuild)").option("--name <name>", "Project name for a folder (non-git) deploy (defaults to the directory name)").option("--watch", "Stream the deployment logs until it finishes").action(async (opts) => {
3770
4539
  const link = readProjectLink();
3771
4540
  const env = opts.env;
3772
4541
  if (env !== "production" && env !== "preview") {
@@ -3848,9 +4617,9 @@ var deployCommand = new Command10("deploy").description("Trigger a deployment fo
3848
4617
  });
3849
4618
 
3850
4619
  // src/commands/deployment.ts
3851
- import { Command as Command11 } from "commander";
4620
+ import { Command as Command12 } from "commander";
3852
4621
  import { createInterface as createInterface3 } from "readline";
3853
- function run2(fn) {
4622
+ function run3(fn) {
3854
4623
  return async (...args) => {
3855
4624
  try {
3856
4625
  await fn(...args);
@@ -3864,18 +4633,18 @@ function report(res, message) {
3864
4633
  if (isJsonMode()) printJson(res);
3865
4634
  else ok(message);
3866
4635
  }
3867
- function shortSha(v) {
4636
+ function shortSha2(v) {
3868
4637
  return typeof v === "string" ? v.slice(0, 7) : "";
3869
4638
  }
3870
4639
  async function confirm(question) {
3871
4640
  if (!process.stdin.isTTY) return true;
3872
4641
  const rl = createInterface3({ input: process.stdin, output: process.stderr });
3873
- const answer = await new Promise((resolve2) => rl.question(`${question} [y/N] `, resolve2));
4642
+ const answer = await new Promise((resolve3) => rl.question(`${question} [y/N] `, resolve3));
3874
4643
  rl.close();
3875
4644
  return /^y(es)?$/i.test(answer.trim());
3876
4645
  }
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) => {
4646
+ var list = new Command12("list").description("List deployments (org-wide, or scoped to a project)").option("--project <id>", "Scope to a project (defaults to the linked project)").option("--env <environment>", "Filter by environment: production | preview").option("--limit <n>", "Max rows to fetch", "50").action(
4647
+ run3(async (opts) => {
3879
4648
  const projectId = opts.project || readProjectLink()?.projectId;
3880
4649
  const params = new URLSearchParams();
3881
4650
  if (projectId) params.set("projectId", projectId);
@@ -3890,15 +4659,15 @@ var list = new Command11("list").description("List deployments (org-wide, or sco
3890
4659
  status: d.status,
3891
4660
  env: d.environment,
3892
4661
  branch: d.branch,
3893
- commit: shortSha(d.commitSha),
4662
+ commit: shortSha2(d.commitSha),
3894
4663
  active: d.isActive ? "*" : "",
3895
4664
  created: d.createdAt
3896
4665
  }));
3897
4666
  printTable(rows, ["id", "status", "env", "branch", "commit", "active", "created"]);
3898
4667
  })
3899
4668
  );
3900
- var get = new Command11("get").description("Show a single deployment").argument("<id>", "Deployment ID").action(
3901
- run2(async (id) => {
4669
+ var get = new Command12("get").description("Show a single deployment").argument("<id>", "Deployment ID").action(
4670
+ run3(async (id) => {
3902
4671
  const res = await apiRequest(`/deployments/${id}`);
3903
4672
  const d = res.data ?? {};
3904
4673
  if (isJsonMode()) return printJson(d);
@@ -3918,20 +4687,20 @@ var get = new Command11("get").description("Show a single deployment").argument(
3918
4687
  );
3919
4688
  })
3920
4689
  );
3921
- var info2 = new Command11("info").description("Show container info for a deployment").argument("<id>", "Deployment ID").action(
3922
- run2(async (id) => {
4690
+ var info2 = new Command12("info").description("Show container info for a deployment").argument("<id>", "Deployment ID").action(
4691
+ run3(async (id) => {
3923
4692
  const res = await apiRequest(`/deployments/${id}/info`);
3924
4693
  printJson(res.data ?? res);
3925
4694
  })
3926
4695
  );
3927
- var usage = new Command11("usage").description("Show container resource usage for a deployment").argument("<id>", "Deployment ID").action(
3928
- run2(async (id) => {
4696
+ var usage = new Command12("usage").description("Show container resource usage for a deployment").argument("<id>", "Deployment ID").action(
4697
+ run3(async (id) => {
3929
4698
  const res = await apiRequest(`/deployments/${id}/usage`);
3930
4699
  printJson(res.data ?? res);
3931
4700
  })
3932
4701
  );
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) => {
4702
+ var redeploy = new Command12("redeploy").description("Redeploy from an existing deployment").argument("<id>", "Deployment ID").option("--use-existing-commit", "Rebuild the same commit instead of the latest on the branch").action(
4703
+ run3(async (id, opts) => {
3935
4704
  const res = await apiRequest(`/deployments/${id}/redeploy`, {
3936
4705
  method: "POST",
3937
4706
  body: JSON.stringify({ useExistingCommit: opts.useExistingCommit === true })
@@ -3939,14 +4708,14 @@ var redeploy = new Command11("redeploy").description("Redeploy from an existing
3939
4708
  report(res, `Redeploy triggered for ${id}`);
3940
4709
  })
3941
4710
  );
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) => {
4711
+ var rollback = new Command12("rollback").description("Roll back to a previous deployment").argument("<id>", "Deployment ID to roll back to").action(
4712
+ run3(async (id) => {
3944
4713
  const res = await apiRequest(`/deployments/${id}/rollback`, { method: "POST" });
3945
4714
  report(res, `Rolled back to ${id}`);
3946
4715
  })
3947
4716
  );
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) => {
4717
+ var pin = new Command12("pin").description("Pin (or unpin) a deployment's rollback artifact").argument("<id>", "Deployment ID").option("--off", "Unpin instead of pin").action(
4718
+ run3(async (id, opts) => {
3950
4719
  const pinned = !opts.off;
3951
4720
  const res = await apiRequest(`/deployments/${id}/pin`, {
3952
4721
  method: "POST",
@@ -3955,32 +4724,32 @@ var pin = new Command11("pin").description("Pin (or unpin) a deployment's rollba
3955
4724
  report(res, `${pinned ? "Pinned" : "Unpinned"} ${id}`);
3956
4725
  })
3957
4726
  );
3958
- var cancel = new Command11("cancel").description("Cancel an in-progress deployment").argument("<id>", "Deployment ID").action(
3959
- run2(async (id) => {
4727
+ var cancel = new Command12("cancel").description("Cancel an in-progress deployment").argument("<id>", "Deployment ID").action(
4728
+ run3(async (id) => {
3960
4729
  const res = await apiRequest(`/deployments/${id}/cancel`, { method: "POST" });
3961
4730
  report(res, `Cancelled ${id}`);
3962
4731
  })
3963
4732
  );
3964
- var restart2 = new Command11("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
3965
- run2(async (id) => {
4733
+ var restart2 = new Command12("restart").description("Restart a deployment's container").argument("<id>", "Deployment ID").action(
4734
+ run3(async (id) => {
3966
4735
  const res = await apiRequest(`/deployments/${id}/restart`, { method: "POST" });
3967
4736
  report(res, `Restarted ${id}`);
3968
4737
  })
3969
4738
  );
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) => {
4739
+ var reject = new Command12("reject").description("Reject a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
4740
+ run3(async (id) => {
3972
4741
  const res = await apiRequest(`/deployments/${id}/reject`, { method: "POST" });
3973
4742
  report(res, `Rejected ${id}`);
3974
4743
  })
3975
4744
  );
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) => {
4745
+ var keep = new Command12("keep").description("Keep a finished deployment awaiting a keep/reject decision").argument("<id>", "Deployment ID").action(
4746
+ run3(async (id) => {
3978
4747
  const res = await apiRequest(`/deployments/${id}/keep`, { method: "POST" });
3979
4748
  report(res, `Kept ${id}`);
3980
4749
  })
3981
4750
  );
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) => {
4751
+ var rm = new Command12("rm").description("Delete a deployment").argument("<id>", "Deployment ID").option("-y, --yes", "Skip the confirmation prompt").action(
4752
+ run3(async (id, opts) => {
3984
4753
  if (!opts.yes && !isJsonMode() && !await confirm(`Delete deployment ${id}?`)) {
3985
4754
  err("Aborted.");
3986
4755
  process.exit(1);
@@ -3989,8 +4758,8 @@ var rm = new Command11("rm").description("Delete a deployment").argument("<id>",
3989
4758
  report(res, `Deleted ${id}`);
3990
4759
  })
3991
4760
  );
3992
- var sslStatus = new Command11("status").description("Check SSL certificate status for a domain").argument("<domain>", "Domain to probe").action(
3993
- run2(async (domain) => {
4761
+ var sslStatus = new Command12("status").description("Check SSL certificate status for a domain").argument("<domain>", "Domain to probe").action(
4762
+ run3(async (domain) => {
3994
4763
  const res = await apiRequest("/deployments/ssl/status", {
3995
4764
  method: "POST",
3996
4765
  body: JSON.stringify({ domain })
@@ -3998,8 +4767,8 @@ var sslStatus = new Command11("status").description("Check SSL certificate statu
3998
4767
  printJson(res);
3999
4768
  })
4000
4769
  );
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) => {
4770
+ var sslRenew = new Command12("renew").description("Renew (issue) an SSL certificate for a domain").argument("<domain>", "Domain to renew").option("--www", "Also include the www subdomain").action(
4771
+ run3(async (domain, opts) => {
4003
4772
  const res = await apiRequest("/deployments/ssl/renew", {
4004
4773
  method: "POST",
4005
4774
  body: JSON.stringify({ domain, includeWww: opts.www === true })
@@ -4007,12 +4776,12 @@ var sslRenew = new Command11("renew").description("Renew (issue) an SSL certific
4007
4776
  report(res, `SSL renewal requested for ${domain}`);
4008
4777
  })
4009
4778
  );
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);
4779
+ var ssl = new Command12("ssl").description("SSL certificate operations").addCommand(sslStatus).addCommand(sslRenew);
4780
+ var deploymentCommand = new Command12("deployment").alias("deployments").description("Manage deployments (list, inspect, redeploy, rollback, \u2026)").addCommand(list).addCommand(get).addCommand(info2).addCommand(usage).addCommand(redeploy).addCommand(rollback).addCommand(pin).addCommand(cancel).addCommand(restart2).addCommand(reject).addCommand(keep).addCommand(rm).addCommand(ssl);
4012
4781
 
4013
4782
  // 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) => {
4783
+ import { Command as Command13 } from "commander";
4784
+ var logsCommand = new Command13("logs").description("View or stream a deployment's logs").argument("<deploymentId>", "Deployment ID").option("-f, --follow", "Stream live logs via SSE until the deployment finishes").option("--tail <n>", "Show only the last N log lines (snapshot mode)").action(async (deploymentId, opts) => {
4016
4785
  if (opts.follow) {
4017
4786
  try {
4018
4787
  const result = await streamDeploymentLogs(deploymentId);
@@ -4044,7 +4813,7 @@ var logsCommand = new Command12("logs").description("View or stream a deployment
4044
4813
  });
4045
4814
 
4046
4815
  // src/commands/project.ts
4047
- import { Command as Command13 } from "commander";
4816
+ import { Command as Command14 } from "commander";
4048
4817
  import chalk9 from "chalk";
4049
4818
  import { createInterface as createInterface4 } from "readline/promises";
4050
4819
  import { stdin as input3, stdout as output3 } from "process";
@@ -4081,7 +4850,7 @@ function printProject(project) {
4081
4850
  }
4082
4851
  }
4083
4852
  var ENVIRONMENTS = ["production", "preview", "development"];
4084
- var listCmd2 = new Command13("list").alias("ls").description("List projects in the active organization").action(
4853
+ var listCmd2 = new Command14("list").alias("ls").description("List projects in the active organization").action(
4085
4854
  action(async () => {
4086
4855
  const rows = [];
4087
4856
  for await (const p of paginate("/projects")) {
@@ -4096,7 +4865,7 @@ var listCmd2 = new Command13("list").alias("ls").description("List projects in t
4096
4865
  printTable(rows, ["id", "name", "slug", "repo", "source"]);
4097
4866
  })
4098
4867
  );
4099
- var getCmd = new Command13("get").description("Show a single project").argument("<id>", "Project ID").action(
4868
+ var getCmd = new Command14("get").description("Show a single project").argument("<id>", "Project ID").action(
4100
4869
  action(async (id) => {
4101
4870
  const { data } = await apiRequest(
4102
4871
  `/projects/${encodeURIComponent(id)}`
@@ -4104,7 +4873,7 @@ var getCmd = new Command13("get").description("Show a single project").argument(
4104
4873
  printProject(data);
4105
4874
  })
4106
4875
  );
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(
4876
+ var createCmd = new Command14("create").description("Create a project").requiredOption("--name <name>", "Project name").option("--slug <slug>", "Free-subdomain slug (slug.opsh.io)").option("--git-owner <owner>", "GitHub owner/org").option("--git-repo <repo>", "GitHub repository name").option("--git-branch <branch>", "Git branch to deploy").option("--framework <framework>", "Stack/framework id").option("--local-path <path>", "Local source path").option("--port <port>", "Container port", (v) => Number(v)).option(
4108
4877
  "--type <type>",
4109
4878
  "Project type: app | docker | services | monorepo"
4110
4879
  ).action(
@@ -4128,7 +4897,7 @@ var createCmd = new Command13("create").description("Create a project").required
4128
4897
  printProject(data);
4129
4898
  })
4130
4899
  );
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(
4900
+ var deleteCmd = new Command14("delete").alias("rm").description("Delete a project (tears down all resources)").argument("<id>", "Project ID").option("--force", "Cancel active work and delete anyway").option("--force-orphan", "Orphan resources that won't destroy, then drop the row").option("--wipe-volumes", "Also destroy persistent volumes").option("-y, --yes", "Skip the confirmation prompt").action(
4132
4901
  action(async (id, opts) => {
4133
4902
  if (!opts.yes) {
4134
4903
  const rl = createInterface4({ input: input3, output: output3 });
@@ -4159,7 +4928,7 @@ var deleteCmd = new Command13("delete").alias("rm").description("Delete a projec
4159
4928
  `);
4160
4929
  })
4161
4930
  );
4162
- var envCmd = new Command13("env").description("Manage project environment variables");
4931
+ var envCmd = new Command14("env").description("Manage project environment variables");
4163
4932
  envCmd.command("get").description("List env vars (secret values are masked by the API)").argument("<id>", "Project ID").option("--environment <env>", "Filter by environment (production|preview|development)").action(
4164
4933
  action(async (id, opts) => {
4165
4934
  const qs = opts.environment ? `?environment=${encodeURIComponent(opts.environment)}` : "";
@@ -4233,7 +5002,7 @@ envCmd.command("set").description("Merge env vars: upsert KEY=VALUE pairs and/or
4233
5002
  );
4234
5003
  })
4235
5004
  );
4236
- var gitCmd = new Command13("git").description("Manage git linkage and auto-deploy");
5005
+ var gitCmd = new Command14("git").description("Manage git linkage and auto-deploy");
4237
5006
  gitCmd.command("link").description("Link a GitHub repository to a project").argument("<id>", "Project ID").requiredOption("--owner <owner>", "GitHub owner/org").requiredOption("--repo <repo>", "Repository name").option("--branch <branch>", "Branch (defaults to the repo's default branch)").option("--installation-id <id>", "GitHub App installation id", (v) => Number(v)).action(
4238
5007
  action(async (id, opts) => {
4239
5008
  const result = await apiRequest(
@@ -4321,7 +5090,7 @@ gitCmd.command("webhook-domain").description("Set or clear the domain that recei
4321
5090
  `);
4322
5091
  })
4323
5092
  );
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(
5093
+ var connectCmd = new Command14("connect").description("Connect a custom domain to a project").argument("<id>", "Project ID").argument("<domain>", "Custom domain hostname").option("--include-www", "Also connect the www. variant").action(
4325
5094
  action(async (id, domain, opts) => {
4326
5095
  const result = await apiRequest(
4327
5096
  `/projects/${encodeURIComponent(id)}/connect`,
@@ -4341,7 +5110,7 @@ var connectCmd = new Command13("connect").description("Connect a custom domain t
4341
5110
  printJson(result.records);
4342
5111
  })
4343
5112
  );
4344
- var enableCmd = new Command13("enable").description("Start a stopped project").argument("<id>", "Project ID").action(
5113
+ var enableCmd = new Command14("enable").description("Start a stopped project").argument("<id>", "Project ID").action(
4345
5114
  action(async (id) => {
4346
5115
  const result = await apiRequest(
4347
5116
  `/projects/${encodeURIComponent(id)}/enable`,
@@ -4353,7 +5122,7 @@ var enableCmd = new Command13("enable").description("Start a stopped project").a
4353
5122
  `);
4354
5123
  })
4355
5124
  );
4356
- var disableCmd = new Command13("disable").description("Stop a running project").argument("<id>", "Project ID").action(
5125
+ var disableCmd = new Command14("disable").description("Stop a running project").argument("<id>", "Project ID").action(
4357
5126
  action(async (id) => {
4358
5127
  const result = await apiRequest(
4359
5128
  `/projects/${encodeURIComponent(id)}/disable`,
@@ -4366,7 +5135,7 @@ var disableCmd = new Command13("disable").description("Stop a running project").
4366
5135
  })
4367
5136
  );
4368
5137
  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(
5138
+ var sleepModeCmd = new Command14("sleep-mode").description("Set the project sleep mode").argument("<id>", "Project ID").argument("<mode>", `One of: ${SLEEP_MODES.join(", ")}`).action(
4370
5139
  action(async (id, mode) => {
4371
5140
  if (!SLEEP_MODES.includes(mode)) {
4372
5141
  err(` mode must be one of: ${SLEEP_MODES.join(", ")}`);
@@ -4384,7 +5153,7 @@ var sleepModeCmd = new Command13("sleep-mode").description("Set the project slee
4384
5153
  })
4385
5154
  );
4386
5155
  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(
5156
+ var transferCmd = new Command14("transfer").description("Promote a project to Openship Cloud, or bring it back (self-hosted only)").argument("<id>", "Project ID").argument("<direction>", `One of: ${TRANSFER_DIRS.join(", ")}`).action(
4388
5157
  action(async (id, direction) => {
4389
5158
  if (!TRANSFER_DIRS.includes(direction)) {
4390
5159
  err(` direction must be one of: ${TRANSFER_DIRS.join(", ")}`);
@@ -4407,7 +5176,7 @@ var transferCmd = new Command13("transfer").description("Promote a project to Op
4407
5176
  );
4408
5177
  })
4409
5178
  );
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(
5179
+ var logsCmd = new Command14("logs").description("Show or stream runtime (container) logs").argument("<id>", "Project ID").option("--tail <n>", "Number of recent lines", (v) => Number(v)).option("-f, --follow", "Stream logs until interrupted").action(
4411
5180
  action(async (id, opts) => {
4412
5181
  const tailQs = opts.tail ? `?tail=${opts.tail}` : "";
4413
5182
  if (!opts.follow) {
@@ -4435,7 +5204,7 @@ var logsCmd = new Command13("logs").description("Show or stream runtime (contain
4435
5204
  }
4436
5205
  })
4437
5206
  );
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(
5207
+ var serverLogsCmd = new Command14("server-logs").description("Show or stream HTTP request logs (edge/OpenResty)").argument("<id>", "Project ID").option("--limit <n>", "Number of recent entries (max 200)", (v) => Number(v)).option("--domain <domain>", "Restrict to a specific domain").option("-f, --follow", "Stream request logs until interrupted").action(
4439
5208
  action(async (id, opts) => {
4440
5209
  const base = `/projects/${encodeURIComponent(id)}/server-logs`;
4441
5210
  const domainQs = opts.domain ? `domain=${encodeURIComponent(opts.domain)}` : "";
@@ -4483,7 +5252,7 @@ function printLogEntry(entry) {
4483
5252
  process.stdout.write(` ${ts} ${color(level.padEnd(5))} ${String(msg)}
4484
5253
  `);
4485
5254
  }
4486
- var projectCommand = new Command13("project").alias("projects").description("Manage Openship projects");
5255
+ var projectCommand = new Command14("project").alias("projects").description("Manage Openship projects");
4487
5256
  projectCommand.addCommand(listCmd2);
4488
5257
  projectCommand.addCommand(getCmd);
4489
5258
  projectCommand.addCommand(createCmd);
@@ -4499,9 +5268,9 @@ projectCommand.addCommand(logsCmd);
4499
5268
  projectCommand.addCommand(serverLogsCmd);
4500
5269
 
4501
5270
  // src/commands/service.ts
4502
- import { Command as Command14 } from "commander";
5271
+ import { Command as Command15 } from "commander";
4503
5272
  import chalk10 from "chalk";
4504
- import { spawnSync as spawnSync3 } from "child_process";
5273
+ import { spawnSync as spawnSync4 } from "child_process";
4505
5274
  import path from "path";
4506
5275
  import { createInterface as createInterface5 } from "readline/promises";
4507
5276
  import { stdin as input4, stdout as output4 } from "process";
@@ -4520,7 +5289,7 @@ function fail(e) {
4520
5289
  process.exit(1);
4521
5290
  }
4522
5291
  function stackCommand(name) {
4523
- return new Command14(name).requiredOption(
5292
+ return new Command15(name).requiredOption(
4524
5293
  "-p, --project <id|slug|name>",
4525
5294
  "Stack (project) id, slug, or name"
4526
5295
  );
@@ -4768,7 +5537,7 @@ function mapComposeService(name, def, baseDir) {
4768
5537
  var syncCmd = stackCommand("sync").description("Sync a stack's services from a docker-compose file (services not in the file are removed)").argument("<compose-file>", "Path to docker-compose.yml / compose.yaml").option("-y, --yes", "Skip the confirmation prompt").action(async (composeFile, opts) => {
4769
5538
  requireAuth();
4770
5539
  const abs = path.resolve(composeFile);
4771
- const proc = spawnSync3(
5540
+ const proc = spawnSync4(
4772
5541
  "docker",
4773
5542
  ["compose", "-f", abs, "config", "--format", "json"],
4774
5543
  { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }
@@ -4862,7 +5631,7 @@ var containersCmd = stackCommand("containers").description("List the stack's act
4862
5631
  fail(e);
4863
5632
  }
4864
5633
  });
4865
- var driftCmd = new Command14("drift").description(
5634
+ var driftCmd = new Command15("drift").description(
4866
5635
  "Resolve compose drift on a service (upstream compose changed a value you edited)"
4867
5636
  );
4868
5637
  function driftActionCommand(action2) {
@@ -4890,7 +5659,7 @@ function driftActionCommand(action2) {
4890
5659
  }
4891
5660
  driftCmd.addCommand(driftActionCommand("accept"));
4892
5661
  driftCmd.addCommand(driftActionCommand("keep"));
4893
- var envCmd2 = new Command14("env").description("Read and write a service's environment variables");
5662
+ var envCmd2 = new Command15("env").description("Read and write a service's environment variables");
4894
5663
  var envGetCmd = stackCommand("get").description("List a service's environment variables (secrets masked)").argument("<service>", "Service name or id").option("-e, --env <environment>", "Environment: production | preview | development").action(async (service, opts) => {
4895
5664
  requireAuth();
4896
5665
  try {
@@ -5023,7 +5792,7 @@ var execCmd = stackCommand("exec").description("Open an interactive shell in a s
5023
5792
  );
5024
5793
  process.exit(1);
5025
5794
  });
5026
- var serviceCommand = new Command14("service").alias("services").description("Manage the services in a compose stack (a multi-service project)");
5795
+ var serviceCommand = new Command15("service").alias("services").description("Manage the services in a compose stack (a multi-service project)");
5027
5796
  serviceCommand.addCommand(listCmd3);
5028
5797
  serviceCommand.addCommand(getCmd2);
5029
5798
  serviceCommand.addCommand(createCmd2);
@@ -5039,7 +5808,7 @@ serviceCommand.addCommand(logsCmd2);
5039
5808
  serviceCommand.addCommand(execCmd);
5040
5809
 
5041
5810
  // src/commands/domain.ts
5042
- import { Command as Command15 } from "commander";
5811
+ import { Command as Command16 } from "commander";
5043
5812
  import chalk11 from "chalk";
5044
5813
  import ora3 from "ora";
5045
5814
  function spin(text2) {
@@ -5075,7 +5844,7 @@ function printRecords(result) {
5075
5844
  ["type", "host", "value"]
5076
5845
  );
5077
5846
  }
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) => {
5847
+ var listCmd4 = new Command16("list").description("List a project's custom domains").requiredOption("-p, --project <id>", "Project ID to list domains for").action(async (opts) => {
5079
5848
  try {
5080
5849
  const res = await apiRequest(
5081
5850
  `/domains?projectId=${encodeURIComponent(opts.project)}`
@@ -5090,7 +5859,7 @@ var listCmd4 = new Command15("list").description("List a project's custom domain
5090
5859
  fail2(e);
5091
5860
  }
5092
5861
  });
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) => {
5862
+ var addCmd2 = new Command16("add").description("Add a custom domain to a project").argument("<hostname>", "Domain hostname (e.g. app.example.com)").requiredOption("-p, --project <id>", "Project ID to attach the domain to").option("--primary", "Mark this domain as the project's primary", false).action(async (hostname, opts) => {
5094
5863
  const sp = spin(`Adding ${hostname}\u2026`);
5095
5864
  try {
5096
5865
  const res = await apiRequest("/domains", {
@@ -5109,7 +5878,7 @@ var addCmd2 = new Command15("add").description("Add a custom domain to a project
5109
5878
  fail2(e);
5110
5879
  }
5111
5880
  });
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) => {
5881
+ var previewCmd = new Command16("preview").description("Preview the DNS records a hostname would need (no changes saved)").argument("<hostname>", "Domain hostname to preview").action(async (hostname) => {
5113
5882
  try {
5114
5883
  const res = await apiRequest("/domains/preview", {
5115
5884
  method: "POST",
@@ -5120,7 +5889,7 @@ var previewCmd = new Command15("preview").description("Preview the DNS records a
5120
5889
  fail2(e);
5121
5890
  }
5122
5891
  });
5123
- var verifyCmd = new Command15("verify").description("Run DNS verification for a domain").argument("<id>", "Domain ID").action(async (id) => {
5892
+ var verifyCmd = new Command16("verify").description("Run DNS verification for a domain").argument("<id>", "Domain ID").action(async (id) => {
5124
5893
  const sp = spin("Checking DNS records\u2026");
5125
5894
  try {
5126
5895
  const res = await apiRaw(`/domains/${encodeURIComponent(id)}/verify`, { method: "POST" });
@@ -5146,7 +5915,7 @@ var verifyCmd = new Command15("verify").description("Run DNS verification for a
5146
5915
  fail2(e);
5147
5916
  }
5148
5917
  });
5149
- var primaryCmd = new Command15("primary").description("Make a domain the project's primary hostname").argument("<id>", "Domain ID").action(async (id) => {
5918
+ var primaryCmd = new Command16("primary").description("Make a domain the project's primary hostname").argument("<id>", "Domain ID").action(async (id) => {
5150
5919
  const sp = spin("Setting primary\u2026");
5151
5920
  try {
5152
5921
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/primary`, {
@@ -5159,7 +5928,7 @@ var primaryCmd = new Command15("primary").description("Make a domain the project
5159
5928
  fail2(e);
5160
5929
  }
5161
5930
  });
5162
- var recordsCmd = new Command15("records").description("Show the DNS records for an existing domain").argument("<id>", "Domain ID").action(async (id) => {
5931
+ var recordsCmd = new Command16("records").description("Show the DNS records for an existing domain").argument("<id>", "Domain ID").action(async (id) => {
5163
5932
  try {
5164
5933
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/records`);
5165
5934
  printRecords(res.data);
@@ -5177,7 +5946,7 @@ function printSsl(data) {
5177
5946
  if (data.issuer) info(` issuer: ${data.issuer}`);
5178
5947
  if (data.expiresAt) info(` expires: ${data.expiresAt}`);
5179
5948
  }
5180
- var renewCmd = new Command15("renew").description("Renew the SSL certificate for a domain").argument("<id>", "Domain ID").action(async (id) => {
5949
+ var renewCmd = new Command16("renew").description("Renew the SSL certificate for a domain").argument("<id>", "Domain ID").action(async (id) => {
5181
5950
  const sp = spin("Renewing certificate\u2026");
5182
5951
  try {
5183
5952
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/renew`, {
@@ -5190,7 +5959,7 @@ var renewCmd = new Command15("renew").description("Renew the SSL certificate for
5190
5959
  fail2(e);
5191
5960
  }
5192
5961
  });
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) => {
5962
+ var verifySslCmd = new Command16("verify-ssl").description("Recheck that a domain's SSL certificate is issued and valid (no reissue)").argument("<id>", "Domain ID").action(async (id) => {
5194
5963
  const sp = spin("Checking certificate\u2026");
5195
5964
  try {
5196
5965
  const res = await apiRequest(`/domains/${encodeURIComponent(id)}/verify-ssl`, {
@@ -5205,7 +5974,7 @@ var verifySslCmd = new Command15("verify-ssl").description("Recheck that a domai
5205
5974
  fail2(e);
5206
5975
  }
5207
5976
  });
5208
- var renewAllCmd = new Command15("renew-all").description("Renew SSL for every near-expiry domain in your organization").action(async () => {
5977
+ var renewAllCmd = new Command16("renew-all").description("Renew SSL for every near-expiry domain in your organization").action(async () => {
5209
5978
  const sp = spin("Renewing expiring certificates\u2026");
5210
5979
  try {
5211
5980
  const res = await apiRequest("/domains/renew-all", { method: "POST" });
@@ -5227,10 +5996,10 @@ var renewAllCmd = new Command15("renew-all").description("Renew SSL for every ne
5227
5996
  fail2(e);
5228
5997
  }
5229
5998
  });
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);
5999
+ var domainCommand = new Command16("domain").description("Manage custom domains, DNS verification, and SSL certificates").addCommand(listCmd4).addCommand(addCmd2).addCommand(previewCmd).addCommand(verifyCmd).addCommand(primaryCmd).addCommand(recordsCmd).addCommand(renewCmd).addCommand(verifySslCmd).addCommand(renewAllCmd);
5231
6000
 
5232
6001
  // src/commands/server.ts
5233
- import { Command as Command16 } from "commander";
6002
+ import { Command as Command17 } from "commander";
5234
6003
  import chalk12 from "chalk";
5235
6004
  import ora4 from "ora";
5236
6005
  var INSTALLABLE = ["docker", "git", "openresty", "certbot", "rsync"];
@@ -5263,7 +6032,7 @@ function connBody(o) {
5263
6032
  sshArgs: o.sshArgs
5264
6033
  };
5265
6034
  }
5266
- var server = new Command16("server").description("Manage self-hosted SSH servers");
6035
+ var server = new Command17("server").description("Manage self-hosted SSH servers");
5267
6036
  server.command("list").alias("ls").description("List servers in the active organization").action(
5268
6037
  guard(async () => {
5269
6038
  const servers = await apiRequest("/system/servers");
@@ -5525,9 +6294,9 @@ function fmtUptime(seconds) {
5525
6294
  var serverCommand = server;
5526
6295
 
5527
6296
  // src/commands/system.ts
5528
- import { Command as Command17 } from "commander";
6297
+ import { Command as Command18 } from "commander";
5529
6298
  import ora5 from "ora";
5530
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7 } from "fs";
6299
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
5531
6300
  import { createInterface as createInterface6 } from "readline/promises";
5532
6301
  import { stdin as input5, stdout as output5 } from "process";
5533
6302
  async function guarded(fn) {
@@ -5573,7 +6342,7 @@ async function promptHidden(query) {
5573
6342
  output5.write("\n");
5574
6343
  return answer;
5575
6344
  }
5576
- var settingsCommand = new Command17("settings").description("Read or update instance settings");
6345
+ var settingsCommand = new Command18("settings").description("Read or update instance settings");
5577
6346
  settingsCommand.command("get").description("Show current instance settings").action(async () => {
5578
6347
  await guarded(async () => {
5579
6348
  const s = await apiRequest("/system/settings");
@@ -5612,7 +6381,7 @@ settingsCommand.command("set").description("Update instance-level settings").opt
5612
6381
  `));
5613
6382
  });
5614
6383
  });
5615
- var onboardingCommand = new Command17("onboarding").description("First-run instance setup");
6384
+ var onboardingCommand = new Command18("onboarding").description("First-run instance setup");
5616
6385
  onboardingCommand.command("apply").description("Configure a fresh instance (fails once already configured)").option("--ssh-host <host>", "SSH host of the target server").option("--ssh-port <n>", "SSH port (default 22)").option("--ssh-user <user>", "SSH user (default root)").option("--ssh-auth-method <method>", "SSH auth method").option("--ssh-password <password>", "SSH password").option("--ssh-key-path <path>", "SSH private key path").option("--ssh-key-passphrase <pass>", "SSH key passphrase").option("--ssh-jump-host <host>", "SSH jump host").option("--ssh-args <args>", "Extra SSH args").option("--server-name <name>", "Display name for the server").option("--auth-mode <mode>", "Initial auth mode: none | local | cloud").option("--tunnel-provider <provider>", "Tunnel provider").option("--tunnel-token <token>", "Tunnel token").option("--default-build-mode <mode>", "Default build mode").option("--default-rollback-window <n>", "Default rollback window").action(async (opts) => {
5617
6386
  await guarded(async () => {
5618
6387
  const body = {
@@ -5646,7 +6415,7 @@ onboardingCommand.command("apply").description("Configure a fresh instance (fail
5646
6415
  }
5647
6416
  });
5648
6417
  });
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) => {
6418
+ var upgradeToAuthCommand = new Command18("upgrade-to-auth").description("Promote a zero-auth instance to email/password login").option("--name <name>", "Account display name").option("--email <email>", "Account email").option("--password <password>", "Account password (prompted if omitted)").option("--use-own-mail-server", "Warm the self-hosted mail server for auth emails").action(async (opts) => {
5650
6419
  await guarded(async () => {
5651
6420
  const name = opts.name;
5652
6421
  const email = opts.email;
@@ -5675,7 +6444,7 @@ var upgradeToAuthCommand = new Command17("upgrade-to-auth").description("Promote
5675
6444
  `));
5676
6445
  });
5677
6446
  });
5678
- var browseCommand = new Command17("browse").description("List directories on the instance host (defaults to home)").argument("[path]", "Directory to list").action(async (path2) => {
6447
+ var browseCommand = new Command18("browse").description("List directories on the instance host (defaults to home)").argument("[path]", "Directory to list").action(async (path2) => {
5679
6448
  await guarded(async () => {
5680
6449
  const qs = path2 ? `?path=${encodeURIComponent(path2)}` : "";
5681
6450
  const res = await apiRequest(`/system/browse${qs}`);
@@ -5704,7 +6473,7 @@ function buildDomain(opts) {
5704
6473
  err("\n A domain is required: pass --hostname <host> or --slug <slug>.\n");
5705
6474
  process.exit(1);
5706
6475
  }
5707
- var migrationCommand = new Command17("migration").description("Team-mode migration lifecycle");
6476
+ var migrationCommand = new Command18("migration").description("Team-mode migration lifecycle");
5708
6477
  migrationCommand.command("preflight").description("Read-only readiness check for the own-server migration").requiredOption("--server-id <id>", "Target server id").option("--hostname <host>", "Custom domain pointing at the server").option("--slug <slug>", "Free <slug>.opsh.io subdomain").action(async (opts) => {
5709
6478
  await guarded(async () => {
5710
6479
  const domain = buildDomain(opts);
@@ -5811,7 +6580,7 @@ migrationCommand.command("switch-back").description("Reverse migration back to s
5811
6580
  }
5812
6581
  });
5813
6582
  });
5814
- var dataTransferCommand = new Command17("data-transfer").description(
6583
+ var dataTransferCommand = new Command18("data-transfer").description(
5815
6584
  "Whole-instance export / import (owner-only)"
5816
6585
  );
5817
6586
  dataTransferCommand.command("export").description("Export the entire instance to a JSON file").option("--passphrase <passphrase>", "Seal secrets under this passphrase").option("--out <file>", "Write the export to this file instead of stdout").action(async (opts) => {
@@ -5827,7 +6596,7 @@ dataTransferCommand.command("export").description("Export the entire instance to
5827
6596
  );
5828
6597
  spin4?.succeed("Export ready.");
5829
6598
  if (opts.out) {
5830
- writeFileSync7(opts.out, JSON.stringify(file));
6599
+ writeFileSync8(opts.out, JSON.stringify(file));
5831
6600
  const tables = Object.keys(file.dump?.tables ?? {}).length;
5832
6601
  report2(
5833
6602
  { out: opts.out, tables },
@@ -5853,7 +6622,7 @@ dataTransferCommand.command("import").description("Import an instance export fil
5853
6622
  }
5854
6623
  let file;
5855
6624
  try {
5856
- file = JSON.parse(readFileSync8(opts.file, "utf8"));
6625
+ file = JSON.parse(readFileSync9(opts.file, "utf8"));
5857
6626
  } catch {
5858
6627
  err(`
5859
6628
  Could not read or parse ${opts.file}.
@@ -5881,10 +6650,10 @@ dataTransferCommand.command("import").description("Import an instance export fil
5881
6650
  }
5882
6651
  });
5883
6652
  });
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);
6653
+ var systemCommand = new Command18("system").description("Instance settings, onboarding, migration, and data transfer").addCommand(settingsCommand).addCommand(onboardingCommand).addCommand(upgradeToAuthCommand).addCommand(browseCommand).addCommand(migrationCommand).addCommand(dataTransferCommand);
5885
6654
 
5886
6655
  // src/commands/mail.ts
5887
- import { Command as Command18 } from "commander";
6656
+ import { Command as Command19 } from "commander";
5888
6657
  import chalk13 from "chalk";
5889
6658
  import ora6 from "ora";
5890
6659
  import { createInterface as createInterface7 } from "readline/promises";
@@ -5926,7 +6695,7 @@ function printRecordsObject(records) {
5926
6695
  if (rows.length === 0) return info(" (no DNS records)");
5927
6696
  printTable(rows, ["key", "type", "host", "value"]);
5928
6697
  }
5929
- var stepsCmd = new Command18("steps").description("List the mail setup steps").action(
6698
+ var stepsCmd = new Command19("steps").description("List the mail setup steps").action(
5930
6699
  guard2(async () => {
5931
6700
  const res = await apiRequest(
5932
6701
  "/mail/steps"
@@ -5939,7 +6708,7 @@ var stepsCmd = new Command18("steps").description("List the mail setup steps").a
5939
6708
  info(` ${res.total} steps total.`);
5940
6709
  })
5941
6710
  );
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(
6711
+ var statusCmd = new Command19("status").description("Show the setup progress for a mail server").argument("[serverId]", "Mail server ID (omit for the empty welcome shell)").action(
5943
6712
  guard2(async (serverId) => {
5944
6713
  const q = serverId ? `?serverId=${encodeURIComponent(serverId)}` : "";
5945
6714
  const res = await apiRequest(`/mail/status${q}`);
@@ -5960,7 +6729,7 @@ var statusCmd = new Command18("status").description("Show the setup progress for
5960
6729
  }
5961
6730
  })
5962
6731
  );
5963
- var serversCmd = new Command18("servers").description("List every server the mail stack is installed on").action(
6732
+ var serversCmd = new Command19("servers").description("List every server the mail stack is installed on").action(
5964
6733
  guard2(async () => {
5965
6734
  const res = await apiRequest(
5966
6735
  "/mail/servers"
@@ -5981,7 +6750,7 @@ var serversCmd = new Command18("servers").description("List every server the mai
5981
6750
  );
5982
6751
  })
5983
6752
  );
5984
- var scanCmd = new Command18("scan").description("Probe a server for an existing mail install (read-only)").argument("<serverId>", "Server ID to scan").action(
6753
+ var scanCmd = new Command19("scan").description("Probe a server for an existing mail install (read-only)").argument("<serverId>", "Server ID to scan").action(
5985
6754
  guard2(async (serverId) => {
5986
6755
  const sp = spin2("Scanning server\u2026");
5987
6756
  const res = await apiRequest("/mail/scan", { method: "POST", body: JSON.stringify({ serverId }) });
@@ -5996,7 +6765,7 @@ var scanCmd = new Command18("scan").description("Probe a server for an existing
5996
6765
  else info(" Nothing to adopt on this server.");
5997
6766
  })
5998
6767
  );
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(
6768
+ var adoptCmd = new Command19("adopt").description("Re-adopt an existing mail install whose orchestrator state was lost").argument("<serverId>", "Server ID to adopt").action(
6000
6769
  guard2(async (serverId) => {
6001
6770
  const sp = spin2("Adopting mail server\u2026");
6002
6771
  const res = await apiRequest(
@@ -6009,7 +6778,7 @@ var adoptCmd = new Command18("adopt").description("Re-adopt an existing mail ins
6009
6778
  info(` completed: ${res.completed ? "yes" : "no"}`);
6010
6779
  })
6011
6780
  );
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(
6781
+ var setupCmd = new Command19("setup").description("Start or resume the mail setup wizard (streams over SSE)").argument("<serverId>", "Server ID to install the mail stack on").requiredOption("-d, --domain <domain>", "Mail domain (e.g. example.com)").option("--start-step <n>", "Resume from a specific step (1-13)").option("--config <json>", "iRedMail config overrides as a JSON object").action(
6013
6782
  guard2(async (serverId, opts) => {
6014
6783
  let config;
6015
6784
  if (opts.config) {
@@ -6078,7 +6847,7 @@ var setupCmd = new Command18("setup").description("Start or resume the mail setu
6078
6847
  if (failed) process.exit(1);
6079
6848
  })
6080
6849
  );
6081
- var cancelCmd = new Command18("cancel").description("Cancel the mail setup currently running").action(
6850
+ var cancelCmd = new Command19("cancel").description("Cancel the mail setup currently running").action(
6082
6851
  guard2(async () => {
6083
6852
  const res = await apiRequest("/mail/setup/cancel", {
6084
6853
  method: "POST"
@@ -6088,7 +6857,7 @@ var cancelCmd = new Command18("cancel").description("Cancel the mail setup curre
6088
6857
  })
6089
6858
  );
6090
6859
  function ackCommand(name, path2, description, successMsg) {
6091
- return new Command18(name).description(description).argument("<serverId>", "Mail server ID").action(
6860
+ return new Command19(name).description(description).argument("<serverId>", "Mail server ID").action(
6092
6861
  guard2(async (serverId) => {
6093
6862
  const res = await apiRequest(path2, {
6094
6863
  method: "POST",
@@ -6111,7 +6880,7 @@ var ptrAckCmd = ackCommand(
6111
6880
  "Acknowledge that reverse DNS (PTR) is configured",
6112
6881
  "PTR acknowledged. Re-run `mail setup` with --start-step to continue."
6113
6882
  );
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(
6883
+ var resetCmd = new Command19("reset").description("Wipe the on-server setup state file (does NOT touch installed daemons)").argument("<serverId>", "Mail server ID").option("-y, --yes", "Skip the confirmation prompt").action(
6115
6884
  guard2(async (serverId, opts) => {
6116
6885
  if (!opts.yes && !isJsonMode()) {
6117
6886
  const rl = createInterface7({ input: input6, output: output6 });
@@ -6127,7 +6896,7 @@ var resetCmd = new Command18("reset").description("Wipe the on-server setup stat
6127
6896
  ok(" Setup state reset.");
6128
6897
  })
6129
6898
  );
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(
6899
+ var forgetCmd = new Command19("forget").description("Stop managing a mail server (drops the DB row; leaves the stack + state intact)").argument("<serverId>", "Mail server ID").action(
6131
6900
  guard2(async (serverId) => {
6132
6901
  const res = await apiRequest(`/mail/servers/${encodeURIComponent(serverId)}`, {
6133
6902
  method: "DELETE"
@@ -6136,7 +6905,7 @@ var forgetCmd = new Command18("forget").description("Stop managing a mail server
6136
6905
  ok(` Forgot mail server ${serverId} (re-adopt with \`mail scan\` + \`mail adopt\`).`);
6137
6906
  })
6138
6907
  );
6139
- var healthCmd = new Command18("health").description("Show live status of every mail daemon").argument("<serverId>", "Mail server ID").action(
6908
+ var healthCmd = new Command19("health").description("Show live status of every mail daemon").argument("<serverId>", "Mail server ID").action(
6140
6909
  guard2(async (serverId) => {
6141
6910
  const sp = spin2("Checking mail daemons\u2026");
6142
6911
  const res = await apiRequest(`/mail/health/${encodeURIComponent(serverId)}`);
@@ -6153,7 +6922,7 @@ var healthCmd = new Command18("health").description("Show live status of every m
6153
6922
  );
6154
6923
  })
6155
6924
  );
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(
6925
+ var logsCmd3 = new Command19("logs").description("Tail a mail component's journal (snapshot)").argument("<serverId>", "Mail server ID").argument("<component>", "Component key (postfix|dovecot|amavis|clamav|iredapd|postgresql|\u2026)").option("-n, --lines <n>", "Number of lines (max 1000)", "200").action(
6157
6926
  guard2(async (serverId, component, opts) => {
6158
6927
  const res = await apiRequest(
6159
6928
  `/mail/admin/${encodeURIComponent(serverId)}/components/${encodeURIComponent(component)}/logs?lines=${encodeURIComponent(opts.lines)}`
@@ -6163,9 +6932,9 @@ var logsCmd3 = new Command18("logs").description("Tail a mail component's journa
6163
6932
  for (const line of res.lines) process.stdout.write(line + "\n");
6164
6933
  })
6165
6934
  );
6166
- var postmasterCmd = new Command18("postmaster").description("Manage the postmaster mailbox");
6935
+ var postmasterCmd = new Command19("postmaster").description("Manage the postmaster mailbox");
6167
6936
  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(
6937
+ new Command19("set-password").description("Rotate the postmaster password").argument("<serverId>", "Mail server ID").option("--password <password>", "New password (min 12 chars); prompted if omitted").action(
6169
6938
  guard2(async (serverId, opts) => {
6170
6939
  let password2 = opts.password;
6171
6940
  if (!password2) {
@@ -6191,12 +6960,12 @@ postmasterCmd.addCommand(
6191
6960
  })
6192
6961
  )
6193
6962
  );
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);
6963
+ var mailCommand = new Command19("mail").description("Self-hosted mail server (iRedMail) setup and admin [self-host]").addCommand(stepsCmd).addCommand(statusCmd).addCommand(serversCmd).addCommand(scanCmd).addCommand(adoptCmd).addCommand(setupCmd).addCommand(cancelCmd).addCommand(dnsAckCmd).addCommand(ptrAckCmd).addCommand(resetCmd).addCommand(forgetCmd).addCommand(healthCmd).addCommand(logsCmd3).addCommand(postmasterCmd);
6195
6964
 
6196
6965
  // src/commands/backup.ts
6197
- import { Command as Command19 } from "commander";
6966
+ import { Command as Command20 } from "commander";
6198
6967
  import ora7 from "ora";
6199
- import { readFileSync as readFileSync9 } from "fs";
6968
+ import { readFileSync as readFileSync10 } from "fs";
6200
6969
  async function guard3(fn) {
6201
6970
  try {
6202
6971
  await fn();
@@ -6279,7 +7048,7 @@ async function followStream(path2, label) {
6279
7048
  spinner3?.stop();
6280
7049
  return status;
6281
7050
  }
6282
- var policyCmd = new Command19("policy").description("Backup policies (schedules) for a project");
7051
+ var policyCmd = new Command20("policy").description("Backup policies (schedules) for a project");
6283
7052
  policyCmd.command("list").description("List backup policies for a project").requiredOption("--project <id>", "Project ID").action(
6284
7053
  (opts) => guard3(async () => {
6285
7054
  const { data } = await apiRequest(
@@ -6349,7 +7118,7 @@ policyCmd.command("run").description("Trigger a policy's backup now").argument("
6349
7118
  }
6350
7119
  })
6351
7120
  );
6352
- var runCmd = new Command19("run").description("Backup runs (executions)");
7121
+ var runCmd = new Command20("run").description("Backup runs (executions)");
6353
7122
  runCmd.command("list").description("List backup runs for a project").requiredOption("--project <id>", "Project ID").option("--service <id>", "Filter to a single service").option("--limit <n>", "Max rows (default 50)").action(
6354
7123
  (opts) => guard3(async () => {
6355
7124
  const qs = new URLSearchParams();
@@ -6425,7 +7194,7 @@ runCmd.command("restore").description("Prepare a restore from a run (stages it;
6425
7194
  }
6426
7195
  })
6427
7196
  );
6428
- var restoreCmd = new Command19("restore").description("Manage staged restores");
7197
+ var restoreCmd = new Command20("restore").description("Manage staged restores");
6429
7198
  restoreCmd.command("apply").description("Apply a staged restore (destructive)").argument("<restoreId>", "Restore ID from `backup run restore`").requiredOption("--token <token>", "Confirmation token from prepare").option("--follow", "Stream the restore to completion").action(
6430
7199
  (restoreId, opts) => guard3(async () => {
6431
7200
  await apiRequest(
@@ -6468,7 +7237,7 @@ restoreCmd.command("get").description("Show one restore (optionally stream it)")
6468
7237
  show(data);
6469
7238
  })
6470
7239
  );
6471
- var destinationCmd = new Command19("destination").description("Backup destinations (storage targets)");
7240
+ var destinationCmd = new Command20("destination").description("Backup destinations (storage targets)");
6472
7241
  destinationCmd.command("list").description("List backup destinations").action(
6473
7242
  () => guard3(async () => {
6474
7243
  const { data } = await apiRequest("/backup-destinations");
@@ -6489,7 +7258,7 @@ destinationCmd.command("create").description("Create a backup destination").requ
6489
7258
  let sftpPrivateKey = opts.sftpPrivateKey;
6490
7259
  if (opts.sftpPrivateKeyFile) {
6491
7260
  try {
6492
- sftpPrivateKey = readFileSync9(opts.sftpPrivateKeyFile, "utf8");
7261
+ sftpPrivateKey = readFileSync10(opts.sftpPrivateKeyFile, "utf8");
6493
7262
  } catch {
6494
7263
  throw new Error(`Cannot read key file: ${opts.sftpPrivateKeyFile}`);
6495
7264
  }
@@ -6540,10 +7309,10 @@ destinationCmd.command("preflight").description("Verify a destination (write + r
6540
7309
  }
6541
7310
  })
6542
7311
  );
6543
- var backupCommand = new Command19("backup").description("Manage backups: policies, runs, restores, destinations").addCommand(policyCmd).addCommand(runCmd).addCommand(restoreCmd).addCommand(destinationCmd);
7312
+ var backupCommand = new Command20("backup").description("Manage backups: policies, runs, restores, destinations").addCommand(policyCmd).addCommand(runCmd).addCommand(restoreCmd).addCommand(destinationCmd);
6544
7313
 
6545
7314
  // src/commands/token.ts
6546
- import { Command as Command20 } from "commander";
7315
+ import { Command as Command21 } from "commander";
6547
7316
  import chalk15 from "chalk";
6548
7317
 
6549
7318
  // src/lib/cmd-helpers.ts
@@ -6571,7 +7340,7 @@ function collectGrant(value, acc) {
6571
7340
  acc.push({ resourceType, resourceId, permissions });
6572
7341
  return acc;
6573
7342
  }
6574
- var listCmd5 = new Command20("list").description("List your personal access tokens").action(async () => {
7343
+ var listCmd5 = new Command21("list").description("List your personal access tokens").action(async () => {
6575
7344
  try {
6576
7345
  const res = await apiRequest("/tokens");
6577
7346
  const rows = res.data ?? [];
@@ -6596,7 +7365,7 @@ var listCmd5 = new Command20("list").description("List your personal access toke
6596
7365
  fail3(e);
6597
7366
  }
6598
7367
  });
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(
7368
+ var createCmd3 = new Command21("create").description("Mint a new personal access token (the secret is shown once)").argument("<name>", "Human-readable token name").option("--read-only", "Reject mutation methods (POST/PUT/PATCH/DELETE)", false).option("--expires <days>", "Expire after N days (1\u2013365); omit for non-expiring", (v) => parseInt(v, 10)).option(
6600
7369
  "--grant <type:id:perms>",
6601
7370
  "Scope the token to a resource (repeatable), e.g. project:abc123:read,write",
6602
7371
  collectGrant,
@@ -6628,7 +7397,7 @@ var createCmd3 = new Command20("create").description("Mint a new personal access
6628
7397
  fail3(e);
6629
7398
  }
6630
7399
  });
6631
- var revokeCmd = new Command20("revoke").description("Revoke one of your tokens").argument("<id>", "Token ID").action(async (id) => {
7400
+ var revokeCmd = new Command21("revoke").description("Revoke one of your tokens").argument("<id>", "Token ID").action(async (id) => {
6632
7401
  const sp = spin3("Revoking token\u2026");
6633
7402
  try {
6634
7403
  await apiRequest(`/tokens/${encodeURIComponent(id)}`, { method: "DELETE" });
@@ -6640,11 +7409,11 @@ var revokeCmd = new Command20("revoke").description("Revoke one of your tokens")
6640
7409
  fail3(e);
6641
7410
  }
6642
7411
  });
6643
- var tokenCommand = new Command20("token").description("Manage personal access tokens").addCommand(listCmd5).addCommand(createCmd3).addCommand(revokeCmd);
7412
+ var tokenCommand = new Command21("token").description("Manage personal access tokens").addCommand(listCmd5).addCommand(createCmd3).addCommand(revokeCmd);
6644
7413
 
6645
7414
  // 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) => {
7415
+ import { Command as Command22 } from "commander";
7416
+ var apiCommand = new Command22("api").description("Make an authenticated request to any Openship API route (like `gh api`)").argument("<path>", "Path under /api, e.g. /projects or /deployments/<id>").option("-X, --method <method>", "HTTP method (defaults to GET, or POST when --data is given)").option("-d, --data <json>", "Request body as a JSON string").option("-q, --query <kv...>", "Query parameter key=value (repeatable)").action(async (path2, opts) => {
6648
7417
  const method = (opts.method || (opts.data ? "POST" : "GET")).toUpperCase();
6649
7418
  let url = path2.startsWith("/") ? path2 : `/${path2}`;
6650
7419
  if (opts.query?.length) {
@@ -6678,20 +7447,20 @@ var apiCommand = new Command21("api").description("Make an authenticated request
6678
7447
  });
6679
7448
 
6680
7449
  // src/commands/reset-admin.ts
6681
- import { Command as Command22 } from "commander";
7450
+ import { Command as Command23 } from "commander";
6682
7451
  import chalk16 from "chalk";
6683
7452
  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";
7453
+ import { readFileSync as readFileSync11 } from "fs";
7454
+ import { homedir as homedir8 } from "os";
7455
+ import { join as join13 } from "path";
6687
7456
  function resolvedApiPort() {
6688
7457
  try {
6689
- return JSON.parse(readFileSync10(join11(homedir7(), ".openship", "ports.json"), "utf8")).api;
7458
+ return JSON.parse(readFileSync11(join13(homedir8(), ".openship", "ports.json"), "utf8")).api;
6690
7459
  } catch {
6691
7460
  return void 0;
6692
7461
  }
6693
7462
  }
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) => {
7463
+ var resetAdminCommand = new Command23("reset-admin-password").description("Reset the local admin login on THIS machine (no sign-in required)").option("--port <port>", "API port of the running service (default: the resolved port from ~/.openship/ports.json, else 4000)").option("--email <email>", "Also set the admin email").option("--name <name>", "Also set the admin display name").option("--password <password>", "New password (prompted if omitted)").action(async (opts) => {
6695
7464
  intro(chalk16.cyan("Reset Openship admin password"));
6696
7465
  let pw = opts.password;
6697
7466
  if (!pw) {
@@ -6739,11 +7508,11 @@ var resetAdminCommand = new Command22("reset-admin-password").description("Reset
6739
7508
  });
6740
7509
 
6741
7510
  // 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";
7511
+ import { Command as Command24 } from "commander";
7512
+ import { chmodSync as chmodSync2, existsSync as existsSync12, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync9 } from "fs";
7513
+ import { spawn as spawn3, spawnSync as spawnSync5 } from "child_process";
7514
+ import { homedir as homedir9 } from "os";
7515
+ import { join as join14 } from "path";
6747
7516
  import ora9 from "ora";
6748
7517
  function assetForPlatform() {
6749
7518
  const { platform, arch } = process;
@@ -6755,33 +7524,33 @@ function assetForPlatform() {
6755
7524
  throw new Error(`Unsupported platform: ${platform} (${arch})`);
6756
7525
  }
6757
7526
  function installDmg(dmg) {
6758
- const homeApps = join12(homedir8(), "Applications");
7527
+ const homeApps = join14(homedir9(), "Applications");
6759
7528
  let dest = homeApps;
6760
7529
  try {
6761
- mkdirSync8(homeApps, { recursive: true });
7530
+ mkdirSync9(homeApps, { recursive: true });
6762
7531
  } catch {
6763
7532
  dest = "/Applications";
6764
7533
  }
6765
- const attach = spawnSync4("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
7534
+ const attach = spawnSync5("hdiutil", ["attach", "-nobrowse", "-readonly", "-noverify", dmg], {
6766
7535
  encoding: "utf8"
6767
7536
  });
6768
7537
  if (attach.status !== 0) throw new Error(`hdiutil attach failed: ${attach.stderr?.trim()}`);
6769
7538
  const mount = (attach.stdout.match(/\/Volumes\/[^\n]*/g) ?? []).pop()?.trim();
6770
7539
  if (!mount) throw new Error("Could not determine the mounted volume");
6771
- let target = join12(dest, "Openship.app");
7540
+ let target = join14(dest, "Openship.app");
6772
7541
  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" });
7542
+ const appInDmg = join14(mount, "Openship.app");
7543
+ if (!existsSync12(appInDmg)) throw new Error("Openship.app not found in the disk image");
7544
+ spawnSync5("rm", ["-rf", target]);
7545
+ let copy = spawnSync5("ditto", [appInDmg, target], { encoding: "utf8" });
6777
7546
  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" });
7547
+ target = join14("/Applications", "Openship.app");
7548
+ spawnSync5("rm", ["-rf", target]);
7549
+ copy = spawnSync5("ditto", [appInDmg, target], { encoding: "utf8" });
6781
7550
  }
6782
7551
  if (copy.status !== 0) throw new Error(`ditto copy failed: ${copy.stderr?.trim()}`);
6783
7552
  } finally {
6784
- spawnSync4("hdiutil", ["detach", mount, "-quiet"]);
7553
+ spawnSync5("hdiutil", ["detach", mount, "-quiet"]);
6785
7554
  }
6786
7555
  return target;
6787
7556
  }
@@ -6790,10 +7559,10 @@ function installAppImage(appImage) {
6790
7559
  return appImage;
6791
7560
  }
6792
7561
  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(
7562
+ const localAppData = process.env.LOCALAPPDATA || join14(homedir9(), "AppData", "Local");
7563
+ const target = join14(localAppData, "Programs", "Openship");
7564
+ mkdirSync9(target, { recursive: true });
7565
+ const expand = spawnSync5(
6797
7566
  "powershell",
6798
7567
  [
6799
7568
  "-NoProfile",
@@ -6808,22 +7577,22 @@ function installZip(zip) {
6808
7577
  }
6809
7578
  function launch(kind, target) {
6810
7579
  if (kind === "dmg") {
6811
- spawnSync4("open", [target]);
7580
+ spawnSync5("open", [target]);
6812
7581
  return;
6813
7582
  }
6814
7583
  if (kind === "appimage") {
6815
- const child = spawn2(target, [], { detached: true, stdio: "ignore" });
7584
+ const child = spawn3(target, [], { detached: true, stdio: "ignore" });
6816
7585
  child.on("error", () => {
6817
- spawn2(target, ["--appimage-extract-and-run"], { detached: true, stdio: "ignore" }).unref();
7586
+ spawn3(target, ["--appimage-extract-and-run"], { detached: true, stdio: "ignore" }).unref();
6818
7587
  });
6819
7588
  child.unref();
6820
7589
  return;
6821
7590
  }
6822
- const exe = join12(target, "Openship.exe");
6823
- const path2 = existsSync10(exe) ? exe : target;
6824
- spawnSync4("cmd", ["/c", "start", "", path2]);
7591
+ const exe = join14(target, "Openship.exe");
7592
+ const path2 = existsSync12(exe) ? exe : target;
7593
+ spawnSync5("cmd", ["/c", "start", "", path2]);
6825
7594
  }
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) => {
7595
+ var installCommand = new Command24("install").description("Download and install the Openship desktop app for this OS").option("--version <tag>", "Release tag to install (e.g. v1.2.3)").option("--latest", "Install the latest release (default)").option("--force", "Re-download even if a verified copy is cached").option("--no-verify", "Skip SHA-256 verification (allowed when no sidecar exists)").option("--no-launch", "Install without launching the app").action(async (opts) => {
6827
7596
  let asset;
6828
7597
  try {
6829
7598
  asset = assetForPlatform();
@@ -6846,13 +7615,13 @@ var installCommand = new Command23("install").description("Download and install
6846
7615
  process.exit(1);
6847
7616
  }
6848
7617
  const dir = releaseDir(tag);
6849
- const assetPath = join12(dir, asset.name);
7618
+ const assetPath = join14(dir, asset.name);
6850
7619
  const sidecarPath = `${assetPath}.sha256`;
6851
7620
  const assetUrl2 = `${RELEASES}/download/${tag}/${asset.name}`;
6852
7621
  const sidecarUrl = `${assetUrl2}.sha256`;
6853
7622
  let downloaded = false;
6854
7623
  let sha;
6855
- const cachedUsable = !opts.force && existsSync10(assetPath) && (existsSync10(sidecarPath) || opts.verify === false);
7624
+ const cachedUsable = !opts.force && existsSync12(assetPath) && (existsSync12(sidecarPath) || opts.verify === false);
6856
7625
  if (cachedUsable) {
6857
7626
  info(` Using cached ${asset.name} (${tag}).`);
6858
7627
  } else {
@@ -6874,8 +7643,8 @@ var installCommand = new Command23("install").description("Download and install
6874
7643
  const s2 = spin4("Verifying checksum\u2026");
6875
7644
  try {
6876
7645
  let sidecarBody;
6877
- if (existsSync10(sidecarPath) && !downloaded) {
6878
- sidecarBody = readFileSync11(sidecarPath, "utf8");
7646
+ if (existsSync12(sidecarPath) && !downloaded) {
7647
+ sidecarBody = readFileSync12(sidecarPath, "utf8");
6879
7648
  } else {
6880
7649
  sidecarBody = await fetchSidecar(sidecarUrl);
6881
7650
  }
@@ -6898,8 +7667,8 @@ var installCommand = new Command23("install").description("Download and install
6898
7667
  err(`Expected ${expected}, got ${actual}. The download may be corrupt or tampered with.`);
6899
7668
  process.exit(1);
6900
7669
  }
6901
- mkdirSync8(dir, { recursive: true });
6902
- writeFileSync8(sidecarPath, sidecarBody);
7670
+ mkdirSync9(dir, { recursive: true });
7671
+ writeFileSync9(sidecarPath, sidecarBody);
6903
7672
  s2?.succeed("Checksum verified");
6904
7673
  } catch (e) {
6905
7674
  s2?.fail("Verification failed");
@@ -6938,15 +7707,15 @@ var installCommand = new Command23("install").description("Download and install
6938
7707
  });
6939
7708
 
6940
7709
  // src/commands/update.ts
6941
- import { Command as Command24 } from "commander";
6942
- import { spawnSync as spawnSync5 } from "child_process";
7710
+ import { Command as Command25 } from "commander";
7711
+ import { spawnSync as spawnSync6 } from "child_process";
6943
7712
  function detectPackageManager2(override) {
6944
7713
  if (override === "bun" || override === "npm") return override;
6945
- const hasBun = spawnSync5("bun", ["--version"], { stdio: "ignore" }).status === 0;
7714
+ const hasBun = spawnSync6("bun", ["--version"], { stdio: "ignore" }).status === 0;
6946
7715
  return hasBun ? "bun" : "npm";
6947
7716
  }
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";
7717
+ var updateCommand = new Command25("update").description("Update the Openship CLI + bundled server to the latest release").option("--check", "Only report the current + latest version; don't install").option("--via <manager>", "Package manager to update with: bun | npm").action(async (opts) => {
7718
+ const current = "0.2.3";
6950
7719
  let latest;
6951
7720
  try {
6952
7721
  latest = (await resolveLatestTag()).replace(/^v/, "");
@@ -6974,7 +7743,7 @@ var updateCommand = new Command24("update").description("Update the Openship CLI
6974
7743
  const ref = `openship@${latest}`;
6975
7744
  const argv = pm === "bun" ? ["add", "-g", ref] : ["install", "-g", ref];
6976
7745
  info(`Updating v${current} \u2192 v${latest} (${cliInstallCommand(pm, latest)})...`);
6977
- const res = spawnSync5(pm, argv, { stdio: "inherit" });
7746
+ const res = spawnSync6(pm, argv, { stdio: "inherit" });
6978
7747
  if (res.status !== 0) {
6979
7748
  err(`Update failed (${pm} exited ${res.status ?? "with a signal"}). Reinstall manually: ${cliInstallCommand(pm, latest)}`);
6980
7749
  process.exitCode = 1;
@@ -6991,18 +7760,18 @@ var updateCommand = new Command24("update").description("Update the Openship CLI
6991
7760
  });
6992
7761
 
6993
7762
  // 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";
7763
+ import { Command as Command26 } from "commander";
7764
+ import { existsSync as existsSync13, readdirSync, readFileSync as readFileSync13, rmSync as rmSync4, statSync } from "fs";
7765
+ import { join as join15 } from "path";
6997
7766
  function listAssets() {
6998
- if (!existsSync11(RELEASES_DIR)) return [];
7767
+ if (!existsSync13(RELEASES_DIR)) return [];
6999
7768
  const out = [];
7000
7769
  for (const tag of readdirSync(RELEASES_DIR)) {
7001
7770
  const dir = releaseDir(tag);
7002
7771
  if (!statSync(dir).isDirectory()) continue;
7003
7772
  for (const name of readdirSync(dir)) {
7004
7773
  if (name.endsWith(".sha256")) continue;
7005
- const path2 = join13(dir, name);
7774
+ const path2 = join15(dir, name);
7006
7775
  const st = statSync(path2);
7007
7776
  if (!st.isFile()) continue;
7008
7777
  out.push({
@@ -7010,17 +7779,17 @@ function listAssets() {
7010
7779
  name,
7011
7780
  path: path2,
7012
7781
  size: st.size,
7013
- hasSidecar: existsSync11(`${path2}.sha256`)
7782
+ hasSidecar: existsSync13(`${path2}.sha256`)
7014
7783
  });
7015
7784
  }
7016
7785
  }
7017
7786
  return out;
7018
7787
  }
7019
- var pathCmd = new Command25("path").description("Print the cache directory path").action(() => {
7788
+ var pathCmd = new Command26("path").description("Print the cache directory path").action(() => {
7020
7789
  if (isJsonMode()) printJson({ path: CACHE_DIR });
7021
7790
  else process.stdout.write(CACHE_DIR + "\n");
7022
7791
  });
7023
- var listCmd6 = new Command25("list").alias("ls").description("List cached release assets").action(() => {
7792
+ var listCmd6 = new Command26("list").alias("ls").description("List cached release assets").action(() => {
7024
7793
  const assets = listAssets();
7025
7794
  printTable(
7026
7795
  assets.map((a) => ({
@@ -7032,7 +7801,7 @@ var listCmd6 = new Command25("list").alias("ls").description("List cached releas
7032
7801
  ["tag", "asset", "size", "sidecar"]
7033
7802
  );
7034
7803
  });
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) => {
7804
+ var verifyCmd2 = new Command26("verify").description("Re-hash cached assets and compare to their .sha256 sidecar").argument("[tag]", "Only verify assets under this release tag").action(async (tag) => {
7036
7805
  const assets = listAssets().filter((a) => !tag || a.tag === tag);
7037
7806
  const results = [];
7038
7807
  let bad = 0;
@@ -7041,7 +7810,7 @@ var verifyCmd2 = new Command25("verify").description("Re-hash cached assets and
7041
7810
  results.push({ tag: a.tag, asset: a.name, result: "no-sidecar" });
7042
7811
  continue;
7043
7812
  }
7044
- const expected = parseSha256(readFileSync12(`${a.path}.sha256`, "utf8"));
7813
+ const expected = parseSha256(readFileSync13(`${a.path}.sha256`, "utf8"));
7045
7814
  const actual = await hashFile(a.path);
7046
7815
  const okMatch = expected !== null && expected === actual;
7047
7816
  if (!okMatch) bad += 1;
@@ -7055,9 +7824,9 @@ var verifyCmd2 = new Command25("verify").description("Re-hash cached assets and
7055
7824
  }
7056
7825
  if (bad > 0) process.exit(1);
7057
7826
  });
7058
- var cleanCmd = new Command25("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
7827
+ var cleanCmd = new Command26("clean").description("Delete cached release assets").argument("[tag]", "Only remove this release tag (default: all)").action((tag) => {
7059
7828
  const target = tag ? releaseDir(tag) : RELEASES_DIR;
7060
- if (!existsSync11(target)) {
7829
+ if (!existsSync13(target)) {
7061
7830
  if (isJsonMode()) printJson({ removed: false, path: target });
7062
7831
  else info(` Nothing to clean (${target}).`);
7063
7832
  return;
@@ -7068,7 +7837,7 @@ var cleanCmd = new Command25("clean").description("Delete cached release assets"
7068
7837
  Removed ${target}
7069
7838
  `);
7070
7839
  });
7071
- var cacheCommand = new Command25("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
7840
+ var cacheCommand = new Command26("cache").description("Manage the local download cache (list/verify/clean/path)").action(() => {
7072
7841
  err("Specify a subcommand: path | list | verify | clean");
7073
7842
  process.exit(1);
7074
7843
  }).addCommand(pathCmd).addCommand(listCmd6).addCommand(verifyCmd2).addCommand(cleanCmd);
@@ -7076,11 +7845,10 @@ var cacheCommand = new Command25("cache").description("Manage the local download
7076
7845
  // src/commands/wizard.ts
7077
7846
  import chalk17 from "chalk";
7078
7847
  import open from "open";
7079
- import { createServer as createServer2 } from "http";
7080
7848
  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";
7849
+ import { existsSync as existsSync14, readFileSync as readFileSync14 } from "fs";
7850
+ import { homedir as homedir10 } from "os";
7851
+ import { join as join16 } from "path";
7084
7852
  import {
7085
7853
  intro as intro2,
7086
7854
  outro as outro2,
@@ -7135,10 +7903,10 @@ async function bootstrapAdmin(apiPort, admin) {
7135
7903
  }
7136
7904
  function lastServiceError() {
7137
7905
  for (const name of ["up.err.log", "up.log"]) {
7138
- const p = join14(homedir9(), ".openship", "logs", name);
7139
- if (!existsSync12(p)) continue;
7906
+ const p = join16(homedir10(), ".openship", "logs", name);
7907
+ if (!existsSync14(p)) continue;
7140
7908
  try {
7141
- const lines = readFileSync13(p, "utf8").trim().split("\n");
7909
+ const lines = readFileSync14(p, "utf8").trim().split("\n");
7142
7910
  const hit = [...lines].reverse().find((l) => /error|locked|EADDRINUSE|throw|cannot/i.test(l));
7143
7911
  if (hit) return hit.trim().slice(0, 200);
7144
7912
  } catch {
@@ -7198,58 +7966,46 @@ async function connectOpenshipCloud(port) {
7198
7966
  }
7199
7967
  const verifier = b64url(randomBytes2(32));
7200
7968
  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);
7969
+ const state = b64url(randomBytes2(24));
7970
+ const apiBase = cloudApiUrl.replace(/\/$/, "");
7971
+ const handoff = `${apiBase}/api/cloud/connect-handoff?redirect=${encodeURIComponent(apiBase)}&state=${encodeURIComponent(state)}&code_challenge=${challenge}&mode=device`;
7972
+ const overSsh = !!(process.env.SSH_CONNECTION || process.env.SSH_TTY || process.env.SSH_CLIENT);
7973
+ note(handoff, "Open this URL in your browser to authorize (then click Authorize)");
7974
+ if (!overSsh) void open(handoff).catch(() => {
7238
7975
  });
7239
7976
  const s = spinner2();
7240
- s.start("Waiting for Openship Cloud authorization in your browser");
7241
- const code = await codePromise;
7977
+ s.start("Waiting for you to authorize in the browser");
7978
+ let code = null;
7979
+ const deadline = Date.now() + 3e5;
7980
+ while (Date.now() < deadline) {
7981
+ await new Promise((r) => setTimeout(r, 2500));
7982
+ try {
7983
+ const res2 = await fetch(
7984
+ `${apiBase}/api/cloud/connect-poll?state=${encodeURIComponent(state)}`,
7985
+ { signal: AbortSignal.timeout(5e3) }
7986
+ );
7987
+ if (!res2.ok) continue;
7988
+ const data = await res2.json();
7989
+ if (data.status === "ready" && data.code) {
7990
+ code = data.code;
7991
+ break;
7992
+ }
7993
+ } catch {
7994
+ }
7995
+ }
7242
7996
  if (!code) {
7243
- s.stop("Openship Cloud wasn't authorized.", 1);
7997
+ s.stop("Openship Cloud wasn't authorized in time \u2014 re-run the connect step to try again.", 1);
7244
7998
  return null;
7245
7999
  }
7246
- s.message("Linking this instance to Openship Cloud");
8000
+ s.stop("Authorized.");
8001
+ const linking = spinner2();
8002
+ linking.start("Linking this instance to Openship Cloud");
7247
8003
  const res = await internalPost(port, "/api/system/cloud-connect", { code, codeVerifier: verifier });
7248
8004
  if (!res.ok) {
7249
- s.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
8005
+ linking.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
7250
8006
  return null;
7251
8007
  }
7252
- s.stop(`Connected to Openship Cloud${res.data?.email ? ` as ${res.data.email}` : ""}.`);
8008
+ linking.stop(`Connected to Openship Cloud${res.data?.email ? ` as ${res.data.email}` : ""}.`);
7253
8009
  return { email: res.data?.email ?? null };
7254
8010
  }
7255
8011
  async function promptLocalAdmin() {
@@ -7513,7 +8269,7 @@ async function runWizard() {
7513
8269
  domainPlan = { type: "byo", hostname };
7514
8270
  break planning;
7515
8271
  }
7516
- const uiTag = `v${"0.2.2"}`;
8272
+ const uiTag = `v${"0.2.3"}`;
7517
8273
  const dl = spinner2();
7518
8274
  dl.start("Pulling the Openship dist from GitHub");
7519
8275
  try {
@@ -7714,9 +8470,9 @@ ${pad("Login")}${admin.email} ${chalk17.dim("(email + password you set)")}
7714
8470
  );
7715
8471
  }
7716
8472
  function storedPorts() {
7717
- const p = join14(homedir9(), ".openship", "ports.json");
8473
+ const p = join16(homedir10(), ".openship", "ports.json");
7718
8474
  try {
7719
- return existsSync12(p) ? JSON.parse(readFileSync13(p, "utf8")) : {};
8475
+ return existsSync14(p) ? JSON.parse(readFileSync14(p, "utf8")) : {};
7720
8476
  } catch {
7721
8477
  return {};
7722
8478
  }
@@ -7788,8 +8544,8 @@ ${chalk17.dim("Dashboard".padEnd(11))}${dashUrl}
7788
8544
  }
7789
8545
 
7790
8546
  // 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) => {
8547
+ var program = new Command27();
8548
+ program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.2.3").option("--json", "Machine-readable JSON output (stdout data only)").hook("preAction", (thisCommand) => {
7793
8549
  if (thisCommand.opts().json) setJsonMode(true);
7794
8550
  }).action(async () => {
7795
8551
  if (serviceStatus().installed) await runControl();
@@ -7803,6 +8559,7 @@ program.addCommand(openCommand);
7803
8559
  program.addCommand(loginCommand);
7804
8560
  program.addCommand(logoutCommand);
7805
8561
  program.addCommand(initCommand);
8562
+ program.addCommand(configCommand);
7806
8563
  program.addCommand(contextCommand);
7807
8564
  program.addCommand(statusCommand);
7808
8565
  program.addCommand(doctorCommand);