artyfax 0.3.0 → 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.
- package/dist/cli.js +585 -136
- 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.
|
|
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}`;
|
|
@@ -320,13 +321,6 @@ async function resolveSlug(config, slugOrId) {
|
|
|
320
321
|
} catch {
|
|
321
322
|
}
|
|
322
323
|
}
|
|
323
|
-
if (slugOrId.match(/^[0-9a-f-]{36}$/i)) {
|
|
324
|
-
try {
|
|
325
|
-
const doc = await apiFetch(config, `/documents/${slugOrId}`);
|
|
326
|
-
return doc;
|
|
327
|
-
} catch {
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
324
|
if (/^[A-Z]+-\d+$/.test(slugOrId)) {
|
|
331
325
|
try {
|
|
332
326
|
const { id } = await apiFetch(config, `/resolve/${encodeURIComponent(slugOrId)}`);
|
|
@@ -335,6 +329,13 @@ async function resolveSlug(config, slugOrId) {
|
|
|
335
329
|
} catch {
|
|
336
330
|
}
|
|
337
331
|
}
|
|
332
|
+
if (!slugOrId.includes("/") && !/^[A-Z]+-\d+$/.test(slugOrId)) {
|
|
333
|
+
try {
|
|
334
|
+
return await apiFetch(config, `/documents/${encodeURIComponent(slugOrId)}`);
|
|
335
|
+
} catch (e) {
|
|
336
|
+
if (e.status !== 404) throw e;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
338
339
|
const docs = await apiFetch(
|
|
339
340
|
config,
|
|
340
341
|
`/documents?limit=500`
|
|
@@ -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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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 }));
|
|
@@ -687,17 +717,6 @@ function formatBytes(bytes) {
|
|
|
687
717
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
688
718
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
689
719
|
}
|
|
690
|
-
async function resolveDocArg(config, arg) {
|
|
691
|
-
const looksLikeBareId = !arg.includes("/") && !/^[A-Z]+-\d+$/.test(arg);
|
|
692
|
-
if (looksLikeBareId) {
|
|
693
|
-
try {
|
|
694
|
-
return await apiFetch(config, `/documents/${encodeURIComponent(arg)}`);
|
|
695
|
-
} catch (e) {
|
|
696
|
-
if (e.status !== 404) throw e;
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
return resolveSlug(config, arg);
|
|
700
|
-
}
|
|
701
720
|
function readImageFile(file) {
|
|
702
721
|
const path = resolve2(file);
|
|
703
722
|
let stat;
|
|
@@ -730,7 +749,7 @@ async function setCover(config, slugOrId, file, opts) {
|
|
|
730
749
|
const image = readImageFile(file);
|
|
731
750
|
const spin = spinner("Resolving document\u2026");
|
|
732
751
|
spin.start();
|
|
733
|
-
const doc = await
|
|
752
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
734
753
|
spin.text = `Uploading ${image.name}\u2026`;
|
|
735
754
|
const uploaded = await uploadImage(config, doc.id, image, "cover");
|
|
736
755
|
spin.text = "Setting cover\u2026";
|
|
@@ -752,7 +771,7 @@ async function addImage(config, slugOrId, file, opts) {
|
|
|
752
771
|
const image = readImageFile(file);
|
|
753
772
|
const spin = spinner("Resolving document\u2026");
|
|
754
773
|
spin.start();
|
|
755
|
-
const doc = await
|
|
774
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
756
775
|
spin.text = `Uploading ${image.name}\u2026`;
|
|
757
776
|
const uploaded = await uploadImage(config, doc.id, image, "content");
|
|
758
777
|
spin.stop();
|
|
@@ -767,6 +786,106 @@ async function addImage(config, slugOrId, file, opts) {
|
|
|
767
786
|
}
|
|
768
787
|
}
|
|
769
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 = `
|
|
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
|
+
|
|
770
889
|
// src/commands/share.ts
|
|
771
890
|
async function shareCreate(config, slugOrId, json) {
|
|
772
891
|
const spin = spinner("Resolving document\u2026");
|
|
@@ -782,9 +901,13 @@ async function shareCreate(config, slugOrId, json) {
|
|
|
782
901
|
}
|
|
783
902
|
);
|
|
784
903
|
spin.stop();
|
|
785
|
-
const
|
|
904
|
+
const hash = result.share?.hash;
|
|
905
|
+
if (!hash) {
|
|
906
|
+
throw new Error("Share created but the server response contained no hash.");
|
|
907
|
+
}
|
|
908
|
+
const url = `${config.endpoint}/d/${hash}`;
|
|
786
909
|
if (json) {
|
|
787
|
-
console.log(JSON.stringify({ hash
|
|
910
|
+
console.log(JSON.stringify({ hash, url, slug: doc.slug }));
|
|
788
911
|
} else {
|
|
789
912
|
console.log(`${c.teal("Share created:")} ${url}`);
|
|
790
913
|
}
|
|
@@ -910,6 +1033,10 @@ async function tagDelete(config, tag, json) {
|
|
|
910
1033
|
}
|
|
911
1034
|
|
|
912
1035
|
// src/commands/version.ts
|
|
1036
|
+
function fmtTs(ts) {
|
|
1037
|
+
if (!ts) return "\u2014";
|
|
1038
|
+
return ts.replace("T", " ").replace(/\.\d+Z$/, "");
|
|
1039
|
+
}
|
|
913
1040
|
async function versionList(config, slugOrId, json) {
|
|
914
1041
|
const spin = spinner("Resolving document\u2026");
|
|
915
1042
|
spin.start();
|
|
@@ -921,18 +1048,24 @@ async function versionList(config, slugOrId, json) {
|
|
|
921
1048
|
console.log(JSON.stringify(data.versions));
|
|
922
1049
|
return;
|
|
923
1050
|
}
|
|
924
|
-
|
|
925
|
-
|
|
1051
|
+
const versions = data.versions ?? [];
|
|
1052
|
+
const currentSaved = fmtTs(data.current?.updated_at);
|
|
1053
|
+
if (versions.length === 0) {
|
|
1054
|
+
console.log(`${c.bright(doc.slug)} \u2014 ${c.muted(`no previous versions (current saved ${currentSaved}).`)}`);
|
|
926
1055
|
return;
|
|
927
1056
|
}
|
|
928
|
-
console.log(
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1057
|
+
console.log(
|
|
1058
|
+
`${c.bright(doc.slug)} \u2014 current saved ${c.muted(currentSaved)}, ${versions.length} previous version${versions.length === 1 ? "" : "s"}
|
|
1059
|
+
`
|
|
1060
|
+
);
|
|
1061
|
+
const n = versions.length;
|
|
1062
|
+
const rows = versions.map((v, i) => [
|
|
1063
|
+
c.muted(`v${n - i}`),
|
|
1064
|
+
fmtTs(v.created_at),
|
|
1065
|
+
v.source || c.muted("\u2014"),
|
|
1066
|
+
v.published ? c.teal("\u25CF published") : ""
|
|
934
1067
|
]);
|
|
935
|
-
console.log(table(rows, { header: ["", "
|
|
1068
|
+
console.log(table(rows, { header: ["", "Saved", "Source", ""] }));
|
|
936
1069
|
}
|
|
937
1070
|
async function versionRestore(config, slugOrId, yes, json) {
|
|
938
1071
|
const spin = spinner("Resolving document\u2026");
|
|
@@ -941,25 +1074,26 @@ async function versionRestore(config, slugOrId, yes, json) {
|
|
|
941
1074
|
spin.text = "Loading versions\u2026";
|
|
942
1075
|
const data = await apiFetch(config, `/documents/${doc.id}/versions`);
|
|
943
1076
|
spin.stop();
|
|
944
|
-
|
|
1077
|
+
const versions = data.versions ?? [];
|
|
1078
|
+
if (versions.length === 0) {
|
|
945
1079
|
console.log(c.muted("No previous versions to restore."));
|
|
946
1080
|
return;
|
|
947
1081
|
}
|
|
948
|
-
|
|
949
|
-
console.log(`${c.bright(doc.slug)} \u2014 ${previous.length} previous version${previous.length === 1 ? "" : "s"}:
|
|
1082
|
+
console.log(`${c.bright(doc.slug)} \u2014 ${versions.length} previous version${versions.length === 1 ? "" : "s"}:
|
|
950
1083
|
`);
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const label = `v${
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
console.log(` ${c.amber(label)} ${
|
|
957
|
-
}
|
|
958
|
-
const target = previous[0];
|
|
1084
|
+
const n = versions.length;
|
|
1085
|
+
versions.forEach((v, i) => {
|
|
1086
|
+
const label = `v${n - i}`;
|
|
1087
|
+
const source = v.source ? c.muted(` (${v.source})`) : "";
|
|
1088
|
+
const pin = v.published ? c.teal(" \u25CF published") : "";
|
|
1089
|
+
console.log(` ${c.amber(label)} ${fmtTs(v.created_at)}${source}${pin}`);
|
|
1090
|
+
});
|
|
959
1091
|
console.log();
|
|
1092
|
+
const target = versions[0];
|
|
960
1093
|
if (!yes) {
|
|
961
|
-
const
|
|
962
|
-
|
|
1094
|
+
const confirmed = await confirm(
|
|
1095
|
+
`Restore to v${n} (${fmtTs(target.created_at)})? The current draft is snapshotted first, so this is reversible.`
|
|
1096
|
+
);
|
|
963
1097
|
if (!confirmed) {
|
|
964
1098
|
console.log(c.muted("Cancelled."));
|
|
965
1099
|
return;
|
|
@@ -969,14 +1103,60 @@ async function versionRestore(config, slugOrId, yes, json) {
|
|
|
969
1103
|
restoreSpin.start();
|
|
970
1104
|
await apiFetch(config, `/documents/${doc.id}/versions/restore`, {
|
|
971
1105
|
method: "POST",
|
|
972
|
-
|
|
1106
|
+
// The restore endpoint keys on the numeric version id, not a timestamp.
|
|
1107
|
+
body: JSON.stringify({ version_id: target.id })
|
|
973
1108
|
});
|
|
974
1109
|
restoreSpin.stop();
|
|
975
1110
|
if (json) {
|
|
976
|
-
console.log(JSON.stringify({ restored: true, slug: doc.slug,
|
|
1111
|
+
console.log(JSON.stringify({ restored: true, slug: doc.slug, version_id: target.id, created_at: target.created_at }));
|
|
977
1112
|
} else {
|
|
978
|
-
console.log(`${c.teal("Restored:")} ${doc.slug} to ${target.
|
|
1113
|
+
console.log(`${c.teal("Restored:")} ${doc.slug} to v${n} (${fmtTs(target.created_at)})`);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
// src/commands/publish.ts
|
|
1118
|
+
async function publish(config, slugOrId, opts) {
|
|
1119
|
+
const spin = spinner("Resolving document\u2026");
|
|
1120
|
+
spin.start();
|
|
1121
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
1122
|
+
spin.text = opts.version != null ? `Publishing version ${opts.version}\u2026` : "Publishing\u2026";
|
|
1123
|
+
const body = {};
|
|
1124
|
+
if (opts.version != null) body.version_id = opts.version;
|
|
1125
|
+
const data = await apiFetch(
|
|
1126
|
+
config,
|
|
1127
|
+
`/documents/${doc.id}/publish`,
|
|
1128
|
+
{ method: "POST", body: JSON.stringify(body) }
|
|
1129
|
+
);
|
|
1130
|
+
spin.stop();
|
|
1131
|
+
if (opts.json) {
|
|
1132
|
+
console.log(JSON.stringify({ published: true, slug: doc.slug, ...data }));
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
const versionNote = data.published_version_id != null ? c.muted(` (version ${data.published_version_id})`) : "";
|
|
1136
|
+
console.log(`${c.teal("Published:")} ${doc.slug}${versionNote}`);
|
|
1137
|
+
console.log(c.muted("Public and shared views now serve this version. Edits stay draft until you publish again."));
|
|
1138
|
+
}
|
|
1139
|
+
async function unpublish(config, slugOrId, json) {
|
|
1140
|
+
const spin = spinner("Resolving document\u2026");
|
|
1141
|
+
spin.start();
|
|
1142
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
1143
|
+
spin.text = "Unpublishing\u2026";
|
|
1144
|
+
const data = await apiFetch(
|
|
1145
|
+
config,
|
|
1146
|
+
`/documents/${doc.id}/unpublish`,
|
|
1147
|
+
{ method: "POST" }
|
|
1148
|
+
);
|
|
1149
|
+
spin.stop();
|
|
1150
|
+
if (json) {
|
|
1151
|
+
console.log(JSON.stringify({ unpublished: true, slug: doc.slug, ...data }));
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
if (data.was_published === false) {
|
|
1155
|
+
console.log(c.muted(`${doc.slug} was not published - nothing to do.`));
|
|
1156
|
+
return;
|
|
979
1157
|
}
|
|
1158
|
+
console.log(`${c.teal("Unpublished:")} ${doc.slug}`);
|
|
1159
|
+
console.log(c.muted("Public and shared views now serve the live document again."));
|
|
980
1160
|
}
|
|
981
1161
|
|
|
982
1162
|
// src/commands/annotation.ts
|
|
@@ -1511,8 +1691,32 @@ async function workSetStatus(config, docId, status, json) {
|
|
|
1511
1691
|
}
|
|
1512
1692
|
console.log(c.bright(`${data.work_id || docId} \u2192 ${demote ? "removed from board" : statusLabel(status)}`));
|
|
1513
1693
|
}
|
|
1514
|
-
function
|
|
1515
|
-
|
|
1694
|
+
async function workTouch(config, ids, flags, json) {
|
|
1695
|
+
if (ids.length === 0) {
|
|
1696
|
+
console.error(c.rose("Error: pass one or more work IDs or doc IDs to keep live."));
|
|
1697
|
+
process.exitCode = 1;
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const action = typeof flags.action === "string" ? flags.action : void 0;
|
|
1701
|
+
const agent = typeof flags.agent === "string" ? flags.agent : void 0;
|
|
1702
|
+
const spin = spinner("Sending heartbeat\u2026");
|
|
1703
|
+
spin.start();
|
|
1704
|
+
const data = await apiFetch(config, "/projects/work/touch", {
|
|
1705
|
+
method: "POST",
|
|
1706
|
+
headers: { "Content-Type": "application/json" },
|
|
1707
|
+
body: JSON.stringify({ ids, action, agent })
|
|
1708
|
+
});
|
|
1709
|
+
spin.stop();
|
|
1710
|
+
if (json) {
|
|
1711
|
+
console.log(JSON.stringify(data));
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
const warmed = data.warmed ?? [];
|
|
1715
|
+
if (warmed.length === 0) {
|
|
1716
|
+
console.log(c.muted("No in-progress items matched - nothing warmed."));
|
|
1717
|
+
return;
|
|
1718
|
+
}
|
|
1719
|
+
console.log(c.bright(`Kept live: ${warmed.length} item${warmed.length === 1 ? "" : "s"}`));
|
|
1516
1720
|
}
|
|
1517
1721
|
async function workCreate(config, title, flags, json) {
|
|
1518
1722
|
const projectId = flags.project;
|
|
@@ -1534,10 +1738,12 @@ async function workCreate(config, title, flags, json) {
|
|
|
1534
1738
|
slug,
|
|
1535
1739
|
title,
|
|
1536
1740
|
category: "inbox",
|
|
1537
|
-
|
|
1538
|
-
|
|
1741
|
+
// AX-165: stubs store no body. The title lives in metadata as the
|
|
1742
|
+
// source of truth; the reader composes the empty-doc view from it and
|
|
1743
|
+
// the editor seeds `# Title` on first edit.
|
|
1744
|
+
md_content: "",
|
|
1539
1745
|
format: "md",
|
|
1540
|
-
html_content:
|
|
1746
|
+
html_content: ""
|
|
1541
1747
|
})
|
|
1542
1748
|
});
|
|
1543
1749
|
if (saveRes.ok) {
|
|
@@ -1702,18 +1908,25 @@ When asked for the next task for a specific agent (for example "next Codex task"
|
|
|
1702
1908
|
|
|
1703
1909
|
### Thin work items
|
|
1704
1910
|
|
|
1705
|
-
|
|
1911
|
+
A work item that is only a title and a one-to-three-sentence description, with no task checklist, is **not** a starting point for implementation. It is a request to *design*, not to build - so stop.
|
|
1706
1912
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1913
|
+
A thin stub's body is usually **empty** - quick-create and the CLI store the title and description in metadata only, and the reader composes the view from them until real content exists (AX-165). So \`get_document\` returning empty \`content\` does not mean the item has no substance: read its \`description\` field. When you write the stub up into a design, that body content replaces the empty state, and the write-up should also refresh the title and description to match what the item now delivers.
|
|
1914
|
+
|
|
1915
|
+
1. Write it up as a proper design/plan doc first.
|
|
1916
|
+
2. Put the write-up and the proposed tasks to the requester for **approval** - a structured question/approval tool where the runtime provides one (e.g. \`AskUserQuestion\`); otherwise say so and wait for typed approval.
|
|
1917
|
+
3. Only after approval: generate the task checklist, mark the first task \`active\`, and begin - updating progress as you go.
|
|
1712
1918
|
|
|
1713
1919
|
If the requester corrects workflow sequencing mid-session, pause implementation, repair the Artyfax document/checklist first, and resume from the checklist rather than local memory.
|
|
1714
1920
|
|
|
1715
1921
|
Open questions are a completion blocker, not just a planning step. Any unresolved question or unticked task - including ones you uncover while building - must go back to the user through a structured question (\`AskUserQuestion\` where available) before the item can move to In Review. You surface and the user decides; you never resolve an open question on your own authority.
|
|
1716
1922
|
|
|
1923
|
+
### Promoting to the board
|
|
1924
|
+
|
|
1925
|
+
A document with \`project_id\` + \`parent\` and **no** \`work_status\` is a child doc carried under its parent - it is **not** a board card. Setting \`work_status\` is what promotes a doc to a board work item and mints its \`${p}-\` card.
|
|
1926
|
+
|
|
1927
|
+
- **The parent is the single board item.** A parent-and-children design lives on the board as **one** card (the parent), which rolls up its children's progress via \`task_source: children\`. The children are docs carried under it, not separate cards.
|
|
1928
|
+
- **Never set \`work_status\` on a child doc without explicit approval.** Don't promote children to the board "for structure" or to make slices individually trackable - that is the whole point of parent/child docs: one board item carrying many docs. The default for a child doc is \`project_id\` + \`parent\`, no \`work_status\`. The user elevates a child to its own board card if and when they want it there - their call, never a default.
|
|
1929
|
+
|
|
1717
1930
|
### Board update cadence
|
|
1718
1931
|
|
|
1719
1932
|
The board is the cross-session signal - other agents and the user read live progress from it, so it must reflect reality *as you work*, not after the fact.
|
|
@@ -1732,7 +1945,7 @@ Use Artyfax MCP/CLI for live work updates (status changes, task ticks, assignmen
|
|
|
1732
1945
|
### Conventions
|
|
1733
1946
|
|
|
1734
1947
|
- **Work IDs** (e.g. ${p}-15) are stable. Reference them in commits and changelogs. All MCP tools accept them directly.
|
|
1735
|
-
- **Descriptions**:
|
|
1948
|
+
- **Descriptions are high-value content**: one field feeds the board card, grid cards, search, the public page's og:description (the Google snippet), and - for a body-less stub - the entire visible substance of the item. Craft deliberately (120-180 chars, plain text, state what it delivers) and refresh it whenever the item's scope shifts. A description you write is recorded as manual and never overwritten by body derivation; clear it (save it empty) to hand it back to auto-derivation. Exception: while a doc is body-less its description is a brain dump - edits stay part of the stub, and the first real content save re-derives it from the body's opening (write the intro as the summary it will become); once the doc has content, description edits lock as manual. If it can't fit the bar, the item has outgrown being a stub - put the substance in the body. The MCP tool guidance carries the full rules.
|
|
1736
1949
|
- **Statuses**: backlog > up_next > in_progress > in_review > done (or on_hold, cancelled).
|
|
1737
1950
|
`;
|
|
1738
1951
|
}
|
|
@@ -1855,17 +2068,16 @@ async function resolveId(config, identifier) {
|
|
|
1855
2068
|
const data = await apiFetch(config, `/resolve/${encodeURIComponent(identifier)}`);
|
|
1856
2069
|
return data.id;
|
|
1857
2070
|
}
|
|
2071
|
+
async function fetchTasks(config, id) {
|
|
2072
|
+
const data = await apiFetch(config, `/documents/${id}/tasks`);
|
|
2073
|
+
return data.tasks;
|
|
2074
|
+
}
|
|
1858
2075
|
async function taskList(config, identifier, json) {
|
|
1859
2076
|
const spin = spinner("Loading tasks\u2026");
|
|
1860
2077
|
spin.start();
|
|
1861
2078
|
const id = await resolveId(config, identifier);
|
|
1862
|
-
const
|
|
2079
|
+
const tasks = await fetchTasks(config, id);
|
|
1863
2080
|
spin.stop();
|
|
1864
|
-
if (doc.format !== "md" || !doc.md_source) {
|
|
1865
|
-
console.error(c.rose("Task listing only supported for markdown documents"));
|
|
1866
|
-
process.exit(1);
|
|
1867
|
-
}
|
|
1868
|
-
const tasks = parseTasks(doc.md_source);
|
|
1869
2081
|
if (json) {
|
|
1870
2082
|
console.log(JSON.stringify(tasks, null, 2));
|
|
1871
2083
|
return;
|
|
@@ -1891,8 +2103,7 @@ async function taskUpdate(config, identifier, taskText, state, json) {
|
|
|
1891
2103
|
const spin = spinner(`Setting task to ${state}\u2026`);
|
|
1892
2104
|
spin.start();
|
|
1893
2105
|
const id = await resolveId(config, identifier);
|
|
1894
|
-
const
|
|
1895
|
-
const tasks = parseTasks(doc.md_source);
|
|
2106
|
+
const tasks = await fetchTasks(config, id);
|
|
1896
2107
|
const needle = taskText.toLowerCase();
|
|
1897
2108
|
const match = tasks.find((t) => t.text.toLowerCase().includes(needle));
|
|
1898
2109
|
if (!match) {
|
|
@@ -1913,31 +2124,158 @@ async function taskUpdate(config, identifier, taskText, state, json) {
|
|
|
1913
2124
|
const stateLabel = { done: c.teal("done"), active: c.amber("active"), pending: c.muted("pending") };
|
|
1914
2125
|
console.log(`${stateLabel[state]} ${match.text}`);
|
|
1915
2126
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2127
|
+
|
|
2128
|
+
// ../shared/src/child-order.ts
|
|
2129
|
+
function resolveChildOrder(input) {
|
|
2130
|
+
const { activeIds, archivedIds, requested } = input;
|
|
2131
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2132
|
+
const dupes = /* @__PURE__ */ new Set();
|
|
2133
|
+
for (const id of requested) {
|
|
2134
|
+
if (seen.has(id)) dupes.add(id);
|
|
2135
|
+
seen.add(id);
|
|
2136
|
+
}
|
|
2137
|
+
if (dupes.size > 0) {
|
|
2138
|
+
return { ok: false, error: `Duplicate id(s) in the order: ${[...dupes].join(", ")}` };
|
|
2139
|
+
}
|
|
2140
|
+
const activeSet = new Set(activeIds);
|
|
2141
|
+
const archivedSet = new Set(archivedIds);
|
|
2142
|
+
const archivedRequested = [];
|
|
2143
|
+
const foreign = [];
|
|
2144
|
+
for (const id of requested) {
|
|
2145
|
+
if (activeSet.has(id)) continue;
|
|
2146
|
+
if (archivedSet.has(id)) archivedRequested.push(id);
|
|
2147
|
+
else foreign.push(id);
|
|
2148
|
+
}
|
|
2149
|
+
if (archivedRequested.length > 0) {
|
|
2150
|
+
return {
|
|
2151
|
+
ok: false,
|
|
2152
|
+
error: `Archived page(s) can't be positioned: ${archivedRequested.join(", ")}. Archived pages keep their order automatically.`
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
if (foreign.length > 0) {
|
|
2156
|
+
return { ok: false, error: `Not a child of this parent: ${foreign.join(", ")}` };
|
|
2157
|
+
}
|
|
2158
|
+
const requestedSet = new Set(requested);
|
|
2159
|
+
const missing = activeIds.filter((id) => !requestedSet.has(id));
|
|
2160
|
+
if (missing.length > 0) {
|
|
2161
|
+
return {
|
|
2162
|
+
ok: false,
|
|
2163
|
+
error: `The order must list every visible child. Missing: ${missing.join(", ")}`
|
|
2164
|
+
};
|
|
1935
2165
|
}
|
|
1936
|
-
return
|
|
2166
|
+
return { ok: true, childIds: [...requested, ...archivedIds] };
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
// src/commands/pages.ts
|
|
2170
|
+
async function fetchChildrenSorted(config, parentId) {
|
|
2171
|
+
const { documents } = await apiFetch(
|
|
2172
|
+
config,
|
|
2173
|
+
`/documents?parent_id=${encodeURIComponent(parentId)}&archived=include&limit=all`
|
|
2174
|
+
);
|
|
2175
|
+
return [...documents].sort(
|
|
2176
|
+
(a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0) || (a.title ?? "").localeCompare(b.title ?? "")
|
|
2177
|
+
);
|
|
2178
|
+
}
|
|
2179
|
+
async function loadChildren(config, parentId) {
|
|
2180
|
+
const sorted = await fetchChildrenSorted(config, parentId);
|
|
2181
|
+
return {
|
|
2182
|
+
activeIds: sorted.filter((d) => !d.archived).map((d) => d.id),
|
|
2183
|
+
archivedIds: sorted.filter((d) => d.archived).map((d) => d.id)
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
async function writeOrder(config, parentId, childIds) {
|
|
2187
|
+
await apiFetch(config, `/documents/${parentId}/children/order`, {
|
|
2188
|
+
method: "PUT",
|
|
2189
|
+
body: JSON.stringify({ child_ids: childIds })
|
|
2190
|
+
});
|
|
2191
|
+
}
|
|
2192
|
+
function fail(spin, message) {
|
|
2193
|
+
spin.stop();
|
|
2194
|
+
console.error(c.rose(message));
|
|
2195
|
+
process.exit(1);
|
|
2196
|
+
}
|
|
2197
|
+
async function pagesList(config, parentRef, json) {
|
|
2198
|
+
const spin = spinner("Loading pages\u2026");
|
|
2199
|
+
spin.start();
|
|
2200
|
+
const parentId = (await resolveSlug(config, parentRef)).id;
|
|
2201
|
+
const children = await fetchChildrenSorted(config, parentId);
|
|
2202
|
+
spin.stop();
|
|
2203
|
+
const visible = children.filter((d) => !d.archived);
|
|
2204
|
+
const archivedCount = children.length - visible.length;
|
|
2205
|
+
if (json) {
|
|
2206
|
+
console.log(JSON.stringify(visible));
|
|
2207
|
+
return;
|
|
2208
|
+
}
|
|
2209
|
+
if (visible.length === 0) {
|
|
2210
|
+
console.log(
|
|
2211
|
+
c.muted(
|
|
2212
|
+
archivedCount > 0 ? `No visible child pages (${archivedCount} archived hidden).` : "No child pages."
|
|
2213
|
+
)
|
|
2214
|
+
);
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
const rows = visible.map((d, i) => [c.muted(`${i + 1}`), d.title ?? "", c.muted(d.slug ?? "")]);
|
|
2218
|
+
console.log(table(rows, { header: [c.muted("#"), "Title", c.muted("Slug")] }));
|
|
2219
|
+
if (archivedCount > 0) {
|
|
2220
|
+
console.log(c.muted(`
|
|
2221
|
+
${archivedCount} archived page${archivedCount === 1 ? "" : "s"} hidden`));
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
async function pagesOrder(config, parentRef, childRefs, json) {
|
|
2225
|
+
const spin = spinner("Setting page order\u2026");
|
|
2226
|
+
spin.start();
|
|
2227
|
+
const parentId = (await resolveSlug(config, parentRef)).id;
|
|
2228
|
+
const requested = await Promise.all(childRefs.map(async (r) => (await resolveSlug(config, r)).id));
|
|
2229
|
+
const { activeIds, archivedIds } = await loadChildren(config, parentId);
|
|
2230
|
+
const order = resolveChildOrder({ activeIds, archivedIds, requested });
|
|
2231
|
+
if (!order.ok) fail(spin, order.error);
|
|
2232
|
+
await writeOrder(config, parentId, order.childIds);
|
|
2233
|
+
spin.stop();
|
|
2234
|
+
if (json) {
|
|
2235
|
+
console.log(JSON.stringify({ ok: true, child_ids: order.childIds }));
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
const n = order.childIds.length;
|
|
2239
|
+
console.log(`${c.teal("\u2713")} Reordered ${n} page${n === 1 ? "" : "s"}`);
|
|
2240
|
+
}
|
|
2241
|
+
async function pagesMove(config, childRef, position, targetRef, json) {
|
|
2242
|
+
if (position !== "before" && position !== "after") {
|
|
2243
|
+
console.error(c.rose(`Position must be "before" or "after", got "${position}".`));
|
|
2244
|
+
process.exit(1);
|
|
2245
|
+
}
|
|
2246
|
+
const spin = spinner("Moving page\u2026");
|
|
2247
|
+
spin.start();
|
|
2248
|
+
const child = await resolveSlug(config, childRef);
|
|
2249
|
+
const target = await resolveSlug(config, targetRef);
|
|
2250
|
+
if (child.id === target.id) fail(spin, "Can't move a page relative to itself.");
|
|
2251
|
+
if (!child.parent_id) fail(spin, `"${child.title}" is not a child page - it has no parent.`);
|
|
2252
|
+
if (target.parent_id !== child.parent_id) {
|
|
2253
|
+
fail(spin, `"${target.title}" is not a sibling of "${child.title}".`);
|
|
2254
|
+
}
|
|
2255
|
+
const { activeIds, archivedIds } = await loadChildren(config, child.parent_id);
|
|
2256
|
+
if (!activeIds.includes(child.id)) {
|
|
2257
|
+
fail(spin, `"${child.title}" is archived - unarchive it before reordering.`);
|
|
2258
|
+
}
|
|
2259
|
+
if (!activeIds.includes(target.id)) {
|
|
2260
|
+
fail(spin, `"${target.title}" is archived - pick a visible sibling as the target.`);
|
|
2261
|
+
}
|
|
2262
|
+
const without = activeIds.filter((id) => id !== child.id);
|
|
2263
|
+
const targetIdx = without.indexOf(target.id);
|
|
2264
|
+
const insertIdx = position === "before" ? targetIdx : targetIdx + 1;
|
|
2265
|
+
without.splice(insertIdx, 0, child.id);
|
|
2266
|
+
const order = resolveChildOrder({ activeIds, archivedIds, requested: without });
|
|
2267
|
+
if (!order.ok) fail(spin, order.error);
|
|
2268
|
+
await writeOrder(config, child.parent_id, order.childIds);
|
|
2269
|
+
spin.stop();
|
|
2270
|
+
if (json) {
|
|
2271
|
+
console.log(JSON.stringify({ ok: true, child_ids: order.childIds }));
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
console.log(`${c.teal("\u2713")} Moved "${child.title}" ${position} "${target.title}"`);
|
|
1937
2275
|
}
|
|
1938
2276
|
|
|
1939
2277
|
// src/cli.ts
|
|
1940
|
-
var VERSION = true ? "0.3.
|
|
2278
|
+
var VERSION = true ? "0.3.2" : "0.0.0-dev";
|
|
1941
2279
|
function brandedHelp() {
|
|
1942
2280
|
return `
|
|
1943
2281
|
${c.amber("artyfax")} ${c.muted(`v${VERSION}`)} \u2014 your personal document library
|
|
@@ -1959,6 +2297,8 @@ ${c.bright("Documents")}
|
|
|
1959
2297
|
unsecure <slug> Remove encryption
|
|
1960
2298
|
cover <doc> <file> Set a document's cover image from a local file
|
|
1961
2299
|
image <doc> <file> Upload a local image, print the markdown to embed
|
|
2300
|
+
publish <doc> Pin a published version (public/shared views serve it)
|
|
2301
|
+
unpublish <doc> Clear the published pointer, revert to serving live
|
|
1962
2302
|
|
|
1963
2303
|
${c.bright("Sub-resources")}
|
|
1964
2304
|
share create <slug> Create a share link
|
|
@@ -2040,7 +2380,11 @@ ${c.bright("Options:")}
|
|
|
2040
2380
|
${c.bright("Examples:")}
|
|
2041
2381
|
artyfax save notes.md --category inbox
|
|
2042
2382
|
artyfax save --url https://example.com/article --category articles
|
|
2043
|
-
artyfax save report.md --secure --category reports
|
|
2383
|
+
artyfax save report.md --secure --category reports
|
|
2384
|
+
|
|
2385
|
+
${c.bright("Markdown:")} mermaid diagrams render (\`\`\`mermaid fences); colour flowchart
|
|
2386
|
+
nodes on-brand with \`class <node> amber|teal|blue|violet|rose\` rather than raw hex.
|
|
2387
|
+
Full syntax: the editor's Markdown Reference, or the seeded "Markdown Formatting" doc.`,
|
|
2044
2388
|
read: `${c.amber("artyfax read")} \u2014 read document content
|
|
2045
2389
|
|
|
2046
2390
|
${c.bright("Usage:")} artyfax read <slug>
|
|
@@ -2179,6 +2523,25 @@ ${c.bright("Usage:")} artyfax version list <slug> Show version history
|
|
|
2179
2523
|
${c.bright("Examples:")}
|
|
2180
2524
|
artyfax version list inbox/my-doc
|
|
2181
2525
|
artyfax version restore inbox/my-doc --yes`,
|
|
2526
|
+
publish: `${c.amber("artyfax publish")} \u2014 pin a published version
|
|
2527
|
+
|
|
2528
|
+
${c.bright("Usage:")} artyfax publish <doc> [version-id]
|
|
2529
|
+
|
|
2530
|
+
Public and shared views serve the pinned version while later edits stay draft
|
|
2531
|
+
until you publish again. With no version-id, publishes the current draft;
|
|
2532
|
+
pass a version id (from ${c.amber("artyfax version list")}) to publish an older state.
|
|
2533
|
+
|
|
2534
|
+
${c.bright("Examples:")}
|
|
2535
|
+
artyfax publish plans/my-post
|
|
2536
|
+
artyfax publish AX-12 1843`,
|
|
2537
|
+
unpublish: `${c.amber("artyfax unpublish")} \u2014 clear the published pointer
|
|
2538
|
+
|
|
2539
|
+
${c.bright("Usage:")} artyfax unpublish <doc>
|
|
2540
|
+
|
|
2541
|
+
Public and shared views revert to serving the live document.
|
|
2542
|
+
|
|
2543
|
+
${c.bright("Examples:")}
|
|
2544
|
+
artyfax unpublish plans/my-post`,
|
|
2182
2545
|
annotation: `${c.amber("artyfax annotation")} \u2014 read a document's annotations (highlights, underlines, strikethroughs, notes, comments, redactions)
|
|
2183
2546
|
|
|
2184
2547
|
${c.bright("Usage:")} artyfax annotation list <slug> List annotations (--include-recipients, --since <iso>)
|
|
@@ -2198,7 +2561,20 @@ ${c.bright("Usage:")} artyfax doctor [--json]`,
|
|
|
2198
2561
|
|
|
2199
2562
|
${c.bright("Usage:")} artyfax skill install [--project] Install Artyfax skill
|
|
2200
2563
|
artyfax skill status Check installation
|
|
2201
|
-
artyfax skill update Update to latest version
|
|
2564
|
+
artyfax skill update Update to latest version`,
|
|
2565
|
+
pages: `${c.amber("artyfax pages")} \u2014 list and reorder a parent document's child pages
|
|
2566
|
+
|
|
2567
|
+
${c.bright("Usage:")} artyfax pages list <parent> Show child pages in reading order
|
|
2568
|
+
artyfax pages order <parent> <child...> Set the full reading order
|
|
2569
|
+
artyfax pages move <child> before|after <target> Nudge one page past a sibling
|
|
2570
|
+
|
|
2571
|
+
Refs accept a document id, slug, or work id (e.g. ${c.muted("AX-12")}).
|
|
2572
|
+
${c.bright("order")} must name every visible child once; archived pages keep their order automatically.
|
|
2573
|
+
|
|
2574
|
+
${c.bright("Examples:")}
|
|
2575
|
+
artyfax pages list AX-94
|
|
2576
|
+
artyfax pages order AX-94 intro setup usage
|
|
2577
|
+
artyfax pages move usage before setup`
|
|
2202
2578
|
};
|
|
2203
2579
|
function parseArgs(args) {
|
|
2204
2580
|
const flags = {};
|
|
@@ -2237,10 +2613,36 @@ function parseArgs(args) {
|
|
|
2237
2613
|
}
|
|
2238
2614
|
return { command, subcommand, positional, flags };
|
|
2239
2615
|
}
|
|
2240
|
-
var SUB_RESOURCES = /* @__PURE__ */ new Set(["share", "cat", "tag", "version", "annotation", "skill", "snippets", "work", "project", "workspace"]);
|
|
2616
|
+
var SUB_RESOURCES = /* @__PURE__ */ new Set(["share", "cat", "tag", "version", "annotation", "skill", "snippets", "work", "project", "workspace", "pages"]);
|
|
2241
2617
|
function isSubResource(cmd) {
|
|
2242
2618
|
return SUB_RESOURCES.has(cmd);
|
|
2243
2619
|
}
|
|
2620
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2621
|
+
"init",
|
|
2622
|
+
"save",
|
|
2623
|
+
"read",
|
|
2624
|
+
"list",
|
|
2625
|
+
"get",
|
|
2626
|
+
"search",
|
|
2627
|
+
"delete",
|
|
2628
|
+
"open",
|
|
2629
|
+
"secure",
|
|
2630
|
+
"unsecure",
|
|
2631
|
+
"update",
|
|
2632
|
+
"metadata",
|
|
2633
|
+
"cover",
|
|
2634
|
+
"image",
|
|
2635
|
+
"unsplash",
|
|
2636
|
+
"publish",
|
|
2637
|
+
"unpublish",
|
|
2638
|
+
"doctor",
|
|
2639
|
+
"task",
|
|
2640
|
+
"tasks",
|
|
2641
|
+
...SUB_RESOURCES
|
|
2642
|
+
]);
|
|
2643
|
+
function isKnownCommand(command) {
|
|
2644
|
+
return KNOWN_COMMANDS.has(command);
|
|
2645
|
+
}
|
|
2244
2646
|
async function main() {
|
|
2245
2647
|
const parsed = parseArgs(process.argv.slice(2));
|
|
2246
2648
|
const { command, subcommand, positional, flags } = parsed;
|
|
@@ -2262,36 +2664,7 @@ async function main() {
|
|
|
2262
2664
|
console.log(brandedHelp());
|
|
2263
2665
|
process.exit(0);
|
|
2264
2666
|
}
|
|
2265
|
-
|
|
2266
|
-
"init",
|
|
2267
|
-
"save",
|
|
2268
|
-
"read",
|
|
2269
|
-
"list",
|
|
2270
|
-
"get",
|
|
2271
|
-
"search",
|
|
2272
|
-
"delete",
|
|
2273
|
-
"open",
|
|
2274
|
-
"secure",
|
|
2275
|
-
"unsecure",
|
|
2276
|
-
"update",
|
|
2277
|
-
"metadata",
|
|
2278
|
-
"cover",
|
|
2279
|
-
"image",
|
|
2280
|
-
"share",
|
|
2281
|
-
"cat",
|
|
2282
|
-
"tag",
|
|
2283
|
-
"version",
|
|
2284
|
-
"annotation",
|
|
2285
|
-
"skill",
|
|
2286
|
-
"snippets",
|
|
2287
|
-
"doctor",
|
|
2288
|
-
"work",
|
|
2289
|
-
"project",
|
|
2290
|
-
"workspace",
|
|
2291
|
-
"task",
|
|
2292
|
-
"tasks"
|
|
2293
|
-
]);
|
|
2294
|
-
if (!KNOWN_COMMANDS.has(command)) {
|
|
2667
|
+
if (!isKnownCommand(command)) {
|
|
2295
2668
|
error(`Unknown command: ${command}`, `Run \`artyfax--help\` for available commands`);
|
|
2296
2669
|
}
|
|
2297
2670
|
if (command === "skill") {
|
|
@@ -2422,6 +2795,24 @@ async function main() {
|
|
|
2422
2795
|
await open(config, slug);
|
|
2423
2796
|
break;
|
|
2424
2797
|
}
|
|
2798
|
+
case "publish": {
|
|
2799
|
+
const slug = positional[0];
|
|
2800
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax publish <doc> [version-id]");
|
|
2801
|
+
const versionArg = positional[1];
|
|
2802
|
+
let version;
|
|
2803
|
+
if (versionArg !== void 0) {
|
|
2804
|
+
version = Number(versionArg);
|
|
2805
|
+
if (!Number.isInteger(version)) error(`Invalid version id: ${versionArg}`, "Usage: artyfax publish <doc> [version-id]");
|
|
2806
|
+
}
|
|
2807
|
+
await publish(config, slug, { version, json });
|
|
2808
|
+
break;
|
|
2809
|
+
}
|
|
2810
|
+
case "unpublish": {
|
|
2811
|
+
const slug = positional[0];
|
|
2812
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax unpublish <doc>");
|
|
2813
|
+
await unpublish(config, slug, json);
|
|
2814
|
+
break;
|
|
2815
|
+
}
|
|
2425
2816
|
case "secure": {
|
|
2426
2817
|
const slug = positional[0];
|
|
2427
2818
|
if (!slug) error("Missing slug argument", "Usage: artyfax secure <slug>");
|
|
@@ -2436,8 +2827,8 @@ async function main() {
|
|
|
2436
2827
|
}
|
|
2437
2828
|
case "update": {
|
|
2438
2829
|
const slug = positional[0];
|
|
2439
|
-
if (!slug) error("Missing slug argument", "Usage: artyfax update <slug> [file] (reads from stdin if no file)");
|
|
2440
|
-
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);
|
|
2441
2832
|
break;
|
|
2442
2833
|
}
|
|
2443
2834
|
case "metadata": {
|
|
@@ -2458,15 +2849,35 @@ async function main() {
|
|
|
2458
2849
|
case "cover": {
|
|
2459
2850
|
const slug = positional[0];
|
|
2460
2851
|
const file = positional[1];
|
|
2461
|
-
|
|
2462
|
-
|
|
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
|
+
}
|
|
2463
2861
|
break;
|
|
2464
2862
|
}
|
|
2465
2863
|
case "image": {
|
|
2466
2864
|
const slug = positional[0];
|
|
2467
2865
|
const file = positional[1];
|
|
2468
|
-
|
|
2469
|
-
|
|
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 });
|
|
2470
2881
|
break;
|
|
2471
2882
|
}
|
|
2472
2883
|
case "share": {
|
|
@@ -2637,6 +3048,38 @@ async function main() {
|
|
|
2637
3048
|
}
|
|
2638
3049
|
break;
|
|
2639
3050
|
}
|
|
3051
|
+
case "pages": {
|
|
3052
|
+
switch (subcommand) {
|
|
3053
|
+
case "list": {
|
|
3054
|
+
const parent = positional[0];
|
|
3055
|
+
if (!parent) error("Missing parent", "Usage: artyfax pages list <parent>");
|
|
3056
|
+
await pagesList(config, parent, json);
|
|
3057
|
+
break;
|
|
3058
|
+
}
|
|
3059
|
+
case "order": {
|
|
3060
|
+
const parent = positional[0];
|
|
3061
|
+
const childRefs = positional.slice(1);
|
|
3062
|
+
if (!parent || childRefs.length === 0) {
|
|
3063
|
+
error("Missing arguments", "Usage: artyfax pages order <parent> <child...>");
|
|
3064
|
+
}
|
|
3065
|
+
await pagesOrder(config, parent, childRefs, json);
|
|
3066
|
+
break;
|
|
3067
|
+
}
|
|
3068
|
+
case "move": {
|
|
3069
|
+
const child = positional[0];
|
|
3070
|
+
const position = positional[1];
|
|
3071
|
+
const target = positional[2];
|
|
3072
|
+
if (!child || !position || !target) {
|
|
3073
|
+
error("Missing arguments", "Usage: artyfax pages move <child> before|after <target>");
|
|
3074
|
+
}
|
|
3075
|
+
await pagesMove(config, child, position, target, json);
|
|
3076
|
+
break;
|
|
3077
|
+
}
|
|
3078
|
+
default:
|
|
3079
|
+
error(`Unknown pages subcommand: ${subcommand || "(none)"}`, "Usage: artyfax pages <list|order|move>");
|
|
3080
|
+
}
|
|
3081
|
+
break;
|
|
3082
|
+
}
|
|
2640
3083
|
case "work": {
|
|
2641
3084
|
switch (subcommand) {
|
|
2642
3085
|
case "":
|
|
@@ -2673,8 +3116,13 @@ async function main() {
|
|
|
2673
3116
|
await workCreate(config, title, flags, json);
|
|
2674
3117
|
break;
|
|
2675
3118
|
}
|
|
3119
|
+
case "touch": {
|
|
3120
|
+
if (positional.length === 0) error("Missing work item ID(s)", "Usage: artyfax work touch <id...> [--action editing|building|reviewing|running]");
|
|
3121
|
+
await workTouch(config, positional, flags, json);
|
|
3122
|
+
break;
|
|
3123
|
+
}
|
|
2676
3124
|
default:
|
|
2677
|
-
error(`Unknown work subcommand: ${subcommand}`, "Usage: artyfax work <list|next|start|done|status|create>");
|
|
3125
|
+
error(`Unknown work subcommand: ${subcommand}`, "Usage: artyfax work <list|next|start|done|status|create|touch>");
|
|
2678
3126
|
}
|
|
2679
3127
|
break;
|
|
2680
3128
|
}
|
|
@@ -2732,5 +3180,6 @@ if (isDirectRun()) {
|
|
|
2732
3180
|
}
|
|
2733
3181
|
export {
|
|
2734
3182
|
VERSION,
|
|
3183
|
+
isKnownCommand,
|
|
2735
3184
|
parseArgs
|
|
2736
3185
|
};
|