artyfax 0.3.1 → 0.3.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.
Files changed (2) hide show
  1. package/dist/cli.js +166 -15
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -55,7 +55,8 @@ async function apiFetch(config, path, init2) {
55
55
  if (raw) {
56
56
  try {
57
57
  const parsed = JSON.parse(raw);
58
- if (parsed.error) message = parsed.error;
58
+ if (parsed.message) message = parsed.message;
59
+ else if (parsed.error) message = parsed.error;
59
60
  } catch {
60
61
  const snippet = raw.replace(/\s+/g, " ").trim().slice(0, 200);
61
62
  if (snippet) message = `HTTP ${res.status}: ${snippet}`;
@@ -375,6 +376,11 @@ async function read(config, slugOrId, forceSecure) {
375
376
  } else {
376
377
  process.stdout.write(data.md_source ?? data.content);
377
378
  }
379
+ if (data.content_hash) {
380
+ process.stderr.write(`
381
+ content_hash: ${data.content_hash}
382
+ `);
383
+ }
378
384
  }
379
385
 
380
386
  // src/commands/secure.ts
@@ -399,7 +405,7 @@ async function secure(config, slugOrId) {
399
405
  spin.text = "Uploading\u2026";
400
406
  await apiFetch(config, `/documents/${doc.id}/content`, {
401
407
  method: "PATCH",
402
- body: JSON.stringify({ content: encrypted })
408
+ body: JSON.stringify({ content: encrypted, force: true })
403
409
  });
404
410
  await apiFetch(config, `/documents/${doc.id}`, {
405
411
  method: "PATCH",
@@ -427,7 +433,7 @@ async function unsecure(config, slugOrId) {
427
433
  spin.text = "Uploading\u2026";
428
434
  await apiFetch(config, `/documents/${doc.id}/content`, {
429
435
  method: "PATCH",
430
- body: JSON.stringify({ content: plaintext })
436
+ body: JSON.stringify({ content: plaintext, force: true })
431
437
  });
432
438
  await apiFetch(config, `/documents/${doc.id}`, {
433
439
  method: "PATCH",
@@ -595,7 +601,7 @@ async function open(config, slugOrId) {
595
601
  // src/commands/update.ts
596
602
  import { readFileSync } from "fs";
597
603
  import { resolve } from "path";
598
- async function update(config, slugOrId, file, json) {
604
+ async function update(config, slugOrId, file, json, force = false) {
599
605
  const spin = spinner("Resolving document\u2026");
600
606
  spin.start();
601
607
  const doc = await resolveSlug(config, slugOrId);
@@ -624,11 +630,35 @@ async function update(config, slugOrId, file, json) {
624
630
  const sdk = await getSDK(config);
625
631
  content = await encryptSecure(content, sdk);
626
632
  }
633
+ let ifMatch = null;
634
+ if (!force) {
635
+ spin.text = "Checking current version\u2026";
636
+ const cur = await apiFetch(config, `/documents/${doc.id}/content`);
637
+ ifMatch = cur.content_hash ?? null;
638
+ }
627
639
  spin.text = "Uploading\u2026";
628
- await apiFetch(config, `/documents/${doc.id}/content`, {
629
- method: "PATCH",
630
- body: JSON.stringify({ content, source: "cli" })
631
- });
640
+ try {
641
+ await apiFetch(config, `/documents/${doc.id}/content`, {
642
+ method: "PATCH",
643
+ body: JSON.stringify(
644
+ force ? { content, source: "cli", force: true } : { content, source: "cli", if_match: ifMatch ?? void 0 }
645
+ )
646
+ });
647
+ } catch (e) {
648
+ spin.stop();
649
+ const status = e.status;
650
+ if (status === 409) {
651
+ throw new Error(
652
+ "This document changed since you read it. Re-pull it with `artyfax read` and re-apply your edit, or pass --force to overwrite."
653
+ );
654
+ }
655
+ if (status === 428) {
656
+ throw new Error(
657
+ "Could not read the current version to guard this write. Re-run, or pass --force to overwrite."
658
+ );
659
+ }
660
+ throw e;
661
+ }
632
662
  spin.stop();
633
663
  if (json) {
634
664
  console.log(JSON.stringify({ updated: true, slug: doc.slug, id: doc.id, secure: doc.secure === 1 }));
@@ -756,6 +786,106 @@ async function addImage(config, slugOrId, file, opts) {
756
786
  }
757
787
  }
758
788
 
789
+ // ../shared/src/limits.ts
790
+ var SERVER_SIDE_BYTE_CAP = 10 * 1024 * 1024;
791
+ var CLIENT_UPLOAD_BYTE_CAP_BY_TIER = {
792
+ free: 25 * 1024 * 1024,
793
+ personal: 50 * 1024 * 1024,
794
+ pro: 100 * 1024 * 1024
795
+ };
796
+ var TOTAL_STORAGE_BYTES_BY_TIER = {
797
+ free: 100 * 1024 * 1024,
798
+ personal: 10 * 1024 * 1024 * 1024,
799
+ pro: 50 * 1024 * 1024 * 1024
800
+ };
801
+
802
+ // ../shared/src/unsplash.ts
803
+ function sanitizeMarkdownLabel(text) {
804
+ if (!text) return "";
805
+ return text.replace(/[[\]]/g, "").replace(/\s+/g, " ").trim();
806
+ }
807
+
808
+ // src/commands/unsplash.ts
809
+ async function search2(config, query, page) {
810
+ const qs = new URLSearchParams({ q: query, page: String(page) });
811
+ return apiFetch(config, `/unsplash/search?${qs.toString()}`);
812
+ }
813
+ async function unsplashSearch(config, query, opts) {
814
+ const spin = spinner("Searching Unsplash\u2026");
815
+ spin.start();
816
+ const res = await search2(config, query, opts.page ?? 1);
817
+ spin.stop();
818
+ if (opts.json) {
819
+ console.log(JSON.stringify(res));
820
+ return;
821
+ }
822
+ if (res.results.length === 0) {
823
+ console.log(c.muted(`No Unsplash photos found for "${query}".`));
824
+ return;
825
+ }
826
+ res.results.forEach((r, i) => {
827
+ const n = c.bright(String(i + 1).padStart(2));
828
+ const desc = r.description ? c.bright(r.description) : c.muted("(no description)");
829
+ console.log(`${n}. ${desc}`);
830
+ console.log(` ${c.muted(`${r.photographer.name} \xB7 ${r.width}\xD7${r.height} \xB7 ${r.photo_id}`)}`);
831
+ });
832
+ console.log(
833
+ c.muted(
834
+ `
835
+ ${res.total} results. Use: artyfax cover <doc> --photo <id> | artyfax image <doc> --photo <id>`
836
+ )
837
+ );
838
+ }
839
+ async function resolvePhotoId(config, opts) {
840
+ if (opts.photo) return opts.photo;
841
+ if (!opts.unsplash) throw new Error('Provide --photo <id> or --unsplash "<query>"');
842
+ const res = await search2(config, opts.unsplash, 1);
843
+ if (res.results.length === 0) throw new Error(`No Unsplash photos found for "${opts.unsplash}"`);
844
+ return res.results[0].photo_id;
845
+ }
846
+ async function coverFromUnsplash(config, slugOrId, opts) {
847
+ const spin = spinner("Resolving document\u2026");
848
+ spin.start();
849
+ const doc = await resolveSlug(config, slugOrId);
850
+ spin.text = "Finding photo\u2026";
851
+ const photoId = await resolvePhotoId(config, opts);
852
+ spin.text = "Importing cover from Unsplash\u2026";
853
+ const result = await apiFetch(
854
+ config,
855
+ `/documents/${doc.id}/cover/from-unsplash`,
856
+ { method: "POST", body: JSON.stringify({ photo_id: photoId }) }
857
+ );
858
+ spin.stop();
859
+ if (opts.json) {
860
+ console.log(JSON.stringify({ ok: true, id: doc.id, slug: doc.slug, cover_image: result.path }));
861
+ } else {
862
+ console.log(`${c.teal("Cover set")} on ${c.bright(doc.slug)} ${c.muted(`(Unsplash ${photoId})`)}`);
863
+ }
864
+ }
865
+ async function imageFromUnsplash(config, slugOrId, opts) {
866
+ const spin = spinner("Resolving document\u2026");
867
+ spin.start();
868
+ const doc = await resolveSlug(config, slugOrId);
869
+ spin.text = "Finding photo\u2026";
870
+ const photoId = await resolvePhotoId(config, opts);
871
+ spin.text = "Importing image from Unsplash\u2026";
872
+ const result = await apiFetch(
873
+ config,
874
+ `/documents/${doc.id}/images/from-unsplash`,
875
+ { method: "POST", body: JSON.stringify({ photo_id: photoId }) }
876
+ );
877
+ spin.stop();
878
+ const markdown = `![${sanitizeMarkdownLabel(result.alt)}](${result.path})
879
+ : ${result.credit_markdown}`;
880
+ if (opts.json) {
881
+ console.log(JSON.stringify({ ok: true, id: doc.id, slug: doc.slug, path: result.path, markdown }));
882
+ } else {
883
+ console.log(`${c.teal("Imported")} to ${c.bright(doc.slug)} ${c.muted(`(Unsplash ${photoId})`)}`);
884
+ console.log(c.muted(" Paste this into the document body:"));
885
+ console.log(` ${markdown.replace("\n", "\n ")}`);
886
+ }
887
+ }
888
+
759
889
  // src/commands/share.ts
760
890
  async function shareCreate(config, slugOrId, json) {
761
891
  const spin = spinner("Resolving document\u2026");
@@ -2145,7 +2275,7 @@ async function pagesMove(config, childRef, position, targetRef, json) {
2145
2275
  }
2146
2276
 
2147
2277
  // src/cli.ts
2148
- var VERSION = true ? "0.3.1" : "0.0.0-dev";
2278
+ var VERSION = true ? "0.3.2" : "0.0.0-dev";
2149
2279
  function brandedHelp() {
2150
2280
  return `
2151
2281
  ${c.amber("artyfax")} ${c.muted(`v${VERSION}`)} \u2014 your personal document library
@@ -2502,6 +2632,7 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
2502
2632
  "metadata",
2503
2633
  "cover",
2504
2634
  "image",
2635
+ "unsplash",
2505
2636
  "publish",
2506
2637
  "unpublish",
2507
2638
  "doctor",
@@ -2696,8 +2827,8 @@ async function main() {
2696
2827
  }
2697
2828
  case "update": {
2698
2829
  const slug = positional[0];
2699
- if (!slug) error("Missing slug argument", "Usage: artyfax update <slug> [file] (reads from stdin if no file)");
2700
- await update(config, slug, positional[1], json);
2830
+ if (!slug) error("Missing slug argument", "Usage: artyfax update <slug> [file] [--force] (reads from stdin if no file)");
2831
+ await update(config, slug, positional[1], json, !!flags.force);
2701
2832
  break;
2702
2833
  }
2703
2834
  case "metadata": {
@@ -2718,15 +2849,35 @@ async function main() {
2718
2849
  case "cover": {
2719
2850
  const slug = positional[0];
2720
2851
  const file = positional[1];
2721
- if (!slug || !file) error("Missing arguments", 'Usage: artyfax cover <doc> <image-file> [--alt "..."]');
2722
- await setCover(config, slug, file, { alt: flags.alt, json });
2852
+ const unsplash = flags.unsplash;
2853
+ const photo = flags.photo;
2854
+ if (!slug) error("Missing arguments", 'Usage: artyfax cover <doc> <image-file> | cover <doc> --unsplash "<query>" | --photo <id>');
2855
+ if (unsplash || photo) {
2856
+ await coverFromUnsplash(config, slug, { unsplash, photo, json });
2857
+ } else {
2858
+ if (!file) error("Missing image", 'Provide an image file, or --unsplash "<query>", or --photo <id>');
2859
+ await setCover(config, slug, file, { alt: flags.alt, json });
2860
+ }
2723
2861
  break;
2724
2862
  }
2725
2863
  case "image": {
2726
2864
  const slug = positional[0];
2727
2865
  const file = positional[1];
2728
- if (!slug || !file) error("Missing arguments", 'Usage: artyfax image <doc> <image-file> [--alt "..."]');
2729
- await addImage(config, slug, file, { alt: flags.alt, json });
2866
+ const unsplash = flags.unsplash;
2867
+ const photo = flags.photo;
2868
+ if (!slug) error("Missing arguments", 'Usage: artyfax image <doc> <image-file> | image <doc> --unsplash "<query>" | --photo <id>');
2869
+ if (unsplash || photo) {
2870
+ await imageFromUnsplash(config, slug, { unsplash, photo, json });
2871
+ } else {
2872
+ if (!file) error("Missing image", 'Provide an image file, or --unsplash "<query>", or --photo <id>');
2873
+ await addImage(config, slug, file, { alt: flags.alt, json });
2874
+ }
2875
+ break;
2876
+ }
2877
+ case "unsplash": {
2878
+ const query = positional.join(" ").trim();
2879
+ if (!query) error("Missing query", "Usage: artyfax unsplash <query>");
2880
+ await unsplashSearch(config, query, { json, page: flags.page ? Number(flags.page) : void 0 });
2730
2881
  break;
2731
2882
  }
2732
2883
  case "share": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "artyfax",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "CLI for Artyfax - your personal document library. Save, theme, search, and share.",
5
5
  "type": "module",
6
6
  "bin": {