@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 +140 -6
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
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
|
|
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:
|
|
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
|
|
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
|
/**
|