sequoia-cli 0.1.1 → 0.2.0

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 +250 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -67309,6 +67309,11 @@ function parseFrontmatter(content, mapping) {
67309
67309
  frontmatter.ogImage = raw[coverField] || raw.ogImage;
67310
67310
  const tagsField = mapping?.tags || "tags";
67311
67311
  frontmatter.tags = raw[tagsField] || raw.tags;
67312
+ const draftField = mapping?.draft || "draft";
67313
+ const draftValue = raw[draftField] ?? raw.draft;
67314
+ if (draftValue !== undefined) {
67315
+ frontmatter.draft = draftValue === true || draftValue === "true";
67316
+ }
67312
67317
  frontmatter.atUri = raw.atUri;
67313
67318
  return { frontmatter, body };
67314
67319
  }
@@ -67552,6 +67557,16 @@ async function updateDocument(agent, post, atUri, config, coverImage) {
67552
67557
  record
67553
67558
  });
67554
67559
  }
67560
+ function parseAtUri(atUri) {
67561
+ const match2 = atUri.match(/^at:\/\/([^/]+)\/([^/]+)\/(.+)$/);
67562
+ if (!match2)
67563
+ return null;
67564
+ return {
67565
+ did: match2[1],
67566
+ collection: match2[2],
67567
+ rkey: match2[3]
67568
+ };
67569
+ }
67555
67570
  async function listDocuments(agent, publicationUri) {
67556
67571
  const documents = [];
67557
67572
  let cursor;
@@ -67606,6 +67621,129 @@ async function createPublication(agent, options) {
67606
67621
  });
67607
67622
  return response.data.uri;
67608
67623
  }
67624
+ function countGraphemes(str) {
67625
+ if (typeof Intl !== "undefined" && Intl.Segmenter) {
67626
+ const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
67627
+ return [...segmenter.segment(str)].length;
67628
+ }
67629
+ return [...str].length;
67630
+ }
67631
+ function truncateToGraphemes(str, maxGraphemes) {
67632
+ if (typeof Intl !== "undefined" && Intl.Segmenter) {
67633
+ const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
67634
+ const segments = [...segmenter.segment(str)];
67635
+ if (segments.length <= maxGraphemes)
67636
+ return str;
67637
+ return segments.slice(0, maxGraphemes - 3).map((s) => s.segment).join("") + "...";
67638
+ }
67639
+ const chars = [...str];
67640
+ if (chars.length <= maxGraphemes)
67641
+ return str;
67642
+ return chars.slice(0, maxGraphemes - 3).join("") + "...";
67643
+ }
67644
+ async function createBlueskyPost(agent, options) {
67645
+ const { title, description, canonicalUrl, coverImage, publishedAt } = options;
67646
+ const MAX_GRAPHEMES = 300;
67647
+ let postText;
67648
+ const urlPart = `
67649
+
67650
+ ${canonicalUrl}`;
67651
+ const urlGraphemes = countGraphemes(urlPart);
67652
+ if (description) {
67653
+ const fullText = `${title}
67654
+
67655
+ ${description}${urlPart}`;
67656
+ if (countGraphemes(fullText) <= MAX_GRAPHEMES) {
67657
+ postText = fullText;
67658
+ } else {
67659
+ const availableForDesc = MAX_GRAPHEMES - countGraphemes(title) - countGraphemes(`
67660
+
67661
+ `) - urlGraphemes - countGraphemes(`
67662
+
67663
+ `);
67664
+ if (availableForDesc > 10) {
67665
+ const truncatedDesc = truncateToGraphemes(description, availableForDesc);
67666
+ postText = `${title}
67667
+
67668
+ ${truncatedDesc}${urlPart}`;
67669
+ } else {
67670
+ postText = `${title}${urlPart}`;
67671
+ }
67672
+ }
67673
+ } else {
67674
+ postText = `${title}${urlPart}`;
67675
+ }
67676
+ if (countGraphemes(postText) > MAX_GRAPHEMES) {
67677
+ postText = truncateToGraphemes(postText, MAX_GRAPHEMES);
67678
+ }
67679
+ const encoder = new TextEncoder;
67680
+ const urlStartInText = postText.lastIndexOf(canonicalUrl);
67681
+ const beforeUrl = postText.substring(0, urlStartInText);
67682
+ const byteStart = encoder.encode(beforeUrl).length;
67683
+ const byteEnd = byteStart + encoder.encode(canonicalUrl).length;
67684
+ const facets = [
67685
+ {
67686
+ index: {
67687
+ byteStart,
67688
+ byteEnd
67689
+ },
67690
+ features: [
67691
+ {
67692
+ $type: "app.bsky.richtext.facet#link",
67693
+ uri: canonicalUrl
67694
+ }
67695
+ ]
67696
+ }
67697
+ ];
67698
+ const embed = {
67699
+ $type: "app.bsky.embed.external",
67700
+ external: {
67701
+ uri: canonicalUrl,
67702
+ title: title.substring(0, 500),
67703
+ description: (description || "").substring(0, 1000)
67704
+ }
67705
+ };
67706
+ if (coverImage) {
67707
+ embed.external.thumb = coverImage;
67708
+ }
67709
+ const record = {
67710
+ $type: "app.bsky.feed.post",
67711
+ text: postText,
67712
+ facets,
67713
+ embed,
67714
+ createdAt: new Date(publishedAt).toISOString()
67715
+ };
67716
+ const response = await agent.com.atproto.repo.createRecord({
67717
+ repo: agent.session.did,
67718
+ collection: "app.bsky.feed.post",
67719
+ record
67720
+ });
67721
+ return {
67722
+ uri: response.data.uri,
67723
+ cid: response.data.cid
67724
+ };
67725
+ }
67726
+ async function addBskyPostRefToDocument(agent, documentAtUri, bskyPostRef) {
67727
+ const parsed = parseAtUri(documentAtUri);
67728
+ if (!parsed) {
67729
+ throw new Error(`Invalid document URI: ${documentAtUri}`);
67730
+ }
67731
+ const existingRecord = await agent.com.atproto.repo.getRecord({
67732
+ repo: parsed.did,
67733
+ collection: parsed.collection,
67734
+ rkey: parsed.rkey
67735
+ });
67736
+ const updatedRecord = {
67737
+ ...existingRecord.data.value,
67738
+ bskyPostRef
67739
+ };
67740
+ await agent.com.atproto.repo.putRecord({
67741
+ repo: parsed.did,
67742
+ collection: parsed.collection,
67743
+ rkey: parsed.rkey,
67744
+ record: updatedRecord
67745
+ });
67746
+ }
67609
67747
 
67610
67748
  // src/lib/prompts.ts
67611
67749
  function exitOnCancel(value) {
@@ -67818,6 +67956,9 @@ function generateConfigTemplate(options) {
67818
67956
  if (options.ignore && options.ignore.length > 0) {
67819
67957
  config.ignore = options.ignore;
67820
67958
  }
67959
+ if (options.bluesky) {
67960
+ config.bluesky = options.bluesky;
67961
+ }
67821
67962
  return JSON.stringify(config, null, 2);
67822
67963
  }
67823
67964
  async function loadState(configDir) {
@@ -67932,6 +68073,11 @@ var initCommand = import_cmd_ts2.command({
67932
68073
  message: "Field name for tags:",
67933
68074
  defaultValue: "tags",
67934
68075
  placeholder: "tags, categories, keywords, etc."
68076
+ }),
68077
+ draftField: () => Qt({
68078
+ message: "Field name for draft status:",
68079
+ defaultValue: "draft",
68080
+ placeholder: "draft, private, hidden, etc."
67935
68081
  })
67936
68082
  }, { onCancel });
67937
68083
  const fieldMappings = [
@@ -67939,7 +68085,8 @@ var initCommand = import_cmd_ts2.command({
67939
68085
  ["description", frontmatterConfig.descField, "description"],
67940
68086
  ["publishDate", frontmatterConfig.dateField, "publishDate"],
67941
68087
  ["coverImage", frontmatterConfig.coverField, "ogImage"],
67942
- ["tags", frontmatterConfig.tagsField, "tags"]
68088
+ ["tags", frontmatterConfig.tagsField, "tags"],
68089
+ ["draft", frontmatterConfig.draftField, "draft"]
67943
68090
  ];
67944
68091
  const builtMapping = fieldMappings.reduce((acc, [key, value, defaultValue]) => {
67945
68092
  if (value !== defaultValue) {
@@ -68027,6 +68174,35 @@ var initCommand = import_cmd_ts2.command({
68027
68174
  }
68028
68175
  publicationUri = uri;
68029
68176
  }
68177
+ const enableBluesky = await Mt2({
68178
+ message: "Enable automatic Bluesky posting when publishing?",
68179
+ initialValue: false
68180
+ });
68181
+ if (enableBluesky === Symbol.for("cancel")) {
68182
+ onCancel();
68183
+ }
68184
+ let blueskyConfig;
68185
+ if (enableBluesky) {
68186
+ const maxAgeDaysInput = await Qt({
68187
+ message: "Maximum age (in days) for posts to be shared on Bluesky:",
68188
+ defaultValue: "7",
68189
+ placeholder: "7",
68190
+ validate: (value) => {
68191
+ const num = parseInt(value, 10);
68192
+ if (isNaN(num) || num < 1) {
68193
+ return "Please enter a positive number";
68194
+ }
68195
+ }
68196
+ });
68197
+ if (maxAgeDaysInput === Symbol.for("cancel")) {
68198
+ onCancel();
68199
+ }
68200
+ const maxAgeDays = parseInt(maxAgeDaysInput, 10);
68201
+ blueskyConfig = {
68202
+ enabled: true,
68203
+ ...maxAgeDays !== 7 && { maxAgeDays }
68204
+ };
68205
+ }
68030
68206
  const pdsUrl = credentials?.pdsUrl;
68031
68207
  const configContent = generateConfigTemplate({
68032
68208
  siteUrl: siteConfig.siteUrl,
@@ -68037,7 +68213,8 @@ var initCommand = import_cmd_ts2.command({
68037
68213
  pathPrefix: siteConfig.pathPrefix || "/posts",
68038
68214
  publicationUri,
68039
68215
  pdsUrl,
68040
- frontmatter: frontmatterMapping
68216
+ frontmatter: frontmatterMapping,
68217
+ bluesky: blueskyConfig
68041
68218
  });
68042
68219
  const configPath = path6.join(process.cwd(), "sequoia.json");
68043
68220
  await fs5.writeFile(configPath, configContent);
@@ -68269,7 +68446,12 @@ var publishCommand = import_cmd_ts4.command({
68269
68446
  const posts = await scanContentDirectory(contentDir, config.frontmatter, config.ignore);
68270
68447
  s.stop(`Found ${posts.length} posts`);
68271
68448
  const postsToPublish = [];
68449
+ const draftPosts = [];
68272
68450
  for (const post of posts) {
68451
+ if (post.frontmatter.draft) {
68452
+ draftPosts.push(post);
68453
+ continue;
68454
+ }
68273
68455
  const contentHash = await getContentHash(post.rawContent);
68274
68456
  const relativeFilePath = path8.relative(configDir, post.filePath);
68275
68457
  const postState = state.posts[relativeFilePath];
@@ -68293,6 +68475,9 @@ var publishCommand = import_cmd_ts4.command({
68293
68475
  });
68294
68476
  }
68295
68477
  }
68478
+ if (draftPosts.length > 0) {
68479
+ R2.info(`Skipping ${draftPosts.length} draft post${draftPosts.length === 1 ? "" : "s"}`);
68480
+ }
68296
68481
  if (postsToPublish.length === 0) {
68297
68482
  R2.success("All posts are up to date. Nothing to publish.");
68298
68483
  return;
@@ -68300,11 +68485,34 @@ var publishCommand = import_cmd_ts4.command({
68300
68485
  R2.info(`
68301
68486
  ${postsToPublish.length} posts to publish:
68302
68487
  `);
68488
+ const blueskyEnabled = config.bluesky?.enabled ?? false;
68489
+ const maxAgeDays = config.bluesky?.maxAgeDays ?? 7;
68490
+ const cutoffDate = new Date;
68491
+ cutoffDate.setDate(cutoffDate.getDate() - maxAgeDays);
68303
68492
  for (const { post, action, reason } of postsToPublish) {
68304
68493
  const icon = action === "create" ? "+" : "~";
68305
- R2.message(` ${icon} ${post.frontmatter.title} (${reason})`);
68494
+ const relativeFilePath = path8.relative(configDir, post.filePath);
68495
+ const existingBskyPostRef = state.posts[relativeFilePath]?.bskyPostRef;
68496
+ let bskyNote = "";
68497
+ if (blueskyEnabled) {
68498
+ if (existingBskyPostRef) {
68499
+ bskyNote = " [bsky: exists]";
68500
+ } else {
68501
+ const publishDate = new Date(post.frontmatter.publishDate);
68502
+ if (publishDate < cutoffDate) {
68503
+ bskyNote = ` [bsky: skipped, older than ${maxAgeDays} days]`;
68504
+ } else {
68505
+ bskyNote = " [bsky: will post]";
68506
+ }
68507
+ }
68508
+ }
68509
+ R2.message(` ${icon} ${post.frontmatter.title} (${reason})${bskyNote}`);
68306
68510
  }
68307
68511
  if (dryRun) {
68512
+ if (blueskyEnabled) {
68513
+ R2.info(`
68514
+ Bluesky posting: enabled (max age: ${maxAgeDays} days)`);
68515
+ }
68308
68516
  R2.info(`
68309
68517
  Dry run complete. No changes made.`);
68310
68518
  return;
@@ -68322,6 +68530,7 @@ Dry run complete. No changes made.`);
68322
68530
  let publishedCount = 0;
68323
68531
  let updatedCount = 0;
68324
68532
  let errorCount = 0;
68533
+ let bskyPostCount = 0;
68325
68534
  for (const { post, action } of postsToPublish) {
68326
68535
  s.start(`Publishing: ${post.frontmatter.title}`);
68327
68536
  try {
@@ -68340,6 +68549,9 @@ Dry run complete. No changes made.`);
68340
68549
  }
68341
68550
  let atUri;
68342
68551
  let contentForHash;
68552
+ let bskyPostRef;
68553
+ const relativeFilePath = path8.relative(configDir, post.filePath);
68554
+ const existingBskyPostRef = state.posts[relativeFilePath]?.bskyPostRef;
68343
68555
  if (action === "create") {
68344
68556
  atUri = await createDocument(agent, post, config, coverImage);
68345
68557
  s.stop(`Created: ${atUri}`);
@@ -68355,12 +68567,41 @@ Dry run complete. No changes made.`);
68355
68567
  contentForHash = post.rawContent;
68356
68568
  updatedCount++;
68357
68569
  }
68570
+ if (blueskyEnabled) {
68571
+ if (existingBskyPostRef) {
68572
+ R2.info(` Bluesky post already exists, skipping`);
68573
+ bskyPostRef = existingBskyPostRef;
68574
+ } else {
68575
+ const publishDate = new Date(post.frontmatter.publishDate);
68576
+ if (publishDate < cutoffDate) {
68577
+ R2.info(` Post is older than ${maxAgeDays} days, skipping Bluesky post`);
68578
+ } else {
68579
+ try {
68580
+ const pathPrefix = config.pathPrefix || "/posts";
68581
+ const canonicalUrl = `${config.siteUrl}${pathPrefix}/${post.slug}`;
68582
+ bskyPostRef = await createBlueskyPost(agent, {
68583
+ title: post.frontmatter.title,
68584
+ description: post.frontmatter.description,
68585
+ canonicalUrl,
68586
+ coverImage,
68587
+ publishedAt: post.frontmatter.publishDate
68588
+ });
68589
+ await addBskyPostRefToDocument(agent, atUri, bskyPostRef);
68590
+ R2.info(` Created Bluesky post: ${bskyPostRef.uri}`);
68591
+ bskyPostCount++;
68592
+ } catch (bskyError) {
68593
+ const errorMsg = bskyError instanceof Error ? bskyError.message : String(bskyError);
68594
+ R2.warn(` Failed to create Bluesky post: ${errorMsg}`);
68595
+ }
68596
+ }
68597
+ }
68598
+ }
68358
68599
  const contentHash = await getContentHash(contentForHash);
68359
- const relativeFilePath = path8.relative(configDir, post.filePath);
68360
68600
  state.posts[relativeFilePath] = {
68361
68601
  contentHash,
68362
68602
  atUri,
68363
- lastPublished: new Date().toISOString()
68603
+ lastPublished: new Date().toISOString(),
68604
+ bskyPostRef
68364
68605
  };
68365
68606
  } catch (error) {
68366
68607
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -68374,6 +68615,9 @@ Dry run complete. No changes made.`);
68374
68615
  ---`);
68375
68616
  R2.info(`Published: ${publishedCount}`);
68376
68617
  R2.info(`Updated: ${updatedCount}`);
68618
+ if (bskyPostCount > 0) {
68619
+ R2.info(`Bluesky posts: ${bskyPostCount}`);
68620
+ }
68377
68621
  if (errorCount > 0) {
68378
68622
  R2.warn(`Errors: ${errorCount}`);
68379
68623
  }
@@ -68549,7 +68793,7 @@ Publish evergreen content to the ATmosphere
68549
68793
 
68550
68794
  > https://tangled.org/stevedylan.dev/sequoia
68551
68795
  `,
68552
- version: "0.1.1",
68796
+ version: "0.2.0",
68553
68797
  cmds: {
68554
68798
  auth: authCommand,
68555
68799
  init: initCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequoia-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sequoia": "dist/index.js"