@theholocron/cli 2.0.0-alpha.38 → 2.0.0-alpha.40
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 +110 -25
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1964,20 +1964,86 @@ const WORKFLOW_CHECK_CONTEXTS = {
|
|
|
1964
1964
|
typecheck: "Typecheck / tsc --noEmit"
|
|
1965
1965
|
};
|
|
1966
1966
|
/**
|
|
1967
|
-
* Generate the thin caller content for a workflow, optionally injecting
|
|
1968
|
-
* `with:`
|
|
1967
|
+
* Generate the thin caller content for a workflow, optionally injecting or
|
|
1968
|
+
* merging `with:` overrides into the jobs block.
|
|
1969
|
+
*
|
|
1970
|
+
* Two strategies are used depending on the template:
|
|
1971
|
+
* - Templates that already have a `with:` block (e.g. lint, sync-github):
|
|
1972
|
+
* the override entries are merged in, replacing existing keys and appending
|
|
1973
|
+
* new ones.
|
|
1974
|
+
* - Templates that end with ` secrets: inherit`: a new `with:` block is
|
|
1975
|
+
* injected immediately before `secrets: inherit`.
|
|
1976
|
+
* If neither pattern matches the template, a warning is emitted and the
|
|
1977
|
+
* base template is returned unchanged.
|
|
1969
1978
|
*/
|
|
1970
1979
|
function generateThinCallerContent(name, withOverrides) {
|
|
1971
1980
|
const base = WORKFLOW_TEMPLATES[name];
|
|
1972
1981
|
if (!base) return "";
|
|
1973
1982
|
if (!withOverrides || Object.keys(withOverrides).length === 0) return base;
|
|
1974
|
-
const
|
|
1975
|
-
|
|
1983
|
+
const fmt = (k, v) => ` ${k}: ${v === true ? "true" : v === false ? "false" : String(v)}`;
|
|
1984
|
+
const withBlockRe = /( {4}with:\n)((?:[ ]{6}[^\n]+\n)*)/;
|
|
1985
|
+
const existingMatch = base.match(withBlockRe);
|
|
1986
|
+
if (existingMatch) {
|
|
1987
|
+
const existingEntries = new Map(existingMatch[2].split("\n").filter(Boolean).map((line) => {
|
|
1988
|
+
const m = line.match(/^ {6}([^:]+):\s*(.*)/);
|
|
1989
|
+
return m ? [m[1].trim(), m[2].trim()] : null;
|
|
1990
|
+
}).filter((e) => e !== null));
|
|
1991
|
+
for (const [k, v] of Object.entries(withOverrides)) existingEntries.set(k, v === true ? "true" : v === false ? "false" : String(v));
|
|
1992
|
+
const merged = [...existingEntries.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
|
|
1993
|
+
return base.replace(withBlockRe, ` with:\n${merged}\n`);
|
|
1994
|
+
}
|
|
1995
|
+
const withBlock = Object.entries(withOverrides).map(([k, v]) => fmt(k, v)).join("\n");
|
|
1996
|
+
const result = base.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
|
|
1997
|
+
if (result === base) console.warn(`[generateThinCallerContent] could not inject with: overrides into "${name}" template`);
|
|
1998
|
+
return result;
|
|
1976
1999
|
}
|
|
1977
2000
|
//#endregion
|
|
1978
2001
|
//#region src/commands/sync-github.ts
|
|
1979
2002
|
const DEFAULT_REPO = "theholocron/.github";
|
|
1980
2003
|
const API_BASE = "https://api.github.com";
|
|
2004
|
+
/**
|
|
2005
|
+
* Extracts the `project.workflows` array from a `holocron.config.ts` source string.
|
|
2006
|
+
* Handles both plain string entries and `{ name, with }` object entries.
|
|
2007
|
+
* Falls back to an empty array if the array cannot be found or parsed.
|
|
2008
|
+
*/
|
|
2009
|
+
function parseWorkflowsFromTs(source) {
|
|
2010
|
+
const keyMatch = source.match(/\bworkflows\s*:\s*\[/);
|
|
2011
|
+
if (!keyMatch) return [];
|
|
2012
|
+
const start = keyMatch.index + keyMatch[0].length;
|
|
2013
|
+
let depth = 1;
|
|
2014
|
+
let i = start;
|
|
2015
|
+
while (i < source.length && depth > 0) {
|
|
2016
|
+
if (source[i] === "[") depth++;
|
|
2017
|
+
else if (source[i] === "]") depth--;
|
|
2018
|
+
i++;
|
|
2019
|
+
}
|
|
2020
|
+
const body = source.slice(start, i - 1);
|
|
2021
|
+
const entries = [];
|
|
2022
|
+
const objSpans = [];
|
|
2023
|
+
const objRe = /\{\s*name\s*:\s*"([^"]+)"(?:\s*,\s*with\s*:\s*(\{[^}]*\}))?\s*\}/g;
|
|
2024
|
+
let m;
|
|
2025
|
+
while ((m = objRe.exec(body)) !== null) {
|
|
2026
|
+
objSpans.push([m.index, m.index + m[0].length]);
|
|
2027
|
+
let withObj;
|
|
2028
|
+
if (m[2]) try {
|
|
2029
|
+
withObj = JSON.parse(m[2]);
|
|
2030
|
+
} catch {}
|
|
2031
|
+
entries.push({
|
|
2032
|
+
pos: m.index,
|
|
2033
|
+
entry: {
|
|
2034
|
+
name: m[1],
|
|
2035
|
+
...withObj && { with: withObj }
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
2039
|
+
const strRe = /"([^"]+)"/g;
|
|
2040
|
+
while ((m = strRe.exec(body)) !== null) if (!objSpans.some(([s, e]) => m.index >= s && m.index < e)) entries.push({
|
|
2041
|
+
pos: m.index,
|
|
2042
|
+
entry: { name: m[1] }
|
|
2043
|
+
});
|
|
2044
|
+
entries.sort((a, b) => a.pos - b.pos);
|
|
2045
|
+
return entries.map(({ entry }) => entry);
|
|
2046
|
+
}
|
|
1981
2047
|
function reusableHeader(source) {
|
|
1982
2048
|
return [
|
|
1983
2049
|
`# AUTO-GENERATED — do not edit in theholocron/.github directly.`,
|
|
@@ -1988,40 +2054,47 @@ function reusableHeader(source) {
|
|
|
1988
2054
|
``
|
|
1989
2055
|
].join("\n");
|
|
1990
2056
|
}
|
|
1991
|
-
function thinCallerHeader() {
|
|
2057
|
+
function thinCallerHeader(forPrimary = false) {
|
|
1992
2058
|
return [
|
|
1993
|
-
`# AUTO-GENERATED — do not edit in theholocron/.github directly.`,
|
|
2059
|
+
forPrimary ? `# AUTO-GENERATED — do not edit in theholocron/.github directly.` : `# AUTO-GENERATED — do not edit directly.`,
|
|
1994
2060
|
`# Source: theholocron/holocron · packages/cli/src/commands/setup-workflows.ts`,
|
|
1995
2061
|
`# Synced: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
1996
2062
|
`# Tool: holocron sync-github`,
|
|
1997
|
-
`# Changes: edit
|
|
2063
|
+
`# Changes: edit source in theholocron/holocron and push to alpha or main.`,
|
|
1998
2064
|
``
|
|
1999
2065
|
].join("\n");
|
|
2000
2066
|
}
|
|
2001
|
-
function buildBatch(repo, allowedWorkflows) {
|
|
2067
|
+
function buildBatch(repo, allowedWorkflows, withOverrides) {
|
|
2002
2068
|
const files = [];
|
|
2003
2069
|
const isPrimaryGithubRepo = repo === DEFAULT_REPO;
|
|
2004
2070
|
if (isPrimaryGithubRepo) for (const [name, content] of Object.entries(ACTIONS)) files.push({
|
|
2005
2071
|
path: `.github/actions/${name}.yml`,
|
|
2006
2072
|
content: reusableHeader(`packages/cli/src/templates/index.ts`) + content
|
|
2007
2073
|
});
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
files.push({
|
|
2074
|
+
if (isPrimaryGithubRepo) {
|
|
2075
|
+
for (const [name, content] of Object.entries(REUSABLE_WORKFLOWS)) files.push({
|
|
2011
2076
|
path: `.github/workflows/${name}.yml`,
|
|
2012
2077
|
content: reusableHeader(`packages/cli/src/templates/index.ts`) + content
|
|
2013
2078
|
});
|
|
2014
|
-
|
|
2015
|
-
|
|
2079
|
+
for (const [name, content] of Object.entries(WORKFLOW_TEMPLATES)) {
|
|
2080
|
+
files.push({
|
|
2081
|
+
path: `workflow-templates/${name}.yml`,
|
|
2082
|
+
content: thinCallerHeader(true) + content
|
|
2083
|
+
});
|
|
2084
|
+
const props = WORKFLOW_TEMPLATE_PROPERTIES[name];
|
|
2085
|
+
if (props) files.push({
|
|
2086
|
+
path: `workflow-templates/${name}.properties.json`,
|
|
2087
|
+
content: props
|
|
2088
|
+
});
|
|
2089
|
+
}
|
|
2090
|
+
} else for (const name of Object.keys(REUSABLE_WORKFLOWS)) {
|
|
2091
|
+
if (allowedWorkflows && !allowedWorkflows.has(name)) continue;
|
|
2092
|
+
const content = generateThinCallerContent(name, withOverrides?.get(name));
|
|
2093
|
+
if (!content) continue;
|
|
2016
2094
|
files.push({
|
|
2017
|
-
path:
|
|
2095
|
+
path: `.github/workflows/${name}.yml`,
|
|
2018
2096
|
content: thinCallerHeader() + content
|
|
2019
2097
|
});
|
|
2020
|
-
const props = WORKFLOW_TEMPLATE_PROPERTIES[name];
|
|
2021
|
-
if (props) files.push({
|
|
2022
|
-
path: `workflow-templates/${name}.properties.json`,
|
|
2023
|
-
content: props
|
|
2024
|
-
});
|
|
2025
2098
|
}
|
|
2026
2099
|
return files;
|
|
2027
2100
|
}
|
|
@@ -2098,15 +2171,27 @@ async function runSyncGithub(input) {
|
|
|
2098
2171
|
const { tree: existingTree } = await (await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/trees/${baseTreeSha}?recursive=1`, { headers })).json();
|
|
2099
2172
|
const existingBlobs = new Map(existingTree.filter((i) => i.type === "blob").map((i) => [i.path, i.sha]));
|
|
2100
2173
|
let allowedWorkflows;
|
|
2174
|
+
let withOverrides;
|
|
2101
2175
|
if (repo !== DEFAULT_REPO) try {
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
const
|
|
2106
|
-
|
|
2176
|
+
let entries = [];
|
|
2177
|
+
const jsonRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/contents/holocron.config.json`, { headers });
|
|
2178
|
+
if (jsonRes.ok) {
|
|
2179
|
+
const data = await jsonRes.json();
|
|
2180
|
+
entries = (JSON.parse(Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"))?.project?.workflows ?? []).map((w) => typeof w === "string" ? { name: w } : w);
|
|
2181
|
+
} else {
|
|
2182
|
+
const tsRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/contents/holocron.config.ts`, { headers });
|
|
2183
|
+
if (tsRes.ok) {
|
|
2184
|
+
const data = await tsRes.json();
|
|
2185
|
+
entries = parseWorkflowsFromTs(Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"));
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
if (entries.length > 0) {
|
|
2189
|
+
allowedWorkflows = new Set(entries.map((e) => e.name));
|
|
2190
|
+
const overrideEntries = entries.filter((e) => e.with != null).map((e) => [e.name, e.with]);
|
|
2191
|
+
if (overrideEntries.length > 0) withOverrides = new Map(overrideEntries);
|
|
2107
2192
|
}
|
|
2108
2193
|
} catch {}
|
|
2109
|
-
const batch = buildBatch(repo, allowedWorkflows);
|
|
2194
|
+
const batch = buildBatch(repo, allowedWorkflows, withOverrides);
|
|
2110
2195
|
let created = 0;
|
|
2111
2196
|
let updated = 0;
|
|
2112
2197
|
let unchanged = 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/cli",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
3
|
+
"version": "2.0.0-alpha.40",
|
|
4
4
|
"description": "The Holocron CLI — a pluggable, capability-based orchestrator for spinning up and operating software projects.",
|
|
5
5
|
"homepage": "https://github.com/theholocron/holocron/tree/main/packages/cli#readme",
|
|
6
6
|
"bugs": "https://github.com/theholocron/holocron/issues",
|