@anyship/cli 0.2.1 → 0.3.1

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
@@ -21,23 +21,20 @@ never hand-pick files.
21
21
 
22
22
  ### `anyship login`
23
23
 
24
- Run **`anyship login`** with no arguments and it opens your browser to sign in
25
- (OAuth) the same login you use for the anyship connector, no key required. It
26
- stores the resulting token in `~/.anyship/config.json` (mode `0600`), refreshing
27
- it automatically. This is the common path now that most people connect with
28
- browser login.
29
-
30
- Prefer an API key (CI, scripts)? Pass one and it's validated + stored instead:
24
+ An **`anyship_sk_…` API key is the only credential there is no browser/OAuth
25
+ login.** `anyship login` takes your key, validates it against the control plane,
26
+ and stores it in `~/.anyship/config.json` (mode `0600`), so `deploy` and `mcp`
27
+ authenticate automatically afterward. Grab the key from the anyship dashboard.
31
28
 
32
29
  ```bash
33
- anyship login # browser login (OAuth) no key
34
- anyship login --key anyship_sk_… # or an API key: echo $KEY | anyship login --with-token
30
+ anyship login --key anyship_sk_… # validate + store the key
31
+ echo $KEY | anyship login --with-token # or pipe it (CI / scripts no key in shell history)
35
32
  anyship logout # remove stored credentials
36
33
  ```
37
34
 
38
- `deploy` and `mcp` then authenticate automatically. If neither a key nor a stored
39
- token is present, `deploy` triggers the browser login on first use; the `mcp`
40
- server uses only stored auth (run `anyship login` once first).
35
+ If no key is present, `login` errors telling you to supply one it never opens a
36
+ browser. (OAuth exists only for **dashboard** sign-in Google/GitHub never for
37
+ the CLI, MCP, or deploy.)
41
38
 
42
39
  ### `anyship deploy [path]`
43
40
 
@@ -60,7 +57,7 @@ other tool (`list_projects`, `deployment_status`, `add_domain`, `logs`, …) is
60
57
  proxied to the control plane. Add it to Claude Code:
61
58
 
62
59
  ```bash
63
- anyship login
60
+ anyship login --key anyship_sk_…
64
61
  claude mcp add anyship -- npx -y @anyship/cli mcp
65
62
  ```
66
63
 
@@ -69,14 +66,15 @@ files and never handles your API key.
69
66
 
70
67
  ## Configuration
71
68
 
72
- Auth resolves in this order an **API key** if you have one, otherwise **OAuth**:
69
+ The `anyship_sk_…` API key is the only credential. It resolves in this order (first
70
+ hit wins); if none is found, the command fails loudly rather than falling back to
71
+ any other account:
73
72
 
74
73
  1. `--key` flag
75
74
  2. `ANYSHIP_API_KEY` env
76
- 3. a stored API key (`~/.anyship/config.json`)
77
- 4. an `mcp_servers.anyship` entry in `~/.codex/config.toml`
78
- 5. a stored **OAuth token** (`~/.anyship/config.json`, auto-refreshed)
79
- 6. otherwise → browser login (interactive commands only)
75
+ 3. a stored key (`~/.anyship/config.json`, written by `anyship login`)
76
+ 4. the anyship MCP-connector key from an existing client config — `~/.codex/config.toml`
77
+ or `~/.claude.json` (so configuring the connector is enough for the CLI too)
80
78
 
81
- The control-plane URL comes from `--api`, `ANYSHIP_API_URL`, the stored config, or
82
- the default. Requires Node.js 18+.
79
+ There is no OAuth token and no browser-login fallback. The control-plane URL comes
80
+ from `--api`, `ANYSHIP_API_URL`, the stored config, or the default. Requires Node.js 18+.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@anyship/cli",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "anyship CLI — deploy a local project directory, run a local MCP server for AI agents, and log in.",
5
5
  "type": "module",
6
6
  "bin": {
7
- "anyship": "./bin/anyship.mjs"
7
+ "anyship": "bin/anyship.mjs"
8
8
  },
9
9
  "exports": {
10
10
  ".": "./src/index.mjs"
package/src/config.mjs CHANGED
@@ -13,8 +13,8 @@ export function anyshipConfigPath(home = homedir()) {
13
13
  return join(anyshipConfigDir(home), "config.json");
14
14
  }
15
15
 
16
- // Returns the whole stored object (apiKey, apiUrl, oauth, …) so writes preserve
17
- // every field the OAuth token block must survive an apiKey/apiUrl patch.
16
+ // Returns the whole stored object (apiKey, apiUrl, …) so a patch write preserves
17
+ // any fields it doesn't touch.
18
18
  export function readAnyshipConfig(home = homedir()) {
19
19
  try {
20
20
  const parsed = JSON.parse(readFileSync(anyshipConfigPath(home), "utf8"));
@@ -5,7 +5,6 @@ import { homedir } from "node:os";
5
5
  import { basename, join, relative, resolve } from "node:path";
6
6
  import { zipSync } from "fflate";
7
7
  import { readAnyshipConfig } from "./config.mjs";
8
- import { browserLogin, storedAccessToken } from "./oauth.mjs";
9
8
 
10
9
  export const DEFAULT_API = "https://drydock.anyship.workers.dev";
11
10
 
@@ -31,8 +30,8 @@ export function deployUsage(commandName = "anyship deploy") {
31
30
 
32
31
  Options:
33
32
  --project <name> anyship project name
34
- --api <url> control-plane URL (defaults to ~/.codex/config.toml, ANYSHIP_API_URL, or ${DEFAULT_API})
35
- --key <token> anyship API key (defaults to ANYSHIP_API_KEY or ~/.codex/config.toml)
33
+ --api <url> control-plane URL (defaults to your MCP connector config, ANYSHIP_API_URL, or ${DEFAULT_API})
34
+ --key <token> anyship API key (defaults to ANYSHIP_API_KEY, ~/.anyship, or the key in your MCP connector config — ~/.codex/config.toml or ~/.claude.json)
36
35
  --env <json> JSON object of env vars/secrets to merge for the project
37
36
  --dry-run Print the files that would be archived without uploading
38
37
  --json Print dry-run output as JSON
@@ -110,7 +109,49 @@ export function codexAnyshipConfig(home = homedir()) {
110
109
  return { url, token };
111
110
  }
112
111
 
113
- export function apiKey(args, config = codexAnyshipConfig(), stored = readAnyshipConfig()) {
112
+ // Read the anyship connector's key/url from Claude Code's MCP config
113
+ // (~/.claude.json). The connector is stored with the key baked into an
114
+ // `Authorization: Bearer` header, either at the top level or under a per-project
115
+ // `projects[<path>].mcpServers`. Prefer an entry that actually carries a token.
116
+ export function claudeCodeAnyshipConfig(home = homedir()) {
117
+ const configPath = join(home, ".claude.json");
118
+ if (!existsSync(configPath)) return {};
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(readFileSync(configPath, "utf8"));
122
+ } catch {
123
+ return {};
124
+ }
125
+ const fromServers = (servers) => {
126
+ const entry = servers?.anyship;
127
+ if (!entry || typeof entry !== "object") return null;
128
+ const url = typeof entry.url === "string" ? entry.url.replace(/\/mcp\/?$/, "") : undefined;
129
+ const auth = (entry.headers && (entry.headers.Authorization || entry.headers.authorization)) || "";
130
+ const token = /Bearer\s+(anyship_sk_[A-Za-z0-9]+)/.exec(auth)?.[1];
131
+ return url || token ? { url, token } : null;
132
+ };
133
+ const found = [];
134
+ const push = (r) => { if (r) found.push(r); };
135
+ push(fromServers(parsed.mcpServers));
136
+ if (parsed.projects && typeof parsed.projects === "object") {
137
+ for (const proj of Object.values(parsed.projects)) push(fromServers(proj?.mcpServers));
138
+ }
139
+ return found.find((c) => c.token) || found[0] || {};
140
+ }
141
+
142
+ // The anyship connector config from whichever MCP client the user set up —
143
+ // Codex (~/.codex/config.toml) or Claude Code (~/.claude.json). Prefer whichever
144
+ // actually carries a key, so configuring the connector is enough for the CLI too
145
+ // (no separate `anyship login`).
146
+ export function mcpConnectorConfig(home = homedir()) {
147
+ const codex = codexAnyshipConfig(home);
148
+ if (codex.token) return codex;
149
+ const claudeCode = claudeCodeAnyshipConfig(home);
150
+ if (claudeCode.token) return claudeCode;
151
+ return codex.url ? codex : claudeCode;
152
+ }
153
+
154
+ export function apiKey(args, config = mcpConnectorConfig(), stored = readAnyshipConfig()) {
114
155
  return (
115
156
  args.key ||
116
157
  process.env.ANYSHIP_API_KEY ||
@@ -121,7 +162,7 @@ export function apiKey(args, config = codexAnyshipConfig(), stored = readAnyship
121
162
  );
122
163
  }
123
164
 
124
- export function apiUrl(args, config = codexAnyshipConfig(), stored = readAnyshipConfig()) {
165
+ export function apiUrl(args, config = mcpConnectorConfig(), stored = readAnyshipConfig()) {
125
166
  return (
126
167
  args.api ||
127
168
  process.env.ANYSHIP_API_URL ||
@@ -131,23 +172,16 @@ export function apiUrl(args, config = codexAnyshipConfig(), stored = readAnyship
131
172
  ).replace(/\/+$/, "");
132
173
  }
133
174
 
134
- // Bearer for control-plane calls. Precedence: an API key (flag / env / stored /
135
- // codex config) a stored OAuth token (refreshed if expiring) an interactive
136
- // browser login. The no-key OAuth path is the common one now that most users
137
- // connect anyship with browser login. Pass opts.interactive === false to fail
138
- // instead of opening a browser (CI / non-interactive).
175
+ // Bearer for control-plane calls: the anyship_sk_ API key (flag / env / stored /
176
+ // codex config). The API key is the only credential there is no OAuth. Fails
177
+ // loudly when none is set rather than falling back to any other account.
139
178
  export async function resolveBearer(args, io, opts = {}) {
140
179
  const stored = readAnyshipConfig(opts.home);
141
180
  const key = apiKey(args, undefined, stored);
142
181
  if (key) return key;
143
- const api = apiUrl(args, undefined, stored);
144
- const existing = await storedAccessToken(api, { home: opts.home, fetch: opts.fetch });
145
- if (existing) return existing;
146
- if (opts.interactive === false) {
147
- throw new Error("not authenticated — set ANYSHIP_API_KEY or run `anyship login`");
148
- }
149
- const oauth = await browserLogin(api, io, { home: opts.home, fetch: opts.fetch });
150
- return oauth.accessToken;
182
+ throw new Error(
183
+ "not authenticated pass --key anyship_sk_…, set ANYSHIP_API_KEY, or run `anyship login --key anyship_sk_…`. Get your key from the anyship dashboard.",
184
+ );
151
185
  }
152
186
 
153
187
  function pathParts(rel) {
package/src/login.mjs CHANGED
@@ -4,7 +4,6 @@
4
4
  // no --env and no token in shell history.
5
5
  import { createInterface } from "node:readline";
6
6
  import { DEFAULT_API } from "./deploy-directory.mjs";
7
- import { browserLogin } from "./oauth.mjs";
8
7
  import {
9
8
  anyshipConfigPath,
10
9
  clearAnyshipConfig,
@@ -67,8 +66,8 @@ function promptSecret(query, input, output) {
67
66
  });
68
67
  }
69
68
 
70
- // An explicitly-supplied key (flag / env / piped). Returns "" when none — that's
71
- // the signal to fall back to browser-login OAuth.
69
+ // An explicitly-supplied key (flag / env / piped). Returns "" when none — login
70
+ // then errors, since an anyship_sk_ key is the only credential.
72
71
  async function resolveKey(args, deps) {
73
72
  if (args.key) return args.key.trim();
74
73
  if (process.env.ANYSHIP_API_KEY) return process.env.ANYSHIP_API_KEY.trim();
@@ -112,11 +111,11 @@ export async function runLogin(argv, io = defaultIo(), deps = {}) {
112
111
  const api = (args.api || process.env.ANYSHIP_API_URL || readAnyshipConfig(deps.home).apiUrl || DEFAULT_API).replace(/\/+$/, "");
113
112
  const key = await resolveKey(args, deps);
114
113
 
115
- // No key supplied → browser-login OAuth (the common case). A key validate + store.
114
+ // An anyship_sk_ key is the only credential no browser login.
116
115
  if (!key) {
117
- await browserLogin(api, io, deps);
118
- io.stdout(" Signed in to anyship. `anyship deploy` and `anyship mcp` are now authenticated.");
119
- return 0;
116
+ throw new Error(
117
+ "no API key provided. Pass --key anyship_sk_…, set ANYSHIP_API_KEY, or pipe it with --with-token. Get your key from the anyship dashboard.",
118
+ );
120
119
  }
121
120
 
122
121
  if (!/^anyship_sk_/.test(key)) {
@@ -9,16 +9,23 @@
9
9
  // remote control-plane `/mcp`, so this server automatically stays in sync with
10
10
  // whatever tools the platform exposes — only `deploy` is handled locally.
11
11
 
12
- import { deployDirectory, apiKey, apiUrl, codexAnyshipConfig } from "./deploy-directory.mjs";
13
- import { storedAccessToken } from "./oauth.mjs";
12
+ import { deployDirectory, apiKey, apiUrl, mcpConnectorConfig } from "./deploy-directory.mjs";
14
13
  import { createInterface } from "node:readline";
15
14
  import { resolve } from "node:path";
16
15
 
17
16
  const PROTOCOL_VERSION = "2025-06-18";
18
17
  const SERVER_INFO = { name: "anyship", version: "0.1.0" };
19
18
 
19
+ // The AUTH paragraph is load-bearing — keep it this explicit. This server runs
20
+ // from the CLI, which already holds the credential, so an agent should never
21
+ // think about auth; past agents mistook a generic "requires authentication"
22
+ // prompt for an OAuth step and stalled. Shut that down here, permanently.
20
23
  const SERVER_INSTRUCTIONS = [
21
- "anyship deploys a web app to a live URL. To deploy a project on disk, call `deploy` with the project name and the directory path — anyship gathers every file that isn't gitignored (via git), uploads it, detects the stack, and builds. You never select, list, or exclude files, and you never handle an API key: this server is already authenticated.",
24
+ "anyship deploys a web app to a live URL. To deploy a project on disk, call `deploy` with the project name and the directory path — anyship gathers every file that isn't gitignored (via git), uploads it, detects the stack, and builds. You never select, list, or exclude files.",
25
+ "",
26
+ "DEPLOY is deterministic; you are not — run the one call and trust it, and do NOT reason about, verify, or optimize any part of it. This tool does 100% of the work. Do NOT: choose which subdirectory to send, run a build yourself, generate or read config files, hunt for a 'web root'/output dir, dry-run to inspect files, or judge size. Point it at the project's top-level directory and let anyship handle detection, build, and what to serve. If a deploy fails, read its error — don't predict failures beforehand.",
27
+ "",
28
+ "AUTH — you do not deal with this, at all. This server runs from the anyship CLI, which already holds the credential, so it is authenticated for you. Treat auth as an invisible, solved detail: there is NO OAuth and NO sign-in flow, and no token for you to obtain, paste, or ask anyone for (the only credential is an `anyship_sk_` API key the CLI supplies automatically). Never tell the user to \"authorize\" / \"connect\" / \"sign in\", never ask for auth codes, tokens, or callback URLs, and never report auth (or a client's generic \"requires authentication\" prompt) as a reason you can't deploy. Just call the tools. The ONE exception: if a tool actually returns `not authenticated`, the whole fix is one shell command the user runs once — `anyship login --key anyship_sk_…` — then retry.",
22
29
  "",
23
30
  "Point `deploy` at the project's web root — the folder holding index.html / package.json (defaults to the current directory).",
24
31
  "",
@@ -52,23 +59,19 @@ const LOCAL_DEPLOY_TOOL = {
52
59
  // limitation — irrelevant here, where deploy reads the disk directly.
53
60
  const LOCAL_OVERRIDDEN = new Set(["deploy", "deploy_archive", "cli_instructions"]);
54
61
 
55
- // Bearer for the remote proxy: an API key (test-injected, env, stored, or codex
56
- // config) or a stored OAuth token from a prior `anyship login`. This background
57
- // server never opens a browser — the user authenticates once with `anyship login`
58
- // (which does the browser flow); here we only use what's already stored.
62
+ // Bearer for the remote proxy: the anyship_sk_ API key (test-injected, env,
63
+ // ~/.anyship, or the MCP connector config Codex or Claude Code). The API key
64
+ // is the only credential.
59
65
  async function remoteBearer(deps) {
60
- const config = deps.config ?? codexAnyshipConfig();
66
+ const config = deps.config ?? mcpConnectorConfig();
61
67
  const key = deps.apiKey ? deps.apiKey({}, config) : apiKey({}, config);
62
68
  if (key) return key;
63
- const api = deps.apiUrl ? deps.apiUrl({}, config) : apiUrl({}, config);
64
- const oauthTok = await storedAccessToken(api);
65
- if (oauthTok) return oauthTok;
66
- throw new Error("not authenticated — run `anyship login` first");
69
+ throw new Error("not authenticated set ANYSHIP_API_KEY or run `anyship login --key anyship_sk_…`");
67
70
  }
68
71
 
69
72
  // Forward a JSON-RPC call to the remote control-plane /mcp.
70
73
  async function remoteProxy(method, params, deps) {
71
- const config = deps.config ?? codexAnyshipConfig();
74
+ const config = deps.config ?? mcpConnectorConfig();
72
75
  const api = deps.apiUrl ? deps.apiUrl({}, config) : apiUrl({}, config);
73
76
  const token = await remoteBearer(deps);
74
77
  const res = await (deps.fetch ?? fetch)(`${api}/mcp`, {
package/src/oauth.mjs DELETED
@@ -1,164 +0,0 @@
1
- // Browser-login OAuth for the CLI — the same flow an MCP client runs, so a user
2
- // who connected anyship with browser login (no API key) can also use the CLI.
3
- // Loopback Authorization Code + PKCE: discover the auth server, dynamically
4
- // register, open the browser, catch the redirect on 127.0.0.1, exchange the code
5
- // for tokens, and store them in ~/.anyship/config.json. Refreshes on expiry.
6
- import { createServer } from "node:http";
7
- import { createHash, randomBytes } from "node:crypto";
8
- import { spawn } from "node:child_process";
9
- import { readAnyshipConfig, writeAnyshipConfig } from "./config.mjs";
10
-
11
- const b64url = (b) => Buffer.from(b).toString("base64url");
12
-
13
- async function discover(api, fetchImpl) {
14
- const res = await fetchImpl(`${api}/.well-known/oauth-authorization-server`);
15
- if (!res.ok) throw new Error(`OAuth discovery failed at ${api} (HTTP ${res.status})`);
16
- const meta = await res.json();
17
- if (!meta?.authorization_endpoint || !meta?.token_endpoint || !meta?.registration_endpoint) {
18
- throw new Error(`${api} does not advertise the OAuth endpoints needed for login`);
19
- }
20
- return meta;
21
- }
22
-
23
- function openBrowser(url) {
24
- const p = process.platform;
25
- const [cmd, args] =
26
- p === "darwin" ? ["open", [url]] : p === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
27
- try {
28
- spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
29
- } catch {
30
- /* headless / no browser — the URL is printed for manual use */
31
- }
32
- }
33
-
34
- // Loopback server on an ephemeral port; resolves with the OAuth redirect params.
35
- function startCallbackServer() {
36
- return new Promise((resolve) => {
37
- let resolveCode;
38
- const waitForCode = new Promise((r) => (resolveCode = r));
39
- const server = createServer((req, res) => {
40
- const u = new URL(req.url, "http://127.0.0.1");
41
- if (u.pathname !== "/callback") {
42
- res.writeHead(404);
43
- res.end();
44
- return;
45
- }
46
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
47
- res.end(
48
- "<!doctype html><html><head><meta charset='utf-8'></head>" +
49
- "<body style='font:16px/1.5 system-ui;text-align:center;padding:48px'>" +
50
- "<h2>anyship — signed in ✓</h2><p>You can close this tab and return to your terminal.</p>" +
51
- "</body></html>",
52
- );
53
- resolveCode({ code: u.searchParams.get("code"), state: u.searchParams.get("state") });
54
- });
55
- server.listen(0, "127.0.0.1", () => resolve({ server, port: server.address().port, waitForCode }));
56
- });
57
- }
58
-
59
- // Run the full interactive browser login and persist the tokens. Returns the
60
- // stored oauth block.
61
- export async function browserLogin(api, io = defaultIo(), deps = {}) {
62
- const fetchImpl = deps.fetch ?? fetch;
63
- const meta = await discover(api, fetchImpl);
64
-
65
- const { server, port, waitForCode } = await startCallbackServer();
66
- const redirectUri = `http://127.0.0.1:${port}/callback`;
67
- try {
68
- const reg = await fetchImpl(meta.registration_endpoint, {
69
- method: "POST",
70
- headers: { "content-type": "application/json" },
71
- body: JSON.stringify({
72
- client_name: "Anyship CLI",
73
- redirect_uris: [redirectUri],
74
- token_endpoint_auth_method: "none",
75
- grant_types: ["authorization_code", "refresh_token"],
76
- response_types: ["code"],
77
- }),
78
- });
79
- const client = await reg.json().catch(() => null);
80
- if (!client?.client_id) throw new Error("OAuth client registration failed");
81
-
82
- const verifier = b64url(randomBytes(32));
83
- const challenge = b64url(createHash("sha256").update(verifier).digest());
84
- const state = b64url(randomBytes(16));
85
- const authUrl = `${meta.authorization_endpoint}?${new URLSearchParams({
86
- response_type: "code",
87
- client_id: client.client_id,
88
- redirect_uri: redirectUri,
89
- scope: "openid profile email offline_access",
90
- state,
91
- code_challenge: challenge,
92
- code_challenge_method: "S256",
93
- })}`;
94
- io.stderr(`Opening your browser to sign in to anyship…\nIf it doesn't open, visit:\n ${authUrl}`);
95
- openBrowser(authUrl);
96
-
97
- const got = await waitForCode;
98
- if (got.state !== state) throw new Error("OAuth state mismatch — aborting");
99
- if (!got.code) throw new Error("no authorization code returned");
100
-
101
- const tok = await fetchImpl(meta.token_endpoint, {
102
- method: "POST",
103
- headers: { "content-type": "application/x-www-form-urlencoded" },
104
- body: new URLSearchParams({
105
- grant_type: "authorization_code",
106
- code: got.code,
107
- redirect_uri: redirectUri,
108
- client_id: client.client_id,
109
- code_verifier: verifier,
110
- }).toString(),
111
- });
112
- const t = await tok.json().catch(() => null);
113
- if (!t?.access_token) throw new Error("token exchange failed");
114
-
115
- const oauth = {
116
- apiUrl: api,
117
- clientId: client.client_id,
118
- accessToken: t.access_token,
119
- refreshToken: t.refresh_token || null,
120
- expiresAt: t.expires_in ? Date.now() + t.expires_in * 1000 : null,
121
- };
122
- writeAnyshipConfig({ oauth }, deps.home);
123
- io.stderr("Signed in to anyship.");
124
- return oauth;
125
- } finally {
126
- server.close();
127
- }
128
- }
129
-
130
- // Return a valid stored access token (refreshing if near expiry), or null if
131
- // there's no usable OAuth token for this api.
132
- export async function storedAccessToken(api, deps = {}) {
133
- const oauth = readAnyshipConfig(deps.home).oauth;
134
- if (!oauth?.accessToken || oauth.apiUrl !== api) return null;
135
- if (!oauth.expiresAt || oauth.expiresAt - Date.now() > 60_000) return oauth.accessToken;
136
- if (!oauth.refreshToken) return null;
137
-
138
- const fetchImpl = deps.fetch ?? fetch;
139
- const meta = await discover(api, fetchImpl).catch(() => null);
140
- if (!meta?.token_endpoint) return null;
141
- const res = await fetchImpl(meta.token_endpoint, {
142
- method: "POST",
143
- headers: { "content-type": "application/x-www-form-urlencoded" },
144
- body: new URLSearchParams({
145
- grant_type: "refresh_token",
146
- refresh_token: oauth.refreshToken,
147
- client_id: oauth.clientId,
148
- }).toString(),
149
- }).catch(() => null);
150
- const t = res && (await res.json().catch(() => null));
151
- if (!t?.access_token) return null;
152
- const updated = {
153
- ...oauth,
154
- accessToken: t.access_token,
155
- refreshToken: t.refresh_token || oauth.refreshToken,
156
- expiresAt: t.expires_in ? Date.now() + t.expires_in * 1000 : null,
157
- };
158
- writeAnyshipConfig({ oauth: updated }, deps.home);
159
- return updated.accessToken;
160
- }
161
-
162
- function defaultIo() {
163
- return { stdout: (t) => console.log(t), stderr: (t) => console.error(t) };
164
- }