@marina-cloud/cli 0.0.3 → 0.0.4
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 +11 -4
- package/dist/marina.mjs +176 -88
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,9 +11,15 @@ marina setup
|
|
|
11
11
|
marina deploy
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
For a complete first run, including the deployment skill and demo app:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install -g @marina-cloud/cli && marina setup --skills --deploy-demo --json
|
|
18
|
+
```
|
|
19
|
+
|
|
14
20
|
`marina setup` opens Clerk in the browser and delivers the resulting API key
|
|
15
|
-
directly back to the CLI. For unattended environments, `MARINA_TOKEN`
|
|
16
|
-
|
|
21
|
+
directly back to the CLI. For unattended environments, `MARINA_TOKEN` is the
|
|
22
|
+
process-only credential override.
|
|
17
23
|
|
|
18
24
|
The saved credential lives in `~/.marina/profile` with user-only permissions.
|
|
19
25
|
Use `marina profile` to inspect the active profile and `marina logout` to remove
|
|
@@ -26,8 +32,9 @@ marina skills install
|
|
|
26
32
|
marina skills install --agent codex
|
|
27
33
|
```
|
|
28
34
|
|
|
29
|
-
The CLI
|
|
30
|
-
when a newer release is available.
|
|
35
|
+
The CLI reads release metadata from Marina's control plane and prints an
|
|
36
|
+
advisory update command at most once per day when a newer release is available.
|
|
37
|
+
It never updates itself.
|
|
31
38
|
|
|
32
39
|
## Agent-friendly output
|
|
33
40
|
|
package/dist/marina.mjs
CHANGED
|
@@ -16,14 +16,151 @@ import {
|
|
|
16
16
|
} from "node:fs";
|
|
17
17
|
import { homedir } from "node:os";
|
|
18
18
|
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
// package.json
|
|
21
|
+
var package_default = {
|
|
22
|
+
name: "@marina-cloud/cli",
|
|
23
|
+
version: "0.0.4",
|
|
24
|
+
description: "Command-line client for Marina Cloud",
|
|
25
|
+
homepage: "https://github.com/marina-hq/marina#readme",
|
|
26
|
+
bugs: {
|
|
27
|
+
url: "https://github.com/marina-hq/marina/issues"
|
|
28
|
+
},
|
|
29
|
+
license: "Apache-2.0",
|
|
30
|
+
repository: {
|
|
31
|
+
type: "git",
|
|
32
|
+
url: "git+https://github.com/marina-hq/marina.git",
|
|
33
|
+
directory: "packages/cli"
|
|
34
|
+
},
|
|
35
|
+
bin: {
|
|
36
|
+
marina: "dist/marina.mjs"
|
|
37
|
+
},
|
|
38
|
+
files: [
|
|
39
|
+
"dist"
|
|
40
|
+
],
|
|
41
|
+
type: "module",
|
|
42
|
+
publishConfig: {
|
|
43
|
+
access: "public",
|
|
44
|
+
provenance: true
|
|
45
|
+
},
|
|
46
|
+
scripts: {
|
|
47
|
+
build: 'esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --loader:.md=text --loader:.yaml=text --loader:.html=text --loader:.css=text --loader:.txt=text --outfile=dist/marina.mjs --banner:js="#!/usr/bin/env node" && chmod +x dist/marina.mjs',
|
|
48
|
+
typecheck: "tsc --noEmit",
|
|
49
|
+
test: "pnpm build && node --experimental-strip-types --test src/*.test.ts",
|
|
50
|
+
prepack: "pnpm build"
|
|
51
|
+
},
|
|
52
|
+
devDependencies: {
|
|
53
|
+
"@types/node": "^26.2.0",
|
|
54
|
+
esbuild: "^0.25.0",
|
|
55
|
+
fflate: "^0.8.2",
|
|
56
|
+
typescript: "^5.9.0"
|
|
57
|
+
},
|
|
58
|
+
engines: {
|
|
59
|
+
node: ">=22"
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/identity.ts
|
|
64
|
+
var CLI_VERSION = package_default.version;
|
|
65
|
+
var cliRequestHeaders = () => ({
|
|
66
|
+
"user-agent": `marina-cli/${CLI_VERSION}`,
|
|
67
|
+
"x-marina-client": "cli",
|
|
68
|
+
"x-marina-client-version": CLI_VERSION
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// src/config.ts
|
|
19
72
|
var MARINA_HOME = process.env.MARINA_HOME ?? join(homedir(), ".marina");
|
|
20
73
|
var PROFILE = join(MARINA_HOME, "profile");
|
|
21
|
-
var
|
|
74
|
+
var PRODUCTION_CONTROL_PLANE = "https://marina.cloud";
|
|
75
|
+
var CONTROL_PLANE_CACHE_MS = 60 * 60 * 1e3;
|
|
76
|
+
var resolvedControlPlane = null;
|
|
77
|
+
function normalizeApiUrl(value) {
|
|
78
|
+
const url = new URL(value);
|
|
79
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
80
|
+
throw new Error("the Marina control plane must use http or https");
|
|
81
|
+
}
|
|
82
|
+
if (url.username || url.password || url.search || url.hash || url.pathname !== "/") {
|
|
83
|
+
throw new Error("the Marina control plane must be an origin without a path");
|
|
84
|
+
}
|
|
85
|
+
if (url.protocol === "http:" && url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.hostname !== "[::1]") {
|
|
86
|
+
throw new Error("the Marina control plane must use https unless it is local");
|
|
87
|
+
}
|
|
88
|
+
return url.origin;
|
|
89
|
+
}
|
|
90
|
+
var controlPlaneHost = () => normalizeApiUrl(
|
|
91
|
+
process.env.MARINA_CONTROL_PLANE ?? readProfile().control_plane_host ?? PRODUCTION_CONTROL_PLANE
|
|
92
|
+
);
|
|
93
|
+
function cachedControlPlane(host = controlPlaneHost()) {
|
|
94
|
+
const cached = readProfile().control_plane;
|
|
95
|
+
if (!cached || cached.host !== host) return null;
|
|
96
|
+
try {
|
|
97
|
+
normalizeApiUrl(cached.api_url);
|
|
98
|
+
normalizeApiUrl(cached.dashboard_url);
|
|
99
|
+
return cached;
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
var apiUrl = () => normalizeApiUrl(
|
|
105
|
+
process.env.MARINA_API ?? resolvedControlPlane?.api_url ?? cachedControlPlane()?.api_url ?? controlPlaneHost()
|
|
106
|
+
);
|
|
22
107
|
var dashboardUrl = () => {
|
|
23
|
-
if (process.env.MARINA_DASHBOARD) return process.env.MARINA_DASHBOARD
|
|
24
|
-
|
|
25
|
-
|
|
108
|
+
if (process.env.MARINA_DASHBOARD) return normalizeApiUrl(process.env.MARINA_DASHBOARD);
|
|
109
|
+
return normalizeApiUrl(
|
|
110
|
+
resolvedControlPlane?.dashboard_url ?? cachedControlPlane()?.dashboard_url ?? controlPlaneHost()
|
|
111
|
+
);
|
|
26
112
|
};
|
|
113
|
+
async function resolveControlPlane() {
|
|
114
|
+
const host = controlPlaneHost();
|
|
115
|
+
const cached = cachedControlPlane(host);
|
|
116
|
+
const checkedAt = cached ? Date.parse(cached.checked_at) : Number.NaN;
|
|
117
|
+
if (cached && Number.isFinite(checkedAt) && Date.now() - checkedAt < CONTROL_PLANE_CACHE_MS) {
|
|
118
|
+
resolvedControlPlane = {
|
|
119
|
+
api_url: cached.api_url,
|
|
120
|
+
dashboard_url: cached.dashboard_url,
|
|
121
|
+
latest_cli_version: cached.latest_cli_version
|
|
122
|
+
};
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (process.env.MARINA_DISABLE_CONTROL_PLANE_DISCOVERY === "1") return;
|
|
126
|
+
try {
|
|
127
|
+
const response = await fetch(`${host}/.well-known/marina`, {
|
|
128
|
+
headers: { ...cliRequestHeaders(), accept: "application/json" },
|
|
129
|
+
signal: AbortSignal.timeout(3e3)
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok)
|
|
132
|
+
throw new Error(`control-plane discovery failed (${String(response.status)})`);
|
|
133
|
+
const body = await response.json();
|
|
134
|
+
if (body.schema_version !== 1 || typeof body.api_url !== "string" || typeof body.dashboard_url !== "string") {
|
|
135
|
+
throw new Error("control-plane discovery returned an invalid response");
|
|
136
|
+
}
|
|
137
|
+
const discovered = {
|
|
138
|
+
api_url: normalizeApiUrl(body.api_url),
|
|
139
|
+
dashboard_url: normalizeApiUrl(body.dashboard_url),
|
|
140
|
+
latest_cli_version: typeof body.clients?.cli?.latest_version === "string" ? body.clients.cli.latest_version : null
|
|
141
|
+
};
|
|
142
|
+
resolvedControlPlane = discovered;
|
|
143
|
+
writeProfile({
|
|
144
|
+
...readProfile(),
|
|
145
|
+
control_plane: {
|
|
146
|
+
host,
|
|
147
|
+
checked_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
148
|
+
...discovered
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
} catch {
|
|
152
|
+
if (cached) {
|
|
153
|
+
resolvedControlPlane = {
|
|
154
|
+
api_url: cached.api_url,
|
|
155
|
+
dashboard_url: cached.dashboard_url,
|
|
156
|
+
latest_cli_version: cached.latest_cli_version
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function latestCliVersion() {
|
|
162
|
+
return resolvedControlPlane?.latest_cli_version ?? cachedControlPlane()?.latest_cli_version ?? null;
|
|
163
|
+
}
|
|
27
164
|
var profilePath = () => PROFILE;
|
|
28
165
|
function readProfile() {
|
|
29
166
|
try {
|
|
@@ -90,7 +227,7 @@ async function request(path, init) {
|
|
|
90
227
|
if (!token) throw new ApiError("unauthenticated", "not signed in \u2014 run `marina setup`", 401);
|
|
91
228
|
const res = await fetch(`${apiUrl()}${path}`, {
|
|
92
229
|
...init,
|
|
93
|
-
headers: { authorization: `Bearer ${token}`, ...init?.headers }
|
|
230
|
+
headers: { ...cliRequestHeaders(), authorization: `Bearer ${token}`, ...init?.headers }
|
|
94
231
|
});
|
|
95
232
|
const body = await res.json().catch(() => ({}));
|
|
96
233
|
if (!res.ok) {
|
|
@@ -107,7 +244,7 @@ async function exchangeCliLogin(code, codeVerifier) {
|
|
|
107
244
|
try {
|
|
108
245
|
res = await fetch(`${apiUrl()}/cli/auth/exchange`, {
|
|
109
246
|
method: "POST",
|
|
110
|
-
headers: { "content-type": "application/json" },
|
|
247
|
+
headers: { ...cliRequestHeaders(), "content-type": "application/json" },
|
|
111
248
|
body: JSON.stringify({ code, code_verifier: codeVerifier }),
|
|
112
249
|
signal: AbortSignal.timeout(LOGIN_EXCHANGE_TIMEOUT_MS)
|
|
113
250
|
});
|
|
@@ -1769,52 +1906,8 @@ function installSkills(agent) {
|
|
|
1769
1906
|
return selected.map((target) => install(target, true));
|
|
1770
1907
|
}
|
|
1771
1908
|
|
|
1772
|
-
// package.json
|
|
1773
|
-
var package_default = {
|
|
1774
|
-
name: "@marina-cloud/cli",
|
|
1775
|
-
version: "0.0.3",
|
|
1776
|
-
description: "Command-line client for Marina Cloud",
|
|
1777
|
-
homepage: "https://github.com/marina-hq/marina#readme",
|
|
1778
|
-
bugs: {
|
|
1779
|
-
url: "https://github.com/marina-hq/marina/issues"
|
|
1780
|
-
},
|
|
1781
|
-
license: "Apache-2.0",
|
|
1782
|
-
repository: {
|
|
1783
|
-
type: "git",
|
|
1784
|
-
url: "git+https://github.com/marina-hq/marina.git",
|
|
1785
|
-
directory: "packages/cli"
|
|
1786
|
-
},
|
|
1787
|
-
bin: {
|
|
1788
|
-
marina: "dist/marina.mjs"
|
|
1789
|
-
},
|
|
1790
|
-
files: [
|
|
1791
|
-
"dist"
|
|
1792
|
-
],
|
|
1793
|
-
type: "module",
|
|
1794
|
-
publishConfig: {
|
|
1795
|
-
access: "public",
|
|
1796
|
-
provenance: true
|
|
1797
|
-
},
|
|
1798
|
-
scripts: {
|
|
1799
|
-
build: 'esbuild src/index.ts --bundle --platform=node --target=node22 --format=esm --loader:.md=text --loader:.yaml=text --loader:.html=text --loader:.css=text --loader:.txt=text --outfile=dist/marina.mjs --banner:js="#!/usr/bin/env node" && chmod +x dist/marina.mjs',
|
|
1800
|
-
typecheck: "tsc --noEmit",
|
|
1801
|
-
test: "pnpm build && node --experimental-strip-types --test src/*.test.ts",
|
|
1802
|
-
prepack: "pnpm build"
|
|
1803
|
-
},
|
|
1804
|
-
devDependencies: {
|
|
1805
|
-
"@types/node": "^26.2.0",
|
|
1806
|
-
esbuild: "^0.25.0",
|
|
1807
|
-
fflate: "^0.8.2",
|
|
1808
|
-
typescript: "^5.9.0"
|
|
1809
|
-
},
|
|
1810
|
-
engines: {
|
|
1811
|
-
node: ">=22"
|
|
1812
|
-
}
|
|
1813
|
-
};
|
|
1814
|
-
|
|
1815
1909
|
// src/update.ts
|
|
1816
|
-
var
|
|
1817
|
-
var LATEST_URL = "https://registry.npmjs.org/@marina-cloud%2Fcli/latest";
|
|
1910
|
+
var NAG_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
1818
1911
|
function isNewerVersion(candidate, current) {
|
|
1819
1912
|
const parse = (version) => version.replace(/^v/, "").split("-", 1)[0].split(".").map((part) => Number.parseInt(part, 10));
|
|
1820
1913
|
const left = parse(candidate);
|
|
@@ -1829,40 +1922,28 @@ function isNewerVersion(candidate, current) {
|
|
|
1829
1922
|
async function availableUpdate() {
|
|
1830
1923
|
if (process.env.MARINA_DISABLE_UPDATE_CHECK) return null;
|
|
1831
1924
|
const current = package_default.version;
|
|
1925
|
+
const latest = latestCliVersion();
|
|
1926
|
+
if (!latest || !isNewerVersion(latest, current)) return null;
|
|
1832
1927
|
const profile = readProfile();
|
|
1833
|
-
const
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
});
|
|
1841
|
-
if (response.ok) {
|
|
1842
|
-
const body = await response.json();
|
|
1843
|
-
if (body.version) {
|
|
1844
|
-
latest = body.version;
|
|
1845
|
-
writeProfile({
|
|
1846
|
-
...readProfile(),
|
|
1847
|
-
update: { checked_at: (/* @__PURE__ */ new Date()).toISOString(), latest }
|
|
1848
|
-
});
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
} catch {
|
|
1852
|
-
}
|
|
1853
|
-
}
|
|
1854
|
-
return latest && isNewerVersion(latest, current) ? {
|
|
1928
|
+
const notifiedAt = profile.update?.notified_at ? Date.parse(profile.update.notified_at) : Number.NaN;
|
|
1929
|
+
if (Number.isFinite(notifiedAt) && Date.now() - notifiedAt < NAG_INTERVAL_MS) return null;
|
|
1930
|
+
writeProfile({
|
|
1931
|
+
...profile,
|
|
1932
|
+
update: { latest, notified_at: (/* @__PURE__ */ new Date()).toISOString() }
|
|
1933
|
+
});
|
|
1934
|
+
return {
|
|
1855
1935
|
current,
|
|
1856
1936
|
latest,
|
|
1857
1937
|
command: "npm install -g @marina-cloud/cli@latest"
|
|
1858
|
-
}
|
|
1938
|
+
};
|
|
1859
1939
|
}
|
|
1860
1940
|
|
|
1861
1941
|
// src/index.ts
|
|
1862
1942
|
var HELP = `${bold("marina")} \u2014 deploy internal apps
|
|
1863
1943
|
|
|
1864
1944
|
marina setup sign in with Clerk in your browser
|
|
1865
|
-
--
|
|
1945
|
+
--skills install the Marina deployment skill
|
|
1946
|
+
--deploy-demo deploy Hello Marina after signing in
|
|
1866
1947
|
marina logout remove the saved credential
|
|
1867
1948
|
marina profile show where this CLI is signed in
|
|
1868
1949
|
marina skills install install or update the Marina deployment skill
|
|
@@ -1881,8 +1962,7 @@ var HELP = `${bold("marina")} \u2014 deploy internal apps
|
|
|
1881
1962
|
marina list apps in your workspace
|
|
1882
1963
|
marina open open this project's app
|
|
1883
1964
|
|
|
1884
|
-
${dim("--json machine-readable result on stdout, progress on stderr")}
|
|
1885
|
-
${dim(`API: ${apiUrl()} (override with MARINA_API)`)}`;
|
|
1965
|
+
${dim("--json machine-readable result on stdout, progress on stderr")}`;
|
|
1886
1966
|
var ANSI_ESCAPE = new RegExp(`${String.fromCodePoint(27)}\\[[0-9;]*m`, "g");
|
|
1887
1967
|
var shouldCheckForUpdates = false;
|
|
1888
1968
|
function targetApp(flag) {
|
|
@@ -1895,7 +1975,7 @@ function targetApp(flag) {
|
|
|
1895
1975
|
return app;
|
|
1896
1976
|
}
|
|
1897
1977
|
var shortDigest = (digest) => digest?.slice(7, 14) ?? "\u2014";
|
|
1898
|
-
async function deploy(dirArg, flags) {
|
|
1978
|
+
async function deploy(dirArg, flags, emitResult = true) {
|
|
1899
1979
|
let dir = null;
|
|
1900
1980
|
let link = null;
|
|
1901
1981
|
let target;
|
|
@@ -1984,7 +2064,7 @@ async function deploy(dirArg, flags) {
|
|
|
1984
2064
|
if (deployed.version_url) say(` try this version: ${deployed.version_url}`);
|
|
1985
2065
|
say(dim(" a publisher can approve it from the Marina inbox"));
|
|
1986
2066
|
}
|
|
1987
|
-
|
|
2067
|
+
const payload = {
|
|
1988
2068
|
command: "deploy",
|
|
1989
2069
|
status: "succeeded",
|
|
1990
2070
|
app: slug,
|
|
@@ -1995,7 +2075,9 @@ async function deploy(dirArg, flags) {
|
|
|
1995
2075
|
preparation: deployed.preparation,
|
|
1996
2076
|
live: published,
|
|
1997
2077
|
demo: dirArg === "demo"
|
|
1998
|
-
}
|
|
2078
|
+
};
|
|
2079
|
+
if (emitResult) result(payload);
|
|
2080
|
+
return payload;
|
|
1999
2081
|
}
|
|
2000
2082
|
async function status(appFlag) {
|
|
2001
2083
|
const app = targetApp(appFlag);
|
|
@@ -2111,12 +2193,13 @@ async function main() {
|
|
|
2111
2193
|
const { positionals, values } = parseArgs({
|
|
2112
2194
|
allowPositionals: true,
|
|
2113
2195
|
options: {
|
|
2114
|
-
token: { type: "string" },
|
|
2115
2196
|
name: { type: "string" },
|
|
2116
2197
|
app: { type: "string" },
|
|
2117
2198
|
to: { type: "string" },
|
|
2118
2199
|
plain: { type: "boolean" },
|
|
2119
2200
|
agent: { type: "string" },
|
|
2201
|
+
skills: { type: "boolean" },
|
|
2202
|
+
"deploy-demo": { type: "boolean" },
|
|
2120
2203
|
json: { type: "boolean" },
|
|
2121
2204
|
help: { type: "boolean", short: "h" }
|
|
2122
2205
|
}
|
|
@@ -2128,14 +2211,12 @@ async function main() {
|
|
|
2128
2211
|
result({ command: "help", usage: HELP.replaceAll(ANSI_ESCAPE, "") });
|
|
2129
2212
|
return;
|
|
2130
2213
|
}
|
|
2214
|
+
await resolveControlPlane();
|
|
2131
2215
|
shouldCheckForUpdates = true;
|
|
2132
2216
|
switch (command) {
|
|
2133
2217
|
case "setup":
|
|
2134
2218
|
case "login": {
|
|
2135
|
-
|
|
2136
|
-
if (token) {
|
|
2137
|
-
saveToken(token);
|
|
2138
|
-
} else {
|
|
2219
|
+
if (!process.env.MARINA_TOKEN) {
|
|
2139
2220
|
let lastReportedRemaining = -1;
|
|
2140
2221
|
const signedIn = await loginWithBrowser(
|
|
2141
2222
|
(url) => {
|
|
@@ -2166,11 +2247,18 @@ async function main() {
|
|
|
2166
2247
|
saveToken(signedIn.token);
|
|
2167
2248
|
}
|
|
2168
2249
|
say(`${green("ok")} signed in ${dim(`(${apiUrl()})`)}`);
|
|
2250
|
+
const installed = values.skills ? installSkills(values.agent) : void 0;
|
|
2251
|
+
for (const skill of installed ?? []) {
|
|
2252
|
+
say(`${green("ok")} ${skill.status} Marina skill for ${skill.agent} ${dim(skill.path)}`);
|
|
2253
|
+
}
|
|
2254
|
+
const demo = values["deploy-demo"] ? await deploy("demo", { name: values.name }, false) : void 0;
|
|
2169
2255
|
result({
|
|
2170
2256
|
command: "setup",
|
|
2171
2257
|
signed_in: true,
|
|
2172
2258
|
api: apiUrl(),
|
|
2173
|
-
profile: profilePath()
|
|
2259
|
+
profile: profilePath(),
|
|
2260
|
+
...installed ? { skills: installed } : {},
|
|
2261
|
+
...demo ? { deploy: demo } : {}
|
|
2174
2262
|
});
|
|
2175
2263
|
return;
|
|
2176
2264
|
}
|
|
@@ -2266,8 +2354,8 @@ async function run() {
|
|
|
2266
2354
|
if (!shouldCheckForUpdates) return;
|
|
2267
2355
|
const update = await availableUpdate();
|
|
2268
2356
|
if (update) {
|
|
2269
|
-
const
|
|
2270
|
-
note(isJsonMode() ?
|
|
2357
|
+
const detail = `Marina CLI ${update.latest} is available (current ${update.current}) \u2014 ${update.command}`;
|
|
2358
|
+
note(isJsonMode() ? detail : `${bold("Update available:")} ${detail}`);
|
|
2271
2359
|
}
|
|
2272
2360
|
}
|
|
2273
2361
|
run().catch((error) => {
|