@theholocron/cli 4.10.2 → 4.11.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/README.md CHANGED
@@ -133,6 +133,21 @@ Writes `"holocron": "<holocronScript ?? 'holocron'>"` plus one
133
133
  `"<task>": "holocron run <task>"` per runnable task. `syncScripts: false`
134
134
  opts out entirely.
135
135
 
136
+ ### `holocron run`
137
+
138
+ ```bash
139
+ holocron run <task> [job] [--required] [--filter <pkg>] [--dry-run] [-- <passthrough>]
140
+ ```
141
+
142
+ Runs one task locally — resolving turbo vs the package manager vs the tool
143
+ itself. A `job` argument targets a sub-job of tasks that have them: `holocron
144
+ run audit performance` runs Lighthouse CI, `holocron run audit knip` runs Knip,
145
+ `holocron run audit` (no job) runs every audit sub-job in declared order. A
146
+ sub-job whose tool isn't installed, or that has no local equivalent
147
+ (`bundle-size`), is skipped with a note — `--required` makes that a failure. For
148
+ a task with no sub-jobs the `job` slot is the first passthrough arg
149
+ (`holocron run build src/`).
150
+
136
151
  ### `holocron ci`
137
152
 
138
153
  ```bash
@@ -146,7 +161,9 @@ first failure — the "will my PR be green?" pre-flight, and what the
146
161
  `--all` forces the full set; `--filter` is a `turbo --filter=` passthrough.
147
162
  Order comes from the `CI_ORDER` constant in `@theholocron/astromech`
148
163
  (`typecheck → lint → test → build → …`). Tasks with no local equivalent
149
- (`audit`, `codeql`, `deploy`) are reported as enforced-in-CI, never failures.
164
+ (`codeql`, `deploy`) are reported as enforced-in-CI, never failures; `audit`
165
+ expands into its sub-jobs (each under its own `audit / …` check context)
166
+ unless the repo ships its own `"audit"` script.
150
167
 
151
168
  ### Lint parity
152
169
 
@@ -306,9 +323,11 @@ all three. See the [telemetry guide](https://docs.theholocron.dev/holocron/telem
306
323
  capability config packages, builds the capability registry
307
324
  - `src/cli.ts` — yargs entry, dispatches subcommands. `holocron run` / `ci` are
308
325
  delegated to [`@theholocron/astromech`](../astromech) (`createAstromech`).
309
- `holocron sync-github` pushes the reusable `workflow_call` implementations +
310
- composite actions from `astromech.reusableTemplates()` to `theholocron/.github`
311
- (a pure sync target — never hand-edit its `.github/workflows/*`).
326
+ `holocron sync` / `holocron setup` write each repo's `.github/workflows/*.yml`
327
+ thin callers from `astromech.thinCallers()`; `holocron sync-github` pushes the
328
+ reusable `workflow_call` implementations + composite actions from
329
+ `astromech.reusableTemplates()` to `theholocron/.github` (a pure sync target —
330
+ never hand-edit its `.github/workflows/*`).
312
331
  - `src/commands/` — `setup`, `sync`, `doctor`, `deploy`, `secret set`,
313
332
  `secrets sync`, `npm publish-initial`, `sync-github`, `upgrade node`,
314
333
  `plugin create`, `auth`
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
4
  import path, { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import { checkbox, input, select } from "@inquirer/prompts";
6
- import { KNOWN_WORKFLOWS, createAstromech, deriveDeployPaths, extractPreviewConfig, generateCombinedDeployContent, generateThinCallerContent, lintThinCallerWith, normalizeWorkflowWith, reusableTemplates } from "@theholocron/astromech";
6
+ import { KNOWN_WORKFLOWS, createAstromech, extractPreviewConfig, reusableTemplates } from "@theholocron/astromech";
7
7
  import { loadTasksConfig } from "@theholocron/astromech/config";
8
8
  import yargs from "yargs";
9
9
  import { hideBin } from "yargs/helpers";
@@ -4385,31 +4385,16 @@ async function runSetup(input) {
4385
4385
  if (loader.has("source") && tasks && tasks.length > 0) {
4386
4386
  const source = loader.get("source");
4387
4387
  print(style.step("workflows"));
4388
+ const files = createAstromech({
4389
+ cwd: input.context.repoRoot,
4390
+ config: { tasks },
4391
+ orgContext: {
4392
+ org: config.org,
4393
+ domain: config.domain
4394
+ }
4395
+ }).thinCallers();
4388
4396
  for (const entry of tasks) {
4389
4397
  const name = typeof entry === "string" ? entry : entry.name;
4390
- const rawWith = typeof entry === "object" ? entry.with : void 0;
4391
- const normalized = rawWith ? normalizeWorkflowWith(rawWith) : void 0;
4392
- let withOverrides = normalized;
4393
- let comments;
4394
- if (name === "lint") {
4395
- let rootFiles = [];
4396
- try {
4397
- rootFiles = readdirSync(input.context.repoRoot);
4398
- } catch {}
4399
- const lint = lintThinCallerWith({
4400
- explicit: typeof entry === "object" ? entry.linters : void 0,
4401
- rootFiles,
4402
- extra: normalized
4403
- });
4404
- withOverrides = lint.withOverrides;
4405
- comments = lint.comments;
4406
- }
4407
- const additionalPaths = (typeof entry === "object" ? entry.paths : void 0) ?? (name === "deploy" && rawWith ? deriveDeployPaths(rawWith) : void 0);
4408
- if (name === "test" && withOverrides) {
4409
- const runUnit = withOverrides["run-unit"];
4410
- const runStorybook = withOverrides["run-storybook"];
4411
- if (runUnit === false && runStorybook === false) throw new ConfigError("test workflow: at least one of \"run-unit\" or \"run-storybook\" must be true. Library repos use run-unit: true; UI/Storybook repos use run-storybook: true.");
4412
- }
4413
4398
  if (!KNOWN_WORKFLOWS.has(name)) {
4414
4399
  steps.push({
4415
4400
  capability: "source",
@@ -4420,22 +4405,12 @@ async function runSetup(input) {
4420
4405
  print(formatStep(steps[steps.length - 1]));
4421
4406
  continue;
4422
4407
  }
4423
- if (name === "deploy" && rawWith) {
4424
- const previewCfg = extractPreviewConfig(rawWith, {
4425
- org: config.org,
4426
- domain: config.domain
4427
- });
4428
- if (previewCfg) {
4429
- const paths = additionalPaths;
4430
- steps.push(await runStep("source", "write workflow deploy (with preview)", dryRun, async () => {
4431
- await source.writeWorkflowFile("deploy.yml", `${workflowHeader$1()}${generateCombinedDeployContent(withOverrides, paths, previewCfg)}`);
4432
- }));
4433
- print(formatStep(steps[steps.length - 1]));
4434
- continue;
4435
- }
4436
- }
4437
- steps.push(await runStep("source", `write workflow ${name}`, dryRun, async () => {
4438
- await source.writeWorkflowFile(`${name}.yml`, `${workflowHeader$1()}${generateThinCallerContent(name, withOverrides, additionalPaths, void 0, comments)}`);
4408
+ const filename = name === "deploy" ? "deploy.yml" : `${name}.yml`;
4409
+ const content = files.get(filename);
4410
+ if (content === void 0) continue;
4411
+ const step = name === "deploy" && content.includes("workflows/preview.yml@main") ? "write workflow deploy (with preview)" : `write workflow ${name}`;
4412
+ steps.push(await runStep("source", step, dryRun, async () => {
4413
+ await source.writeWorkflowFile(filename, `${workflowHeader$1()}${content}`);
4439
4414
  }));
4440
4415
  print(formatStep(steps[steps.length - 1]));
4441
4416
  }
@@ -5647,51 +5622,36 @@ async function runSync(input) {
5647
5622
  message: "no tasks configured"
5648
5623
  });
5649
5624
  print(formatSyncStep(steps[steps.length - 1]));
5650
- } else for (const entry of taskEntries) {
5651
- const name = typeof entry === "string" ? entry : entry.name;
5652
- const rawWith = typeof entry === "object" ? entry.with : void 0;
5653
- const normalized = rawWith ? normalizeWorkflowWith(rawWith) : void 0;
5654
- let withOverrides = normalized;
5655
- let comments;
5656
- if (name === "lint") {
5657
- const lint = lintThinCallerWith({
5658
- explicit: typeof entry === "object" ? entry.linters : void 0,
5659
- rootFiles: safeReaddir(input.context.repoRoot),
5660
- extra: normalized
5661
- });
5662
- withOverrides = lint.withOverrides;
5663
- comments = lint.comments;
5664
- }
5665
- const additionalPaths = (typeof entry === "object" ? entry.paths : void 0) ?? (name === "deploy" && rawWith ? deriveDeployPaths(rawWith) : void 0);
5666
- if (!KNOWN_WORKFLOWS.has(name)) {
5667
- steps.push({
5668
- capability: "local",
5669
- step: `sync workflow ${name}`,
5670
- status: "skip",
5671
- message: `unknown workflow "${name}" — no template available`
5672
- });
5673
- print(formatSyncStep(steps[steps.length - 1]));
5674
- continue;
5675
- }
5676
- if (name === "deploy" && rawWith) {
5677
- const previewCfg = extractPreviewConfig(rawWith, {
5625
+ } else {
5626
+ const files = createAstromech({
5627
+ cwd: input.context.repoRoot,
5628
+ config: { tasks: config.tasks },
5629
+ orgContext: {
5678
5630
  org: config.org,
5679
5631
  domain: config.domain
5680
- });
5681
- if (previewCfg) {
5682
- steps.push(await runSyncStep("local", "sync workflow deploy (with preview)", dryRun, async () => {
5683
- const content = `${workflowHeader()}${generateCombinedDeployContent(withOverrides, additionalPaths, previewCfg)}`;
5684
- await writeWorkflowFile(input.context.repoRoot, "deploy.yml", content);
5685
- }));
5632
+ }
5633
+ }).thinCallers();
5634
+ for (const entry of taskEntries) {
5635
+ const name = typeof entry === "string" ? entry : entry.name;
5636
+ if (!KNOWN_WORKFLOWS.has(name)) {
5637
+ steps.push({
5638
+ capability: "local",
5639
+ step: `sync workflow ${name}`,
5640
+ status: "skip",
5641
+ message: `unknown workflow "${name}" — no template available`
5642
+ });
5686
5643
  print(formatSyncStep(steps[steps.length - 1]));
5687
5644
  continue;
5688
5645
  }
5646
+ const filename = name === "deploy" ? "deploy.yml" : `${name}.yml`;
5647
+ const content = files.get(filename);
5648
+ if (content === void 0) continue;
5649
+ const step = `sync workflow ${name}${name === "deploy" && content.includes("workflows/preview.yml@main") ? " (with preview)" : ""}`;
5650
+ steps.push(await runSyncStep("local", step, dryRun, async () => {
5651
+ await writeWorkflowFile(input.context.repoRoot, filename, `${workflowHeader()}${content}`);
5652
+ }));
5653
+ print(formatSyncStep(steps[steps.length - 1]));
5689
5654
  }
5690
- steps.push(await runSyncStep("local", `sync workflow ${name}`, dryRun, async () => {
5691
- const content = `${workflowHeader()}${generateThinCallerContent(name, withOverrides, additionalPaths, void 0, comments)}`;
5692
- await writeWorkflowFile(input.context.repoRoot, `${name}.yml`, content);
5693
- }));
5694
- print(formatSyncStep(steps[steps.length - 1]));
5695
5655
  }
5696
5656
  }
5697
5657
  if (stepName === "scripts") if (config.syncScripts === false) {
@@ -5781,14 +5741,6 @@ function formatSyncStep(step) {
5781
5741
  const detail = step.message ? ` (${step.message})` : "";
5782
5742
  return ` ${icon} ${step.step}${detail}`;
5783
5743
  }
5784
- /** Repo-root filenames for linter auto-detection; `[]` when the dir is unreadable. */
5785
- function safeReaddir(dir) {
5786
- try {
5787
- return readdirSync(dir);
5788
- } catch {
5789
- return [];
5790
- }
5791
- }
5792
5744
  async function writePackageJsonField(repoRoot, field, value) {
5793
5745
  const pkgPath = join(repoRoot, "package.json");
5794
5746
  let content;
@@ -6983,10 +6935,13 @@ try {
6983
6935
  },
6984
6936
  ...argv.steps && argv.steps.length > 0 ? { steps: argv.steps } : {}
6985
6937
  })).summary.fail > 0) process.exitCode = 1;
6986
- }).command("run <task> [passthrough..]", "Run a task locally (test, typecheck, lint, build) — figures out turbo / the tool / the package manager", (y) => y.positional("task", {
6938
+ }).command("run <task> [job] [passthrough..]", "Run a task locally (test, typecheck, lint, build, audit) — figures out turbo / the tool / the package manager", (y) => y.positional("task", {
6987
6939
  type: "string",
6988
6940
  demandOption: true,
6989
- describe: "Task name (test, typecheck, lint, build). See `holocron run --help`."
6941
+ describe: "Task name (test, typecheck, lint, build, audit). See `holocron run --help`."
6942
+ }).positional("job", {
6943
+ type: "string",
6944
+ describe: "Sub-job within the task (e.g. `holocron run audit performance`). Omit to run every job. Only `audit` has jobs today."
6990
6945
  }).positional("passthrough", {
6991
6946
  type: "string",
6992
6947
  array: true,
@@ -7006,6 +6961,7 @@ try {
7006
6961
  logger,
7007
6962
  config
7008
6963
  }).run(argv.task, {
6964
+ ...argv.job !== void 0 ? { job: argv.job } : {},
7009
6965
  passthrough: argv.passthrough ?? [],
7010
6966
  dryRun: argv.dryRun,
7011
6967
  required: argv.required,
@@ -7424,8 +7380,8 @@ try {
7424
7380
  let extra = [];
7425
7381
  try {
7426
7382
  const raw = readFileSync(join(argv.cwd, "holocron.config.json"), "utf8");
7427
- const upgradeNode = JSON.parse(raw).upgrade?.node;
7428
- if (Array.isArray(upgradeNode?.extra)) extra = upgradeNode.extra;
7383
+ const extraRaw = JSON.parse(raw).upgrade?.node?.extra;
7384
+ if (Array.isArray(extraRaw)) extra = extraRaw;
7429
7385
  } catch {}
7430
7386
  const report = await runUpgradeNode({
7431
7387
  to: argv.to,