@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 +4 -2
- package/dist/marina.mjs +128 -64
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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
|
-
|
|
1865
|
-
|
|
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
|
|
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
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
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
|
}
|