@marina-cloud/cli 0.0.2 → 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 +15 -6
  2. package/dist/marina.mjs +216 -93
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,30 +2,39 @@
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
8
11
  marina deploy
9
12
  ```
10
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
+
11
20
  `marina setup` opens Clerk in the browser and delivers the resulting API key
12
- directly back to the CLI. For unattended environments, `MARINA_TOKEN` or
13
- `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.
14
23
 
15
24
  The saved credential lives in `~/.marina/profile` with user-only permissions.
16
25
  Use `marina profile` to inspect the active profile and `marina logout` to remove
17
26
  its credential.
18
27
 
19
- Setup automatically installs Marina's deployment skill when Codex or Claude is
20
- detected. Install or refresh it explicitly with:
28
+ Install Marina deployment skills with:
21
29
 
22
30
  ```sh
23
31
  marina skills install
24
32
  marina skills install --agent codex
25
33
  ```
26
34
 
27
- The CLI checks npm at most once per day and prints an advisory update command
28
- 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.
29
38
 
30
39
  ## Agent-friendly output
31
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
  });
@@ -147,9 +284,10 @@ async function startDeploy(zip, name, app) {
147
284
  });
148
285
  return res.deploy;
149
286
  }
150
- async function pollDeploy(id) {
287
+ async function pollDeploy(id, onProgress = () => void 0) {
151
288
  for (; ; ) {
152
289
  const { deploy: deploy2 } = await request(`/v1/deploys/${id}`);
290
+ onProgress(deploy2);
153
291
  if (deploy2.status !== "queued" && deploy2.status !== "building") return deploy2;
154
292
  await new Promise((resolve2) => setTimeout(resolve2, 500));
155
293
  }
@@ -1564,6 +1702,28 @@ function say(line = "") {
1564
1702
  function note(line) {
1565
1703
  console.error(line);
1566
1704
  }
1705
+ function createProgress() {
1706
+ let active = false;
1707
+ let lastPhase = null;
1708
+ let frame = 0;
1709
+ const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1710
+ const interactive = process.stderr.isTTY === true && !json;
1711
+ return {
1712
+ update(phase, line) {
1713
+ if (interactive) {
1714
+ process.stderr.write(`\r\x1B[2K${frames[frame++ % frames.length]} ${line}`);
1715
+ active = true;
1716
+ } else if (phase !== lastPhase) {
1717
+ console.error(json ? JSON.stringify({ type: "progress", phase, message: line }) : line);
1718
+ }
1719
+ lastPhase = phase;
1720
+ },
1721
+ clear() {
1722
+ if (active) process.stderr.write("\r\x1B[2K");
1723
+ active = false;
1724
+ }
1725
+ };
1726
+ }
1567
1727
  function result(payload) {
1568
1728
  if (json) console.log(JSON.stringify({ schema_version: 1, ok: true, ...payload }, null, 2));
1569
1729
  }
@@ -1746,52 +1906,8 @@ function installSkills(agent) {
1746
1906
  return selected.map((target) => install(target, true));
1747
1907
  }
1748
1908
 
1749
- // package.json
1750
- var package_default = {
1751
- name: "@marina-cloud/cli",
1752
- version: "0.0.2",
1753
- description: "Command-line client for Marina Cloud",
1754
- homepage: "https://github.com/marina-hq/marina#readme",
1755
- bugs: {
1756
- url: "https://github.com/marina-hq/marina/issues"
1757
- },
1758
- license: "Apache-2.0",
1759
- repository: {
1760
- type: "git",
1761
- url: "git+https://github.com/marina-hq/marina.git",
1762
- directory: "packages/cli"
1763
- },
1764
- bin: {
1765
- marina: "dist/marina.mjs"
1766
- },
1767
- files: [
1768
- "dist"
1769
- ],
1770
- type: "module",
1771
- publishConfig: {
1772
- access: "public",
1773
- provenance: true
1774
- },
1775
- scripts: {
1776
- 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',
1777
- typecheck: "tsc --noEmit",
1778
- test: "pnpm build && node --experimental-strip-types --test src/*.test.ts",
1779
- prepack: "pnpm build"
1780
- },
1781
- devDependencies: {
1782
- "@types/node": "^26.2.0",
1783
- esbuild: "^0.25.0",
1784
- fflate: "^0.8.2",
1785
- typescript: "^5.9.0"
1786
- },
1787
- engines: {
1788
- node: ">=22"
1789
- }
1790
- };
1791
-
1792
1909
  // src/update.ts
1793
- var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1794
- var LATEST_URL = "https://registry.npmjs.org/@marina-cloud%2Fcli/latest";
1910
+ var NAG_INTERVAL_MS = 24 * 60 * 60 * 1e3;
1795
1911
  function isNewerVersion(candidate, current) {
1796
1912
  const parse = (version) => version.replace(/^v/, "").split("-", 1)[0].split(".").map((part) => Number.parseInt(part, 10));
1797
1913
  const left = parse(candidate);
@@ -1806,40 +1922,28 @@ function isNewerVersion(candidate, current) {
1806
1922
  async function availableUpdate() {
1807
1923
  if (process.env.MARINA_DISABLE_UPDATE_CHECK) return null;
1808
1924
  const current = package_default.version;
1925
+ const latest = latestCliVersion();
1926
+ if (!latest || !isNewerVersion(latest, current)) return null;
1809
1927
  const profile = readProfile();
1810
- const checkedAt = profile.update ? Date.parse(profile.update.checked_at) : Number.NaN;
1811
- let latest = profile.update?.latest;
1812
- if (!latest || !Number.isFinite(checkedAt) || Date.now() - checkedAt >= CHECK_INTERVAL_MS) {
1813
- try {
1814
- const response = await fetch(LATEST_URL, {
1815
- headers: { accept: "application/json" },
1816
- signal: AbortSignal.timeout(1500)
1817
- });
1818
- if (response.ok) {
1819
- const body = await response.json();
1820
- if (body.version) {
1821
- latest = body.version;
1822
- writeProfile({
1823
- ...readProfile(),
1824
- update: { checked_at: (/* @__PURE__ */ new Date()).toISOString(), latest }
1825
- });
1826
- }
1827
- }
1828
- } catch {
1829
- }
1830
- }
1831
- 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 {
1832
1935
  current,
1833
1936
  latest,
1834
1937
  command: "npm install -g @marina-cloud/cli@latest"
1835
- } : null;
1938
+ };
1836
1939
  }
1837
1940
 
1838
1941
  // src/index.ts
1839
1942
  var HELP = `${bold("marina")} \u2014 deploy internal apps
1840
1943
 
1841
1944
  marina setup sign in with Clerk in your browser
1842
- --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
1843
1947
  marina logout remove the saved credential
1844
1948
  marina profile show where this CLI is signed in
1845
1949
  marina skills install install or update the Marina deployment skill
@@ -1858,8 +1962,7 @@ var HELP = `${bold("marina")} \u2014 deploy internal apps
1858
1962
  marina list apps in your workspace
1859
1963
  marina open open this project's app
1860
1964
 
1861
- ${dim("--json machine-readable result on stdout, progress on stderr")}
1862
- ${dim(`API: ${apiUrl()} (override with MARINA_API)`)}`;
1965
+ ${dim("--json machine-readable result on stdout, progress on stderr")}`;
1863
1966
  var ANSI_ESCAPE = new RegExp(`${String.fromCodePoint(27)}\\[[0-9;]*m`, "g");
1864
1967
  var shouldCheckForUpdates = false;
1865
1968
  function targetApp(flag) {
@@ -1872,7 +1975,7 @@ function targetApp(flag) {
1872
1975
  return app;
1873
1976
  }
1874
1977
  var shortDigest = (digest) => digest?.slice(7, 14) ?? "\u2014";
1875
- async function deploy(dirArg, flags) {
1978
+ async function deploy(dirArg, flags, emitResult = true) {
1876
1979
  let dir = null;
1877
1980
  let link = null;
1878
1981
  let target;
@@ -1894,13 +1997,14 @@ async function deploy(dirArg, flags) {
1894
1997
  packed = pack(dir);
1895
1998
  }
1896
1999
  for (const secret of packed.skippedSecrets) say(dim(`skipped ${secret} \u2014 secrets stay local`));
1897
- say(
1898
- `uploading ${bold(name)} ${dim(`(${String(packed.fileCount)} files, ${String(Math.round(packed.totalBytes / 1024))} KB)`)}`
1899
- );
2000
+ const progress = createProgress();
2001
+ const size = `${String(packed.fileCount)} files, ${String(Math.round(packed.totalBytes / 1024))} KB`;
2002
+ progress.update("uploading", `Uploading ${name} (${size})`);
1900
2003
  let started;
1901
2004
  try {
1902
2005
  started = await startDeploy(packed.zip, name, target);
1903
2006
  } catch (error) {
2007
+ progress.clear();
1904
2008
  if (error instanceof ApiError && error.code === "not_found" && link) {
1905
2009
  failure(
1906
2010
  "link_stale",
@@ -1910,7 +2014,18 @@ async function deploy(dirArg, flags) {
1910
2014
  }
1911
2015
  throw error;
1912
2016
  }
1913
- const deployed = await pollDeploy(started.id);
2017
+ const deployedAt = Date.now();
2018
+ let deployed;
2019
+ try {
2020
+ deployed = await pollDeploy(started.id, (current) => {
2021
+ if (current.status !== "queued" && current.status !== "building") return;
2022
+ const elapsed = Math.max(1, Math.round((Date.now() - deployedAt) / 1e3));
2023
+ const message = current.status === "queued" ? `Waiting for a deploy worker (${String(elapsed)}s)` : `Building and verifying (${String(elapsed)}s)`;
2024
+ progress.update(current.status, message);
2025
+ });
2026
+ } finally {
2027
+ progress.clear();
2028
+ }
1914
2029
  if (deployed.status === "refused" && deployed.refusal) {
1915
2030
  say(`${red("refused")} ${deployed.refusal.message}`);
1916
2031
  if (deployed.refusal.action) say(` ${deployed.refusal.action}`);
@@ -1949,7 +2064,7 @@ async function deploy(dirArg, flags) {
1949
2064
  if (deployed.version_url) say(` try this version: ${deployed.version_url}`);
1950
2065
  say(dim(" a publisher can approve it from the Marina inbox"));
1951
2066
  }
1952
- result({
2067
+ const payload = {
1953
2068
  command: "deploy",
1954
2069
  status: "succeeded",
1955
2070
  app: slug,
@@ -1960,7 +2075,9 @@ async function deploy(dirArg, flags) {
1960
2075
  preparation: deployed.preparation,
1961
2076
  live: published,
1962
2077
  demo: dirArg === "demo"
1963
- });
2078
+ };
2079
+ if (emitResult) result(payload);
2080
+ return payload;
1964
2081
  }
1965
2082
  async function status(appFlag) {
1966
2083
  const app = targetApp(appFlag);
@@ -2076,12 +2193,13 @@ async function main() {
2076
2193
  const { positionals, values } = parseArgs({
2077
2194
  allowPositionals: true,
2078
2195
  options: {
2079
- token: { type: "string" },
2080
2196
  name: { type: "string" },
2081
2197
  app: { type: "string" },
2082
2198
  to: { type: "string" },
2083
2199
  plain: { type: "boolean" },
2084
2200
  agent: { type: "string" },
2201
+ skills: { type: "boolean" },
2202
+ "deploy-demo": { type: "boolean" },
2085
2203
  json: { type: "boolean" },
2086
2204
  help: { type: "boolean", short: "h" }
2087
2205
  }
@@ -2093,14 +2211,12 @@ async function main() {
2093
2211
  result({ command: "help", usage: HELP.replaceAll(ANSI_ESCAPE, "") });
2094
2212
  return;
2095
2213
  }
2214
+ await resolveControlPlane();
2096
2215
  shouldCheckForUpdates = true;
2097
2216
  switch (command) {
2098
2217
  case "setup":
2099
2218
  case "login": {
2100
- const token = values.token ?? process.env.MARINA_TOKEN;
2101
- if (token) {
2102
- saveToken(token);
2103
- } else {
2219
+ if (!process.env.MARINA_TOKEN) {
2104
2220
  let lastReportedRemaining = -1;
2105
2221
  const signedIn = await loginWithBrowser(
2106
2222
  (url) => {
@@ -2131,11 +2247,18 @@ async function main() {
2131
2247
  saveToken(signedIn.token);
2132
2248
  }
2133
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;
2134
2255
  result({
2135
2256
  command: "setup",
2136
2257
  signed_in: true,
2137
2258
  api: apiUrl(),
2138
- profile: profilePath()
2259
+ profile: profilePath(),
2260
+ ...installed ? { skills: installed } : {},
2261
+ ...demo ? { deploy: demo } : {}
2139
2262
  });
2140
2263
  return;
2141
2264
  }
@@ -2231,8 +2354,8 @@ async function run() {
2231
2354
  if (!shouldCheckForUpdates) return;
2232
2355
  const update = await availableUpdate();
2233
2356
  if (update) {
2234
- const message = `Marina CLI ${update.latest} is available (current ${update.current}) \u2014 ${update.command}`;
2235
- 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}`);
2236
2359
  }
2237
2360
  }
2238
2361
  run().catch((error) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marina-cloud/cli",
3
- "version": "0.0.2",
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": {