@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.
Files changed (3) hide show
  1. package/README.md +11 -4
  2. package/dist/marina.mjs +176 -88
  3. 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` or
16
- `marina setup --token mar_…` is the explicit fallback.
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 checks npm at most once per day and prints an advisory update command
30
- when a newer release is available. It never updates itself.
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 apiUrl = () => (process.env.MARINA_API ?? "https://v1.marina.cloud").replace(/\/$/, "");
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.replace(/\/$/, "");
24
- const api = new URL(apiUrl());
25
- return api.hostname === "localhost" || api.hostname === "127.0.0.1" ? "http://localhost:5173" : api.origin;
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 CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
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 checkedAt = profile.update ? Date.parse(profile.update.checked_at) : Number.NaN;
1834
- let latest = profile.update?.latest;
1835
- if (!latest || !Number.isFinite(checkedAt) || Date.now() - checkedAt >= CHECK_INTERVAL_MS) {
1836
- try {
1837
- const response = await fetch(LATEST_URL, {
1838
- headers: { accept: "application/json" },
1839
- signal: AbortSignal.timeout(1500)
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
- } : null;
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
- --token mar_\u2026 save an existing API key (headless fallback)
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
- result({
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
- const token = values.token ?? process.env.MARINA_TOKEN;
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 message = `Marina CLI ${update.latest} is available (current ${update.current}) \u2014 ${update.command}`;
2270
- note(isJsonMode() ? message : dim(message));
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marina-cloud/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Command-line client for Marina Cloud",
5
5
  "homepage": "https://github.com/marina-hq/marina#readme",
6
6
  "bugs": {