sequoia-cli 0.1.2-beta.0 → 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 +295 -160
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -67266,18 +67266,10 @@ function parseFrontmatter(content, mapping) {
67266
67266
  const raw = {};
67267
67267
  const lines = frontmatterStr.split(`
67268
67268
  `);
67269
- let i = 0;
67270
- while (i < lines.length) {
67271
- const line = lines[i];
67272
- if (line === undefined) {
67273
- i++;
67274
- continue;
67275
- }
67269
+ for (const line of lines) {
67276
67270
  const sepIndex = line.indexOf(separator);
67277
- if (sepIndex === -1) {
67278
- i++;
67271
+ if (sepIndex === -1)
67279
67272
  continue;
67280
- }
67281
67273
  const key = line.slice(0, sepIndex).trim();
67282
67274
  let value = line.slice(sepIndex + 1).trim();
67283
67275
  if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
@@ -67286,36 +67278,6 @@ function parseFrontmatter(content, mapping) {
67286
67278
  if (value.startsWith("[") && value.endsWith("]")) {
67287
67279
  const arrayContent = value.slice(1, -1);
67288
67280
  raw[key] = arrayContent.split(",").map((item) => item.trim().replace(/^["']|["']$/g, ""));
67289
- } else if (value === "" && !isToml) {
67290
- const arrayItems = [];
67291
- let j2 = i + 1;
67292
- while (j2 < lines.length) {
67293
- const nextLine = lines[j2];
67294
- if (nextLine === undefined) {
67295
- j2++;
67296
- continue;
67297
- }
67298
- const listMatch = nextLine.match(/^\s+-\s*(.*)$/);
67299
- if (listMatch && listMatch[1] !== undefined) {
67300
- let itemValue = listMatch[1].trim();
67301
- if (itemValue.startsWith('"') && itemValue.endsWith('"') || itemValue.startsWith("'") && itemValue.endsWith("'")) {
67302
- itemValue = itemValue.slice(1, -1);
67303
- }
67304
- arrayItems.push(itemValue);
67305
- j2++;
67306
- } else if (nextLine.trim() === "") {
67307
- j2++;
67308
- } else {
67309
- break;
67310
- }
67311
- }
67312
- if (arrayItems.length > 0) {
67313
- raw[key] = arrayItems;
67314
- i = j2;
67315
- continue;
67316
- } else {
67317
- raw[key] = value;
67318
- }
67319
67281
  } else if (value === "true") {
67320
67282
  raw[key] = true;
67321
67283
  } else if (value === "false") {
@@ -67323,7 +67285,6 @@ function parseFrontmatter(content, mapping) {
67323
67285
  } else {
67324
67286
  raw[key] = value;
67325
67287
  }
67326
- i++;
67327
67288
  }
67328
67289
  const frontmatter = {};
67329
67290
  const titleField = mapping?.title || "title";
@@ -67348,37 +67309,17 @@ function parseFrontmatter(content, mapping) {
67348
67309
  frontmatter.ogImage = raw[coverField] || raw.ogImage;
67349
67310
  const tagsField = mapping?.tags || "tags";
67350
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
+ }
67351
67317
  frontmatter.atUri = raw.atUri;
67352
- return { frontmatter, body, rawFrontmatter: raw };
67318
+ return { frontmatter, body };
67353
67319
  }
67354
67320
  function getSlugFromFilename(filename) {
67355
67321
  return filename.replace(/\.mdx?$/, "").toLowerCase().replace(/\s+/g, "-");
67356
67322
  }
67357
- function getSlugFromOptions(relativePath, rawFrontmatter, options = {}) {
67358
- const { slugSource = "filename", slugField = "slug", removeIndexFromSlug = false } = options;
67359
- let slug;
67360
- switch (slugSource) {
67361
- case "path":
67362
- slug = relativePath.replace(/\.mdx?$/, "").toLowerCase().replace(/\s+/g, "-");
67363
- break;
67364
- case "frontmatter":
67365
- const frontmatterValue = rawFrontmatter[slugField] || rawFrontmatter.slug || rawFrontmatter.url;
67366
- if (frontmatterValue && typeof frontmatterValue === "string") {
67367
- slug = frontmatterValue.replace(/^\//, "").toLowerCase().replace(/\s+/g, "-");
67368
- } else {
67369
- slug = getSlugFromFilename(path3.basename(relativePath));
67370
- }
67371
- break;
67372
- case "filename":
67373
- default:
67374
- slug = getSlugFromFilename(path3.basename(relativePath));
67375
- break;
67376
- }
67377
- if (removeIndexFromSlug) {
67378
- slug = slug.replace(/\/_?index$/, "");
67379
- }
67380
- return slug;
67381
- }
67382
67323
  async function getContentHash(content) {
67383
67324
  const encoder = new TextEncoder;
67384
67325
  const data = encoder.encode(content);
@@ -67394,23 +67335,7 @@ function shouldIgnore(relativePath, ignorePatterns) {
67394
67335
  }
67395
67336
  return false;
67396
67337
  }
67397
- async function scanContentDirectory(contentDir, frontmatterMappingOrOptions, ignorePatterns = []) {
67398
- let options;
67399
- if (frontmatterMappingOrOptions && (("slugSource" in frontmatterMappingOrOptions) || ("frontmatterMapping" in frontmatterMappingOrOptions) || ("ignorePatterns" in frontmatterMappingOrOptions))) {
67400
- options = frontmatterMappingOrOptions;
67401
- } else {
67402
- options = {
67403
- frontmatterMapping: frontmatterMappingOrOptions,
67404
- ignorePatterns
67405
- };
67406
- }
67407
- const {
67408
- frontmatterMapping,
67409
- ignorePatterns: ignore = [],
67410
- slugSource,
67411
- slugField,
67412
- removeIndexFromSlug
67413
- } = options;
67338
+ async function scanContentDirectory(contentDir, frontmatterMapping, ignorePatterns = []) {
67414
67339
  const patterns = ["**/*.md", "**/*.mdx"];
67415
67340
  const posts = [];
67416
67341
  for (const pattern of patterns) {
@@ -67419,25 +67344,21 @@ async function scanContentDirectory(contentDir, frontmatterMappingOrOptions, ign
67419
67344
  absolute: false
67420
67345
  });
67421
67346
  for (const relativePath of files) {
67422
- if (shouldIgnore(relativePath, ignore)) {
67347
+ if (shouldIgnore(relativePath, ignorePatterns)) {
67423
67348
  continue;
67424
67349
  }
67425
67350
  const filePath = path3.join(contentDir, relativePath);
67426
67351
  const rawContent = await fs2.readFile(filePath, "utf-8");
67427
67352
  try {
67428
- const { frontmatter, body, rawFrontmatter } = parseFrontmatter(rawContent, frontmatterMapping);
67429
- const slug = getSlugFromOptions(relativePath, rawFrontmatter, {
67430
- slugSource,
67431
- slugField,
67432
- removeIndexFromSlug
67433
- });
67353
+ const { frontmatter, body } = parseFrontmatter(rawContent, frontmatterMapping);
67354
+ const filename = path3.basename(relativePath);
67355
+ const slug = getSlugFromFilename(filename);
67434
67356
  posts.push({
67435
67357
  filePath,
67436
67358
  slug,
67437
67359
  frontmatter,
67438
67360
  content: body,
67439
- rawContent,
67440
- rawFrontmatter
67361
+ rawContent
67441
67362
  });
67442
67363
  } catch (error) {
67443
67364
  console.error(`Error parsing ${relativePath}:`, error);
@@ -67580,13 +67501,8 @@ async function resolveImagePath(ogImage, imagesDir, contentDir) {
67580
67501
  async function createDocument(agent, post, config, coverImage) {
67581
67502
  const pathPrefix = config.pathPrefix || "/posts";
67582
67503
  const postPath = `${pathPrefix}/${post.slug}`;
67504
+ const textContent = stripMarkdownForText(post.content);
67583
67505
  const publishDate = new Date(post.frontmatter.publishDate);
67584
- let textContent;
67585
- if (config.textContentField && post.rawFrontmatter?.[config.textContentField]) {
67586
- textContent = String(post.rawFrontmatter[config.textContentField]);
67587
- } else {
67588
- textContent = stripMarkdownForText(post.content);
67589
- }
67590
67506
  const record = {
67591
67507
  $type: "site.standard.document",
67592
67508
  title: post.frontmatter.title,
@@ -67596,9 +67512,6 @@ async function createDocument(agent, post, config, coverImage) {
67596
67512
  publishedAt: publishDate.toISOString(),
67597
67513
  canonicalUrl: `${config.siteUrl}${postPath}`
67598
67514
  };
67599
- if (post.frontmatter.description) {
67600
- record.description = post.frontmatter.description;
67601
- }
67602
67515
  if (coverImage) {
67603
67516
  record.coverImage = coverImage;
67604
67517
  }
@@ -67620,13 +67533,8 @@ async function updateDocument(agent, post, atUri, config, coverImage) {
67620
67533
  const [, , collection, rkey] = uriMatch;
67621
67534
  const pathPrefix = config.pathPrefix || "/posts";
67622
67535
  const postPath = `${pathPrefix}/${post.slug}`;
67536
+ const textContent = stripMarkdownForText(post.content);
67623
67537
  const publishDate = new Date(post.frontmatter.publishDate);
67624
- let textContent;
67625
- if (config.textContentField && post.rawFrontmatter?.[config.textContentField]) {
67626
- textContent = String(post.rawFrontmatter[config.textContentField]);
67627
- } else {
67628
- textContent = stripMarkdownForText(post.content);
67629
- }
67630
67538
  const record = {
67631
67539
  $type: "site.standard.document",
67632
67540
  title: post.frontmatter.title,
@@ -67636,9 +67544,6 @@ async function updateDocument(agent, post, atUri, config, coverImage) {
67636
67544
  publishedAt: publishDate.toISOString(),
67637
67545
  canonicalUrl: `${config.siteUrl}${postPath}`
67638
67546
  };
67639
- if (post.frontmatter.description) {
67640
- record.description = post.frontmatter.description;
67641
- }
67642
67547
  if (coverImage) {
67643
67548
  record.coverImage = coverImage;
67644
67549
  }
@@ -67652,6 +67557,16 @@ async function updateDocument(agent, post, atUri, config, coverImage) {
67652
67557
  record
67653
67558
  });
67654
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
+ }
67655
67570
  async function listDocuments(agent, publicationUri) {
67656
67571
  const documents = [];
67657
67572
  let cursor;
@@ -67706,6 +67621,129 @@ async function createPublication(agent, options) {
67706
67621
  });
67707
67622
  return response.data.uri;
67708
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
+ }
67709
67747
 
67710
67748
  // src/lib/prompts.ts
67711
67749
  function exitOnCancel(value) {
@@ -67918,17 +67956,8 @@ function generateConfigTemplate(options) {
67918
67956
  if (options.ignore && options.ignore.length > 0) {
67919
67957
  config.ignore = options.ignore;
67920
67958
  }
67921
- if (options.slugSource && options.slugSource !== "filename") {
67922
- config.slugSource = options.slugSource;
67923
- }
67924
- if (options.slugField && options.slugField !== "slug") {
67925
- config.slugField = options.slugField;
67926
- }
67927
- if (options.removeIndexFromSlug) {
67928
- config.removeIndexFromSlug = options.removeIndexFromSlug;
67929
- }
67930
- if (options.textContentField) {
67931
- config.textContentField = options.textContentField;
67959
+ if (options.bluesky) {
67960
+ config.bluesky = options.bluesky;
67932
67961
  }
67933
67962
  return JSON.stringify(config, null, 2);
67934
67963
  }
@@ -68044,6 +68073,11 @@ var initCommand = import_cmd_ts2.command({
68044
68073
  message: "Field name for tags:",
68045
68074
  defaultValue: "tags",
68046
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."
68047
68081
  })
68048
68082
  }, { onCancel });
68049
68083
  const fieldMappings = [
@@ -68051,7 +68085,8 @@ var initCommand = import_cmd_ts2.command({
68051
68085
  ["description", frontmatterConfig.descField, "description"],
68052
68086
  ["publishDate", frontmatterConfig.dateField, "publishDate"],
68053
68087
  ["coverImage", frontmatterConfig.coverField, "ogImage"],
68054
- ["tags", frontmatterConfig.tagsField, "tags"]
68088
+ ["tags", frontmatterConfig.tagsField, "tags"],
68089
+ ["draft", frontmatterConfig.draftField, "draft"]
68055
68090
  ];
68056
68091
  const builtMapping = fieldMappings.reduce((acc, [key, value, defaultValue]) => {
68057
68092
  if (value !== defaultValue) {
@@ -68139,6 +68174,35 @@ var initCommand = import_cmd_ts2.command({
68139
68174
  }
68140
68175
  publicationUri = uri;
68141
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
+ }
68142
68206
  const pdsUrl = credentials?.pdsUrl;
68143
68207
  const configContent = generateConfigTemplate({
68144
68208
  siteUrl: siteConfig.siteUrl,
@@ -68149,7 +68213,8 @@ var initCommand = import_cmd_ts2.command({
68149
68213
  pathPrefix: siteConfig.pathPrefix || "/posts",
68150
68214
  publicationUri,
68151
68215
  pdsUrl,
68152
- frontmatter: frontmatterMapping
68216
+ frontmatter: frontmatterMapping,
68217
+ bluesky: blueskyConfig
68153
68218
  });
68154
68219
  const configPath = path6.join(process.cwd(), "sequoia.json");
68155
68220
  await fs5.writeFile(configPath, configContent);
@@ -68216,24 +68281,38 @@ var injectCommand = import_cmd_ts3.command({
68216
68281
  const resolvedOutputDir = path7.isAbsolute(outputDir) ? outputDir : path7.join(configDir, outputDir);
68217
68282
  R2.info(`Scanning for HTML files in: ${resolvedOutputDir}`);
68218
68283
  const state = await loadState(configDir);
68219
- const slugToAtUri = new Map;
68284
+ const genericFilenames = new Set([
68285
+ "+page",
68286
+ "index",
68287
+ "_index",
68288
+ "page",
68289
+ "readme"
68290
+ ]);
68291
+ const pathToAtUri = new Map;
68220
68292
  for (const [filePath, postState] of Object.entries(state.posts)) {
68221
- if (postState.atUri && postState.slug) {
68222
- slugToAtUri.set(postState.slug, postState.atUri);
68223
- const lastSegment = postState.slug.split("/").pop();
68224
- if (lastSegment && lastSegment !== postState.slug) {
68225
- slugToAtUri.set(lastSegment, postState.atUri);
68293
+ if (postState.atUri) {
68294
+ let basename4 = path7.basename(filePath, path7.extname(filePath));
68295
+ if (genericFilenames.has(basename4.toLowerCase())) {
68296
+ const pathParts = filePath.split(/[/\\]/).filter((p) => p && !(p.startsWith("(") && p.endsWith(")")));
68297
+ if (pathParts.length >= 2) {
68298
+ const slug = pathParts[pathParts.length - 2];
68299
+ if (slug && slug !== "." && slug !== "content" && slug !== "routes" && slug !== "src") {
68300
+ basename4 = slug;
68301
+ }
68302
+ }
68303
+ }
68304
+ pathToAtUri.set(basename4, postState.atUri);
68305
+ const dirName = path7.basename(path7.dirname(filePath));
68306
+ if (dirName !== "." && dirName !== "content" && !(dirName.startsWith("(") && dirName.endsWith(")"))) {
68307
+ pathToAtUri.set(`${dirName}/${basename4}`, postState.atUri);
68226
68308
  }
68227
- } else if (postState.atUri) {
68228
- const basename4 = path7.basename(filePath, path7.extname(filePath));
68229
- slugToAtUri.set(basename4.toLowerCase(), postState.atUri);
68230
68309
  }
68231
68310
  }
68232
- if (slugToAtUri.size === 0) {
68311
+ if (pathToAtUri.size === 0) {
68233
68312
  R2.warn("No published posts found in state. Run 'sequoia publish' first.");
68234
68313
  return;
68235
68314
  }
68236
- R2.info(`Found ${slugToAtUri.size} slug mappings from published posts`);
68315
+ R2.info(`Found ${pathToAtUri.size} published posts in state`);
68237
68316
  const htmlFiles = await glob("**/*.html", {
68238
68317
  cwd: resolvedOutputDir,
68239
68318
  absolute: false
@@ -68252,16 +68331,19 @@ var injectCommand = import_cmd_ts3.command({
68252
68331
  const htmlDir = path7.dirname(relativePath);
68253
68332
  const htmlBasename = path7.basename(relativePath, ".html");
68254
68333
  let atUri;
68255
- atUri = slugToAtUri.get(htmlBasename);
68334
+ atUri = pathToAtUri.get(htmlBasename);
68256
68335
  if (!atUri && htmlBasename === "index" && htmlDir !== ".") {
68257
- atUri = slugToAtUri.get(htmlDir);
68336
+ const slug = path7.basename(htmlDir);
68337
+ atUri = pathToAtUri.get(slug);
68258
68338
  if (!atUri) {
68259
- const lastDir = path7.basename(htmlDir);
68260
- atUri = slugToAtUri.get(lastDir);
68339
+ const parentDir = path7.dirname(htmlDir);
68340
+ if (parentDir !== ".") {
68341
+ atUri = pathToAtUri.get(`${path7.basename(parentDir)}/${slug}`);
68342
+ }
68261
68343
  }
68262
68344
  }
68263
68345
  if (!atUri && htmlDir !== ".") {
68264
- atUri = slugToAtUri.get(`${htmlDir}/${htmlBasename}`);
68346
+ atUri = pathToAtUri.get(`${htmlDir}/${htmlBasename}`);
68265
68347
  }
68266
68348
  if (!atUri) {
68267
68349
  skippedCount++;
@@ -68361,16 +68443,15 @@ var publishCommand = import_cmd_ts4.command({
68361
68443
  const state = await loadState(configDir);
68362
68444
  const s = Ie();
68363
68445
  s.start("Scanning for posts...");
68364
- const posts = await scanContentDirectory(contentDir, {
68365
- frontmatterMapping: config.frontmatter,
68366
- ignorePatterns: config.ignore,
68367
- slugSource: config.slugSource,
68368
- slugField: config.slugField,
68369
- removeIndexFromSlug: config.removeIndexFromSlug
68370
- });
68446
+ const posts = await scanContentDirectory(contentDir, config.frontmatter, config.ignore);
68371
68447
  s.stop(`Found ${posts.length} posts`);
68372
68448
  const postsToPublish = [];
68449
+ const draftPosts = [];
68373
68450
  for (const post of posts) {
68451
+ if (post.frontmatter.draft) {
68452
+ draftPosts.push(post);
68453
+ continue;
68454
+ }
68374
68455
  const contentHash = await getContentHash(post.rawContent);
68375
68456
  const relativeFilePath = path8.relative(configDir, post.filePath);
68376
68457
  const postState = state.posts[relativeFilePath];
@@ -68394,6 +68475,9 @@ var publishCommand = import_cmd_ts4.command({
68394
68475
  });
68395
68476
  }
68396
68477
  }
68478
+ if (draftPosts.length > 0) {
68479
+ R2.info(`Skipping ${draftPosts.length} draft post${draftPosts.length === 1 ? "" : "s"}`);
68480
+ }
68397
68481
  if (postsToPublish.length === 0) {
68398
68482
  R2.success("All posts are up to date. Nothing to publish.");
68399
68483
  return;
@@ -68401,11 +68485,34 @@ var publishCommand = import_cmd_ts4.command({
68401
68485
  R2.info(`
68402
68486
  ${postsToPublish.length} posts to publish:
68403
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);
68404
68492
  for (const { post, action, reason } of postsToPublish) {
68405
68493
  const icon = action === "create" ? "+" : "~";
68406
- 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}`);
68407
68510
  }
68408
68511
  if (dryRun) {
68512
+ if (blueskyEnabled) {
68513
+ R2.info(`
68514
+ Bluesky posting: enabled (max age: ${maxAgeDays} days)`);
68515
+ }
68409
68516
  R2.info(`
68410
68517
  Dry run complete. No changes made.`);
68411
68518
  return;
@@ -68423,6 +68530,7 @@ Dry run complete. No changes made.`);
68423
68530
  let publishedCount = 0;
68424
68531
  let updatedCount = 0;
68425
68532
  let errorCount = 0;
68533
+ let bskyPostCount = 0;
68426
68534
  for (const { post, action } of postsToPublish) {
68427
68535
  s.start(`Publishing: ${post.frontmatter.title}`);
68428
68536
  try {
@@ -68441,6 +68549,9 @@ Dry run complete. No changes made.`);
68441
68549
  }
68442
68550
  let atUri;
68443
68551
  let contentForHash;
68552
+ let bskyPostRef;
68553
+ const relativeFilePath = path8.relative(configDir, post.filePath);
68554
+ const existingBskyPostRef = state.posts[relativeFilePath]?.bskyPostRef;
68444
68555
  if (action === "create") {
68445
68556
  atUri = await createDocument(agent, post, config, coverImage);
68446
68557
  s.stop(`Created: ${atUri}`);
@@ -68456,13 +68567,41 @@ Dry run complete. No changes made.`);
68456
68567
  contentForHash = post.rawContent;
68457
68568
  updatedCount++;
68458
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
+ }
68459
68599
  const contentHash = await getContentHash(contentForHash);
68460
- const relativeFilePath = path8.relative(configDir, post.filePath);
68461
68600
  state.posts[relativeFilePath] = {
68462
68601
  contentHash,
68463
68602
  atUri,
68464
68603
  lastPublished: new Date().toISOString(),
68465
- slug: post.slug
68604
+ bskyPostRef
68466
68605
  };
68467
68606
  } catch (error) {
68468
68607
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -68476,6 +68615,9 @@ Dry run complete. No changes made.`);
68476
68615
  ---`);
68477
68616
  R2.info(`Published: ${publishedCount}`);
68478
68617
  R2.info(`Updated: ${updatedCount}`);
68618
+ if (bskyPostCount > 0) {
68619
+ R2.info(`Bluesky posts: ${bskyPostCount}`);
68620
+ }
68479
68621
  if (errorCount > 0) {
68480
68622
  R2.warn(`Errors: ${errorCount}`);
68481
68623
  }
@@ -68549,18 +68691,11 @@ var syncCommand = import_cmd_ts5.command({
68549
68691
  }
68550
68692
  const contentDir = path9.isAbsolute(config.contentDir) ? config.contentDir : path9.join(configDir, config.contentDir);
68551
68693
  s.start("Scanning local content...");
68552
- const localPosts = await scanContentDirectory(contentDir, {
68553
- frontmatterMapping: config.frontmatter,
68554
- ignorePatterns: config.ignore,
68555
- slugSource: config.slugSource,
68556
- slugField: config.slugField,
68557
- removeIndexFromSlug: config.removeIndexFromSlug
68558
- });
68694
+ const localPosts = await scanContentDirectory(contentDir, config.frontmatter);
68559
68695
  s.stop(`Found ${localPosts.length} local posts`);
68560
- const pathPrefix = config.pathPrefix || "/posts";
68561
68696
  const postsByPath = new Map;
68562
68697
  for (const post of localPosts) {
68563
- const postPath = `${pathPrefix}/${post.slug}`;
68698
+ const postPath = `/posts/${post.slug}`;
68564
68699
  postsByPath.set(postPath, post);
68565
68700
  }
68566
68701
  const state = await loadState(configDir);
@@ -68658,7 +68793,7 @@ Publish evergreen content to the ATmosphere
68658
68793
 
68659
68794
  > https://tangled.org/stevedylan.dev/sequoia
68660
68795
  `,
68661
- version: "0.1.2-beta.0",
68796
+ version: "0.2.0",
68662
68797
  cmds: {
68663
68798
  auth: authCommand,
68664
68799
  init: initCommand,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequoia-cli",
3
- "version": "0.1.2-beta.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "sequoia": "dist/index.js"