@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 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. Shows you a verification code and approval URL, then tries to open that URL
192
- in your browser.
193
- 4. Waits until you approve the session in the browser.
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 (!(isInteractiveTerminal$1(deps) && deps.openURL)) {
17925
- log(url);
17926
- return {
17927
- opened: false,
17928
- url
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 openURL(deps, (await deps.client.createHostedAuthURL({
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 openURL(deps, (await deps.client.createHostedAuthURL({
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 (!(isInteractiveTerminal(deps) && deps.openBrowser)) {
18089
- log(url);
18090
- return;
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 openOrPrint(deps, await connectURLFor(deps));
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 openOrPrint(deps, await connectURLFor(deps, existing.profileId));
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.0";
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 verificationURLFor = (url, email, webBaseURL = process.env.SOCIAL_WEB_URL) => {
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
- const parsed = new URL(url);
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
- return url;
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 verificationURL = verificationURLFor(deviceCode.verification_uri_complete, email);
27196
+ const callbackURL = deviceCallbackURLFor(deviceCode, email);
27233
27197
  const userCode = formatUserCode(deviceCode.user_code);
27234
- ctx.ui.note(`Code: ${userCode}\nURL: ${verificationURL}\n\n${onboardingLegalNotice()}`, `Sign in as ${email}`);
27235
- await openBrowser(verificationURL);
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: token.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 = object({
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 writeOutput(deps, await connectLinkedinAccount(deps));
27917
+ await runConnectOutput(deps, connectLinkedinAccount);
27852
27918
  }),
27853
27919
  x: createConnectPlatformCommand("x", async () => {
27854
- await writeOutput(deps, await connectXAccount(deps));
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 writeOutput(deps, await reconnectLinkedinAccount(deps, String(args.account)));
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 writeOutput(deps, await reconnectXAccount(deps, { account: String(args.account) }));
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
package/package.json CHANGED
@@ -11,7 +11,7 @@
11
11
  "access": "public"
12
12
  },
13
13
  "private": false,
14
- "version": "0.2.0",
14
+ "version": "0.2.2",
15
15
  "type": "module",
16
16
  "engines": {
17
17
  "node": ">=22.5.0"