@marina-cloud/cli 0.0.1 → 0.0.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 CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  Deploy small internal apps to Marina Cloud.
4
4
 
5
+ Source code and Marina's deployment skill live in the
6
+ [marina-hq/marina](https://github.com/marina-hq/marina) repository.
7
+
5
8
  ```sh
6
9
  npm install -g @marina-cloud/cli
7
10
  marina setup
@@ -16,8 +19,7 @@ The saved credential lives in `~/.marina/profile` with user-only permissions.
16
19
  Use `marina profile` to inspect the active profile and `marina logout` to remove
17
20
  its credential.
18
21
 
19
- Setup automatically installs Marina's deployment skill when Codex or Claude is
20
- detected. Install or refresh it explicitly with:
22
+ Install Marina deployment skills with:
21
23
 
22
24
  ```sh
23
25
  marina skills install
package/dist/marina.mjs CHANGED
@@ -75,6 +75,7 @@ function writeLink(dir, link) {
75
75
  }
76
76
 
77
77
  // src/api.ts
78
+ var LOGIN_EXCHANGE_TIMEOUT_MS = 15e3;
78
79
  var ApiError = class extends Error {
79
80
  code;
80
81
  status;
@@ -102,11 +103,24 @@ async function request(path, init) {
102
103
  return body;
103
104
  }
104
105
  async function exchangeCliLogin(code, codeVerifier) {
105
- const res = await fetch(`${apiUrl()}/cli/auth/exchange`, {
106
- method: "POST",
107
- headers: { "content-type": "application/json" },
108
- body: JSON.stringify({ code, code_verifier: codeVerifier })
109
- });
106
+ let res;
107
+ try {
108
+ res = await fetch(`${apiUrl()}/cli/auth/exchange`, {
109
+ method: "POST",
110
+ headers: { "content-type": "application/json" },
111
+ body: JSON.stringify({ code, code_verifier: codeVerifier }),
112
+ signal: AbortSignal.timeout(LOGIN_EXCHANGE_TIMEOUT_MS)
113
+ });
114
+ } catch (error) {
115
+ if (error.name === "TimeoutError") {
116
+ throw new ApiError(
117
+ "login_timeout",
118
+ "the login confirmation request timed out after 15 seconds \u2014 run `marina setup` to try again",
119
+ 408
120
+ );
121
+ }
122
+ throw error;
123
+ }
110
124
  const body = await res.json().catch(() => ({}));
111
125
  if (!res.ok || !body.token) {
112
126
  throw new ApiError(
@@ -133,9 +147,10 @@ async function startDeploy(zip, name, app) {
133
147
  });
134
148
  return res.deploy;
135
149
  }
136
- async function pollDeploy(id) {
150
+ async function pollDeploy(id, onProgress = () => void 0) {
137
151
  for (; ; ) {
138
152
  const { deploy: deploy2 } = await request(`/v1/deploys/${id}`);
153
+ onProgress(deploy2);
139
154
  if (deploy2.status !== "queued" && deploy2.status !== "building") return deploy2;
140
155
  await new Promise((resolve2) => setTimeout(resolve2, 500));
141
156
  }
@@ -1414,22 +1429,53 @@ import { spawn } from "node:child_process";
1414
1429
  import { createServer } from "node:http";
1415
1430
  import { hostname } from "node:os";
1416
1431
  var LOGIN_TIMEOUT_MS = 5 * 6e4;
1432
+ var PROGRESS_INTERVAL_MS = 1e3;
1417
1433
  var page = (title, message) => `<!doctype html>
1418
1434
  <html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
1419
1435
  <title>${title}</title><style>
1420
1436
  body{margin:0;display:grid;place-items:center;min-height:100vh;font:15px/1.5 system-ui,sans-serif;color:#142238;background:#faf9f7}
1421
1437
  main{text-align:center;padding:32px}h1{font-size:22px;margin:0 0 6px}p{margin:0;color:#667085}
1422
1438
  </style></head><body><main><h1>${title}</h1><p>${message}</p></main></body></html>`;
1423
- var closeServer = (server) => new Promise((resolve2) => server.close(() => resolve2()));
1439
+ var closeLoginServer = (server) => new Promise((resolve2) => {
1440
+ server.close(() => resolve2());
1441
+ server.closeAllConnections();
1442
+ });
1424
1443
  function openBrowser(url) {
1425
1444
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer.exe" : "xdg-open";
1426
1445
  const child = spawn(command, [url], { detached: true, stdio: "ignore" });
1427
1446
  child.on("error", () => void 0);
1428
1447
  child.unref();
1429
1448
  }
1430
- function callbackCode(server, expectedState) {
1449
+ function timeoutLabel(timeoutMs) {
1450
+ if (timeoutMs % 6e4 === 0) {
1451
+ const minutes = timeoutMs / 6e4;
1452
+ return `${String(minutes)} minute${minutes === 1 ? "" : "s"}`;
1453
+ }
1454
+ const seconds = Math.ceil(timeoutMs / 1e3);
1455
+ return `${String(seconds)} second${seconds === 1 ? "" : "s"}`;
1456
+ }
1457
+ function waitForLoginCallback(server, expectedState, onProgress, timeoutMs = LOGIN_TIMEOUT_MS) {
1431
1458
  return new Promise((resolve2, reject) => {
1432
- const timer = setTimeout(() => reject(new Error("browser login timed out")), LOGIN_TIMEOUT_MS);
1459
+ const startedAt = Date.now();
1460
+ const waiting = () => {
1461
+ const elapsedMs = Date.now() - startedAt;
1462
+ onProgress({
1463
+ phase: "waiting",
1464
+ elapsedMs,
1465
+ remainingMs: Math.max(0, timeoutMs - elapsedMs)
1466
+ });
1467
+ };
1468
+ waiting();
1469
+ const progress = setInterval(waiting, PROGRESS_INTERVAL_MS);
1470
+ progress.unref();
1471
+ const timer = setTimeout(() => {
1472
+ clearInterval(progress);
1473
+ reject(
1474
+ new Error(
1475
+ `browser login timed out after ${timeoutLabel(timeoutMs)} \u2014 run \`marina setup\` to try again`
1476
+ )
1477
+ );
1478
+ }, timeoutMs);
1433
1479
  timer.unref();
1434
1480
  server.on("request", (request2, response) => {
1435
1481
  const url = new URL(request2.url ?? "/", "http://127.0.0.1");
@@ -1439,6 +1485,7 @@ function callbackCode(server, expectedState) {
1439
1485
  }
1440
1486
  response.setHeader("cache-control", "no-store");
1441
1487
  response.setHeader("content-type", "text/html; charset=utf-8");
1488
+ response.setHeader("connection", "close");
1442
1489
  const state = url.searchParams.get("state");
1443
1490
  const code = url.searchParams.get("code");
1444
1491
  const error = url.searchParams.get("error");
@@ -1448,6 +1495,7 @@ function callbackCode(server, expectedState) {
1448
1495
  return;
1449
1496
  }
1450
1497
  clearTimeout(timer);
1498
+ clearInterval(progress);
1451
1499
  if (error || !code) {
1452
1500
  response.writeHead(400);
1453
1501
  response.end(
@@ -1460,6 +1508,7 @@ function callbackCode(server, expectedState) {
1460
1508
  );
1461
1509
  return;
1462
1510
  }
1511
+ onProgress({ phase: "received" });
1463
1512
  response.writeHead(200);
1464
1513
  response.end(
1465
1514
  page("Marina CLI is signed in", "You can close this window and return to the terminal."),
@@ -1468,7 +1517,7 @@ function callbackCode(server, expectedState) {
1468
1517
  });
1469
1518
  });
1470
1519
  }
1471
- async function loginWithBrowser(onOpen) {
1520
+ async function loginWithBrowser(onOpen, onProgress = () => void 0) {
1472
1521
  const verifier = randomBytes(32).toString("base64url");
1473
1522
  const challenge = createHash("sha256").update(verifier).digest("base64url");
1474
1523
  const state = randomBytes(32).toString("base64url");
@@ -1489,13 +1538,13 @@ async function loginWithBrowser(onOpen) {
1489
1538
  "device_name",
1490
1539
  `Marina CLI on ${hostname().split(".")[0]}`.slice(0, 80)
1491
1540
  );
1492
- const codePromise = callbackCode(server, state);
1541
+ const codePromise = waitForLoginCallback(server, state, onProgress);
1493
1542
  onOpen(authorize.toString());
1494
1543
  openBrowser(authorize.toString());
1495
1544
  const code = await codePromise;
1496
1545
  return await exchangeCliLogin(code, verifier);
1497
1546
  } finally {
1498
- await closeServer(server);
1547
+ await closeLoginServer(server);
1499
1548
  }
1500
1549
  }
1501
1550
 
@@ -1516,6 +1565,28 @@ function say(line = "") {
1516
1565
  function note(line) {
1517
1566
  console.error(line);
1518
1567
  }
1568
+ function createProgress() {
1569
+ let active = false;
1570
+ let lastPhase = null;
1571
+ let frame = 0;
1572
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1573
+ const interactive = process.stderr.isTTY === true && !json;
1574
+ return {
1575
+ update(phase, line) {
1576
+ if (interactive) {
1577
+ process.stderr.write(`\r\x1B[2K${frames[frame++ % frames.length]} ${line}`);
1578
+ active = true;
1579
+ } else if (phase !== lastPhase) {
1580
+ console.error(json ? JSON.stringify({ type: "progress", phase, message: line }) : line);
1581
+ }
1582
+ lastPhase = phase;
1583
+ },
1584
+ clear() {
1585
+ if (active) process.stderr.write("\r\x1B[2K");
1586
+ active = false;
1587
+ }
1588
+ };
1589
+ }
1519
1590
  function result(payload) {
1520
1591
  if (json) console.log(JSON.stringify({ schema_version: 1, ok: true, ...payload }, null, 2));
1521
1592
  }
@@ -1690,21 +1761,6 @@ function install(target, update) {
1690
1761
  status: existing === null ? "installed" : "updated"
1691
1762
  };
1692
1763
  }
1693
- function autoInstallSkills() {
1694
- const detectedTargets = targets();
1695
- const installed = [];
1696
- const updatesAvailable = [];
1697
- for (const target of detectedTargets) {
1698
- const result2 = install(target, false);
1699
- if (result2) installed.push(result2);
1700
- else updatesAvailable.push(target.agent);
1701
- }
1702
- return {
1703
- detected: detectedTargets.map((target) => target.agent),
1704
- installed,
1705
- updatesAvailable
1706
- };
1707
- }
1708
1764
  function installSkills(agent) {
1709
1765
  const selected = targets(agent);
1710
1766
  if (selected.length === 0) {
@@ -1716,7 +1772,7 @@ function installSkills(agent) {
1716
1772
  // package.json
1717
1773
  var package_default = {
1718
1774
  name: "@marina-cloud/cli",
1719
- version: "0.0.1",
1775
+ version: "0.0.3",
1720
1776
  description: "Command-line client for Marina Cloud",
1721
1777
  homepage: "https://github.com/marina-hq/marina#readme",
1722
1778
  bugs: {
@@ -1861,13 +1917,14 @@ async function deploy(dirArg, flags) {
1861
1917
  packed = pack(dir);
1862
1918
  }
1863
1919
  for (const secret of packed.skippedSecrets) say(dim(`skipped ${secret} \u2014 secrets stay local`));
1864
- say(
1865
- `uploading ${bold(name)} ${dim(`(${String(packed.fileCount)} files, ${String(Math.round(packed.totalBytes / 1024))} KB)`)}`
1866
- );
1920
+ const progress = createProgress();
1921
+ const size = `${String(packed.fileCount)} files, ${String(Math.round(packed.totalBytes / 1024))} KB`;
1922
+ progress.update("uploading", `Uploading ${name} (${size})`);
1867
1923
  let started;
1868
1924
  try {
1869
1925
  started = await startDeploy(packed.zip, name, target);
1870
1926
  } catch (error) {
1927
+ progress.clear();
1871
1928
  if (error instanceof ApiError && error.code === "not_found" && link) {
1872
1929
  failure(
1873
1930
  "link_stale",
@@ -1877,7 +1934,18 @@ async function deploy(dirArg, flags) {
1877
1934
  }
1878
1935
  throw error;
1879
1936
  }
1880
- const deployed = await pollDeploy(started.id);
1937
+ const deployedAt = Date.now();
1938
+ let deployed;
1939
+ try {
1940
+ deployed = await pollDeploy(started.id, (current) => {
1941
+ if (current.status !== "queued" && current.status !== "building") return;
1942
+ const elapsed = Math.max(1, Math.round((Date.now() - deployedAt) / 1e3));
1943
+ const message = current.status === "queued" ? `Waiting for a deploy worker (${String(elapsed)}s)` : `Building and verifying (${String(elapsed)}s)`;
1944
+ progress.update(current.status, message);
1945
+ });
1946
+ } finally {
1947
+ progress.clear();
1948
+ }
1881
1949
  if (deployed.status === "refused" && deployed.refusal) {
1882
1950
  say(`${red("refused")} ${deployed.refusal.message}`);
1883
1951
  if (deployed.refusal.action) say(` ${deployed.refusal.action}`);
@@ -2061,27 +2129,6 @@ async function main() {
2061
2129
  return;
2062
2130
  }
2063
2131
  shouldCheckForUpdates = true;
2064
- let skillBootstrap = null;
2065
- if (command !== "skills") {
2066
- try {
2067
- skillBootstrap = autoInstallSkills();
2068
- for (const installed of skillBootstrap.installed) {
2069
- if (installed.status === "installed") {
2070
- say(dim(`installed Marina skill for ${installed.agent}`));
2071
- }
2072
- }
2073
- if (skillBootstrap.updatesAvailable.length > 0) {
2074
- const agent = skillBootstrap.updatesAvailable.length > 1 ? "all" : skillBootstrap.updatesAvailable[0];
2075
- say(
2076
- dim(
2077
- `Marina skill update available \u2014 run marina skills install --agent ${agent ?? "all"}`
2078
- )
2079
- );
2080
- }
2081
- } catch {
2082
- say(dim("could not install the Marina agent skill; run marina skills install later"));
2083
- }
2084
- }
2085
2132
  switch (command) {
2086
2133
  case "setup":
2087
2134
  case "login": {
@@ -2089,24 +2136,41 @@ async function main() {
2089
2136
  if (token) {
2090
2137
  saveToken(token);
2091
2138
  } else {
2092
- const signedIn = await loginWithBrowser((url) => {
2093
- say("Opening your browser to sign in\u2026");
2094
- say(dim(url));
2095
- });
2139
+ let lastReportedRemaining = -1;
2140
+ const signedIn = await loginWithBrowser(
2141
+ (url) => {
2142
+ say("Opening your browser to sign in\u2026");
2143
+ say(dim(url));
2144
+ },
2145
+ (progress) => {
2146
+ if (progress.phase === "waiting") {
2147
+ const remainingSeconds = Math.ceil(progress.remainingMs / 1e3);
2148
+ if (lastReportedRemaining < 0) {
2149
+ say(
2150
+ dim(
2151
+ `Waiting for browser authorization (${String(Math.ceil(remainingSeconds / 60))} minute timeout)\u2026`
2152
+ )
2153
+ );
2154
+ lastReportedRemaining = remainingSeconds;
2155
+ } else if (remainingSeconds <= lastReportedRemaining - 15) {
2156
+ const minutes = Math.floor(remainingSeconds / 60);
2157
+ const seconds = String(remainingSeconds % 60).padStart(2, "0");
2158
+ say(dim(`Still waiting\u2026 ${String(minutes)}:${seconds} remaining`));
2159
+ lastReportedRemaining = remainingSeconds;
2160
+ }
2161
+ } else if (progress.phase === "received") {
2162
+ say(dim("Authorization received. Completing sign-in\u2026"));
2163
+ }
2164
+ }
2165
+ );
2096
2166
  saveToken(signedIn.token);
2097
2167
  }
2098
2168
  say(`${green("ok")} signed in ${dim(`(${apiUrl()})`)}`);
2099
- if (skillBootstrap?.detected.length === 0) {
2100
- say(
2101
- dim("no Codex or Claude installation detected \u2014 use marina skills install --agent later")
2102
- );
2103
- }
2104
2169
  result({
2105
2170
  command: "setup",
2106
2171
  signed_in: true,
2107
2172
  api: apiUrl(),
2108
- profile: profilePath(),
2109
- skills: skillBootstrap?.installed ?? []
2173
+ profile: profilePath()
2110
2174
  });
2111
2175
  return;
2112
2176
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marina-cloud/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Command-line client for Marina Cloud",
5
5
  "homepage": "https://github.com/marina-hq/marina#readme",
6
6
  "bugs": {