@theholocron/cli 3.49.0 → 3.50.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/cli.mjs +85 -59
- package/dist/cli.mjs.map +1 -1
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -4895,8 +4895,8 @@ function deriveBasepath(domain, repoName) {
|
|
|
4895
4895
|
}
|
|
4896
4896
|
return repoName;
|
|
4897
4897
|
}
|
|
4898
|
-
function
|
|
4899
|
-
return
|
|
4898
|
+
function toNavLabel(basepath) {
|
|
4899
|
+
return basepath.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
4900
4900
|
}
|
|
4901
4901
|
function extractFromJson(raw, repoName) {
|
|
4902
4902
|
let config;
|
|
@@ -4909,40 +4909,31 @@ function extractFromJson(raw, repoName) {
|
|
|
4909
4909
|
if (!providers?.wiki) return null;
|
|
4910
4910
|
const wikiEntry = providers.wiki;
|
|
4911
4911
|
let domain;
|
|
4912
|
-
let subtitle;
|
|
4913
|
-
let icon;
|
|
4914
4912
|
if (Array.isArray(wikiEntry) && wikiEntry.length === 2) {
|
|
4915
4913
|
const opts = wikiEntry[1];
|
|
4916
4914
|
domain = typeof opts.domain === "string" ? opts.domain : void 0;
|
|
4917
|
-
subtitle = typeof opts.subtitle === "string" ? opts.subtitle : void 0;
|
|
4918
|
-
icon = typeof opts.icon === "string" ? opts.icon : void 0;
|
|
4919
4915
|
}
|
|
4920
|
-
if (!subtitle && typeof config.description === "string") subtitle = config.description;
|
|
4921
4916
|
const basepath = deriveBasepath(domain, repoName);
|
|
4922
4917
|
return {
|
|
4923
|
-
displayName:
|
|
4918
|
+
displayName: toNavLabel(basepath),
|
|
4924
4919
|
basepath,
|
|
4925
|
-
...
|
|
4926
|
-
...icon ? { icon } : {}
|
|
4920
|
+
...domain ? { domain } : {}
|
|
4927
4921
|
};
|
|
4928
4922
|
}
|
|
4929
4923
|
function extractFromTs(raw, repoName) {
|
|
4930
4924
|
const hasWikiProvider = /providers\s*:\s*\{[^}]*\bwiki\b/s.test(raw);
|
|
4931
4925
|
const hasWikiPreset = /\bwikiCapability\b|\bwiki\s*\(\s*\)/.test(raw);
|
|
4926
|
+
/* c8 ignore next */
|
|
4932
4927
|
if (!hasWikiProvider && !hasWikiPreset) return null;
|
|
4933
4928
|
const domain = raw.match(/\bdomain\s*:\s*["']([^"']+)["']/)?.[1];
|
|
4934
|
-
let subtitle = raw.match(/\bsubtitle\s*:\s*["']([^"']+)["']/)?.[1];
|
|
4935
|
-
const icon = raw.match(/\bicon\s*:\s*["']([^"']+)["']/)?.[1];
|
|
4936
|
-
if (!subtitle) subtitle = raw.match(/\bdescription\s*:\s*["']([^"']+)["']/)?.[1];
|
|
4937
4929
|
const basepath = deriveBasepath(domain, repoName);
|
|
4938
4930
|
return {
|
|
4939
|
-
displayName:
|
|
4931
|
+
displayName: toNavLabel(basepath),
|
|
4940
4932
|
basepath,
|
|
4941
|
-
...
|
|
4942
|
-
...icon ? { icon } : {}
|
|
4933
|
+
...domain ? { domain } : {}
|
|
4943
4934
|
};
|
|
4944
4935
|
}
|
|
4945
|
-
async function
|
|
4936
|
+
async function discoverWikiRepos(org, token, fetchFn) {
|
|
4946
4937
|
const rest = createRestClient({
|
|
4947
4938
|
baseUrl: "https://api.github.com",
|
|
4948
4939
|
token,
|
|
@@ -4965,52 +4956,81 @@ async function discoverWikiProducts(org, token, fetchFn) {
|
|
|
4965
4956
|
if (batch.length < 100) break;
|
|
4966
4957
|
page++;
|
|
4967
4958
|
}
|
|
4968
|
-
const
|
|
4959
|
+
const repos = [];
|
|
4969
4960
|
for (const repo of allRepos.filter((r) => !r.archived)) {
|
|
4970
|
-
let
|
|
4961
|
+
let found = null;
|
|
4971
4962
|
try {
|
|
4972
4963
|
const contents = await rest.request(`/repos/${repo.full_name}/contents/holocron.config.json`);
|
|
4973
|
-
if (contents.encoding === "base64")
|
|
4964
|
+
if (contents.encoding === "base64") found = extractFromJson(Buffer.from(contents.content.replace(/\s/g, ""), "base64").toString("utf8"), repo.name);
|
|
4974
4965
|
} catch {}
|
|
4975
|
-
if (!
|
|
4966
|
+
if (!found) try {
|
|
4976
4967
|
const contents = await rest.request(`/repos/${repo.full_name}/contents/holocron.config.ts`);
|
|
4977
|
-
if (contents.encoding === "base64")
|
|
4968
|
+
if (contents.encoding === "base64") found = extractFromTs(Buffer.from(contents.content.replace(/\s/g, ""), "base64").toString("utf8"), repo.name);
|
|
4978
4969
|
} catch {}
|
|
4979
|
-
if (
|
|
4970
|
+
if (found) repos.push(found);
|
|
4980
4971
|
}
|
|
4981
|
-
|
|
4982
|
-
return
|
|
4972
|
+
repos.sort((a, b) => a.basepath.localeCompare(b.basepath));
|
|
4973
|
+
return repos;
|
|
4983
4974
|
}
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4975
|
+
/**
|
|
4976
|
+
* Builds the complete navbar-links YAML block for a given repo.
|
|
4977
|
+
*
|
|
4978
|
+
* Includes:
|
|
4979
|
+
* - A GitHub button linking to the repo itself (always first)
|
|
4980
|
+
* - Minimal nav links to every other wiki-enabled repo (excluding self)
|
|
4981
|
+
*
|
|
4982
|
+
* The block is replaced wholesale on each sync so removed wikis disappear
|
|
4983
|
+
* and new ones appear automatically.
|
|
4984
|
+
*/
|
|
4985
|
+
function buildNavbarLinks(repos, currentBasepath, repoFullName) {
|
|
4986
|
+
const lines = [
|
|
4987
|
+
`navbar-links:`,
|
|
4988
|
+
` - type: github`,
|
|
4989
|
+
` value: https://github.com/${repoFullName}`
|
|
4990
|
+
];
|
|
4991
|
+
for (const repo of repos) {
|
|
4992
|
+
if (repo.basepath === currentBasepath) continue;
|
|
4993
|
+
const url = repo.domain ? `https://${repo.domain}` : `https://wiki.theholocron.dev/${repo.basepath}`;
|
|
4994
|
+
lines.push(` - type: minimal`, ` value: ${url}`, ` label: ${repo.displayName}`);
|
|
4991
4995
|
}
|
|
4992
4996
|
return lines.join("\n");
|
|
4993
4997
|
}
|
|
4994
|
-
|
|
4998
|
+
/**
|
|
4999
|
+
* Idempotently ensures `edit-this-page:` (inside instances) is present and
|
|
5000
|
+
* replaces `navbar-links:` with the provided shared block.
|
|
5001
|
+
*
|
|
5002
|
+
* Returns true when the file was modified.
|
|
5003
|
+
* Throws when fern/docs.yml does not exist.
|
|
5004
|
+
*/
|
|
5005
|
+
async function mergeWikiConfig(docsYmlPath, config) {
|
|
4995
5006
|
let content;
|
|
4996
5007
|
try {
|
|
4997
5008
|
content = await readFile(docsYmlPath, "utf8");
|
|
4998
5009
|
} catch {
|
|
4999
5010
|
throw new Error(`fern/docs.yml not found at ${docsYmlPath}`);
|
|
5000
5011
|
}
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
+
let updated = content;
|
|
5013
|
+
if (!updated.includes("edit-this-page:")) {
|
|
5014
|
+
const editBlock = [
|
|
5015
|
+
` edit-this-page:`,
|
|
5016
|
+
` github:`,
|
|
5017
|
+
` owner: ${config.owner}`,
|
|
5018
|
+
` repo: ${config.repoName}`,
|
|
5019
|
+
` branch: main`
|
|
5020
|
+
].join("\n");
|
|
5021
|
+
if (/^ {4}multi-source: true$/m.test(updated)) updated = updated.replace(/^( {4}multi-source: true)$/m, `$1\n${editBlock}`);
|
|
5022
|
+
else if (/^ {4}custom-domain: .+$/m.test(updated)) updated = updated.replace(/^( {4}custom-domain: .+)$/m, `$1\n${editBlock}`);
|
|
5023
|
+
else updated = updated.replace(/^( {2}- url: .+)$/m, `$1\n${editBlock}`);
|
|
5024
|
+
}
|
|
5025
|
+
const navbarRe = /^navbar-links:(?:\n[ \t][^\n]*)*/m;
|
|
5026
|
+
if (navbarRe.test(updated)) updated = updated.replace(navbarRe, config.navbarBlock);
|
|
5027
|
+
else if (/^colors:/m.test(updated)) updated = updated.replace(/^(colors:)/m, `${config.navbarBlock}\n\n$1`);
|
|
5028
|
+
else updated = updated.trimEnd() + "\n\n" + config.navbarBlock + "\n";
|
|
5029
|
+
if (updated !== content) {
|
|
5030
|
+
await writeFile(docsYmlPath, updated, "utf8");
|
|
5031
|
+
return true;
|
|
5012
5032
|
}
|
|
5013
|
-
|
|
5033
|
+
return false;
|
|
5014
5034
|
}
|
|
5015
5035
|
async function runSyncWiki(input) {
|
|
5016
5036
|
const config = input.loaded.resolved;
|
|
@@ -5021,13 +5041,21 @@ async function runSyncWiki(input) {
|
|
|
5021
5041
|
status: "skip",
|
|
5022
5042
|
message: "no wiki provider configured"
|
|
5023
5043
|
};
|
|
5024
|
-
const
|
|
5025
|
-
if (!
|
|
5044
|
+
const rawRepo = input.context.repo ?? config.repo?.name;
|
|
5045
|
+
if (!rawRepo) return {
|
|
5046
|
+
capability: "local",
|
|
5047
|
+
step: "sync wiki",
|
|
5048
|
+
status: "skip",
|
|
5049
|
+
message: "no repo configured"
|
|
5050
|
+
};
|
|
5051
|
+
const [owner, repoName] = rawRepo.split("/");
|
|
5052
|
+
if (!owner || !repoName) return {
|
|
5026
5053
|
capability: "local",
|
|
5027
5054
|
step: "sync wiki",
|
|
5028
5055
|
status: "skip",
|
|
5029
|
-
message: "
|
|
5056
|
+
message: "repo must be owner/name format"
|
|
5030
5057
|
};
|
|
5058
|
+
const org = config.org ?? owner;
|
|
5031
5059
|
const token = resolveToken(input);
|
|
5032
5060
|
if (!token) return {
|
|
5033
5061
|
capability: "local",
|
|
@@ -5042,26 +5070,24 @@ async function runSyncWiki(input) {
|
|
|
5042
5070
|
};
|
|
5043
5071
|
const docsYmlPath = join(input.context.repoRoot, "fern", "docs.yml");
|
|
5044
5072
|
try {
|
|
5045
|
-
const
|
|
5046
|
-
|
|
5047
|
-
capability: "local",
|
|
5048
|
-
step: "sync wiki",
|
|
5049
|
-
status: "skip",
|
|
5050
|
-
message: "no wiki-enabled repos found"
|
|
5051
|
-
};
|
|
5052
|
-
await mergeProducts(docsYmlPath, products);
|
|
5073
|
+
const repos = await discoverWikiRepos(org, token, input.fetch);
|
|
5074
|
+
const wikiOpts = config.providers.wiki?.tuple?.options;
|
|
5053
5075
|
return {
|
|
5054
5076
|
capability: "local",
|
|
5055
5077
|
step: "sync wiki",
|
|
5056
5078
|
status: "ok",
|
|
5057
|
-
message:
|
|
5079
|
+
message: await mergeWikiConfig(docsYmlPath, {
|
|
5080
|
+
owner,
|
|
5081
|
+
repoName,
|
|
5082
|
+
navbarBlock: buildNavbarLinks(repos, deriveBasepath(typeof wikiOpts?.domain === "string" ? wikiOpts.domain : void 0, repoName), rawRepo)
|
|
5083
|
+
}) ? `updated (${repos.length} wiki repos discovered)` : "already up to date"
|
|
5058
5084
|
};
|
|
5059
5085
|
} catch (err) {
|
|
5060
5086
|
return {
|
|
5061
5087
|
capability: "local",
|
|
5062
5088
|
step: "sync wiki",
|
|
5063
5089
|
status: "fail",
|
|
5064
|
-
message: err instanceof Error ? err.message : String(err)
|
|
5090
|
+
message: err instanceof Error ? err.message : /* c8 ignore next */ String(err)
|
|
5065
5091
|
};
|
|
5066
5092
|
}
|
|
5067
5093
|
}
|
|
@@ -5509,7 +5535,7 @@ var security_default = "name: Security\n\non: # yamllint disable-line rule:truth
|
|
|
5509
5535
|
var stale_default = "name: Stale\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n days-before-stale:\n description: Days of inactivity before an issue is marked stale\n type: number\n required: false\n default: 30\n days-before-close:\n description: Days of inactivity after stale label before closing\n type: number\n required: false\n default: 5\n\njobs:\n stale:\n name: Mark stale issues and pull requests\n permissions:\n contents: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0\n name: Run Stale\n with:\n close-issue-message: >\n This issue was closed because it has been stalled for\n ${{ inputs.days-before-close }} days with no activity.\n days-before-close: ${{ inputs.days-before-close }}\n days-before-stale: ${{ inputs.days-before-stale }}\n exempt-all-pr-milestones: true\n stale-issue-label: wontfix\n stale-issue-message: >\n This issue is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n stale-pr-label: wontfix\n stale-pr-message: >\n This PR is stale because it has been open ${{ inputs.days-before-stale }}\n days with no activity. Remove the stale label or comment, or this will be\n closed in ${{ inputs.days-before-close }} days.\n";
|
|
5510
5536
|
//#endregion
|
|
5511
5537
|
//#region src/templates/workflows/sync.yml
|
|
5512
|
-
var sync_default = "name: Sync\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n steps:\n description: >\n Sync steps to run (default: all). Valid values:\n labels, properties, teams, topics, keywords, description, homepage, readme, workflows.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme
|
|
5538
|
+
var sync_default = "name: Sync\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n steps:\n description: >\n Sync steps to run (default: all). Valid values:\n labels, properties, teams, topics, keywords, description, homepage, readme, workflows, wiki.\n Pass a space-separated list to run a subset, e.g. \"readme\" or \"readme wiki\".\n type: string\n required: false\n secrets:\n HOLOCRON_ADMIN_TOKEN:\n description: Fine-grained PAT with admin scopes (labels, properties, teams).\n required: false\n HOLOCRON_DEPLOY_TOKEN:\n description: Fine-grained PAT for GitHub Pages configuration.\n required: false\n HOLOCRON_ISSUES_TOKEN:\n description: Fine-grained PAT for issue management.\n required: false\n HOLOCRON_ORG_TOKEN:\n description: Org-scoped fine-grained PAT for team sync and org properties.\n required: false\n HOLOCRON_READ_TOKEN:\n description: Fine-grained PAT for read-only GitHub API calls.\n required: false\n HOLOCRON_SYNC_TOKEN:\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for gh CLI calls. Used when\n HOLOCRON_SYNC_TOKEN is not set.\n required: false\n\njobs:\n sync:\n name: Sync repo from config\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: write\n pull-requests: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Run holocron sync\n run: |\n if [ -n \"$STEPS\" ]; then\n # shellcheck disable=SC2086\n pnpm --workspace-root exec holocron sync --steps $STEPS\n else\n pnpm --workspace-root exec holocron sync\n fi\n env:\n HOLOCRON_ADMIN_TOKEN: ${{ secrets.HOLOCRON_ADMIN_TOKEN }}\n HOLOCRON_DEPLOY_TOKEN: ${{ secrets.HOLOCRON_DEPLOY_TOKEN }}\n HOLOCRON_ISSUES_TOKEN: ${{ secrets.HOLOCRON_ISSUES_TOKEN }}\n HOLOCRON_ORG_TOKEN: ${{ secrets.HOLOCRON_ORG_TOKEN }}\n HOLOCRON_READ_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n\n - name: Format generated files\n run: pnpm exec prettier --write README.md docs/src/content/docs/index.mdx 2>/dev/null || true\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n id: auto-commit\n name: Commit sync changes\n with:\n token: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n branch: chore/auto-sync\n commit-message: \"chore: sync from holocron.config\"\n commit-options: \"--no-verify\"\n\n - name: Open PR if changes were committed\n if: steps.auto-commit.outputs.changes-detected == 'true'\n run: |\n gh pr create \\\n --title \"chore: sync README and repo metadata\" \\\n --body \"Automated sync triggered by changes to config or package files. Merge to apply.\" \\\n --base main \\\n --head chore/auto-sync \\\n || echo \"PR already open — branch updated.\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Propagate wiki nav to all repos\n if: >-\n steps.auto-commit.outputs.changes-detected == 'true' &&\n (inputs.steps == '' || contains(inputs.steps, 'wiki'))\n run: |\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=wiki\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN || github.token }}\n";
|
|
5513
5539
|
//#endregion
|
|
5514
5540
|
//#region src/templates/workflows/sync-dispatch.yml
|
|
5515
5541
|
var sync_dispatch_default = "name: Sync Dispatch\n\non: # yamllint disable-line rule:truthy\n workflow_dispatch:\n inputs:\n steps:\n description: >\n Sync steps to pass to each repo's sync.yml. Default is \"readme\"\n (only README marker blocks are updated).\n type: string\n required: false\n default: readme\n\npermissions:\n contents: read\n\njobs:\n broadcast:\n name: Broadcast sync to all repos\n runs-on: ubuntu-latest\n timeout-minutes: 15\n steps:\n - name: Dispatch sync to all repos with sync.yml\n run: |\n gh api /orgs/theholocron/repos --paginate --jq '.[].name' \\\n | while IFS= read -r repo; do\n gh api \"/repos/theholocron/$repo/contents/.github/workflows/sync.yml\" --silent 2>/dev/null || continue\n echo \"Dispatching sync to theholocron/$repo\"\n gh workflow run sync.yml \\\n --repo \"theholocron/$repo\" \\\n --field \"steps=$STEPS\" \\\n || echo \"Warning: could not dispatch to theholocron/$repo — skipping\"\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n STEPS: ${{ inputs.steps }}\n";
|