chirpie 1.2.5 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +74 -12
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -967,6 +967,58 @@ postsCommand.command("unhide <id>").description("Put a hidden post back in your
967
967
 
968
968
  // src/commands/accounts.ts
969
969
  import { Command as Command6 } from "commander";
970
+
971
+ // src/lib/connect-input.ts
972
+ var BLUESKY_APP_PASSWORD_REQUIRED = "bluesky_app_password_required";
973
+ var BLUESKY_APP_PASSWORD_MESSAGE = "Bluesky needs an app password, not your account password. Create one in Bluesky under Settings, Privacy and Security, App Passwords (https://bsky.app/settings/app-passwords). It looks like xxxx-xxxx-xxxx-xxxx.";
974
+ var APP_PASSWORD = /^[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/;
975
+ function normalizeBlueskyAppPassword(raw) {
976
+ return raw.trim();
977
+ }
978
+ function isBlueskyAppPassword(raw) {
979
+ return APP_PASSWORD.test(normalizeBlueskyAppPassword(raw));
980
+ }
981
+ var MASTODON_INSTANCE_INVALID = "mastodon_instance_invalid";
982
+ var MASTODON_INSTANCE_MESSAGE = "Enter your Mastodon server (mastodon.social), your handle (@you@mastodon.social) or your profile URL (https://mastodon.social/@you).";
983
+ var DOTTED_NAME = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/i;
984
+ var ADDRESS = /^[^@/\s]+@([^@/\s]+)$/;
985
+ var REMOTE_PROFILE_PATH = /^\/@[^@/]+@([^@/]+)(?:\/|$)/;
986
+ function toUrl(candidate) {
987
+ const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(candidate) ? candidate : `https://${candidate}`;
988
+ try {
989
+ return new URL(withScheme);
990
+ } catch {
991
+ return null;
992
+ }
993
+ }
994
+ function fromUrl(url) {
995
+ if (url.protocol !== "https:") return null;
996
+ if (!DOTTED_NAME.test(url.hostname)) return null;
997
+ return { origin: url.origin, host: url.host };
998
+ }
999
+ function parseMastodonInstance(raw) {
1000
+ const trimmed = raw.trim();
1001
+ if (!trimmed || /\s/.test(trimmed)) return null;
1002
+ const bare = trimmed.replace(/^@+/, "");
1003
+ const address = ADDRESS.exec(bare);
1004
+ const url = toUrl(address ? address[1] : bare);
1005
+ if (!url) return null;
1006
+ const remote = REMOTE_PROFILE_PATH.exec(url.pathname);
1007
+ if (!address && remote) {
1008
+ let decoded;
1009
+ try {
1010
+ decoded = decodeURIComponent(remote[1]);
1011
+ } catch {
1012
+ return null;
1013
+ }
1014
+ const home = toUrl(decoded);
1015
+ if (!home) return null;
1016
+ return fromUrl(home);
1017
+ }
1018
+ return fromUrl(url);
1019
+ }
1020
+
1021
+ // src/commands/accounts.ts
970
1022
  async function readSecretFromStdin(prompt) {
971
1023
  const stdin = process.stdin;
972
1024
  process.stderr.write(prompt);
@@ -1158,24 +1210,29 @@ accountsCommand.command("connect-x").description("Connect a new X/Twitter accoun
1158
1210
  process.exit(1);
1159
1211
  }
1160
1212
  });
1161
- accountsCommand.command("connect-bluesky").description("Connect a Bluesky account via app password").requiredOption(
1213
+ accountsCommand.command("connect-bluesky").description(
1214
+ "Connect a Bluesky account with an app password (xxxx-xxxx-xxxx-xxxx), never your account password"
1215
+ ).requiredOption(
1162
1216
  "--handle <handle>",
1163
1217
  "Bluesky handle. The first part alone is enough (user), and user.bsky.social, a handle on your own domain, or the account email also work"
1164
- ).option("--app-password <password>", "App password (omit to enter securely via prompt)").option("--json", "Output raw JSON").action(async (opts, cmd) => {
1218
+ ).option(
1219
+ "--app-password <password>",
1220
+ "Bluesky app password, shaped xxxx-xxxx-xxxx-xxxx. Create one in Bluesky under Settings, Privacy and Security, App Passwords (omit to enter securely via prompt)"
1221
+ ).option("--json", "Output raw JSON").action(async (opts, cmd) => {
1165
1222
  const client = getClient();
1166
1223
  try {
1167
1224
  let appPassword = opts.appPassword;
1168
1225
  if (!appPassword) {
1169
- appPassword = await readSecretFromStdin("App Password: ");
1170
- if (!appPassword) {
1171
- error("App password is required");
1172
- process.exit(1);
1173
- }
1226
+ appPassword = await readSecretFromStdin("App password (xxxx-xxxx-xxxx-xxxx): ");
1227
+ }
1228
+ if (!appPassword || !isBlueskyAppPassword(appPassword)) {
1229
+ error(`${BLUESKY_APP_PASSWORD_REQUIRED}: ${BLUESKY_APP_PASSWORD_MESSAGE}`);
1230
+ process.exit(1);
1174
1231
  }
1175
1232
  const result = await client.connectBlueskyAccount({
1176
1233
  platform: "bluesky",
1177
1234
  identifier: opts.handle,
1178
- app_password: appPassword
1235
+ app_password: normalizeBlueskyAppPassword(appPassword)
1179
1236
  });
1180
1237
  if (wantsJson(opts, cmd)) {
1181
1238
  json(result);
@@ -1183,7 +1240,7 @@ accountsCommand.command("connect-bluesky").description("Connect a Bluesky accoun
1183
1240
  info(`Connected Bluesky account: ${result.username} (${result.platform_user_id})`);
1184
1241
  }
1185
1242
  } catch (err) {
1186
- error(err instanceof Error ? err.message : "Failed to connect Bluesky account");
1243
+ apiError(err, "Failed to connect Bluesky account");
1187
1244
  process.exit(1);
1188
1245
  }
1189
1246
  });
@@ -1232,13 +1289,18 @@ accountsCommand.command("connect-threads").description("Connect a Threads (Meta)
1232
1289
  });
1233
1290
  accountsCommand.command("connect-mastodon").description("Connect a Mastodon account via OAuth").requiredOption(
1234
1291
  "--instance <instance>",
1235
- "Mastodon instance. A bare host is enough (mastodon.social), and https://mastodon.social or an @you@fosstodon.org address also work"
1292
+ "Your Mastodon server, handle or profile URL: mastodon.social, @you@mastodon.social and https://mastodon.social/@you all work. Only the server is kept"
1236
1293
  ).option("--json", "Output raw JSON").action(async (opts, cmd) => {
1294
+ const instance = parseMastodonInstance(String(opts.instance ?? ""));
1295
+ if (!instance) {
1296
+ error(`${MASTODON_INSTANCE_INVALID}: ${MASTODON_INSTANCE_MESSAGE}`);
1297
+ process.exit(1);
1298
+ }
1237
1299
  const client = getClient();
1238
1300
  try {
1239
1301
  const result = await client.connectMastodonAccount({
1240
1302
  platform: "mastodon",
1241
- instance_url: opts.instance
1303
+ instance_url: instance.origin
1242
1304
  });
1243
1305
  if (wantsJson(opts, cmd)) {
1244
1306
  json(result);
@@ -1249,7 +1311,7 @@ accountsCommand.command("connect-mastodon").description("Connect a Mastodon acco
1249
1311
  `);
1250
1312
  }
1251
1313
  } catch (err) {
1252
- error(err instanceof Error ? err.message : "Failed to start Mastodon connection");
1314
+ apiError(err, "Failed to start Mastodon connection");
1253
1315
  process.exit(1);
1254
1316
  }
1255
1317
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chirpie",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "description": "Chirpie CLI: post to social media from the command line",
5
5
  "repository": {
6
6
  "type": "git",
@@ -44,7 +44,7 @@
44
44
  ],
45
45
  "license": "MIT",
46
46
  "dependencies": {
47
- "@chirpie/sdk": "^1.2.5",
47
+ "@chirpie/sdk": "^1.2.6",
48
48
  "commander": "^13"
49
49
  },
50
50
  "devDependencies": {