@theholocron/cli 5.0.0-alpha.167 → 5.0.0-alpha.169

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 CHANGED
@@ -4,7 +4,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSyn
4
4
  import { homedir, hostname, tmpdir, userInfo } from "node:os";
5
5
  import path, { basename, dirname, join, relative, resolve, sep } from "node:path";
6
6
  import { checkbox, confirm, input, select } from "@inquirer/prompts";
7
- import { KNOWN_WORKFLOWS, createAlexignore, createAlexrc, createAstromech, createCommitMsg, createDependabot, createDevmoji, createEditorconfig, createEditorconfigChecker, createHeader, createLabeler, createPreCommit, createPrePush, createPrepareCommitMsg, createYamlignore, createYamllint, deployOnRelease, extractPreviewConfig, readWorkspacePackages, reusableTemplates } from "@theholocron/astromech";
7
+ import { KNOWN_WORKFLOWS, createAlexignore, createAlexrc, createAstromech, createCommitMsg, createDependabot, createDevmoji, createEditorconfig, createEditorconfigChecker, createHeader, createLabeler, createPreCommit, createPrePush, createPrepareCommitMsg, createYamlignore, createYamllint, defaultListDirs, defaultPaths, defaultReadJson, deployOnRelease, extractPreviewConfig, readWorkspacePackages, reusableTemplates, workspacePackages } from "@theholocron/astromech";
8
8
  import { loadTasksConfig } from "@theholocron/astromech/config";
9
9
  import { lintCommitMsgFile } from "@theholocron/astromech/lint";
10
10
  import { LOG_LEVELS, NoopAnalyticsSink, NoopErrorSink, redactObject } from "@theholocron/observability/core";
@@ -5217,6 +5217,106 @@ const STALE_LABELS = [
5217
5217
  //#region src/templates/dco.yml
5218
5218
  var dco_default = "allowRemediationCommits:\n individual: true\n";
5219
5219
  //#endregion
5220
+ //#region src/commands/preview-deploy-paths/preview-deploy-paths.ts
5221
+ /** `realpathSync`, falling back to the unresolved input on a path that doesn't exist (e.g. a test fixture). */
5222
+ function tryRealpath(path) {
5223
+ try {
5224
+ return realpathSync(path);
5225
+ } catch {
5226
+ return path;
5227
+ }
5228
+ }
5229
+ /**
5230
+ * `delivery.previewDeploy`'s package directory plus its `workspace:*`
5231
+ * dependencies' directories (holocron#1023) — the reusable workflow's
5232
+ * `paths` input, so a PR touching only an unrelated package in the
5233
+ * monorepo skips the actual deploy work instead of spinning up a Vercel
5234
+ * preview for every PR regardless of relevance. `undefined` for a
5235
+ * root-level consumer (`cwd: "."` or omitted) -- every PR already IS a PR
5236
+ * to that package, no filter needed -- or when `cwd` doesn't resolve to
5237
+ * any known workspace package (soft-skip; the caller leaves `paths`
5238
+ * unset rather than guessing).
5239
+ */
5240
+ function resolvePreviewDeployPaths(gitRoot, cwdValue) {
5241
+ if (!cwdValue || cwdValue === ".") return void 0;
5242
+ const all = workspacePackages(gitRoot, {
5243
+ readJson: defaultReadJson,
5244
+ listDirs: defaultListDirs
5245
+ });
5246
+ const target = tryRealpath(resolve(gitRoot, cwdValue));
5247
+ const pkg = all.find((candidate) => tryRealpath(candidate.dir) === target);
5248
+ if (!pkg) return void 0;
5249
+ return defaultPaths(gitRoot, pkg, all);
5250
+ }
5251
+ /**
5252
+ * Returns `tasks` unchanged unless it has a `delivery.previewDeploy` entry
5253
+ * with a non-root `with.cwd` and no `with.paths` of its own -- in which
5254
+ * case that one entry gets an auto-computed `paths` (see
5255
+ * {@link resolvePreviewDeployPaths}), added without mutating the input.
5256
+ * An explicit `with.paths` from the repo's own config always wins; this
5257
+ * only fills in what the repo didn't already specify.
5258
+ */
5259
+ function withAutoPreviewDeployPaths(tasks, gitRoot) {
5260
+ return tasks.map((entry) => {
5261
+ if (typeof entry === "string" || entry.name !== "delivery.previewDeploy") return entry;
5262
+ const cwdValue = entry.with?.["cwd"];
5263
+ if (typeof cwdValue !== "string" || entry.with?.["paths"] !== void 0) return entry;
5264
+ const paths = resolvePreviewDeployPaths(gitRoot, cwdValue);
5265
+ if (!paths) return entry;
5266
+ return {
5267
+ ...entry,
5268
+ with: {
5269
+ ...entry.with,
5270
+ paths
5271
+ }
5272
+ };
5273
+ });
5274
+ }
5275
+ //#endregion
5276
+ //#region src/commands/workflow-source-marker/workflow-source-marker.ts
5277
+ /** `# Source package: <path>` line at the head of a generated thin-caller workflow — see {@link resolvePkgRelPath}. */
5278
+ const SOURCE_PACKAGE_MARKER = /^# Source package: (.+)$/m;
5279
+ /** Strip control characters (including ANSI escapes) before a value reaches a printed error message. */
5280
+ function sanitizeForDisplay(value) {
5281
+ return value.replace(/[\x00-\x1f\x7f]/g, "");
5282
+ }
5283
+ /**
5284
+ * Repo-root-relative path of the PACKAGE a workflow-writing command run is
5285
+ * scoped to — the directory holding the discovered `holocron.config.*`
5286
+ * (`configFilepath`), not necessarily `--cwd` itself. `loadConfig()` walks
5287
+ * up to find the config from any subdirectory ("every command is usable
5288
+ * from any subdirectory of a repo, not only its exact root"), so a `--cwd`
5289
+ * one level too deep would otherwise stamp a different marker than a later
5290
+ * run from the package's own root and misfire the collision check this
5291
+ * feeds (holocron#1026).
5292
+ *
5293
+ * "." for a root invocation, "apps/sentinel" for a subpackage. Normalized
5294
+ * to forward slashes so the same package produces the same marker on
5295
+ * Windows and POSIX. Throws if the result contains a line break — a path
5296
+ * containing one could otherwise break out of the `# Source package: `
5297
+ * comment line and inject arbitrary workflow YAML when the generated
5298
+ * content is later written. Not reachable through this CLI's own flows
5299
+ * (no code path writes a config file whose directory name contains a line
5300
+ * break), but cheap to reject outright rather than trust the filesystem.
5301
+ */
5302
+ function resolvePkgRelPath(gitRoot, configFilepath) {
5303
+ let root;
5304
+ try {
5305
+ root = realpathSync(gitRoot);
5306
+ } catch {
5307
+ root = gitRoot;
5308
+ }
5309
+ let configDir;
5310
+ try {
5311
+ configDir = realpathSync(dirname(configFilepath));
5312
+ } catch {
5313
+ configDir = dirname(configFilepath);
5314
+ }
5315
+ const pkgRelPath = (relative(root, configDir) || ".").split(sep).join("/");
5316
+ if (/[\r\n]/.test(pkgRelPath)) throw new Error(`refusing to use a package path containing a line break: ${JSON.stringify(pkgRelPath)}`);
5317
+ return pkgRelPath;
5318
+ }
5319
+ //#endregion
5220
5320
  //#region src/commands/setup/agent-prompts-data.ts
5221
5321
  /**
5222
5322
  * Canonical AI engineering workflow role prompts.
@@ -6061,6 +6161,11 @@ async function runSetup(input) {
6061
6161
  const steps = [];
6062
6162
  const repo = config.repo;
6063
6163
  const effectivePreset = repo?.protection;
6164
+ const setupGitRoot = findRootSync(input.context.repoRoot, ".git", {
6165
+ fallback: "start",
6166
+ realpath: true
6167
+ });
6168
+ const setupPkgRelPath = resolvePkgRelPath(setupGitRoot, input.loaded.filepath);
6064
6169
  const hooksEnabled = input.hooks ?? (typeof config.hooks === "boolean" ? config.hooks : typeof config.hooks === "object" ? config.hooks.prePush !== false : effectivePreset === "strict");
6065
6170
  print(style.header(`Holocron setup — ${config.name}${dryRun ? " (dry-run)" : ""}`));
6066
6171
  print(style.dim(` config: ${input.loaded.filepath}`));
@@ -6109,7 +6214,7 @@ async function runSetup(input) {
6109
6214
  print(formatStep(steps[steps.length - 1]));
6110
6215
  }
6111
6216
  }
6112
- const tasks = config.tasks;
6217
+ const tasks = config.tasks && config.tasks.length > 0 ? withAutoPreviewDeployPaths(config.tasks, setupGitRoot) : config.tasks;
6113
6218
  if (loader.has("source") && tasks && tasks.length > 0) {
6114
6219
  const source = loader.get("source");
6115
6220
  print(style.step("workflows"));
@@ -6140,7 +6245,7 @@ async function runSetup(input) {
6140
6245
  if (content === void 0) continue;
6141
6246
  const step = content.includes("workflows/preview.yml@main") ? `write workflow ${name} (with preview)` : `write workflow ${name}`;
6142
6247
  steps.push(await runStep("source", step, dryRun, async () => {
6143
- await source.writeWorkflowFile(filename, `${thinCallerHeader()}${content}`);
6248
+ await source.writeWorkflowFile(filename, `${thinCallerHeader()}# Source package: ${setupPkgRelPath}\n\n${content}`);
6144
6249
  }));
6145
6250
  print(formatStep(steps[steps.length - 1]));
6146
6251
  }
@@ -7243,6 +7348,7 @@ async function runSync(input) {
7243
7348
  } catch {
7244
7349
  resolvedRepoRoot = input.context.repoRoot;
7245
7350
  }
7351
+ const pkgRelPath = resolvePkgRelPath(gitRoot, input.loaded.filepath);
7246
7352
  logger.info({
7247
7353
  config: config.name,
7248
7354
  steps: requestedSteps ?? "all",
@@ -7483,7 +7589,8 @@ async function runSync(input) {
7483
7589
  print(formatSyncStep(steps[steps.length - 1]));
7484
7590
  }
7485
7591
  if (stepName === "workflows") {
7486
- const taskEntries = config.tasks ?? [];
7592
+ const rawTaskEntries = config.tasks ?? [];
7593
+ const taskEntries = rawTaskEntries.length === 0 ? rawTaskEntries : withAutoPreviewDeployPaths(rawTaskEntries, gitRoot);
7487
7594
  if (taskEntries.length === 0) {
7488
7595
  steps.push({
7489
7596
  capability: "local",
@@ -7495,7 +7602,7 @@ async function runSync(input) {
7495
7602
  } else {
7496
7603
  const files = createAstromech({
7497
7604
  cwd: input.context.repoRoot,
7498
- config: { tasks: config.tasks },
7605
+ config: { tasks: taskEntries },
7499
7606
  orgContext: {
7500
7607
  org: config.org,
7501
7608
  domain: config.domain
@@ -7520,7 +7627,8 @@ async function runSync(input) {
7520
7627
  if (content === void 0) continue;
7521
7628
  const step = `sync workflow ${name}${content.includes("workflows/preview.yml@main") ? " (with preview)" : ""}`;
7522
7629
  steps.push(await runSyncStep("local", step, dryRun, async () => {
7523
- await writeWorkflowFile(gitRoot, filename, `${workflowHeader()}${content}`);
7630
+ await assertNoPackageCollision(gitRoot, filename, name, pkgRelPath);
7631
+ await writeWorkflowFile(gitRoot, filename, `${workflowHeader()}# Source package: ${pkgRelPath}\n\n${content}`);
7524
7632
  }));
7525
7633
  print(formatSyncStep(steps[steps.length - 1]));
7526
7634
  }
@@ -7783,6 +7891,32 @@ async function writeWorkflowFile(repoRoot, filename, content) {
7783
7891
  await writeFile(tmpPath, content, "utf8");
7784
7892
  await rename(tmpPath, join(dir, filename));
7785
7893
  }
7894
+ /**
7895
+ * Two different monorepo packages opting into the same generic task name
7896
+ * (e.g. both `apps/sentinel` and some future `apps/other` configuring
7897
+ * `delivery.previewDeploy`) would otherwise overwrite each other's
7898
+ * `.github/workflows/<task>.yml` on whichever package's `holocron repo sync`
7899
+ * ran last — only one package's behavior would ever actually run in CI,
7900
+ * with no signal that the other's was silently dropped (holocron#1026).
7901
+ *
7902
+ * A file with no marker at all predates this check (or was written before
7903
+ * #1026) and is never flagged — only a marker that's present AND disagrees
7904
+ * with the current run's own package is a real collision. Any read error
7905
+ * other than "file doesn't exist" propagates rather than being treated as
7906
+ * "nothing to collide with" — the file may genuinely be unreadable, not
7907
+ * missing.
7908
+ */
7909
+ async function assertNoPackageCollision(repoRoot, filename, taskName, pkgRelPath) {
7910
+ let existing;
7911
+ try {
7912
+ existing = await readFile(join(repoRoot, ".github", "workflows", filename), "utf8");
7913
+ } catch (err) {
7914
+ if (err.code === "ENOENT") return;
7915
+ throw err;
7916
+ }
7917
+ const previousPkg = SOURCE_PACKAGE_MARKER.exec(existing)?.[1];
7918
+ if (previousPkg !== void 0 && previousPkg !== pkgRelPath) throw new Error(`task "${taskName}" would overwrite .github/workflows/${filename}, already generated for package "${sanitizeForDisplay(previousPkg)}" — the same task name can't be used by two different packages (holocron#1026). If "${sanitizeForDisplay(previousPkg)}" moved or was renamed to this path, delete .github/workflows/${filename} first and re-run sync; otherwise rename one or give it a different thin-caller filename.`);
7919
+ }
7786
7920
  //#endregion
7787
7921
  //#region src/observability/resolve-telemetry/resolve-telemetry.ts
7788
7922
  /**