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