@theholocron/cli 5.0.0-alpha.144 → 5.0.0-alpha.146

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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
- import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
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";
@@ -18,8 +18,8 @@ import { createLogger, isCI, parseLogLevel, resolveAxiomFromEnv } from "@theholo
18
18
  import chalk from "chalk";
19
19
  import ora from "ora";
20
20
  import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
21
- import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
22
- import { createHash } from "node:crypto";
21
+ import { access, copyFile, mkdir, readFile, readdir, rename, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
22
+ import { createHash, randomUUID } from "node:crypto";
23
23
  import { generateReadme } from "@theholocron/components-doc/markdown";
24
24
  import { getClients, getConfigs, getDocs, getPlugins, getSkills, getThemes, getUtils } from "@theholocron/registry-doc";
25
25
  import { createGitHubClient } from "@theholocron/github-client";
@@ -6462,6 +6462,55 @@ function runSkillsUpdate(input) {
6462
6462
  return { status: exitCode === 0 ? "ok" : "fail" };
6463
6463
  }
6464
6464
  //#endregion
6465
+ //#region src/utils/find-git-root.ts
6466
+ /**
6467
+ * Walks up from `startDir` looking for a `.git` entry — a directory for a
6468
+ * normal checkout, a file for a worktree/submodule (contains a `gitdir:`
6469
+ * pointer, never followed here; its presence is enough to mark the root).
6470
+ *
6471
+ * `--cwd` picks which `holocron.config.ts` to load, which is NOT
6472
+ * necessarily the git repository root — a monorepo subpackage (e.g.
6473
+ * `packages/sentinel`) has its own config but no `.git` of its own.
6474
+ * Anything inherently repo-wide (`.github/workflows`, `.github/CODEOWNERS`,
6475
+ * `turbo.json`, "is this repo a monorepo") must resolve against the real
6476
+ * root, not `--cwd`, or it silently writes into a GitHub-invisible nested
6477
+ * `.github/` instead (holocron#1014).
6478
+ *
6479
+ * Falls back to the resolved start dir when no `.git` is found walking all
6480
+ * the way to the filesystem root — soft-skip, not a hard failure; every
6481
+ * caller already treats `--cwd` itself as the root today, so an
6482
+ * unexpected non-git checkout just keeps today's behavior.
6483
+ *
6484
+ * Resolves `startDir` to an absolute, symlink-free path before walking:
6485
+ * a relative `--cwd` (e.g. `"packages/sentinel"`) would otherwise hit
6486
+ * `dirname`'s fixed point at `"."` after only two steps, far short of the
6487
+ * real root, and a `--cwd` reached through a symlink would walk the
6488
+ * symlink's own lexical ancestors instead of the physical directory
6489
+ * tree its target actually lives in — in both cases landing on the
6490
+ * wrong directory, not genuinely "no `.git` found anywhere."
6491
+ *
6492
+ * `realpathSync` itself throws when `startDir` doesn't exist on disk at
6493
+ * all (a real possibility in tests, which often use a synthetic path
6494
+ * like `"/tmp/test"` that's never actually created) — caught and treated
6495
+ * the same as "no `.git` found," since this function's whole contract is
6496
+ * soft-skip, never a hard failure.
6497
+ */
6498
+ function findGitRoot(startDir) {
6499
+ let resolvedStart;
6500
+ try {
6501
+ resolvedStart = realpathSync(startDir);
6502
+ } catch {
6503
+ return startDir;
6504
+ }
6505
+ let dir = resolvedStart;
6506
+ for (;;) {
6507
+ if (existsSync(`${dir}/.git`)) return dir;
6508
+ const parent = dirname(dir);
6509
+ if (parent === dir) return resolvedStart;
6510
+ dir = parent;
6511
+ }
6512
+ }
6513
+ //#endregion
6465
6514
  //#region src/commands/sync-readme.ts
6466
6515
  const MARKER_INSTALL_START = "<!-- holocron:installation -->";
6467
6516
  const MARKER_INSTALL_END = "<!-- /holocron:installation -->";
@@ -6868,6 +6917,13 @@ async function runSync(input) {
6868
6917
  const dryRun = input.context.dryRun ?? false;
6869
6918
  const requestedSteps = input.steps;
6870
6919
  const steps = [];
6920
+ const gitRoot = findGitRoot(input.context.repoRoot);
6921
+ let resolvedRepoRoot;
6922
+ try {
6923
+ resolvedRepoRoot = realpathSync(input.context.repoRoot);
6924
+ } catch {
6925
+ resolvedRepoRoot = input.context.repoRoot;
6926
+ }
6871
6927
  logger.info({
6872
6928
  config: config.name,
6873
6929
  steps: requestedSteps ?? "all",
@@ -6908,7 +6964,7 @@ async function runSync(input) {
6908
6964
  const properties = {};
6909
6965
  const effectivePreset = repo?.protection;
6910
6966
  if (effectivePreset && effectivePreset !== "none") properties["holocron_branch_protection_level"] = effectivePreset;
6911
- const isMonorepo = await access(join(input.context.repoRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
6967
+ const isMonorepo = await access(join(gitRoot, "pnpm-workspace.yaml")).then(() => true).catch(() => false);
6912
6968
  properties["monorepo"] = String(isMonorepo);
6913
6969
  const manual = repo?.properties ?? {};
6914
6970
  if (manual.lifecycle) properties["lifecycle"] = manual.lifecycle;
@@ -6916,12 +6972,11 @@ async function runSync(input) {
6916
6972
  if (manual.runtime_environment) properties["runtime_environment"] = manual.runtime_environment;
6917
6973
  if (manual.uses_external_packages !== void 0) properties["uses_external_packages"] = String(manual.uses_external_packages);
6918
6974
  {
6919
- const rootPackageJson = await readFile(join(input.context.repoRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
6920
- const workspacePackages = readWorkspacePackages(input.context.repoRoot);
6921
- const workspacePackageJsons = await readWorkspacePackageJsons(input.context.repoRoot, workspacePackages);
6975
+ const rootPackageJson = await readFile(join(gitRoot, "package.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => null);
6976
+ const workspacePackageJsons = await readWorkspacePackageJsons(gitRoot, readWorkspacePackages(gitRoot));
6922
6977
  properties["holocron_profile"] = deriveProfile({
6923
6978
  rootPackageJson,
6924
- repoName: basename(input.context.repoRoot),
6979
+ repoName: basename(gitRoot),
6925
6980
  isMonorepo,
6926
6981
  workspacePackageJsons
6927
6982
  });
@@ -7146,7 +7201,7 @@ async function runSync(input) {
7146
7201
  if (content === void 0) continue;
7147
7202
  const step = `sync workflow ${name}${content.includes("workflows/preview.yml@main") ? " (with preview)" : ""}`;
7148
7203
  steps.push(await runSyncStep("local", step, dryRun, async () => {
7149
- await writeWorkflowFile(input.context.repoRoot, filename, `${workflowHeader()}${content}`);
7204
+ await writeWorkflowFile(gitRoot, filename, `${workflowHeader()}${content}`);
7150
7205
  }));
7151
7206
  print(formatSyncStep(steps[steps.length - 1]));
7152
7207
  }
@@ -7182,6 +7237,16 @@ async function runSync(input) {
7182
7237
  }
7183
7238
  }
7184
7239
  if (stepName === "turbo") {
7240
+ if (resolvedRepoRoot !== gitRoot) {
7241
+ steps.push({
7242
+ capability: "local",
7243
+ step: "sync turbo.json",
7244
+ status: "skip",
7245
+ message: "--cwd is a monorepo subpackage, not the git root — run from the root to sync turbo.json"
7246
+ });
7247
+ print(formatSyncStep(steps[steps.length - 1]));
7248
+ continue;
7249
+ }
7185
7250
  const astromechTurbo = createAstromech({
7186
7251
  cwd: input.context.repoRoot,
7187
7252
  config: { tasks: config.tasks },
@@ -7198,17 +7263,17 @@ async function runSync(input) {
7198
7263
  print(formatSyncStep(steps[steps.length - 1]));
7199
7264
  } else {
7200
7265
  steps.push(await runSyncStep("local", "sync turbo.json", dryRun, async () => {
7201
- await writeFile(join(input.context.repoRoot, "turbo.json"), content, "utf8");
7266
+ await writeFile(join(gitRoot, "turbo.json"), content, "utf8");
7202
7267
  }));
7203
7268
  print(formatSyncStep(steps[steps.length - 1]));
7204
- const rootPkgRaw = await readFile(join(input.context.repoRoot, "package.json"), "utf8").catch(() => null);
7269
+ const rootPkgRaw = await readFile(join(gitRoot, "package.json"), "utf8").catch(() => null);
7205
7270
  const rootPkg = rootPkgRaw ? JSON.parse(rootPkgRaw) : null;
7206
- const workspaceYaml = await readFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), "utf8").catch(() => "");
7271
+ const workspaceYaml = await readFile(join(gitRoot, "pnpm-workspace.yaml"), "utf8").catch(() => "");
7207
7272
  const rootScripts = rootPkg?.scripts ?? {};
7208
7273
  const workspaceFix = astromechTurbo.ensureRootWorkspaceMember(workspaceYaml, rootScripts);
7209
7274
  if (workspaceFix.changed) {
7210
7275
  steps.push(await runSyncStep("local", "fix pnpm-workspace.yaml (root workspace member, #692)", dryRun, async () => {
7211
- await writeFile(join(input.context.repoRoot, "pnpm-workspace.yaml"), workspaceFix.content, "utf8");
7276
+ await writeFile(join(gitRoot, "pnpm-workspace.yaml"), workspaceFix.content, "utf8");
7212
7277
  }));
7213
7278
  print(formatSyncStep(steps[steps.length - 1]));
7214
7279
  }
@@ -7216,7 +7281,7 @@ async function runSync(input) {
7216
7281
  const depFix = astromechTurbo.ensureTurboDependency(rootPkgRaw, workspaceFix.content);
7217
7282
  if (depFix.changed) {
7218
7283
  steps.push(await runSyncStep("local", "add turbo devDependency", dryRun, async () => {
7219
- await writeFile(join(input.context.repoRoot, "package.json"), depFix.content, "utf8");
7284
+ await writeFile(join(gitRoot, "package.json"), depFix.content, "utf8");
7220
7285
  }));
7221
7286
  print(formatSyncStep(steps[steps.length - 1]));
7222
7287
  }
@@ -7226,7 +7291,10 @@ async function runSync(input) {
7226
7291
  if (stepName === "wiki") {
7227
7292
  const result = await runSyncWiki({
7228
7293
  loaded: input.loaded,
7229
- context: input.context
7294
+ context: {
7295
+ ...input.context,
7296
+ repoRoot: gitRoot
7297
+ }
7230
7298
  });
7231
7299
  steps.push(result);
7232
7300
  print(formatSyncStep(result));
@@ -7379,10 +7447,22 @@ async function updateReadmeDescription(repoRoot, description) {
7379
7447
  await writeFile(readmePath, lines.join("\n"), "utf8");
7380
7448
  return true;
7381
7449
  }
7450
+ /**
7451
+ * Writes via a temp file + rename in the same directory, not a direct
7452
+ * `writeFile` — a monorepo subpackage sync can target the same root-level
7453
+ * file a root sync (or another subpackage's sync) also writes, and a
7454
+ * process killed mid-write would otherwise leave the live GitHub Actions
7455
+ * workflow truncated until some later sync happens to restore it. A same-
7456
+ * directory rename is atomic on POSIX filesystems, so readers only ever
7457
+ * see the old complete content or the new complete content, never a
7458
+ * partial write.
7459
+ */
7382
7460
  async function writeWorkflowFile(repoRoot, filename, content) {
7383
7461
  const dir = join(repoRoot, ".github", "workflows");
7384
7462
  await mkdir(dir, { recursive: true });
7385
- await writeFile(join(dir, filename), content, "utf8");
7463
+ const tmpPath = join(dir, `.${filename}.${randomUUID()}.tmp`);
7464
+ await writeFile(tmpPath, content, "utf8");
7465
+ await rename(tmpPath, join(dir, filename));
7386
7466
  }
7387
7467
  //#endregion
7388
7468
  //#region src/telemetry/resolve.ts