@takuhon/cli 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ADMIN_DIST_DIRNAME,
|
|
4
4
|
resolveAdminBundleDir
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-YLSMA3YM.js";
|
|
6
6
|
|
|
7
7
|
// src/index.ts
|
|
8
|
-
import { readFileSync as
|
|
8
|
+
import { readFileSync as readFileSync16, realpathSync } from "fs";
|
|
9
9
|
import { stdin, stdout } from "process";
|
|
10
10
|
import { createInterface } from "readline/promises";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
@@ -355,18 +355,32 @@ import { applyPublicPrivacyFilter, normalize, validate } from "@takuhon/core";
|
|
|
355
355
|
|
|
356
356
|
// src/build-html.ts
|
|
357
357
|
import { generateJsonLd, renderActivitySvg } from "@takuhon/core";
|
|
358
|
+
|
|
359
|
+
// src/html-helpers.ts
|
|
358
360
|
function escapeHtml(value) {
|
|
359
361
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
360
362
|
}
|
|
361
|
-
function escapeJsonLd(json) {
|
|
362
|
-
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
363
|
-
}
|
|
364
363
|
function safeUrl(url) {
|
|
365
364
|
const trimmed = url.trim();
|
|
366
365
|
const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed)?.[1]?.toLowerCase();
|
|
367
366
|
if (scheme === void 0) return trimmed;
|
|
368
367
|
return scheme === "http" || scheme === "https" || scheme === "mailto" ? trimmed : void 0;
|
|
369
368
|
}
|
|
369
|
+
function dateRange(start, end, isCurrent) {
|
|
370
|
+
const left = start ?? "";
|
|
371
|
+
const right = isCurrent === true || end === null ? "Present" : end ?? "";
|
|
372
|
+
if (left && right) return `${left} \u2013 ${right}`;
|
|
373
|
+
return left || right;
|
|
374
|
+
}
|
|
375
|
+
function nonEmpty(values, separator) {
|
|
376
|
+
const joined = values.filter((v) => typeof v === "string" && v.length > 0).join(separator);
|
|
377
|
+
return joined.length > 0 ? joined : void 0;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// src/build-html.ts
|
|
381
|
+
function escapeJsonLd(json) {
|
|
382
|
+
return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
|
|
383
|
+
}
|
|
370
384
|
var CSS = `:root{--fg:#1a1a1a;--muted:#666;--accent:#0b5fff;--line:#e5e5e5}
|
|
371
385
|
*{box-sizing:border-box}
|
|
372
386
|
body{margin:0;color:var(--fg);font:16px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:#fff}
|
|
@@ -392,16 +406,6 @@ ul{padding:0;margin:0;list-style:none}
|
|
|
392
406
|
nav.locales{display:flex;gap:.75rem;margin-bottom:1rem;font-size:.9rem}
|
|
393
407
|
.activity svg{max-width:100%;height:auto}
|
|
394
408
|
footer.powered{max-width:42rem;margin:0 auto;padding:1.5rem 1.25rem;color:var(--muted);font-size:.85rem}`;
|
|
395
|
-
function dateRange(start, end, isCurrent) {
|
|
396
|
-
const left = start ?? "";
|
|
397
|
-
const right = isCurrent === true || end === null ? "Present" : end ?? "";
|
|
398
|
-
if (left && right) return `${left} \u2013 ${right}`;
|
|
399
|
-
return left || right;
|
|
400
|
-
}
|
|
401
|
-
function nonEmpty(values, separator) {
|
|
402
|
-
const joined = values.filter((v) => typeof v === "string" && v.length > 0).join(separator);
|
|
403
|
-
return joined.length > 0 ? joined : void 0;
|
|
404
|
-
}
|
|
405
409
|
function renderEntry(entry) {
|
|
406
410
|
const href = entry.url ? safeUrl(entry.url) : void 0;
|
|
407
411
|
const heading = href ? `<a href="${escapeHtml(href)}">${escapeHtml(entry.heading)}</a>` : escapeHtml(entry.heading);
|
|
@@ -648,14 +652,226 @@ ${footer ? `${footer}
|
|
|
648
652
|
}
|
|
649
653
|
|
|
650
654
|
// src/site.ts
|
|
651
|
-
import { resolveLocale } from "@takuhon/core";
|
|
655
|
+
import { deriveCv, resolveLocale } from "@takuhon/core";
|
|
656
|
+
|
|
657
|
+
// src/cv-html.ts
|
|
658
|
+
var SECTION_TITLES = {
|
|
659
|
+
experience: "Experience",
|
|
660
|
+
education: "Education",
|
|
661
|
+
skills: "Skills",
|
|
662
|
+
certifications: "Certifications",
|
|
663
|
+
publications: "Publications",
|
|
664
|
+
honors: "Honors & Awards",
|
|
665
|
+
courses: "Courses",
|
|
666
|
+
patents: "Patents",
|
|
667
|
+
languages: "Languages",
|
|
668
|
+
volunteering: "Volunteering",
|
|
669
|
+
memberships: "Memberships"
|
|
670
|
+
};
|
|
671
|
+
var CSS2 = `:root{--fg:#1a1a1a;--muted:#555;--accent:#0b5fff;--line:#d9d9d9}
|
|
672
|
+
*{box-sizing:border-box}
|
|
673
|
+
body{margin:0;background:#f3f4f6;color:var(--fg);font:13px/1.5 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif}
|
|
674
|
+
main{background:#fff;width:210mm;min-height:297mm;margin:1.5rem auto;padding:18mm 16mm;box-shadow:0 1px 6px rgba(0,0,0,.15)}
|
|
675
|
+
h1{font-size:1.7rem;margin:0}
|
|
676
|
+
.tagline{font-size:1.05rem;color:var(--muted);margin:.15rem 0 0}
|
|
677
|
+
.contact{color:var(--muted);font-size:.85rem;margin:.4rem 0 0;display:flex;flex-wrap:wrap;gap:.25rem 1rem}
|
|
678
|
+
.contact a{color:var(--accent)}
|
|
679
|
+
.bio{margin:.75rem 0 0}
|
|
680
|
+
header{border-bottom:2px solid var(--fg);padding-bottom:.6rem;margin-bottom:.4rem}
|
|
681
|
+
section{margin-top:1.1rem}
|
|
682
|
+
h2{font-size:.95rem;text-transform:uppercase;letter-spacing:.05em;color:var(--accent);border-bottom:1px solid var(--line);margin:0 0 .5rem;padding-bottom:.15rem}
|
|
683
|
+
ul{padding:0;margin:0;list-style:none}
|
|
684
|
+
.entry{margin:0 0 .7rem;break-inside:avoid}
|
|
685
|
+
.entry .row{display:flex;justify-content:space-between;gap:1rem;align-items:baseline}
|
|
686
|
+
.entry h3{font-size:.95rem;margin:0}
|
|
687
|
+
.entry .dates{color:var(--muted);font-size:.8rem;white-space:nowrap}
|
|
688
|
+
.entry .sub{color:var(--muted);margin:.05rem 0}
|
|
689
|
+
.entry p{margin:.2rem 0 0}
|
|
690
|
+
.entry a{color:var(--accent)}
|
|
691
|
+
.chips{display:flex;flex-wrap:wrap;gap:.3rem}
|
|
692
|
+
.chips li{border:1px solid var(--line);border-radius:1rem;padding:.05rem .55rem;font-size:.8rem}
|
|
693
|
+
@media print{
|
|
694
|
+
body{background:#fff}
|
|
695
|
+
main{width:auto;min-height:0;margin:0;padding:0;box-shadow:none}
|
|
696
|
+
a{color:var(--fg)}
|
|
697
|
+
}
|
|
698
|
+
@page{size:A4;margin:14mm}`;
|
|
699
|
+
function renderEntry2(entry) {
|
|
700
|
+
const href = entry.url ? safeUrl(entry.url) : void 0;
|
|
701
|
+
const title = href ? `<a href="${escapeHtml(href)}">${escapeHtml(entry.heading)}</a>` : escapeHtml(entry.heading);
|
|
702
|
+
const dates = entry.dates ? `<span class="dates">${escapeHtml(entry.dates)}</span>` : "";
|
|
703
|
+
const parts = [`<div class="row"><h3>${title}</h3>${dates}</div>`];
|
|
704
|
+
if (entry.sub) parts.push(`<p class="sub">${escapeHtml(entry.sub)}</p>`);
|
|
705
|
+
if (entry.body) parts.push(`<p>${escapeHtml(entry.body)}</p>`);
|
|
706
|
+
return `<li class="entry">${parts.join("")}</li>`;
|
|
707
|
+
}
|
|
708
|
+
function entryListSection(title, entries) {
|
|
709
|
+
return `<section><h2>${escapeHtml(title)}</h2><ul>${entries.map(renderEntry2).join("")}</ul></section>`;
|
|
710
|
+
}
|
|
711
|
+
function chipSection(title, labels) {
|
|
712
|
+
const chips = labels.map((l) => `<li>${escapeHtml(l)}</li>`).join("");
|
|
713
|
+
return `<section><h2>${escapeHtml(title)}</h2><ul class="chips">${chips}</ul></section>`;
|
|
714
|
+
}
|
|
715
|
+
function languageLabel(l) {
|
|
716
|
+
return `${l.displayName ?? l.language} \u2014 ${l.proficiency}`;
|
|
717
|
+
}
|
|
718
|
+
function renderSection(section) {
|
|
719
|
+
const title = SECTION_TITLES[section.kind];
|
|
720
|
+
switch (section.kind) {
|
|
721
|
+
case "experience":
|
|
722
|
+
return entryListSection(
|
|
723
|
+
title,
|
|
724
|
+
section.entries.map((c) => ({
|
|
725
|
+
heading: c.role,
|
|
726
|
+
sub: c.organization,
|
|
727
|
+
dates: dateRange(c.startDate, c.endDate, c.isCurrent),
|
|
728
|
+
body: c.description,
|
|
729
|
+
url: c.url
|
|
730
|
+
}))
|
|
731
|
+
);
|
|
732
|
+
case "education":
|
|
733
|
+
return entryListSection(
|
|
734
|
+
title,
|
|
735
|
+
section.entries.map((e) => {
|
|
736
|
+
const degree = nonEmpty([e.degree, e.fieldOfStudy], ", ");
|
|
737
|
+
return {
|
|
738
|
+
heading: degree ?? e.institution,
|
|
739
|
+
sub: degree ? e.institution : void 0,
|
|
740
|
+
dates: dateRange(e.startDate, e.endDate, e.isCurrent),
|
|
741
|
+
body: e.description,
|
|
742
|
+
url: e.url
|
|
743
|
+
};
|
|
744
|
+
})
|
|
745
|
+
);
|
|
746
|
+
case "skills":
|
|
747
|
+
return chipSection(
|
|
748
|
+
title,
|
|
749
|
+
section.entries.map((s) => s.label)
|
|
750
|
+
);
|
|
751
|
+
case "languages":
|
|
752
|
+
return chipSection(title, section.entries.map(languageLabel));
|
|
753
|
+
case "certifications":
|
|
754
|
+
return entryListSection(
|
|
755
|
+
title,
|
|
756
|
+
section.entries.map((c) => ({
|
|
757
|
+
heading: c.title,
|
|
758
|
+
sub: c.issuingOrganization,
|
|
759
|
+
dates: dateRange(c.issueDate, c.expirationDate),
|
|
760
|
+
url: c.url
|
|
761
|
+
}))
|
|
762
|
+
);
|
|
763
|
+
case "publications":
|
|
764
|
+
return entryListSection(
|
|
765
|
+
title,
|
|
766
|
+
section.entries.map((x) => ({
|
|
767
|
+
heading: x.title,
|
|
768
|
+
sub: nonEmpty([x.publisher, x.coAuthors?.join(", ")], " \xB7 "),
|
|
769
|
+
dates: dateRange(x.date),
|
|
770
|
+
body: x.description,
|
|
771
|
+
url: x.url ?? (x.doi ? `https://doi.org/${x.doi}` : void 0)
|
|
772
|
+
}))
|
|
773
|
+
);
|
|
774
|
+
case "honors":
|
|
775
|
+
return entryListSection(
|
|
776
|
+
title,
|
|
777
|
+
section.entries.map((x) => ({
|
|
778
|
+
heading: x.title,
|
|
779
|
+
sub: x.issuer,
|
|
780
|
+
dates: dateRange(x.date),
|
|
781
|
+
body: x.description,
|
|
782
|
+
url: x.url
|
|
783
|
+
}))
|
|
784
|
+
);
|
|
785
|
+
case "courses":
|
|
786
|
+
return entryListSection(
|
|
787
|
+
title,
|
|
788
|
+
section.entries.map((x) => ({
|
|
789
|
+
heading: x.title,
|
|
790
|
+
sub: x.provider,
|
|
791
|
+
dates: dateRange(x.completionDate),
|
|
792
|
+
body: x.description,
|
|
793
|
+
url: x.certificateUrl
|
|
794
|
+
}))
|
|
795
|
+
);
|
|
796
|
+
case "patents":
|
|
797
|
+
return entryListSection(
|
|
798
|
+
title,
|
|
799
|
+
section.entries.map((x) => ({
|
|
800
|
+
heading: x.title,
|
|
801
|
+
sub: nonEmpty([x.patentNumber, x.office, x.status], " \xB7 "),
|
|
802
|
+
dates: dateRange(x.filingDate ?? x.grantDate),
|
|
803
|
+
body: x.description,
|
|
804
|
+
url: x.url
|
|
805
|
+
}))
|
|
806
|
+
);
|
|
807
|
+
case "volunteering":
|
|
808
|
+
return entryListSection(
|
|
809
|
+
title,
|
|
810
|
+
section.entries.map((x) => ({
|
|
811
|
+
heading: x.role,
|
|
812
|
+
sub: nonEmpty([x.organization, x.cause], " \xB7 "),
|
|
813
|
+
dates: dateRange(x.startDate, x.endDate, x.isCurrent),
|
|
814
|
+
body: x.description,
|
|
815
|
+
url: x.url
|
|
816
|
+
}))
|
|
817
|
+
);
|
|
818
|
+
case "memberships":
|
|
819
|
+
return entryListSection(
|
|
820
|
+
title,
|
|
821
|
+
section.entries.map((x) => ({
|
|
822
|
+
heading: x.role ?? x.organization,
|
|
823
|
+
sub: x.role ? x.organization : void 0,
|
|
824
|
+
dates: dateRange(x.startDate, x.endDate, x.isCurrent),
|
|
825
|
+
body: x.description,
|
|
826
|
+
url: x.url
|
|
827
|
+
}))
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function renderHeader2(cv) {
|
|
832
|
+
const h = cv.header;
|
|
833
|
+
const parts = [`<h1>${escapeHtml(h.displayName)}</h1>`];
|
|
834
|
+
if (h.tagline) parts.push(`<p class="tagline">${escapeHtml(h.tagline)}</p>`);
|
|
835
|
+
const contact = [];
|
|
836
|
+
if (h.location) contact.push(`<span>${escapeHtml(h.location)}</span>`);
|
|
837
|
+
if (h.email) {
|
|
838
|
+
contact.push(`<a href="mailto:${escapeHtml(h.email)}">${escapeHtml(h.email)}</a>`);
|
|
839
|
+
}
|
|
840
|
+
const formHref = h.formUrl ? safeUrl(h.formUrl) : void 0;
|
|
841
|
+
if (formHref) contact.push(`<a href="${escapeHtml(formHref)}">Contact</a>`);
|
|
842
|
+
if (contact.length > 0) parts.push(`<div class="contact">${contact.join("")}</div>`);
|
|
843
|
+
if (h.bio) parts.push(`<p class="bio">${escapeHtml(h.bio)}</p>`);
|
|
844
|
+
return `<header>${parts.join("")}</header>`;
|
|
845
|
+
}
|
|
846
|
+
function renderCvHtml(cv) {
|
|
847
|
+
const title = `${cv.header.displayName} \u2014 CV`;
|
|
848
|
+
const body = [renderHeader2(cv), ...cv.sections.map(renderSection)].join("\n");
|
|
849
|
+
return `<!DOCTYPE html>
|
|
850
|
+
<html lang="${escapeHtml(cv.resolvedLocale)}">
|
|
851
|
+
<head>
|
|
852
|
+
<meta charset="utf-8">
|
|
853
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
854
|
+
<title>${escapeHtml(title)}</title>
|
|
855
|
+
<style>${CSS2}</style>
|
|
856
|
+
</head>
|
|
857
|
+
<body>
|
|
858
|
+
<main>
|
|
859
|
+
${body}
|
|
860
|
+
</main>
|
|
861
|
+
</body>
|
|
862
|
+
</html>
|
|
863
|
+
`;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/site.ts
|
|
652
867
|
function generateSite(profile, options = {}) {
|
|
653
868
|
const { baseUrl } = options;
|
|
654
869
|
const defaultLocale = profile.settings.defaultLocale;
|
|
655
870
|
const locales = [.../* @__PURE__ */ new Set([defaultLocale, ...profile.settings.availableLocales])];
|
|
656
871
|
const jsonLd = profile.settings.enableJsonLd !== false;
|
|
657
872
|
const activitySnapshot = profile.settings.activity?.enabled === true ? options.activitySnapshot ?? void 0 : void 0;
|
|
658
|
-
|
|
873
|
+
const pages = [];
|
|
874
|
+
for (const locale of locales) {
|
|
659
875
|
const localized = resolveLocale(profile, locale);
|
|
660
876
|
const isDefault = locale === defaultLocale;
|
|
661
877
|
const localeNav = locales.map((to) => ({
|
|
@@ -673,12 +889,20 @@ function generateSite(profile, options = {}) {
|
|
|
673
889
|
jsonLd,
|
|
674
890
|
activitySnapshot
|
|
675
891
|
});
|
|
676
|
-
|
|
892
|
+
pages.push({
|
|
677
893
|
route: isDefault ? "/" : `/${locale}/`,
|
|
678
894
|
file: isDefault ? "index.html" : `${locale}/index.html`,
|
|
679
895
|
html
|
|
680
|
-
};
|
|
681
|
-
|
|
896
|
+
});
|
|
897
|
+
if (options.cv === true) {
|
|
898
|
+
pages.push({
|
|
899
|
+
route: isDefault ? "/cv/" : `/${locale}/cv/`,
|
|
900
|
+
file: isDefault ? "cv.html" : `${locale}/cv.html`,
|
|
901
|
+
html: renderCvHtml(deriveCv(localized))
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return pages;
|
|
682
906
|
}
|
|
683
907
|
function absoluteUrl(baseUrl, locale, defaultLocale) {
|
|
684
908
|
return locale === defaultLocale ? `${baseUrl}/` : `${baseUrl}/${locale}/`;
|
|
@@ -706,10 +930,10 @@ var DEFAULT_PATH2 = "takuhon.json";
|
|
|
706
930
|
var DEFAULT_PORT = 4321;
|
|
707
931
|
var USAGE = `Usage: takuhon dev [path] [--port <n>] [--base-url <url>]
|
|
708
932
|
|
|
709
|
-
Serve a takuhon.json as a local static preview (one page per locale
|
|
710
|
-
surface \`takuhon build\` produces. With
|
|
711
|
-
is re-read and re-rendered on every
|
|
712
|
-
to see changes. Stop with Ctrl-C.
|
|
933
|
+
Serve a takuhon.json as a local static preview (one page per locale, plus a
|
|
934
|
+
print-ready CV page at /cv) \u2014 the same surface \`takuhon build\` produces. With
|
|
935
|
+
no path, serves ./takuhon.json. The file is re-read and re-rendered on every
|
|
936
|
+
request, so edit it and reload the browser to see changes. Stop with Ctrl-C.
|
|
713
937
|
|
|
714
938
|
Options:
|
|
715
939
|
--port <n> Port to listen on (default: ${DEFAULT_PORT}).
|
|
@@ -749,7 +973,7 @@ ${lines.join("\n")}`
|
|
|
749
973
|
const filtered = applyPublicPrivacyFilter(normalize(result.data));
|
|
750
974
|
const activitySnapshot = filtered.settings.activity?.enabled === true ? readActivitySnapshotSync(path) : null;
|
|
751
975
|
const pages = new Map(
|
|
752
|
-
generateSite(filtered, { baseUrl, activitySnapshot }).map((p) => [p.route, p.html])
|
|
976
|
+
generateSite(filtered, { baseUrl, activitySnapshot, cv: true }).map((p) => [p.route, p.html])
|
|
753
977
|
);
|
|
754
978
|
return { ok: true, pages };
|
|
755
979
|
}
|
|
@@ -1472,7 +1696,7 @@ import { dirname as dirname5, join as join6 } from "path";
|
|
|
1472
1696
|
import { applyPublicPrivacyFilter as applyPublicPrivacyFilter2, normalize as normalize2, validate as validate2 } from "@takuhon/core";
|
|
1473
1697
|
var DEFAULT_PATH4 = "takuhon.json";
|
|
1474
1698
|
var DEFAULT_OUTPUT = "dist";
|
|
1475
|
-
var USAGE3 = `Usage: takuhon build [path] [--output <dir>] [--base-url <url>]
|
|
1699
|
+
var USAGE3 = `Usage: takuhon build [path] [--output <dir>] [--base-url <url>] [--cv]
|
|
1476
1700
|
|
|
1477
1701
|
Render a takuhon.json into a static site (one HTML page per locale, with
|
|
1478
1702
|
build-time Schema.org JSON-LD). With no path, builds ./takuhon.json.
|
|
@@ -1483,6 +1707,9 @@ Options:
|
|
|
1483
1707
|
to <dir>/<locale>/index.html.
|
|
1484
1708
|
--base-url <url> Site origin (e.g. https://me.example). Enables absolute
|
|
1485
1709
|
canonical and hreflang links; without it those are omitted.
|
|
1710
|
+
--cv Also emit a print-ready CV/r\xE9sum\xE9 page per locale
|
|
1711
|
+
(<dir>/cv.html and <dir>/<locale>/cv.html). Open it and use
|
|
1712
|
+
the browser's "Save as PDF" to produce a r\xE9sum\xE9 PDF.
|
|
1486
1713
|
|
|
1487
1714
|
The public privacy filter is applied (meta.privacy is honoured). Asset URLs are
|
|
1488
1715
|
referenced as-is and are not copied. The output directory is written into, not
|
|
@@ -1511,8 +1738,13 @@ function parseArgs3(args) {
|
|
|
1511
1738
|
let path;
|
|
1512
1739
|
let output;
|
|
1513
1740
|
let baseUrl;
|
|
1741
|
+
let cv = false;
|
|
1514
1742
|
for (let i = 0; i < args.length; i++) {
|
|
1515
1743
|
const arg = args[i];
|
|
1744
|
+
if (arg === "--cv") {
|
|
1745
|
+
cv = true;
|
|
1746
|
+
continue;
|
|
1747
|
+
}
|
|
1516
1748
|
if (arg === "--output" || arg === "--base-url") {
|
|
1517
1749
|
const value = args[i + 1];
|
|
1518
1750
|
if (value === void 0 || value === "" || value.startsWith("-")) {
|
|
@@ -1550,7 +1782,8 @@ function parseArgs3(args) {
|
|
|
1550
1782
|
path: path ?? DEFAULT_PATH4,
|
|
1551
1783
|
output: output ?? DEFAULT_OUTPUT,
|
|
1552
1784
|
// Drop any trailing slash so URL joins are predictable.
|
|
1553
|
-
baseUrl: baseUrl?.replace(/\/+$/, "")
|
|
1785
|
+
baseUrl: baseUrl?.replace(/\/+$/, ""),
|
|
1786
|
+
cv
|
|
1554
1787
|
};
|
|
1555
1788
|
}
|
|
1556
1789
|
function isHttpUrl3(value) {
|
|
@@ -1562,7 +1795,7 @@ function isHttpUrl3(value) {
|
|
|
1562
1795
|
}
|
|
1563
1796
|
}
|
|
1564
1797
|
function buildSite(parsed) {
|
|
1565
|
-
const { path, output, baseUrl } = parsed;
|
|
1798
|
+
const { path, output, baseUrl, cv } = parsed;
|
|
1566
1799
|
let raw;
|
|
1567
1800
|
try {
|
|
1568
1801
|
raw = readFileSync7(path, "utf8");
|
|
@@ -1597,7 +1830,7 @@ ${lines.join("\n")}
|
|
|
1597
1830
|
const activitySnapshot = filtered.settings.activity?.enabled === true ? readActivitySnapshotSync(path) : null;
|
|
1598
1831
|
const written = [];
|
|
1599
1832
|
try {
|
|
1600
|
-
for (const page of generateSite(filtered, { baseUrl, activitySnapshot })) {
|
|
1833
|
+
for (const page of generateSite(filtered, { baseUrl, activitySnapshot, cv })) {
|
|
1601
1834
|
const outFile = join6(output, page.file);
|
|
1602
1835
|
mkdirSync3(dirname5(outFile), { recursive: true });
|
|
1603
1836
|
writeFileAtomic(outFile, page.html);
|
|
@@ -1914,18 +2147,144 @@ function isNotFound5(error) {
|
|
|
1914
2147
|
return typeof error === "object" && error !== null && error.code === "ENOENT";
|
|
1915
2148
|
}
|
|
1916
2149
|
|
|
1917
|
-
// src/
|
|
2150
|
+
// src/mcp-command.ts
|
|
1918
2151
|
import { readFileSync as readFileSync10 } from "fs";
|
|
2152
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2153
|
+
import { validate as validate4 } from "@takuhon/core";
|
|
2154
|
+
import { createTakuhonMcpServer } from "@takuhon/mcp";
|
|
2155
|
+
var DEFAULT_PATH7 = "takuhon.json";
|
|
2156
|
+
var SERVER_NAME = "takuhon";
|
|
2157
|
+
var USAGE6 = `Usage: takuhon mcp [path]
|
|
2158
|
+
|
|
2159
|
+
Serve a takuhon.json over the Model Context Protocol on stdio, so an MCP client
|
|
2160
|
+
(e.g. Claude Desktop) can read the profile read-only. With no path, serves
|
|
2161
|
+
./takuhon.json. The file is re-read on every request, so edits are reflected
|
|
2162
|
+
without restarting.
|
|
2163
|
+
|
|
2164
|
+
Exposes (read-only, the same surface as the public API, privacy filter applied):
|
|
2165
|
+
tools: get_profile, get_section, get_jsonld, list_locales
|
|
2166
|
+
resources: takuhon://profile, takuhon://schema
|
|
2167
|
+
|
|
2168
|
+
stdin/stdout carry the protocol stream; diagnostics go to stderr. The server
|
|
2169
|
+
runs until the client disconnects or you press Ctrl-C.
|
|
2170
|
+
|
|
2171
|
+
Example Claude Desktop config (claude_desktop_config.json):
|
|
2172
|
+
{
|
|
2173
|
+
"mcpServers": {
|
|
2174
|
+
"my-profile": { "command": "takuhon", "args": ["mcp", "/path/to/takuhon.json"] }
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
Exit codes: 0 = served then stopped, 2 = bad arguments / file missing /
|
|
2179
|
+
unreadable / invalid.
|
|
2180
|
+
`;
|
|
2181
|
+
var VERSION = readCliVersion();
|
|
2182
|
+
function loadMcpProfile(path) {
|
|
2183
|
+
let raw;
|
|
2184
|
+
try {
|
|
2185
|
+
raw = readFileSync10(path, "utf8");
|
|
2186
|
+
} catch {
|
|
2187
|
+
throw new Error(`cannot read '${path}'.`);
|
|
2188
|
+
}
|
|
2189
|
+
let data;
|
|
2190
|
+
try {
|
|
2191
|
+
data = JSON.parse(raw);
|
|
2192
|
+
} catch (error) {
|
|
2193
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2194
|
+
throw new Error(`'${path}' is not valid JSON: ${detail}`, { cause: error });
|
|
2195
|
+
}
|
|
2196
|
+
const result = validate4(data);
|
|
2197
|
+
if (!result.ok) {
|
|
2198
|
+
const lines = result.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
|
|
2199
|
+
throw new Error(`'${path}' is not a valid takuhon profile:
|
|
2200
|
+
${lines.join("\n")}`);
|
|
2201
|
+
}
|
|
2202
|
+
return result.data;
|
|
2203
|
+
}
|
|
2204
|
+
async function runMcp(args = [], deps = {}) {
|
|
2205
|
+
const err = deps.stderr ?? ((text) => void process.stderr.write(text));
|
|
2206
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
2207
|
+
const out = deps.stdout ?? ((text) => void process.stdout.write(text));
|
|
2208
|
+
out(USAGE6);
|
|
2209
|
+
return 0;
|
|
2210
|
+
}
|
|
2211
|
+
const parsed = parseArgs6(args);
|
|
2212
|
+
if ("error" in parsed) {
|
|
2213
|
+
err(`${parsed.error}
|
|
2214
|
+
Run \`takuhon mcp --help\` for usage.
|
|
2215
|
+
`);
|
|
2216
|
+
return 2;
|
|
2217
|
+
}
|
|
2218
|
+
try {
|
|
2219
|
+
loadMcpProfile(parsed.path);
|
|
2220
|
+
} catch (error) {
|
|
2221
|
+
err(`takuhon: ${error instanceof Error ? error.message : String(error)}
|
|
2222
|
+
`);
|
|
2223
|
+
return 2;
|
|
2224
|
+
}
|
|
2225
|
+
const server = createTakuhonMcpServer({
|
|
2226
|
+
// Re-read per request so edits are reflected; if the file later becomes
|
|
2227
|
+
// invalid, the load failure surfaces to the client as a tool error result.
|
|
2228
|
+
loadProfile: () => loadMcpProfile(parsed.path),
|
|
2229
|
+
name: SERVER_NAME,
|
|
2230
|
+
version: VERSION
|
|
2231
|
+
});
|
|
2232
|
+
const transport = (deps.createTransport ?? (() => new StdioServerTransport()))();
|
|
2233
|
+
await server.connect(transport);
|
|
2234
|
+
err(`takuhon mcp: serving ${parsed.path} (read-only) on stdio
|
|
2235
|
+
`);
|
|
2236
|
+
return await new Promise((resolve7) => {
|
|
2237
|
+
let done = false;
|
|
2238
|
+
function finish() {
|
|
2239
|
+
if (done) return;
|
|
2240
|
+
done = true;
|
|
2241
|
+
process.removeListener("SIGINT", shutdown);
|
|
2242
|
+
process.removeListener("SIGTERM", shutdown);
|
|
2243
|
+
resolve7(0);
|
|
2244
|
+
}
|
|
2245
|
+
function shutdown() {
|
|
2246
|
+
void server.close();
|
|
2247
|
+
finish();
|
|
2248
|
+
}
|
|
2249
|
+
server.server.onclose = finish;
|
|
2250
|
+
process.once("SIGINT", shutdown);
|
|
2251
|
+
process.once("SIGTERM", shutdown);
|
|
2252
|
+
});
|
|
2253
|
+
}
|
|
2254
|
+
function parseArgs6(args) {
|
|
2255
|
+
let path;
|
|
2256
|
+
for (const arg of args) {
|
|
2257
|
+
if (arg.startsWith("-")) {
|
|
2258
|
+
return { error: `takuhon: unknown option \`${arg}\` for \`mcp\`.` };
|
|
2259
|
+
}
|
|
2260
|
+
if (path !== void 0) {
|
|
2261
|
+
return { error: "takuhon: `mcp` takes at most one path argument." };
|
|
2262
|
+
}
|
|
2263
|
+
path = arg;
|
|
2264
|
+
}
|
|
2265
|
+
return { path: path ?? DEFAULT_PATH7 };
|
|
2266
|
+
}
|
|
2267
|
+
function readCliVersion() {
|
|
2268
|
+
try {
|
|
2269
|
+
const pkg2 = JSON.parse(readFileSync10(new URL("../package.json", import.meta.url), "utf8"));
|
|
2270
|
+
return pkg2.version;
|
|
2271
|
+
} catch {
|
|
2272
|
+
return "0.0.0";
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
// src/migrate-command.ts
|
|
2277
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
1919
2278
|
import { resolve as resolve5 } from "path";
|
|
1920
2279
|
import {
|
|
1921
2280
|
MigrationError as MigrationError2,
|
|
1922
2281
|
SCHEMA_VERSION as SCHEMA_VERSION2,
|
|
1923
2282
|
SUPPORTED_SCHEMA_VERSIONS,
|
|
1924
2283
|
migrateTakuhon as migrateTakuhon2,
|
|
1925
|
-
validate as
|
|
2284
|
+
validate as validate5
|
|
1926
2285
|
} from "@takuhon/core";
|
|
1927
|
-
var
|
|
1928
|
-
var
|
|
2286
|
+
var DEFAULT_PATH8 = "takuhon.json";
|
|
2287
|
+
var USAGE7 = `Usage: takuhon migrate [path] [--to <version>] [--out <file>] [--dry-run]
|
|
1929
2288
|
|
|
1930
2289
|
Forward-migrate a takuhon.json to a newer schema version. The source version
|
|
1931
2290
|
is read from the file's own schemaVersion. With no path, migrates
|
|
@@ -1947,9 +2306,9 @@ Exit codes: 0 = migrated / already current / dry-run, 1 = cannot migrate,
|
|
|
1947
2306
|
`;
|
|
1948
2307
|
function runMigrate(args = [], deps = {}) {
|
|
1949
2308
|
if (args[0] === "--help" || args[0] === "-h") {
|
|
1950
|
-
return { code: 0, stdout:
|
|
2309
|
+
return { code: 0, stdout: USAGE7, stderr: "" };
|
|
1951
2310
|
}
|
|
1952
|
-
const parsed =
|
|
2311
|
+
const parsed = parseArgs7(args);
|
|
1953
2312
|
if ("error" in parsed) {
|
|
1954
2313
|
return {
|
|
1955
2314
|
code: 2,
|
|
@@ -1962,7 +2321,7 @@ Run \`takuhon migrate --help\` for usage.
|
|
|
1962
2321
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
1963
2322
|
return migrateFile(parsed, now);
|
|
1964
2323
|
}
|
|
1965
|
-
function
|
|
2324
|
+
function parseArgs7(args) {
|
|
1966
2325
|
let path;
|
|
1967
2326
|
let to;
|
|
1968
2327
|
let out;
|
|
@@ -2005,13 +2364,13 @@ function parseArgs6(args) {
|
|
|
2005
2364
|
error: `takuhon: unsupported --to version "${target}". Supported: ${SUPPORTED_SCHEMA_VERSIONS.join(", ")}.`
|
|
2006
2365
|
};
|
|
2007
2366
|
}
|
|
2008
|
-
return { path: path ??
|
|
2367
|
+
return { path: path ?? DEFAULT_PATH8, to: target, out, dryRun };
|
|
2009
2368
|
}
|
|
2010
2369
|
function migrateFile(parsed, now) {
|
|
2011
2370
|
const { path, to: target, out, dryRun } = parsed;
|
|
2012
2371
|
let raw;
|
|
2013
2372
|
try {
|
|
2014
|
-
raw =
|
|
2373
|
+
raw = readFileSync11(path, "utf8");
|
|
2015
2374
|
} catch {
|
|
2016
2375
|
return {
|
|
2017
2376
|
code: 2,
|
|
@@ -2062,7 +2421,7 @@ Migrations are forward-only; to move to an older schema, restore from a backup w
|
|
|
2062
2421
|
}
|
|
2063
2422
|
throw error;
|
|
2064
2423
|
}
|
|
2065
|
-
const revalidated =
|
|
2424
|
+
const revalidated = validate5(migrated);
|
|
2066
2425
|
if (!revalidated.ok) {
|
|
2067
2426
|
const lines2 = revalidated.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
|
|
2068
2427
|
return {
|
|
@@ -2125,11 +2484,11 @@ ${lines2.join("\n")}
|
|
|
2125
2484
|
}
|
|
2126
2485
|
|
|
2127
2486
|
// src/refresh-admin-command.ts
|
|
2128
|
-
import { readFileSync as
|
|
2487
|
+
import { readFileSync as readFileSync12 } from "fs";
|
|
2129
2488
|
import { cp, mkdtemp, readdir, rename, rm, stat } from "fs/promises";
|
|
2130
2489
|
import { join as join7, resolve as resolve6 } from "path";
|
|
2131
2490
|
var PROFILE_FILENAME = "takuhon.json";
|
|
2132
|
-
var
|
|
2491
|
+
var USAGE8 = `Usage: takuhon admin update [path]
|
|
2133
2492
|
|
|
2134
2493
|
Refresh a project's ${ADMIN_DIST_DIRNAME}/ with the admin form-UI bundle shipped in
|
|
2135
2494
|
this @takuhon/cli. Use it after upgrading @takuhon/cli to pick up a newer admin
|
|
@@ -2142,7 +2501,7 @@ no ${ADMIN_DIST_DIRNAME}/ to refresh / copy failed.
|
|
|
2142
2501
|
`;
|
|
2143
2502
|
async function runRefreshAdmin(args = [], opts = {}) {
|
|
2144
2503
|
if (args[0] === "--help" || args[0] === "-h") {
|
|
2145
|
-
return { code: 0, stdout:
|
|
2504
|
+
return { code: 0, stdout: USAGE8, stderr: "" };
|
|
2146
2505
|
}
|
|
2147
2506
|
const unknownFlag = args.find((arg) => arg.startsWith("-"));
|
|
2148
2507
|
if (unknownFlag !== void 0) {
|
|
@@ -2225,7 +2584,7 @@ Run \`takuhon admin update --help\` for usage.
|
|
|
2225
2584
|
};
|
|
2226
2585
|
}
|
|
2227
2586
|
function readVersion() {
|
|
2228
|
-
const pkg2 = JSON.parse(
|
|
2587
|
+
const pkg2 = JSON.parse(readFileSync12(new URL("../package.json", import.meta.url), "utf8"));
|
|
2229
2588
|
return pkg2.version;
|
|
2230
2589
|
}
|
|
2231
2590
|
function isNodeErrnoException(err) {
|
|
@@ -2233,10 +2592,10 @@ function isNodeErrnoException(err) {
|
|
|
2233
2592
|
}
|
|
2234
2593
|
|
|
2235
2594
|
// src/restore-command.ts
|
|
2236
|
-
import { readFileSync as
|
|
2237
|
-
import { validate as
|
|
2238
|
-
var
|
|
2239
|
-
var
|
|
2595
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
2596
|
+
import { validate as validate6 } from "@takuhon/core";
|
|
2597
|
+
var DEFAULT_PATH9 = "takuhon.json";
|
|
2598
|
+
var USAGE9 = `Usage: takuhon restore --from <backup> [path] [--yes]
|
|
2240
2599
|
|
|
2241
2600
|
Overwrite a profile with a previously saved backup. With no path, restores
|
|
2242
2601
|
./takuhon.json in the current working directory.
|
|
@@ -2253,9 +2612,9 @@ Exit codes: 0 = restored / aborted, 1 = backup failed validation,
|
|
|
2253
2612
|
`;
|
|
2254
2613
|
async function runRestore(args = [], deps = {}) {
|
|
2255
2614
|
if (args[0] === "--help" || args[0] === "-h") {
|
|
2256
|
-
return { code: 0, stdout:
|
|
2615
|
+
return { code: 0, stdout: USAGE9, stderr: "" };
|
|
2257
2616
|
}
|
|
2258
|
-
const parsed =
|
|
2617
|
+
const parsed = parseArgs8(args);
|
|
2259
2618
|
if ("error" in parsed) {
|
|
2260
2619
|
return {
|
|
2261
2620
|
code: 2,
|
|
@@ -2268,7 +2627,7 @@ Run \`takuhon restore --help\` for usage.
|
|
|
2268
2627
|
const now = deps.now ?? (() => /* @__PURE__ */ new Date());
|
|
2269
2628
|
return restoreFile(parsed, now, deps.confirm);
|
|
2270
2629
|
}
|
|
2271
|
-
function
|
|
2630
|
+
function parseArgs8(args) {
|
|
2272
2631
|
let from;
|
|
2273
2632
|
let path;
|
|
2274
2633
|
let yes = false;
|
|
@@ -2302,13 +2661,13 @@ function parseArgs7(args) {
|
|
|
2302
2661
|
if (from === void 0 || from.length === 0) {
|
|
2303
2662
|
return { error: "takuhon: `restore` requires `--from <backup>`." };
|
|
2304
2663
|
}
|
|
2305
|
-
return { from, path: path ??
|
|
2664
|
+
return { from, path: path ?? DEFAULT_PATH9, yes };
|
|
2306
2665
|
}
|
|
2307
2666
|
async function restoreFile(parsed, now, confirm) {
|
|
2308
2667
|
const { from, path, yes } = parsed;
|
|
2309
2668
|
let backupRaw;
|
|
2310
2669
|
try {
|
|
2311
|
-
backupRaw =
|
|
2670
|
+
backupRaw = readFileSync13(from, "utf8");
|
|
2312
2671
|
} catch {
|
|
2313
2672
|
return { code: 2, stdout: "", stderr: `takuhon: cannot read backup '${from}'.
|
|
2314
2673
|
` };
|
|
@@ -2325,7 +2684,7 @@ async function restoreFile(parsed, now, confirm) {
|
|
|
2325
2684
|
`
|
|
2326
2685
|
};
|
|
2327
2686
|
}
|
|
2328
|
-
const result =
|
|
2687
|
+
const result = validate6(backupData);
|
|
2329
2688
|
if (!result.ok) {
|
|
2330
2689
|
const lines2 = result.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
|
|
2331
2690
|
return {
|
|
@@ -2338,7 +2697,7 @@ ${lines2.join("\n")}
|
|
|
2338
2697
|
}
|
|
2339
2698
|
let currentRaw;
|
|
2340
2699
|
try {
|
|
2341
|
-
currentRaw =
|
|
2700
|
+
currentRaw = readFileSync13(path, "utf8");
|
|
2342
2701
|
} catch (error) {
|
|
2343
2702
|
if (!isNotFound6(error)) {
|
|
2344
2703
|
return { code: 2, stdout: "", stderr: `takuhon: cannot read current profile '${path}'.
|
|
@@ -2408,12 +2767,12 @@ function isNotFound6(error) {
|
|
|
2408
2767
|
}
|
|
2409
2768
|
|
|
2410
2769
|
// src/sync-command.ts
|
|
2411
|
-
import { readFileSync as
|
|
2412
|
-
import { validate as
|
|
2413
|
-
var
|
|
2770
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
2771
|
+
import { validate as validate7 } from "@takuhon/core";
|
|
2772
|
+
var DEFAULT_PATH10 = "takuhon.json";
|
|
2414
2773
|
var ADMIN_PROFILE_PATH = "/api/admin/profile";
|
|
2415
2774
|
var TOKEN_ENV = "TAKUHON_ADMIN_TOKEN";
|
|
2416
|
-
var
|
|
2775
|
+
var USAGE10 = `Usage: takuhon sync [path] --url <base-url> [--if-match <etag>] [--dry-run]
|
|
2417
2776
|
|
|
2418
2777
|
Push a local takuhon.json to a deployed takuhon instance by calling its admin
|
|
2419
2778
|
write endpoint (PUT <base-url>/api/admin/profile). With no path, syncs
|
|
@@ -2439,9 +2798,9 @@ unset / auth failure / network error / other non-success response.
|
|
|
2439
2798
|
`;
|
|
2440
2799
|
async function runSync(args = [], deps = {}) {
|
|
2441
2800
|
if (args[0] === "--help" || args[0] === "-h") {
|
|
2442
|
-
return { code: 0, stdout:
|
|
2801
|
+
return { code: 0, stdout: USAGE10, stderr: "" };
|
|
2443
2802
|
}
|
|
2444
|
-
const parsed =
|
|
2803
|
+
const parsed = parseArgs9(args);
|
|
2445
2804
|
if ("error" in parsed) {
|
|
2446
2805
|
return {
|
|
2447
2806
|
code: 2,
|
|
@@ -2453,7 +2812,7 @@ Run \`takuhon sync --help\` for usage.
|
|
|
2453
2812
|
}
|
|
2454
2813
|
return syncProfile(parsed, deps);
|
|
2455
2814
|
}
|
|
2456
|
-
function
|
|
2815
|
+
function parseArgs9(args) {
|
|
2457
2816
|
let path;
|
|
2458
2817
|
let url;
|
|
2459
2818
|
let ifMatch;
|
|
@@ -2499,7 +2858,7 @@ function parseArgs8(args) {
|
|
|
2499
2858
|
}
|
|
2500
2859
|
const base = parseOrigin(url);
|
|
2501
2860
|
if ("error" in base) return base;
|
|
2502
|
-
return { path: path ??
|
|
2861
|
+
return { path: path ?? DEFAULT_PATH10, url: base.origin, ifMatch, dryRun };
|
|
2503
2862
|
}
|
|
2504
2863
|
function parseOrigin(value) {
|
|
2505
2864
|
let parsed;
|
|
@@ -2532,7 +2891,7 @@ async function syncProfile(parsed, deps) {
|
|
|
2532
2891
|
const { path, url, ifMatch, dryRun } = parsed;
|
|
2533
2892
|
let raw;
|
|
2534
2893
|
try {
|
|
2535
|
-
raw =
|
|
2894
|
+
raw = readFileSync14(path, "utf8");
|
|
2536
2895
|
} catch {
|
|
2537
2896
|
return { code: 2, stdout: "", stderr: `takuhon: cannot read '${path}'.
|
|
2538
2897
|
` };
|
|
@@ -2545,7 +2904,7 @@ async function syncProfile(parsed, deps) {
|
|
|
2545
2904
|
return { code: 2, stdout: "", stderr: `takuhon: '${path}' is not valid JSON: ${detail}
|
|
2546
2905
|
` };
|
|
2547
2906
|
}
|
|
2548
|
-
const result =
|
|
2907
|
+
const result = validate7(data);
|
|
2549
2908
|
if (!result.ok) {
|
|
2550
2909
|
const lines = result.errors.map((e) => ` ${e.pointer || "/"}: ${e.message}`);
|
|
2551
2910
|
return {
|
|
@@ -2669,10 +3028,10 @@ async function readProblem(res) {
|
|
|
2669
3028
|
}
|
|
2670
3029
|
|
|
2671
3030
|
// src/validate-command.ts
|
|
2672
|
-
import { readFileSync as
|
|
2673
|
-
import { validate as
|
|
2674
|
-
var
|
|
2675
|
-
var
|
|
3031
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
3032
|
+
import { validate as validate8 } from "@takuhon/core";
|
|
3033
|
+
var DEFAULT_PATH11 = "takuhon.json";
|
|
3034
|
+
var USAGE11 = `Usage: takuhon validate [path]
|
|
2676
3035
|
|
|
2677
3036
|
Validate a takuhon.json against the takuhon schema. With no path, validates
|
|
2678
3037
|
./takuhon.json in the current working directory.
|
|
@@ -2681,7 +3040,7 @@ Exit codes: 0 = valid, 1 = invalid, 2 = file missing / unreadable / not JSON.
|
|
|
2681
3040
|
`;
|
|
2682
3041
|
function runValidate(args = []) {
|
|
2683
3042
|
if (args[0] === "--help" || args[0] === "-h") {
|
|
2684
|
-
return { code: 0, stdout:
|
|
3043
|
+
return { code: 0, stdout: USAGE11, stderr: "" };
|
|
2685
3044
|
}
|
|
2686
3045
|
if (args.length > 1) {
|
|
2687
3046
|
return {
|
|
@@ -2693,10 +3052,10 @@ function runValidate(args = []) {
|
|
|
2693
3052
|
return validateFile(args[0]);
|
|
2694
3053
|
}
|
|
2695
3054
|
function validateFile(pathArg) {
|
|
2696
|
-
const target = pathArg ??
|
|
3055
|
+
const target = pathArg ?? DEFAULT_PATH11;
|
|
2697
3056
|
let raw;
|
|
2698
3057
|
try {
|
|
2699
|
-
raw =
|
|
3058
|
+
raw = readFileSync15(target, "utf8");
|
|
2700
3059
|
} catch {
|
|
2701
3060
|
return {
|
|
2702
3061
|
code: 2,
|
|
@@ -2717,7 +3076,7 @@ function validateFile(pathArg) {
|
|
|
2717
3076
|
`
|
|
2718
3077
|
};
|
|
2719
3078
|
}
|
|
2720
|
-
const result =
|
|
3079
|
+
const result = validate8(data);
|
|
2721
3080
|
if (result.ok) {
|
|
2722
3081
|
return {
|
|
2723
3082
|
code: 0,
|
|
@@ -2738,9 +3097,9 @@ ${lines.join("\n")}
|
|
|
2738
3097
|
}
|
|
2739
3098
|
|
|
2740
3099
|
// src/index.ts
|
|
2741
|
-
var pkg = JSON.parse(
|
|
2742
|
-
var
|
|
2743
|
-
var HELP = `takuhon ${
|
|
3100
|
+
var pkg = JSON.parse(readFileSync16(new URL("../package.json", import.meta.url), "utf8"));
|
|
3101
|
+
var VERSION2 = pkg.version;
|
|
3102
|
+
var HELP = `takuhon ${VERSION2}
|
|
2744
3103
|
|
|
2745
3104
|
Takuhon \u2014 open-source portable profile API server.
|
|
2746
3105
|
|
|
@@ -2771,6 +3130,9 @@ Commands:
|
|
|
2771
3130
|
takuhon admin update [path] Refresh a project's admin-dist/ with the admin form UI
|
|
2772
3131
|
bundled in this @takuhon/cli (use after upgrading the
|
|
2773
3132
|
CLI). Updates an existing admin-dist/ only.
|
|
3133
|
+
takuhon mcp [path] Serve a takuhon.json over the Model Context Protocol on
|
|
3134
|
+
stdio (read-only), so an MCP client such as Claude Desktop
|
|
3135
|
+
can read the profile. Re-reads the file per request.
|
|
2774
3136
|
takuhon sync [path] --url <url> Push a takuhon.json to a deployment's admin API
|
|
2775
3137
|
(PUT <url>/api/admin/profile). Reads the admin token
|
|
2776
3138
|
from TAKUHON_ADMIN_TOKEN. --if-match <etag> opts into
|
|
@@ -2788,7 +3150,7 @@ Scaffolding a new profile project:
|
|
|
2788
3150
|
async function main(argv) {
|
|
2789
3151
|
const first = argv[0];
|
|
2790
3152
|
if (first === "--version" || first === "-v") {
|
|
2791
|
-
process.stdout.write(`${
|
|
3153
|
+
process.stdout.write(`${VERSION2}
|
|
2792
3154
|
`);
|
|
2793
3155
|
return 0;
|
|
2794
3156
|
}
|
|
@@ -2817,6 +3179,9 @@ async function main(argv) {
|
|
|
2817
3179
|
}
|
|
2818
3180
|
return runAdmin(argv.slice(1));
|
|
2819
3181
|
}
|
|
3182
|
+
if (first === "mcp") {
|
|
3183
|
+
return runMcp(argv.slice(1));
|
|
3184
|
+
}
|
|
2820
3185
|
if (first === "import") {
|
|
2821
3186
|
return emit(runImport(argv.slice(1)));
|
|
2822
3187
|
}
|