@theholocron/cli 2.0.0-alpha.39 → 2.0.0-alpha.41

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.
Files changed (2) hide show
  1. package/dist/cli.mjs +93 -12
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -971,6 +971,9 @@ jobs:
971
971
 
972
972
  - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4
973
973
  if: \${{ hashFiles(inputs.configuration-path || '.github/labeler.yml') != '' }}
974
+ # v3.4 bundles Node 20; allow it to run under Actions' current default.
975
+ env:
976
+ ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true
974
977
  with:
975
978
  # Fall back to default path when triggered directly (not via workflow_call)
976
979
  # because inputs.* defaults only apply on workflow_call events.
@@ -1964,20 +1967,86 @@ const WORKFLOW_CHECK_CONTEXTS = {
1964
1967
  typecheck: "Typecheck / tsc --noEmit"
1965
1968
  };
1966
1969
  /**
1967
- * Generate the thin caller content for a workflow, optionally injecting
1968
- * `with:` inputs before `secrets: inherit` in the jobs block.
1970
+ * Generate the thin caller content for a workflow, optionally injecting or
1971
+ * merging `with:` overrides into the jobs block.
1972
+ *
1973
+ * Two strategies are used depending on the template:
1974
+ * - Templates that already have a `with:` block (e.g. lint, sync-github):
1975
+ * the override entries are merged in, replacing existing keys and appending
1976
+ * new ones.
1977
+ * - Templates that end with ` secrets: inherit`: a new `with:` block is
1978
+ * injected immediately before `secrets: inherit`.
1979
+ * If neither pattern matches the template, a warning is emitted and the
1980
+ * base template is returned unchanged.
1969
1981
  */
1970
1982
  function generateThinCallerContent(name, withOverrides) {
1971
1983
  const base = WORKFLOW_TEMPLATES[name];
1972
1984
  if (!base) return "";
1973
1985
  if (!withOverrides || Object.keys(withOverrides).length === 0) return base;
1974
- const withBlock = Object.entries(withOverrides).map(([k, v]) => ` ${k}: ${v === true ? "true" : v === false ? "false" : String(v)}`).join("\n");
1975
- return base.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
1986
+ const fmt = (k, v) => ` ${k}: ${v === true ? "true" : v === false ? "false" : String(v)}`;
1987
+ const withBlockRe = /( {4}with:\n)((?:[ ]{6}[^\n]+\n)*)/;
1988
+ const existingMatch = base.match(withBlockRe);
1989
+ if (existingMatch) {
1990
+ const existingEntries = new Map(existingMatch[2].split("\n").filter(Boolean).map((line) => {
1991
+ const m = line.match(/^ {6}([^:]+):\s*(.*)/);
1992
+ return m ? [m[1].trim(), m[2].trim()] : null;
1993
+ }).filter((e) => e !== null));
1994
+ for (const [k, v] of Object.entries(withOverrides)) existingEntries.set(k, v === true ? "true" : v === false ? "false" : String(v));
1995
+ const merged = [...existingEntries.entries()].map(([k, v]) => ` ${k}: ${v}`).join("\n");
1996
+ return base.replace(withBlockRe, ` with:\n${merged}\n`);
1997
+ }
1998
+ const withBlock = Object.entries(withOverrides).map(([k, v]) => fmt(k, v)).join("\n");
1999
+ const result = base.replace(/ {4}secrets: inherit\n$/, ` with:\n${withBlock}\n secrets: inherit\n`);
2000
+ if (result === base) console.warn(`[generateThinCallerContent] could not inject with: overrides into "${name}" template`);
2001
+ return result;
1976
2002
  }
1977
2003
  //#endregion
1978
2004
  //#region src/commands/sync-github.ts
1979
2005
  const DEFAULT_REPO = "theholocron/.github";
1980
2006
  const API_BASE = "https://api.github.com";
2007
+ /**
2008
+ * Extracts the `project.workflows` array from a `holocron.config.ts` source string.
2009
+ * Handles both plain string entries and `{ name, with }` object entries.
2010
+ * Falls back to an empty array if the array cannot be found or parsed.
2011
+ */
2012
+ function parseWorkflowsFromTs(source) {
2013
+ const keyMatch = source.match(/\bworkflows\s*:\s*\[/);
2014
+ if (!keyMatch) return [];
2015
+ const start = keyMatch.index + keyMatch[0].length;
2016
+ let depth = 1;
2017
+ let i = start;
2018
+ while (i < source.length && depth > 0) {
2019
+ if (source[i] === "[") depth++;
2020
+ else if (source[i] === "]") depth--;
2021
+ i++;
2022
+ }
2023
+ const body = source.slice(start, i - 1);
2024
+ const entries = [];
2025
+ const objSpans = [];
2026
+ const objRe = /\{\s*name\s*:\s*"([^"]+)"(?:\s*,\s*with\s*:\s*(\{[^}]*\}))?\s*\}/g;
2027
+ let m;
2028
+ while ((m = objRe.exec(body)) !== null) {
2029
+ objSpans.push([m.index, m.index + m[0].length]);
2030
+ let withObj;
2031
+ if (m[2]) try {
2032
+ withObj = JSON.parse(m[2]);
2033
+ } catch {}
2034
+ entries.push({
2035
+ pos: m.index,
2036
+ entry: {
2037
+ name: m[1],
2038
+ ...withObj && { with: withObj }
2039
+ }
2040
+ });
2041
+ }
2042
+ const strRe = /"([^"]+)"/g;
2043
+ while ((m = strRe.exec(body)) !== null) if (!objSpans.some(([s, e]) => m.index >= s && m.index < e)) entries.push({
2044
+ pos: m.index,
2045
+ entry: { name: m[1] }
2046
+ });
2047
+ entries.sort((a, b) => a.pos - b.pos);
2048
+ return entries.map(({ entry }) => entry);
2049
+ }
1981
2050
  function reusableHeader(source) {
1982
2051
  return [
1983
2052
  `# AUTO-GENERATED — do not edit in theholocron/.github directly.`,
@@ -1998,7 +2067,7 @@ function thinCallerHeader(forPrimary = false) {
1998
2067
  ``
1999
2068
  ].join("\n");
2000
2069
  }
2001
- function buildBatch(repo, allowedWorkflows) {
2070
+ function buildBatch(repo, allowedWorkflows, withOverrides) {
2002
2071
  const files = [];
2003
2072
  const isPrimaryGithubRepo = repo === DEFAULT_REPO;
2004
2073
  if (isPrimaryGithubRepo) for (const [name, content] of Object.entries(ACTIONS)) files.push({
@@ -2023,7 +2092,7 @@ function buildBatch(repo, allowedWorkflows) {
2023
2092
  }
2024
2093
  } else for (const name of Object.keys(REUSABLE_WORKFLOWS)) {
2025
2094
  if (allowedWorkflows && !allowedWorkflows.has(name)) continue;
2026
- const content = generateThinCallerContent(name);
2095
+ const content = generateThinCallerContent(name, withOverrides?.get(name));
2027
2096
  if (!content) continue;
2028
2097
  files.push({
2029
2098
  path: `.github/workflows/${name}.yml`,
@@ -2105,15 +2174,27 @@ async function runSyncGithub(input) {
2105
2174
  const { tree: existingTree } = await (await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/git/trees/${baseTreeSha}?recursive=1`, { headers })).json();
2106
2175
  const existingBlobs = new Map(existingTree.filter((i) => i.type === "blob").map((i) => [i.path, i.sha]));
2107
2176
  let allowedWorkflows;
2177
+ let withOverrides;
2108
2178
  if (repo !== DEFAULT_REPO) try {
2109
- const configRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/contents/holocron.config.json`, { headers });
2110
- if (configRes.ok) {
2111
- const configData = await configRes.json();
2112
- const workflows = JSON.parse(Buffer.from(configData.content.replace(/\n/g, ""), "base64").toString("utf8"))?.project?.workflows ?? [];
2113
- if (workflows.length > 0) allowedWorkflows = new Set(workflows.map((w) => typeof w === "string" ? w : w.name));
2179
+ let entries = [];
2180
+ const jsonRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/contents/holocron.config.json`, { headers });
2181
+ if (jsonRes.ok) {
2182
+ const data = await jsonRes.json();
2183
+ entries = (JSON.parse(Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"))?.project?.workflows ?? []).map((w) => typeof w === "string" ? { name: w } : w);
2184
+ } else {
2185
+ const tsRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/contents/holocron.config.ts`, { headers });
2186
+ if (tsRes.ok) {
2187
+ const data = await tsRes.json();
2188
+ entries = parseWorkflowsFromTs(Buffer.from(data.content.replace(/\n/g, ""), "base64").toString("utf8"));
2189
+ }
2190
+ }
2191
+ if (entries.length > 0) {
2192
+ allowedWorkflows = new Set(entries.map((e) => e.name));
2193
+ const overrideEntries = entries.filter((e) => e.with != null).map((e) => [e.name, e.with]);
2194
+ if (overrideEntries.length > 0) withOverrides = new Map(overrideEntries);
2114
2195
  }
2115
2196
  } catch {}
2116
- const batch = buildBatch(repo, allowedWorkflows);
2197
+ const batch = buildBatch(repo, allowedWorkflows, withOverrides);
2117
2198
  let created = 0;
2118
2199
  let updated = 0;
2119
2200
  let unchanged = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/cli",
3
- "version": "2.0.0-alpha.39",
3
+ "version": "2.0.0-alpha.41",
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",