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/README.md +208 -0
- package/dist/index.js +1072 -315
- package/dist/server/index.js +4498 -55391
- package/dist/server/migrations/0056_moaning_daredevil.sql +2 -0
- package/dist/server/migrations/meta/0056_snapshot.json +9191 -0
- package/dist/server/migrations/meta/_journal.json +7 -0
- package/package.json +8 -4
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
|
|
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/
|
|
1946
|
-
function
|
|
1947
|
-
|
|
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 =
|
|
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
|
|
2384
|
-
import { homedir as
|
|
2385
|
-
import { dirname as dirname2, join as
|
|
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((
|
|
2436
|
-
file.on("finish", () =>
|
|
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((
|
|
3401
|
+
return new Promise((resolve3) => {
|
|
2842
3402
|
const srv = createServer();
|
|
2843
|
-
srv.once("error", () =>
|
|
2844
|
-
srv.listen(port, "127.0.0.1", () => srv.close(() =>
|
|
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((
|
|
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 ?
|
|
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 =
|
|
2928
|
-
var
|
|
3582
|
+
var SERVER_DIR = join7(DIST_DIR, "server");
|
|
3583
|
+
var OS_DIR4 = join7(homedir6(), ".openship");
|
|
2929
3584
|
function ensureAuthSecret() {
|
|
2930
|
-
const path2 =
|
|
2931
|
-
if (
|
|
2932
|
-
|
|
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 =
|
|
2939
|
-
if (
|
|
2940
|
-
|
|
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
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
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 ||
|
|
3049
|
-
|
|
3050
|
-
const logDir =
|
|
3051
|
-
|
|
3052
|
-
const instanceLogPath =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
3281
|
-
const linkPath =
|
|
3282
|
-
if (
|
|
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
|
-
|
|
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/
|
|
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
|
|
3359
|
-
var useCmd = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4167
|
+
import { Command as Command9 } from "commander";
|
|
3399
4168
|
import chalk7 from "chalk";
|
|
3400
|
-
import { readFileSync as
|
|
3401
|
-
import { homedir as
|
|
3402
|
-
import { join as
|
|
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(
|
|
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
|
|
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
|
|
3446
|
-
import { existsSync as
|
|
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
|
|
4227
|
+
var doctorCommand = new Command10("doctor").description("Diagnose the CLI setup (config, active context, runtime)").action(async () => {
|
|
3459
4228
|
const checks = [];
|
|
3460
|
-
const hasConfig =
|
|
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
|
|
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
|
|
3514
|
-
import { join as
|
|
3515
|
-
var LINK_REL =
|
|
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 =
|
|
3521
|
-
if (
|
|
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(
|
|
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
|
|
4307
|
+
import { readFileSync as readFileSync8, existsSync as existsSync11, rmSync as rmSync3 } from "fs";
|
|
3539
4308
|
import { tmpdir } from "os";
|
|
3540
|
-
import { join as
|
|
4309
|
+
import { join as join12, basename } from "path";
|
|
3541
4310
|
function detectPackageManager(dir) {
|
|
3542
|
-
if (
|
|
3543
|
-
if (
|
|
3544
|
-
if (
|
|
3545
|
-
if (
|
|
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 (
|
|
3550
|
-
if (
|
|
3551
|
-
if (
|
|
3552
|
-
if (
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
4620
|
+
import { Command as Command12 } from "commander";
|
|
3852
4621
|
import { createInterface as createInterface3 } from "readline";
|
|
3853
|
-
function
|
|
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
|
|
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((
|
|
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
|
|
3878
|
-
|
|
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:
|
|
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
|
|
3901
|
-
|
|
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
|
|
3922
|
-
|
|
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
|
|
3928
|
-
|
|
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
|
|
3934
|
-
|
|
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
|
|
3943
|
-
|
|
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
|
|
3949
|
-
|
|
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
|
|
3959
|
-
|
|
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
|
|
3965
|
-
|
|
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
|
|
3971
|
-
|
|
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
|
|
3977
|
-
|
|
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
|
|
3983
|
-
|
|
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
|
|
3993
|
-
|
|
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
|
|
4002
|
-
|
|
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
|
|
4011
|
-
var deploymentCommand = new
|
|
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
|
|
4015
|
-
var logsCommand = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5271
|
+
import { Command as Command15 } from "commander";
|
|
4503
5272
|
import chalk10 from "chalk";
|
|
4504
|
-
import { spawnSync as
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6297
|
+
import { Command as Command18 } from "commander";
|
|
5529
6298
|
import ora5 from "ora";
|
|
5530
|
-
import { readFileSync as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6935
|
+
var postmasterCmd = new Command19("postmaster").description("Manage the postmaster mailbox");
|
|
6167
6936
|
postmasterCmd.addCommand(
|
|
6168
|
-
new
|
|
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
|
|
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
|
|
6966
|
+
import { Command as Command20 } from "commander";
|
|
6198
6967
|
import ora7 from "ora";
|
|
6199
|
-
import { readFileSync as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
6647
|
-
var apiCommand = new
|
|
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
|
|
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
|
|
6685
|
-
import { homedir as
|
|
6686
|
-
import { join as
|
|
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(
|
|
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
|
|
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
|
|
6743
|
-
import { chmodSync as chmodSync2, existsSync as
|
|
6744
|
-
import { spawn as
|
|
6745
|
-
import { homedir as
|
|
6746
|
-
import { join as
|
|
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 =
|
|
7527
|
+
const homeApps = join14(homedir9(), "Applications");
|
|
6759
7528
|
let dest = homeApps;
|
|
6760
7529
|
try {
|
|
6761
|
-
|
|
7530
|
+
mkdirSync9(homeApps, { recursive: true });
|
|
6762
7531
|
} catch {
|
|
6763
7532
|
dest = "/Applications";
|
|
6764
7533
|
}
|
|
6765
|
-
const attach =
|
|
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 =
|
|
7540
|
+
let target = join14(dest, "Openship.app");
|
|
6772
7541
|
try {
|
|
6773
|
-
const appInDmg =
|
|
6774
|
-
if (!
|
|
6775
|
-
|
|
6776
|
-
let copy =
|
|
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 =
|
|
6779
|
-
|
|
6780
|
-
copy =
|
|
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
|
-
|
|
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 ||
|
|
6794
|
-
const target =
|
|
6795
|
-
|
|
6796
|
-
const expand =
|
|
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
|
-
|
|
7580
|
+
spawnSync5("open", [target]);
|
|
6812
7581
|
return;
|
|
6813
7582
|
}
|
|
6814
7583
|
if (kind === "appimage") {
|
|
6815
|
-
const child =
|
|
7584
|
+
const child = spawn3(target, [], { detached: true, stdio: "ignore" });
|
|
6816
7585
|
child.on("error", () => {
|
|
6817
|
-
|
|
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 =
|
|
6823
|
-
const path2 =
|
|
6824
|
-
|
|
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
|
|
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 =
|
|
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 &&
|
|
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 (
|
|
6878
|
-
sidecarBody =
|
|
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
|
-
|
|
6902
|
-
|
|
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
|
|
6942
|
-
import { spawnSync as
|
|
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 =
|
|
7714
|
+
const hasBun = spawnSync6("bun", ["--version"], { stdio: "ignore" }).status === 0;
|
|
6946
7715
|
return hasBun ? "bun" : "npm";
|
|
6947
7716
|
}
|
|
6948
|
-
var updateCommand = new
|
|
6949
|
-
const current = "0.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 =
|
|
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
|
|
6995
|
-
import { existsSync as
|
|
6996
|
-
import { join as
|
|
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 (!
|
|
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 =
|
|
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:
|
|
7782
|
+
hasSidecar: existsSync13(`${path2}.sha256`)
|
|
7014
7783
|
});
|
|
7015
7784
|
}
|
|
7016
7785
|
}
|
|
7017
7786
|
return out;
|
|
7018
7787
|
}
|
|
7019
|
-
var pathCmd = new
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
7082
|
-
import { homedir as
|
|
7083
|
-
import { join as
|
|
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 =
|
|
7139
|
-
if (!
|
|
7906
|
+
const p = join16(homedir10(), ".openship", "logs", name);
|
|
7907
|
+
if (!existsSync14(p)) continue;
|
|
7140
7908
|
try {
|
|
7141
|
-
const lines =
|
|
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(
|
|
7202
|
-
const
|
|
7203
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
7206
|
-
|
|
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
|
|
7241
|
-
|
|
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.
|
|
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
|
-
|
|
8005
|
+
linking.stop(`Couldn't link Openship Cloud: ${res.data?.error || "failed"}`, 1);
|
|
7250
8006
|
return null;
|
|
7251
8007
|
}
|
|
7252
|
-
|
|
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.
|
|
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 =
|
|
8473
|
+
const p = join16(homedir10(), ".openship", "ports.json");
|
|
7718
8474
|
try {
|
|
7719
|
-
return
|
|
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
|
|
7792
|
-
program.name("openship").description("Openship CLI \u2014 install, run, and manage Openship from your terminal").version("0.2.
|
|
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);
|