artyfax 0.3.0 → 0.3.1
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 +420 -122
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -320,13 +320,6 @@ async function resolveSlug(config, slugOrId) {
|
|
|
320
320
|
} catch {
|
|
321
321
|
}
|
|
322
322
|
}
|
|
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
323
|
if (/^[A-Z]+-\d+$/.test(slugOrId)) {
|
|
331
324
|
try {
|
|
332
325
|
const { id } = await apiFetch(config, `/resolve/${encodeURIComponent(slugOrId)}`);
|
|
@@ -335,6 +328,13 @@ async function resolveSlug(config, slugOrId) {
|
|
|
335
328
|
} catch {
|
|
336
329
|
}
|
|
337
330
|
}
|
|
331
|
+
if (!slugOrId.includes("/") && !/^[A-Z]+-\d+$/.test(slugOrId)) {
|
|
332
|
+
try {
|
|
333
|
+
return await apiFetch(config, `/documents/${encodeURIComponent(slugOrId)}`);
|
|
334
|
+
} catch (e) {
|
|
335
|
+
if (e.status !== 404) throw e;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
338
|
const docs = await apiFetch(
|
|
339
339
|
config,
|
|
340
340
|
`/documents?limit=500`
|
|
@@ -687,17 +687,6 @@ function formatBytes(bytes) {
|
|
|
687
687
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
688
688
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
|
|
689
689
|
}
|
|
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
690
|
function readImageFile(file) {
|
|
702
691
|
const path = resolve2(file);
|
|
703
692
|
let stat;
|
|
@@ -730,7 +719,7 @@ async function setCover(config, slugOrId, file, opts) {
|
|
|
730
719
|
const image = readImageFile(file);
|
|
731
720
|
const spin = spinner("Resolving document\u2026");
|
|
732
721
|
spin.start();
|
|
733
|
-
const doc = await
|
|
722
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
734
723
|
spin.text = `Uploading ${image.name}\u2026`;
|
|
735
724
|
const uploaded = await uploadImage(config, doc.id, image, "cover");
|
|
736
725
|
spin.text = "Setting cover\u2026";
|
|
@@ -752,7 +741,7 @@ async function addImage(config, slugOrId, file, opts) {
|
|
|
752
741
|
const image = readImageFile(file);
|
|
753
742
|
const spin = spinner("Resolving document\u2026");
|
|
754
743
|
spin.start();
|
|
755
|
-
const doc = await
|
|
744
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
756
745
|
spin.text = `Uploading ${image.name}\u2026`;
|
|
757
746
|
const uploaded = await uploadImage(config, doc.id, image, "content");
|
|
758
747
|
spin.stop();
|
|
@@ -782,9 +771,13 @@ async function shareCreate(config, slugOrId, json) {
|
|
|
782
771
|
}
|
|
783
772
|
);
|
|
784
773
|
spin.stop();
|
|
785
|
-
const
|
|
774
|
+
const hash = result.share?.hash;
|
|
775
|
+
if (!hash) {
|
|
776
|
+
throw new Error("Share created but the server response contained no hash.");
|
|
777
|
+
}
|
|
778
|
+
const url = `${config.endpoint}/d/${hash}`;
|
|
786
779
|
if (json) {
|
|
787
|
-
console.log(JSON.stringify({ hash
|
|
780
|
+
console.log(JSON.stringify({ hash, url, slug: doc.slug }));
|
|
788
781
|
} else {
|
|
789
782
|
console.log(`${c.teal("Share created:")} ${url}`);
|
|
790
783
|
}
|
|
@@ -910,6 +903,10 @@ async function tagDelete(config, tag, json) {
|
|
|
910
903
|
}
|
|
911
904
|
|
|
912
905
|
// src/commands/version.ts
|
|
906
|
+
function fmtTs(ts) {
|
|
907
|
+
if (!ts) return "\u2014";
|
|
908
|
+
return ts.replace("T", " ").replace(/\.\d+Z$/, "");
|
|
909
|
+
}
|
|
913
910
|
async function versionList(config, slugOrId, json) {
|
|
914
911
|
const spin = spinner("Resolving document\u2026");
|
|
915
912
|
spin.start();
|
|
@@ -921,18 +918,24 @@ async function versionList(config, slugOrId, json) {
|
|
|
921
918
|
console.log(JSON.stringify(data.versions));
|
|
922
919
|
return;
|
|
923
920
|
}
|
|
924
|
-
|
|
925
|
-
|
|
921
|
+
const versions = data.versions ?? [];
|
|
922
|
+
const currentSaved = fmtTs(data.current?.updated_at);
|
|
923
|
+
if (versions.length === 0) {
|
|
924
|
+
console.log(`${c.bright(doc.slug)} \u2014 ${c.muted(`no previous versions (current saved ${currentSaved}).`)}`);
|
|
926
925
|
return;
|
|
927
926
|
}
|
|
928
|
-
console.log(
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
927
|
+
console.log(
|
|
928
|
+
`${c.bright(doc.slug)} \u2014 current saved ${c.muted(currentSaved)}, ${versions.length} previous version${versions.length === 1 ? "" : "s"}
|
|
929
|
+
`
|
|
930
|
+
);
|
|
931
|
+
const n = versions.length;
|
|
932
|
+
const rows = versions.map((v, i) => [
|
|
933
|
+
c.muted(`v${n - i}`),
|
|
934
|
+
fmtTs(v.created_at),
|
|
935
|
+
v.source || c.muted("\u2014"),
|
|
936
|
+
v.published ? c.teal("\u25CF published") : ""
|
|
934
937
|
]);
|
|
935
|
-
console.log(table(rows, { header: ["", "
|
|
938
|
+
console.log(table(rows, { header: ["", "Saved", "Source", ""] }));
|
|
936
939
|
}
|
|
937
940
|
async function versionRestore(config, slugOrId, yes, json) {
|
|
938
941
|
const spin = spinner("Resolving document\u2026");
|
|
@@ -941,25 +944,26 @@ async function versionRestore(config, slugOrId, yes, json) {
|
|
|
941
944
|
spin.text = "Loading versions\u2026";
|
|
942
945
|
const data = await apiFetch(config, `/documents/${doc.id}/versions`);
|
|
943
946
|
spin.stop();
|
|
944
|
-
|
|
947
|
+
const versions = data.versions ?? [];
|
|
948
|
+
if (versions.length === 0) {
|
|
945
949
|
console.log(c.muted("No previous versions to restore."));
|
|
946
950
|
return;
|
|
947
951
|
}
|
|
948
|
-
|
|
949
|
-
console.log(`${c.bright(doc.slug)} \u2014 ${previous.length} previous version${previous.length === 1 ? "" : "s"}:
|
|
952
|
+
console.log(`${c.bright(doc.slug)} \u2014 ${versions.length} previous version${versions.length === 1 ? "" : "s"}:
|
|
950
953
|
`);
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const label = `v${
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
console.log(` ${c.amber(label)} ${
|
|
957
|
-
}
|
|
958
|
-
const target = previous[0];
|
|
954
|
+
const n = versions.length;
|
|
955
|
+
versions.forEach((v, i) => {
|
|
956
|
+
const label = `v${n - i}`;
|
|
957
|
+
const source = v.source ? c.muted(` (${v.source})`) : "";
|
|
958
|
+
const pin = v.published ? c.teal(" \u25CF published") : "";
|
|
959
|
+
console.log(` ${c.amber(label)} ${fmtTs(v.created_at)}${source}${pin}`);
|
|
960
|
+
});
|
|
959
961
|
console.log();
|
|
962
|
+
const target = versions[0];
|
|
960
963
|
if (!yes) {
|
|
961
|
-
const
|
|
962
|
-
|
|
964
|
+
const confirmed = await confirm(
|
|
965
|
+
`Restore to v${n} (${fmtTs(target.created_at)})? The current draft is snapshotted first, so this is reversible.`
|
|
966
|
+
);
|
|
963
967
|
if (!confirmed) {
|
|
964
968
|
console.log(c.muted("Cancelled."));
|
|
965
969
|
return;
|
|
@@ -969,14 +973,60 @@ async function versionRestore(config, slugOrId, yes, json) {
|
|
|
969
973
|
restoreSpin.start();
|
|
970
974
|
await apiFetch(config, `/documents/${doc.id}/versions/restore`, {
|
|
971
975
|
method: "POST",
|
|
972
|
-
|
|
976
|
+
// The restore endpoint keys on the numeric version id, not a timestamp.
|
|
977
|
+
body: JSON.stringify({ version_id: target.id })
|
|
973
978
|
});
|
|
974
979
|
restoreSpin.stop();
|
|
975
980
|
if (json) {
|
|
976
|
-
console.log(JSON.stringify({ restored: true, slug: doc.slug,
|
|
981
|
+
console.log(JSON.stringify({ restored: true, slug: doc.slug, version_id: target.id, created_at: target.created_at }));
|
|
977
982
|
} else {
|
|
978
|
-
console.log(`${c.teal("Restored:")} ${doc.slug} to ${target.
|
|
983
|
+
console.log(`${c.teal("Restored:")} ${doc.slug} to v${n} (${fmtTs(target.created_at)})`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// src/commands/publish.ts
|
|
988
|
+
async function publish(config, slugOrId, opts) {
|
|
989
|
+
const spin = spinner("Resolving document\u2026");
|
|
990
|
+
spin.start();
|
|
991
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
992
|
+
spin.text = opts.version != null ? `Publishing version ${opts.version}\u2026` : "Publishing\u2026";
|
|
993
|
+
const body = {};
|
|
994
|
+
if (opts.version != null) body.version_id = opts.version;
|
|
995
|
+
const data = await apiFetch(
|
|
996
|
+
config,
|
|
997
|
+
`/documents/${doc.id}/publish`,
|
|
998
|
+
{ method: "POST", body: JSON.stringify(body) }
|
|
999
|
+
);
|
|
1000
|
+
spin.stop();
|
|
1001
|
+
if (opts.json) {
|
|
1002
|
+
console.log(JSON.stringify({ published: true, slug: doc.slug, ...data }));
|
|
1003
|
+
return;
|
|
979
1004
|
}
|
|
1005
|
+
const versionNote = data.published_version_id != null ? c.muted(` (version ${data.published_version_id})`) : "";
|
|
1006
|
+
console.log(`${c.teal("Published:")} ${doc.slug}${versionNote}`);
|
|
1007
|
+
console.log(c.muted("Public and shared views now serve this version. Edits stay draft until you publish again."));
|
|
1008
|
+
}
|
|
1009
|
+
async function unpublish(config, slugOrId, json) {
|
|
1010
|
+
const spin = spinner("Resolving document\u2026");
|
|
1011
|
+
spin.start();
|
|
1012
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
1013
|
+
spin.text = "Unpublishing\u2026";
|
|
1014
|
+
const data = await apiFetch(
|
|
1015
|
+
config,
|
|
1016
|
+
`/documents/${doc.id}/unpublish`,
|
|
1017
|
+
{ method: "POST" }
|
|
1018
|
+
);
|
|
1019
|
+
spin.stop();
|
|
1020
|
+
if (json) {
|
|
1021
|
+
console.log(JSON.stringify({ unpublished: true, slug: doc.slug, ...data }));
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
if (data.was_published === false) {
|
|
1025
|
+
console.log(c.muted(`${doc.slug} was not published - nothing to do.`));
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
console.log(`${c.teal("Unpublished:")} ${doc.slug}`);
|
|
1029
|
+
console.log(c.muted("Public and shared views now serve the live document again."));
|
|
980
1030
|
}
|
|
981
1031
|
|
|
982
1032
|
// src/commands/annotation.ts
|
|
@@ -1511,8 +1561,32 @@ async function workSetStatus(config, docId, status, json) {
|
|
|
1511
1561
|
}
|
|
1512
1562
|
console.log(c.bright(`${data.work_id || docId} \u2192 ${demote ? "removed from board" : statusLabel(status)}`));
|
|
1513
1563
|
}
|
|
1514
|
-
function
|
|
1515
|
-
|
|
1564
|
+
async function workTouch(config, ids, flags, json) {
|
|
1565
|
+
if (ids.length === 0) {
|
|
1566
|
+
console.error(c.rose("Error: pass one or more work IDs or doc IDs to keep live."));
|
|
1567
|
+
process.exitCode = 1;
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
const action = typeof flags.action === "string" ? flags.action : void 0;
|
|
1571
|
+
const agent = typeof flags.agent === "string" ? flags.agent : void 0;
|
|
1572
|
+
const spin = spinner("Sending heartbeat\u2026");
|
|
1573
|
+
spin.start();
|
|
1574
|
+
const data = await apiFetch(config, "/projects/work/touch", {
|
|
1575
|
+
method: "POST",
|
|
1576
|
+
headers: { "Content-Type": "application/json" },
|
|
1577
|
+
body: JSON.stringify({ ids, action, agent })
|
|
1578
|
+
});
|
|
1579
|
+
spin.stop();
|
|
1580
|
+
if (json) {
|
|
1581
|
+
console.log(JSON.stringify(data));
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
const warmed = data.warmed ?? [];
|
|
1585
|
+
if (warmed.length === 0) {
|
|
1586
|
+
console.log(c.muted("No in-progress items matched - nothing warmed."));
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
console.log(c.bright(`Kept live: ${warmed.length} item${warmed.length === 1 ? "" : "s"}`));
|
|
1516
1590
|
}
|
|
1517
1591
|
async function workCreate(config, title, flags, json) {
|
|
1518
1592
|
const projectId = flags.project;
|
|
@@ -1534,10 +1608,12 @@ async function workCreate(config, title, flags, json) {
|
|
|
1534
1608
|
slug,
|
|
1535
1609
|
title,
|
|
1536
1610
|
category: "inbox",
|
|
1537
|
-
|
|
1538
|
-
|
|
1611
|
+
// AX-165: stubs store no body. The title lives in metadata as the
|
|
1612
|
+
// source of truth; the reader composes the empty-doc view from it and
|
|
1613
|
+
// the editor seeds `# Title` on first edit.
|
|
1614
|
+
md_content: "",
|
|
1539
1615
|
format: "md",
|
|
1540
|
-
html_content:
|
|
1616
|
+
html_content: ""
|
|
1541
1617
|
})
|
|
1542
1618
|
});
|
|
1543
1619
|
if (saveRes.ok) {
|
|
@@ -1702,18 +1778,25 @@ When asked for the next task for a specific agent (for example "next Codex task"
|
|
|
1702
1778
|
|
|
1703
1779
|
### Thin work items
|
|
1704
1780
|
|
|
1705
|
-
|
|
1781
|
+
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
1782
|
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1783
|
+
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.
|
|
1784
|
+
|
|
1785
|
+
1. Write it up as a proper design/plan doc first.
|
|
1786
|
+
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.
|
|
1787
|
+
3. Only after approval: generate the task checklist, mark the first task \`active\`, and begin - updating progress as you go.
|
|
1712
1788
|
|
|
1713
1789
|
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
1790
|
|
|
1715
1791
|
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
1792
|
|
|
1793
|
+
### Promoting to the board
|
|
1794
|
+
|
|
1795
|
+
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.
|
|
1796
|
+
|
|
1797
|
+
- **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.
|
|
1798
|
+
- **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.
|
|
1799
|
+
|
|
1717
1800
|
### Board update cadence
|
|
1718
1801
|
|
|
1719
1802
|
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 +1815,7 @@ Use Artyfax MCP/CLI for live work updates (status changes, task ticks, assignmen
|
|
|
1732
1815
|
### Conventions
|
|
1733
1816
|
|
|
1734
1817
|
- **Work IDs** (e.g. ${p}-15) are stable. Reference them in commits and changelogs. All MCP tools accept them directly.
|
|
1735
|
-
- **Descriptions**:
|
|
1818
|
+
- **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
1819
|
- **Statuses**: backlog > up_next > in_progress > in_review > done (or on_hold, cancelled).
|
|
1737
1820
|
`;
|
|
1738
1821
|
}
|
|
@@ -1855,17 +1938,16 @@ async function resolveId(config, identifier) {
|
|
|
1855
1938
|
const data = await apiFetch(config, `/resolve/${encodeURIComponent(identifier)}`);
|
|
1856
1939
|
return data.id;
|
|
1857
1940
|
}
|
|
1941
|
+
async function fetchTasks(config, id) {
|
|
1942
|
+
const data = await apiFetch(config, `/documents/${id}/tasks`);
|
|
1943
|
+
return data.tasks;
|
|
1944
|
+
}
|
|
1858
1945
|
async function taskList(config, identifier, json) {
|
|
1859
1946
|
const spin = spinner("Loading tasks\u2026");
|
|
1860
1947
|
spin.start();
|
|
1861
1948
|
const id = await resolveId(config, identifier);
|
|
1862
|
-
const
|
|
1949
|
+
const tasks = await fetchTasks(config, id);
|
|
1863
1950
|
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
1951
|
if (json) {
|
|
1870
1952
|
console.log(JSON.stringify(tasks, null, 2));
|
|
1871
1953
|
return;
|
|
@@ -1891,8 +1973,7 @@ async function taskUpdate(config, identifier, taskText, state, json) {
|
|
|
1891
1973
|
const spin = spinner(`Setting task to ${state}\u2026`);
|
|
1892
1974
|
spin.start();
|
|
1893
1975
|
const id = await resolveId(config, identifier);
|
|
1894
|
-
const
|
|
1895
|
-
const tasks = parseTasks(doc.md_source);
|
|
1976
|
+
const tasks = await fetchTasks(config, id);
|
|
1896
1977
|
const needle = taskText.toLowerCase();
|
|
1897
1978
|
const match = tasks.find((t) => t.text.toLowerCase().includes(needle));
|
|
1898
1979
|
if (!match) {
|
|
@@ -1913,31 +1994,158 @@ async function taskUpdate(config, identifier, taskText, state, json) {
|
|
|
1913
1994
|
const stateLabel = { done: c.teal("done"), active: c.amber("active"), pending: c.muted("pending") };
|
|
1914
1995
|
console.log(`${stateLabel[state]} ${match.text}`);
|
|
1915
1996
|
}
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
const
|
|
1920
|
-
|
|
1921
|
-
const
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1997
|
+
|
|
1998
|
+
// ../shared/src/child-order.ts
|
|
1999
|
+
function resolveChildOrder(input) {
|
|
2000
|
+
const { activeIds, archivedIds, requested } = input;
|
|
2001
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2002
|
+
const dupes = /* @__PURE__ */ new Set();
|
|
2003
|
+
for (const id of requested) {
|
|
2004
|
+
if (seen.has(id)) dupes.add(id);
|
|
2005
|
+
seen.add(id);
|
|
2006
|
+
}
|
|
2007
|
+
if (dupes.size > 0) {
|
|
2008
|
+
return { ok: false, error: `Duplicate id(s) in the order: ${[...dupes].join(", ")}` };
|
|
2009
|
+
}
|
|
2010
|
+
const activeSet = new Set(activeIds);
|
|
2011
|
+
const archivedSet = new Set(archivedIds);
|
|
2012
|
+
const archivedRequested = [];
|
|
2013
|
+
const foreign = [];
|
|
2014
|
+
for (const id of requested) {
|
|
2015
|
+
if (activeSet.has(id)) continue;
|
|
2016
|
+
if (archivedSet.has(id)) archivedRequested.push(id);
|
|
2017
|
+
else foreign.push(id);
|
|
2018
|
+
}
|
|
2019
|
+
if (archivedRequested.length > 0) {
|
|
2020
|
+
return {
|
|
2021
|
+
ok: false,
|
|
2022
|
+
error: `Archived page(s) can't be positioned: ${archivedRequested.join(", ")}. Archived pages keep their order automatically.`
|
|
2023
|
+
};
|
|
1935
2024
|
}
|
|
1936
|
-
|
|
2025
|
+
if (foreign.length > 0) {
|
|
2026
|
+
return { ok: false, error: `Not a child of this parent: ${foreign.join(", ")}` };
|
|
2027
|
+
}
|
|
2028
|
+
const requestedSet = new Set(requested);
|
|
2029
|
+
const missing = activeIds.filter((id) => !requestedSet.has(id));
|
|
2030
|
+
if (missing.length > 0) {
|
|
2031
|
+
return {
|
|
2032
|
+
ok: false,
|
|
2033
|
+
error: `The order must list every visible child. Missing: ${missing.join(", ")}`
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
return { ok: true, childIds: [...requested, ...archivedIds] };
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// src/commands/pages.ts
|
|
2040
|
+
async function fetchChildrenSorted(config, parentId) {
|
|
2041
|
+
const { documents } = await apiFetch(
|
|
2042
|
+
config,
|
|
2043
|
+
`/documents?parent_id=${encodeURIComponent(parentId)}&archived=include&limit=all`
|
|
2044
|
+
);
|
|
2045
|
+
return [...documents].sort(
|
|
2046
|
+
(a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0) || (a.title ?? "").localeCompare(b.title ?? "")
|
|
2047
|
+
);
|
|
2048
|
+
}
|
|
2049
|
+
async function loadChildren(config, parentId) {
|
|
2050
|
+
const sorted = await fetchChildrenSorted(config, parentId);
|
|
2051
|
+
return {
|
|
2052
|
+
activeIds: sorted.filter((d) => !d.archived).map((d) => d.id),
|
|
2053
|
+
archivedIds: sorted.filter((d) => d.archived).map((d) => d.id)
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
async function writeOrder(config, parentId, childIds) {
|
|
2057
|
+
await apiFetch(config, `/documents/${parentId}/children/order`, {
|
|
2058
|
+
method: "PUT",
|
|
2059
|
+
body: JSON.stringify({ child_ids: childIds })
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
function fail(spin, message) {
|
|
2063
|
+
spin.stop();
|
|
2064
|
+
console.error(c.rose(message));
|
|
2065
|
+
process.exit(1);
|
|
2066
|
+
}
|
|
2067
|
+
async function pagesList(config, parentRef, json) {
|
|
2068
|
+
const spin = spinner("Loading pages\u2026");
|
|
2069
|
+
spin.start();
|
|
2070
|
+
const parentId = (await resolveSlug(config, parentRef)).id;
|
|
2071
|
+
const children = await fetchChildrenSorted(config, parentId);
|
|
2072
|
+
spin.stop();
|
|
2073
|
+
const visible = children.filter((d) => !d.archived);
|
|
2074
|
+
const archivedCount = children.length - visible.length;
|
|
2075
|
+
if (json) {
|
|
2076
|
+
console.log(JSON.stringify(visible));
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
if (visible.length === 0) {
|
|
2080
|
+
console.log(
|
|
2081
|
+
c.muted(
|
|
2082
|
+
archivedCount > 0 ? `No visible child pages (${archivedCount} archived hidden).` : "No child pages."
|
|
2083
|
+
)
|
|
2084
|
+
);
|
|
2085
|
+
return;
|
|
2086
|
+
}
|
|
2087
|
+
const rows = visible.map((d, i) => [c.muted(`${i + 1}`), d.title ?? "", c.muted(d.slug ?? "")]);
|
|
2088
|
+
console.log(table(rows, { header: [c.muted("#"), "Title", c.muted("Slug")] }));
|
|
2089
|
+
if (archivedCount > 0) {
|
|
2090
|
+
console.log(c.muted(`
|
|
2091
|
+
${archivedCount} archived page${archivedCount === 1 ? "" : "s"} hidden`));
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
async function pagesOrder(config, parentRef, childRefs, json) {
|
|
2095
|
+
const spin = spinner("Setting page order\u2026");
|
|
2096
|
+
spin.start();
|
|
2097
|
+
const parentId = (await resolveSlug(config, parentRef)).id;
|
|
2098
|
+
const requested = await Promise.all(childRefs.map(async (r) => (await resolveSlug(config, r)).id));
|
|
2099
|
+
const { activeIds, archivedIds } = await loadChildren(config, parentId);
|
|
2100
|
+
const order = resolveChildOrder({ activeIds, archivedIds, requested });
|
|
2101
|
+
if (!order.ok) fail(spin, order.error);
|
|
2102
|
+
await writeOrder(config, parentId, order.childIds);
|
|
2103
|
+
spin.stop();
|
|
2104
|
+
if (json) {
|
|
2105
|
+
console.log(JSON.stringify({ ok: true, child_ids: order.childIds }));
|
|
2106
|
+
return;
|
|
2107
|
+
}
|
|
2108
|
+
const n = order.childIds.length;
|
|
2109
|
+
console.log(`${c.teal("\u2713")} Reordered ${n} page${n === 1 ? "" : "s"}`);
|
|
2110
|
+
}
|
|
2111
|
+
async function pagesMove(config, childRef, position, targetRef, json) {
|
|
2112
|
+
if (position !== "before" && position !== "after") {
|
|
2113
|
+
console.error(c.rose(`Position must be "before" or "after", got "${position}".`));
|
|
2114
|
+
process.exit(1);
|
|
2115
|
+
}
|
|
2116
|
+
const spin = spinner("Moving page\u2026");
|
|
2117
|
+
spin.start();
|
|
2118
|
+
const child = await resolveSlug(config, childRef);
|
|
2119
|
+
const target = await resolveSlug(config, targetRef);
|
|
2120
|
+
if (child.id === target.id) fail(spin, "Can't move a page relative to itself.");
|
|
2121
|
+
if (!child.parent_id) fail(spin, `"${child.title}" is not a child page - it has no parent.`);
|
|
2122
|
+
if (target.parent_id !== child.parent_id) {
|
|
2123
|
+
fail(spin, `"${target.title}" is not a sibling of "${child.title}".`);
|
|
2124
|
+
}
|
|
2125
|
+
const { activeIds, archivedIds } = await loadChildren(config, child.parent_id);
|
|
2126
|
+
if (!activeIds.includes(child.id)) {
|
|
2127
|
+
fail(spin, `"${child.title}" is archived - unarchive it before reordering.`);
|
|
2128
|
+
}
|
|
2129
|
+
if (!activeIds.includes(target.id)) {
|
|
2130
|
+
fail(spin, `"${target.title}" is archived - pick a visible sibling as the target.`);
|
|
2131
|
+
}
|
|
2132
|
+
const without = activeIds.filter((id) => id !== child.id);
|
|
2133
|
+
const targetIdx = without.indexOf(target.id);
|
|
2134
|
+
const insertIdx = position === "before" ? targetIdx : targetIdx + 1;
|
|
2135
|
+
without.splice(insertIdx, 0, child.id);
|
|
2136
|
+
const order = resolveChildOrder({ activeIds, archivedIds, requested: without });
|
|
2137
|
+
if (!order.ok) fail(spin, order.error);
|
|
2138
|
+
await writeOrder(config, child.parent_id, order.childIds);
|
|
2139
|
+
spin.stop();
|
|
2140
|
+
if (json) {
|
|
2141
|
+
console.log(JSON.stringify({ ok: true, child_ids: order.childIds }));
|
|
2142
|
+
return;
|
|
2143
|
+
}
|
|
2144
|
+
console.log(`${c.teal("\u2713")} Moved "${child.title}" ${position} "${target.title}"`);
|
|
1937
2145
|
}
|
|
1938
2146
|
|
|
1939
2147
|
// src/cli.ts
|
|
1940
|
-
var VERSION = true ? "0.3.
|
|
2148
|
+
var VERSION = true ? "0.3.1" : "0.0.0-dev";
|
|
1941
2149
|
function brandedHelp() {
|
|
1942
2150
|
return `
|
|
1943
2151
|
${c.amber("artyfax")} ${c.muted(`v${VERSION}`)} \u2014 your personal document library
|
|
@@ -1959,6 +2167,8 @@ ${c.bright("Documents")}
|
|
|
1959
2167
|
unsecure <slug> Remove encryption
|
|
1960
2168
|
cover <doc> <file> Set a document's cover image from a local file
|
|
1961
2169
|
image <doc> <file> Upload a local image, print the markdown to embed
|
|
2170
|
+
publish <doc> Pin a published version (public/shared views serve it)
|
|
2171
|
+
unpublish <doc> Clear the published pointer, revert to serving live
|
|
1962
2172
|
|
|
1963
2173
|
${c.bright("Sub-resources")}
|
|
1964
2174
|
share create <slug> Create a share link
|
|
@@ -2040,7 +2250,11 @@ ${c.bright("Options:")}
|
|
|
2040
2250
|
${c.bright("Examples:")}
|
|
2041
2251
|
artyfax save notes.md --category inbox
|
|
2042
2252
|
artyfax save --url https://example.com/article --category articles
|
|
2043
|
-
artyfax save report.md --secure --category reports
|
|
2253
|
+
artyfax save report.md --secure --category reports
|
|
2254
|
+
|
|
2255
|
+
${c.bright("Markdown:")} mermaid diagrams render (\`\`\`mermaid fences); colour flowchart
|
|
2256
|
+
nodes on-brand with \`class <node> amber|teal|blue|violet|rose\` rather than raw hex.
|
|
2257
|
+
Full syntax: the editor's Markdown Reference, or the seeded "Markdown Formatting" doc.`,
|
|
2044
2258
|
read: `${c.amber("artyfax read")} \u2014 read document content
|
|
2045
2259
|
|
|
2046
2260
|
${c.bright("Usage:")} artyfax read <slug>
|
|
@@ -2179,6 +2393,25 @@ ${c.bright("Usage:")} artyfax version list <slug> Show version history
|
|
|
2179
2393
|
${c.bright("Examples:")}
|
|
2180
2394
|
artyfax version list inbox/my-doc
|
|
2181
2395
|
artyfax version restore inbox/my-doc --yes`,
|
|
2396
|
+
publish: `${c.amber("artyfax publish")} \u2014 pin a published version
|
|
2397
|
+
|
|
2398
|
+
${c.bright("Usage:")} artyfax publish <doc> [version-id]
|
|
2399
|
+
|
|
2400
|
+
Public and shared views serve the pinned version while later edits stay draft
|
|
2401
|
+
until you publish again. With no version-id, publishes the current draft;
|
|
2402
|
+
pass a version id (from ${c.amber("artyfax version list")}) to publish an older state.
|
|
2403
|
+
|
|
2404
|
+
${c.bright("Examples:")}
|
|
2405
|
+
artyfax publish plans/my-post
|
|
2406
|
+
artyfax publish AX-12 1843`,
|
|
2407
|
+
unpublish: `${c.amber("artyfax unpublish")} \u2014 clear the published pointer
|
|
2408
|
+
|
|
2409
|
+
${c.bright("Usage:")} artyfax unpublish <doc>
|
|
2410
|
+
|
|
2411
|
+
Public and shared views revert to serving the live document.
|
|
2412
|
+
|
|
2413
|
+
${c.bright("Examples:")}
|
|
2414
|
+
artyfax unpublish plans/my-post`,
|
|
2182
2415
|
annotation: `${c.amber("artyfax annotation")} \u2014 read a document's annotations (highlights, underlines, strikethroughs, notes, comments, redactions)
|
|
2183
2416
|
|
|
2184
2417
|
${c.bright("Usage:")} artyfax annotation list <slug> List annotations (--include-recipients, --since <iso>)
|
|
@@ -2198,7 +2431,20 @@ ${c.bright("Usage:")} artyfax doctor [--json]`,
|
|
|
2198
2431
|
|
|
2199
2432
|
${c.bright("Usage:")} artyfax skill install [--project] Install Artyfax skill
|
|
2200
2433
|
artyfax skill status Check installation
|
|
2201
|
-
artyfax skill update Update to latest version
|
|
2434
|
+
artyfax skill update Update to latest version`,
|
|
2435
|
+
pages: `${c.amber("artyfax pages")} \u2014 list and reorder a parent document's child pages
|
|
2436
|
+
|
|
2437
|
+
${c.bright("Usage:")} artyfax pages list <parent> Show child pages in reading order
|
|
2438
|
+
artyfax pages order <parent> <child...> Set the full reading order
|
|
2439
|
+
artyfax pages move <child> before|after <target> Nudge one page past a sibling
|
|
2440
|
+
|
|
2441
|
+
Refs accept a document id, slug, or work id (e.g. ${c.muted("AX-12")}).
|
|
2442
|
+
${c.bright("order")} must name every visible child once; archived pages keep their order automatically.
|
|
2443
|
+
|
|
2444
|
+
${c.bright("Examples:")}
|
|
2445
|
+
artyfax pages list AX-94
|
|
2446
|
+
artyfax pages order AX-94 intro setup usage
|
|
2447
|
+
artyfax pages move usage before setup`
|
|
2202
2448
|
};
|
|
2203
2449
|
function parseArgs(args) {
|
|
2204
2450
|
const flags = {};
|
|
@@ -2237,10 +2483,35 @@ function parseArgs(args) {
|
|
|
2237
2483
|
}
|
|
2238
2484
|
return { command, subcommand, positional, flags };
|
|
2239
2485
|
}
|
|
2240
|
-
var SUB_RESOURCES = /* @__PURE__ */ new Set(["share", "cat", "tag", "version", "annotation", "skill", "snippets", "work", "project", "workspace"]);
|
|
2486
|
+
var SUB_RESOURCES = /* @__PURE__ */ new Set(["share", "cat", "tag", "version", "annotation", "skill", "snippets", "work", "project", "workspace", "pages"]);
|
|
2241
2487
|
function isSubResource(cmd) {
|
|
2242
2488
|
return SUB_RESOURCES.has(cmd);
|
|
2243
2489
|
}
|
|
2490
|
+
var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
2491
|
+
"init",
|
|
2492
|
+
"save",
|
|
2493
|
+
"read",
|
|
2494
|
+
"list",
|
|
2495
|
+
"get",
|
|
2496
|
+
"search",
|
|
2497
|
+
"delete",
|
|
2498
|
+
"open",
|
|
2499
|
+
"secure",
|
|
2500
|
+
"unsecure",
|
|
2501
|
+
"update",
|
|
2502
|
+
"metadata",
|
|
2503
|
+
"cover",
|
|
2504
|
+
"image",
|
|
2505
|
+
"publish",
|
|
2506
|
+
"unpublish",
|
|
2507
|
+
"doctor",
|
|
2508
|
+
"task",
|
|
2509
|
+
"tasks",
|
|
2510
|
+
...SUB_RESOURCES
|
|
2511
|
+
]);
|
|
2512
|
+
function isKnownCommand(command) {
|
|
2513
|
+
return KNOWN_COMMANDS.has(command);
|
|
2514
|
+
}
|
|
2244
2515
|
async function main() {
|
|
2245
2516
|
const parsed = parseArgs(process.argv.slice(2));
|
|
2246
2517
|
const { command, subcommand, positional, flags } = parsed;
|
|
@@ -2262,36 +2533,7 @@ async function main() {
|
|
|
2262
2533
|
console.log(brandedHelp());
|
|
2263
2534
|
process.exit(0);
|
|
2264
2535
|
}
|
|
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)) {
|
|
2536
|
+
if (!isKnownCommand(command)) {
|
|
2295
2537
|
error(`Unknown command: ${command}`, `Run \`artyfax--help\` for available commands`);
|
|
2296
2538
|
}
|
|
2297
2539
|
if (command === "skill") {
|
|
@@ -2422,6 +2664,24 @@ async function main() {
|
|
|
2422
2664
|
await open(config, slug);
|
|
2423
2665
|
break;
|
|
2424
2666
|
}
|
|
2667
|
+
case "publish": {
|
|
2668
|
+
const slug = positional[0];
|
|
2669
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax publish <doc> [version-id]");
|
|
2670
|
+
const versionArg = positional[1];
|
|
2671
|
+
let version;
|
|
2672
|
+
if (versionArg !== void 0) {
|
|
2673
|
+
version = Number(versionArg);
|
|
2674
|
+
if (!Number.isInteger(version)) error(`Invalid version id: ${versionArg}`, "Usage: artyfax publish <doc> [version-id]");
|
|
2675
|
+
}
|
|
2676
|
+
await publish(config, slug, { version, json });
|
|
2677
|
+
break;
|
|
2678
|
+
}
|
|
2679
|
+
case "unpublish": {
|
|
2680
|
+
const slug = positional[0];
|
|
2681
|
+
if (!slug) error("Missing slug argument", "Usage: artyfax unpublish <doc>");
|
|
2682
|
+
await unpublish(config, slug, json);
|
|
2683
|
+
break;
|
|
2684
|
+
}
|
|
2425
2685
|
case "secure": {
|
|
2426
2686
|
const slug = positional[0];
|
|
2427
2687
|
if (!slug) error("Missing slug argument", "Usage: artyfax secure <slug>");
|
|
@@ -2637,6 +2897,38 @@ async function main() {
|
|
|
2637
2897
|
}
|
|
2638
2898
|
break;
|
|
2639
2899
|
}
|
|
2900
|
+
case "pages": {
|
|
2901
|
+
switch (subcommand) {
|
|
2902
|
+
case "list": {
|
|
2903
|
+
const parent = positional[0];
|
|
2904
|
+
if (!parent) error("Missing parent", "Usage: artyfax pages list <parent>");
|
|
2905
|
+
await pagesList(config, parent, json);
|
|
2906
|
+
break;
|
|
2907
|
+
}
|
|
2908
|
+
case "order": {
|
|
2909
|
+
const parent = positional[0];
|
|
2910
|
+
const childRefs = positional.slice(1);
|
|
2911
|
+
if (!parent || childRefs.length === 0) {
|
|
2912
|
+
error("Missing arguments", "Usage: artyfax pages order <parent> <child...>");
|
|
2913
|
+
}
|
|
2914
|
+
await pagesOrder(config, parent, childRefs, json);
|
|
2915
|
+
break;
|
|
2916
|
+
}
|
|
2917
|
+
case "move": {
|
|
2918
|
+
const child = positional[0];
|
|
2919
|
+
const position = positional[1];
|
|
2920
|
+
const target = positional[2];
|
|
2921
|
+
if (!child || !position || !target) {
|
|
2922
|
+
error("Missing arguments", "Usage: artyfax pages move <child> before|after <target>");
|
|
2923
|
+
}
|
|
2924
|
+
await pagesMove(config, child, position, target, json);
|
|
2925
|
+
break;
|
|
2926
|
+
}
|
|
2927
|
+
default:
|
|
2928
|
+
error(`Unknown pages subcommand: ${subcommand || "(none)"}`, "Usage: artyfax pages <list|order|move>");
|
|
2929
|
+
}
|
|
2930
|
+
break;
|
|
2931
|
+
}
|
|
2640
2932
|
case "work": {
|
|
2641
2933
|
switch (subcommand) {
|
|
2642
2934
|
case "":
|
|
@@ -2673,8 +2965,13 @@ async function main() {
|
|
|
2673
2965
|
await workCreate(config, title, flags, json);
|
|
2674
2966
|
break;
|
|
2675
2967
|
}
|
|
2968
|
+
case "touch": {
|
|
2969
|
+
if (positional.length === 0) error("Missing work item ID(s)", "Usage: artyfax work touch <id...> [--action editing|building|reviewing|running]");
|
|
2970
|
+
await workTouch(config, positional, flags, json);
|
|
2971
|
+
break;
|
|
2972
|
+
}
|
|
2676
2973
|
default:
|
|
2677
|
-
error(`Unknown work subcommand: ${subcommand}`, "Usage: artyfax work <list|next|start|done|status|create>");
|
|
2974
|
+
error(`Unknown work subcommand: ${subcommand}`, "Usage: artyfax work <list|next|start|done|status|create|touch>");
|
|
2678
2975
|
}
|
|
2679
2976
|
break;
|
|
2680
2977
|
}
|
|
@@ -2732,5 +3029,6 @@ if (isDirectRun()) {
|
|
|
2732
3029
|
}
|
|
2733
3030
|
export {
|
|
2734
3031
|
VERSION,
|
|
3032
|
+
isKnownCommand,
|
|
2735
3033
|
parseArgs
|
|
2736
3034
|
};
|