@usesocial/cli 0.2.0 → 0.2.2
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 +12 -0
- package/README.md +4 -3
- package/bin/social.mjs +0 -7
- package/dist/index.mjs +228 -161
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @usesocial/cli
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Clean up account connect progress output so X and LinkedIn show the auth handshake spinner before opening the browser URL.
|
|
8
|
+
|
|
9
|
+
## 0.2.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Improve account connection progress output and hosted auth resilience.
|
|
14
|
+
|
|
3
15
|
## 0.2.0
|
|
4
16
|
|
|
5
17
|
### Minor 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
|
@@ -17919,18 +17919,14 @@ const CONNECTED_STATUS = "connected";
|
|
|
17919
17919
|
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
|
-
const openURL = async (deps, url) => {
|
|
17922
|
+
const openURL = async (deps, url, options = {}) => {
|
|
17923
17923
|
const log = deps.log ?? console.error;
|
|
17924
|
-
if (
|
|
17925
|
-
|
|
17926
|
-
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
17930
|
-
}
|
|
17931
|
-
const handoff = await Promise.resolve(deps.openURL(url));
|
|
17932
|
-
if (!handoff.opened) log(url);
|
|
17933
|
-
return handoff;
|
|
17924
|
+
if (options.log ?? true) log(`Opening ${url}`);
|
|
17925
|
+
if (!(isInteractiveTerminal$1(deps) && deps.openURL)) return {
|
|
17926
|
+
opened: false,
|
|
17927
|
+
url
|
|
17928
|
+
};
|
|
17929
|
+
return await Promise.resolve(deps.openURL(url));
|
|
17934
17930
|
};
|
|
17935
17931
|
const pollForSeat$2 = async (deps) => {
|
|
17936
17932
|
const intervalMs = deps.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
@@ -18003,6 +17999,10 @@ const pollForAccount$1 = async (deps, predicate, errorMessage) => {
|
|
|
18003
17999
|
}
|
|
18004
18000
|
throw new Error(errorMessage);
|
|
18005
18001
|
};
|
|
18002
|
+
const authURLFor$1 = async (deps, operation) => ({
|
|
18003
|
+
url: deps.handshakeAuthURL ? await deps.handshakeAuthURL(operation) : await operation(),
|
|
18004
|
+
openingLogged: deps.handshakeAuthURL !== void 0
|
|
18005
|
+
});
|
|
18006
18006
|
const accountForIdentifier = async (deps, identifier) => {
|
|
18007
18007
|
const target = linkedinReconnectTargetFrom(identifier);
|
|
18008
18008
|
const account = (await listLinkedinLifecycleAccounts(deps)).find((candidate) => accountMatchesReconnectTarget(candidate, target));
|
|
@@ -18019,10 +18019,11 @@ const connectLinkedinAccount = async (deps) => {
|
|
|
18019
18019
|
await prepareAccountConnect$1(deps);
|
|
18020
18020
|
const before = await listLinkedinLifecycleAccounts(deps);
|
|
18021
18021
|
const existingProfileIds = new Set(before.map((account) => account.profileId));
|
|
18022
|
-
await
|
|
18022
|
+
const authURL = await authURLFor$1(deps, async () => (await deps.client.createHostedAuthURL({
|
|
18023
18023
|
platform: LINKEDIN_PLATFORM,
|
|
18024
18024
|
mode: "create"
|
|
18025
18025
|
})).url);
|
|
18026
|
+
await openURL(deps, authURL.url, { log: !authURL.openingLogged });
|
|
18026
18027
|
return {
|
|
18027
18028
|
platform: LINKEDIN_PLATFORM,
|
|
18028
18029
|
status: "connected",
|
|
@@ -18031,11 +18032,12 @@ const connectLinkedinAccount = async (deps) => {
|
|
|
18031
18032
|
};
|
|
18032
18033
|
const reconnectLinkedinAccount = async (deps, identifier) => {
|
|
18033
18034
|
const account = await reconnectAccountForIdentifier(deps, identifier);
|
|
18034
|
-
await
|
|
18035
|
+
const authURL = await authURLFor$1(deps, async () => (await deps.client.createHostedAuthURL({
|
|
18035
18036
|
platform: LINKEDIN_PLATFORM,
|
|
18036
18037
|
mode: "reconnect",
|
|
18037
18038
|
account: reconnectCommandTargetFor(account)
|
|
18038
18039
|
})).url);
|
|
18040
|
+
await openURL(deps, authURL.url, { log: !authURL.openingLogged });
|
|
18039
18041
|
return {
|
|
18040
18042
|
platform: LINKEDIN_PLATFORM,
|
|
18041
18043
|
status: "connected",
|
|
@@ -18083,13 +18085,11 @@ const connectURLFor = async (deps, reconnectProfileId) => {
|
|
|
18083
18085
|
return url.toString();
|
|
18084
18086
|
};
|
|
18085
18087
|
const isInteractiveTerminal = (deps) => deps.isInteractiveTerminal?.() ?? process.stdout.isTTY === true;
|
|
18086
|
-
const openOrPrint = async (deps, url) => {
|
|
18088
|
+
const openOrPrint = async (deps, url, options = {}) => {
|
|
18087
18089
|
const log = deps.log ?? console.error;
|
|
18088
|
-
if (
|
|
18089
|
-
|
|
18090
|
-
|
|
18091
|
-
}
|
|
18092
|
-
if (await deps.openBrowser(url) === false) log(url);
|
|
18090
|
+
if (options.log ?? true) log(`Opening ${url}`);
|
|
18091
|
+
if (!(isInteractiveTerminal(deps) && deps.openBrowser)) return;
|
|
18092
|
+
await deps.openBrowser(url);
|
|
18093
18093
|
};
|
|
18094
18094
|
const pollForAccount = async (deps, matches) => {
|
|
18095
18095
|
const sleep = deps.sleep ?? defaultSleep;
|
|
@@ -18105,6 +18105,10 @@ const pollForAccount = async (deps, matches) => {
|
|
|
18105
18105
|
}
|
|
18106
18106
|
throw new Error("x_connect_timed_out");
|
|
18107
18107
|
};
|
|
18108
|
+
const authURLFor = async (deps, operation) => ({
|
|
18109
|
+
url: deps.handshakeAuthURL ? await deps.handshakeAuthURL(operation) : await operation(),
|
|
18110
|
+
openingLogged: deps.handshakeAuthURL !== void 0
|
|
18111
|
+
});
|
|
18108
18112
|
const pollForSeat$1 = async (deps) => {
|
|
18109
18113
|
const sleep = deps.sleep ?? defaultSleep;
|
|
18110
18114
|
const now = deps.now ?? Date.now;
|
|
@@ -18151,7 +18155,8 @@ const connectXAccount = async (deps) => {
|
|
|
18151
18155
|
const before = await listXLifecycleAccounts(deps, true);
|
|
18152
18156
|
const connectedBefore = new Set(before.filter((account) => account.status === "connected").map(accountKey));
|
|
18153
18157
|
await prepareAccountConnect(deps);
|
|
18154
|
-
await
|
|
18158
|
+
const authURL = await authURLFor(deps, () => connectURLFor(deps));
|
|
18159
|
+
await openOrPrint(deps, authURL.url, { log: !authURL.openingLogged });
|
|
18155
18160
|
return {
|
|
18156
18161
|
platform: "x",
|
|
18157
18162
|
status: "connected",
|
|
@@ -18162,7 +18167,8 @@ const reconnectXAccount = async (deps, args) => {
|
|
|
18162
18167
|
assertXAccountSelector(args.account);
|
|
18163
18168
|
const existing = findAccount(await listXLifecycleAccounts(deps, true), args.account);
|
|
18164
18169
|
if (!existing) throw new Error("x_account_not_found");
|
|
18165
|
-
await
|
|
18170
|
+
const authURL = await authURLFor(deps, () => connectURLFor(deps, existing.profileId));
|
|
18171
|
+
await openOrPrint(deps, authURL.url, { log: !authURL.openingLogged });
|
|
18166
18172
|
return {
|
|
18167
18173
|
platform: "x",
|
|
18168
18174
|
status: "connected",
|
|
@@ -20775,7 +20781,7 @@ const env = createEnv({
|
|
|
20775
20781
|
}
|
|
20776
20782
|
});
|
|
20777
20783
|
const SERVICE_NAME = "social-cli";
|
|
20778
|
-
const VERSION = "0.2.
|
|
20784
|
+
const VERSION = "0.2.2";
|
|
20779
20785
|
const apiURL = (path) => createAbsoluteURL(env.SOCIAL_API_URL, path);
|
|
20780
20786
|
//#endregion
|
|
20781
20787
|
//#region src/lib/bearer.ts
|
|
@@ -21131,6 +21137,9 @@ const writeStdoutBuffer = (buffer) => {
|
|
|
21131
21137
|
const writeStdout = (value) => {
|
|
21132
21138
|
writeStdoutBuffer(Buffer.from(`${value}\n`));
|
|
21133
21139
|
};
|
|
21140
|
+
const printLine = (value) => {
|
|
21141
|
+
writeStdout(value);
|
|
21142
|
+
};
|
|
21134
21143
|
const printData = (value) => {
|
|
21135
21144
|
writeStdout(JSON.stringify(value));
|
|
21136
21145
|
};
|
|
@@ -27067,96 +27076,21 @@ const accessPhase = async (ctx) => {
|
|
|
27067
27076
|
};
|
|
27068
27077
|
};
|
|
27069
27078
|
//#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
27079
|
//#region src/login/phases/0.sign-in.ts
|
|
27150
27080
|
const CLIENT_ID = "social-cli";
|
|
27151
27081
|
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
27082
|
+
const MAGIC_LINK_PATH = "/api/auth/sign-in/magic-link";
|
|
27152
27083
|
const TERMS_URL = new URL(siteConfig.links.terms, siteConfig.publicWebURL).toString();
|
|
27084
|
+
const LOGIN_SUCCESS_MESSAGE = "Logged in! 🎉";
|
|
27085
|
+
const NEXT_COMMANDS_MESSAGE = "Now run `social account connect linkedin` or `social account connect x`.";
|
|
27153
27086
|
const formatUserCode = (code) => `${code.slice(0, 4)}-${code.slice(4)}`;
|
|
27154
27087
|
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");
|
|
27088
|
+
const isAuthErrorResponse = (data) => isRecord$2(data) && (typeof data.error === "string" || typeof data.error_description === "string" || typeof data.message === "string");
|
|
27156
27089
|
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
27090
|
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");
|
|
27091
|
+
const isMagicLinkResponse = (data) => isRecord$2(data) && data.status === true;
|
|
27158
27092
|
const authErrorMessage = (fallback, response, data) => {
|
|
27159
|
-
if (data?.error_description || data?.error) return data.error_description ?? data.error ?? fallback;
|
|
27093
|
+
if (data?.error_description || data?.error || data?.message) return data.error_description ?? data.error ?? data.message ?? fallback;
|
|
27160
27094
|
const status = [response.status, response.statusText].filter(Boolean).join(" ");
|
|
27161
27095
|
return status ? `${fallback}: ${status}` : fallback;
|
|
27162
27096
|
};
|
|
@@ -27196,30 +27130,60 @@ const pollDeviceToken = async (deviceCode) => {
|
|
|
27196
27130
|
}
|
|
27197
27131
|
throw new Error("Device sign-in expired before approval.");
|
|
27198
27132
|
};
|
|
27199
|
-
const
|
|
27133
|
+
const magicLinkURLFor = (webBaseURL = env.SOCIAL_WEB_URL) => createAbsoluteURL(webBaseURL, MAGIC_LINK_PATH);
|
|
27134
|
+
const deviceCallbackURLFor = (deviceCode, email) => {
|
|
27135
|
+
let parsed;
|
|
27200
27136
|
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();
|
|
27137
|
+
parsed = new URL(deviceCode.verification_uri_complete);
|
|
27213
27138
|
} catch {
|
|
27214
|
-
|
|
27139
|
+
parsed = new URL("/device", "https://social.local");
|
|
27140
|
+
}
|
|
27141
|
+
parsed.searchParams.set("user_code", formatUserCode(deviceCode.user_code));
|
|
27142
|
+
parsed.searchParams.set("email", email);
|
|
27143
|
+
return `${parsed.pathname}${parsed.search}`;
|
|
27144
|
+
};
|
|
27145
|
+
const sendMagicLinkSignIn = async (email, callbackURL, deps = {}) => {
|
|
27146
|
+
const response = await (deps.fetch ?? fetch)(magicLinkURLFor(deps.webBaseURL), {
|
|
27147
|
+
method: "POST",
|
|
27148
|
+
headers: {
|
|
27149
|
+
"content-type": "application/json",
|
|
27150
|
+
"x-social-surface": "cli"
|
|
27151
|
+
},
|
|
27152
|
+
body: JSON.stringify({
|
|
27153
|
+
email,
|
|
27154
|
+
callbackURL
|
|
27155
|
+
})
|
|
27156
|
+
});
|
|
27157
|
+
const data = await response.json().catch(() => void 0);
|
|
27158
|
+
if (!(response.ok && isMagicLinkResponse(data))) {
|
|
27159
|
+
const error = isAuthErrorResponse(data) ? data : void 0;
|
|
27160
|
+
throw new Error(authErrorMessage("Could not send sign-in email", response, error));
|
|
27161
|
+
}
|
|
27162
|
+
};
|
|
27163
|
+
const waitForDeviceToken = async (ctx, deviceCode) => {
|
|
27164
|
+
const spinner = ctx.ui.spinner();
|
|
27165
|
+
const message = "Waiting for magic link approval";
|
|
27166
|
+
spinner.start(message);
|
|
27167
|
+
const clearTick = spinner.withElapsed(message);
|
|
27168
|
+
try {
|
|
27169
|
+
const token = await pollDeviceToken(deviceCode);
|
|
27170
|
+
const tokenExpiresAt = token.expires_in ? Date.now() + token.expires_in * 1e3 : void 0;
|
|
27171
|
+
await writeCredentials({
|
|
27172
|
+
accessToken: token.access_token,
|
|
27173
|
+
tokenType: token.token_type,
|
|
27174
|
+
expiresAt: tokenExpiresAt,
|
|
27175
|
+
scope: token.scope
|
|
27176
|
+
});
|
|
27177
|
+
clearTick();
|
|
27178
|
+
spinner.stop(LOGIN_SUCCESS_MESSAGE);
|
|
27179
|
+
ctx.ui.info(NEXT_COMMANDS_MESSAGE);
|
|
27180
|
+
return token;
|
|
27181
|
+
} catch (error) {
|
|
27182
|
+
clearTick();
|
|
27183
|
+
spinner.error(message);
|
|
27184
|
+
throw error;
|
|
27215
27185
|
}
|
|
27216
27186
|
};
|
|
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
27187
|
const signInPhase = async (ctx) => {
|
|
27224
27188
|
const email = await resolveEmail(ctx);
|
|
27225
27189
|
if (!email) return {
|
|
@@ -27229,27 +27193,25 @@ const signInPhase = async (ctx) => {
|
|
|
27229
27193
|
};
|
|
27230
27194
|
const deviceCode = await createDeviceCode(ctx.args.scope ?? "read");
|
|
27231
27195
|
const expiresAt = new Date(Date.now() + deviceCode.expires_in * 1e3).toISOString();
|
|
27232
|
-
const
|
|
27196
|
+
const callbackURL = deviceCallbackURLFor(deviceCode, email);
|
|
27233
27197
|
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
|
-
});
|
|
27198
|
+
await ctx.ui.spin("Sending sign-in email", () => sendMagicLinkSignIn(email, callbackURL), "Magic link sent");
|
|
27199
|
+
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
27200
|
return {
|
|
27245
27201
|
status: "done",
|
|
27246
27202
|
data: {
|
|
27247
27203
|
email,
|
|
27248
27204
|
expiresAt,
|
|
27249
|
-
scope:
|
|
27205
|
+
scope: (await waitForDeviceToken(ctx, deviceCode)).scope
|
|
27250
27206
|
}
|
|
27251
27207
|
};
|
|
27252
27208
|
};
|
|
27209
|
+
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}`;
|
|
27210
|
+
const resolveEmail = async (ctx) => await ctx.ui.text({
|
|
27211
|
+
message: "Email",
|
|
27212
|
+
placeholder: "you@example.com",
|
|
27213
|
+
validate: (value) => value?.includes("@") ? void 0 : "Enter an email address."
|
|
27214
|
+
});
|
|
27253
27215
|
//#endregion
|
|
27254
27216
|
//#region src/login/phases/1.scope.ts
|
|
27255
27217
|
const DEFAULT_SCOPE_ALIAS = "read,write";
|
|
@@ -27268,6 +27230,85 @@ const scopePhase = async (ctx) => {
|
|
|
27268
27230
|
data: await ctx.client.cli.session.upsert({ scopeAlias })
|
|
27269
27231
|
};
|
|
27270
27232
|
};
|
|
27233
|
+
//#endregion
|
|
27234
|
+
//#region src/lib/browser.ts
|
|
27235
|
+
const WSL_VERSION = /microsoft|wsl/i;
|
|
27236
|
+
const BROWSER_TARGET_TIMEOUT_MS = 5e3;
|
|
27237
|
+
const LOCAL_HOSTS = new Set([
|
|
27238
|
+
"localhost",
|
|
27239
|
+
"127.0.0.1",
|
|
27240
|
+
"::1"
|
|
27241
|
+
]);
|
|
27242
|
+
const displayURL = (url) => {
|
|
27243
|
+
try {
|
|
27244
|
+
const parsed = new URL(url);
|
|
27245
|
+
parsed.searchParams.delete("token");
|
|
27246
|
+
parsed.hash = "";
|
|
27247
|
+
return parsed.toString();
|
|
27248
|
+
} catch {
|
|
27249
|
+
return url;
|
|
27250
|
+
}
|
|
27251
|
+
};
|
|
27252
|
+
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.";
|
|
27253
|
+
const assertBrowserTargetReachable = async (url) => {
|
|
27254
|
+
let parsed;
|
|
27255
|
+
try {
|
|
27256
|
+
parsed = new URL(url);
|
|
27257
|
+
} catch {
|
|
27258
|
+
return;
|
|
27259
|
+
}
|
|
27260
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return;
|
|
27261
|
+
if (!LOCAL_HOSTS.has(parsed.hostname)) return;
|
|
27262
|
+
const controller = new AbortController();
|
|
27263
|
+
const timeout = setTimeout(() => controller.abort(), BROWSER_TARGET_TIMEOUT_MS);
|
|
27264
|
+
const target = parsed.origin;
|
|
27265
|
+
try {
|
|
27266
|
+
const response = await fetch(target, {
|
|
27267
|
+
method: "GET",
|
|
27268
|
+
redirect: "manual",
|
|
27269
|
+
signal: controller.signal
|
|
27270
|
+
});
|
|
27271
|
+
if (response.status >= 400) throw new Error(`social is reachable at ${displayURL(target)}, but it returned ${response.status}. Try again in a moment.`);
|
|
27272
|
+
} catch (error) {
|
|
27273
|
+
if (error instanceof Error && error.message.startsWith("social is reachable")) throw error;
|
|
27274
|
+
throw new Error(`Can't reach ${displayURL(target)}. ${hintFor(parsed)}`);
|
|
27275
|
+
} finally {
|
|
27276
|
+
clearTimeout(timeout);
|
|
27277
|
+
}
|
|
27278
|
+
};
|
|
27279
|
+
const runCommand = async (command) => await new Promise((resolve) => {
|
|
27280
|
+
const [file, ...args] = command;
|
|
27281
|
+
if (!file) {
|
|
27282
|
+
resolve(false);
|
|
27283
|
+
return;
|
|
27284
|
+
}
|
|
27285
|
+
const proc = spawn(file, args, { stdio: "ignore" });
|
|
27286
|
+
proc.once("error", () => resolve(false));
|
|
27287
|
+
proc.once("exit", (code) => resolve(code === 0));
|
|
27288
|
+
});
|
|
27289
|
+
const isWSL = async () => {
|
|
27290
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
|
|
27291
|
+
const version = await readFile("/proc/version", "utf8").catch(() => "");
|
|
27292
|
+
return WSL_VERSION.test(version);
|
|
27293
|
+
};
|
|
27294
|
+
const openBrowser = async (url) => {
|
|
27295
|
+
await assertBrowserTargetReachable(url);
|
|
27296
|
+
if (process.platform === "darwin") return await runCommand(["open", url]);
|
|
27297
|
+
if (process.platform === "win32") return await runCommand([
|
|
27298
|
+
"cmd",
|
|
27299
|
+
"/c",
|
|
27300
|
+
"start",
|
|
27301
|
+
url
|
|
27302
|
+
]);
|
|
27303
|
+
if (await isWSL()) return await runCommand(["wslview", url]) || await runCommand([
|
|
27304
|
+
"powershell.exe",
|
|
27305
|
+
"-NoProfile",
|
|
27306
|
+
"-Command",
|
|
27307
|
+
"Start-Process",
|
|
27308
|
+
url
|
|
27309
|
+
]);
|
|
27310
|
+
return await runCommand(["xdg-open", url]);
|
|
27311
|
+
};
|
|
27271
27312
|
const SEAT_CHECKOUT_RETURN_PATH = "/setup/billing/done";
|
|
27272
27313
|
const SEAT_POLL_INTERVAL_MS = 2e3;
|
|
27273
27314
|
const SEAT_POLL_TIMEOUT_MS = 1800 * 1e3;
|
|
@@ -27710,11 +27751,7 @@ const LifecycleAccountOutput = object({
|
|
|
27710
27751
|
connectedAt: string().optional(),
|
|
27711
27752
|
lastSeenAt: string().optional()
|
|
27712
27753
|
}).passthrough();
|
|
27713
|
-
const AccountConnectOutput =
|
|
27714
|
-
platform: _enum(["linkedin", "x"]),
|
|
27715
|
-
status: literal("connected"),
|
|
27716
|
-
account: LifecycleAccountOutput
|
|
27717
|
-
});
|
|
27754
|
+
const AccountConnectOutput = string();
|
|
27718
27755
|
const AccountDisconnectOutput = object({
|
|
27719
27756
|
platform: _enum(["linkedin", "x"]),
|
|
27720
27757
|
account: LifecycleAccountOutput,
|
|
@@ -27789,20 +27826,56 @@ const writeOutput = async (deps, value) => {
|
|
|
27789
27826
|
}
|
|
27790
27827
|
printData(value);
|
|
27791
27828
|
};
|
|
27829
|
+
const writeText = (deps, value) => {
|
|
27830
|
+
if (deps.writeText) {
|
|
27831
|
+
deps.writeText(value);
|
|
27832
|
+
return;
|
|
27833
|
+
}
|
|
27834
|
+
printLine(value);
|
|
27835
|
+
};
|
|
27792
27836
|
const platformLabel = (platform) => platform === "x" ? "X" : "LinkedIn";
|
|
27837
|
+
const lifecycleConnectContract = {
|
|
27838
|
+
capability: "write",
|
|
27839
|
+
auth: writeAuthContract,
|
|
27840
|
+
mutates: true,
|
|
27841
|
+
outputSchema: AccountConnectOutput,
|
|
27842
|
+
responseShaping: { supported: false },
|
|
27843
|
+
idempotency: "non-idempotent",
|
|
27844
|
+
confirmation: false
|
|
27845
|
+
};
|
|
27846
|
+
const handleFor = (account) => account.handle.startsWith("@") ? account.handle : `@${account.handle}`;
|
|
27847
|
+
const authHandshakeMessage = "Handshaking auth...";
|
|
27848
|
+
const lifecycleDepsWithTextLogs = (deps) => ({
|
|
27849
|
+
...deps,
|
|
27850
|
+
log: (message) => writeText(deps, message),
|
|
27851
|
+
handshakeAuthURL: async (operation) => {
|
|
27852
|
+
if (deps.ui) {
|
|
27853
|
+
const spinner = deps.ui.spinner();
|
|
27854
|
+
spinner.start(authHandshakeMessage);
|
|
27855
|
+
try {
|
|
27856
|
+
const url = await operation();
|
|
27857
|
+
spinner.stop(`Opening ${url}`);
|
|
27858
|
+
return url;
|
|
27859
|
+
} catch (error) {
|
|
27860
|
+
spinner.error(authHandshakeMessage);
|
|
27861
|
+
throw error;
|
|
27862
|
+
}
|
|
27863
|
+
}
|
|
27864
|
+
writeText(deps, authHandshakeMessage);
|
|
27865
|
+
const url = await operation();
|
|
27866
|
+
writeText(deps, `Opening ${url}`);
|
|
27867
|
+
return url;
|
|
27868
|
+
}
|
|
27869
|
+
});
|
|
27870
|
+
const runConnectOutput = async (deps, connect) => {
|
|
27871
|
+
writeText(deps, `${handleFor((await connect(lifecycleDepsWithTextLogs(deps))).account)} connected!`);
|
|
27872
|
+
};
|
|
27793
27873
|
const createConnectPlatformCommand = (platform, run) => defineCommand({
|
|
27794
27874
|
meta: commandMeta({
|
|
27795
27875
|
name: platform,
|
|
27796
27876
|
description: `Connect a ${platformLabel(platform)} account.`,
|
|
27797
27877
|
capability: "write",
|
|
27798
|
-
contract:
|
|
27799
|
-
capability: "write",
|
|
27800
|
-
auth: writeAuthContract,
|
|
27801
|
-
mutates: true,
|
|
27802
|
-
outputSchema: AccountConnectOutput,
|
|
27803
|
-
idempotency: "non-idempotent",
|
|
27804
|
-
confirmation: false
|
|
27805
|
-
},
|
|
27878
|
+
contract: lifecycleConnectContract,
|
|
27806
27879
|
mutates: true
|
|
27807
27880
|
}),
|
|
27808
27881
|
run: async () => {
|
|
@@ -27814,14 +27887,7 @@ const createAccountPlatformCommand = (platform, description, run, options = {})
|
|
|
27814
27887
|
name: platform,
|
|
27815
27888
|
description,
|
|
27816
27889
|
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
|
-
},
|
|
27890
|
+
contract: options.contract ?? lifecycleConnectContract,
|
|
27825
27891
|
mutates: true
|
|
27826
27892
|
}),
|
|
27827
27893
|
args: { account: options.accountArg ?? accountArg },
|
|
@@ -27848,10 +27914,10 @@ const createAccountCommand = (deps) => {
|
|
|
27848
27914
|
},
|
|
27849
27915
|
subCommands: {
|
|
27850
27916
|
linkedin: createConnectPlatformCommand("linkedin", async () => {
|
|
27851
|
-
await
|
|
27917
|
+
await runConnectOutput(deps, connectLinkedinAccount);
|
|
27852
27918
|
}),
|
|
27853
27919
|
x: createConnectPlatformCommand("x", async () => {
|
|
27854
|
-
await
|
|
27920
|
+
await runConnectOutput(deps, connectXAccount);
|
|
27855
27921
|
})
|
|
27856
27922
|
}
|
|
27857
27923
|
}),
|
|
@@ -27862,10 +27928,10 @@ const createAccountCommand = (deps) => {
|
|
|
27862
27928
|
},
|
|
27863
27929
|
subCommands: {
|
|
27864
27930
|
linkedin: createAccountPlatformCommand("linkedin", "Reconnect a LinkedIn account.", async (args) => {
|
|
27865
|
-
await
|
|
27931
|
+
await runConnectOutput(deps, (connectDeps) => reconnectLinkedinAccount(connectDeps, String(args.account)));
|
|
27866
27932
|
}, { accountArg: linkedinReconnectAccountArg }),
|
|
27867
27933
|
x: createAccountPlatformCommand("x", "Reconnect an X account.", async (args) => {
|
|
27868
|
-
await
|
|
27934
|
+
await runConnectOutput(deps, (connectDeps) => reconnectXAccount(connectDeps, { account: String(args.account) }));
|
|
27869
27935
|
}, { accountArg: xAccountArg })
|
|
27870
27936
|
}
|
|
27871
27937
|
}),
|
|
@@ -28182,7 +28248,8 @@ const createCLIDeps = () => {
|
|
|
28182
28248
|
url
|
|
28183
28249
|
};
|
|
28184
28250
|
},
|
|
28185
|
-
writeOutput: printData
|
|
28251
|
+
writeOutput: printData,
|
|
28252
|
+
ui: createUI(process.stdout.isTTY === true)
|
|
28186
28253
|
};
|
|
28187
28254
|
};
|
|
28188
28255
|
//#endregion
|