@theholocron/cli 3.46.1 → 3.48.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 +1184 -1062
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +52 -60
- package/dist/index.mjs +17 -22
- package/dist/{capabilities/index.d.mts → plugin/capabilities.d.mts} +1 -1
- package/dist/{capabilities/index.mjs → plugin/capabilities.mjs} +1 -1
- package/package.json +13 -11
package/dist/cli.mjs
CHANGED
|
@@ -5,36 +5,33 @@ import path, { basename, dirname, join, relative, resolve } from "node:path";
|
|
|
5
5
|
import { checkbox, input, select } from "@inquirer/prompts";
|
|
6
6
|
import yargs from "yargs";
|
|
7
7
|
import { hideBin } from "yargs/helpers";
|
|
8
|
-
import { AuthError, ProviderApiError, ProviderApiError as ProviderApiError$1 } from "@theholocron/http-client";
|
|
8
|
+
import { AuthError, ProviderApiError, ProviderApiError as ProviderApiError$1, createRestClient } from "@theholocron/http-client";
|
|
9
|
+
import { createEnvLookup } from "@theholocron/env-utils";
|
|
9
10
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
10
11
|
import { pathToFileURL } from "node:url";
|
|
11
12
|
import ora from "ora";
|
|
12
13
|
import chalk from "chalk";
|
|
13
14
|
import { execFile, execFileSync, spawnSync } from "node:child_process";
|
|
14
15
|
import { homedir } from "node:os";
|
|
15
|
-
import { createHash } from "node:crypto";
|
|
16
16
|
import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
17
18
|
import { generateReadme } from "@theholocron/components-doc/markdown";
|
|
18
19
|
import { getClients, getConfigs, getDocs, getPlugins, getSkills, getThemes, getUtils } from "@theholocron/registry-doc";
|
|
19
20
|
import { createGitHubClient } from "@theholocron/github-client";
|
|
20
21
|
import { promisify } from "node:util";
|
|
21
22
|
import * as Sentry from "@sentry/node";
|
|
22
23
|
//#region src/env.ts
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (val) return val;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
};
|
|
24
|
+
/** Singleton env for simple global lookups throughout the CLI. */
|
|
25
|
+
const env = createEnvLookup();
|
|
26
|
+
/**
|
|
27
|
+
* Create an injectable env for commands that accept a fake env in tests.
|
|
28
|
+
* Pass `input.env` when available, falls back to `process.env`.
|
|
29
|
+
*/
|
|
30
|
+
function makeEnv(source) {
|
|
31
|
+
return createEnvLookup(source);
|
|
35
32
|
}
|
|
36
33
|
//#endregion
|
|
37
|
-
//#region src/keyring.ts
|
|
34
|
+
//#region src/auth/keyring.ts
|
|
38
35
|
/**
|
|
39
36
|
* Keyring-backed bootstrap credential store.
|
|
40
37
|
*
|
|
@@ -104,7 +101,7 @@ function listStoredProviders() {
|
|
|
104
101
|
}
|
|
105
102
|
}
|
|
106
103
|
//#endregion
|
|
107
|
-
//#region src/auth-resolver.ts
|
|
104
|
+
//#region src/auth/auth-resolver.ts
|
|
108
105
|
/**
|
|
109
106
|
* Build a strict, single-feature token resolver.
|
|
110
107
|
*
|
|
@@ -114,7 +111,7 @@ function listStoredProviders() {
|
|
|
114
111
|
*/
|
|
115
112
|
function createFeatureResolver(config) {
|
|
116
113
|
return function resolveFeatureToken(input = {}) {
|
|
117
|
-
const env =
|
|
114
|
+
const env = makeEnv(input.env);
|
|
118
115
|
const keyring = input.keyring ?? getToken;
|
|
119
116
|
const token = input.cliToken || env.get(config.envName) || keyring(config.keyringKey);
|
|
120
117
|
if (!token) throw new AuthError(`no GitHub token found for this operation. Pass --token <PAT>, set ${config.envName}, or run: holocron auth set ${config.keyringKey} <PAT>`);
|
|
@@ -122,7 +119,45 @@ function createFeatureResolver(config) {
|
|
|
122
119
|
};
|
|
123
120
|
}
|
|
124
121
|
//#endregion
|
|
125
|
-
//#region src/
|
|
122
|
+
//#region src/auth/token-args.ts
|
|
123
|
+
var TokenParseError = class extends Error {
|
|
124
|
+
name = "TokenParseError";
|
|
125
|
+
};
|
|
126
|
+
/**
|
|
127
|
+
* Converts raw --token CLI values into a typed result.
|
|
128
|
+
*
|
|
129
|
+
* Bare form: --token ghp_xxx → { cliToken: "ghp_xxx" }
|
|
130
|
+
* Keyed form: --token github=ghp_xxx → { cliTokens: { github: "ghp_xxx" } }
|
|
131
|
+
* Mixed: --token github=ghp_xxx --token v_yyy
|
|
132
|
+
* → { cliToken: "v_yyy", cliTokens: { github: "ghp_xxx" } }
|
|
133
|
+
*
|
|
134
|
+
* Values may contain "=" (e.g. base64 strings) — only the first "=" is treated as a separator.
|
|
135
|
+
*/
|
|
136
|
+
function parseTokenArgs(tokens) {
|
|
137
|
+
if (tokens.length === 0) return {};
|
|
138
|
+
const cliTokens = {};
|
|
139
|
+
const bare = [];
|
|
140
|
+
for (const raw of tokens) {
|
|
141
|
+
const eqIdx = raw.indexOf("=");
|
|
142
|
+
if (eqIdx === -1) {
|
|
143
|
+
bare.push(raw);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const vendor = raw.slice(0, eqIdx);
|
|
147
|
+
const value = raw.slice(eqIdx + 1);
|
|
148
|
+
if (vendor.trim() === "") throw new TokenParseError(`invalid --token value "${raw}": vendor name must not be empty`);
|
|
149
|
+
if (/\s/.test(vendor)) throw new TokenParseError(`invalid --token value "${raw}": vendor name must not contain whitespace`);
|
|
150
|
+
if (value === "") throw new TokenParseError(`invalid --token value "${raw}": token value must not be empty`);
|
|
151
|
+
cliTokens[vendor] = value;
|
|
152
|
+
}
|
|
153
|
+
if (bare.length > 1) throw new TokenParseError(`only one bare --token value is allowed; got ${bare.length.toString()} — use vendor=value form for multiple tokens`);
|
|
154
|
+
const result = {};
|
|
155
|
+
if (bare.length === 1) result.cliToken = bare[0];
|
|
156
|
+
if (Object.keys(cliTokens).length > 0) result.cliTokens = cliTokens;
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/plugin/capabilities.ts
|
|
126
161
|
const CARDINALITY = {
|
|
127
162
|
source: "single",
|
|
128
163
|
ci: "single",
|
|
@@ -148,7 +183,7 @@ const CARDINALITY = {
|
|
|
148
183
|
*/
|
|
149
184
|
const REQUIRED_CAPABILITIES = [];
|
|
150
185
|
//#endregion
|
|
151
|
-
//#region src/config.ts
|
|
186
|
+
//#region src/config/config.ts
|
|
152
187
|
/**
|
|
153
188
|
* `holocron.config.json` schema, parser, and provider resolution.
|
|
154
189
|
*
|
|
@@ -336,10 +371,10 @@ const defaultImporter$1 = async (pkg) => {
|
|
|
336
371
|
* pulling FROM it would just re-store the same value.
|
|
337
372
|
*/
|
|
338
373
|
function resolveAuthSetToken(input) {
|
|
339
|
-
const
|
|
374
|
+
const e = makeEnv(input.env);
|
|
340
375
|
const upper = input.provider.toUpperCase();
|
|
341
376
|
const holocronKey = `HOLOCRON_${upper}_TOKEN`;
|
|
342
|
-
return input.positional ||
|
|
377
|
+
return input.positional || e.get(holocronKey) || e.get(`${upper}_TOKEN`) || null;
|
|
343
378
|
}
|
|
344
379
|
async function runAuthSet(input) {
|
|
345
380
|
const print = input.print ?? ((l) => console.log(l));
|
|
@@ -499,7 +534,7 @@ async function tryLoadHint(importer, packageName) {
|
|
|
499
534
|
}
|
|
500
535
|
}
|
|
501
536
|
//#endregion
|
|
502
|
-
//#region src/loader.ts
|
|
537
|
+
//#region src/plugin/loader.ts
|
|
503
538
|
/**
|
|
504
539
|
* `PluginLoader` — loads provider plugins per the resolved config and
|
|
505
540
|
* builds a typed capability registry the runtime can query.
|
|
@@ -1324,11 +1359,11 @@ async function runNew(input) {
|
|
|
1324
1359
|
const setupEnv = {};
|
|
1325
1360
|
const adminToken = input.token ?? keychainLookup("github.admin");
|
|
1326
1361
|
if (adminToken) setupArgs.push("--token", adminToken);
|
|
1327
|
-
if (!
|
|
1362
|
+
if (!env.get("HOLOCRON_ORG_TOKEN")) {
|
|
1328
1363
|
const orgToken = keychainLookup("github.org");
|
|
1329
1364
|
if (orgToken) setupEnv["HOLOCRON_ORG_TOKEN"] = orgToken;
|
|
1330
1365
|
}
|
|
1331
|
-
if (!
|
|
1366
|
+
if (!env.get("HOLOCRON_DEPLOY_TOKEN")) {
|
|
1332
1367
|
const deployToken = keychainLookup("github.deploy");
|
|
1333
1368
|
if (deployToken) setupEnv["HOLOCRON_DEPLOY_TOKEN"] = deployToken;
|
|
1334
1369
|
}
|
|
@@ -1459,7 +1494,7 @@ async function runNpmPublishInitial(input = {}) {
|
|
|
1459
1494
|
const tag = input.tag ?? "alpha";
|
|
1460
1495
|
const dryRun = input.dryRun ?? false;
|
|
1461
1496
|
const otp = input.otp;
|
|
1462
|
-
const env = input.env
|
|
1497
|
+
const env = makeEnv(input.env);
|
|
1463
1498
|
const exec = input.exec ?? defaultExec$2;
|
|
1464
1499
|
const publishArgs = [
|
|
1465
1500
|
"-r",
|
|
@@ -1554,7 +1589,7 @@ function printNextSteps$1(print, env, packageNames, repoName) {
|
|
|
1554
1589
|
print(" → next: configure Trusted Publisher for each package on npm:");
|
|
1555
1590
|
for (const name of packageNames) print(` https://www.npmjs.com/package/${name}/access`);
|
|
1556
1591
|
print(` Publisher: GitHub Actions Org: theholocron Repo: ${repoName} Workflow: release.yml`);
|
|
1557
|
-
if (env.NPM_TOKEN) {
|
|
1592
|
+
if (env.get("NPM_TOKEN")) {
|
|
1558
1593
|
print("");
|
|
1559
1594
|
print(" → cleanup: $NPM_TOKEN was used. Revoke it now (no API for self-revoke; UI-only):");
|
|
1560
1595
|
print(" https://www.npmjs.com/settings/~/tokens");
|
|
@@ -2689,9 +2724,8 @@ async function runSecretSet(input) {
|
|
|
2689
2724
|
async function resolveValue(input) {
|
|
2690
2725
|
if (input.value) return input.value;
|
|
2691
2726
|
if (input.fromStdin) return (await (input.readStdin ?? defaultReadStdin)()).replace(/\r?\n$/, "");
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
return env[input.name];
|
|
2727
|
+
if (input.fromEnv) return env.get(input.fromEnv);
|
|
2728
|
+
return env.get(input.name);
|
|
2695
2729
|
}
|
|
2696
2730
|
async function defaultReadStdin() {
|
|
2697
2731
|
const chunks = [];
|
|
@@ -2815,19 +2849,547 @@ function vaultProviderName(loader) {
|
|
|
2815
2849
|
return loader.get("vault").providerName;
|
|
2816
2850
|
}
|
|
2817
2851
|
//#endregion
|
|
2818
|
-
//#region src/
|
|
2852
|
+
//#region src/commands/setup/labels.ts
|
|
2853
|
+
const CANONICAL_LABELS = [
|
|
2854
|
+
{
|
|
2855
|
+
name: "bug",
|
|
2856
|
+
color: "d73a4a",
|
|
2857
|
+
description: "Something isn't working"
|
|
2858
|
+
},
|
|
2859
|
+
{
|
|
2860
|
+
name: "chore",
|
|
2861
|
+
color: "ededed",
|
|
2862
|
+
description: "Maintenance, no user-facing change"
|
|
2863
|
+
},
|
|
2864
|
+
{
|
|
2865
|
+
name: "ci",
|
|
2866
|
+
color: "0075ca",
|
|
2867
|
+
description: "CI/CD pipeline changes"
|
|
2868
|
+
},
|
|
2869
|
+
{
|
|
2870
|
+
name: "dependencies",
|
|
2871
|
+
color: "0366d6",
|
|
2872
|
+
description: "Dependency update"
|
|
2873
|
+
},
|
|
2874
|
+
{
|
|
2875
|
+
name: "documentation",
|
|
2876
|
+
color: "0075ca",
|
|
2877
|
+
description: "Documentation only"
|
|
2878
|
+
},
|
|
2879
|
+
{
|
|
2880
|
+
name: "duplicate",
|
|
2881
|
+
color: "cfd3d7",
|
|
2882
|
+
description: "Already reported"
|
|
2883
|
+
},
|
|
2884
|
+
{
|
|
2885
|
+
name: "enhancement",
|
|
2886
|
+
color: "a2eeef",
|
|
2887
|
+
description: "New feature or request"
|
|
2888
|
+
},
|
|
2889
|
+
{
|
|
2890
|
+
name: "good first issue",
|
|
2891
|
+
color: "7057ff",
|
|
2892
|
+
description: "Good for newcomers"
|
|
2893
|
+
},
|
|
2894
|
+
{
|
|
2895
|
+
name: "help wanted",
|
|
2896
|
+
color: "008672",
|
|
2897
|
+
description: "Extra attention needed"
|
|
2898
|
+
},
|
|
2899
|
+
{
|
|
2900
|
+
name: "invalid",
|
|
2901
|
+
color: "e4e669",
|
|
2902
|
+
description: "Doesn't seem right"
|
|
2903
|
+
},
|
|
2904
|
+
{
|
|
2905
|
+
name: "performance",
|
|
2906
|
+
color: "fbca04",
|
|
2907
|
+
description: "Performance improvement"
|
|
2908
|
+
},
|
|
2909
|
+
{
|
|
2910
|
+
name: "question",
|
|
2911
|
+
color: "d876e3",
|
|
2912
|
+
description: "Further information requested"
|
|
2913
|
+
},
|
|
2914
|
+
{
|
|
2915
|
+
name: "refactor",
|
|
2916
|
+
color: "cfd3d7",
|
|
2917
|
+
description: "Code restructuring"
|
|
2918
|
+
},
|
|
2919
|
+
{
|
|
2920
|
+
name: "released",
|
|
2921
|
+
color: "ededed",
|
|
2922
|
+
description: "Included in a release"
|
|
2923
|
+
},
|
|
2924
|
+
{
|
|
2925
|
+
name: "test",
|
|
2926
|
+
color: "bfd4f2",
|
|
2927
|
+
description: "Test-related changes"
|
|
2928
|
+
},
|
|
2929
|
+
{
|
|
2930
|
+
name: "triage",
|
|
2931
|
+
color: "e4e669",
|
|
2932
|
+
description: "Needs investigation"
|
|
2933
|
+
},
|
|
2934
|
+
{
|
|
2935
|
+
name: "wontfix",
|
|
2936
|
+
color: "ffffff",
|
|
2937
|
+
description: "Won't be addressed"
|
|
2938
|
+
}
|
|
2939
|
+
];
|
|
2940
|
+
const STALE_LABELS = [
|
|
2941
|
+
"github_actions",
|
|
2942
|
+
"javascript",
|
|
2943
|
+
"autorelease: pending",
|
|
2944
|
+
"autorelease: tagged",
|
|
2945
|
+
"released on @alpha"
|
|
2946
|
+
];
|
|
2947
|
+
//#endregion
|
|
2948
|
+
//#region src/templates/config.yml
|
|
2949
|
+
var config_default = "# Configuration for sentiment-bot - https://github.com/behaviorbot/sentiment-bot\n\n# *Required* toxicity threshold between 0 and .99 with the higher numbers being the most toxic\n# Anything higher than this threshold will be marked as toxic and commented on\nsentimentBotToxicityThreshold: .7\n\n# *Required* Comment to reply with\nsentimentBotReplyComment: >\n Please be sure to review the [Code of Conduct](https://docs.theholocron.dev/reference/code-of-conduct/) and be respectful of other users.\n";
|
|
2950
|
+
//#endregion
|
|
2951
|
+
//#region src/utils/create-header.ts
|
|
2952
|
+
function createHeader(options) {
|
|
2953
|
+
const { source, tool = "holocron setup", forPrimary = false } = options;
|
|
2954
|
+
const doNotEdit = forPrimary ? `AUTO-GENERATED — do not edit in theholocron/.github directly.` : `AUTO-GENERATED — do not edit directly.`;
|
|
2955
|
+
return {
|
|
2956
|
+
workflowHeader(format = "yaml") {
|
|
2957
|
+
if (format === "cjs") return [
|
|
2958
|
+
`/* ${doNotEdit}`,
|
|
2959
|
+
` * Source: theholocron/holocron · ${source}`,
|
|
2960
|
+
` * Tool: ${tool}`,
|
|
2961
|
+
` * Changes: edit source in theholocron/holocron`,
|
|
2962
|
+
` */`,
|
|
2963
|
+
``
|
|
2964
|
+
].join("\n");
|
|
2965
|
+
const yamlLines = [
|
|
2966
|
+
`# ${doNotEdit}`,
|
|
2967
|
+
`# Source: theholocron/holocron · ${source}`,
|
|
2968
|
+
`# Tool: ${tool}`,
|
|
2969
|
+
`# Changes: edit source in theholocron/holocron`,
|
|
2970
|
+
``
|
|
2971
|
+
].join("\n");
|
|
2972
|
+
if (format === "shebang") return `#!/bin/sh\n\n${yamlLines}`;
|
|
2973
|
+
return yamlLines;
|
|
2974
|
+
},
|
|
2975
|
+
scaffoldHeader() {
|
|
2976
|
+
return [
|
|
2977
|
+
`# Scaffolded by holocron setup — edit this file freely.`,
|
|
2978
|
+
`# Source: theholocron/holocron · ${source}`,
|
|
2979
|
+
``
|
|
2980
|
+
].join("\n");
|
|
2981
|
+
}
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
//#endregion
|
|
2985
|
+
//#region src/templates/configs/alexjs/alexignore
|
|
2986
|
+
var alexignore_default = ".github/*\nCHANGELOG.md\nLICENSE\n";
|
|
2987
|
+
//#endregion
|
|
2988
|
+
//#region src/templates/configs/alexjs/alexrc.json
|
|
2989
|
+
var alexrc_default = { allow: [
|
|
2990
|
+
"dead",
|
|
2991
|
+
"failure",
|
|
2992
|
+
"failures",
|
|
2993
|
+
"hook",
|
|
2994
|
+
"hooks",
|
|
2995
|
+
"husky",
|
|
2996
|
+
"period"
|
|
2997
|
+
] };
|
|
2998
|
+
//#endregion
|
|
2999
|
+
//#region src/templates/configs/alexjs/create-config.ts
|
|
3000
|
+
const { workflowHeader: workflowHeader$6 } = createHeader({ source: "packages/cli/src/templates/configs/alexjs/create-config.ts" });
|
|
3001
|
+
function createRcConfig() {
|
|
3002
|
+
return JSON.stringify(alexrc_default, null, 2) + "\n";
|
|
3003
|
+
}
|
|
3004
|
+
function createIgnoreConfig() {
|
|
3005
|
+
return `${workflowHeader$6()}${alexignore_default}`;
|
|
3006
|
+
}
|
|
3007
|
+
//#endregion
|
|
3008
|
+
//#region src/templates/configs/codecov/codecov.yml
|
|
3009
|
+
var codecov_default = "codecov:\n require_ci_to_pass: true\n\ncoverage:\n precision: 2\n round: down\n status:\n project:\n default:\n target: auto\n threshold: 2%\n patch:\n default:\n target: 80%\n\ncomment:\n layout: \"reach,diff,flags,components\"\n behavior: default\n require_changes: true\n\ncomponent_management:\n default_rules:\n statuses:\n - type: patch\n target: 80%\n individual_components:\n";
|
|
3010
|
+
//#endregion
|
|
3011
|
+
//#region src/templates/configs/codecov/utils.ts
|
|
3012
|
+
const INDIVIDUAL_COMPONENTS_MARKER = " individual_components:";
|
|
3013
|
+
function codecovComponentBlock(packages) {
|
|
3014
|
+
if (packages.length === 0) return "\n []\n";
|
|
3015
|
+
return "\n" + packages.flatMap(({ slug }) => [
|
|
3016
|
+
` - component_id: ${slug}`,
|
|
3017
|
+
` name: "${slug}"`,
|
|
3018
|
+
` paths:`,
|
|
3019
|
+
` - packages/${slug}/**`,
|
|
3020
|
+
``
|
|
3021
|
+
]).join("\n");
|
|
3022
|
+
}
|
|
3023
|
+
function mergeCodecovComponents(existing, packages) {
|
|
3024
|
+
const idx = existing.indexOf(INDIVIDUAL_COMPONENTS_MARKER);
|
|
3025
|
+
if (idx === -1) return existing;
|
|
3026
|
+
return existing.slice(0, idx + 24) + codecovComponentBlock(packages);
|
|
3027
|
+
}
|
|
3028
|
+
async function readWorkspacePackages(repoRoot) {
|
|
3029
|
+
const packagesDir = join(repoRoot, "packages");
|
|
3030
|
+
const entries = await readdir(packagesDir, { withFileTypes: true }).catch(() => null);
|
|
3031
|
+
if (!entries) return [];
|
|
3032
|
+
const packages = [];
|
|
3033
|
+
for (const entry of entries) {
|
|
3034
|
+
if (!entry.isDirectory()) continue;
|
|
3035
|
+
try {
|
|
3036
|
+
const raw = await readFile(join(packagesDir, entry.name, "package.json"), "utf8");
|
|
3037
|
+
const pkg = JSON.parse(raw);
|
|
3038
|
+
if (typeof pkg.name === "string") packages.push({
|
|
3039
|
+
slug: entry.name,
|
|
3040
|
+
name: pkg.name
|
|
3041
|
+
});
|
|
3042
|
+
} catch {}
|
|
3043
|
+
}
|
|
3044
|
+
return packages.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
3045
|
+
}
|
|
3046
|
+
//#endregion
|
|
3047
|
+
//#region src/templates/configs/codecov/create-config.ts
|
|
3048
|
+
const { scaffoldHeader } = createHeader({ source: "packages/cli/src/templates/configs/codecov/create-config.ts" });
|
|
3049
|
+
function createConfig$4(packages) {
|
|
3050
|
+
return `${scaffoldHeader()}${codecov_default.trimEnd()}${codecovComponentBlock(packages)}`;
|
|
3051
|
+
}
|
|
3052
|
+
//#endregion
|
|
3053
|
+
//#region src/templates/configs/devmoji/create-config.ts
|
|
3054
|
+
const { workflowHeader: workflowHeader$5 } = createHeader({ source: "packages/cli/src/templates/configs/devmoji/create-config.ts" });
|
|
3055
|
+
function createConfig$3() {
|
|
3056
|
+
return [
|
|
3057
|
+
workflowHeader$5("cjs"),
|
|
3058
|
+
`/* eslint-disable */`,
|
|
3059
|
+
`const { defineConfig } = require("@theholocron/devmoji-config");`,
|
|
3060
|
+
`module.exports = defineConfig();`,
|
|
3061
|
+
``
|
|
3062
|
+
].join("\n");
|
|
3063
|
+
}
|
|
3064
|
+
//#endregion
|
|
3065
|
+
//#region src/templates/configs/editorconfig/editorconfig
|
|
3066
|
+
var editorconfig_default = "root = true\n\n[*]\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\nindent_style = tab\nindent_size = 4\n\n[.gitattributes]\nindent_style = space\nindent_size = 2\n\n[*.{json,yml,yaml}]\nindent_style = space\nindent_size = 2\n\n[*.{md,mdx}]\ntrim_trailing_whitespace = false\n\n[.*{rc,ignore}]\nindent_style = space\nindent_size = 2\n";
|
|
3067
|
+
//#endregion
|
|
3068
|
+
//#region src/templates/configs/editorconfig/create-config.ts
|
|
3069
|
+
const { workflowHeader: workflowHeader$4 } = createHeader({ source: "packages/cli/src/templates/configs/editorconfig/create-config.ts" });
|
|
3070
|
+
function createConfig$2() {
|
|
3071
|
+
return `${workflowHeader$4()}${editorconfig_default}`;
|
|
3072
|
+
}
|
|
3073
|
+
//#endregion
|
|
3074
|
+
//#region src/templates/configs/editorconfig-checker/editorconfig-checker.json
|
|
3075
|
+
var editorconfig_checker_default = {
|
|
3076
|
+
Version: "v3.7.0",
|
|
3077
|
+
Verbose: false,
|
|
3078
|
+
Format: "",
|
|
3079
|
+
Debug: false,
|
|
3080
|
+
IgnoreDefaults: false,
|
|
3081
|
+
SpacesAfterTabs: false,
|
|
3082
|
+
NoColor: false,
|
|
3083
|
+
Exclude: [
|
|
3084
|
+
"(^|.+/)LICENSE$",
|
|
3085
|
+
"^public/.*",
|
|
3086
|
+
"\\.md$",
|
|
3087
|
+
"\\.mdx$"
|
|
3088
|
+
],
|
|
3089
|
+
AllowedContentTypes: [],
|
|
3090
|
+
PassedFiles: [],
|
|
3091
|
+
Disable: {
|
|
3092
|
+
"EndOfLine": false,
|
|
3093
|
+
"Indentation": false,
|
|
3094
|
+
"InsertFinalNewline": false,
|
|
3095
|
+
"TrimTrailingWhitespace": false,
|
|
3096
|
+
"IndentSize": false,
|
|
3097
|
+
"MaxLineLength": false
|
|
3098
|
+
}
|
|
3099
|
+
};
|
|
3100
|
+
//#endregion
|
|
3101
|
+
//#region src/templates/configs/editorconfig-checker/create-config.ts
|
|
3102
|
+
function createConfig$1() {
|
|
3103
|
+
return JSON.stringify(editorconfig_checker_default, null, 2) + "\n";
|
|
3104
|
+
}
|
|
3105
|
+
//#endregion
|
|
3106
|
+
//#region src/templates/configs/prepare-commit-msg/prepare-commit-msg
|
|
3107
|
+
var prepare_commit_msg_default = "NAME=$(git config user.name)\nEMAIL=$(git config user.email)\n\nif [ -z \"$NAME\" ]; then\n echo \"empty git config user.name\"\n exit 1\nfi\n\nif [ -z \"$EMAIL\" ]; then\n echo \"empty git config user.email\"\n exit 1\nfi\n\ngit interpret-trailers --if-exists doNothing --trailer \\\n \"Signed-off-by: $NAME <$EMAIL>\" \\\n --in-place \"$1\"\n\nnpx devmoji -e\n";
|
|
3108
|
+
//#endregion
|
|
3109
|
+
//#region src/templates/configs/prepare-commit-msg/create-config.ts
|
|
3110
|
+
const { workflowHeader: workflowHeader$3 } = createHeader({ source: "packages/cli/src/templates/configs/prepare-commit-msg/create-config.ts" });
|
|
3111
|
+
function createConfig() {
|
|
3112
|
+
return `${workflowHeader$3("shebang")}${prepare_commit_msg_default}`;
|
|
3113
|
+
}
|
|
3114
|
+
//#endregion
|
|
3115
|
+
//#region src/templates/dco.yml
|
|
3116
|
+
var dco_default = "allowRemediationCommits:\n individual: true\n";
|
|
3117
|
+
//#endregion
|
|
3118
|
+
//#region src/templates/dependabot.yml
|
|
3119
|
+
var dependabot_default = "version: 2\nupdates:\n - package-ecosystem: npm\n directory: /\n schedule:\n interval: weekly\n commit-message:\n prefix: \"chore(deps)\"\n prefix-development: \"chore(deps-dev)\"\n groups:\n security-patches:\n applies-to: security-updates\n patterns:\n - \"*\"\n all-dependencies:\n update-types:\n - minor\n - patch\n\n - package-ecosystem: github-actions\n directory: /\n schedule:\n interval: weekly\n commit-message:\n prefix: \"chore(deps)\"\n groups:\n all-actions:\n patterns:\n - \"*\"\n";
|
|
3120
|
+
//#endregion
|
|
3121
|
+
//#region src/templates/labeler.yml
|
|
3122
|
+
var labeler_default = "bug:\n - '^fix'\n\nchore:\n - '^chore(?!\\(deps)'\n\nci:\n - '^ci'\n\ndependencies:\n - '^chore\\(deps'\n\ndocumentation:\n - '^docs'\n\nenhancement:\n - '^feat'\n\nperformance:\n - '^perf'\n\nrefactor:\n - '^refactor'\n\ntest:\n - '^test'\n";
|
|
3123
|
+
//#endregion
|
|
3124
|
+
//#region src/commands/setup-workflows/index.ts
|
|
2819
3125
|
/**
|
|
2820
|
-
*
|
|
3126
|
+
* Thin workflow wrapper templates for `holocron setup`.
|
|
2821
3127
|
*
|
|
2822
|
-
*
|
|
2823
|
-
*
|
|
2824
|
-
*
|
|
3128
|
+
* Each entry is a complete `.github/workflows/<name>.yml` that delegates
|
|
3129
|
+
* to the corresponding reusable `ci-<name>.yml` in `theholocron/.github`.
|
|
3130
|
+
* Files are overwritten on each setup run — they are generated artifacts.
|
|
3131
|
+
*/
|
|
3132
|
+
const WORKFLOW_TEMPLATES = {
|
|
3133
|
+
lint: "name: Lint\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: lint-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n issues: write\n statuses: write\n\njobs:\n lint:\n name: Lint\n uses: theholocron/.github/.github/workflows/lint.yml@main\n secrets: inherit\n with:\n enable-auto-commit: true\n",
|
|
3134
|
+
test: "name: Test\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: test-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n id-token: write\n statuses: write\n\njobs:\n test:\n name: Test\n uses: theholocron/.github/.github/workflows/test.yml@main\n with:\n run-unit: true\n secrets: inherit\n",
|
|
3135
|
+
typecheck: "name: Typecheck\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: typecheck-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n typecheck:\n name: Typecheck\n uses: theholocron/.github/.github/workflows/typecheck.yml@main\n secrets: inherit\n",
|
|
3136
|
+
security: "name: Security\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n schedule:\n - cron: \"0 0 * * 1\"\n\npermissions:\n actions: read\n contents: read\n security-events: write\n\njobs:\n security:\n uses: theholocron/.github/.github/workflows/security.yml@main\n secrets: inherit\n",
|
|
3137
|
+
preview: "name: Preview\n\non: # yamllint disable-line rule:truthy\n pull_request:\n branches: [main]\n\nconcurrency:\n group: preview-${{ github.event.pull_request.number }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n deployments: write\n pull-requests: write\n\njobs:\n preview:\n name: Preview\n uses: theholocron/.github/.github/workflows/preview.yml@main\n secrets: inherit\n",
|
|
3138
|
+
review: "name: Review\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\nconcurrency:\n group: review-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n checks: write\n pull-requests: write\n\njobs:\n review:\n name: Review\n uses: theholocron/.github/.github/workflows/review.yml@main\n secrets: inherit\n",
|
|
3139
|
+
release: "name: Release\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n - alpha\n workflow_dispatch:\n inputs:\n dry_run:\n description: >\n Dry run — analyze commits and preview the release without git writes\n or publish. Push-triggered runs always run fully; this only applies\n to manual workflow_dispatch triggers.\n required: false\n default: true\n type: boolean\n\npermissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n release:\n uses: theholocron/.github/.github/workflows/release.yml@main\n with:\n dry-run: ${{ inputs.dry_run == true }}\n secrets: inherit\n",
|
|
3140
|
+
stale: "name: Stale\n\non: # yamllint disable-line rule:truthy\n schedule:\n - cron: \"30 1 * * *\"\n\npermissions:\n contents: write\n issues: write\n pull-requests: write\n\njobs:\n stale:\n uses: theholocron/.github/.github/workflows/stale.yml@main\n secrets: inherit\n",
|
|
3141
|
+
sync: "name: Sync\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n paths:\n - holocron.config.ts\n - package.json\n - pnpm-workspace.yaml\n workflow_dispatch:\n inputs:\n steps:\n description: \"Sync steps to run (default: all)\"\n type: string\n required: false\n\nconcurrency:\n group: sync-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n sync:\n name: Sync\n uses: theholocron/.github/.github/workflows/sync.yml@main\n with:\n steps: ${{ inputs.steps }}\n secrets: inherit\n",
|
|
3142
|
+
greetings: "name: Greetings\n\non: # yamllint disable-line rule:truthy\n pull_request:\n issues:\n\npermissions:\n issues: write\n pull-requests: write\n\njobs:\n greetings:\n uses: theholocron/.github/.github/workflows/greetings.yml@main\n secrets: inherit\n",
|
|
3143
|
+
dependencies: "name: Dependencies\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n dependencies:\n uses: theholocron/.github/.github/workflows/dependencies.yml@main\n secrets: inherit\n",
|
|
3144
|
+
bookkeeping: "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n pull_request:\n types:\n - opened\n - edited\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\n\njobs:\n bookkeeping:\n uses: theholocron/.github/.github/workflows/bookkeeping.yml@main\n secrets: inherit\n",
|
|
3145
|
+
audit: "name: Audit\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\npermissions:\n contents: read\n\njobs:\n audit:\n uses: theholocron/.github/.github/workflows/audit.yml@main\n secrets: inherit\n",
|
|
3146
|
+
deploy: "name: Deploy\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n workflow_dispatch:\n\nconcurrency:\n group: pages\n cancel-in-progress: false\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\njobs:\n deploy:\n name: Deploy\n uses: theholocron/.github/.github/workflows/deploy.yml@main\n secrets: inherit\n",
|
|
3147
|
+
wiki: "name: Wiki\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\nconcurrency:\n group: ${{ github.event_name == 'pull_request' && format('wiki-preview-{0}', github.event.pull_request.number) || 'wiki' }}\n cancel-in-progress: ${{ github.event_name == 'pull_request' }}\n\npermissions:\n contents: read\n deployments: write\n\njobs:\n publish:\n name: Publish\n if: ${{ github.event_name != 'pull_request' }}\n uses: theholocron/.github/.github/workflows/wiki.yml@main\n secrets: inherit\n\n preview:\n name: Preview\n if: ${{ github.event_name == 'pull_request' }}\n uses: theholocron/.github/.github/workflows/wiki.yml@main\n with:\n preview: true\n preview-id: pr-${{ github.event.pull_request.number }}\n secrets: inherit\n"
|
|
3148
|
+
};
|
|
3149
|
+
const KNOWN_WORKFLOWS = new Set(Object.keys(WORKFLOW_TEMPLATES));
|
|
3150
|
+
/**
|
|
3151
|
+
* GitHub check context name each CI workflow produces on a PR.
|
|
2825
3152
|
*
|
|
2826
|
-
*
|
|
3153
|
+
* The format is "{caller-workflow-name} / {reusable-job-name}". The caller
|
|
3154
|
+
* job's own `name:` field does NOT appear in the external check name — only
|
|
3155
|
+
* the calling workflow's top-level `name:` and the inner reusable-workflow
|
|
3156
|
+
* job name matter. Only workflows that gate merges are listed here.
|
|
2827
3157
|
*/
|
|
2828
|
-
const
|
|
2829
|
-
|
|
2830
|
-
|
|
3158
|
+
const WORKFLOW_CHECK_CONTEXTS = {
|
|
3159
|
+
lint: "Lint / Lint entire codebase",
|
|
3160
|
+
test: "Test / Run tests and collect coverage",
|
|
3161
|
+
typecheck: "Typecheck / tsc --noEmit"
|
|
3162
|
+
};
|
|
3163
|
+
/**
|
|
3164
|
+
* Generate the thin caller content for a workflow, optionally injecting or
|
|
3165
|
+
* merging `with:` overrides into the jobs block.
|
|
3166
|
+
*
|
|
3167
|
+
* Two strategies are used depending on the template:
|
|
3168
|
+
* - Templates that already have a `with:` block (e.g. lint):
|
|
3169
|
+
* the override entries are merged in, replacing existing keys and appending
|
|
3170
|
+
* new ones.
|
|
3171
|
+
* - Templates that end with ` secrets: inherit`: a new `with:` block is
|
|
3172
|
+
* injected immediately before `secrets: inherit`.
|
|
3173
|
+
* If neither pattern matches the template, a warning is emitted and the
|
|
3174
|
+
* base template is returned unchanged.
|
|
3175
|
+
*/
|
|
3176
|
+
function generateThinCallerContent(name, withOverrides, additionalPaths) {
|
|
3177
|
+
const base = WORKFLOW_TEMPLATES[name];
|
|
3178
|
+
if (!base) return "";
|
|
3179
|
+
const yamlScalar = (v) => {
|
|
3180
|
+
if (v === true) return "true";
|
|
3181
|
+
if (v === false) return "false";
|
|
3182
|
+
const s = String(v);
|
|
3183
|
+
return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
|
|
3184
|
+
};
|
|
3185
|
+
const fmt = (k, v) => ` ${k}: ${yamlScalar(v)}`;
|
|
3186
|
+
let result = base;
|
|
3187
|
+
if (additionalPaths && additionalPaths.length > 0) {
|
|
3188
|
+
const pathsBlockRe = /( {4}paths:\n)((?:[ ]{6}- [^\n]+\n)+)/;
|
|
3189
|
+
if (pathsBlockRe.test(result)) result = result.replace(pathsBlockRe, (_, header, existing) => {
|
|
3190
|
+
const existingPaths = new Set([...existing.matchAll(/- (.+)/g)].map((m) => m[1]));
|
|
3191
|
+
const newEntries = additionalPaths.filter((p) => !existingPaths.has(p)).map((p) => ` - ${p}\n`).join("");
|
|
3192
|
+
return header + existing + newEntries;
|
|
3193
|
+
});
|
|
3194
|
+
else {
|
|
3195
|
+
const pathsBlock = ` paths:\n${additionalPaths.map((p) => ` - ${p}\n`).join("")}`;
|
|
3196
|
+
result = result.replace(/( {4}branches: \[main\]\n)/, `$1${pathsBlock}`);
|
|
3197
|
+
}
|
|
3198
|
+
}
|
|
3199
|
+
if (!withOverrides || Object.keys(withOverrides).length === 0) return result;
|
|
3200
|
+
const withBlockRe = /( {4}with:\n)((?:[ ]{6}[^\n]+\n)*)/;
|
|
3201
|
+
const existingMatch = result.match(withBlockRe);
|
|
3202
|
+
if (existingMatch) {
|
|
3203
|
+
const existingEntries = new Map(existingMatch[2].split("\n").filter(Boolean).map((line) => {
|
|
3204
|
+
const m = line.match(/^ {6}([^:]+):\s*(.*)/);
|
|
3205
|
+
return m ? [m[1].trim(), m[2].trim()] : null;
|
|
3206
|
+
}).filter((e) => e !== null));
|
|
3207
|
+
for (const [k, v] of Object.entries(withOverrides)) existingEntries.set(k, yamlScalar(v));
|
|
3208
|
+
const merged = [...existingEntries.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
|
|
3209
|
+
return result.replace(withBlockRe, ` with:\n${merged}\n`);
|
|
3210
|
+
}
|
|
3211
|
+
const withBlock = Object.entries(withOverrides).map(([k, v]) => fmt(k, v)).join("\n");
|
|
3212
|
+
const injected = result.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
|
|
3213
|
+
if (injected === result) console.warn(`[generateThinCallerContent] could not inject with: overrides into "${name}" template`);
|
|
3214
|
+
return injected;
|
|
3215
|
+
}
|
|
3216
|
+
/**
|
|
3217
|
+
* Extract the Cloudflare Pages preview config from a deploy workflow's `with:` object.
|
|
3218
|
+
*
|
|
3219
|
+
* Accepts three forms:
|
|
3220
|
+
* - `preview: true` — derive both project and domain from org context
|
|
3221
|
+
* - `preview: { project: "..." }` — explicit project; domain derived from context if omitted
|
|
3222
|
+
* - `preview: { project: "...", domain: "..." }` — fully explicit
|
|
3223
|
+
*
|
|
3224
|
+
* Returns null when `preview:` is absent, false, or can't be resolved.
|
|
3225
|
+
*/
|
|
3226
|
+
function extractPreviewConfig(raw, ctx = {}) {
|
|
3227
|
+
const preview = raw["preview"];
|
|
3228
|
+
if (!preview) return null;
|
|
3229
|
+
if (preview === true) {
|
|
3230
|
+
const project = ctx.org ? `${ctx.org}-preview` : null;
|
|
3231
|
+
const domain = ctx.domain ? `preview.${ctx.domain}` : void 0;
|
|
3232
|
+
if (!project) return null;
|
|
3233
|
+
return {
|
|
3234
|
+
project,
|
|
3235
|
+
...domain ? { domain } : {}
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
3238
|
+
if (typeof preview !== "object") return null;
|
|
3239
|
+
const p = preview;
|
|
3240
|
+
const project = typeof p["project"] === "string" && p["project"] ? p["project"] : ctx.org ? `${ctx.org}-preview` : null;
|
|
3241
|
+
if (!project) return null;
|
|
3242
|
+
const domain = typeof p["domain"] === "string" && p["domain"] ? p["domain"] : ctx.domain ? `preview.${ctx.domain}` : void 0;
|
|
3243
|
+
return {
|
|
3244
|
+
project,
|
|
3245
|
+
...domain ? { domain } : {}
|
|
3246
|
+
};
|
|
3247
|
+
}
|
|
3248
|
+
/**
|
|
3249
|
+
* Generate the full thin-caller YAML for a `deploy.yml` that handles both
|
|
3250
|
+
* production (push to main → GitHub Pages) and preview (pull_request →
|
|
3251
|
+
* Cloudflare Pages) in a single file.
|
|
3252
|
+
*
|
|
3253
|
+
* Both jobs receive the same docs/storybook `with:` inputs. If the per-repo
|
|
3254
|
+
* config supplies `cloudflare-project` it is forwarded; otherwise the reusable
|
|
3255
|
+
* falls back to the `CLOUDFLARE_PAGES_PROJECT` org variable — set that once and
|
|
3256
|
+
* all repos with a `deploy` workflow get previews without per-repo config.
|
|
3257
|
+
*/
|
|
3258
|
+
function generateCombinedDeployContent(deployWith, paths, preview) {
|
|
3259
|
+
const yamlScalar = (v) => {
|
|
3260
|
+
if (v === true) return "true";
|
|
3261
|
+
if (v === false) return "false";
|
|
3262
|
+
const s = String(v);
|
|
3263
|
+
return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
|
|
3264
|
+
};
|
|
3265
|
+
const withLines = (entries) => Object.entries(entries).map(([k, v]) => ` ${k}: ${yamlScalar(v)}`).join("\n");
|
|
3266
|
+
const pathsBlock = paths.length > 0 ? ` paths:\n${paths.map((p) => ` - ${p}\n`).join("")}` : "";
|
|
3267
|
+
const previewWith = {
|
|
3268
|
+
...deployWith,
|
|
3269
|
+
"cloudflare-project": preview.project
|
|
3270
|
+
};
|
|
3271
|
+
const deployWithBlock = Object.keys(deployWith).length > 0 ? ` with:\n${withLines(deployWith)}\n` : "";
|
|
3272
|
+
const previewWithBlock = ` with:\n${withLines(previewWith)}\n`;
|
|
3273
|
+
return [
|
|
3274
|
+
`name: Deploy`,
|
|
3275
|
+
``,
|
|
3276
|
+
`on: # yamllint disable-line rule:truthy`,
|
|
3277
|
+
` push:`,
|
|
3278
|
+
` branches: [main]`,
|
|
3279
|
+
...pathsBlock ? [`${pathsBlock}`] : [],
|
|
3280
|
+
` pull_request:`,
|
|
3281
|
+
` branches: [main]`,
|
|
3282
|
+
` types: [opened, synchronize, reopened, closed]`,
|
|
3283
|
+
...pathsBlock ? [`${pathsBlock}`] : [],
|
|
3284
|
+
` workflow_dispatch:`,
|
|
3285
|
+
``,
|
|
3286
|
+
`concurrency:`,
|
|
3287
|
+
` group: $\{{ github.event_name == 'pull_request' && format('preview-{0}', github.event.pull_request.number) || 'pages' }}`,
|
|
3288
|
+
` cancel-in-progress: $\{{ github.event_name == 'pull_request' && github.event.action != 'closed' }}`,
|
|
3289
|
+
``,
|
|
3290
|
+
`permissions:`,
|
|
3291
|
+
` contents: read`,
|
|
3292
|
+
` deployments: write`,
|
|
3293
|
+
` pages: write`,
|
|
3294
|
+
` id-token: write`,
|
|
3295
|
+
` pull-requests: write`,
|
|
3296
|
+
``,
|
|
3297
|
+
`jobs:`,
|
|
3298
|
+
` deploy:`,
|
|
3299
|
+
` name: Deploy`,
|
|
3300
|
+
` if: \${{ github.event_name != 'pull_request' }}`,
|
|
3301
|
+
` uses: theholocron/.github/.github/workflows/deploy.yml@main`,
|
|
3302
|
+
...deployWithBlock ? [deployWithBlock.trimEnd()] : [],
|
|
3303
|
+
` secrets: inherit`,
|
|
3304
|
+
``,
|
|
3305
|
+
` preview:`,
|
|
3306
|
+
` name: Preview`,
|
|
3307
|
+
` if: \${{ github.event_name == 'pull_request' }}`,
|
|
3308
|
+
` uses: theholocron/.github/.github/workflows/preview.yml@main`,
|
|
3309
|
+
previewWithBlock.trimEnd(),
|
|
3310
|
+
` secrets: inherit`,
|
|
3311
|
+
``
|
|
3312
|
+
].join("\n");
|
|
3313
|
+
}
|
|
3314
|
+
/**
|
|
3315
|
+
* Expand structured with-values to flat GitHub Actions inputs before
|
|
3316
|
+
* generating the thin caller. Handles:
|
|
3317
|
+
* - deploy shorthand: docs/storybook → type + storybook-projects
|
|
3318
|
+
* - preview: stripped (handled separately via extractPreviewConfig)
|
|
3319
|
+
* - run-chromatic object → run-chromatic: true + chromatic-projects
|
|
3320
|
+
* - plain arrays → JSON-stringified for YAML scalar quoting
|
|
3321
|
+
*
|
|
3322
|
+
* Used by both `holocron setup` and `sync-workflow-templates`.
|
|
3323
|
+
*/
|
|
3324
|
+
function normalizeWorkflowWith(raw) {
|
|
3325
|
+
const result = { ...raw };
|
|
3326
|
+
delete result["preview"];
|
|
3327
|
+
const hasDocs = raw["docs"] === true || raw["docs"] !== null && typeof raw["docs"] === "object";
|
|
3328
|
+
const storybookProjects = raw["storybook"];
|
|
3329
|
+
if (hasDocs) {
|
|
3330
|
+
result["type"] = "docs";
|
|
3331
|
+
delete result["docs"];
|
|
3332
|
+
}
|
|
3333
|
+
if (Array.isArray(storybookProjects)) {
|
|
3334
|
+
if (!hasDocs) result["type"] = "storybook";
|
|
3335
|
+
result["storybook-projects"] = JSON.stringify(storybookProjects.map(({ name, path = "." }) => ({
|
|
3336
|
+
name,
|
|
3337
|
+
workingDir: path
|
|
3338
|
+
})));
|
|
3339
|
+
delete result["storybook"];
|
|
3340
|
+
}
|
|
3341
|
+
const runChromatic = raw["run-chromatic"];
|
|
3342
|
+
if (runChromatic !== null && typeof runChromatic === "object" && "projects" in runChromatic) {
|
|
3343
|
+
result["run-chromatic"] = true;
|
|
3344
|
+
const projects = runChromatic.projects.map((p) => ({
|
|
3345
|
+
...p,
|
|
3346
|
+
...Array.isArray(p.untraced) ? { untraced: p.untraced.join("\n") } : {}
|
|
3347
|
+
}));
|
|
3348
|
+
result["chromatic-projects"] = JSON.stringify(projects);
|
|
3349
|
+
}
|
|
3350
|
+
for (const [k, v] of Object.entries(result)) if (Array.isArray(v)) result[k] = JSON.stringify(v);
|
|
3351
|
+
return result;
|
|
3352
|
+
}
|
|
3353
|
+
/**
|
|
3354
|
+
* Derive on.push.paths entries from the deploy with: shorthand.
|
|
3355
|
+
* Used by both `holocron setup` and `sync-workflow-templates`.
|
|
3356
|
+
*/
|
|
3357
|
+
function deriveDeployPaths(raw) {
|
|
3358
|
+
const paths = [];
|
|
3359
|
+
const docs = raw["docs"];
|
|
3360
|
+
if (docs === true) {
|
|
3361
|
+
paths.push("docs/**");
|
|
3362
|
+
paths.push("astro.config.ts");
|
|
3363
|
+
paths.push("pnpm-workspace.yaml");
|
|
3364
|
+
paths.push("pnpm-lock.yaml");
|
|
3365
|
+
} else if (docs !== null && typeof docs === "object" && "path" in docs) {
|
|
3366
|
+
const p = docs.path;
|
|
3367
|
+
if (p && p !== ".") paths.push(`${p}/**`);
|
|
3368
|
+
}
|
|
3369
|
+
const storybookProjects = raw["storybook"];
|
|
3370
|
+
if (Array.isArray(storybookProjects)) for (const s of storybookProjects) {
|
|
3371
|
+
const p = s.path || ".";
|
|
3372
|
+
if (p === ".") {
|
|
3373
|
+
paths.push("src/**");
|
|
3374
|
+
paths.push(".storybook/**");
|
|
3375
|
+
} else paths.push(`${p}/**`);
|
|
3376
|
+
}
|
|
3377
|
+
return paths;
|
|
3378
|
+
}
|
|
3379
|
+
//#endregion
|
|
3380
|
+
//#region src/commands/setup/agent-prompts-data.ts
|
|
3381
|
+
/**
|
|
3382
|
+
* Canonical AI engineering workflow role prompts.
|
|
3383
|
+
*
|
|
3384
|
+
* Written to `.agents/prompts/<role>.md` by `holocron setup` when `agent` is
|
|
3385
|
+
* configured. Paths are gitignored and regenerated on every setup run so the
|
|
3386
|
+
* content always reflects the current CLI version.
|
|
3387
|
+
*
|
|
3388
|
+
* Source: .notes/ai-engineering-workflow.spec.md
|
|
3389
|
+
*/
|
|
3390
|
+
const DECISIONS_TEMPLATE = "---\nid: ADR-XXXX\ntitle: \"\"\nstatus: proposed\ndate: YYYY-MM-DD\nowners: []\nspecs: []\ndiscussion:\n github:\nsupersedes: []\nsuperseded-by: []\ntags: []\n---\n\n# [Short title of the decision]\n\n- Status: [proposed | accepted | rejected | deprecated | superseded by ADR-XXXX]\n- Date: YYYY-MM-DD\n\n## Context and Problem Statement\n\n2–3 sentences describing the situation that forced this decision.\n\n## Decision Drivers\n\n- [driver 1 — a constraint, goal, or value]\n- [driver 2]\n\n## Considered Options\n\n- [Option A]\n- [Option B]\n- [Option C — do nothing]\n\n## Decision Outcome\n\nChosen option: **[Option A]**, because [one-sentence justification].\n\n### Positive Consequences\n\n- …\n\n### Negative Consequences\n\n- …\n\n## Pros and Cons of the Options\n\n### [Option A]\n\n- Good, because [argument]\n- Bad, because [argument]\n\n### [Option B]\n\n- Good, because [argument]\n- Bad, because [argument]\n";
|
|
3391
|
+
const AGENT_PROMPTS = {
|
|
3392
|
+
"discovery.md": `# Discovery Agent
|
|
2831
3393
|
|
|
2832
3394
|
You are performing engineering discovery.
|
|
2833
3395
|
|
|
@@ -3203,660 +3765,34 @@ the system must do — not how to implement it.
|
|
|
3203
3765
|
> **Drafts** live in \`.notes/*.spec.md\` until accepted, then graduate here.
|
|
3204
3766
|
`;
|
|
3205
3767
|
//#endregion
|
|
3206
|
-
//#region src/commands/
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3768
|
+
//#region src/commands/setup/agent-prompts.ts
|
|
3769
|
+
const AGENTS_PROMPTS_ROOT = ".agents/prompts";
|
|
3770
|
+
const PROMPTS_GITIGNORE_START = "# managed by holocron setup — prompts";
|
|
3771
|
+
const PROMPTS_GITIGNORE_END = "# end managed by holocron setup — prompts";
|
|
3772
|
+
async function installAgentPrompts({ repoRoot }) {
|
|
3773
|
+
const promptsDir = join(repoRoot, AGENTS_PROMPTS_ROOT);
|
|
3774
|
+
await mkdir(promptsDir, { recursive: true });
|
|
3775
|
+
for (const [filename, content] of Object.entries(AGENT_PROMPTS)) await writeFile(join(promptsDir, filename), content, "utf8");
|
|
3776
|
+
const gitignorePath = join(repoRoot, ".gitignore");
|
|
3777
|
+
const existing = await readFile(gitignorePath, "utf8").catch(() => "");
|
|
3778
|
+
const block = [
|
|
3779
|
+
PROMPTS_GITIGNORE_START,
|
|
3780
|
+
`/${AGENTS_PROMPTS_ROOT}/`,
|
|
3781
|
+
PROMPTS_GITIGNORE_END
|
|
3782
|
+
].join("\n");
|
|
3783
|
+
let updated;
|
|
3784
|
+
if (existing.includes(PROMPTS_GITIGNORE_START)) {
|
|
3785
|
+
const start = existing.indexOf(PROMPTS_GITIGNORE_START);
|
|
3786
|
+
const end = existing.indexOf(PROMPTS_GITIGNORE_END, start);
|
|
3787
|
+
const afterBlock = end !== -1 ? existing.slice(end + 41) : "\n";
|
|
3788
|
+
updated = existing.slice(0, start) + block + afterBlock;
|
|
3789
|
+
} else updated = (existing.trimEnd() ? existing.trimEnd() + "\n\n" : "") + block + "\n";
|
|
3790
|
+
await writeFile(gitignorePath, updated, "utf8");
|
|
3791
|
+
return `wrote ${Object.keys(AGENT_PROMPTS).length} prompt files to ${AGENTS_PROMPTS_ROOT}/`;
|
|
3792
|
+
}
|
|
3211
3793
|
//#endregion
|
|
3212
|
-
//#region src/commands/
|
|
3213
|
-
var bookkeeping_default$1 = "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n pull_request:\n types:\n - opened\n - edited\n\npermissions:\n contents: read\n issues: write\n pull-requests: write\n\njobs:\n bookkeeping:\n uses: theholocron/.github/.github/workflows/bookkeeping.yml@main\n secrets: inherit\n";
|
|
3214
|
-
//#endregion
|
|
3215
|
-
//#region src/commands/workflows/codeql.yml
|
|
3216
|
-
var codeql_default$1 = "name: CodeQL\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n pull_request:\n branches:\n - main\n schedule:\n - cron: \"0 0 * * 1\"\n\npermissions:\n actions: read\n contents: read\n security-events: write\n\njobs:\n codeql:\n uses: theholocron/.github/.github/workflows/codeql.yml@main\n secrets: inherit\n";
|
|
3217
|
-
//#endregion
|
|
3218
|
-
//#region src/commands/workflows/dependencies.yml
|
|
3219
|
-
var dependencies_default$1 = "name: Dependencies\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n dependencies:\n uses: theholocron/.github/.github/workflows/dependencies.yml@main\n secrets: inherit\n";
|
|
3220
|
-
//#endregion
|
|
3221
|
-
//#region src/commands/workflows/deploy.yml
|
|
3222
|
-
var deploy_default$1 = "name: Deploy\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n workflow_dispatch:\n\nconcurrency:\n group: pages\n cancel-in-progress: false\n\npermissions:\n contents: read\n pages: write\n id-token: write\n\njobs:\n deploy:\n name: Deploy\n uses: theholocron/.github/.github/workflows/deploy.yml@main\n secrets: inherit\n";
|
|
3223
|
-
//#endregion
|
|
3224
|
-
//#region src/commands/workflows/greetings.yml
|
|
3225
|
-
var greetings_default$1 = "name: Greetings\n\non: # yamllint disable-line rule:truthy\n pull_request:\n issues:\n\npermissions:\n issues: write\n pull-requests: write\n\njobs:\n greetings:\n uses: theholocron/.github/.github/workflows/greetings.yml@main\n secrets: inherit\n";
|
|
3226
|
-
//#endregion
|
|
3227
|
-
//#region src/commands/workflows/lint.yml
|
|
3228
|
-
var lint_default$1 = "name: Lint\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: lint-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n issues: write\n statuses: write\n\njobs:\n lint:\n name: Lint\n uses: theholocron/.github/.github/workflows/lint.yml@main\n secrets: inherit\n with:\n enable-auto-commit: true\n";
|
|
3229
|
-
//#endregion
|
|
3230
|
-
//#region src/commands/workflows/post-release.yml
|
|
3231
|
-
var post_release_default$1 = "name: Post-release Sync\n\non: # yamllint disable-line rule:truthy\n release:\n types: [published]\n\npermissions:\n contents: read\n\njobs:\n broadcast:\n name: Post-release Sync\n uses: theholocron/.github/.github/workflows/post-release.yml@main\n secrets: inherit\n";
|
|
3232
|
-
//#endregion
|
|
3233
|
-
//#region src/commands/workflows/release.yml
|
|
3234
|
-
var release_default$1 = "name: Release\n\non: # yamllint disable-line rule:truthy\n push:\n branches:\n - main\n - alpha\n workflow_dispatch:\n inputs:\n dry_run:\n description: >\n Dry run — analyze commits and preview the release without git writes\n or publish. Push-triggered runs always run fully; this only applies\n to manual workflow_dispatch triggers.\n required: false\n default: true\n type: boolean\n\npermissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n\nconcurrency:\n group: ${{ github.workflow }}-${{ github.ref }}\n cancel-in-progress: false\n\njobs:\n release:\n uses: theholocron/.github/.github/workflows/release.yml@main\n with:\n dry-run: ${{ inputs.dry_run == true }}\n secrets: inherit\n";
|
|
3235
|
-
//#endregion
|
|
3236
|
-
//#region src/commands/workflows/review.yml
|
|
3237
|
-
var review_default$1 = "name: Review\n\non: # yamllint disable-line rule:truthy\n pull_request:\n\nconcurrency:\n group: review-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n checks: write\n pull-requests: write\n\njobs:\n review:\n name: Review\n uses: theholocron/.github/.github/workflows/review.yml@main\n secrets: inherit\n";
|
|
3238
|
-
//#endregion
|
|
3239
|
-
//#region src/commands/workflows/stale.yml
|
|
3240
|
-
var stale_default$1 = "name: Stale\n\non: # yamllint disable-line rule:truthy\n schedule:\n - cron: \"30 1 * * *\"\n\npermissions:\n contents: write\n issues: write\n pull-requests: write\n\njobs:\n stale:\n uses: theholocron/.github/.github/workflows/stale.yml@main\n secrets: inherit\n";
|
|
3241
|
-
//#endregion
|
|
3242
|
-
//#region src/commands/workflows/sync.yml
|
|
3243
|
-
var sync_default$1 = "name: Sync\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n paths:\n - holocron.config.ts\n - package.json\n - pnpm-workspace.yaml\n workflow_dispatch:\n inputs:\n steps:\n description: \"Sync steps to run (default: all)\"\n type: string\n required: false\n\nconcurrency:\n group: sync-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: write\n pull-requests: write\n\njobs:\n sync:\n name: Sync\n uses: theholocron/.github/.github/workflows/sync.yml@main\n with:\n steps: ${{ inputs.steps }}\n secrets: inherit\n";
|
|
3244
|
-
//#endregion
|
|
3245
|
-
//#region src/commands/workflows/test.yml
|
|
3246
|
-
var test_default$1 = "name: Test\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: test-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n id-token: write\n statuses: write\n\njobs:\n test:\n name: Test\n uses: theholocron/.github/.github/workflows/test.yml@main\n with:\n run-unit: true\n secrets: inherit\n";
|
|
3247
|
-
//#endregion
|
|
3248
|
-
//#region src/commands/workflows/typecheck.yml
|
|
3249
|
-
var typecheck_default$1 = "name: Typecheck\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main, alpha]\n pull_request:\n\nconcurrency:\n group: typecheck-${{ github.ref }}\n cancel-in-progress: true\n\npermissions:\n contents: read\n\njobs:\n typecheck:\n name: Typecheck\n uses: theholocron/.github/.github/workflows/typecheck.yml@main\n secrets: inherit\n";
|
|
3250
|
-
//#endregion
|
|
3251
|
-
//#region src/commands/workflows/wiki.yml
|
|
3252
|
-
var wiki_default$1 = "name: Wiki\n\non: # yamllint disable-line rule:truthy\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\nconcurrency:\n group: ${{ github.event_name == 'pull_request' && format('wiki-preview-{0}', github.event.pull_request.number) || 'wiki' }}\n cancel-in-progress: ${{ github.event_name == 'pull_request' }}\n\npermissions:\n contents: read\n deployments: write\n\njobs:\n publish:\n name: Publish\n if: ${{ github.event_name != 'pull_request' }}\n uses: theholocron/.github/.github/workflows/wiki.yml@main\n secrets: inherit\n\n preview:\n name: Preview\n if: ${{ github.event_name == 'pull_request' }}\n uses: theholocron/.github/.github/workflows/wiki.yml@main\n with:\n preview: true\n preview-id: pr-${{ github.event.pull_request.number }}\n secrets: inherit\n";
|
|
3253
|
-
//#endregion
|
|
3254
|
-
//#region src/commands/setup-workflows.ts
|
|
3255
|
-
/**
|
|
3256
|
-
* Header prepended to every auto-generated workflow thin caller.
|
|
3257
|
-
*
|
|
3258
|
-
* Used by both `holocron setup` (initial creation) and `holocron sync-github`
|
|
3259
|
-
* (subsequent updates) so the header is always identical regardless of which
|
|
3260
|
-
* command last wrote the file.
|
|
3261
|
-
*
|
|
3262
|
-
* @param source - path within theholocron/holocron that owns the template
|
|
3263
|
-
* @param forPrimary - true only when writing to theholocron/.github itself
|
|
3264
|
-
*/
|
|
3265
|
-
function workflowHeader(source = "packages/cli/src/commands/setup-workflows.ts", forPrimary = false, tool = "holocron sync-github") {
|
|
3266
|
-
return [
|
|
3267
|
-
forPrimary ? `# AUTO-GENERATED — do not edit in theholocron/.github directly.` : `# AUTO-GENERATED — do not edit directly.`,
|
|
3268
|
-
`# Source: theholocron/holocron · ${source}`,
|
|
3269
|
-
`# Tool: ${tool}`,
|
|
3270
|
-
`# Changes: edit source in theholocron/holocron and push to alpha or main.`,
|
|
3271
|
-
``
|
|
3272
|
-
].join("\n");
|
|
3273
|
-
}
|
|
3274
|
-
function scaffoldHeader(source = "packages/cli/src/commands/setup.ts") {
|
|
3275
|
-
return [
|
|
3276
|
-
`# Scaffolded by holocron setup — edit this file freely.`,
|
|
3277
|
-
`# Source: theholocron/holocron · ${source}`,
|
|
3278
|
-
``
|
|
3279
|
-
].join("\n");
|
|
3280
|
-
}
|
|
3281
|
-
const WORKFLOW_TEMPLATES = {
|
|
3282
|
-
lint: lint_default$1,
|
|
3283
|
-
test: test_default$1,
|
|
3284
|
-
typecheck: typecheck_default$1,
|
|
3285
|
-
codeql: codeql_default$1,
|
|
3286
|
-
review: review_default$1,
|
|
3287
|
-
"post-release": post_release_default$1,
|
|
3288
|
-
release: release_default$1,
|
|
3289
|
-
stale: stale_default$1,
|
|
3290
|
-
sync: sync_default$1,
|
|
3291
|
-
greetings: greetings_default$1,
|
|
3292
|
-
dependencies: dependencies_default$1,
|
|
3293
|
-
bookkeeping: bookkeeping_default$1,
|
|
3294
|
-
audit: audit_default$1,
|
|
3295
|
-
deploy: deploy_default$1,
|
|
3296
|
-
wiki: wiki_default$1
|
|
3297
|
-
};
|
|
3298
|
-
const KNOWN_WORKFLOWS = new Set(Object.keys(WORKFLOW_TEMPLATES));
|
|
3299
|
-
/**
|
|
3300
|
-
* GitHub check context name each CI workflow produces on a PR.
|
|
3301
|
-
*
|
|
3302
|
-
* The format is "{caller-workflow-name} / {reusable-job-name}". The caller
|
|
3303
|
-
* job's own `name:` field does NOT appear in the external check name — only
|
|
3304
|
-
* the calling workflow's top-level `name:` and the inner reusable-workflow
|
|
3305
|
-
* job name matter. Only workflows that gate merges are listed here.
|
|
3306
|
-
*/
|
|
3307
|
-
const WORKFLOW_CHECK_CONTEXTS = {
|
|
3308
|
-
lint: "Lint / Lint entire codebase",
|
|
3309
|
-
test: "Test / Run tests and collect coverage",
|
|
3310
|
-
typecheck: "Typecheck / tsc --noEmit"
|
|
3311
|
-
};
|
|
3312
|
-
/**
|
|
3313
|
-
* Generate the thin caller content for a workflow, optionally injecting or
|
|
3314
|
-
* merging `with:` overrides into the jobs block.
|
|
3315
|
-
*
|
|
3316
|
-
* Two strategies are used depending on the template:
|
|
3317
|
-
* - Templates that already have a `with:` block (e.g. lint):
|
|
3318
|
-
* the override entries are merged in, replacing existing keys and appending
|
|
3319
|
-
* new ones.
|
|
3320
|
-
* - Templates that end with ` secrets: inherit`: a new `with:` block is
|
|
3321
|
-
* injected immediately before `secrets: inherit`.
|
|
3322
|
-
* If neither pattern matches the template, a warning is emitted and the
|
|
3323
|
-
* base template is returned unchanged.
|
|
3324
|
-
*/
|
|
3325
|
-
function generateThinCallerContent(name, withOverrides, additionalPaths) {
|
|
3326
|
-
const base = WORKFLOW_TEMPLATES[name];
|
|
3327
|
-
if (!base) return "";
|
|
3328
|
-
const yamlScalar = (v) => {
|
|
3329
|
-
if (v === true) return "true";
|
|
3330
|
-
if (v === false) return "false";
|
|
3331
|
-
const s = String(v);
|
|
3332
|
-
return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
|
|
3333
|
-
};
|
|
3334
|
-
const fmt = (k, v) => ` ${k}: ${yamlScalar(v)}`;
|
|
3335
|
-
let result = base;
|
|
3336
|
-
if (additionalPaths && additionalPaths.length > 0) {
|
|
3337
|
-
const pathsBlockRe = /( {4}paths:\n)((?:[ ]{6}- [^\n]+\n)+)/;
|
|
3338
|
-
if (pathsBlockRe.test(result)) result = result.replace(pathsBlockRe, (_, header, existing) => {
|
|
3339
|
-
const existingPaths = new Set([...existing.matchAll(/- (.+)/g)].map((m) => m[1]));
|
|
3340
|
-
const newEntries = additionalPaths.filter((p) => !existingPaths.has(p)).map((p) => ` - ${p}\n`).join("");
|
|
3341
|
-
return header + existing + newEntries;
|
|
3342
|
-
});
|
|
3343
|
-
else {
|
|
3344
|
-
const pathsBlock = ` paths:\n${additionalPaths.map((p) => ` - ${p}\n`).join("")}`;
|
|
3345
|
-
result = result.replace(/( {4}branches: \[main\]\n)/, `$1${pathsBlock}`);
|
|
3346
|
-
}
|
|
3347
|
-
}
|
|
3348
|
-
if (!withOverrides || Object.keys(withOverrides).length === 0) return result;
|
|
3349
|
-
const withBlockRe = /( {4}with:\n)((?:[ ]{6}[^\n]+\n)*)/;
|
|
3350
|
-
const existingMatch = result.match(withBlockRe);
|
|
3351
|
-
if (existingMatch) {
|
|
3352
|
-
const existingEntries = new Map(existingMatch[2].split("\n").filter(Boolean).map((line) => {
|
|
3353
|
-
const m = line.match(/^ {6}([^:]+):\s*(.*)/);
|
|
3354
|
-
return m ? [m[1].trim(), m[2].trim()] : null;
|
|
3355
|
-
}).filter((e) => e !== null));
|
|
3356
|
-
for (const [k, v] of Object.entries(withOverrides)) existingEntries.set(k, yamlScalar(v));
|
|
3357
|
-
const merged = [...existingEntries.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
|
|
3358
|
-
return result.replace(withBlockRe, ` with:\n${merged}\n`);
|
|
3359
|
-
}
|
|
3360
|
-
const withBlock = Object.entries(withOverrides).map(([k, v]) => fmt(k, v)).join("\n");
|
|
3361
|
-
const injected = result.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
|
|
3362
|
-
if (injected === result) console.warn(`[generateThinCallerContent] could not inject with: overrides into "${name}" template`);
|
|
3363
|
-
return injected;
|
|
3364
|
-
}
|
|
3365
|
-
/**
|
|
3366
|
-
* Extract the Cloudflare Pages preview config from a deploy workflow's `with:` object.
|
|
3367
|
-
*
|
|
3368
|
-
* Accepts three forms:
|
|
3369
|
-
* - `preview: true` — derive both project and domain from org context
|
|
3370
|
-
* - `preview: { project: "..." }` — explicit project; domain derived from context if omitted
|
|
3371
|
-
* - `preview: { project: "...", domain: "..." }` — fully explicit
|
|
3372
|
-
*
|
|
3373
|
-
* Returns null when `preview:` is absent, false, or can't be resolved.
|
|
3374
|
-
*/
|
|
3375
|
-
function extractPreviewConfig(raw, ctx = {}) {
|
|
3376
|
-
const preview = raw["preview"];
|
|
3377
|
-
if (!preview) return null;
|
|
3378
|
-
if (preview === true) {
|
|
3379
|
-
const project = ctx.org ? `${ctx.org}-preview` : null;
|
|
3380
|
-
const domain = ctx.domain ? `preview.${ctx.domain}` : void 0;
|
|
3381
|
-
if (!project) return null;
|
|
3382
|
-
return {
|
|
3383
|
-
project,
|
|
3384
|
-
...domain ? { domain } : {}
|
|
3385
|
-
};
|
|
3386
|
-
}
|
|
3387
|
-
if (typeof preview !== "object") return null;
|
|
3388
|
-
const p = preview;
|
|
3389
|
-
const project = typeof p["project"] === "string" && p["project"] ? p["project"] : ctx.org ? `${ctx.org}-preview` : null;
|
|
3390
|
-
if (!project) return null;
|
|
3391
|
-
const domain = typeof p["domain"] === "string" && p["domain"] ? p["domain"] : ctx.domain ? `preview.${ctx.domain}` : void 0;
|
|
3392
|
-
return {
|
|
3393
|
-
project,
|
|
3394
|
-
...domain ? { domain } : {}
|
|
3395
|
-
};
|
|
3396
|
-
}
|
|
3397
|
-
/**
|
|
3398
|
-
* Generate the full thin-caller YAML for a `deploy.yml` that handles both
|
|
3399
|
-
* production (push to main → GitHub Pages) and preview (pull_request →
|
|
3400
|
-
* Cloudflare Pages) in a single file.
|
|
3401
|
-
*
|
|
3402
|
-
* Both jobs receive the same docs/storybook `with:` inputs. If the per-repo
|
|
3403
|
-
* config supplies `cloudflare-project` it is forwarded; otherwise the reusable
|
|
3404
|
-
* falls back to the `CLOUDFLARE_PAGES_PROJECT` org variable — set that once and
|
|
3405
|
-
* all repos with a `deploy` workflow get previews without per-repo config.
|
|
3406
|
-
*/
|
|
3407
|
-
function generateCombinedDeployContent(deployWith, paths, preview) {
|
|
3408
|
-
const yamlScalar = (v) => {
|
|
3409
|
-
if (v === true) return "true";
|
|
3410
|
-
if (v === false) return "false";
|
|
3411
|
-
const s = String(v);
|
|
3412
|
-
return s.startsWith("[") || s.startsWith("{") ? `'${s}'` : s;
|
|
3413
|
-
};
|
|
3414
|
-
const withLines = (entries) => Object.entries(entries).map(([k, v]) => ` ${k}: ${yamlScalar(v)}`).join("\n");
|
|
3415
|
-
const pathsBlock = paths.length > 0 ? ` paths:\n${paths.map((p) => ` - ${p}\n`).join("")}` : "";
|
|
3416
|
-
const previewWith = {
|
|
3417
|
-
...deployWith,
|
|
3418
|
-
"cloudflare-project": preview.project
|
|
3419
|
-
};
|
|
3420
|
-
const deployWithBlock = Object.keys(deployWith).length > 0 ? ` with:\n${withLines(deployWith)}\n` : "";
|
|
3421
|
-
const previewWithBlock = ` with:\n${withLines(previewWith)}\n`;
|
|
3422
|
-
const cleanupWithBlock = ` with:\n cloudflare-project: ${preview.project}`;
|
|
3423
|
-
return [
|
|
3424
|
-
`name: Deploy`,
|
|
3425
|
-
``,
|
|
3426
|
-
`on: # yamllint disable-line rule:truthy`,
|
|
3427
|
-
` push:`,
|
|
3428
|
-
` branches: [main]`,
|
|
3429
|
-
...pathsBlock ? [`${pathsBlock}`] : [],
|
|
3430
|
-
` pull_request:`,
|
|
3431
|
-
` branches: [main]`,
|
|
3432
|
-
` types: [opened, synchronize, reopened, closed]`,
|
|
3433
|
-
...pathsBlock ? [`${pathsBlock}`] : [],
|
|
3434
|
-
` workflow_dispatch:`,
|
|
3435
|
-
``,
|
|
3436
|
-
`concurrency:`,
|
|
3437
|
-
` group: $\{{ github.event_name == 'pull_request' && format('deploy-preview-{0}', github.event.pull_request.number) || 'pages' }}`,
|
|
3438
|
-
` cancel-in-progress: $\{{ github.event_name == 'pull_request' && github.event.action != 'closed' }}`,
|
|
3439
|
-
``,
|
|
3440
|
-
`permissions:`,
|
|
3441
|
-
` contents: read`,
|
|
3442
|
-
` deployments: write`,
|
|
3443
|
-
` pages: write`,
|
|
3444
|
-
` id-token: write`,
|
|
3445
|
-
` pull-requests: write`,
|
|
3446
|
-
``,
|
|
3447
|
-
`jobs:`,
|
|
3448
|
-
` deploy:`,
|
|
3449
|
-
` name: Deploy`,
|
|
3450
|
-
` if: \${{ github.event_name != 'pull_request' }}`,
|
|
3451
|
-
` uses: theholocron/.github/.github/workflows/deploy.yml@main`,
|
|
3452
|
-
...deployWithBlock ? [deployWithBlock.trimEnd()] : [],
|
|
3453
|
-
` secrets: inherit`,
|
|
3454
|
-
``,
|
|
3455
|
-
` preview:`,
|
|
3456
|
-
` name: Deploy Preview`,
|
|
3457
|
-
` if: \${{ github.event_name == 'pull_request' && github.event.action != 'closed' }}`,
|
|
3458
|
-
` uses: theholocron/.github/.github/workflows/deploy-preview.yml@main`,
|
|
3459
|
-
previewWithBlock.trimEnd(),
|
|
3460
|
-
` secrets: inherit`,
|
|
3461
|
-
``,
|
|
3462
|
-
` cleanup:`,
|
|
3463
|
-
` name: Clean up Preview`,
|
|
3464
|
-
` if: \${{ github.event_name == 'pull_request' && github.event.action == 'closed' }}`,
|
|
3465
|
-
` uses: theholocron/.github/.github/workflows/cleanup-preview.yml@main`,
|
|
3466
|
-
cleanupWithBlock,
|
|
3467
|
-
` secrets: inherit`,
|
|
3468
|
-
``
|
|
3469
|
-
].join("\n");
|
|
3470
|
-
}
|
|
3471
|
-
/**
|
|
3472
|
-
* Expand structured with-values to flat GitHub Actions inputs before
|
|
3473
|
-
* generating the thin caller. Handles:
|
|
3474
|
-
* - deploy shorthand: docs/storybook → type + storybook-projects
|
|
3475
|
-
* - preview: stripped (handled separately via extractPreviewConfig)
|
|
3476
|
-
* - run-chromatic object → run-chromatic: true + chromatic-projects
|
|
3477
|
-
* - plain arrays → JSON-stringified for YAML scalar quoting
|
|
3478
|
-
*
|
|
3479
|
-
* Used by both `holocron setup` and `sync-workflow-templates`.
|
|
3480
|
-
*/
|
|
3481
|
-
function normalizeWorkflowWith(raw) {
|
|
3482
|
-
const result = { ...raw };
|
|
3483
|
-
delete result["preview"];
|
|
3484
|
-
const hasDocs = raw["docs"] === true || raw["docs"] !== null && typeof raw["docs"] === "object";
|
|
3485
|
-
const storybookProjects = raw["storybook"];
|
|
3486
|
-
if (hasDocs) {
|
|
3487
|
-
result["type"] = "docs";
|
|
3488
|
-
delete result["docs"];
|
|
3489
|
-
}
|
|
3490
|
-
if (Array.isArray(storybookProjects)) {
|
|
3491
|
-
if (!hasDocs) result["type"] = "storybook";
|
|
3492
|
-
result["storybook-projects"] = JSON.stringify(storybookProjects.map(({ name, path = "." }) => ({
|
|
3493
|
-
name,
|
|
3494
|
-
workingDir: path
|
|
3495
|
-
})));
|
|
3496
|
-
delete result["storybook"];
|
|
3497
|
-
}
|
|
3498
|
-
const runChromatic = raw["run-chromatic"];
|
|
3499
|
-
if (runChromatic !== null && typeof runChromatic === "object" && "projects" in runChromatic) {
|
|
3500
|
-
result["run-chromatic"] = true;
|
|
3501
|
-
const projects = runChromatic.projects.map((p) => ({
|
|
3502
|
-
...p,
|
|
3503
|
-
...Array.isArray(p.untraced) ? { untraced: p.untraced.join("\n") } : {}
|
|
3504
|
-
}));
|
|
3505
|
-
result["chromatic-projects"] = JSON.stringify(projects);
|
|
3506
|
-
}
|
|
3507
|
-
for (const [k, v] of Object.entries(result)) if (Array.isArray(v)) result[k] = JSON.stringify(v);
|
|
3508
|
-
return result;
|
|
3509
|
-
}
|
|
3510
|
-
/**
|
|
3511
|
-
* Derive on.push.paths entries from the deploy with: shorthand.
|
|
3512
|
-
* Used by both `holocron setup` and `sync-workflow-templates`.
|
|
3513
|
-
*/
|
|
3514
|
-
function deriveDeployPaths(raw) {
|
|
3515
|
-
const paths = [];
|
|
3516
|
-
const docs = raw["docs"];
|
|
3517
|
-
if (docs === true) {
|
|
3518
|
-
paths.push("docs/**");
|
|
3519
|
-
paths.push("astro.config.ts");
|
|
3520
|
-
paths.push("pnpm-workspace.yaml");
|
|
3521
|
-
paths.push("pnpm-lock.yaml");
|
|
3522
|
-
} else if (docs !== null && typeof docs === "object" && "path" in docs) {
|
|
3523
|
-
const p = docs.path;
|
|
3524
|
-
if (p && p !== ".") paths.push(`${p}/**`);
|
|
3525
|
-
}
|
|
3526
|
-
const storybookProjects = raw["storybook"];
|
|
3527
|
-
if (Array.isArray(storybookProjects)) for (const s of storybookProjects) {
|
|
3528
|
-
const p = s.path || ".";
|
|
3529
|
-
if (p === ".") {
|
|
3530
|
-
paths.push("src/**");
|
|
3531
|
-
paths.push(".storybook/**");
|
|
3532
|
-
} else paths.push(`${p}/**`);
|
|
3533
|
-
}
|
|
3534
|
-
return paths;
|
|
3535
|
-
}
|
|
3536
|
-
//#endregion
|
|
3537
|
-
//#region src/commands/setup.ts
|
|
3538
|
-
/**
|
|
3539
|
-
* `holocron setup` — orchestrates per-capability setup actions across
|
|
3540
|
-
* every plugin loaded from `holocron.config.json`.
|
|
3541
|
-
*
|
|
3542
|
-
* Per CLAUDE.md soft-skip: each step is wrapped in a try/catch and
|
|
3543
|
-
* failures don't abort subsequent capabilities. The summary at the end
|
|
3544
|
-
* reports counts so the operator can see what worked + what didn't.
|
|
3545
|
-
*
|
|
3546
|
-
* Per the Standards: when `ctx.dryRun` is true, mutating calls are
|
|
3547
|
-
* replaced with "would" log lines. Read-only probes (e.g.,
|
|
3548
|
-
* `vault.list`) still run so the operator sees real state.
|
|
3549
|
-
*
|
|
3550
|
-
* The orchestrator knows about specific capability methods by name
|
|
3551
|
-
* (e.g., `source.enableVulnerabilityAlerts`). This deliberate coupling
|
|
3552
|
-
* makes the "what does setup do" contract explicit and concrete —
|
|
3553
|
-
* decoupling via a per-capability `setupSteps()` method would be more
|
|
3554
|
-
* extensible but pushes the same knowledge into N plugins instead of
|
|
3555
|
-
* one central place.
|
|
3556
|
-
*/
|
|
3557
|
-
function editorconfigContent() {
|
|
3558
|
-
return [
|
|
3559
|
-
workflowHeader("packages/cli/src/commands/setup.ts"),
|
|
3560
|
-
`root = true`,
|
|
3561
|
-
``,
|
|
3562
|
-
`[*]`,
|
|
3563
|
-
`end_of_line = lf`,
|
|
3564
|
-
`charset = utf-8`,
|
|
3565
|
-
`trim_trailing_whitespace = true`,
|
|
3566
|
-
`insert_final_newline = true`,
|
|
3567
|
-
`indent_style = tab`,
|
|
3568
|
-
`indent_size = 4`,
|
|
3569
|
-
``,
|
|
3570
|
-
`[.gitattributes]`,
|
|
3571
|
-
`indent_style = space`,
|
|
3572
|
-
`indent_size = 2`,
|
|
3573
|
-
``,
|
|
3574
|
-
`[*.{json,yml,yaml}]`,
|
|
3575
|
-
`indent_style = space`,
|
|
3576
|
-
`indent_size = 2`,
|
|
3577
|
-
``,
|
|
3578
|
-
`[*.{md,mdx}]`,
|
|
3579
|
-
`trim_trailing_whitespace = false`,
|
|
3580
|
-
``,
|
|
3581
|
-
`[.*{rc,ignore}]`,
|
|
3582
|
-
`indent_style = space`,
|
|
3583
|
-
`indent_size = 2`,
|
|
3584
|
-
``
|
|
3585
|
-
].join("\n");
|
|
3586
|
-
}
|
|
3587
|
-
const INDIVIDUAL_COMPONENTS_MARKER = " individual_components:";
|
|
3588
|
-
function codecovComponentBlock(packages) {
|
|
3589
|
-
if (packages.length === 0) return "\n []\n";
|
|
3590
|
-
return "\n" + packages.flatMap(({ slug }) => [
|
|
3591
|
-
` - component_id: ${slug}`,
|
|
3592
|
-
` name: "${slug}"`,
|
|
3593
|
-
` paths:`,
|
|
3594
|
-
` - packages/${slug}/**`,
|
|
3595
|
-
``
|
|
3596
|
-
]).join("\n");
|
|
3597
|
-
}
|
|
3598
|
-
function mergeCodecovComponents(existing, packages) {
|
|
3599
|
-
const idx = existing.indexOf(INDIVIDUAL_COMPONENTS_MARKER);
|
|
3600
|
-
if (idx === -1) return existing;
|
|
3601
|
-
return existing.slice(0, idx + 24) + codecovComponentBlock(packages);
|
|
3602
|
-
}
|
|
3603
|
-
function codecovContent(packages) {
|
|
3604
|
-
return [
|
|
3605
|
-
scaffoldHeader(),
|
|
3606
|
-
`codecov:`,
|
|
3607
|
-
` require_ci_to_pass: true`,
|
|
3608
|
-
``,
|
|
3609
|
-
`coverage:`,
|
|
3610
|
-
` precision: 2`,
|
|
3611
|
-
` round: down`,
|
|
3612
|
-
` status:`,
|
|
3613
|
-
` project:`,
|
|
3614
|
-
` default:`,
|
|
3615
|
-
` target: auto`,
|
|
3616
|
-
` threshold: 2%`,
|
|
3617
|
-
` patch:`,
|
|
3618
|
-
` default:`,
|
|
3619
|
-
` target: 80%`,
|
|
3620
|
-
``,
|
|
3621
|
-
`comment:`,
|
|
3622
|
-
` layout: "reach,diff,flags,components"`,
|
|
3623
|
-
` behavior: default`,
|
|
3624
|
-
` require_changes: true`,
|
|
3625
|
-
``,
|
|
3626
|
-
`component_management:`,
|
|
3627
|
-
` default_rules:`,
|
|
3628
|
-
` statuses:`,
|
|
3629
|
-
` - type: patch`,
|
|
3630
|
-
` target: 80%`,
|
|
3631
|
-
` individual_components:`
|
|
3632
|
-
].join("\n") + codecovComponentBlock(packages);
|
|
3633
|
-
}
|
|
3634
|
-
async function readWorkspacePackages(repoRoot) {
|
|
3635
|
-
const packagesDir = join(repoRoot, "packages");
|
|
3636
|
-
const entries = await readdir(packagesDir, { withFileTypes: true }).catch(() => null);
|
|
3637
|
-
if (!entries) return [];
|
|
3638
|
-
const packages = [];
|
|
3639
|
-
for (const entry of entries) {
|
|
3640
|
-
if (!entry.isDirectory()) continue;
|
|
3641
|
-
try {
|
|
3642
|
-
const raw = await readFile(join(packagesDir, entry.name, "package.json"), "utf8");
|
|
3643
|
-
const pkg = JSON.parse(raw);
|
|
3644
|
-
if (typeof pkg.name === "string") packages.push({
|
|
3645
|
-
slug: entry.name,
|
|
3646
|
-
name: pkg.name
|
|
3647
|
-
});
|
|
3648
|
-
} catch {}
|
|
3649
|
-
}
|
|
3650
|
-
return packages.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
3651
|
-
}
|
|
3652
|
-
const EDITORCONFIG_CHECKER_CONFIG = JSON.stringify({
|
|
3653
|
-
Version: "v3.7.0",
|
|
3654
|
-
Verbose: false,
|
|
3655
|
-
Format: "",
|
|
3656
|
-
Debug: false,
|
|
3657
|
-
IgnoreDefaults: false,
|
|
3658
|
-
SpacesAfterTabs: false,
|
|
3659
|
-
NoColor: false,
|
|
3660
|
-
Exclude: [
|
|
3661
|
-
"(^|.+/)LICENSE$",
|
|
3662
|
-
"^public/.*",
|
|
3663
|
-
"\\.md$",
|
|
3664
|
-
"\\.mdx$"
|
|
3665
|
-
],
|
|
3666
|
-
AllowedContentTypes: [],
|
|
3667
|
-
PassedFiles: [],
|
|
3668
|
-
Disable: {
|
|
3669
|
-
EndOfLine: false,
|
|
3670
|
-
Indentation: false,
|
|
3671
|
-
InsertFinalNewline: false,
|
|
3672
|
-
TrimTrailingWhitespace: false,
|
|
3673
|
-
IndentSize: false,
|
|
3674
|
-
MaxLineLength: false
|
|
3675
|
-
}
|
|
3676
|
-
}, null, 2) + "\n";
|
|
3677
|
-
const ALEX_CONFIG = JSON.stringify({ allow: [
|
|
3678
|
-
"dead",
|
|
3679
|
-
"failure",
|
|
3680
|
-
"failures",
|
|
3681
|
-
"hook",
|
|
3682
|
-
"hooks",
|
|
3683
|
-
"husky",
|
|
3684
|
-
"period"
|
|
3685
|
-
] }, null, 2) + "\n";
|
|
3686
|
-
function devmojiConfigContent() {
|
|
3687
|
-
return [
|
|
3688
|
-
`/* eslint-disable */`,
|
|
3689
|
-
`// devmoji.config.cjs — generated by holocron setup, do not edit`,
|
|
3690
|
-
`// https://github.com/folke/devmoji`,
|
|
3691
|
-
`const { defineConfig } = require("@theholocron/devmoji-config");`,
|
|
3692
|
-
`module.exports = defineConfig();`,
|
|
3693
|
-
``
|
|
3694
|
-
].join("\n");
|
|
3695
|
-
}
|
|
3696
|
-
function prepareCommitMsgHookContent() {
|
|
3697
|
-
return [
|
|
3698
|
-
`#!/bin/sh`,
|
|
3699
|
-
``,
|
|
3700
|
-
`NAME=$(git config user.name)`,
|
|
3701
|
-
`EMAIL=$(git config user.email)`,
|
|
3702
|
-
``,
|
|
3703
|
-
`if [ -z "$NAME" ]; then`,
|
|
3704
|
-
`\techo "empty git config user.name"`,
|
|
3705
|
-
`\texit 1`,
|
|
3706
|
-
`fi`,
|
|
3707
|
-
``,
|
|
3708
|
-
`if [ -z "$EMAIL" ]; then`,
|
|
3709
|
-
`\techo "empty git config user.email"`,
|
|
3710
|
-
`\texit 1`,
|
|
3711
|
-
`fi`,
|
|
3712
|
-
``,
|
|
3713
|
-
`git interpret-trailers --if-exists doNothing --trailer \\`,
|
|
3714
|
-
`\t"Signed-off-by: $NAME <$EMAIL>" \\`,
|
|
3715
|
-
`\t--in-place "$1"`,
|
|
3716
|
-
``,
|
|
3717
|
-
`npx devmoji -e`,
|
|
3718
|
-
``
|
|
3719
|
-
].join("\n");
|
|
3720
|
-
}
|
|
3721
|
-
const CANONICAL_LABELS = [
|
|
3722
|
-
{
|
|
3723
|
-
name: "bug",
|
|
3724
|
-
color: "d73a4a",
|
|
3725
|
-
description: "Something isn't working"
|
|
3726
|
-
},
|
|
3727
|
-
{
|
|
3728
|
-
name: "chore",
|
|
3729
|
-
color: "ededed",
|
|
3730
|
-
description: "Maintenance, no user-facing change"
|
|
3731
|
-
},
|
|
3732
|
-
{
|
|
3733
|
-
name: "ci",
|
|
3734
|
-
color: "0075ca",
|
|
3735
|
-
description: "CI/CD pipeline changes"
|
|
3736
|
-
},
|
|
3737
|
-
{
|
|
3738
|
-
name: "dependencies",
|
|
3739
|
-
color: "0366d6",
|
|
3740
|
-
description: "Dependency update"
|
|
3741
|
-
},
|
|
3742
|
-
{
|
|
3743
|
-
name: "documentation",
|
|
3744
|
-
color: "0075ca",
|
|
3745
|
-
description: "Documentation only"
|
|
3746
|
-
},
|
|
3747
|
-
{
|
|
3748
|
-
name: "duplicate",
|
|
3749
|
-
color: "cfd3d7",
|
|
3750
|
-
description: "Already reported"
|
|
3751
|
-
},
|
|
3752
|
-
{
|
|
3753
|
-
name: "enhancement",
|
|
3754
|
-
color: "a2eeef",
|
|
3755
|
-
description: "New feature or request"
|
|
3756
|
-
},
|
|
3757
|
-
{
|
|
3758
|
-
name: "good first issue",
|
|
3759
|
-
color: "7057ff",
|
|
3760
|
-
description: "Good for newcomers"
|
|
3761
|
-
},
|
|
3762
|
-
{
|
|
3763
|
-
name: "help wanted",
|
|
3764
|
-
color: "008672",
|
|
3765
|
-
description: "Extra attention needed"
|
|
3766
|
-
},
|
|
3767
|
-
{
|
|
3768
|
-
name: "invalid",
|
|
3769
|
-
color: "e4e669",
|
|
3770
|
-
description: "Doesn't seem right"
|
|
3771
|
-
},
|
|
3772
|
-
{
|
|
3773
|
-
name: "performance",
|
|
3774
|
-
color: "fbca04",
|
|
3775
|
-
description: "Performance improvement"
|
|
3776
|
-
},
|
|
3777
|
-
{
|
|
3778
|
-
name: "question",
|
|
3779
|
-
color: "d876e3",
|
|
3780
|
-
description: "Further information requested"
|
|
3781
|
-
},
|
|
3782
|
-
{
|
|
3783
|
-
name: "refactor",
|
|
3784
|
-
color: "cfd3d7",
|
|
3785
|
-
description: "Code restructuring"
|
|
3786
|
-
},
|
|
3787
|
-
{
|
|
3788
|
-
name: "released",
|
|
3789
|
-
color: "ededed",
|
|
3790
|
-
description: "Included in a release"
|
|
3791
|
-
},
|
|
3792
|
-
{
|
|
3793
|
-
name: "test",
|
|
3794
|
-
color: "bfd4f2",
|
|
3795
|
-
description: "Test-related changes"
|
|
3796
|
-
},
|
|
3797
|
-
{
|
|
3798
|
-
name: "triage",
|
|
3799
|
-
color: "e4e669",
|
|
3800
|
-
description: "Needs investigation"
|
|
3801
|
-
},
|
|
3802
|
-
{
|
|
3803
|
-
name: "wontfix",
|
|
3804
|
-
color: "ffffff",
|
|
3805
|
-
description: "Won't be addressed"
|
|
3806
|
-
}
|
|
3807
|
-
];
|
|
3808
|
-
const STALE_LABELS = [
|
|
3809
|
-
"github_actions",
|
|
3810
|
-
"javascript",
|
|
3811
|
-
"autorelease: pending",
|
|
3812
|
-
"autorelease: tagged",
|
|
3813
|
-
"released on @alpha"
|
|
3814
|
-
];
|
|
3815
|
-
function labelerConfig() {
|
|
3816
|
-
return [
|
|
3817
|
-
workflowHeader("packages/cli/src/commands/setup.ts"),
|
|
3818
|
-
`bug:`,
|
|
3819
|
-
` - '^fix'`,
|
|
3820
|
-
``,
|
|
3821
|
-
`chore:`,
|
|
3822
|
-
` - '^chore(?!\\(deps)'`,
|
|
3823
|
-
``,
|
|
3824
|
-
`ci:`,
|
|
3825
|
-
` - '^ci'`,
|
|
3826
|
-
``,
|
|
3827
|
-
`dependencies:`,
|
|
3828
|
-
` - '^chore\\(deps'`,
|
|
3829
|
-
``,
|
|
3830
|
-
`documentation:`,
|
|
3831
|
-
` - '^docs'`,
|
|
3832
|
-
``,
|
|
3833
|
-
`enhancement:`,
|
|
3834
|
-
` - '^feat'`,
|
|
3835
|
-
``,
|
|
3836
|
-
`performance:`,
|
|
3837
|
-
` - '^perf'`,
|
|
3838
|
-
``,
|
|
3839
|
-
`refactor:`,
|
|
3840
|
-
` - '^refactor'`,
|
|
3841
|
-
``,
|
|
3842
|
-
`test:`,
|
|
3843
|
-
` - '^test'`,
|
|
3844
|
-
``
|
|
3845
|
-
].join("\n");
|
|
3846
|
-
}
|
|
3794
|
+
//#region src/commands/setup/branch-protection.ts
|
|
3847
3795
|
const RULESET_NAME = "holocron-default-branch";
|
|
3848
|
-
const BALANCED_REPO_SETTINGS = {
|
|
3849
|
-
allow_squash_merge: true,
|
|
3850
|
-
allow_merge_commit: false,
|
|
3851
|
-
allow_rebase_merge: false,
|
|
3852
|
-
allow_auto_merge: true,
|
|
3853
|
-
allow_update_branch: true,
|
|
3854
|
-
delete_branch_on_merge: true,
|
|
3855
|
-
has_issues: true,
|
|
3856
|
-
has_discussions: true,
|
|
3857
|
-
has_projects: true,
|
|
3858
|
-
has_wiki: false
|
|
3859
|
-
};
|
|
3860
3796
|
function buildClassicProtectionPayload(requiredChecks = []) {
|
|
3861
3797
|
return {
|
|
3862
3798
|
required_status_checks: requiredChecks.length > 0 ? {
|
|
@@ -3969,6 +3905,278 @@ async function upsertBranchProtection(source, dryRun, requiredChecks) {
|
|
|
3969
3905
|
};
|
|
3970
3906
|
}
|
|
3971
3907
|
}
|
|
3908
|
+
//#endregion
|
|
3909
|
+
//#region src/commands/setup/engineering.ts
|
|
3910
|
+
async function writeIfAbsent(filePath, content) {
|
|
3911
|
+
try {
|
|
3912
|
+
await access(filePath);
|
|
3913
|
+
return false;
|
|
3914
|
+
} catch {
|
|
3915
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
3916
|
+
await writeFile(filePath, content, "utf8");
|
|
3917
|
+
return true;
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
async function installEngineeringStructure({ repoRoot }) {
|
|
3921
|
+
const results = [];
|
|
3922
|
+
const writes = [
|
|
3923
|
+
[join(repoRoot, "docs/wiki/decisions/template.md"), DECISIONS_TEMPLATE],
|
|
3924
|
+
[join(repoRoot, "docs/wiki/decisions/README.md"), DECISIONS_README],
|
|
3925
|
+
[join(repoRoot, "docs/wiki/standards/README.md"), STANDARDS_README],
|
|
3926
|
+
[join(repoRoot, "docs/wiki/specifications/README.md"), SPECIFICATIONS_README]
|
|
3927
|
+
];
|
|
3928
|
+
for (const [path, content] of writes) if (await writeIfAbsent(path, content)) results.push(path.replace(repoRoot + "/", ""));
|
|
3929
|
+
return results.length > 0 ? `created: ${results.join(", ")}` : "all files already exist — nothing to write";
|
|
3930
|
+
}
|
|
3931
|
+
//#endregion
|
|
3932
|
+
//#region src/commands/setup/repo-settings.ts
|
|
3933
|
+
const BALANCED_REPO_SETTINGS = {
|
|
3934
|
+
allow_squash_merge: true,
|
|
3935
|
+
allow_merge_commit: false,
|
|
3936
|
+
allow_rebase_merge: false,
|
|
3937
|
+
allow_auto_merge: true,
|
|
3938
|
+
allow_update_branch: true,
|
|
3939
|
+
delete_branch_on_merge: true,
|
|
3940
|
+
has_issues: true,
|
|
3941
|
+
has_discussions: true,
|
|
3942
|
+
has_projects: true,
|
|
3943
|
+
has_wiki: false
|
|
3944
|
+
};
|
|
3945
|
+
//#endregion
|
|
3946
|
+
//#region src/commands/setup/run-step.ts
|
|
3947
|
+
async function runStep(capability, step, dryRun, body, opts = {}) {
|
|
3948
|
+
if (dryRun) return {
|
|
3949
|
+
capability,
|
|
3950
|
+
step,
|
|
3951
|
+
status: "dry-run"
|
|
3952
|
+
};
|
|
3953
|
+
try {
|
|
3954
|
+
const note = await body();
|
|
3955
|
+
const result = {
|
|
3956
|
+
capability,
|
|
3957
|
+
step,
|
|
3958
|
+
status: "ok"
|
|
3959
|
+
};
|
|
3960
|
+
if (typeof note === "string") result.message = note;
|
|
3961
|
+
return result;
|
|
3962
|
+
} catch (err) {
|
|
3963
|
+
if (err instanceof ProviderApiError$1) {
|
|
3964
|
+
if (err.status !== void 0 && opts.skipCodes?.includes(err.status)) return {
|
|
3965
|
+
capability,
|
|
3966
|
+
step,
|
|
3967
|
+
status: "skip",
|
|
3968
|
+
message: err.message
|
|
3969
|
+
};
|
|
3970
|
+
if (err.status === 403) {
|
|
3971
|
+
const reason = classify403(err);
|
|
3972
|
+
return {
|
|
3973
|
+
capability,
|
|
3974
|
+
step,
|
|
3975
|
+
status: reason === "plan" ? "skip" : "fail",
|
|
3976
|
+
message: err.message,
|
|
3977
|
+
reason
|
|
3978
|
+
};
|
|
3979
|
+
}
|
|
3980
|
+
}
|
|
3981
|
+
return {
|
|
3982
|
+
capability,
|
|
3983
|
+
step,
|
|
3984
|
+
status: "fail",
|
|
3985
|
+
message: err instanceof Error ? err.message : String(err)
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
function classify403(err) {
|
|
3990
|
+
const detailText = typeof err.details === "string" ? err.details : typeof err.details === "object" && err.details !== null && "message" in err.details ? String(err.details.message) : "";
|
|
3991
|
+
const text = `${err.message} ${detailText}`.toLowerCase();
|
|
3992
|
+
if (text.includes("advanced security") || text.includes("not enabled for this repository") || text.includes("upgrade") || text.includes("not available on")) return "plan";
|
|
3993
|
+
return "permissions";
|
|
3994
|
+
}
|
|
3995
|
+
function formatStep(step) {
|
|
3996
|
+
const tag = step.reason === "permissions" ? " [permissions]" : step.reason === "plan" ? " [plan restriction]" : "";
|
|
3997
|
+
const detail = step.message ? style.dim(` (${step.message})`) : "";
|
|
3998
|
+
const label = `${step.step}${tag}${detail}`;
|
|
3999
|
+
if (step.status === "ok") return ` ${style.success(label)}`;
|
|
4000
|
+
if (step.status === "fail") return ` ${style.fail(label)}`;
|
|
4001
|
+
if (step.status === "dry-run") return ` ${style.dim(`… ${label}`)}`;
|
|
4002
|
+
return ` ${style.dim(`· ${label}`)}`;
|
|
4003
|
+
}
|
|
4004
|
+
//#endregion
|
|
4005
|
+
//#region src/commands/setup/skills.ts
|
|
4006
|
+
async function fetchExternalSkill(entry) {
|
|
4007
|
+
if (entry.sourceType !== "github")
|
|
4008
|
+
/* c8 ignore next */
|
|
4009
|
+
throw new Error(`unsupported sourceType: ${entry.sourceType}`);
|
|
4010
|
+
const url = `https://raw.githubusercontent.com/${entry.source}/HEAD/${entry.skillPath}`;
|
|
4011
|
+
const res = await fetch(url);
|
|
4012
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
4013
|
+
const content = await res.text();
|
|
4014
|
+
return {
|
|
4015
|
+
content,
|
|
4016
|
+
stale: !!entry.computedHash && createHash("sha256").update(content).digest("hex") !== entry.computedHash
|
|
4017
|
+
};
|
|
4018
|
+
}
|
|
4019
|
+
const AGENTS_SKILLS_ROOT = ".agents/skills";
|
|
4020
|
+
/** Relative path of the agent-specific symlink. undefined = unsupported agent. */
|
|
4021
|
+
const AGENT_SYMLINK_PATHS = { claude: (name) => `.claude/skills/${name}` };
|
|
4022
|
+
const GITIGNORE_BLOCK_START = "# managed by holocron setup — skills";
|
|
4023
|
+
const GITIGNORE_BLOCK_END = "# end managed by holocron setup — skills";
|
|
4024
|
+
async function installSkills({ agent, skills, repoRoot }) {
|
|
4025
|
+
const symlinkFn = AGENT_SYMLINK_PATHS[agent];
|
|
4026
|
+
if (!symlinkFn) return `agent "${agent}" has no known skill install path — skipping`;
|
|
4027
|
+
const require = createRequire(pathToFileURL(join(repoRoot, "package.json")));
|
|
4028
|
+
let skillsRoot;
|
|
4029
|
+
try {
|
|
4030
|
+
skillsRoot = dirname(require.resolve("@theholocron/skills/package.json"));
|
|
4031
|
+
} catch {
|
|
4032
|
+
if (spawnSync("pnpm", [
|
|
4033
|
+
"add",
|
|
4034
|
+
"-D",
|
|
4035
|
+
"@theholocron/skills"
|
|
4036
|
+
], {
|
|
4037
|
+
cwd: repoRoot,
|
|
4038
|
+
stdio: "inherit"
|
|
4039
|
+
}).status !== 0) throw new Error("failed to auto-install @theholocron/skills");
|
|
4040
|
+
try {
|
|
4041
|
+
skillsRoot = dirname(require.resolve("@theholocron/skills/package.json"));
|
|
4042
|
+
} catch {
|
|
4043
|
+
throw new Error("failed to auto-install @theholocron/skills");
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
const gitignorePath = join(repoRoot, ".gitignore");
|
|
4047
|
+
const existingContent = await readFile(gitignorePath, "utf8").catch(() => "");
|
|
4048
|
+
const previouslyInstalled = parsePreviousSkills(existingContent, symlinkFn);
|
|
4049
|
+
const currentSet = new Set(skills);
|
|
4050
|
+
const stale = previouslyInstalled.filter((n) => !currentSet.has(n));
|
|
4051
|
+
for (const name of stale) {
|
|
4052
|
+
await rm(join(repoRoot, symlinkFn(name)), { force: true }).catch(() => void 0);
|
|
4053
|
+
await rm(join(repoRoot, AGENTS_SKILLS_ROOT, name), {
|
|
4054
|
+
recursive: true,
|
|
4055
|
+
force: true
|
|
4056
|
+
}).catch(() => void 0);
|
|
4057
|
+
}
|
|
4058
|
+
const installed = [];
|
|
4059
|
+
const missing = [];
|
|
4060
|
+
for (const name of skills) {
|
|
4061
|
+
const srcDir = join(skillsRoot, "src", name);
|
|
4062
|
+
try {
|
|
4063
|
+
await stat(srcDir);
|
|
4064
|
+
} catch {
|
|
4065
|
+
missing.push(name);
|
|
4066
|
+
continue;
|
|
4067
|
+
}
|
|
4068
|
+
const agentsDir = join(repoRoot, AGENTS_SKILLS_ROOT, name);
|
|
4069
|
+
await copyDirRecursive(srcDir, agentsDir);
|
|
4070
|
+
const symlinkPath = join(repoRoot, symlinkFn(name));
|
|
4071
|
+
await mkdir(dirname(symlinkPath), { recursive: true });
|
|
4072
|
+
try {
|
|
4073
|
+
await unlink(symlinkPath);
|
|
4074
|
+
} catch {}
|
|
4075
|
+
await symlink(relative(dirname(symlinkPath), agentsDir).replace(/\\/g, "/"), symlinkPath);
|
|
4076
|
+
installed.push(name);
|
|
4077
|
+
}
|
|
4078
|
+
const externalFailed = [];
|
|
4079
|
+
const externalStale = [];
|
|
4080
|
+
if (missing.length > 0) {
|
|
4081
|
+
let lock = null;
|
|
4082
|
+
try {
|
|
4083
|
+
lock = JSON.parse(await readFile(join(skillsRoot, "skills-lock.json"), "utf8"));
|
|
4084
|
+
} catch {}
|
|
4085
|
+
if (lock?.skills) for (const name of [...missing]) {
|
|
4086
|
+
const entry = lock.skills[name];
|
|
4087
|
+
if (!entry) continue;
|
|
4088
|
+
try {
|
|
4089
|
+
const { content, stale: isStale } = await fetchExternalSkill(entry);
|
|
4090
|
+
const agentsDir = join(repoRoot, AGENTS_SKILLS_ROOT, name);
|
|
4091
|
+
await mkdir(agentsDir, { recursive: true });
|
|
4092
|
+
await writeFile(join(agentsDir, "SKILL.md"), content);
|
|
4093
|
+
const symlinkPath = join(repoRoot, symlinkFn(name));
|
|
4094
|
+
await mkdir(dirname(symlinkPath), { recursive: true });
|
|
4095
|
+
try {
|
|
4096
|
+
await unlink(symlinkPath);
|
|
4097
|
+
} catch {}
|
|
4098
|
+
await symlink(relative(dirname(symlinkPath), agentsDir).replace(/\\/g, "/"), symlinkPath);
|
|
4099
|
+
missing.splice(missing.indexOf(name), 1);
|
|
4100
|
+
installed.push(name);
|
|
4101
|
+
if (isStale) externalStale.push(name);
|
|
4102
|
+
} catch {
|
|
4103
|
+
missing.splice(missing.indexOf(name), 1);
|
|
4104
|
+
externalFailed.push(name);
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
if (installed.length > 0 || stale.length > 0 || missing.length > 0 || externalFailed.length > 0) await updateSkillsGitignore(gitignorePath, existingContent, [
|
|
4109
|
+
...installed,
|
|
4110
|
+
...missing,
|
|
4111
|
+
...externalFailed
|
|
4112
|
+
], symlinkFn);
|
|
4113
|
+
const parts = [`installed ${installed.length}`];
|
|
4114
|
+
if (stale.length > 0) parts.push(`pruned: ${stale.join(", ")}`);
|
|
4115
|
+
if (externalStale.length > 0) parts.push(`stale: ${externalStale.join(", ")} (run \`holocron skills update\` to refresh)`);
|
|
4116
|
+
if (externalFailed.length > 0) parts.push(`fetch failed: ${externalFailed.join(", ")}`);
|
|
4117
|
+
if (missing.length > 0) parts.push(`unknown: ${missing.join(", ")}`);
|
|
4118
|
+
return parts.join("; ");
|
|
4119
|
+
}
|
|
4120
|
+
function parsePreviousSkills(gitignoreContent, symlinkFn) {
|
|
4121
|
+
if (!gitignoreContent.includes(GITIGNORE_BLOCK_START)) return [];
|
|
4122
|
+
const startIdx = gitignoreContent.indexOf(GITIGNORE_BLOCK_START);
|
|
4123
|
+
const endIdx = gitignoreContent.indexOf(GITIGNORE_BLOCK_END, startIdx);
|
|
4124
|
+
const block = endIdx !== -1 ? gitignoreContent.slice(startIdx, endIdx) : gitignoreContent.slice(startIdx);
|
|
4125
|
+
const placeholder = "__placeholder__";
|
|
4126
|
+
const symlinkPrefix = `/${symlinkFn(placeholder)}`.replace(placeholder, "");
|
|
4127
|
+
return block.split("\n").filter((line) => line.startsWith(symlinkPrefix)).map((line) => line.slice(symlinkPrefix.length));
|
|
4128
|
+
}
|
|
4129
|
+
async function copyDirRecursive(src, dest) {
|
|
4130
|
+
await mkdir(dest, { recursive: true });
|
|
4131
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
4132
|
+
for (const entry of entries) {
|
|
4133
|
+
const srcPath = join(src, entry.name);
|
|
4134
|
+
const destPath = join(dest, entry.name);
|
|
4135
|
+
if (entry.isDirectory()) await copyDirRecursive(srcPath, destPath);
|
|
4136
|
+
else await copyFile(srcPath, destPath);
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
async function updateSkillsGitignore(gitignorePath, existingContent, skills, symlinkFn) {
|
|
4140
|
+
const entries = [`/${AGENTS_SKILLS_ROOT}/`, ...skills.map((n) => `/${symlinkFn(n)}`)];
|
|
4141
|
+
const block = [
|
|
4142
|
+
GITIGNORE_BLOCK_START,
|
|
4143
|
+
...entries,
|
|
4144
|
+
GITIGNORE_BLOCK_END
|
|
4145
|
+
].join("\n");
|
|
4146
|
+
let content;
|
|
4147
|
+
if (existingContent.includes(GITIGNORE_BLOCK_START)) {
|
|
4148
|
+
const start = existingContent.indexOf(GITIGNORE_BLOCK_START);
|
|
4149
|
+
const end = existingContent.indexOf(GITIGNORE_BLOCK_END, start);
|
|
4150
|
+
const afterBlock = end !== -1 ? existingContent.slice(end + 40) : "\n";
|
|
4151
|
+
content = existingContent.slice(0, start) + block + afterBlock;
|
|
4152
|
+
} else content = (existingContent.trimEnd() ? existingContent.trimEnd() + "\n\n" : "") + block + "\n";
|
|
4153
|
+
await writeFile(gitignorePath, content, "utf8");
|
|
4154
|
+
}
|
|
4155
|
+
//#endregion
|
|
4156
|
+
//#region src/commands/setup/run-setup.ts
|
|
4157
|
+
/**
|
|
4158
|
+
* `holocron setup` — orchestrates per-capability setup actions across
|
|
4159
|
+
* every plugin loaded from `holocron.config.json`.
|
|
4160
|
+
*
|
|
4161
|
+
* Per CLAUDE.md soft-skip: each step is wrapped in a try/catch and
|
|
4162
|
+
* failures don't abort subsequent capabilities. The summary at the end
|
|
4163
|
+
* reports counts so the operator can see what worked + what didn't.
|
|
4164
|
+
*
|
|
4165
|
+
* Per the Standards: when `ctx.dryRun` is true, mutating calls are
|
|
4166
|
+
* replaced with "would" log lines. Read-only probes (e.g.,
|
|
4167
|
+
* `vault.list`) still run so the operator sees real state.
|
|
4168
|
+
*
|
|
4169
|
+
* The orchestrator knows about specific capability methods by name
|
|
4170
|
+
* (e.g., `source.enableVulnerabilityAlerts`). This deliberate coupling
|
|
4171
|
+
* makes the "what does setup do" contract explicit and concrete —
|
|
4172
|
+
* decoupling via a per-capability `setupSteps()` method would be more
|
|
4173
|
+
* extensible but pushes the same knowledge into N plugins instead of
|
|
4174
|
+
* one central place.
|
|
4175
|
+
*/
|
|
4176
|
+
const { workflowHeader: workflowHeader$2 } = createHeader({
|
|
4177
|
+
source: "packages/cli/src/commands/setup/run-setup.ts",
|
|
4178
|
+
tool: "holocron setup"
|
|
4179
|
+
});
|
|
3972
4180
|
async function runSetup(input) {
|
|
3973
4181
|
const print = input.print ?? ((line) => console.log(line));
|
|
3974
4182
|
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
@@ -4056,14 +4264,14 @@ async function runSetup(input) {
|
|
|
4056
4264
|
if (previewCfg) {
|
|
4057
4265
|
const paths = additionalPaths;
|
|
4058
4266
|
steps.push(await runStep("source", "write workflow deploy (with preview)", dryRun, async () => {
|
|
4059
|
-
await source.writeWorkflowFile("deploy.yml", workflowHeader()
|
|
4267
|
+
await source.writeWorkflowFile("deploy.yml", `${workflowHeader$2()}${generateCombinedDeployContent(withOverrides, paths, previewCfg)}`);
|
|
4060
4268
|
}));
|
|
4061
4269
|
print(formatStep(steps[steps.length - 1]));
|
|
4062
4270
|
continue;
|
|
4063
4271
|
}
|
|
4064
4272
|
}
|
|
4065
4273
|
steps.push(await runStep("source", `write workflow ${name}`, dryRun, async () => {
|
|
4066
|
-
await source.writeWorkflowFile(`${name}.yml`, workflowHeader()
|
|
4274
|
+
await source.writeWorkflowFile(`${name}.yml`, `${workflowHeader$2()}${generateThinCallerContent(name, withOverrides, additionalPaths)}`);
|
|
4067
4275
|
}));
|
|
4068
4276
|
print(formatStep(steps[steps.length - 1]));
|
|
4069
4277
|
}
|
|
@@ -4071,37 +4279,49 @@ async function runSetup(input) {
|
|
|
4071
4279
|
if (loader.has("source") && (config.workflows ?? []).map((e) => typeof e === "string" ? e : e.name).includes("bookkeeping")) {
|
|
4072
4280
|
const source = loader.get("source");
|
|
4073
4281
|
steps.push(await runStep("source", "write .github/labeler.yml", dryRun, async () => {
|
|
4074
|
-
await source.writeRepoFile(".github/labeler.yml",
|
|
4282
|
+
await source.writeRepoFile(".github/labeler.yml", `${workflowHeader$2()}${labeler_default}`);
|
|
4075
4283
|
}));
|
|
4076
4284
|
print(formatStep(steps[steps.length - 1]));
|
|
4077
4285
|
}
|
|
4078
4286
|
if (loader.has("source") && effectivePreset !== "none") {
|
|
4079
4287
|
const source = loader.get("source");
|
|
4080
4288
|
steps.push(await runStep("source", "write .github/dependabot.yml", dryRun, async () => {
|
|
4081
|
-
await source.writeRepoFile(".github/dependabot.yml", workflowHeader(
|
|
4289
|
+
await source.writeRepoFile(".github/dependabot.yml", `${workflowHeader$2()}${dependabot_default}`);
|
|
4290
|
+
}));
|
|
4291
|
+
print(formatStep(steps[steps.length - 1]));
|
|
4292
|
+
steps.push(await runStep("source", "write .github/dco.yml", dryRun, async () => {
|
|
4293
|
+
await source.writeRepoFile(".github/dco.yml", `${workflowHeader$2()}${dco_default}`);
|
|
4082
4294
|
}));
|
|
4083
4295
|
print(formatStep(steps[steps.length - 1]));
|
|
4084
4296
|
}
|
|
4085
4297
|
if (loader.has("source")) {
|
|
4086
4298
|
const source = loader.get("source");
|
|
4299
|
+
steps.push(await runStep("source", "write .github/config.yml", dryRun, async () => {
|
|
4300
|
+
await source.writeRepoFile(".github/config.yml", `${workflowHeader$2()}${config_default}`);
|
|
4301
|
+
}));
|
|
4302
|
+
print(formatStep(steps[steps.length - 1]));
|
|
4087
4303
|
steps.push(await runStep("source", "write .alexrc.json", dryRun, async () => {
|
|
4088
|
-
await source.writeRepoFile(".alexrc.json",
|
|
4304
|
+
await source.writeRepoFile(".alexrc.json", createRcConfig());
|
|
4305
|
+
}));
|
|
4306
|
+
print(formatStep(steps[steps.length - 1]));
|
|
4307
|
+
steps.push(await runStep("source", "write .alexignore", dryRun, async () => {
|
|
4308
|
+
await source.writeRepoFile(".alexignore", createIgnoreConfig());
|
|
4089
4309
|
}));
|
|
4090
4310
|
print(formatStep(steps[steps.length - 1]));
|
|
4091
4311
|
steps.push(await runStep("source", "write .editorconfig", dryRun, async () => {
|
|
4092
|
-
await source.writeRepoFile(".editorconfig",
|
|
4312
|
+
await source.writeRepoFile(".editorconfig", createConfig$2());
|
|
4093
4313
|
}));
|
|
4094
4314
|
print(formatStep(steps[steps.length - 1]));
|
|
4095
4315
|
steps.push(await runStep("source", "write .editorconfig-checker.json", dryRun, async () => {
|
|
4096
|
-
await source.writeRepoFile(".editorconfig-checker.json",
|
|
4316
|
+
await source.writeRepoFile(".editorconfig-checker.json", createConfig$1());
|
|
4097
4317
|
}));
|
|
4098
4318
|
print(formatStep(steps[steps.length - 1]));
|
|
4099
4319
|
steps.push(await runStep("source", "write devmoji.config.cjs", dryRun, async () => {
|
|
4100
|
-
await source.writeRepoFile("devmoji.config.cjs",
|
|
4320
|
+
await source.writeRepoFile("devmoji.config.cjs", createConfig$3());
|
|
4101
4321
|
}));
|
|
4102
4322
|
print(formatStep(steps[steps.length - 1]));
|
|
4103
4323
|
steps.push(await runStep("source", "write .husky/prepare-commit-msg", dryRun, async () => {
|
|
4104
|
-
await source.writeRepoFile(".husky/prepare-commit-msg",
|
|
4324
|
+
await source.writeRepoFile(".husky/prepare-commit-msg", createConfig());
|
|
4105
4325
|
}));
|
|
4106
4326
|
print(formatStep(steps[steps.length - 1]));
|
|
4107
4327
|
{
|
|
@@ -4115,7 +4335,7 @@ async function runSetup(input) {
|
|
|
4115
4335
|
message: "no test workflow configured"
|
|
4116
4336
|
});
|
|
4117
4337
|
else steps.push(await runStep("source", "write codecov.yml", dryRun, async () => {
|
|
4118
|
-
const content = existing != null ? mergeCodecovComponents(existing, packages) :
|
|
4338
|
+
const content = existing != null ? mergeCodecovComponents(existing, packages) : createConfig$4(packages);
|
|
4119
4339
|
await source.writeRepoFile("codecov.yml", content);
|
|
4120
4340
|
return packages.length > 0 ? `${packages.length} components` : "no components";
|
|
4121
4341
|
}));
|
|
@@ -4418,296 +4638,42 @@ async function runSetup(input) {
|
|
|
4418
4638
|
const summaryLine = ` ${summary.ok} ok, ${summary.fail} fail, ${summary.skip} skipped${dryRun ? `, ${summary.dryRun} would-do` : ""}`;
|
|
4419
4639
|
print(summary.fail > 0 ? style.fail(summaryLine.trim()) : style.success(summaryLine.trim()));
|
|
4420
4640
|
const skippedSteps = steps.filter((s) => s.status === "skip");
|
|
4421
|
-
if (skippedSteps.length > 0) {
|
|
4422
|
-
print("");
|
|
4423
|
-
print(style.hint(" Skipped:"));
|
|
4424
|
-
for (const s of skippedSteps)
|
|
4425
|
-
/* v8 ignore next -- all skip steps set message; empty fallback is defensive */
|
|
4426
|
-
print(style.hint(` · ${s.step}${s.message ? ` (${s.message})` : ""}`));
|
|
4427
|
-
}
|
|
4428
|
-
if (steps.some((s) => s.reason === "permissions")) {
|
|
4429
|
-
print("");
|
|
4430
|
-
print(style.warn("Some steps failed with 403 (insufficient token permissions)."));
|
|
4431
|
-
print(style.hint(" Repo-scoped operations (rulesets, settings, workflows) require a"));
|
|
4432
|
-
print(style.hint(" fine-grained PAT passed via --token or HOLOCRON_ADMIN_TOKEN:"));
|
|
4433
|
-
print("");
|
|
4434
|
-
print(style.hint(" · Administration — read and write"));
|
|
4435
|
-
print(style.hint(" · Code scanning alerts — read and write"));
|
|
4436
|
-
print(style.hint(" · Contents — read and write"));
|
|
4437
|
-
print(style.hint(" · Secret scanning alerts — read and write"));
|
|
4438
|
-
print(style.hint(" · Workflows — read and write"));
|
|
4439
|
-
print(style.hint(" · Metadata — read (added automatically)"));
|
|
4440
|
-
print("");
|
|
4441
|
-
print(style.hint(" Org-scoped operations (teams, custom properties) require"));
|
|
4442
|
-
print(style.hint(" HOLOCRON_ORG_TOKEN — a fine-grained PAT with resource owner set to the org:"));
|
|
4443
|
-
print("");
|
|
4444
|
-
print(style.hint(" · Administration — read and write (repository permission)"));
|
|
4445
|
-
print(style.hint(" · Members — read (organization permission)"));
|
|
4446
|
-
print(style.hint(" · Organization custom properties — read and write (organization permission)"));
|
|
4447
|
-
print(style.hint(" · Metadata — read (repository permission, auto-included)"));
|
|
4448
|
-
print("");
|
|
4449
|
-
print(style.hint(" Create tokens at: https://github.com/settings/personal-access-tokens/new"));
|
|
4450
|
-
print(style.hint(" Then re-run: holocron setup --token <your-admin-pat>"));
|
|
4451
|
-
print(style.hint(" Store org token: HOLOCRON_ORG_TOKEN env var or keyring key github.org"));
|
|
4452
|
-
}
|
|
4453
|
-
return {
|
|
4454
|
-
steps,
|
|
4455
|
-
summary
|
|
4456
|
-
};
|
|
4457
|
-
}
|
|
4458
|
-
/**
|
|
4459
|
-
* Fetch a single SKILL.md from its upstream GitHub source.
|
|
4460
|
-
* Verifies the SHA-256 hash when `computedHash` is present in the lock entry.
|
|
4461
|
-
*/
|
|
4462
|
-
async function fetchExternalSkill(entry) {
|
|
4463
|
-
if (entry.sourceType !== "github") throw new Error(`unsupported sourceType: ${entry.sourceType}`);
|
|
4464
|
-
const url = `https://raw.githubusercontent.com/${entry.source}/HEAD/${entry.skillPath}`;
|
|
4465
|
-
const res = await fetch(url);
|
|
4466
|
-
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
4467
|
-
const content = await res.text();
|
|
4468
|
-
return {
|
|
4469
|
-
content,
|
|
4470
|
-
stale: !!entry.computedHash && createHash("sha256").update(content).digest("hex") !== entry.computedHash
|
|
4471
|
-
};
|
|
4472
|
-
}
|
|
4473
|
-
const AGENTS_SKILLS_ROOT = ".agents/skills";
|
|
4474
|
-
/** Relative path of the agent-specific symlink. undefined = unsupported agent. */
|
|
4475
|
-
const AGENT_SYMLINK_PATHS = { claude: (name) => `.claude/skills/${name}` };
|
|
4476
|
-
const GITIGNORE_BLOCK_START = "# managed by holocron setup — skills";
|
|
4477
|
-
const GITIGNORE_BLOCK_END = "# end managed by holocron setup — skills";
|
|
4478
|
-
async function installSkills({ agent, skills, repoRoot }) {
|
|
4479
|
-
const symlinkFn = AGENT_SYMLINK_PATHS[agent];
|
|
4480
|
-
if (!symlinkFn) return `agent "${agent}" has no known skill install path — skipping`;
|
|
4481
|
-
const require = createRequire(pathToFileURL(join(repoRoot, "package.json")));
|
|
4482
|
-
let skillsRoot;
|
|
4483
|
-
try {
|
|
4484
|
-
skillsRoot = dirname(require.resolve("@theholocron/skills/package.json"));
|
|
4485
|
-
} catch {
|
|
4486
|
-
if (spawnSync("pnpm", [
|
|
4487
|
-
"add",
|
|
4488
|
-
"-D",
|
|
4489
|
-
"@theholocron/skills"
|
|
4490
|
-
], {
|
|
4491
|
-
cwd: repoRoot,
|
|
4492
|
-
stdio: "inherit"
|
|
4493
|
-
}).status !== 0) throw new Error("failed to auto-install @theholocron/skills");
|
|
4494
|
-
try {
|
|
4495
|
-
skillsRoot = dirname(require.resolve("@theholocron/skills/package.json"));
|
|
4496
|
-
} catch {
|
|
4497
|
-
throw new Error("failed to auto-install @theholocron/skills");
|
|
4498
|
-
}
|
|
4499
|
-
}
|
|
4500
|
-
const gitignorePath = join(repoRoot, ".gitignore");
|
|
4501
|
-
const existingContent = await readFile(gitignorePath, "utf8").catch(() => "");
|
|
4502
|
-
const previouslyInstalled = parsePreviousSkills(existingContent, symlinkFn);
|
|
4503
|
-
const currentSet = new Set(skills);
|
|
4504
|
-
const stale = previouslyInstalled.filter((n) => !currentSet.has(n));
|
|
4505
|
-
for (const name of stale) {
|
|
4506
|
-
await rm(join(repoRoot, symlinkFn(name)), { force: true }).catch(() => void 0);
|
|
4507
|
-
await rm(join(repoRoot, AGENTS_SKILLS_ROOT, name), {
|
|
4508
|
-
recursive: true,
|
|
4509
|
-
force: true
|
|
4510
|
-
}).catch(() => void 0);
|
|
4511
|
-
}
|
|
4512
|
-
const installed = [];
|
|
4513
|
-
const missing = [];
|
|
4514
|
-
for (const name of skills) {
|
|
4515
|
-
const srcDir = join(skillsRoot, "src", name);
|
|
4516
|
-
try {
|
|
4517
|
-
await stat(srcDir);
|
|
4518
|
-
} catch {
|
|
4519
|
-
missing.push(name);
|
|
4520
|
-
continue;
|
|
4521
|
-
}
|
|
4522
|
-
const agentsDir = join(repoRoot, AGENTS_SKILLS_ROOT, name);
|
|
4523
|
-
await copyDirRecursive(srcDir, agentsDir);
|
|
4524
|
-
const symlinkPath = join(repoRoot, symlinkFn(name));
|
|
4525
|
-
await mkdir(dirname(symlinkPath), { recursive: true });
|
|
4526
|
-
try {
|
|
4527
|
-
await unlink(symlinkPath);
|
|
4528
|
-
} catch {}
|
|
4529
|
-
await symlink(relative(dirname(symlinkPath), agentsDir).replace(/\\/g, "/"), symlinkPath);
|
|
4530
|
-
installed.push(name);
|
|
4531
|
-
}
|
|
4532
|
-
const externalFailed = [];
|
|
4533
|
-
const externalStale = [];
|
|
4534
|
-
if (missing.length > 0) {
|
|
4535
|
-
let lock = null;
|
|
4536
|
-
try {
|
|
4537
|
-
lock = JSON.parse(await readFile(join(skillsRoot, "skills-lock.json"), "utf8"));
|
|
4538
|
-
} catch {}
|
|
4539
|
-
if (lock?.skills) for (const name of [...missing]) {
|
|
4540
|
-
const entry = lock.skills[name];
|
|
4541
|
-
if (!entry) continue;
|
|
4542
|
-
try {
|
|
4543
|
-
const { content, stale: isStale } = await fetchExternalSkill(entry);
|
|
4544
|
-
const agentsDir = join(repoRoot, AGENTS_SKILLS_ROOT, name);
|
|
4545
|
-
await mkdir(agentsDir, { recursive: true });
|
|
4546
|
-
await writeFile(join(agentsDir, "SKILL.md"), content);
|
|
4547
|
-
const symlinkPath = join(repoRoot, symlinkFn(name));
|
|
4548
|
-
await mkdir(dirname(symlinkPath), { recursive: true });
|
|
4549
|
-
try {
|
|
4550
|
-
await unlink(symlinkPath);
|
|
4551
|
-
} catch {}
|
|
4552
|
-
await symlink(relative(dirname(symlinkPath), agentsDir).replace(/\\/g, "/"), symlinkPath);
|
|
4553
|
-
missing.splice(missing.indexOf(name), 1);
|
|
4554
|
-
installed.push(name);
|
|
4555
|
-
if (isStale) externalStale.push(name);
|
|
4556
|
-
} catch {
|
|
4557
|
-
missing.splice(missing.indexOf(name), 1);
|
|
4558
|
-
externalFailed.push(name);
|
|
4559
|
-
}
|
|
4560
|
-
}
|
|
4561
|
-
}
|
|
4562
|
-
if (installed.length > 0 || stale.length > 0 || missing.length > 0 || externalFailed.length > 0) await updateSkillsGitignore(gitignorePath, existingContent, [
|
|
4563
|
-
...installed,
|
|
4564
|
-
...missing,
|
|
4565
|
-
...externalFailed
|
|
4566
|
-
], symlinkFn);
|
|
4567
|
-
const parts = [`installed ${installed.length}`];
|
|
4568
|
-
if (stale.length > 0) parts.push(`pruned: ${stale.join(", ")}`);
|
|
4569
|
-
if (externalStale.length > 0) parts.push(`stale: ${externalStale.join(", ")} (run \`holocron skills update\` to refresh)`);
|
|
4570
|
-
if (externalFailed.length > 0) parts.push(`fetch failed: ${externalFailed.join(", ")}`);
|
|
4571
|
-
if (missing.length > 0) parts.push(`unknown: ${missing.join(", ")}`);
|
|
4572
|
-
return parts.join("; ");
|
|
4573
|
-
}
|
|
4574
|
-
/** Extract skill names from the previous gitignore block so stale dirs can be pruned. */
|
|
4575
|
-
function parsePreviousSkills(gitignoreContent, symlinkFn) {
|
|
4576
|
-
if (!gitignoreContent.includes(GITIGNORE_BLOCK_START)) return [];
|
|
4577
|
-
const startIdx = gitignoreContent.indexOf(GITIGNORE_BLOCK_START);
|
|
4578
|
-
const endIdx = gitignoreContent.indexOf(GITIGNORE_BLOCK_END, startIdx);
|
|
4579
|
-
const block = endIdx !== -1 ? gitignoreContent.slice(startIdx, endIdx) : gitignoreContent.slice(startIdx);
|
|
4580
|
-
const placeholder = "__placeholder__";
|
|
4581
|
-
const symlinkPrefix = `/${symlinkFn(placeholder)}`.replace(placeholder, "");
|
|
4582
|
-
return block.split("\n").filter((line) => line.startsWith(symlinkPrefix)).map((line) => line.slice(symlinkPrefix.length));
|
|
4583
|
-
}
|
|
4584
|
-
async function copyDirRecursive(src, dest) {
|
|
4585
|
-
await mkdir(dest, { recursive: true });
|
|
4586
|
-
const entries = await readdir(src, { withFileTypes: true });
|
|
4587
|
-
for (const entry of entries) {
|
|
4588
|
-
const srcPath = join(src, entry.name);
|
|
4589
|
-
const destPath = join(dest, entry.name);
|
|
4590
|
-
if (entry.isDirectory()) await copyDirRecursive(srcPath, destPath);
|
|
4591
|
-
else await copyFile(srcPath, destPath);
|
|
4641
|
+
if (skippedSteps.length > 0) {
|
|
4642
|
+
print("");
|
|
4643
|
+
print(style.hint(" Skipped:"));
|
|
4644
|
+
for (const s of skippedSteps)
|
|
4645
|
+
/* v8 ignore next -- all skip steps set message; empty fallback is defensive */
|
|
4646
|
+
print(style.hint(` · ${s.step}${s.message ? ` (${s.message})` : ""}`));
|
|
4592
4647
|
}
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
const gitignorePath = join(repoRoot, ".gitignore");
|
|
4618
|
-
const existing = await readFile(gitignorePath, "utf8").catch(() => "");
|
|
4619
|
-
const block = [
|
|
4620
|
-
PROMPTS_GITIGNORE_START,
|
|
4621
|
-
`/${AGENTS_PROMPTS_ROOT}/`,
|
|
4622
|
-
PROMPTS_GITIGNORE_END
|
|
4623
|
-
].join("\n");
|
|
4624
|
-
let updated;
|
|
4625
|
-
if (existing.includes(PROMPTS_GITIGNORE_START)) {
|
|
4626
|
-
const start = existing.indexOf(PROMPTS_GITIGNORE_START);
|
|
4627
|
-
const end = existing.indexOf(PROMPTS_GITIGNORE_END, start);
|
|
4628
|
-
const afterBlock = end !== -1 ? existing.slice(end + 41) : "\n";
|
|
4629
|
-
updated = existing.slice(0, start) + block + afterBlock;
|
|
4630
|
-
} else updated = (existing.trimEnd() ? existing.trimEnd() + "\n\n" : "") + block + "\n";
|
|
4631
|
-
await writeFile(gitignorePath, updated, "utf8");
|
|
4632
|
-
return `wrote ${Object.keys(AGENT_PROMPTS).length} prompt files to ${AGENTS_PROMPTS_ROOT}/`;
|
|
4633
|
-
}
|
|
4634
|
-
async function writeIfAbsent(filePath, content) {
|
|
4635
|
-
try {
|
|
4636
|
-
await access(filePath);
|
|
4637
|
-
return false;
|
|
4638
|
-
} catch {
|
|
4639
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
4640
|
-
await writeFile(filePath, content, "utf8");
|
|
4641
|
-
return true;
|
|
4648
|
+
if (steps.some((s) => s.reason === "permissions")) {
|
|
4649
|
+
print("");
|
|
4650
|
+
print(style.warn("Some steps failed with 403 (insufficient token permissions)."));
|
|
4651
|
+
print(style.hint(" Repo-scoped operations (rulesets, settings, workflows) require a"));
|
|
4652
|
+
print(style.hint(" fine-grained PAT passed via --token or HOLOCRON_ADMIN_TOKEN:"));
|
|
4653
|
+
print("");
|
|
4654
|
+
print(style.hint(" · Administration — read and write"));
|
|
4655
|
+
print(style.hint(" · Code scanning alerts — read and write"));
|
|
4656
|
+
print(style.hint(" · Contents — read and write"));
|
|
4657
|
+
print(style.hint(" · Secret scanning alerts — read and write"));
|
|
4658
|
+
print(style.hint(" · Workflows — read and write"));
|
|
4659
|
+
print(style.hint(" · Metadata — read (added automatically)"));
|
|
4660
|
+
print("");
|
|
4661
|
+
print(style.hint(" Org-scoped operations (teams, custom properties) require"));
|
|
4662
|
+
print(style.hint(" HOLOCRON_ORG_TOKEN — a fine-grained PAT with resource owner set to the org:"));
|
|
4663
|
+
print("");
|
|
4664
|
+
print(style.hint(" · Administration — read and write (repository permission)"));
|
|
4665
|
+
print(style.hint(" · Members — read (organization permission)"));
|
|
4666
|
+
print(style.hint(" · Organization custom properties — read and write (organization permission)"));
|
|
4667
|
+
print(style.hint(" · Metadata — read (repository permission, auto-included)"));
|
|
4668
|
+
print("");
|
|
4669
|
+
print(style.hint(" Create tokens at: https://github.com/settings/personal-access-tokens/new"));
|
|
4670
|
+
print(style.hint(" Then re-run: holocron setup --token <your-admin-pat>"));
|
|
4671
|
+
print(style.hint(" Store org token: HOLOCRON_ORG_TOKEN env var or keyring key github.org"));
|
|
4642
4672
|
}
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
const writes = [
|
|
4647
|
-
[join(repoRoot, "docs/wiki/decisions/template.md"), DECISIONS_TEMPLATE],
|
|
4648
|
-
[join(repoRoot, "docs/wiki/decisions/README.md"), DECISIONS_README],
|
|
4649
|
-
[join(repoRoot, "docs/wiki/standards/README.md"), STANDARDS_README],
|
|
4650
|
-
[join(repoRoot, "docs/wiki/specifications/README.md"), SPECIFICATIONS_README]
|
|
4651
|
-
];
|
|
4652
|
-
for (const [path, content] of writes) if (await writeIfAbsent(path, content)) results.push(path.replace(repoRoot + "/", ""));
|
|
4653
|
-
return results.length > 0 ? `created: ${results.join(", ")}` : "all files already exist — nothing to write";
|
|
4654
|
-
}
|
|
4655
|
-
async function runStep(capability, step, dryRun, body, opts = {}) {
|
|
4656
|
-
if (dryRun) return {
|
|
4657
|
-
capability,
|
|
4658
|
-
step,
|
|
4659
|
-
status: "dry-run"
|
|
4673
|
+
return {
|
|
4674
|
+
steps,
|
|
4675
|
+
summary
|
|
4660
4676
|
};
|
|
4661
|
-
try {
|
|
4662
|
-
const note = await body();
|
|
4663
|
-
const result = {
|
|
4664
|
-
capability,
|
|
4665
|
-
step,
|
|
4666
|
-
status: "ok"
|
|
4667
|
-
};
|
|
4668
|
-
if (typeof note === "string") result.message = note;
|
|
4669
|
-
return result;
|
|
4670
|
-
} catch (err) {
|
|
4671
|
-
if (err instanceof ProviderApiError$1) {
|
|
4672
|
-
if (err.status !== void 0 && opts.skipCodes?.includes(err.status)) return {
|
|
4673
|
-
capability,
|
|
4674
|
-
step,
|
|
4675
|
-
status: "skip",
|
|
4676
|
-
message: err.message
|
|
4677
|
-
};
|
|
4678
|
-
if (err.status === 403) {
|
|
4679
|
-
const reason = classify403(err);
|
|
4680
|
-
return {
|
|
4681
|
-
capability,
|
|
4682
|
-
step,
|
|
4683
|
-
status: reason === "plan" ? "skip" : "fail",
|
|
4684
|
-
message: err.message,
|
|
4685
|
-
reason
|
|
4686
|
-
};
|
|
4687
|
-
}
|
|
4688
|
-
}
|
|
4689
|
-
return {
|
|
4690
|
-
capability,
|
|
4691
|
-
step,
|
|
4692
|
-
status: "fail",
|
|
4693
|
-
message: err instanceof Error ? err.message : String(err)
|
|
4694
|
-
};
|
|
4695
|
-
}
|
|
4696
|
-
}
|
|
4697
|
-
function classify403(err) {
|
|
4698
|
-
const detailText = typeof err.details === "string" ? err.details : typeof err.details === "object" && err.details !== null && "message" in err.details ? String(err.details.message) : "";
|
|
4699
|
-
const text = `${err.message} ${detailText}`.toLowerCase();
|
|
4700
|
-
if (text.includes("advanced security") || text.includes("not enabled for this repository") || text.includes("upgrade") || text.includes("not available on")) return "plan";
|
|
4701
|
-
return "permissions";
|
|
4702
|
-
}
|
|
4703
|
-
function formatStep(step) {
|
|
4704
|
-
const tag = step.reason === "permissions" ? " [permissions]" : step.reason === "plan" ? " [plan restriction]" : "";
|
|
4705
|
-
const detail = step.message ? style.dim(` (${step.message})`) : "";
|
|
4706
|
-
const label = `${step.step}${tag}${detail}`;
|
|
4707
|
-
if (step.status === "ok") return ` ${style.success(label)}`;
|
|
4708
|
-
if (step.status === "fail") return ` ${style.fail(label)}`;
|
|
4709
|
-
if (step.status === "dry-run") return ` ${style.dim(`… ${label}`)}`;
|
|
4710
|
-
return ` ${style.dim(`· ${label}`)}`;
|
|
4711
4677
|
}
|
|
4712
4678
|
//#endregion
|
|
4713
4679
|
//#region src/commands/skills.ts
|
|
@@ -4914,7 +4880,193 @@ async function runSyncReadme(input) {
|
|
|
4914
4880
|
};
|
|
4915
4881
|
}
|
|
4916
4882
|
//#endregion
|
|
4883
|
+
//#region src/commands/sync-wiki.ts
|
|
4884
|
+
function resolveToken(input) {
|
|
4885
|
+
return input.token ?? input.context.cliToken ?? process.env.HOLOCRON_READ_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_TOKEN;
|
|
4886
|
+
}
|
|
4887
|
+
function deriveBasepath(domain, repoName) {
|
|
4888
|
+
if (domain) {
|
|
4889
|
+
const slashIdx = domain.indexOf("/");
|
|
4890
|
+
if (slashIdx !== -1) return domain.slice(slashIdx + 1);
|
|
4891
|
+
}
|
|
4892
|
+
return repoName;
|
|
4893
|
+
}
|
|
4894
|
+
function titleCase(s) {
|
|
4895
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
4896
|
+
}
|
|
4897
|
+
function extractFromJson(raw, repoName) {
|
|
4898
|
+
let config;
|
|
4899
|
+
try {
|
|
4900
|
+
config = JSON.parse(raw);
|
|
4901
|
+
} catch {
|
|
4902
|
+
return null;
|
|
4903
|
+
}
|
|
4904
|
+
const providers = config.providers;
|
|
4905
|
+
if (!providers?.wiki) return null;
|
|
4906
|
+
const wikiEntry = providers.wiki;
|
|
4907
|
+
let domain;
|
|
4908
|
+
let subtitle;
|
|
4909
|
+
let icon;
|
|
4910
|
+
if (Array.isArray(wikiEntry) && wikiEntry.length === 2) {
|
|
4911
|
+
const opts = wikiEntry[1];
|
|
4912
|
+
domain = typeof opts.domain === "string" ? opts.domain : void 0;
|
|
4913
|
+
subtitle = typeof opts.subtitle === "string" ? opts.subtitle : void 0;
|
|
4914
|
+
icon = typeof opts.icon === "string" ? opts.icon : void 0;
|
|
4915
|
+
}
|
|
4916
|
+
if (!subtitle && typeof config.description === "string") subtitle = config.description;
|
|
4917
|
+
const basepath = deriveBasepath(domain, repoName);
|
|
4918
|
+
return {
|
|
4919
|
+
displayName: titleCase(basepath),
|
|
4920
|
+
basepath,
|
|
4921
|
+
...subtitle ? { subtitle } : {},
|
|
4922
|
+
...icon ? { icon } : {}
|
|
4923
|
+
};
|
|
4924
|
+
}
|
|
4925
|
+
function extractFromTs(raw, repoName) {
|
|
4926
|
+
const hasWikiProvider = /providers\s*:\s*\{[^}]*\bwiki\b/s.test(raw);
|
|
4927
|
+
const hasWikiPreset = /\bwikiCapability\b|\bwiki\s*\(\s*\)/.test(raw);
|
|
4928
|
+
if (!hasWikiProvider && !hasWikiPreset) return null;
|
|
4929
|
+
const domain = raw.match(/\bdomain\s*:\s*["']([^"']+)["']/)?.[1];
|
|
4930
|
+
let subtitle = raw.match(/\bsubtitle\s*:\s*["']([^"']+)["']/)?.[1];
|
|
4931
|
+
const icon = raw.match(/\bicon\s*:\s*["']([^"']+)["']/)?.[1];
|
|
4932
|
+
if (!subtitle) subtitle = raw.match(/\bdescription\s*:\s*["']([^"']+)["']/)?.[1];
|
|
4933
|
+
const basepath = deriveBasepath(domain, repoName);
|
|
4934
|
+
return {
|
|
4935
|
+
displayName: titleCase(basepath),
|
|
4936
|
+
basepath,
|
|
4937
|
+
...subtitle ? { subtitle } : {},
|
|
4938
|
+
...icon ? { icon } : {}
|
|
4939
|
+
};
|
|
4940
|
+
}
|
|
4941
|
+
async function discoverWikiProducts(org, token, fetchFn) {
|
|
4942
|
+
const rest = createRestClient({
|
|
4943
|
+
baseUrl: "https://api.github.com",
|
|
4944
|
+
token,
|
|
4945
|
+
extraHeaders: {
|
|
4946
|
+
accept: "application/vnd.github+json",
|
|
4947
|
+
"x-github-api-version": "2022-11-28"
|
|
4948
|
+
},
|
|
4949
|
+
vendor: "GitHub",
|
|
4950
|
+
fetch: fetchFn
|
|
4951
|
+
});
|
|
4952
|
+
const allRepos = [];
|
|
4953
|
+
let page = 1;
|
|
4954
|
+
while (true) {
|
|
4955
|
+
const batch = await rest.request(`/orgs/${org}/repos`, { query: {
|
|
4956
|
+
per_page: "100",
|
|
4957
|
+
page: String(page),
|
|
4958
|
+
type: "all"
|
|
4959
|
+
} });
|
|
4960
|
+
allRepos.push(...batch);
|
|
4961
|
+
if (batch.length < 100) break;
|
|
4962
|
+
page++;
|
|
4963
|
+
}
|
|
4964
|
+
const products = [];
|
|
4965
|
+
for (const repo of allRepos.filter((r) => !r.archived)) {
|
|
4966
|
+
let product = null;
|
|
4967
|
+
try {
|
|
4968
|
+
const contents = await rest.request(`/repos/${repo.full_name}/contents/holocron.config.json`);
|
|
4969
|
+
if (contents.encoding === "base64") product = extractFromJson(Buffer.from(contents.content.replace(/\s/g, ""), "base64").toString("utf8"), repo.name);
|
|
4970
|
+
} catch {}
|
|
4971
|
+
if (!product) try {
|
|
4972
|
+
const contents = await rest.request(`/repos/${repo.full_name}/contents/holocron.config.ts`);
|
|
4973
|
+
if (contents.encoding === "base64") product = extractFromTs(Buffer.from(contents.content.replace(/\s/g, ""), "base64").toString("utf8"), repo.name);
|
|
4974
|
+
} catch {}
|
|
4975
|
+
if (product) products.push(product);
|
|
4976
|
+
}
|
|
4977
|
+
products.sort((a, b) => a.basepath.localeCompare(b.basepath));
|
|
4978
|
+
return products;
|
|
4979
|
+
}
|
|
4980
|
+
function buildProductsBlock(products) {
|
|
4981
|
+
const lines = ["products:"];
|
|
4982
|
+
for (const p of products) {
|
|
4983
|
+
lines.push(` - display-name: ${p.displayName}`);
|
|
4984
|
+
if (p.subtitle) lines.push(` subtitle: ${p.subtitle}`);
|
|
4985
|
+
if (p.icon) lines.push(` icon: ${p.icon}`);
|
|
4986
|
+
lines.push(` href: /${p.basepath}`);
|
|
4987
|
+
}
|
|
4988
|
+
return lines.join("\n");
|
|
4989
|
+
}
|
|
4990
|
+
async function mergeProducts(docsYmlPath, products) {
|
|
4991
|
+
let content;
|
|
4992
|
+
try {
|
|
4993
|
+
content = await readFile(docsYmlPath, "utf8");
|
|
4994
|
+
} catch {
|
|
4995
|
+
throw new Error(`fern/docs.yml not found at ${docsYmlPath}`);
|
|
4996
|
+
}
|
|
4997
|
+
const newBlock = buildProductsBlock(products);
|
|
4998
|
+
const productBlockRe = /^products:(?:\n[ \t][^\n]*)*/m;
|
|
4999
|
+
if (productBlockRe.test(content)) {
|
|
5000
|
+
const updated = content.replace(productBlockRe, newBlock);
|
|
5001
|
+
if (updated !== content) await writeFile(docsYmlPath, updated, "utf8");
|
|
5002
|
+
return;
|
|
5003
|
+
}
|
|
5004
|
+
const instancesIdx = content.indexOf("\ninstances:");
|
|
5005
|
+
if (instancesIdx !== -1) {
|
|
5006
|
+
await writeFile(docsYmlPath, content.slice(0, instancesIdx + 1) + newBlock + "\n\n" + content.slice(instancesIdx + 1), "utf8");
|
|
5007
|
+
return;
|
|
5008
|
+
}
|
|
5009
|
+
await writeFile(docsYmlPath, content.trimEnd() + "\n\n" + newBlock + "\n", "utf8");
|
|
5010
|
+
}
|
|
5011
|
+
async function runSyncWiki(input) {
|
|
5012
|
+
const config = input.loaded.resolved;
|
|
5013
|
+
const dryRun = input.context.dryRun ?? false;
|
|
5014
|
+
if (!config.providers.wiki) return {
|
|
5015
|
+
capability: "local",
|
|
5016
|
+
step: "sync wiki",
|
|
5017
|
+
status: "skip",
|
|
5018
|
+
message: "no wiki provider configured"
|
|
5019
|
+
};
|
|
5020
|
+
const org = config.org ?? input.context.repo?.split("/")[0];
|
|
5021
|
+
if (!org) return {
|
|
5022
|
+
capability: "local",
|
|
5023
|
+
step: "sync wiki",
|
|
5024
|
+
status: "skip",
|
|
5025
|
+
message: "no org configured"
|
|
5026
|
+
};
|
|
5027
|
+
const token = resolveToken(input);
|
|
5028
|
+
if (!token) return {
|
|
5029
|
+
capability: "local",
|
|
5030
|
+
step: "sync wiki",
|
|
5031
|
+
status: "skip",
|
|
5032
|
+
message: "no GitHub token available (set HOLOCRON_READ_TOKEN or GH_TOKEN)"
|
|
5033
|
+
};
|
|
5034
|
+
if (dryRun) return {
|
|
5035
|
+
capability: "local",
|
|
5036
|
+
step: "sync wiki",
|
|
5037
|
+
status: "dry-run"
|
|
5038
|
+
};
|
|
5039
|
+
const docsYmlPath = join(input.context.repoRoot, "fern", "docs.yml");
|
|
5040
|
+
try {
|
|
5041
|
+
const products = await discoverWikiProducts(org, token, input.fetch);
|
|
5042
|
+
if (products.length === 0) return {
|
|
5043
|
+
capability: "local",
|
|
5044
|
+
step: "sync wiki",
|
|
5045
|
+
status: "skip",
|
|
5046
|
+
message: "no wiki-enabled repos found"
|
|
5047
|
+
};
|
|
5048
|
+
await mergeProducts(docsYmlPath, products);
|
|
5049
|
+
return {
|
|
5050
|
+
capability: "local",
|
|
5051
|
+
step: "sync wiki",
|
|
5052
|
+
status: "ok",
|
|
5053
|
+
message: `${products.length} products`
|
|
5054
|
+
};
|
|
5055
|
+
} catch (err) {
|
|
5056
|
+
return {
|
|
5057
|
+
capability: "local",
|
|
5058
|
+
step: "sync wiki",
|
|
5059
|
+
status: "fail",
|
|
5060
|
+
message: err instanceof Error ? err.message : String(err)
|
|
5061
|
+
};
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
//#endregion
|
|
4917
5065
|
//#region src/commands/sync.ts
|
|
5066
|
+
const { workflowHeader: workflowHeader$1 } = createHeader({
|
|
5067
|
+
source: "packages/cli/src/commands/sync.ts",
|
|
5068
|
+
tool: "holocron sync"
|
|
5069
|
+
});
|
|
4918
5070
|
const SYNC_STEPS = [
|
|
4919
5071
|
"labels",
|
|
4920
5072
|
"properties",
|
|
@@ -4924,14 +5076,16 @@ const SYNC_STEPS = [
|
|
|
4924
5076
|
"description",
|
|
4925
5077
|
"homepage",
|
|
4926
5078
|
"readme",
|
|
4927
|
-
"workflows"
|
|
5079
|
+
"workflows",
|
|
5080
|
+
"wiki"
|
|
4928
5081
|
];
|
|
4929
5082
|
const LOCAL_STEPS = /* @__PURE__ */ new Set([
|
|
4930
5083
|
"keywords",
|
|
4931
5084
|
"description",
|
|
4932
5085
|
"homepage",
|
|
4933
5086
|
"readme",
|
|
4934
|
-
"workflows"
|
|
5087
|
+
"workflows",
|
|
5088
|
+
"wiki"
|
|
4935
5089
|
]);
|
|
4936
5090
|
async function runSync(input) {
|
|
4937
5091
|
const print = input.print ?? ((line) => console.log(line));
|
|
@@ -5071,7 +5225,8 @@ async function runSync(input) {
|
|
|
5071
5225
|
"description",
|
|
5072
5226
|
"homepage",
|
|
5073
5227
|
"readme",
|
|
5074
|
-
"workflows"
|
|
5228
|
+
"workflows",
|
|
5229
|
+
"wiki"
|
|
5075
5230
|
]) {
|
|
5076
5231
|
if (requestedSteps !== void 0 && !requestedSteps.includes(stepName)) continue;
|
|
5077
5232
|
if (stepName === "keywords") {
|
|
@@ -5185,7 +5340,7 @@ async function runSync(input) {
|
|
|
5185
5340
|
});
|
|
5186
5341
|
if (previewCfg) {
|
|
5187
5342
|
steps.push(await runSyncStep("local", "sync workflow deploy (with preview)", dryRun, async () => {
|
|
5188
|
-
const content = workflowHeader()
|
|
5343
|
+
const content = `${workflowHeader$1()}${generateCombinedDeployContent(withOverrides, additionalPaths, previewCfg)}`;
|
|
5189
5344
|
await writeWorkflowFile(input.context.repoRoot, "deploy.yml", content);
|
|
5190
5345
|
}));
|
|
5191
5346
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
@@ -5193,12 +5348,20 @@ async function runSync(input) {
|
|
|
5193
5348
|
}
|
|
5194
5349
|
}
|
|
5195
5350
|
steps.push(await runSyncStep("local", `sync workflow ${name}`, dryRun, async () => {
|
|
5196
|
-
const content = workflowHeader()
|
|
5351
|
+
const content = `${workflowHeader$1()}${generateThinCallerContent(name, withOverrides, additionalPaths)}`;
|
|
5197
5352
|
await writeWorkflowFile(input.context.repoRoot, `${name}.yml`, content);
|
|
5198
5353
|
}));
|
|
5199
5354
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
5200
5355
|
}
|
|
5201
5356
|
}
|
|
5357
|
+
if (stepName === "wiki") {
|
|
5358
|
+
const result = await runSyncWiki({
|
|
5359
|
+
loaded: input.loaded,
|
|
5360
|
+
context: input.context
|
|
5361
|
+
});
|
|
5362
|
+
steps.push(result);
|
|
5363
|
+
print(formatSyncStep(result));
|
|
5364
|
+
}
|
|
5202
5365
|
}
|
|
5203
5366
|
const summary = steps.reduce((acc, s) => {
|
|
5204
5367
|
if (s.status === "ok") acc.ok += 1;
|
|
@@ -5310,47 +5473,41 @@ var audit_default = "name: Audit\n\non: # yamllint disable-line rule:truthy\n w
|
|
|
5310
5473
|
//#region src/templates/workflows/bookkeeping.yml
|
|
5311
5474
|
var bookkeeping_default = "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n configuration-path:\n description: Path to the labeler configuration file in the calling repo\n type: string\n required: false\n default: .github/labeler.yml\n\njobs:\n label:\n name: Apply Labels\n permissions:\n contents: read\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n sparse-checkout: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n sparse-checkout-cone-mode: false\n\n - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4\n if: ${{ github.event_name == 'pull_request' && hashFiles(inputs.configuration-path || '.github/labeler.yml') != '' }}\n # v3.4 bundles Node 20; allow it to run under Actions' current default.\n env:\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true\n with:\n # Fall back to default path when triggered directly (not via workflow_call)\n # because inputs.* defaults only apply on workflow_call events.\n configuration-path: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n include-title: 1\n include-body: 0\n sync-labels: 1\n enable-versioned-regex: 0\n repo-token: ${{ github.token }}\n";
|
|
5312
5475
|
//#endregion
|
|
5313
|
-
//#region src/templates/workflows/cleanup-preview.yml
|
|
5314
|
-
var cleanup_preview_default = "name: Clean up Preview\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted.\n required: false\n type: string\n default: \"\"\n\njobs:\n cleanup:\n name: Clean up Preview\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - name: Delete Cloudflare Pages deployments for branch\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n env:\n CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n BRANCH: ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n DEPLOYMENTS=$(curl -s \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" \\\n | jq -r --arg b \"$BRANCH\" \\\n '.result[] | select(.deployment_trigger.metadata.branch == $b) | .id')\n\n if [ -z \"$DEPLOYMENTS\" ]; then\n echo \"No deployments found for branch ${BRANCH} — nothing to clean up.\"\n exit 0\n fi\n\n for id in $DEPLOYMENTS; do\n echo \"Deleting CF Pages deployment: $id\"\n curl -s -X DELETE \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments/${id}?force=true\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" | jq -r 'if .success then \" ✓ deleted\" else \" ✗ \\(.errors[0].message)\" end'\n done\n\n # Mark the GitHub Deployment environment as inactive.\n ENV_NAME=\"${PROJECT} (Preview)\"\n gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n | jq -r \".[] | select(.environment == \\\"${ENV_NAME}\\\") | .id\" \\\n | while read -r deploy_id; do\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}/statuses\" \\\n --method POST --field state=inactive 2>/dev/null || true\n done\n";
|
|
5315
|
-
//#endregion
|
|
5316
|
-
//#region src/templates/workflows/codeql.yml
|
|
5317
|
-
var codeql_default = "name: CodeQL\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n language:\n description: CodeQL language to analyze\n type: string\n required: false\n default: javascript-typescript\n\njobs:\n analyze:\n name: Analyze (${{ inputs.language }})\n permissions:\n actions: read\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n timeout-minutes: 45\n # Do not cancel in-progress security scans.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Initialize CodeQL\n with:\n languages: ${{ inputs.language }}\n\n - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Autobuild\n\n - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Analyze\n with:\n category: /language:${{ inputs.language }}\n";
|
|
5318
|
-
//#endregion
|
|
5319
5476
|
//#region src/templates/workflows/dependencies.yml
|
|
5320
5477
|
var dependencies_default = "name: Dependencies\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n merge-token:\n description: >\n Optional privileged token for auto-merge. Falls back to GITHUB_TOKEN.\n Required when branch protection enforces required reviews — GITHUB_TOKEN\n cannot approve its own PRs.\n required: false\n\njobs:\n dependabot:\n name: Update the dependencies\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n if: github.event.pull_request.user.login == 'dependabot[bot]'\n steps:\n - uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0\n name: Fetch Dependabot metadata\n id: metadata\n\n - run: gh pr merge --auto --squash \"$PR_URL\"\n # --squash is intentional: repo protection sets allow_merge_commit: false,\n # so --merge would fail on any repo using the standard preset.\n name: Enable auto-merge for Dependabot PRs\n if: steps.metadata.outputs.update-type == 'version-update:semver-patch'\n env:\n PR_URL: ${{ github.event.pull_request.html_url }}\n GH_TOKEN: ${{ secrets.merge-token || github.token }}\n";
|
|
5321
5478
|
//#endregion
|
|
5322
5479
|
//#region src/templates/workflows/deploy.yml
|
|
5323
5480
|
var deploy_default = "name: Deploy\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n\njobs:\n deploy:\n name: Deploy\n runs-on: ubuntu-latest\n permissions:\n contents: read\n pages: write\n id-token: write\n environment:\n name: github-pages\n url: ${{ steps.deployment.outputs.page_url }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n cp -r docs/dist/. _site/\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1\n name: Upload pages artifact\n with:\n path: _site\n\n - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5\n id: deployment\n name: Deploy to GitHub Pages\n";
|
|
5324
5481
|
//#endregion
|
|
5325
|
-
//#region src/templates/workflows/deploy-preview.yml
|
|
5326
|
-
var deploy_preview_default = "name: Deploy Preview\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n name:\n description: Repo name prefix used to filter the docs site package (<name>-site); omit to run pnpm -C docs build\n required: false\n type: string\n default: \"\"\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted — set that variable once and all repos get previews\n without per-repo config.\n required: false\n type: string\n default: \"\"\n\njobs:\n deploy-preview:\n name: Deploy Preview\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name != '' }}\n env:\n SITE_NAME: ${{ inputs.name }}\n run: pnpm --filter @theholocron/\"$SITE_NAME\"-site build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name == '' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n SITE_NAME: ${{ inputs.name }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n if [ -n \"$SITE_NAME\" ]; then\n # Site built with base: /<name>/ — nest under _site/<name>/ so asset\n # paths match (/name/_astro/... → served at /name/_astro/...).\n mkdir -p \"_site/${SITE_NAME}\"\n cp -r docs/dist/. \"_site/${SITE_NAME}/\"\n # Redirect root to the base path so the preview URL lands correctly.\n printf \"/ /%s/ 301\\n\" \"$SITE_NAME\" > _site/_redirects\n else\n cp -r docs/dist/. _site/\n fi\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - name: Install Wrangler\n # The action's install step runs npm/pnpm in the repo root; pnpm fails\n # because it rejects adding to a workspace root without -w, and npm fails\n # because package.json contains pnpm catalog: references it cannot parse.\n # Pre-installing from /tmp (no package.json) lets the action find wrangler\n # already on PATH and skip its own install entirely.\n working-directory: /tmp\n run: npm install -g wrangler@4\n\n - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0\n id: deploy\n name: Deploy to Cloudflare Pages\n # Skip when neither the per-repo input nor the org variable is set.\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n with:\n apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n packageManager: npm\n command: >-\n pages deploy _site\n --project-name ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n --branch ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n --commit-dirty=true\n\n - name: Report preview URL\n if: ${{ steps.deploy.outputs.pages-deployment-alias-url != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n ALIAS: ${{ steps.deploy.outputs.pages-deployment-alias-url }}\n SITE_NAME: ${{ inputs.name }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n run: |\n PREVIEW_URL=\"${ALIAS%/}${SITE_NAME:+/${SITE_NAME}}/\"\n # GITHUB_HEAD_REF is the PR branch name — links the deployment to this PR's sidebar widget.\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"%s\",\"description\":\"Cloudflare Pages\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\" \"${PROJECT} (Preview)\")\n DEPLOY_ID=$(echo \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"${PREVIEW_URL}\"\n";
|
|
5327
|
-
//#endregion
|
|
5328
5482
|
//#region src/templates/workflows/greetings.yml
|
|
5329
5483
|
var greetings_default = "name: Greetings\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n greeting:\n name: Greet first-time contributors\n permissions:\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n # Group by the issue/PR number so duplicate events don't race each other.\n steps:\n - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0\n name: Greet on first contribution\n with:\n script: |\n // Only greet on the initial open — ignore synchronize, reopened, etc.\n if (context.payload.action !== 'opened') return;\n\n const actor = context.actor;\n const { owner, repo } = context.repo;\n\n // Payload inspection is more reliable than context.eventName for detecting\n // whether this is an issue vs. PR event — works regardless of how GitHub\n // propagates event names through workflow_call chains.\n const isIssue = !!context.payload.issue && !context.payload.pull_request;\n // listForRepo returns both issues and PRs (GitHub treats PRs as issues),\n // sorted newest-first. Filter by type to track first-issue and first-PR\n // independently, and avoid search-index eventual-consistency lag.\n const { data: recent } = await github.rest.issues.listForRepo({\n owner, repo,\n creator: actor,\n state: 'all',\n per_page: 100\n });\n\n const sameType = recent.filter(item =>\n isIssue ? !item.pull_request : !!item.pull_request\n );\n\n if (sameType.length !== 1) return;\n const body = isIssue\n ? `Hey @${actor}!\\n\\nWe really appreciate you taking the time to report an issue. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`\n : `Hey @${actor}!\\n\\nWe really appreciate you taking the time to help out with this PR. The collaborators on this project attempt to help as many people as possible, but we are a limited number of volunteers, so it is possible that this will not be addressed as swiftly.\\n\\nYour patience is much appreciated and we will get back to you as quickly as possible.`;\n\n await github.rest.issues.createComment({\n owner,\n repo,\n issue_number: context.issue.number,\n body\n });\n";
|
|
5330
5484
|
//#endregion
|
|
5331
5485
|
//#region src/templates/workflows/lint.yml
|
|
5332
5486
|
var lint_default = "name: Lint\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n eslint-config:\n description: >\n Filename for the ESLint flat config used by super-linter for\n JavaScript, JSX, TSX, and TypeScript (ES) files. Defaults to\n eslint.config.ts (the org standard). Note: ESLint 9 requires\n --flag unstable_ts_config to load .ts configs; if super-linter\n cannot load it, override with eslint.config.mjs or eslint.config.js.\n type: string\n required: false\n default: eslint.config.ts\n prettier-config:\n description: >\n Filename for the Prettier config. Defaults to prettier.config.ts\n (the org standard). Prettier 3.x loads .ts configs natively.\n type: string\n required: false\n default: prettier.config.ts\n yaml-config:\n description: >\n Filename for the yamllint config. Defaults to yamllint.config.yml.\n type: string\n required: false\n default: yamllint.config.yml\n enable-auto-commit:\n description: >\n Auto-commit super-linter fixes as a verified commit via a GitHub App.\n Requires SUPER_LINTER_APP_ID and SUPER_LINTER_PRIVATE_KEY secrets.\n type: boolean\n required: false\n default: false\n secrets:\n SUPER_LINTER_APP_ID:\n required: false\n SUPER_LINTER_PRIVATE_KEY:\n required: false\n\njobs:\n super-lint:\n name: Lint entire codebase\n permissions:\n contents: write\n issues: write\n statuses: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n env:\n APP_ID_SET: ${{ secrets.SUPER_LINTER_APP_ID != '' }}\n steps:\n - name: Generate GitHub App token\n id: app-token\n # Runs before checkout so the token is used as the checkout credential,\n # which makes the subsequent push go through the App and produce a\n # Verified commit. Skipped when auto-commit is disabled or secrets unset.\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true'\n uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0\n with:\n app-id: ${{ secrets.SUPER_LINTER_APP_ID }}\n private-key: ${{ secrets.SUPER_LINTER_PRIVATE_KEY }}\n\n - name: Resolve App bot identity\n id: app-bot\n # GitHub marks commits as Verified when the author email matches the\n # App bot's noreply address (<numeric-id>+<slug>[bot]@users.noreply.github.com).\n # The numeric ID must be fetched via the API — it differs from the App ID.\n # app-slug is passed via env rather than interpolated into the script to\n # prevent code injection (CWE-78).\n if: steps.app-token.conclusion == 'success'\n run: |\n BOT_SLUG=\"${APP_SLUG}[bot]\"\n BOT_ID=$(gh api \"/users/${BOT_SLUG}\" --jq .id)\n echo \"name=${BOT_SLUG}\" >> \"$GITHUB_OUTPUT\"\n echo \"email=${BOT_ID}+${BOT_SLUG}@users.noreply.github.com\" >> \"$GITHUB_OUTPUT\"\n env:\n GH_TOKEN: ${{ steps.app-token.outputs.token }}\n APP_SLUG: ${{ steps.app-token.outputs.app-slug }}\n\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n # Use the App token when available so the push credential is the App\n # bot — GitHub marks those commits as Verified automatically.\n token: ${{ steps.app-token.outputs.token || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n\n - name: Detect project features\n # Writes VALIDATE_*/FIX_* to GITHUB_ENV only when the feature exists.\n # Also writes step outputs for values referenced in expression context\n # (GITHUB_ENV is not readable via steps.*.outputs — they need GITHUB_OUTPUT).\n # All values written are hardcoded 'true' — no user input in the script.\n # Config file paths (inputs.*) stay in GH Actions expression context in\n # the super-linter env: block below, never shell-evaluated (CWE-78).\n id: detect\n run: |\n has() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n\n if { has 'eslint.config.ts' || has 'eslint.config.mjs' || has 'eslint.config.js' || has '.eslintrc.json' || has '.eslintrc.yml'; }; then\n {\n echo \"VALIDATE_JAVASCRIPT_ES=true\"\n echo \"VALIDATE_TYPESCRIPT_ES=true\"\n } >> \"$GITHUB_ENV\"\n echo \"eslint=true\" >> \"$GITHUB_OUTPUT\"\n fi\n\n if { has '*.js' || has '*.jsx' || has '*.mjs' || has '*.cjs' || has '*.ts' || has '*.tsx'; }; then\n {\n echo \"VALIDATE_JAVASCRIPT_PRETTIER=true\"\n echo \"VALIDATE_JSX_PRETTIER=true\"\n echo \"VALIDATE_TYPESCRIPT_PRETTIER=true\"\n echo \"VALIDATE_TSX=true\"\n echo \"FIX_JAVASCRIPT_PRETTIER=true\"\n echo \"FIX_JSX_PRETTIER=true\"\n echo \"FIX_TYPESCRIPT_PRETTIER=true\"\n echo \"FIX_TSX=true\"\n } >> \"$GITHUB_ENV\"\n fi\n\n if { has '*.css' || has '*.scss' || has 'stylelint.config.ts' || has 'stylelint.config.mjs' || has 'stylelint.config.js'; }; then\n {\n echo \"VALIDATE_CSS=true\"\n echo \"STYLELINT_CONFIG_FILE=stylelint.config.ts\"\n } >> \"$GITHUB_ENV\"\n fi\n\n if { has '*.graphql' || has '*.gql'; }; then\n {\n echo \"VALIDATE_GRAPHQL_PRETTIER=true\"\n echo \"FIX_GRAPHQL_PRETTIER=true\"\n } >> \"$GITHUB_ENV\"\n fi\n\n if { has '*.html' || has '*.htm'; }; then\n {\n echo \"VALIDATE_HTML_PRETTIER=true\"\n echo \"FIX_HTML_PRETTIER=true\"\n } >> \"$GITHUB_ENV\"\n fi\n\n if { has '.env' || has '.env.example' || has '.env.local'; }; then\n {\n echo \"VALIDATE_ENV=true\"\n echo \"FIX_ENV=true\"\n } >> \"$GITHUB_ENV\"\n fi\n\n if { has 'Dockerfile' || has '*.Dockerfile'; }; then\n echo \"VALIDATE_DOCKERFILE=true\" >> \"$GITHUB_ENV\"\n fi\n\n - uses: super-linter/super-linter/slim@4ce20838b8ab83717e78138c5b3a1407148e0918 # v8.7.0\n name: Run Super Linter\n env:\n GITHUB_TOKEN: ${{ github.token }}\n DEFAULT_BRANCH: ${{ github.event.pull_request.base.ref || github.event.repository.default_branch }}\n ANNOTATE_ONLY: true\n DISABLE_COMMENTS: false\n IGNORE_GITIGNORED_FILES: true\n LINTER_RULES_PATH: /\n EDITORCONFIG_FILE_NAME: \".editorconfig-checker.json\"\n # Config file paths — inputs stay in expression context, never shell-evaluated.\n # When VALIDATE_JAVASCRIPT_ES is not set by detect, ESLint doesn't run so\n # the empty-string fallback (→ eslint.config.mjs in container) is safe.\n JAVASCRIPT_ES_CONFIG_FILE: ${{ steps.detect.outputs.eslint == 'true' && inputs.eslint-config || '' }}\n TYPESCRIPT_ES_CONFIG_FILE: ${{ steps.detect.outputs.eslint == 'true' && inputs.eslint-config || '' }}\n PRETTIER_CONFIG: ${{ inputs.prettier-config }}\n YAML_CONFIG_FILE: ${{ inputs.yaml-config }}\n # Always-on linters\n FIX_MARKDOWN_PRETTIER: true\n VALIDATE_EDITORCONFIG: true\n VALIDATE_GIT_COMMITLINT: true\n VALIDATE_GIT_MERGE_CONFLICT_MARKERS: true\n VALIDATE_GITHUB_ACTIONS: true\n VALIDATE_GITLEAKS: true\n VALIDATE_MARKDOWN_PRETTIER: true\n VALIDATE_YAML: true\n\n - name: Validate ADR and spec frontmatter\n if: hashFiles('scripts/validate-adrs.mjs') != ''\n run: |\n mapfile -t changed < <(git diff --name-only \"$BASE_SHA\" HEAD -- \\\n 'docs/decisions/*.md' '.notes/*.spec.md' 2>/dev/null || true)\n if [ \"${#changed[@]}\" -eq 0 ]; then\n node scripts/validate-adrs.mjs\n else\n node scripts/validate-adrs.mjs \"${changed[@]}\"\n fi\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}\n\n - name: Validate registry consistency\n if: hashFiles('scripts/validate-registry.mjs') != ''\n run: node scripts/validate-registry.mjs\n\n - name: Validate docs presence for new packages\n if: hashFiles('scripts/validate-docs-presence.mjs') != ''\n run: node scripts/validate-docs-presence.mjs\n env:\n BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}\n\n - uses: theholocron/.github/.github/actions/auto-commit@main\n name: Commit and push linting fixes\n if: >\n inputs.enable-auto-commit == true &&\n github.event.pull_request != null &&\n github.event.pull_request.head.repo.full_name == github.repository &&\n github.ref_name != github.event.repository.default_branch &&\n env.APP_ID_SET == 'true'\n with:\n token: ${{ steps.app-token.outputs.token }}\n branch: ${{ github.event.pull_request.head.ref || github.head_ref || github.ref }}\n commit-message: \"chore: fix linting issues\\n\\nSigned-off-by: ${{ steps.app-bot.outputs.name }} <${{ steps.app-bot.outputs.email }}>\"\n commit-options: \"--no-verify\"\n commit-user-name: ${{ steps.app-bot.outputs.name }}\n commit-user-email: ${{ steps.app-bot.outputs.email }}\n commit-author: \"${{ steps.app-bot.outputs.name }} <${{ steps.app-bot.outputs.email }}>\"\n\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [super-lint]\n steps:\n - name: Check job statuses\n run: |\n if [[ \"$RESULTS\" == *\"failure\"* ]] || [[ \"$RESULTS\" == *\"cancelled\"* ]]; then\n exit 1\n fi\n env:\n RESULTS: ${{ join(needs.*.result, ',') }}\n";
|
|
5333
5487
|
//#endregion
|
|
5334
|
-
//#region src/templates/workflows/
|
|
5335
|
-
var
|
|
5488
|
+
//#region src/templates/workflows/preview.yml
|
|
5489
|
+
var preview_default = "name: Preview\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n type:\n description: \"Type of deployment: docs or storybook\"\n required: true\n type: string\n name:\n description: Repo name prefix used to filter the docs site package (<name>-site); omit to run pnpm -C docs build\n required: false\n type: string\n default: \"\"\n storybook-projects:\n description: >\n JSON array of { \"name\"?, \"workingDir\", \"outputDir\"? } objects for storybook deploys.\n Each is built via `pnpm -C <workingDir> build:storybook`. If \"name\" is provided the\n output is placed under `sandbox/<name>/`; omit \"name\" for single-repo deploys and the\n output lands directly in `sandbox/`.\n type: string\n required: false\n default: \"[]\"\n build-script:\n description: pnpm script that builds the Storybook static output (single storybook, type:storybook only)\n type: string\n required: false\n default: build:storybook\n output-dir:\n description: Directory where Storybook writes its static output (single storybook, type:storybook only)\n type: string\n required: false\n default: storybook-static\n cloudflare-project:\n description: >\n Cloudflare Pages project name. Falls back to the CLOUDFLARE_PAGES_PROJECT\n org variable when omitted — set that variable once and all repos get previews\n without per-repo config.\n required: false\n type: string\n default: \"\"\n\njobs:\n deploy-preview:\n name: Preview\n if: ${{ github.event.action != 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name != '' }}\n env:\n SITE_NAME: ${{ inputs.name }}\n run: pnpm --filter @theholocron/\"$SITE_NAME\"-site build\n\n - name: Build docs site\n if: ${{ inputs.type == 'docs' && inputs.name == '' }}\n run: pnpm -C docs build\n\n - name: Build Storybook projects\n if: ${{ inputs.storybook-projects != '[]' }}\n env:\n PROJECTS: ${{ inputs.storybook-projects }}\n run: |\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n pnpm -C \"$workingDir\" build:storybook\n done\n\n - name: Build Storybook\n if: ${{ inputs.type == 'storybook' && inputs.storybook-projects == '[]' }}\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n run: pnpm run \"$BUILD_SCRIPT\"\n\n - name: Assemble site\n env:\n DEPLOY_TYPE: ${{ inputs.type }}\n SITE_NAME: ${{ inputs.name }}\n PROJECTS: ${{ inputs.storybook-projects }}\n STORYBOOK_OUTPUT_DIR: ${{ inputs.output-dir }}\n run: |\n mkdir -p _site\n if [ \"$DEPLOY_TYPE\" = \"docs\" ]; then\n if [ -n \"$SITE_NAME\" ]; then\n # Site built with base: /<name>/ — nest under _site/<name>/ so asset\n # paths match (/name/_astro/... → served at /name/_astro/...).\n mkdir -p \"_site/${SITE_NAME}\"\n cp -r docs/dist/. \"_site/${SITE_NAME}/\"\n # Redirect root to the base path so the preview URL lands correctly.\n printf \"/ /%s/ 301\\n\" \"$SITE_NAME\" > _site/_redirects\n else\n cp -r docs/dist/. _site/\n fi\n fi\n if [ \"$PROJECTS\" != \"[]\" ]; then\n echo \"$PROJECTS\" | jq -c '.[]' | while IFS= read -r project; do\n name=$(echo \"$project\" | jq -r '.name // \"\"')\n workingDir=$(echo \"$project\" | jq -r '.workingDir')\n outputDir=$(echo \"$project\" | jq -r '.outputDir // \"storybook-static\"')\n if [ -n \"$name\" ]; then\n target=\"_site/sandbox/${name}\"\n else\n target=\"_site/sandbox\"\n fi\n mkdir -p \"$target\"\n cp -r \"${workingDir}/${outputDir}/.\" \"$target/\"\n done\n elif [ \"$DEPLOY_TYPE\" = \"storybook\" ]; then\n mkdir -p _site/sandbox\n cp -r \"${STORYBOOK_OUTPUT_DIR}/.\" _site/sandbox/\n fi\n\n - name: Install Wrangler\n # The action's install step runs npm/pnpm in the repo root; pnpm fails\n # because it rejects adding to a workspace root without -w, and npm fails\n # because package.json contains pnpm catalog: references it cannot parse.\n # Pre-installing from /tmp (no package.json) lets the action find wrangler\n # already on PATH and skip its own install entirely.\n working-directory: /tmp\n run: npm install -g wrangler@4\n\n - uses: cloudflare/wrangler-action@ebbaa1584979971c8614a24965b4405ff95890e0 # v4.0.0\n id: deploy\n name: Deploy to Cloudflare Pages\n # Skip when neither the per-repo input nor the org variable is set.\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n with:\n apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n accountId: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n packageManager: npm\n command: >-\n pages deploy _site\n --project-name ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n --branch ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n --commit-dirty=true\n\n - name: Report preview URL\n if: ${{ steps.deploy.outputs.pages-deployment-alias-url != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n ALIAS: ${{ steps.deploy.outputs.pages-deployment-alias-url }}\n SITE_NAME: ${{ inputs.name }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n run: |\n PREVIEW_URL=\"${ALIAS%/}${SITE_NAME:+/${SITE_NAME}}/\"\n # GITHUB_HEAD_REF is the PR branch name — links the deployment to this PR's sidebar widget.\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"%s\",\"description\":\"Cloudflare Pages\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\" \"${PROJECT} (Preview)\")\n DEPLOY_ID=$(echo \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"${PREVIEW_URL}\"\n\n cleanup:\n name: Clean up Preview\n if: ${{ github.event.action == 'closed' }}\n runs-on: ubuntu-latest\n permissions:\n contents: read\n deployments: write\n pull-requests: write\n steps:\n - name: Delete Cloudflare Pages deployments for branch\n if: ${{ inputs.cloudflare-project != '' || vars.CLOUDFLARE_PAGES_PROJECT != '' }}\n env:\n CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}\n CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}\n PROJECT: ${{ inputs.cloudflare-project || vars.CLOUDFLARE_PAGES_PROJECT }}\n BRANCH: ${{ github.event.repository.name }}-pr-${{ github.event.pull_request.number }}\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n run: |\n DEPLOYMENTS=$(curl -s \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" \\\n | jq -r --arg b \"$BRANCH\" \\\n '.result[] | select(.deployment_trigger.metadata.branch == $b) | .id')\n\n if [ -z \"$DEPLOYMENTS\" ]; then\n echo \"No deployments found for branch ${BRANCH} — nothing to clean up.\"\n exit 0\n fi\n\n for id in $DEPLOYMENTS; do\n echo \"Deleting CF Pages deployment: $id\"\n curl -s -X DELETE \\\n \"https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/pages/projects/${PROJECT}/deployments/${id}?force=true\" \\\n -H \"Authorization: Bearer ${CLOUDFLARE_API_TOKEN}\" | jq -r 'if .success then \" ✓ deleted\" else \" ✗ \\(.errors[0].message)\" end'\n done\n\n # Mark the GitHub Deployment environment as inactive.\n ENV_NAME=\"${PROJECT} (Preview)\"\n gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n | jq -r \".[] | select(.environment == \\\"${ENV_NAME}\\\") | .id\" \\\n | while read -r deploy_id; do\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${deploy_id}/statuses\" \\\n --method POST --field state=inactive 2>/dev/null || true\n done\n";
|
|
5336
5490
|
//#endregion
|
|
5337
5491
|
//#region src/templates/workflows/release.yml
|
|
5338
|
-
var release_default = "name: Release\n\n# Semantic-release with OIDC Trusted Publishing.\n# actions/setup-node writes a default NODE_AUTH_TOKEN=${{ github.token }}\n# which shadows OIDC auth. We explicitly clear it so npm falls through to\n# the Trusted Publisher OIDC exchange.\n# The calling repo must have a .releaserc.json that configures branches,\n# plugins, and any publish options. npm@11+ is installed to support OIDC.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n dry-run:\n description: >\n When true, runs semantic-release --dry-run: analyzes commits and\n previews the next release without git writes or publish. Only\n meaningful for workflow_dispatch triggers; push-triggered runs\n always run fully.\n type: boolean\n required: false\n default: false\n run-build:\n description: Run `pnpm build` before releasing\n type: boolean\n required: false\n default: true\n sentry-project:\n description: >\n Sentry project slug for sourcemap upload and release creation after\n publishing. Omit to skip the Sentry release step entirely.\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_RELEASE_TOKEN:\n description: >\n Fine-grained PAT (Contents + Issues + Pull requests: write) owned by\n an admin. Required when the default branch is protected by a ruleset —\n github.token cannot push through rulesets, but an admin PAT can.\n Takes priority over HOLOCRON_SYNC_TOKEN. Falls back to github.token.\n required: false\n HOLOCRON_SYNC_TOKEN:\n description: >\n Legacy alias for HOLOCRON_RELEASE_TOKEN — kept for backward compatibility.\n Prefer HOLOCRON_RELEASE_TOKEN for new repos.\n required: false\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to github.token.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when\n HOLOCRON_READ_TOKEN is not set.\n required: false\n SENTRY_AUTH_TOKEN:\n description: >\n Sentry auth token for sourcemap upload and release creation.\n Required when sentry-project is set. Use the org-level secret.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n release:\n name: Semantic release\n permissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Do not cancel in-progress releases — a partial release is worse than a slow one.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n persist-credentials: false\n # Use HOLOCRON_RELEASE_TOKEN when available — git push (tags, release commits)\n # uses the checkout credential, not GITHUB_TOKEN env var. The\n # built-in github.token cannot push through branch protection rulesets.\n token: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Configure git identity\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n git config --global user.name \"$GIT_NAME\"\n git config --global user.email \"$GIT_EMAIL\"\n git config --global format.signoff true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Upgrade npm for OIDC support\n run: npm install -g npm@11 sigstore\n # sigstore is required by libnpmpublish/provenance.js at module parse\n # time — before any config takes effect. Some npm 11.x builds stopped\n # bundling it; installing it globally into the same prefix ensures it\n # resolves regardless of npm version. (Discovered 2026-07-09.)\n\n - run: pnpm build\n name: Build\n if: ${{ inputs.run-build == true }}\n\n - name: Release\n run: |\n if [ \"$DRY_RUN\" = \"true\" ]; then\n npx semantic-release --dry-run\n else\n npx semantic-release\n fi\n env:\n DRY_RUN: ${{ inputs.dry-run }}\n # Prefer HOLOCRON_RELEASE_TOKEN (fine-grained PAT, Contents+Issues+PRs write,\n # owned by an admin with ruleset bypass) so @semantic-release/git can\n # push the version-bump commit through branch protection. Falls back to\n # HOLOCRON_SYNC_TOKEN (legacy) then github.token for unprotected repos.\n GITHUB_TOKEN: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n HUSKY: \"0\"\n NPM_CONFIG_PROVENANCE: true\n\n - name: Get release version\n id: release_version\n if: ${{ inputs.sentry-project != '' && inputs.dry-run != true }}\n env:\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n run: |\n TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo \"\")\n if [ -n \"$TAG\" ]; then\n echo \"release=${SENTRY_PROJECT}@${TAG#v}\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - name: Create Sentry release\n if: ${{ inputs.sentry-project != '' && steps.release_version.outputs.release != '' && inputs.dry-run != true }}\n uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3\n env:\n SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n SENTRY_ORG: theholocron\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n with:\n environment: production\n version: ${{ steps.release_version.outputs.release }}\n sourcemaps: \"**/dist\"\n";
|
|
5492
|
+
var release_default = "name: Release\n\n# Semantic-release with OIDC Trusted Publishing.\n# actions/setup-node writes a default NODE_AUTH_TOKEN=${{ github.token }}\n# which shadows OIDC auth. We explicitly clear it so npm falls through to\n# the Trusted Publisher OIDC exchange.\n# The calling repo must have a .releaserc.json that configures branches,\n# plugins, and any publish options. npm@11+ is installed to support OIDC.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n dry-run:\n description: >\n When true, runs semantic-release --dry-run: analyzes commits and\n previews the next release without git writes or publish. Only\n meaningful for workflow_dispatch triggers; push-triggered runs\n always run fully.\n type: boolean\n required: false\n default: false\n post-release:\n description: >\n When true, dispatches a readme sync broadcast after a successful\n release. Enable for packages consumed by other repos via the\n registry-doc system so downstream README installation blocks\n stay current.\n type: boolean\n required: false\n default: false\n run-build:\n description: Run `pnpm build` before releasing\n type: boolean\n required: false\n default: true\n sentry-project:\n description: >\n Sentry project slug for sourcemap upload and release creation after\n publishing. Omit to skip the Sentry release step entirely.\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_RELEASE_TOKEN:\n description: >\n Fine-grained PAT (Contents + Issues + Pull requests: write) owned by\n an admin. Required when the default branch is protected by a ruleset —\n github.token cannot push through rulesets, but an admin PAT can.\n Takes priority over HOLOCRON_SYNC_TOKEN. Falls back to github.token.\n required: false\n HOLOCRON_SYNC_TOKEN:\n description: >\n Legacy alias for HOLOCRON_RELEASE_TOKEN — kept for backward compatibility.\n Prefer HOLOCRON_RELEASE_TOKEN for new repos.\n required: false\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to github.token.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when\n HOLOCRON_READ_TOKEN is not set.\n required: false\n SENTRY_AUTH_TOKEN:\n description: >\n Sentry auth token for sourcemap upload and release creation.\n Required when sentry-project is set. Use the org-level secret.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n release:\n name: Semantic release\n permissions:\n contents: write\n id-token: write\n issues: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 30\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n # Do not cancel in-progress releases — a partial release is worse than a slow one.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n persist-credentials: false\n # Use HOLOCRON_RELEASE_TOKEN when available — git push (tags, release commits)\n # uses the checkout credential, not GITHUB_TOKEN env var. The\n # built-in github.token cannot push through branch protection rulesets.\n token: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - name: Configure git identity\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n git config --global user.name \"$GIT_NAME\"\n git config --global user.email \"$GIT_EMAIL\"\n git config --global format.signoff true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.GH_TOKEN || github.token }}\n\n - name: Upgrade npm for OIDC support\n run: npm install -g npm@11 sigstore\n # sigstore is required by libnpmpublish/provenance.js at module parse\n # time — before any config takes effect. Some npm 11.x builds stopped\n # bundling it; installing it globally into the same prefix ensures it\n # resolves regardless of npm version. (Discovered 2026-07-09.)\n\n - run: pnpm build\n name: Build\n if: ${{ inputs.run-build == true }}\n\n - name: Release\n run: |\n if [ \"$DRY_RUN\" = \"true\" ]; then\n npx semantic-release --dry-run\n else\n npx semantic-release\n fi\n env:\n DRY_RUN: ${{ inputs.dry-run }}\n # Prefer HOLOCRON_RELEASE_TOKEN (fine-grained PAT, Contents+Issues+PRs write,\n # owned by an admin with ruleset bypass) so @semantic-release/git can\n # push the version-bump commit through branch protection. Falls back to\n # HOLOCRON_SYNC_TOKEN (legacy) then github.token for unprotected repos.\n GITHUB_TOKEN: ${{ secrets.HOLOCRON_RELEASE_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || github.token }}\n HUSKY: \"0\"\n NPM_CONFIG_PROVENANCE: true\n\n - name: Get release version\n id: release_version\n if: ${{ inputs.sentry-project != '' && inputs.dry-run != true }}\n env:\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n run: |\n TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo \"\")\n if [ -n \"$TAG\" ]; then\n echo \"release=${SENTRY_PROJECT}@${TAG#v}\" >> \"$GITHUB_OUTPUT\"\n fi\n\n - name: Create Sentry release\n if: ${{ inputs.sentry-project != '' && steps.release_version.outputs.release != '' && inputs.dry-run != true }}\n uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3\n env:\n SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}\n SENTRY_ORG: theholocron\n SENTRY_PROJECT: ${{ inputs.sentry-project }}\n with:\n environment: production\n version: ${{ steps.release_version.outputs.release }}\n sourcemaps: \"**/dist\"\n\n sync-readme:\n name: Post-release readme sync\n needs: release\n if: ${{ inputs.post-release == true && inputs.dry-run != true }}\n runs-on: ubuntu-latest\n timeout-minutes: 5\n steps:\n - name: Trigger broadcast readme sync\n run: |\n gh workflow run sync-dispatch.yml \\\n --repo theholocron/.github \\\n --field \"steps=readme\"\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n";
|
|
5339
5493
|
//#endregion
|
|
5340
5494
|
//#region src/templates/workflows/review.yml
|
|
5341
5495
|
var review_default = "name: Review\n\n# ReviewDog is the annotation layer — posts inline PR diff annotations.\n# Runs on pull_request only: inline annotations require PR context,\n# and branch protection ensures all changes go through PRs anyway.\n# super-linter (lint.yml) is the CI gate covering push + PR events.\n# Gitleaks and YAML are intentionally duplicated: super-linter gates\n# merges; ReviewDog surfaces exact line annotations in the PR diff.\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n\njobs:\n reviewdog:\n name: Review PRs\n runs-on: ubuntu-latest\n timeout-minutes: 20\n permissions:\n contents: read\n pull-requests: write\n\n steps:\n - name: Checkout repository\n uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n fetch-depth: 0\n\n - name: Setup\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: theholocron/.github/.github/actions/setup@main\n\n - name: Install ReviewDog\n uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1\n with:\n reviewdog_version: latest\n\n # Detect which tools are relevant for this repo, excluding node_modules.\n # hashFiles('**/*') recurses into node_modules/.pnpm and produces false\n # positives for repos that don't own those file types.\n # -print -quit stops find after the first match without a pipe, avoiding\n # the SIGPIPE/pipefail exit-141 that find|head-1 triggers under\n # GitHub Actions' default bash --noprofile --norc -e -o pipefail mode.\n - name: Detect project features\n id: detect\n shell: bash\n run: |\n has() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n has_ext() { find . -not -path '*/node_modules/*' -name \"$1\" -print -quit 2>/dev/null | grep -q .; }\n { { has 'eslint.config.js' || has 'eslint.config.mjs' || has 'eslint.config.cjs' || \\\n has 'eslint.config.ts' || has '.eslintrc' || has '.eslintrc.js' || \\\n has '.eslintrc.cjs' || has '.eslintrc.json' || has '.eslintrc.yaml' || \\\n has '.eslintrc.yml'; } && grep -qF '\"eslint\":' package.json 2>/dev/null; } && echo \"eslint=true\" >> \"$GITHUB_OUTPUT\" || echo \"eslint=false\" >> \"$GITHUB_OUTPUT\"\n { has 'tsconfig.json' && grep -qF '\"typescript\":' package.json 2>/dev/null; } && echo \"tsconfig=true\" >> \"$GITHUB_OUTPUT\" || echo \"tsconfig=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.sh' && echo \"shell=true\" >> \"$GITHUB_OUTPUT\" || echo \"shell=false\" >> \"$GITHUB_OUTPUT\"\n has 'Dockerfile' || has_ext '*.Dockerfile' || has 'Containerfile' && \\\n echo \"docker=true\" >> \"$GITHUB_OUTPUT\" || echo \"docker=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '.env*' && echo \"dotenv=true\" >> \"$GITHUB_OUTPUT\" || echo \"dotenv=false\" >> \"$GITHUB_OUTPUT\"\n has_ext '*.md' && echo \"markdown=true\" >> \"$GITHUB_OUTPUT\" || echo \"markdown=false\" >> \"$GITHUB_OUTPUT\"\n\n #\n # Always applicable\n #\n\n - name: Gitleaks (secrets)\n uses: reviewdog/action-gitleaks@2b7b5685e3e3eecddab5d30cfa04f18123031421 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / gitleaks\"\n fail_level: error\n gitleaks_flags: --log-opts=${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}\n\n - name: YamlLint\n if: ${{ hashFiles('yamllint.config.yml') != '' }}\n uses: reviewdog/action-yamllint@b5f7217d8c815ae374d1d55840d5e569d82f01f0 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / yamllint\"\n fail_level: error\n yamllint_flags: -c ${{ github.workspace }}/yamllint.config.yml ${{ github.workspace }}\n\n - name: ActionLint (GitHub Actions)\n if: ${{ hashFiles('.github/workflows/*.yml', '.github/workflows/*.yaml') != '' }}\n uses: reviewdog/action-actionlint@6fb7acc99f4a1008869fa8a0f09cfca740837d9d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / actionlint\"\n fail_level: error\n\n #\n # TypeScript / JavaScript\n #\n\n - name: ESLint\n if: steps.detect.outputs.eslint == 'true'\n uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / eslint\"\n fail_level: error\n eslint_flags: .\n\n - name: TypeScript\n if: steps.detect.outputs.tsconfig == 'true'\n uses: EPMatt/reviewdog-action-tsc@63d923a3c5b4497671940b8874f58a404e2351b5 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / tsc\"\n fail_level: error\n\n #\n # Shell\n #\n\n - name: ShellCheck\n if: steps.detect.outputs.shell == 'true'\n uses: reviewdog/action-shellcheck@4c07458293ac342d477251099501a718ae5ef86e # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / shellcheck\"\n fail_level: none\n\n #\n # Docker\n #\n\n - name: Hadolint\n if: steps.detect.outputs.docker == 'true'\n uses: reviewdog/action-hadolint@1b2cfa6ba72072ad35158d7ff3aa49bbdc03506d # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / hadolint\"\n fail_level: none\n\n #\n # Environment files\n #\n\n - name: dotenv-linter\n if: steps.detect.outputs.dotenv == 'true'\n uses: dotenv-linter/action-dotenv-linter@afde61cfda2ecffe7bea35837b6f20b956c88689 # v3.0.0\n with:\n reporter: github-code-suggestions\n\n #\n # Documentation\n #\n\n - name: Alex (inclusive language)\n if: steps.detect.outputs.markdown == 'true'\n uses: reviewdog/action-alex@347481655add010a2ae302df34b57c9bcfa0d6e4 # v1\n with:\n reporter: github-pr-annotations\n tool_name: \"Review / alex\"\n";
|
|
5342
5496
|
//#endregion
|
|
5497
|
+
//#region src/templates/workflows/security.yml
|
|
5498
|
+
var security_default = "name: Security\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n language:\n description: CodeQL language to analyze\n type: string\n required: false\n default: javascript-typescript\n\njobs:\n analyze:\n name: Analyze (${{ inputs.language }})\n permissions:\n actions: read\n contents: read\n security-events: write\n runs-on: ubuntu-latest\n timeout-minutes: 45\n # Do not cancel in-progress security scans.\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Initialize CodeQL\n with:\n languages: ${{ inputs.language }}\n\n - uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Autobuild\n\n - uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0\n name: Analyze\n with:\n category: /language:${{ inputs.language }}\n";
|
|
5499
|
+
//#endregion
|
|
5343
5500
|
//#region src/templates/workflows/stale.yml
|
|
5344
5501
|
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";
|
|
5345
5502
|
//#endregion
|
|
5346
5503
|
//#region src/templates/workflows/sync.yml
|
|
5347
5504
|
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 description\".\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";
|
|
5348
5505
|
//#endregion
|
|
5349
|
-
//#region src/templates/workflows/sync-
|
|
5350
|
-
var
|
|
5506
|
+
//#region src/templates/workflows/sync-dispatch.yml
|
|
5507
|
+
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";
|
|
5351
5508
|
//#endregion
|
|
5352
5509
|
//#region src/templates/workflows/sync-github.yml
|
|
5353
|
-
var sync_github_default = "name: Sync
|
|
5510
|
+
var sync_github_default = "name: Sync workflow templates\n\n# Builds the holocron CLI from source and pushes updated workflow templates\n# and composite actions to downstream .github repos. Runs whenever the\n# template source files change on main or alpha.\n#\n# Secrets required:\n# HOLOCRON_SYNC_TOKEN — fine-grained PAT (resource owner: org) with:\n# Actions: Read and write (dispatch workflow runs via gh workflow run)\n# Contents: Read and write (git trees, blobs, refs)\n# Pull requests: Read and write (open sync PR)\n# Workflows: Read and write (write .github/workflows/*.yml)\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n primary-repo:\n description: >\n Primary .github repo — receives composite actions, reusable workflows,\n and thin-caller templates. Requires a PR (branch protection assumed).\n type: string\n required: false\n default: theholocron/.github\n secondary-repos:\n description: >\n Space-separated list of secondary repos (reusable workflows + thin\n callers only, no composite actions). Changes are delivered via pull\n request, same as the primary repo.\n type: string\n required: false\n default: \"\"\n sync-branch:\n description: Branch name used for the primary and secondary repo PRs\n type: string\n required: false\n default: chore/sync-templates\n secrets:\n HOLOCRON_SYNC_TOKEN:\n required: true\n HOLOCRON_READ_TOKEN:\n description: >\n Fine-grained PAT for read-only GitHub API calls (e.g. resolving git\n committer identity via `gh api user`). Falls back to HOLOCRON_SYNC_TOKEN.\n required: false\n GH_TOKEN:\n description: >\n Generic GitHub token fallback for `gh` CLI calls. Used when neither\n HOLOCRON_READ_TOKEN nor HOLOCRON_SYNC_TOKEN is set.\n required: false\n TURBO_TOKEN:\n required: false\n\njobs:\n sync:\n name: Sync templates\n runs-on: ubuntu-latest\n timeout-minutes: 15\n permissions:\n contents: read\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm build\n name: Build CLI\n\n - name: Cache actionlint\n id: cache-actionlint\n uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0\n with:\n path: /tmp/actionlint\n key: actionlint-v1.7.7-linux-amd64\n\n - name: Download actionlint\n if: steps.cache-actionlint.outputs.cache-hit != 'true'\n run: |\n curl -fsSL https://github.com/rhysd/actionlint/releases/download/v1.7.7/actionlint_1.7.7_linux_amd64.tar.gz \\\n | tar -xz -C /tmp actionlint\n\n - name: Validate generated workflows\n run: |\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --output-dir /tmp/sync-validate\n /tmp/actionlint /tmp/sync-validate/.github/workflows/*.yml\n env:\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n\n - name: Sync primary repo (PR)\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$PRIMARY_REPO\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$PRIMARY_REPO\" \"$SYNC_BRANCH\" 2>/dev/null || true\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n PRIMARY_REPO: ${{ inputs.primary-repo }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n\n - name: Sync secondary repos (PR)\n if: ${{ inputs.secondary-repos != '' }}\n run: |\n GIT_NAME=$(gh api user --jq .name 2>/dev/null || echo \"github-actions[bot]\")\n GIT_EMAIL=$(gh api user --jq '\"\\(.id)+\\(.login)@users.noreply.github.com\"' 2>/dev/null || echo \"41898282+github-actions[bot]@users.noreply.github.com\")\n COMMIT_MSG=\"chore: sync from theholocron/holocron\"$'\\n\\n'\"Signed-off-by: $GIT_NAME <$GIT_EMAIL>\"\n for repo in $SECONDARY_REPOS; do\n node packages/cli/dist/cli.mjs sync-github \\\n --repo \"$repo\" \\\n --branch \"$SYNC_BRANCH\" \\\n --pr \\\n --message \"$COMMIT_MSG\"\n GH_TOKEN=\"$HOLOCRON_SYNC_TOKEN\" gh pr merge --auto --squash \\\n --repo \"$repo\" \"$SYNC_BRANCH\" 2>/dev/null || true\n done\n env:\n GH_TOKEN: ${{ secrets.HOLOCRON_READ_TOKEN || secrets.HOLOCRON_SYNC_TOKEN || secrets.GH_TOKEN }}\n HOLOCRON_SYNC_TOKEN: ${{ secrets.HOLOCRON_SYNC_TOKEN }}\n SECONDARY_REPOS: ${{ inputs.secondary-repos }}\n SYNC_BRANCH: ${{ inputs.sync-branch }}\n";
|
|
5354
5511
|
//#endregion
|
|
5355
5512
|
//#region src/templates/workflows/tag.yml
|
|
5356
5513
|
var tag_default = "name: Tag\n\n# Release Please — fully automated tag and GitHub Release from Conventional Commits.\n# No package.json or npm publishing required. Operates in \"simple\" mode by default:\n# analyzes commits since the last tag, maintains a rolling Release PR, and creates\n# a tag + GitHub Release when that PR is merged.\n#\n# The calling repo must have two files at the root:\n# release-please-config.json — declares packages and release-type\n# .release-please-manifest.json — tracks the current version\n\non: # yamllint disable-line rule:truthy\n # Self-trigger: when this workflow lives in theholocron/.github itself,\n # push to main runs Release Please for that repo's own releases.\n push:\n branches:\n - main\n workflow_call:\n inputs:\n release-type:\n description: Release Please release type (simple, node, python, etc.)\n type: string\n required: false\n default: simple\n config-file:\n description: Path to release-please-config.json\n type: string\n required: false\n default: release-please-config.json\n manifest-file:\n description: Path to .release-please-manifest.json\n type: string\n required: false\n default: .release-please-manifest.json\n\njobs:\n tag:\n name: Tag release\n permissions:\n contents: write\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: google-github-actions/release-please-action@e4dc86ba9405554aeba3c6bb2d169500e7d3b4ee # v4.1.1\n name: Run Release Please\n with:\n release-type: ${{ inputs.release-type }}\n config-file: ${{ inputs.config-file }}\n manifest-file: ${{ inputs.manifest-file }}\n";
|
|
@@ -5362,7 +5519,7 @@ var test_default = "name: Test\n\non: # yamllint disable-line rule:truthy\n wor
|
|
|
5362
5519
|
var typecheck_default = "name: Typecheck\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n secrets:\n TURBO_TOKEN:\n required: false\n\njobs:\n typecheck:\n name: tsc --noEmit\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n env:\n TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}\n TURBO_TEAM: ${{ vars.TURBO_TEAM }}\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: pnpm typecheck\n name: Type check\n\n\n conclusion:\n name: Conclusion\n runs-on: ubuntu-latest\n if: always()\n needs: [typecheck]\n steps:\n - name: Check job statuses\n run: |\n if [[ \"$RESULTS\" == *\"failure\"* ]] || [[ \"$RESULTS\" == *\"cancelled\"* ]]; then\n exit 1\n fi\n env:\n RESULTS: ${{ join(needs.*.result, ',') }}\n";
|
|
5363
5520
|
//#endregion
|
|
5364
5521
|
//#region src/templates/workflows/wiki.yml
|
|
5365
|
-
var wiki_default = "name: Wiki\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n fern-version:\n description: >\n Fern CLI version to install. Pin this to avoid breaking changes when\n Fern updates their config schema.\n type: string\n required: false\n default: \"5.35.4\"\n preview:\n description: >\n When true, publishes a preview instead of production.\n Requires preview-id to be set.\n type: boolean\n required: false\n default: false\n preview-id:\n description: >\n Stable ID for the preview URL. The preview is accessible at\n {fern-org}-preview-{id}.docs.buildwithfern.com. Use the PR number\n (e.g. \"pr-123\") so the same URL is reused on every push to\n the branch.\n type: string\n required: false\n default: \"\"\n fern-org:\n description: >\n Fern workspace org slug (e.g. \"holocron\"). When set, a GitHub\n deployment is created after a successful preview so the URL appears\n in the PR sidebar widget — the same pattern as Cloudflare Pages previews.\n type: string\n required: false\n default: \"\"\n base-path:\n description: >\n Basepath appended to the preview URL (e.g. \"holocron\" when using\n multi-source routing with wiki.theholocron.dev/holocron).\n Omit for single-instance Fern sites.\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_FERN_TOKEN:\n required: false\n FERN_TOKEN:\n required: false\n\njobs:\n publish:\n name: ${{ inputs.preview && 'Preview' || 'Publish' }} to
|
|
5522
|
+
var wiki_default = "name: Wiki\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n fern-version:\n description: >\n Fern CLI version to install. Pin this to avoid breaking changes when\n Fern updates their config schema.\n type: string\n required: false\n default: \"5.35.4\"\n preview:\n description: >\n When true, publishes a preview instead of production.\n Requires preview-id to be set.\n type: boolean\n required: false\n default: false\n preview-id:\n description: >\n Stable ID for the preview URL. The preview is accessible at\n {fern-org}-preview-{id}.docs.buildwithfern.com. Use the PR number\n (e.g. \"pr-123\") so the same URL is reused on every push to\n the branch.\n type: string\n required: false\n default: \"\"\n fern-org:\n description: >\n Fern workspace org slug (e.g. \"holocron\"). When set, a GitHub\n deployment is created after a successful preview so the URL appears\n in the PR sidebar widget — the same pattern as Cloudflare Pages previews.\n type: string\n required: false\n default: \"\"\n base-path:\n description: >\n Basepath appended to the preview URL (e.g. \"holocron\" when using\n multi-source routing with wiki.theholocron.dev/holocron).\n Omit for single-instance Fern sites.\n type: string\n required: false\n default: \"\"\n secrets:\n HOLOCRON_FERN_TOKEN:\n required: false\n FERN_TOKEN:\n required: false\n\njobs:\n publish:\n name: ${{ inputs.preview && 'Preview' || 'Publish' }} to Wiki\n runs-on: ubuntu-latest\n timeout-minutes: 10\n permissions:\n contents: read\n deployments: write\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n\n - name: Install Fern CLI\n run: npm install -g \"fern-api@$FERN_VERSION\"\n env:\n FERN_VERSION: ${{ inputs.fern-version }}\n\n - name: Publish docs\n if: ${{ !inputs.preview }}\n run: fern generate --docs\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n\n - name: Preview docs\n if: ${{ inputs.preview && inputs.preview-id != '' }}\n run: fern generate --docs --preview --id \"$PREVIEW_ID\"\n env:\n FERN_TOKEN: ${{ secrets.HOLOCRON_FERN_TOKEN || secrets.FERN_TOKEN }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n\n - name: Report preview URL\n if: ${{ inputs.preview && inputs.preview-id != '' && inputs.fern-org != '' }}\n env:\n GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n FERN_ORG: ${{ inputs.fern-org }}\n PREVIEW_ID: ${{ inputs.preview-id }}\n BASE_PATH: ${{ inputs.base-path }}\n run: |\n PREVIEW_URL=\"https://${FERN_ORG}-preview-${PREVIEW_ID}.docs.buildwithfern.com${BASE_PATH:+/${BASE_PATH}}\"\n PAYLOAD=$(printf '{\"ref\":\"%s\",\"environment\":\"wiki (Preview)\",\"description\":\"Wiki\",\"production_environment\":false,\"auto_merge\":false,\"required_contexts\":[]}' \\\n \"$GITHUB_HEAD_REF\")\n DEPLOY_ID=$(echo \"$PAYLOAD\" | gh api \"repos/${GITHUB_REPOSITORY}/deployments\" \\\n --method POST --input - | jq -r '.id')\n gh api \"repos/${GITHUB_REPOSITORY}/deployments/${DEPLOY_ID}/statuses\" \\\n --method POST --field state=success --field environment_url=\"$PREVIEW_URL\"\n";
|
|
5366
5523
|
//#endregion
|
|
5367
5524
|
//#region src/templates/index.ts
|
|
5368
5525
|
/**
|
|
@@ -5382,18 +5539,16 @@ const ACTIONS = {
|
|
|
5382
5539
|
const REUSABLE_WORKFLOWS = {
|
|
5383
5540
|
audit: audit_default,
|
|
5384
5541
|
bookkeeping: bookkeeping_default,
|
|
5385
|
-
codeql: codeql_default,
|
|
5386
5542
|
dependencies: dependencies_default,
|
|
5387
5543
|
deploy: deploy_default,
|
|
5388
|
-
|
|
5389
|
-
|
|
5544
|
+
preview: preview_default,
|
|
5545
|
+
security: security_default,
|
|
5390
5546
|
greetings: greetings_default,
|
|
5391
5547
|
lint: lint_default,
|
|
5392
|
-
"post-release": post_release_default,
|
|
5393
5548
|
release: release_default,
|
|
5394
5549
|
review: review_default,
|
|
5395
5550
|
stale: stale_default,
|
|
5396
|
-
"sync-
|
|
5551
|
+
"sync-dispatch": sync_dispatch_default,
|
|
5397
5552
|
tag: tag_default,
|
|
5398
5553
|
"sync-github": sync_github_default,
|
|
5399
5554
|
sync: sync_default,
|
|
@@ -5408,6 +5563,11 @@ const WORKFLOW_TEMPLATE_PROPERTIES = { bookkeeping: JSON.stringify({
|
|
|
5408
5563
|
}, null, 2) };
|
|
5409
5564
|
//#endregion
|
|
5410
5565
|
//#region src/commands/sync-github.ts
|
|
5566
|
+
const { workflowHeader } = createHeader({
|
|
5567
|
+
source: "packages/cli/src/commands/sync-github.ts",
|
|
5568
|
+
tool: "holocron sync-github",
|
|
5569
|
+
forPrimary: true
|
|
5570
|
+
});
|
|
5411
5571
|
const DEFAULT_REPO = "theholocron/.github";
|
|
5412
5572
|
function reusableHeader(source) {
|
|
5413
5573
|
return [
|
|
@@ -5424,17 +5584,17 @@ function buildBatch(repo) {
|
|
|
5424
5584
|
const isPrimaryGithubRepo = repo === DEFAULT_REPO;
|
|
5425
5585
|
if (isPrimaryGithubRepo) for (const [name, content] of Object.entries(ACTIONS)) files.push({
|
|
5426
5586
|
path: `.github/actions/${name}.yml`,
|
|
5427
|
-
content: reusableHeader(`packages/cli/src/templates/index.ts`)
|
|
5587
|
+
content: `${reusableHeader(`packages/cli/src/templates/index.ts`)}${content}`
|
|
5428
5588
|
});
|
|
5429
5589
|
if (isPrimaryGithubRepo) {
|
|
5430
5590
|
for (const [name, content] of Object.entries(REUSABLE_WORKFLOWS)) files.push({
|
|
5431
5591
|
path: `.github/workflows/${name}.yml`,
|
|
5432
|
-
content: reusableHeader(`packages/cli/src/templates/index.ts`)
|
|
5592
|
+
content: `${reusableHeader(`packages/cli/src/templates/index.ts`)}${content}`
|
|
5433
5593
|
});
|
|
5434
5594
|
for (const [name, content] of Object.entries(WORKFLOW_TEMPLATES)) {
|
|
5435
5595
|
files.push({
|
|
5436
5596
|
path: `workflow-templates/${name}.yml`,
|
|
5437
|
-
content: workflowHeader(
|
|
5597
|
+
content: `${workflowHeader()}${content}`
|
|
5438
5598
|
});
|
|
5439
5599
|
const props = WORKFLOW_TEMPLATE_PROPERTIES[name];
|
|
5440
5600
|
if (props) files.push({
|
|
@@ -5789,7 +5949,7 @@ async function runUpgradeNode(input) {
|
|
|
5789
5949
|
};
|
|
5790
5950
|
}
|
|
5791
5951
|
//#endregion
|
|
5792
|
-
//#region src/load-config.ts
|
|
5952
|
+
//#region src/config/load-config.ts
|
|
5793
5953
|
/**
|
|
5794
5954
|
* `holocron.config.{json,js,ts}` file loader.
|
|
5795
5955
|
*
|
|
@@ -5910,21 +6070,21 @@ async function fileExists(path) {
|
|
|
5910
6070
|
//#region src/telemetry.ts
|
|
5911
6071
|
const DSN = "https://95cbb72ad5636c94e119a5405ee8f55f@o4508238154104832.ingest.us.sentry.io/4511810950791168";
|
|
5912
6072
|
function isEnabled() {
|
|
5913
|
-
return !
|
|
6073
|
+
return !env.get("NO_HOLOCRON_TELEMETRY") && true;
|
|
5914
6074
|
}
|
|
5915
6075
|
function init(version) {
|
|
5916
6076
|
if (!isEnabled()) return;
|
|
5917
6077
|
Sentry.init({
|
|
5918
6078
|
dsn: DSN,
|
|
5919
6079
|
release: `holocron@${version}`,
|
|
5920
|
-
environment:
|
|
6080
|
+
environment: env.get("CI") ? "ci" : "local",
|
|
5921
6081
|
tracesSampleRate: 1,
|
|
5922
6082
|
beforeSend: scrubError
|
|
5923
6083
|
});
|
|
5924
6084
|
Sentry.startSession();
|
|
5925
6085
|
Sentry.setTag("os", process.platform);
|
|
5926
6086
|
Sentry.setTag("node", process.version);
|
|
5927
|
-
Sentry.setTag("ci", String(Boolean(
|
|
6087
|
+
Sentry.setTag("ci", String(Boolean(env.get("CI"))));
|
|
5928
6088
|
}
|
|
5929
6089
|
function startCommand(name) {
|
|
5930
6090
|
if (!isEnabled()) return () => {};
|
|
@@ -5959,50 +6119,12 @@ function scrubError(event, _hint) {
|
|
|
5959
6119
|
return JSON.parse(redact(JSON.stringify(event)));
|
|
5960
6120
|
}
|
|
5961
6121
|
//#endregion
|
|
5962
|
-
//#region src/token-args.ts
|
|
5963
|
-
var TokenParseError = class extends Error {
|
|
5964
|
-
name = "TokenParseError";
|
|
5965
|
-
};
|
|
5966
|
-
/**
|
|
5967
|
-
* Converts raw --token CLI values into a typed result.
|
|
5968
|
-
*
|
|
5969
|
-
* Bare form: --token ghp_xxx → { cliToken: "ghp_xxx" }
|
|
5970
|
-
* Keyed form: --token github=ghp_xxx → { cliTokens: { github: "ghp_xxx" } }
|
|
5971
|
-
* Mixed: --token github=ghp_xxx --token v_yyy
|
|
5972
|
-
* → { cliToken: "v_yyy", cliTokens: { github: "ghp_xxx" } }
|
|
5973
|
-
*
|
|
5974
|
-
* Values may contain "=" (e.g. base64 strings) — only the first "=" is treated as a separator.
|
|
5975
|
-
*/
|
|
5976
|
-
function parseTokenArgs(tokens) {
|
|
5977
|
-
if (tokens.length === 0) return {};
|
|
5978
|
-
const cliTokens = {};
|
|
5979
|
-
const bare = [];
|
|
5980
|
-
for (const raw of tokens) {
|
|
5981
|
-
const eqIdx = raw.indexOf("=");
|
|
5982
|
-
if (eqIdx === -1) {
|
|
5983
|
-
bare.push(raw);
|
|
5984
|
-
continue;
|
|
5985
|
-
}
|
|
5986
|
-
const vendor = raw.slice(0, eqIdx);
|
|
5987
|
-
const value = raw.slice(eqIdx + 1);
|
|
5988
|
-
if (vendor.trim() === "") throw new TokenParseError(`invalid --token value "${raw}": vendor name must not be empty`);
|
|
5989
|
-
if (/\s/.test(vendor)) throw new TokenParseError(`invalid --token value "${raw}": vendor name must not contain whitespace`);
|
|
5990
|
-
if (value === "") throw new TokenParseError(`invalid --token value "${raw}": token value must not be empty`);
|
|
5991
|
-
cliTokens[vendor] = value;
|
|
5992
|
-
}
|
|
5993
|
-
if (bare.length > 1) throw new TokenParseError(`only one bare --token value is allowed; got ${bare.length.toString()} — use vendor=value form for multiple tokens`);
|
|
5994
|
-
const result = {};
|
|
5995
|
-
if (bare.length === 1) result.cliToken = bare[0];
|
|
5996
|
-
if (Object.keys(cliTokens).length > 0) result.cliTokens = cliTokens;
|
|
5997
|
-
return result;
|
|
5998
|
-
}
|
|
5999
|
-
//#endregion
|
|
6000
6122
|
//#region src/update-notifier.ts
|
|
6001
6123
|
const PACKAGE_NAME = "@theholocron/cli";
|
|
6002
6124
|
const CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
6003
6125
|
const FETCH_TIMEOUT_MS = 3e3;
|
|
6004
6126
|
function getCacheDir() {
|
|
6005
|
-
return
|
|
6127
|
+
return env.get("HOLOCRON_CACHE_DIR") ?? join(homedir(), ".cache", "holocron");
|
|
6006
6128
|
}
|
|
6007
6129
|
function getCachePath() {
|
|
6008
6130
|
return join(getCacheDir(), "update-check.json");
|
|
@@ -6090,7 +6212,7 @@ function formatNotice(current, latest) {
|
|
|
6090
6212
|
].join("\n");
|
|
6091
6213
|
}
|
|
6092
6214
|
async function checkForUpdates(currentVersion) {
|
|
6093
|
-
if (
|
|
6215
|
+
if (env.get("CI") || env.get("NO_UPDATE_NOTIFIER")) return null;
|
|
6094
6216
|
const channel = getChannel(currentVersion);
|
|
6095
6217
|
const cache = readCache();
|
|
6096
6218
|
const now = Date.now();
|
|
@@ -6126,7 +6248,7 @@ const { version: CLI_VERSION } = JSON.parse(readFileSync(new URL("../package.jso
|
|
|
6126
6248
|
* 3. `org` from `holocron.config.ts`
|
|
6127
6249
|
*/
|
|
6128
6250
|
function resolveOrg(argv, config) {
|
|
6129
|
-
return argv.org ??
|
|
6251
|
+
return argv.org ?? env.get("HOLOCRON_ORG") ?? config.org;
|
|
6130
6252
|
}
|
|
6131
6253
|
/** Parses --token values and returns the context spread, or null on parse error (exits with code 1). */
|
|
6132
6254
|
function tokenContext(rawTokens) {
|
|
@@ -6420,7 +6542,7 @@ try {
|
|
|
6420
6542
|
},
|
|
6421
6543
|
...argv.steps && argv.steps.length > 0 ? { steps: argv.steps } : {}
|
|
6422
6544
|
})).summary.fail > 0) process.exitCode = 1;
|
|
6423
|
-
}).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github
|
|
6545
|
+
}).command("sync-github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {
|
|
6424
6546
|
type: "string",
|
|
6425
6547
|
default: "theholocron/.github",
|
|
6426
6548
|
describe: "Target org/repo (default: theholocron/.github)"
|