@theholocron/cli 2.1.0 → 2.2.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.
@@ -1,5 +1,4 @@
1
1
  import { ProviderApiError } from "@theholocron/http-client";
2
-
3
2
  //#region src/capabilities/index.d.ts
4
3
  /**
5
4
  * Capability interfaces — the contracts that providers implement.
@@ -276,7 +275,8 @@ interface Issues extends ProviderIdentity {
276
275
  create(input: {
277
276
  summary: string;
278
277
  body?: string;
279
- labels?: string[]; /** Numeric id or exact title (case-insensitive). */
278
+ labels?: string[];
279
+ /** Numeric id or exact title (case-insensitive). */
280
280
  milestone?: string;
281
281
  }): Promise<{
282
282
  key: string;
@@ -322,7 +322,8 @@ interface Deployment extends ProviderIdentity {
322
322
  /** Create if missing, otherwise return existing. Idempotent. */
323
323
  ensureProject(input: {
324
324
  name: string;
325
- framework?: string; /** "owner/repo" — passed when linking to a Git provider. */
325
+ framework?: string;
326
+ /** "owner/repo" — passed when linking to a Git provider. */
326
327
  repo?: string;
327
328
  rootDirectory?: string;
328
329
  }): Promise<DeploymentProject>;
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
- import path, { basename, dirname, join, relative } from "node:path";
4
+ import path, { basename, dirname, join, relative, resolve } from "node:path";
5
5
  import yargs from "yargs";
6
6
  import { hideBin } from "yargs/helpers";
7
7
  import { createInterface } from "node:readline";
@@ -10,13 +10,13 @@ import { AuthError, ProviderApiError, ProviderApiError as ProviderApiError$1 } f
10
10
  import { Entry, findCredentials } from "@napi-rs/keyring";
11
11
  import ora from "ora";
12
12
  import chalk from "chalk";
13
+ import { homedir } from "node:os";
13
14
  import { execFile, execFileSync, spawnSync } from "node:child_process";
14
15
  import { createHash } from "node:crypto";
15
16
  import { createGitHubClient } from "@theholocron/github-client";
16
17
  import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
17
18
  import { pathToFileURL } from "node:url";
18
19
  import { promisify } from "node:util";
19
- import { homedir } from "node:os";
20
20
  //#region src/capabilities/index.ts
21
21
  const CARDINALITY = {
22
22
  source: "single",
@@ -433,6 +433,87 @@ async function tryLoadHint(importer, packageName) {
433
433
  }
434
434
  }
435
435
  //#endregion
436
+ //#region src/commands/clone.ts
437
+ async function listOrgRepos(org, token, fetchFn) {
438
+ const repos = [];
439
+ let url = `https://api.github.com/orgs/${org}/repos?per_page=100&type=all`;
440
+ while (url) {
441
+ const res = await fetchFn(url, { headers: {
442
+ Authorization: `Bearer ${token}`,
443
+ Accept: "application/vnd.github+json",
444
+ "X-GitHub-Api-Version": "2022-11-28"
445
+ } });
446
+ if (!res.ok) throw new Error(`GitHub API ${res.status}: ${res.statusText} — check that the token has org:read scope`);
447
+ repos.push(...await res.json());
448
+ const next = res.headers.get("link")?.match(/<([^>]+)>;\s*rel="next"/);
449
+ url = next ? next[1] : null;
450
+ }
451
+ return repos;
452
+ }
453
+ async function runClone(input) {
454
+ const print = input.print ?? ((line) => console.log(line));
455
+ const fetchFn = input.fetch ?? globalThis.fetch;
456
+ const dryRun = input.dryRun ?? false;
457
+ const targetDir = resolve(input.dir ?? join(homedir(), "Code", input.org));
458
+ const exec = input.exec ?? ((cmd, args, opts) => {
459
+ return { status: spawnSync(cmd, args, {
460
+ cwd: opts.cwd,
461
+ stdio: "inherit"
462
+ }).status };
463
+ });
464
+ print(style.header(`Holocron clone — org=${input.org} → ${targetDir}${dryRun ? " (dry-run)" : ""}`));
465
+ if (!existsSync(targetDir)) if (dryRun) print(style.dim(` would create ${targetDir}`));
466
+ else mkdirSync(targetDir, { recursive: true });
467
+ let repos;
468
+ try {
469
+ repos = await listOrgRepos(input.org, input.token, fetchFn);
470
+ } catch (err) {
471
+ return {
472
+ status: "fail",
473
+ cloned: 0,
474
+ skipped: 0,
475
+ failed: 0,
476
+ message: err instanceof Error ? err.message : String(err)
477
+ };
478
+ }
479
+ let cloned = 0;
480
+ let skipped = 0;
481
+ let failed = 0;
482
+ for (const repo of repos) {
483
+ const dest = join(targetDir, repo.name.startsWith(".") ? repo.name.slice(1) : repo.name);
484
+ if (existsSync(dest)) {
485
+ print(style.dim(` skip ${repo.full_name}`));
486
+ skipped++;
487
+ continue;
488
+ }
489
+ if (dryRun) {
490
+ print(style.dim(` would clone ${repo.full_name} → ${dest}`));
491
+ cloned++;
492
+ continue;
493
+ }
494
+ print(style.step(` clone ${repo.full_name}`));
495
+ if (exec("git", [
496
+ "clone",
497
+ repo.ssh_url,
498
+ dest
499
+ ], { cwd: targetDir }).status !== 0) {
500
+ print(style.fail(` failed ${repo.full_name}`));
501
+ failed++;
502
+ } else {
503
+ print(style.success(` cloned ${repo.name}`));
504
+ cloned++;
505
+ }
506
+ }
507
+ const summary = `${cloned} cloned, ${skipped} skipped, ${failed} failed`;
508
+ print(dryRun ? style.dim(`\n dry-run: ${summary}`) : failed > 0 ? style.fail(`\n ${summary}`) : style.success(`\n ${summary}`));
509
+ return {
510
+ status: dryRun ? "dry-run" : failed > 0 ? "fail" : "ok",
511
+ cloned,
512
+ skipped,
513
+ failed
514
+ };
515
+ }
516
+ //#endregion
436
517
  //#region src/commands/new.ts
437
518
  /**
438
519
  * `holocron new <type> <name>` — create a GitHub repo from a template and
@@ -1100,7 +1181,7 @@ var install_default = "name: Install dependencies\ndescription: Install project
1100
1181
  var setup_node_default = "name: Setup Node\ndescription: Install pnpm and Node.js with pnpm dependency caching.\n\ninputs:\n node-version:\n description: Node.js version\n required: false\n default: \"22.x\"\n\nruns:\n using: composite\n\n steps:\n - name: Setup pnpm\n if: ${{ hashFiles('pnpm-lock.yaml') != '' }}\n uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4\n\n - name: Setup Node.js\n uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4\n with:\n node-version: ${{ hashFiles('.node-version') != '' && '' || inputs.node-version }}\n node-version-file: ${{ hashFiles('.node-version') != '' && '.node-version' || '' }}\n cache: ${{ hashFiles('pnpm-lock.yaml') != '' && 'pnpm' || '' }}\n\n - name: Add node_modules/.bin to PATH\n shell: bash\n run: echo \"$GITHUB_WORKSPACE/node_modules/.bin\" >> $GITHUB_PATH\n";
1101
1182
  //#endregion
1102
1183
  //#region src/templates/workflows/audit.yml
1103
- var audit_default$1 = "name: Audit\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n build-script:\n description: Script to build and upload bundle stats to Codecov\n type: string\n required: false\n default: pnpm build\n secrets:\n CODECOV_TOKEN:\n required: false\n\njobs:\n bundle-size:\n name: Audit the bundle size\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n concurrency:\n group: audit-${{ github.ref }}\n cancel-in-progress: true\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: eval \"$BUILD_SCRIPT\"\n name: Build and upload bundle stats\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n";
1184
+ var audit_default$1 = "name: Audit\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n build-script:\n description: Script to build and upload bundle stats to Codecov\n type: string\n required: false\n default: pnpm build\n run-knip:\n description: Run Knip to detect unused files, exports, and dependencies\n type: boolean\n required: false\n default: false\n knip-script:\n description: Script that invokes Knip (must exit non-zero on findings)\n type: string\n required: false\n default: pnpm run audit\n secrets:\n CODECOV_TOKEN:\n required: false\n\njobs:\n bundle-size:\n name: Audit the bundle size\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 15\n concurrency:\n group: audit-${{ github.ref }}\n cancel-in-progress: true\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n fetch-depth: 0\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: eval \"$BUILD_SCRIPT\"\n name: Build and upload bundle stats\n env:\n BUILD_SCRIPT: ${{ inputs.build-script }}\n CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}\n\n knip:\n name: Knip\n if: ${{ inputs.run-knip }}\n permissions:\n contents: read\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n name: Checkout repository\n with:\n persist-credentials: false\n\n - uses: theholocron/.github/.github/actions/setup@main\n name: Setup\n\n - run: eval \"$KNIP_SCRIPT\"\n name: Run Knip\n env:\n KNIP_SCRIPT: ${{ inputs.knip-script }}\n";
1104
1185
  //#endregion
1105
1186
  //#region src/templates/workflows/bookkeeping.yml
1106
1187
  var bookkeeping_default$1 = "name: Bookkeeping\n\non: # yamllint disable-line rule:truthy\n workflow_call:\n inputs:\n configuration-path:\n description: Path to the labeler configuration file in the calling repo\n type: string\n required: false\n default: .github/labeler.yml\n\njobs:\n label:\n name: Apply Labels\n permissions:\n contents: read\n pull-requests: write\n runs-on: ubuntu-latest\n timeout-minutes: 5\n concurrency:\n group: bookkeeping-${{ github.event.pull_request.number || github.event.issue.number }}\n cancel-in-progress: true\n steps:\n - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0\n with:\n sparse-checkout: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n sparse-checkout-cone-mode: false\n\n - uses: github/issue-labeler@c1b0f9f52a63158c4adc09425e858e87b32e9685 # v3.4\n if: ${{ github.event_name == 'pull_request' && hashFiles(inputs.configuration-path || '.github/labeler.yml') != '' }}\n # v3.4 bundles Node 20; allow it to run under Actions' current default.\n env:\n ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: true\n with:\n # Fall back to default path when triggered directly (not via workflow_call)\n # because inputs.* defaults only apply on workflow_call events.\n configuration-path: ${{ inputs.configuration-path || '.github/labeler.yml' }}\n include-title: 1\n include-body: 0\n sync-labels: 1\n enable-versioned-regex: 0\n repo-token: ${{ github.token }}\n";
@@ -3590,7 +3671,8 @@ async function runSetup(input) {
3590
3671
  "prd"
3591
3672
  ]) {
3592
3673
  steps.push(await runStep("vault", `ensureEnvironment ${envName}`, dryRun, async () => {
3593
- return `${envName} ${(await vault.ensureEnvironment(config.name, envName)).alreadyExists ? "exists" : "created"}`;
3674
+ const result = await vault.ensureEnvironment(config.name, envName);
3675
+ return `${envName} ${result.alreadyExists ? "exists" : "created"}`;
3594
3676
  }));
3595
3677
  print(formatStep(steps[steps.length - 1]));
3596
3678
  }
@@ -3686,11 +3768,10 @@ async function fetchExternalSkill(entry) {
3686
3768
  const res = await fetch(url);
3687
3769
  if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
3688
3770
  const content = await res.text();
3689
- if (entry.computedHash) {
3690
- const actual = createHash("sha256").update(content).digest("hex");
3691
- if (actual !== entry.computedHash) throw new Error(`hash mismatch for ${entry.source}/${entry.skillPath}: expected ${entry.computedHash}, got ${actual}`);
3692
- }
3693
- return content;
3771
+ return {
3772
+ content,
3773
+ stale: !!entry.computedHash && createHash("sha256").update(content).digest("hex") !== entry.computedHash
3774
+ };
3694
3775
  }
3695
3776
  const AGENTS_SKILLS_ROOT = ".agents/skills";
3696
3777
  /** Relative path of the agent-specific symlink. undefined = unsupported agent. */
@@ -3740,6 +3821,7 @@ async function installSkills({ agent, skills, repoRoot }) {
3740
3821
  installed.push(name);
3741
3822
  }
3742
3823
  const externalFailed = [];
3824
+ const externalStale = [];
3743
3825
  if (missing.length > 0) {
3744
3826
  let lock = null;
3745
3827
  try {
@@ -3749,7 +3831,7 @@ async function installSkills({ agent, skills, repoRoot }) {
3749
3831
  const entry = lock.skills[name];
3750
3832
  if (!entry) continue;
3751
3833
  try {
3752
- const content = await fetchExternalSkill(entry);
3834
+ const { content, stale: isStale } = await fetchExternalSkill(entry);
3753
3835
  const agentsDir = join(repoRoot, AGENTS_SKILLS_ROOT, name);
3754
3836
  await mkdir(agentsDir, { recursive: true });
3755
3837
  await writeFile(join(agentsDir, "SKILL.md"), content);
@@ -3761,6 +3843,7 @@ async function installSkills({ agent, skills, repoRoot }) {
3761
3843
  await symlink(relative(dirname(symlinkPath), agentsDir).replace(/\\/g, "/"), symlinkPath);
3762
3844
  missing.splice(missing.indexOf(name), 1);
3763
3845
  installed.push(name);
3846
+ if (isStale) externalStale.push(name);
3764
3847
  } catch {
3765
3848
  missing.splice(missing.indexOf(name), 1);
3766
3849
  externalFailed.push(name);
@@ -3774,6 +3857,7 @@ async function installSkills({ agent, skills, repoRoot }) {
3774
3857
  ], symlinkFn);
3775
3858
  const parts = [`installed ${installed.length}`];
3776
3859
  if (stale.length > 0) parts.push(`pruned: ${stale.join(", ")}`);
3860
+ if (externalStale.length > 0) parts.push(`stale: ${externalStale.join(", ")} (run \`holocron skills update\` to refresh)`);
3777
3861
  if (externalFailed.length > 0) parts.push(`fetch failed: ${externalFailed.join(", ")}`);
3778
3862
  if (missing.length > 0) parts.push(`unknown: ${missing.join(", ")}`);
3779
3863
  return parts.join("; ");
@@ -3799,9 +3883,10 @@ async function copyDirRecursive(src, dest) {
3799
3883
  }
3800
3884
  }
3801
3885
  async function updateSkillsGitignore(gitignorePath, existingContent, skills, symlinkFn) {
3886
+ const entries = [`/${AGENTS_SKILLS_ROOT}/`, ...skills.map((n) => `/${symlinkFn(n)}`)];
3802
3887
  const block = [
3803
3888
  GITIGNORE_BLOCK_START,
3804
- ...[`/${AGENTS_SKILLS_ROOT}/`, ...skills.map((n) => `/${symlinkFn(n)}`)],
3889
+ ...entries,
3805
3890
  GITIGNORE_BLOCK_END
3806
3891
  ].join("\n");
3807
3892
  let content;
@@ -4509,6 +4594,28 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
4509
4594
  describe: "Directory to search for holocron.config.json"
4510
4595
  }).command("version", "Print the CLI version", () => {}, () => {
4511
4596
  console.log(`holocron ${CLI_VERSION}`);
4597
+ }).command("clone", "Clone all repos in a GitHub org as siblings under a single directory", (y) => y.option("org", {
4598
+ type: "string",
4599
+ demandOption: true,
4600
+ describe: "GitHub org to clone (e.g., theholocron)"
4601
+ }).option("dir", {
4602
+ type: "string",
4603
+ describe: "Parent directory to clone into (default: ~/Code/<org>)"
4604
+ }), async (argv) => {
4605
+ const tokens = tokenContext(argv.token);
4606
+ if (!tokens) return;
4607
+ const token = tokens.cliTokens?.["github"] ?? tokens.cliToken ?? process.env.GITHUB_TOKEN ?? process.env.HOLOCRON_GITHUB_TOKEN;
4608
+ if (!token) {
4609
+ console.error("clone: GitHub token required — pass --token or set GITHUB_TOKEN");
4610
+ process.exitCode = 1;
4611
+ return;
4612
+ }
4613
+ if ((await runClone({
4614
+ org: argv.org,
4615
+ token,
4616
+ dryRun: argv.dryRun,
4617
+ ...argv.dir ? { dir: argv.dir } : {}
4618
+ })).status === "fail") process.exitCode = 1;
4512
4619
  }).command("doctor", "Load the config and run a smoke check against every provider", (y) => y.option("repo", {
4513
4620
  type: "string",
4514
4621
  describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
package/dist/index.d.mts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { Analytics, Auth, AuthDescription, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConnectionStringOptions, CreateAuthUserInput, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, EnsureResult, Environment, EnvironmentReviewer, Environments, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, REQUIRED_CAPABILITIES, RepoRef, RepoSettings, ResolvedCapability, Ruleset, SecretScope, Secrets, Source, StatusCategory, Storage, StorageBranch, TeamEntry, TeamPermission, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, isMulti } from "./capabilities/index.mjs";
2
2
  import { AuthError, RequestOptions, ResolveTokenConfig as ResolveTokenConfig$1, ResolveTokenInput, RestClient, RestClientConfig, createRestClient } from "@theholocron/http-client";
3
-
4
3
  //#region src/auth-resolver.d.ts
5
4
  type ResolveTokenConfig = Omit<ResolveTokenConfig$1, "getKeyringToken">;
6
5
  /** Wraps `createResolveToken` from `@theholocron/http` and injects the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
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",