@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.
Files changed (2) hide show
  1. package/dist/cli.mjs +110 -25
  2. 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:` inputs before `secrets: inherit` in the jobs block.
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 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`);
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 setup-workflows.ts in theholocron/holocron and push.`,
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
- for (const [name, content] of Object.entries(REUSABLE_WORKFLOWS)) {
2009
- if (allowedWorkflows && !allowedWorkflows.has(name)) continue;
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
- if (isPrimaryGithubRepo) for (const [name, content] of Object.entries(WORKFLOW_TEMPLATES)) {
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: `workflow-templates/${name}.yml`,
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
- const configRes = await fetchFn(`${API_BASE}/repos/${owner}/${repoName}/contents/holocron.config.json`, { headers });
2103
- if (configRes.ok) {
2104
- const configData = await configRes.json();
2105
- const workflows = JSON.parse(Buffer.from(configData.content.replace(/\n/g, ""), "base64").toString("utf8"))?.project?.workflows ?? [];
2106
- if (workflows.length > 0) allowedWorkflows = new Set(workflows.map((w) => typeof w === "string" ? w : w.name));
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.38",
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",