@usesocial/cli 0.1.3 → 0.2.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/CHANGELOG.md +42 -0
- package/README.md +4 -3
- package/bin/social.mjs +0 -7
- package/dist/index.mjs +192 -158
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# @usesocial/cli
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Improve account connection progress output and hosted auth resilience.
|
|
8
|
+
|
|
9
|
+
## 0.2.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`26ca563`](https://github.com/usesocial/monorepo/commit/26ca5635ef5947a08f79d1ffc71fa926571ab0e0) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Add hidden `social <x|linkedin> sync <collection>` and `social <x|linkedin> sql` commands for a per-account local SQLite mirror. `sync` page-walks one collection (cost-estimated, gated by `--billing-limit`, default $10; `--since` windows incremental collections), `sql` runs read-only SQL or prints the schema. The commands remain invokable directly, but stay out of provider help while the cache surface settles. Live reads are unchanged.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [`83650e6`](https://github.com/usesocial/monorepo/commit/83650e6f77b94d5cd4a3a5ec673164ff103d6573) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Price every proxy call from the scraped X price list (mirrored for LinkedIn) against a single `usage_credits` balance, fixing a 10–40× write overcharge that mapped non-DM writes to the most expensive category. `account usage` and `account logs` now report cost in credits, and repeated X reads served from cache no longer recharge.
|
|
18
|
+
|
|
19
|
+
- [`df52bf7`](https://github.com/usesocial/monorepo/commit/df52bf795e39c333d25b0618cedc047ad6f14b24) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Limit `--no-cache` to generated commands backed by cacheable proxy reads so non-cacheable DM, message, timeline, bookmark, and write commands no longer send misleading cache-control headers.
|
|
20
|
+
|
|
21
|
+
- [`83650e6`](https://github.com/usesocial/monorepo/commit/83650e6f77b94d5cd4a3a5ec673164ff103d6573) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Reshape the root command surface to `account · schema · x · linkedin` and fold the old `auth` and `accounts` groups into one `account` command: bare `social account` prints the auth state plus connected accounts, with `login`, `logout`, `connect`, `reconnect`, `disconnect`, `usage`, `logs`, and `config` (including `config cache mode|ttl`) as subcommands. `account usage` shows the billing overview and `account logs` lists recent proxy calls. Own-account commands now infer the X account ID instead of requiring it, and account selectors carry reconnect hints.
|
|
22
|
+
|
|
23
|
+
- [`1a6461e`](https://github.com/usesocial/monorepo/commit/1a6461e39dc02640948de49b26b13e3ba3c1bc4b) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Rewrite generated provider help and schema descriptions to show summary, capability, and runnable usage instead of internal HTTP routes.
|
|
24
|
+
|
|
25
|
+
- [`83650e6`](https://github.com/usesocial/monorepo/commit/83650e6f77b94d5cd4a3a5ec673164ff103d6573) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Unify identifier positionals across X and LinkedIn into one explicit `target` grammar: every command accepts an `@handle`, a profile/post/status URL, a typed `kind:id` reference (e.g. `chat_id:…`, `request_id:…`), or `me`. Pure-parse targets resolve for free; `@handle` lookups are cached and reported under `meta.resolved` with a `source`. Writes return openable URLs and audit-friendly IDs, and read envelopes carry richer default payloads.
|
|
26
|
+
|
|
27
|
+
- [`a93af9c`](https://github.com/usesocial/monorepo/commit/a93af9cdbde74f6552cba9e465425526d4f5ddcc) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Limit account connect and reconnect JSON output to the connected account details.
|
|
28
|
+
|
|
29
|
+
- [`26ca563`](https://github.com/usesocial/monorepo/commit/26ca5635ef5947a08f79d1ffc71fa926571ab0e0) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Hide the LinkedIn `search` group from `social linkedin --help` (still invokable).
|
|
30
|
+
|
|
31
|
+
- [`1893cc5`](https://github.com/usesocial/monorepo/commit/1893cc5d715ffde41b83f7f9822f7b3aca0a305a) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Send LinkedIn inbox mark-read and mark-unread writes with Unipile's current `read_status` body, and keep legacy installed CLIs working through a proxy-side body transform.
|
|
32
|
+
|
|
33
|
+
- [`83650e6`](https://github.com/usesocial/monorepo/commit/83650e6f77b94d5cd4a3a5ec673164ff103d6573) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Add edit and delete write verbs to the LinkedIn inbox — `linkedin message <chat> edit message_id:<id> <text>` and `linkedin message <chat> delete message_id:<id>` — alongside `linkedin messages <chat> mark <status>` for read-status. Delete requires confirmation and now correctly meters its `204 No Content` response as one billed write instead of advertising a cost it never charged.
|
|
34
|
+
|
|
35
|
+
- [`5278a68`](https://github.com/usesocial/monorepo/commit/5278a68e436523d2f910b1e9e065efc3f20fbfc3) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Cap `linkedin messages --limit` at 20 for inbox conversation lists so the CLI fails locally instead of sending provider-rejected LinkedIn inbox requests.
|
|
36
|
+
|
|
37
|
+
- [`83650e6`](https://github.com/usesocial/monorepo/commit/83650e6f77b94d5cd4a3a5ec673164ff103d6573) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Add the full LinkedIn relation-request lifecycle under a `requests` group: `requests send <target> [message]` (renamed from the old root `connect`), `requests sent`, `requests received`, `requests accept request_id:<id>`, and `requests cancel request_id:<id>`.
|
|
38
|
+
|
|
39
|
+
- [`83650e6`](https://github.com/usesocial/monorepo/commit/83650e6f77b94d5cd4a3a5ec673164ff103d6573) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Refresh the LinkedIn surface with richer profiles: `linkedin profile` defaults to `about,experience,education`, reads embed inline author cards, `linkedin connections` routes through the supported connections endpoint, and cache-control flags are honored on cacheable reads.
|
|
40
|
+
|
|
41
|
+
- [`2483598`](https://github.com/usesocial/monorepo/commit/2483598ba0e471a1a0bb7c8819dae158005d0b28) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Replace cacheable-read `--no-cache` with repeatable `--header`/`-H` request headers, including `Cache-Control` overrides for proxy cache freshness.
|
|
42
|
+
|
|
43
|
+
- [`83650e6`](https://github.com/usesocial/monorepo/commit/83650e6f77b94d5cd4a3a5ec673164ff103d6573) Thanks [@CyrusNuevoDia](https://github.com/CyrusNuevoDia)! - Expand the X read surface with six new commands — `x likers`, `x quotes`, `x reposters`, `x liked`, `x mentions`, and `x profile` (fetch any profile by target, not just your own) — and unify direct messages into `x messages` (list or read a thread) plus a single `x message <target> <text>` send verb. Read payloads now inline data already paid for, including DM sender identity, via default field presets.
|
|
44
|
+
|
|
3
45
|
## 0.1.2
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -188,9 +188,10 @@ jq -r '.items[].id' /tmp/hiring-posts.json \
|
|
|
188
188
|
1. Asks what access to grant the agent. Read and Write are both selected by
|
|
189
189
|
default; clear Write in the prompt to grant read-only access.
|
|
190
190
|
2. Asks for your email address.
|
|
191
|
-
3.
|
|
192
|
-
|
|
193
|
-
4. Waits until you approve the session in the
|
|
191
|
+
3. Sends a magic link to that email with the device approval screen already
|
|
192
|
+
attached.
|
|
193
|
+
4. Waits until you click the magic link and approve the CLI session in the
|
|
194
|
+
browser.
|
|
194
195
|
5. Confirms billing checkout in the terminal when a seat is needed.
|
|
195
196
|
6. Stores the returned token in your OS keyring, falling back to
|
|
196
197
|
`~/.social/credentials.json` (mode `0600`) when no keyring is available.
|
package/bin/social.mjs
CHANGED
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
2
|
import { dirname, join } from "node:path";
|
|
4
|
-
import { loadEnvFile } from "node:process";
|
|
5
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
4
|
|
|
7
5
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
|
-
const localEnvPath = join(packageRoot, "..", "..", ".env.staging");
|
|
9
6
|
const entryURL = pathToFileURL(join(packageRoot, "dist", "index.mjs")).href;
|
|
10
7
|
|
|
11
|
-
if (existsSync(localEnvPath)) {
|
|
12
|
-
loadEnvFile(localEnvPath);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
8
|
import(entryURL).catch((error) => {
|
|
16
9
|
console.error(error);
|
|
17
10
|
process.exit(1);
|
package/dist/index.mjs
CHANGED
|
@@ -17920,17 +17920,12 @@ const LEADING_AT_PATTERN$1 = /^@+/;
|
|
|
17920
17920
|
const LINKEDIN_PROFILE_HOST_PATTERN = /(^|\.)linkedin\.com$/i;
|
|
17921
17921
|
const isInteractiveTerminal$1 = (deps) => deps.isInteractiveTerminal?.() ?? process.stdout.isTTY === true;
|
|
17922
17922
|
const openURL = async (deps, url) => {
|
|
17923
|
-
|
|
17924
|
-
if (!(isInteractiveTerminal$1(deps) && deps.openURL)) {
|
|
17925
|
-
|
|
17926
|
-
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
};
|
|
17930
|
-
}
|
|
17931
|
-
const handoff = await Promise.resolve(deps.openURL(url));
|
|
17932
|
-
if (!handoff.opened) log(url);
|
|
17933
|
-
return handoff;
|
|
17923
|
+
(deps.log ?? console.error)(`Opening ${url}`);
|
|
17924
|
+
if (!(isInteractiveTerminal$1(deps) && deps.openURL)) return {
|
|
17925
|
+
opened: false,
|
|
17926
|
+
url
|
|
17927
|
+
};
|
|
17928
|
+
return await Promise.resolve(deps.openURL(url));
|
|
17934
17929
|
};
|
|
17935
17930
|
const pollForSeat$2 = async (deps) => {
|
|
17936
17931
|
const intervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
@@ -18084,12 +18079,9 @@ const connectURLFor = async (deps, reconnectProfileId) => {
|
|
|
18084
18079
|
};
|
|
18085
18080
|
const isInteractiveTerminal = (deps) => deps.isInteractiveTerminal?.() ?? process.stdout.isTTY === true;
|
|
18086
18081
|
const openOrPrint = async (deps, url) => {
|
|
18087
|
-
|
|
18088
|
-
if (!(isInteractiveTerminal(deps) && deps.openBrowser))
|
|
18089
|
-
|
|
18090
|
-
return;
|
|
18091
|
-
}
|
|
18092
|
-
if (await deps.openBrowser(url) === false) log(url);
|
|
18082
|
+
(deps.log ?? console.error)(`Opening ${url}`);
|
|
18083
|
+
if (!(isInteractiveTerminal(deps) && deps.openBrowser)) return;
|
|
18084
|
+
await deps.openBrowser(url);
|
|
18093
18085
|
};
|
|
18094
18086
|
const pollForAccount = async (deps, matches) => {
|
|
18095
18087
|
const sleep = deps.sleep ?? defaultSleep;
|
|
@@ -20756,8 +20748,8 @@ const env = createEnv({
|
|
|
20756
20748
|
SOCIAL_WEB_URL: process.env.SOCIAL_WEB_URL,
|
|
20757
20749
|
WSL_DISTRO_NAME: process.env.WSL_DISTRO_NAME,
|
|
20758
20750
|
WSL_INTEROP: process.env.WSL_INTEROP,
|
|
20759
|
-
NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN:
|
|
20760
|
-
NEXT_PUBLIC_POSTHOG_HOST:
|
|
20751
|
+
NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN: process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN,
|
|
20752
|
+
NEXT_PUBLIC_POSTHOG_HOST: process.env.NEXT_PUBLIC_POSTHOG_HOST,
|
|
20761
20753
|
CI: process.env.CI,
|
|
20762
20754
|
DO_NOT_TRACK: process.env.DO_NOT_TRACK,
|
|
20763
20755
|
SOCIAL_DO_NOT_TRACK: process.env.SOCIAL_DO_NOT_TRACK
|
|
@@ -20775,7 +20767,7 @@ const env = createEnv({
|
|
|
20775
20767
|
}
|
|
20776
20768
|
});
|
|
20777
20769
|
const SERVICE_NAME = "social-cli";
|
|
20778
|
-
const VERSION = "0.1
|
|
20770
|
+
const VERSION = "0.2.1";
|
|
20779
20771
|
const apiURL = (path) => createAbsoluteURL(env.SOCIAL_API_URL, path);
|
|
20780
20772
|
//#endregion
|
|
20781
20773
|
//#region src/lib/bearer.ts
|
|
@@ -21131,6 +21123,9 @@ const writeStdoutBuffer = (buffer) => {
|
|
|
21131
21123
|
const writeStdout = (value) => {
|
|
21132
21124
|
writeStdoutBuffer(Buffer.from(`${value}\n`));
|
|
21133
21125
|
};
|
|
21126
|
+
const printLine = (value) => {
|
|
21127
|
+
writeStdout(value);
|
|
21128
|
+
};
|
|
21134
21129
|
const printData = (value) => {
|
|
21135
21130
|
writeStdout(JSON.stringify(value));
|
|
21136
21131
|
};
|
|
@@ -27067,96 +27062,21 @@ const accessPhase = async (ctx) => {
|
|
|
27067
27062
|
};
|
|
27068
27063
|
};
|
|
27069
27064
|
//#endregion
|
|
27070
|
-
//#region src/lib/browser.ts
|
|
27071
|
-
const WSL_VERSION = /microsoft|wsl/i;
|
|
27072
|
-
const BROWSER_TARGET_TIMEOUT_MS = 5e3;
|
|
27073
|
-
const LOCAL_HOSTS = new Set([
|
|
27074
|
-
"localhost",
|
|
27075
|
-
"127.0.0.1",
|
|
27076
|
-
"::1"
|
|
27077
|
-
]);
|
|
27078
|
-
const displayURL = (url) => {
|
|
27079
|
-
try {
|
|
27080
|
-
const parsed = new URL(url);
|
|
27081
|
-
parsed.searchParams.delete("token");
|
|
27082
|
-
parsed.hash = "";
|
|
27083
|
-
return parsed.toString();
|
|
27084
|
-
} catch {
|
|
27085
|
-
return url;
|
|
27086
|
-
}
|
|
27087
|
-
};
|
|
27088
|
-
const hintFor = (url) => LOCAL_HOSTS.has(url.hostname) ? "Start the web app or local worker, or set SOCIAL_API_URL to your deployed social API." : "Check SOCIAL_API_URL and your connection, then try again.";
|
|
27089
|
-
const assertBrowserTargetReachable = async (url) => {
|
|
27090
|
-
let parsed;
|
|
27091
|
-
try {
|
|
27092
|
-
parsed = new URL(url);
|
|
27093
|
-
} catch {
|
|
27094
|
-
return;
|
|
27095
|
-
}
|
|
27096
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
|
|
27097
|
-
if (!LOCAL_HOSTS.has(parsed.hostname)) return;
|
|
27098
|
-
const controller = new AbortController();
|
|
27099
|
-
const timeout = setTimeout(() => controller.abort(), BROWSER_TARGET_TIMEOUT_MS);
|
|
27100
|
-
const target = parsed.origin;
|
|
27101
|
-
try {
|
|
27102
|
-
const response = await fetch(target, {
|
|
27103
|
-
method: "GET",
|
|
27104
|
-
redirect: "manual",
|
|
27105
|
-
signal: controller.signal
|
|
27106
|
-
});
|
|
27107
|
-
if (response.status >= 400) throw new Error(`social is reachable at ${displayURL(target)}, but it returned ${response.status}. Try again in a moment.`);
|
|
27108
|
-
} catch (error) {
|
|
27109
|
-
if (error instanceof Error && error.message.startsWith("social is reachable")) throw error;
|
|
27110
|
-
throw new Error(`Can't reach ${displayURL(target)}. ${hintFor(parsed)}`);
|
|
27111
|
-
} finally {
|
|
27112
|
-
clearTimeout(timeout);
|
|
27113
|
-
}
|
|
27114
|
-
};
|
|
27115
|
-
const runCommand = async (command) => await new Promise((resolve) => {
|
|
27116
|
-
const [file, ...args] = command;
|
|
27117
|
-
if (!file) {
|
|
27118
|
-
resolve(false);
|
|
27119
|
-
return;
|
|
27120
|
-
}
|
|
27121
|
-
const proc = spawn(file, args, { stdio: "ignore" });
|
|
27122
|
-
proc.once("error", () => resolve(false));
|
|
27123
|
-
proc.once("exit", (code) => resolve(code === 0));
|
|
27124
|
-
});
|
|
27125
|
-
const isWSL = async () => {
|
|
27126
|
-
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
|
|
27127
|
-
const version = await readFile("/proc/version", "utf8").catch(() => "");
|
|
27128
|
-
return WSL_VERSION.test(version);
|
|
27129
|
-
};
|
|
27130
|
-
const openBrowser = async (url) => {
|
|
27131
|
-
await assertBrowserTargetReachable(url);
|
|
27132
|
-
if (process.platform === "darwin") return await runCommand(["open", url]);
|
|
27133
|
-
if (process.platform === "win32") return await runCommand([
|
|
27134
|
-
"cmd",
|
|
27135
|
-
"/c",
|
|
27136
|
-
"start",
|
|
27137
|
-
url
|
|
27138
|
-
]);
|
|
27139
|
-
if (await isWSL()) return await runCommand(["wslview", url]) || await runCommand([
|
|
27140
|
-
"powershell.exe",
|
|
27141
|
-
"-NoProfile",
|
|
27142
|
-
"-Command",
|
|
27143
|
-
"Start-Process",
|
|
27144
|
-
url
|
|
27145
|
-
]);
|
|
27146
|
-
return await runCommand(["xdg-open", url]);
|
|
27147
|
-
};
|
|
27148
|
-
//#endregion
|
|
27149
27065
|
//#region src/login/phases/0.sign-in.ts
|
|
27150
27066
|
const CLIENT_ID = "social-cli";
|
|
27151
27067
|
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
27068
|
+
const MAGIC_LINK_PATH = "/api/auth/sign-in/magic-link";
|
|
27152
27069
|
const TERMS_URL = new URL(siteConfig.links.terms, siteConfig.publicWebURL).toString();
|
|
27070
|
+
const LOGIN_SUCCESS_MESSAGE = "Logged in! 🎉";
|
|
27071
|
+
const NEXT_COMMANDS_MESSAGE = "Now run `social account connect linkedin` or `social account connect x`.";
|
|
27153
27072
|
const formatUserCode = (code) => `${code.slice(0, 4)}-${code.slice(4)}`;
|
|
27154
27073
|
const isRecord$2 = (value) => typeof value === "object" && value !== null;
|
|
27155
|
-
const isAuthErrorResponse = (data) => isRecord$2(data) && (typeof data.error === "string" || typeof data.error_description === "string");
|
|
27074
|
+
const isAuthErrorResponse = (data) => isRecord$2(data) && (typeof data.error === "string" || typeof data.error_description === "string" || typeof data.message === "string");
|
|
27156
27075
|
const isDeviceCodeResponse = (data) => isRecord$2(data) && typeof data.device_code === "string" && typeof data.user_code === "string" && typeof data.verification_uri === "string" && typeof data.verification_uri_complete === "string" && typeof data.expires_in === "number" && (data.interval === void 0 || typeof data.interval === "number");
|
|
27157
27076
|
const isDeviceTokenResponse = (data) => isRecord$2(data) && typeof data.access_token === "string" && (data.token_type === void 0 || typeof data.token_type === "string") && (data.expires_in === void 0 || typeof data.expires_in === "number") && (data.scope === void 0 || typeof data.scope === "string");
|
|
27077
|
+
const isMagicLinkResponse = (data) => isRecord$2(data) && data.status === true;
|
|
27158
27078
|
const authErrorMessage = (fallback, response, data) => {
|
|
27159
|
-
if (data?.error_description || data?.error) return data.error_description ?? data.error ?? fallback;
|
|
27079
|
+
if (data?.error_description || data?.error || data?.message) return data.error_description ?? data.error ?? data.message ?? fallback;
|
|
27160
27080
|
const status = [response.status, response.statusText].filter(Boolean).join(" ");
|
|
27161
27081
|
return status ? `${fallback}: ${status}` : fallback;
|
|
27162
27082
|
};
|
|
@@ -27196,30 +27116,60 @@ const pollDeviceToken = async (deviceCode) => {
|
|
|
27196
27116
|
}
|
|
27197
27117
|
throw new Error("Device sign-in expired before approval.");
|
|
27198
27118
|
};
|
|
27199
|
-
const
|
|
27119
|
+
const magicLinkURLFor = (webBaseURL = env.SOCIAL_WEB_URL) => createAbsoluteURL(webBaseURL, MAGIC_LINK_PATH);
|
|
27120
|
+
const deviceCallbackURLFor = (deviceCode, email) => {
|
|
27121
|
+
let parsed;
|
|
27200
27122
|
try {
|
|
27201
|
-
|
|
27202
|
-
const userCode = parsed.searchParams.get("user_code");
|
|
27203
|
-
if (userCode) parsed.searchParams.set("user_code", formatUserCode(userCode));
|
|
27204
|
-
if (webBaseURL) try {
|
|
27205
|
-
const base = new URL(webBaseURL);
|
|
27206
|
-
parsed.protocol = base.protocol;
|
|
27207
|
-
parsed.host = base.host;
|
|
27208
|
-
parsed.username = "";
|
|
27209
|
-
parsed.password = "";
|
|
27210
|
-
} catch {}
|
|
27211
|
-
parsed.searchParams.set("email", email);
|
|
27212
|
-
return parsed.toString();
|
|
27123
|
+
parsed = new URL(deviceCode.verification_uri_complete);
|
|
27213
27124
|
} catch {
|
|
27214
|
-
|
|
27125
|
+
parsed = new URL("/device", "https://social.local");
|
|
27126
|
+
}
|
|
27127
|
+
parsed.searchParams.set("user_code", formatUserCode(deviceCode.user_code));
|
|
27128
|
+
parsed.searchParams.set("email", email);
|
|
27129
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
27130
|
+
};
|
|
27131
|
+
const sendMagicLinkSignIn = async (email, callbackURL, deps = {}) => {
|
|
27132
|
+
const response = await (deps.fetch ?? fetch)(magicLinkURLFor(deps.webBaseURL), {
|
|
27133
|
+
method: "POST",
|
|
27134
|
+
headers: {
|
|
27135
|
+
"content-type": "application/json",
|
|
27136
|
+
"x-social-surface": "cli"
|
|
27137
|
+
},
|
|
27138
|
+
body: JSON.stringify({
|
|
27139
|
+
email,
|
|
27140
|
+
callbackURL
|
|
27141
|
+
})
|
|
27142
|
+
});
|
|
27143
|
+
const data = await response.json().catch(() => void 0);
|
|
27144
|
+
if (!(response.ok && isMagicLinkResponse(data))) {
|
|
27145
|
+
const error = isAuthErrorResponse(data) ? data : void 0;
|
|
27146
|
+
throw new Error(authErrorMessage("Could not send sign-in email", response, error));
|
|
27147
|
+
}
|
|
27148
|
+
};
|
|
27149
|
+
const waitForDeviceToken = async (ctx, deviceCode) => {
|
|
27150
|
+
const spinner = ctx.ui.spinner();
|
|
27151
|
+
const message = "Waiting for magic link approval";
|
|
27152
|
+
spinner.start(message);
|
|
27153
|
+
const clearTick = spinner.withElapsed(message);
|
|
27154
|
+
try {
|
|
27155
|
+
const token = await pollDeviceToken(deviceCode);
|
|
27156
|
+
const tokenExpiresAt = token.expires_in ? Date.now() + token.expires_in * 1e3 : void 0;
|
|
27157
|
+
await writeCredentials({
|
|
27158
|
+
accessToken: token.access_token,
|
|
27159
|
+
tokenType: token.token_type,
|
|
27160
|
+
expiresAt: tokenExpiresAt,
|
|
27161
|
+
scope: token.scope
|
|
27162
|
+
});
|
|
27163
|
+
clearTick();
|
|
27164
|
+
spinner.stop(LOGIN_SUCCESS_MESSAGE);
|
|
27165
|
+
ctx.ui.info(NEXT_COMMANDS_MESSAGE);
|
|
27166
|
+
return token;
|
|
27167
|
+
} catch (error) {
|
|
27168
|
+
clearTick();
|
|
27169
|
+
spinner.error(message);
|
|
27170
|
+
throw error;
|
|
27215
27171
|
}
|
|
27216
27172
|
};
|
|
27217
|
-
const onboardingLegalNotice = (termsURL = TERMS_URL) => `By signing up and connecting accounts, you accept the terms and conditions and use this software at your own risk.\nRead the terms: ${termsURL}`;
|
|
27218
|
-
const resolveEmail = async (ctx) => await ctx.ui.text({
|
|
27219
|
-
message: "Email",
|
|
27220
|
-
placeholder: "you@example.com",
|
|
27221
|
-
validate: (value) => value?.includes("@") ? void 0 : "Enter an email address."
|
|
27222
|
-
});
|
|
27223
27173
|
const signInPhase = async (ctx) => {
|
|
27224
27174
|
const email = await resolveEmail(ctx);
|
|
27225
27175
|
if (!email) return {
|
|
@@ -27229,27 +27179,25 @@ const signInPhase = async (ctx) => {
|
|
|
27229
27179
|
};
|
|
27230
27180
|
const deviceCode = await createDeviceCode(ctx.args.scope ?? "read");
|
|
27231
27181
|
const expiresAt = new Date(Date.now() + deviceCode.expires_in * 1e3).toISOString();
|
|
27232
|
-
const
|
|
27182
|
+
const callbackURL = deviceCallbackURLFor(deviceCode, email);
|
|
27233
27183
|
const userCode = formatUserCode(deviceCode.user_code);
|
|
27234
|
-
ctx.ui.
|
|
27235
|
-
|
|
27236
|
-
const token = await ctx.ui.spinElapsed("Waiting for browser approval", () => pollDeviceToken(deviceCode), "Device approved");
|
|
27237
|
-
const tokenExpiresAt = token.expires_in ? Date.now() + token.expires_in * 1e3 : void 0;
|
|
27238
|
-
await writeCredentials({
|
|
27239
|
-
accessToken: token.access_token,
|
|
27240
|
-
tokenType: token.token_type,
|
|
27241
|
-
expiresAt: tokenExpiresAt,
|
|
27242
|
-
scope: token.scope
|
|
27243
|
-
});
|
|
27184
|
+
await ctx.ui.spin("Sending sign-in email", () => sendMagicLinkSignIn(email, callbackURL), "Magic link sent");
|
|
27185
|
+
ctx.ui.note(`Code: ${userCode}\nEmail: ${email}\n\nClick the magic link in your inbox to approve this CLI session.\n\n${onboardingLegalNotice()}`, "Check your email");
|
|
27244
27186
|
return {
|
|
27245
27187
|
status: "done",
|
|
27246
27188
|
data: {
|
|
27247
27189
|
email,
|
|
27248
27190
|
expiresAt,
|
|
27249
|
-
scope:
|
|
27191
|
+
scope: (await waitForDeviceToken(ctx, deviceCode)).scope
|
|
27250
27192
|
}
|
|
27251
27193
|
};
|
|
27252
27194
|
};
|
|
27195
|
+
const onboardingLegalNotice = (termsURL = TERMS_URL) => `By signing up and connecting accounts, you accept the terms and conditions and use this software at your own risk.\nRead the terms: ${termsURL}`;
|
|
27196
|
+
const resolveEmail = async (ctx) => await ctx.ui.text({
|
|
27197
|
+
message: "Email",
|
|
27198
|
+
placeholder: "you@example.com",
|
|
27199
|
+
validate: (value) => value?.includes("@") ? void 0 : "Enter an email address."
|
|
27200
|
+
});
|
|
27253
27201
|
//#endregion
|
|
27254
27202
|
//#region src/login/phases/1.scope.ts
|
|
27255
27203
|
const DEFAULT_SCOPE_ALIAS = "read,write";
|
|
@@ -27268,6 +27216,85 @@ const scopePhase = async (ctx) => {
|
|
|
27268
27216
|
data: await ctx.client.cli.session.upsert({ scopeAlias })
|
|
27269
27217
|
};
|
|
27270
27218
|
};
|
|
27219
|
+
//#endregion
|
|
27220
|
+
//#region src/lib/browser.ts
|
|
27221
|
+
const WSL_VERSION = /microsoft|wsl/i;
|
|
27222
|
+
const BROWSER_TARGET_TIMEOUT_MS = 5e3;
|
|
27223
|
+
const LOCAL_HOSTS = new Set([
|
|
27224
|
+
"localhost",
|
|
27225
|
+
"127.0.0.1",
|
|
27226
|
+
"::1"
|
|
27227
|
+
]);
|
|
27228
|
+
const displayURL = (url) => {
|
|
27229
|
+
try {
|
|
27230
|
+
const parsed = new URL(url);
|
|
27231
|
+
parsed.searchParams.delete("token");
|
|
27232
|
+
parsed.hash = "";
|
|
27233
|
+
return parsed.toString();
|
|
27234
|
+
} catch {
|
|
27235
|
+
return url;
|
|
27236
|
+
}
|
|
27237
|
+
};
|
|
27238
|
+
const hintFor = (url) => LOCAL_HOSTS.has(url.hostname) ? "Start the web app or local worker, or set SOCIAL_API_URL to your deployed social API." : "Check SOCIAL_API_URL and your connection, then try again.";
|
|
27239
|
+
const assertBrowserTargetReachable = async (url) => {
|
|
27240
|
+
let parsed;
|
|
27241
|
+
try {
|
|
27242
|
+
parsed = new URL(url);
|
|
27243
|
+
} catch {
|
|
27244
|
+
return;
|
|
27245
|
+
}
|
|
27246
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
|
|
27247
|
+
if (!LOCAL_HOSTS.has(parsed.hostname)) return;
|
|
27248
|
+
const controller = new AbortController();
|
|
27249
|
+
const timeout = setTimeout(() => controller.abort(), BROWSER_TARGET_TIMEOUT_MS);
|
|
27250
|
+
const target = parsed.origin;
|
|
27251
|
+
try {
|
|
27252
|
+
const response = await fetch(target, {
|
|
27253
|
+
method: "GET",
|
|
27254
|
+
redirect: "manual",
|
|
27255
|
+
signal: controller.signal
|
|
27256
|
+
});
|
|
27257
|
+
if (response.status >= 400) throw new Error(`social is reachable at ${displayURL(target)}, but it returned ${response.status}. Try again in a moment.`);
|
|
27258
|
+
} catch (error) {
|
|
27259
|
+
if (error instanceof Error && error.message.startsWith("social is reachable")) throw error;
|
|
27260
|
+
throw new Error(`Can't reach ${displayURL(target)}. ${hintFor(parsed)}`);
|
|
27261
|
+
} finally {
|
|
27262
|
+
clearTimeout(timeout);
|
|
27263
|
+
}
|
|
27264
|
+
};
|
|
27265
|
+
const runCommand = async (command) => await new Promise((resolve) => {
|
|
27266
|
+
const [file, ...args] = command;
|
|
27267
|
+
if (!file) {
|
|
27268
|
+
resolve(false);
|
|
27269
|
+
return;
|
|
27270
|
+
}
|
|
27271
|
+
const proc = spawn(file, args, { stdio: "ignore" });
|
|
27272
|
+
proc.once("error", () => resolve(false));
|
|
27273
|
+
proc.once("exit", (code) => resolve(code === 0));
|
|
27274
|
+
});
|
|
27275
|
+
const isWSL = async () => {
|
|
27276
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
|
|
27277
|
+
const version = await readFile("/proc/version", "utf8").catch(() => "");
|
|
27278
|
+
return WSL_VERSION.test(version);
|
|
27279
|
+
};
|
|
27280
|
+
const openBrowser = async (url) => {
|
|
27281
|
+
await assertBrowserTargetReachable(url);
|
|
27282
|
+
if (process.platform === "darwin") return await runCommand(["open", url]);
|
|
27283
|
+
if (process.platform === "win32") return await runCommand([
|
|
27284
|
+
"cmd",
|
|
27285
|
+
"/c",
|
|
27286
|
+
"start",
|
|
27287
|
+
url
|
|
27288
|
+
]);
|
|
27289
|
+
if (await isWSL()) return await runCommand(["wslview", url]) || await runCommand([
|
|
27290
|
+
"powershell.exe",
|
|
27291
|
+
"-NoProfile",
|
|
27292
|
+
"-Command",
|
|
27293
|
+
"Start-Process",
|
|
27294
|
+
url
|
|
27295
|
+
]);
|
|
27296
|
+
return await runCommand(["xdg-open", url]);
|
|
27297
|
+
};
|
|
27271
27298
|
const SEAT_CHECKOUT_RETURN_PATH = "/setup/billing/done";
|
|
27272
27299
|
const SEAT_POLL_INTERVAL_MS = 2e3;
|
|
27273
27300
|
const SEAT_POLL_TIMEOUT_MS = 1800 * 1e3;
|
|
@@ -27710,11 +27737,7 @@ const LifecycleAccountOutput = object({
|
|
|
27710
27737
|
connectedAt: string().optional(),
|
|
27711
27738
|
lastSeenAt: string().optional()
|
|
27712
27739
|
}).passthrough();
|
|
27713
|
-
const AccountConnectOutput =
|
|
27714
|
-
platform: _enum(["linkedin", "x"]),
|
|
27715
|
-
status: literal("connected"),
|
|
27716
|
-
account: LifecycleAccountOutput
|
|
27717
|
-
});
|
|
27740
|
+
const AccountConnectOutput = string();
|
|
27718
27741
|
const AccountDisconnectOutput = object({
|
|
27719
27742
|
platform: _enum(["linkedin", "x"]),
|
|
27720
27743
|
account: LifecycleAccountOutput,
|
|
@@ -27789,20 +27812,38 @@ const writeOutput = async (deps, value) => {
|
|
|
27789
27812
|
}
|
|
27790
27813
|
printData(value);
|
|
27791
27814
|
};
|
|
27815
|
+
const writeText = (deps, value) => {
|
|
27816
|
+
if (deps.writeText) {
|
|
27817
|
+
deps.writeText(value);
|
|
27818
|
+
return;
|
|
27819
|
+
}
|
|
27820
|
+
printLine(value);
|
|
27821
|
+
};
|
|
27792
27822
|
const platformLabel = (platform) => platform === "x" ? "X" : "LinkedIn";
|
|
27823
|
+
const lifecycleConnectContract = {
|
|
27824
|
+
capability: "write",
|
|
27825
|
+
auth: writeAuthContract,
|
|
27826
|
+
mutates: true,
|
|
27827
|
+
outputSchema: AccountConnectOutput,
|
|
27828
|
+
responseShaping: { supported: false },
|
|
27829
|
+
idempotency: "non-idempotent",
|
|
27830
|
+
confirmation: false
|
|
27831
|
+
};
|
|
27832
|
+
const handleFor = (account) => account.handle.startsWith("@") ? account.handle : `@${account.handle}`;
|
|
27833
|
+
const lifecycleDepsWithTextLogs = (deps) => ({
|
|
27834
|
+
...deps,
|
|
27835
|
+
log: (message) => writeText(deps, message)
|
|
27836
|
+
});
|
|
27837
|
+
const runConnectOutput = async (deps, connect) => {
|
|
27838
|
+
writeText(deps, "Handshaking auth...");
|
|
27839
|
+
writeText(deps, `${handleFor((await connect(lifecycleDepsWithTextLogs(deps))).account)} connected!`);
|
|
27840
|
+
};
|
|
27793
27841
|
const createConnectPlatformCommand = (platform, run) => defineCommand({
|
|
27794
27842
|
meta: commandMeta({
|
|
27795
27843
|
name: platform,
|
|
27796
27844
|
description: `Connect a ${platformLabel(platform)} account.`,
|
|
27797
27845
|
capability: "write",
|
|
27798
|
-
contract:
|
|
27799
|
-
capability: "write",
|
|
27800
|
-
auth: writeAuthContract,
|
|
27801
|
-
mutates: true,
|
|
27802
|
-
outputSchema: AccountConnectOutput,
|
|
27803
|
-
idempotency: "non-idempotent",
|
|
27804
|
-
confirmation: false
|
|
27805
|
-
},
|
|
27846
|
+
contract: lifecycleConnectContract,
|
|
27806
27847
|
mutates: true
|
|
27807
27848
|
}),
|
|
27808
27849
|
run: async () => {
|
|
@@ -27814,14 +27855,7 @@ const createAccountPlatformCommand = (platform, description, run, options = {})
|
|
|
27814
27855
|
name: platform,
|
|
27815
27856
|
description,
|
|
27816
27857
|
capability: "write",
|
|
27817
|
-
contract: options.contract ??
|
|
27818
|
-
capability: "write",
|
|
27819
|
-
auth: writeAuthContract,
|
|
27820
|
-
mutates: true,
|
|
27821
|
-
outputSchema: AccountConnectOutput,
|
|
27822
|
-
idempotency: "non-idempotent",
|
|
27823
|
-
confirmation: false
|
|
27824
|
-
},
|
|
27858
|
+
contract: options.contract ?? lifecycleConnectContract,
|
|
27825
27859
|
mutates: true
|
|
27826
27860
|
}),
|
|
27827
27861
|
args: { account: options.accountArg ?? accountArg },
|
|
@@ -27848,10 +27882,10 @@ const createAccountCommand = (deps) => {
|
|
|
27848
27882
|
},
|
|
27849
27883
|
subCommands: {
|
|
27850
27884
|
linkedin: createConnectPlatformCommand("linkedin", async () => {
|
|
27851
|
-
await
|
|
27885
|
+
await runConnectOutput(deps, connectLinkedinAccount);
|
|
27852
27886
|
}),
|
|
27853
27887
|
x: createConnectPlatformCommand("x", async () => {
|
|
27854
|
-
await
|
|
27888
|
+
await runConnectOutput(deps, connectXAccount);
|
|
27855
27889
|
})
|
|
27856
27890
|
}
|
|
27857
27891
|
}),
|
|
@@ -27862,10 +27896,10 @@ const createAccountCommand = (deps) => {
|
|
|
27862
27896
|
},
|
|
27863
27897
|
subCommands: {
|
|
27864
27898
|
linkedin: createAccountPlatformCommand("linkedin", "Reconnect a LinkedIn account.", async (args) => {
|
|
27865
|
-
await
|
|
27899
|
+
await runConnectOutput(deps, (connectDeps) => reconnectLinkedinAccount(connectDeps, String(args.account)));
|
|
27866
27900
|
}, { accountArg: linkedinReconnectAccountArg }),
|
|
27867
27901
|
x: createAccountPlatformCommand("x", "Reconnect an X account.", async (args) => {
|
|
27868
|
-
await
|
|
27902
|
+
await runConnectOutput(deps, (connectDeps) => reconnectXAccount(connectDeps, { account: String(args.account) }));
|
|
27869
27903
|
}, { accountArg: xAccountArg })
|
|
27870
27904
|
}
|
|
27871
27905
|
}),
|