@theholocron/cli 2.0.0-alpha.67 → 2.0.0-alpha.69

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
@@ -64,6 +64,31 @@ in at load time:
64
64
  A minimal config — for repos with a `package.json` and a GitHub remote
65
65
  — only needs `providers`:
66
66
 
67
+ ### repo options
68
+
69
+ Additional `repo` fields recognised by `holocron setup`:
70
+
71
+ | Field | Type | Description |
72
+ | ------------------ | ----------------------------------------- | ----------- |
73
+ | `repo.teams` | `Array<string \| { slug, permission }>` | GitHub teams granted repo access. String shorthand defaults to `push` (Write). `holocron setup` also writes `.github/CODEOWNERS` for teams with `push`/`maintain`/`admin`. |
74
+ | `repo.topics` | `string[]` | GitHub topics set on the repository. |
75
+ | `repo.protection` | `"balanced" \| "strict" \| "none"` | Branch-protection preset applied by `holocron setup`. |
76
+ | `repo.properties` | `RepoProperties` | Org-level custom property values synced to the GitHub dashboard. |
77
+
78
+ ### Skills installer
79
+
80
+ `holocron setup` can install shared skills from `@theholocron/skills` into the local repo:
81
+
82
+ ```ts
83
+ export default defineConfig({
84
+ agent: "claude", // "claude" | "codex" | "gemini"
85
+ skills: ["git-safety", "pr-workflow"], // skill names from @theholocron/skills
86
+ providers: { source: "github" },
87
+ });
88
+ ```
89
+
90
+ Skills are copied to `.agents/skills/<name>/` and symlinked at the agent's expected path (e.g. `.claude/skills/<name>`). All installed paths are added to a managed block in `.gitignore` automatically.
91
+
67
92
  <!-- prettier-ignore -->
68
93
  ```jsonc
69
94
  { "providers": { "source": "github" } }
@@ -71,6 +71,12 @@ interface LabelDef {
71
71
  readonly color: string;
72
72
  readonly description: string;
73
73
  }
74
+ type TeamPermission = "pull" | "triage" | "push" | "maintain" | "admin";
75
+ /** Shorthand `"slug"` defaults to `push` permission. */
76
+ type TeamEntry = string | {
77
+ slug: string;
78
+ permission: TeamPermission;
79
+ };
74
80
  interface Source extends ProviderIdentity {
75
81
  readonly key: "source";
76
82
  /** Auth sanity-check. Throws ProviderApiError on auth failure. */
@@ -137,6 +143,11 @@ interface Source extends ProviderIdentity {
137
143
  * Optional — providers that don't support topics omit this.
138
144
  */
139
145
  syncTopics?(topics: string[]): Promise<string>;
146
+ /**
147
+ * Sync GitHub team repository access. String shorthand defaults to `push`.
148
+ * Optional — providers without a team concept omit this.
149
+ */
150
+ syncTeams?(teams: TeamEntry[]): Promise<string>;
140
151
  /**
141
152
  * Set the repository description.
142
153
  * Optional — providers that don't support setting descriptions omit this.
@@ -554,4 +565,4 @@ type CardinalityFor<K extends CapabilityKey> = (typeof CARDINALITY)[K];
554
565
  type ResolvedCapability<K extends CapabilityKey> = CardinalityFor<K> extends "many" ? CapabilityImpls[K][] : CapabilityImpls[K];
555
566
  declare function isMulti<K extends CapabilityKey>(key: K): CardinalityFor<K> extends "many" ? true : false;
556
567
  //#endregion
557
- export { 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, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, isMulti };
568
+ export { 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 };
package/dist/cli.mjs CHANGED
@@ -1,18 +1,19 @@
1
1
  #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
2
3
  import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
- import path, { basename, dirname, join } from "node:path";
4
+ import path, { basename, dirname, join, relative } from "node:path";
4
5
  import yargs from "yargs";
5
6
  import { hideBin } from "yargs/helpers";
6
7
  import { createInterface } from "node:readline";
7
8
  import { stdin, stdout } from "node:process";
8
- import { ProviderApiError, ProviderApiError as ProviderApiError$1 } from "@theholocron/http-client";
9
+ import { AuthError, ProviderApiError, ProviderApiError as ProviderApiError$1 } from "@theholocron/http-client";
9
10
  import { Entry, findCredentials } from "@napi-rs/keyring";
10
11
  import ora from "ora";
11
12
  import chalk from "chalk";
12
13
  import { createHash } from "node:crypto";
13
14
  import { createGitHubClient } from "@theholocron/github-client";
14
15
  import { execFile, execFileSync, spawnSync } from "node:child_process";
15
- import { access, readFile, readdir, stat, writeFile } from "node:fs/promises";
16
+ import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
16
17
  import { pathToFileURL } from "node:url";
17
18
  import { promisify } from "node:util";
18
19
  //#region src/capabilities/index.ts
@@ -149,7 +150,9 @@ function resolveConfig(raw) {
149
150
  workflows: raw.workflows,
150
151
  providers,
151
152
  apps: raw.apps ?? [],
152
- doctor: raw.doctor ?? {}
153
+ doctor: raw.doctor ?? {},
154
+ agent: raw.agent,
155
+ skills: raw.skills
153
156
  };
154
157
  }
155
158
  //#endregion
@@ -3298,6 +3301,36 @@ async function runSetup(input) {
3298
3301
  steps.push(await runStep("source", "sync topics", dryRun, () => source.syncTopics(topics)));
3299
3302
  print(formatStep(steps[steps.length - 1]));
3300
3303
  }
3304
+ const teams = repo?.teams ?? [];
3305
+ if (teams.length > 0) if (source.syncTeams) {
3306
+ steps.push(await runStep("source", "sync teams", dryRun, () => source.syncTeams(teams)));
3307
+ print(formatStep(steps[steps.length - 1]));
3308
+ const repoCoord = input.context.repo ?? repo?.name ?? "";
3309
+ const org = repoCoord.includes("/") ? repoCoord.split("/")[0] : "";
3310
+ const writeableTeams = teams.map((t) => typeof t === "string" ? {
3311
+ slug: t,
3312
+ permission: "push"
3313
+ } : t).filter((t) => [
3314
+ "push",
3315
+ "maintain",
3316
+ "admin"
3317
+ ].includes(t.permission));
3318
+ if (org && writeableTeams.length > 0) {
3319
+ steps.push(await runStep("source", "write .github/CODEOWNERS", dryRun, async () => {
3320
+ const content = writeableTeams.map((t) => `* @${org}/${t.slug}`).join("\n") + "\n";
3321
+ await source.writeRepoFile(".github/CODEOWNERS", content);
3322
+ }));
3323
+ print(formatStep(steps[steps.length - 1]));
3324
+ }
3325
+ } else {
3326
+ steps.push({
3327
+ capability: "source",
3328
+ step: "sync teams",
3329
+ status: "skip",
3330
+ message: "provider does not implement syncTeams"
3331
+ });
3332
+ print(formatStep(steps[steps.length - 1]));
3333
+ }
3301
3334
  }
3302
3335
  if (loader.has("environments")) {
3303
3336
  const envs = loader.get("environments");
@@ -3382,6 +3415,23 @@ async function runSetup(input) {
3382
3415
  print(formatStep(steps[steps.length - 1]));
3383
3416
  }
3384
3417
  }
3418
+ if (config.skills && config.skills.length > 0 && config.agent) {
3419
+ print(style.step("skills"));
3420
+ if (!(config.agent in AGENT_SYMLINK_PATHS)) steps.push({
3421
+ capability: "skills",
3422
+ step: "install skills",
3423
+ status: "skip",
3424
+ message: `agent "${config.agent}" has no known skill install path`
3425
+ });
3426
+ else steps.push(await runStep("skills", "install skills", dryRun, async () => {
3427
+ return await installSkills({
3428
+ agent: config.agent,
3429
+ skills: config.skills,
3430
+ repoRoot: input.context.repoRoot
3431
+ });
3432
+ }));
3433
+ print(formatStep(steps[steps.length - 1]));
3434
+ }
3385
3435
  const summary = steps.reduce((acc, s) => {
3386
3436
  if (s.status === "ok") acc.ok += 1;
3387
3437
  else if (s.status === "fail") acc.fail += 1;
@@ -3419,6 +3469,94 @@ async function runSetup(input) {
3419
3469
  summary
3420
3470
  };
3421
3471
  }
3472
+ const AGENTS_SKILLS_ROOT = ".agents/skills";
3473
+ /** Relative path of the agent-specific symlink. undefined = unsupported agent. */
3474
+ const AGENT_SYMLINK_PATHS = { claude: (name) => `.claude/skills/${name}` };
3475
+ const GITIGNORE_BLOCK_START = "# managed by holocron setup — skills";
3476
+ const GITIGNORE_BLOCK_END = "# end managed by holocron setup — skills";
3477
+ async function installSkills({ agent, skills, repoRoot }) {
3478
+ const symlinkFn = AGENT_SYMLINK_PATHS[agent];
3479
+ if (!symlinkFn) return `agent "${agent}" has no known skill install path — skipping`;
3480
+ const require = createRequire(pathToFileURL(join(repoRoot, "package.json")));
3481
+ let skillsRoot;
3482
+ try {
3483
+ skillsRoot = dirname(require.resolve("@theholocron/skills/package.json"));
3484
+ } catch {
3485
+ throw new Error("@theholocron/skills not found — run: pnpm add -D @theholocron/skills");
3486
+ }
3487
+ const gitignorePath = join(repoRoot, ".gitignore");
3488
+ const existingContent = await readFile(gitignorePath, "utf8").catch(() => "");
3489
+ const previouslyInstalled = parsePreviousSkills(existingContent, symlinkFn);
3490
+ const currentSet = new Set(skills);
3491
+ const stale = previouslyInstalled.filter((n) => !currentSet.has(n));
3492
+ for (const name of stale) {
3493
+ await rm(join(repoRoot, symlinkFn(name)), { force: true }).catch(() => void 0);
3494
+ await rm(join(repoRoot, AGENTS_SKILLS_ROOT, name), {
3495
+ recursive: true,
3496
+ force: true
3497
+ }).catch(() => void 0);
3498
+ }
3499
+ const installed = [];
3500
+ const missing = [];
3501
+ for (const name of skills) {
3502
+ const srcDir = join(skillsRoot, "skills", name);
3503
+ try {
3504
+ await stat(srcDir);
3505
+ } catch {
3506
+ missing.push(name);
3507
+ continue;
3508
+ }
3509
+ const agentsDir = join(repoRoot, AGENTS_SKILLS_ROOT, name);
3510
+ await copyDirRecursive(srcDir, agentsDir);
3511
+ const symlinkPath = join(repoRoot, symlinkFn(name));
3512
+ await mkdir(dirname(symlinkPath), { recursive: true });
3513
+ try {
3514
+ await unlink(symlinkPath);
3515
+ } catch {}
3516
+ await symlink(relative(dirname(symlinkPath), agentsDir).replace(/\\/g, "/"), symlinkPath);
3517
+ installed.push(name);
3518
+ }
3519
+ if (installed.length > 0 || stale.length > 0 || missing.length > 0) await updateSkillsGitignore(gitignorePath, existingContent, [...installed, ...missing], symlinkFn);
3520
+ const parts = [`installed ${installed.length}`];
3521
+ if (stale.length > 0) parts.push(`pruned: ${stale.join(", ")}`);
3522
+ if (missing.length > 0) parts.push(`unknown: ${missing.join(", ")}`);
3523
+ return parts.join("; ");
3524
+ }
3525
+ /** Extract skill names from the previous gitignore block so stale dirs can be pruned. */
3526
+ function parsePreviousSkills(gitignoreContent, symlinkFn) {
3527
+ if (!gitignoreContent.includes(GITIGNORE_BLOCK_START)) return [];
3528
+ const startIdx = gitignoreContent.indexOf(GITIGNORE_BLOCK_START);
3529
+ const endIdx = gitignoreContent.indexOf(GITIGNORE_BLOCK_END, startIdx);
3530
+ const block = endIdx !== -1 ? gitignoreContent.slice(startIdx, endIdx) : gitignoreContent.slice(startIdx);
3531
+ const placeholder = "__placeholder__";
3532
+ const symlinkPrefix = `/${symlinkFn(placeholder)}`.replace(placeholder, "");
3533
+ return block.split("\n").filter((line) => line.startsWith(symlinkPrefix)).map((line) => line.slice(symlinkPrefix.length));
3534
+ }
3535
+ async function copyDirRecursive(src, dest) {
3536
+ await mkdir(dest, { recursive: true });
3537
+ const entries = await readdir(src, { withFileTypes: true });
3538
+ for (const entry of entries) {
3539
+ const srcPath = join(src, entry.name);
3540
+ const destPath = join(dest, entry.name);
3541
+ if (entry.isDirectory()) await copyDirRecursive(srcPath, destPath);
3542
+ else await copyFile(srcPath, destPath);
3543
+ }
3544
+ }
3545
+ async function updateSkillsGitignore(gitignorePath, existingContent, skills, symlinkFn) {
3546
+ const block = [
3547
+ GITIGNORE_BLOCK_START,
3548
+ ...[`/${AGENTS_SKILLS_ROOT}/`, ...skills.map((n) => `/${symlinkFn(n)}`)],
3549
+ GITIGNORE_BLOCK_END
3550
+ ].join("\n");
3551
+ let content;
3552
+ if (existingContent.includes(GITIGNORE_BLOCK_START)) {
3553
+ const start = existingContent.indexOf(GITIGNORE_BLOCK_START);
3554
+ const end = existingContent.indexOf(GITIGNORE_BLOCK_END, start);
3555
+ const afterBlock = end !== -1 ? existingContent.slice(end + 40) : "\n";
3556
+ content = existingContent.slice(0, start) + block + afterBlock;
3557
+ } else content = (existingContent.trimEnd() ? existingContent.trimEnd() + "\n\n" : "") + block + "\n";
3558
+ await writeFile(gitignorePath, content, "utf8");
3559
+ }
3422
3560
  async function runStep(capability, step, dryRun, body) {
3423
3561
  if (dryRun) return {
3424
3562
  capability,
@@ -3469,22 +3607,54 @@ function formatStep(step) {
3469
3607
  return ` ${style.dim(`· ${label}`)}`;
3470
3608
  }
3471
3609
  //#endregion
3610
+ //#region src/commands/skills.ts
3611
+ async function runSkillsInstall(input) {
3612
+ const print = input.print ?? ((line) => console.log(line));
3613
+ const config = input.loaded.resolved;
3614
+ if (!config.agent || !config.skills?.length) {
3615
+ print("Nothing to install — set `agent` and `skills` in holocron.config.ts");
3616
+ return;
3617
+ }
3618
+ if (input.context.dryRun) {
3619
+ print(`Would install ${config.skills.length} skill(s) for agent: ${config.agent}`);
3620
+ for (const name of config.skills) print(` → would install: ${name}`);
3621
+ return;
3622
+ }
3623
+ print(`Installing ${config.skills.length} skill(s) for agent: ${config.agent}`);
3624
+ try {
3625
+ print(` → ${await installSkills({
3626
+ agent: config.agent,
3627
+ skills: config.skills,
3628
+ repoRoot: input.context.repoRoot
3629
+ })}`);
3630
+ } catch (err) {
3631
+ print(` ✗ ${err instanceof Error ? err.message : String(err)}`);
3632
+ }
3633
+ }
3634
+ //#endregion
3472
3635
  //#region src/commands/sync.ts
3473
3636
  const SYNC_STEPS = [
3474
3637
  "labels",
3475
3638
  "properties",
3639
+ "teams",
3476
3640
  "topics",
3477
3641
  "keywords",
3478
3642
  "description"
3479
3643
  ];
3644
+ const LOCAL_STEPS = /* @__PURE__ */ new Set(["keywords", "description"]);
3480
3645
  async function runSync(input) {
3481
3646
  const print = input.print ?? ((line) => console.log(line));
3482
3647
  const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
3483
- await loader.load();
3484
3648
  const config = input.loaded.resolved;
3485
3649
  const dryRun = input.context.dryRun ?? false;
3486
3650
  const requestedSteps = input.steps;
3487
3651
  const steps = [];
3652
+ if (!requestedSteps || requestedSteps.some((s) => !LOCAL_STEPS.has(s))) await loader.load();
3653
+ else try {
3654
+ await loader.load();
3655
+ } catch (err) {
3656
+ if (!(err instanceof AuthError)) throw err;
3657
+ }
3488
3658
  print(`Holocron sync — ${config.name}${dryRun ? " (dry-run)" : ""}`);
3489
3659
  print(` config: ${input.loaded.filepath}`);
3490
3660
  print("");
@@ -3493,6 +3663,7 @@ async function runSync(input) {
3493
3663
  print(" → source");
3494
3664
  for (const stepName of SYNC_STEPS) {
3495
3665
  if (requestedSteps !== void 0 && !requestedSteps.includes(stepName)) continue;
3666
+ if (LOCAL_STEPS.has(stepName)) continue;
3496
3667
  if (stepName === "labels") if (source.syncLabels) {
3497
3668
  steps.push(await runSyncStep("source", "sync labels", dryRun, () => source.syncLabels(CANONICAL_LABELS, STALE_LABELS)));
3498
3669
  print(formatSyncStep(steps[steps.length - 1]));
@@ -3528,68 +3699,67 @@ async function runSync(input) {
3528
3699
  });
3529
3700
  print(formatSyncStep(steps[steps.length - 1]));
3530
3701
  }
3531
- if (stepName === "topics") {
3532
- const topics = config.repo?.topics ?? [];
3533
- if (topics.length === 0) {
3702
+ if (stepName === "teams") {
3703
+ const teams = config.repo?.teams ?? [];
3704
+ if (teams.length === 0) {
3534
3705
  steps.push({
3535
3706
  capability: "source",
3536
- step: "sync topics",
3707
+ step: "sync teams",
3537
3708
  status: "skip",
3538
- message: "no topics configured"
3709
+ message: "no teams configured"
3539
3710
  });
3540
3711
  print(formatSyncStep(steps[steps.length - 1]));
3541
- } else if (source.syncTopics) {
3542
- steps.push(await runSyncStep("source", "sync topics", dryRun, () => source.syncTopics(topics)));
3712
+ } else if (source.syncTeams) {
3713
+ steps.push(await runSyncStep("source", "sync teams", dryRun, () => source.syncTeams(teams)));
3543
3714
  print(formatSyncStep(steps[steps.length - 1]));
3715
+ const repoCoord = input.context.repo ?? config.repo?.name ?? "";
3716
+ const org = repoCoord.includes("/") ? repoCoord.split("/")[0] : "";
3717
+ const writeableTeams = teams.map((t) => typeof t === "string" ? {
3718
+ slug: t,
3719
+ permission: "push"
3720
+ } : t).filter((t) => [
3721
+ "push",
3722
+ "maintain",
3723
+ "admin"
3724
+ ].includes(t.permission));
3725
+ if (org && writeableTeams.length > 0) {
3726
+ steps.push(await runSyncStep("source", "write .github/CODEOWNERS", dryRun, async () => {
3727
+ const content = writeableTeams.map((t) => `* @${org}/${t.slug}`).join("\n") + "\n";
3728
+ await source.writeRepoFile(".github/CODEOWNERS", content);
3729
+ }));
3730
+ print(formatSyncStep(steps[steps.length - 1]));
3731
+ }
3544
3732
  } else {
3545
3733
  steps.push({
3546
3734
  capability: "source",
3547
- step: "sync topics",
3735
+ step: "sync teams",
3548
3736
  status: "skip",
3549
- message: "provider does not implement syncTopics"
3737
+ message: "provider does not implement syncTeams"
3550
3738
  });
3551
3739
  print(formatSyncStep(steps[steps.length - 1]));
3552
3740
  }
3553
3741
  }
3554
- if (stepName === "keywords") {
3742
+ if (stepName === "topics") {
3555
3743
  const topics = config.repo?.topics ?? [];
3556
3744
  if (topics.length === 0) {
3557
3745
  steps.push({
3558
3746
  capability: "source",
3559
- step: "sync keywords",
3747
+ step: "sync topics",
3560
3748
  status: "skip",
3561
3749
  message: "no topics configured"
3562
3750
  });
3563
3751
  print(formatSyncStep(steps[steps.length - 1]));
3564
- } else {
3565
- steps.push(await runSyncStep("source", "sync keywords", dryRun, async () => {
3566
- return await writePackageJsonField(input.context.repoRoot, "keywords", topics) ? `${topics.length} keywords written` : `${topics.length} topics (no package.json)`;
3567
- }));
3752
+ } else if (source.syncTopics) {
3753
+ steps.push(await runSyncStep("source", "sync topics", dryRun, () => source.syncTopics(topics)));
3568
3754
  print(formatSyncStep(steps[steps.length - 1]));
3569
- }
3570
- }
3571
- if (stepName === "description") {
3572
- const description = config.description;
3573
- if (!description) {
3755
+ } else {
3574
3756
  steps.push({
3575
3757
  capability: "source",
3576
- step: "sync description",
3758
+ step: "sync topics",
3577
3759
  status: "skip",
3578
- message: "no description configured"
3760
+ message: "provider does not implement syncTopics"
3579
3761
  });
3580
3762
  print(formatSyncStep(steps[steps.length - 1]));
3581
- } else {
3582
- steps.push(await runSyncStep("source", "sync description", dryRun, async () => {
3583
- const pkgWrote = await writePackageJsonField(input.context.repoRoot, "description", description);
3584
- const readmeWrote = await updateReadmeDescription(input.context.repoRoot, description);
3585
- if (source.syncDescription) await source.syncDescription(description);
3586
- const parts = [];
3587
- if (pkgWrote) parts.push("package.json");
3588
- if (readmeWrote) parts.push("README.md");
3589
- if (source.syncDescription) parts.push("GitHub");
3590
- return parts.length > 0 ? parts.join(", ") + " updated" : "description synced";
3591
- }));
3592
- print(formatSyncStep(steps[steps.length - 1]));
3593
3763
  }
3594
3764
  }
3595
3765
  }
@@ -3605,6 +3775,51 @@ async function runSync(input) {
3605
3775
  }
3606
3776
  }
3607
3777
  }
3778
+ for (const stepName of ["keywords", "description"]) {
3779
+ if (requestedSteps !== void 0 && !requestedSteps.includes(stepName)) continue;
3780
+ if (stepName === "keywords") {
3781
+ const topics = config.repo?.topics ?? [];
3782
+ if (topics.length === 0) {
3783
+ steps.push({
3784
+ capability: "local",
3785
+ step: "sync keywords",
3786
+ status: "skip",
3787
+ message: "no topics configured"
3788
+ });
3789
+ print(formatSyncStep(steps[steps.length - 1]));
3790
+ } else {
3791
+ steps.push(await runSyncStep("local", "sync keywords", dryRun, async () => {
3792
+ return await writePackageJsonField(input.context.repoRoot, "keywords", topics) ? `${topics.length} keywords written` : `${topics.length} topics (no package.json)`;
3793
+ }));
3794
+ print(formatSyncStep(steps[steps.length - 1]));
3795
+ }
3796
+ }
3797
+ if (stepName === "description") {
3798
+ const description = config.description;
3799
+ if (!description) {
3800
+ steps.push({
3801
+ capability: "local",
3802
+ step: "sync description",
3803
+ status: "skip",
3804
+ message: "no description configured"
3805
+ });
3806
+ print(formatSyncStep(steps[steps.length - 1]));
3807
+ } else {
3808
+ const source = loader.has("source") ? loader.get("source") : null;
3809
+ steps.push(await runSyncStep("local", "sync description", dryRun, async () => {
3810
+ const pkgWrote = await writePackageJsonField(input.context.repoRoot, "description", description);
3811
+ const readmeWrote = await updateReadmeDescription(input.context.repoRoot, description);
3812
+ if (source?.syncDescription) await source.syncDescription(description);
3813
+ const parts = [];
3814
+ if (pkgWrote) parts.push("package.json");
3815
+ if (readmeWrote) parts.push("README.md");
3816
+ if (source?.syncDescription) parts.push("GitHub");
3817
+ return parts.length > 0 ? parts.join(", ") + " updated" : "description synced";
3818
+ }));
3819
+ print(formatSyncStep(steps[steps.length - 1]));
3820
+ }
3821
+ }
3822
+ }
3608
3823
  const summary = steps.reduce((acc, s) => {
3609
3824
  if (s.status === "ok") acc.ok += 1;
3610
3825
  else if (s.status === "fail") acc.fail += 1;
@@ -3851,6 +4066,18 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
3851
4066
  ...argv.token ? { cliToken: argv.token } : {}
3852
4067
  }
3853
4068
  })).summary.fail > 0) process.exitCode = 1;
4069
+ }).command("skills <action>", "Manage agent skills from the @theholocron/skills registry", (y) => y.positional("action", {
4070
+ type: "string",
4071
+ choices: ["install"],
4072
+ describe: "install — copy skills from @theholocron/skills into .agents/ with agent symlinks"
4073
+ }), async (argv) => {
4074
+ if (argv.action === "install") await runSkillsInstall({
4075
+ loaded: await loadConfig(argv.cwd),
4076
+ context: {
4077
+ repoRoot: argv.cwd,
4078
+ dryRun: argv.dryRun
4079
+ }
4080
+ });
3854
4081
  }).command("secret set <name> [value]", "Set a single secret via the configured `secrets` capability", (y) => y.positional("name", {
3855
4082
  type: "string",
3856
4083
  demandOption: true,
@@ -3956,10 +4183,10 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
3956
4183
  dryRun: argv.dryRun,
3957
4184
  ...argv.otp ? { otp: argv.otp } : {}
3958
4185
  })).status === "fail") process.exitCode = 1;
3959
- }).demandCommand(1, "Run `holocron npm --help` to see available npm subcommands."), () => {}).command("sync [steps..]", "Sync source-level state (labels, properties, topics) from config to the provider", (y) => y.positional("steps", {
4186
+ }).demandCommand(1, "Run `holocron npm --help` to see available npm subcommands."), () => {}).command("sync [steps..]", "Sync state from config to the provider and local files (labels, properties, topics, keywords, description)", (y) => y.positional("steps", {
3960
4187
  type: "string",
3961
4188
  array: true,
3962
- describe: "Steps to run: labels, properties, topics (default: all)"
4189
+ describe: "Steps to run: labels, properties, topics, keywords, description (default: all)"
3963
4190
  }).option("repo", {
3964
4191
  type: "string",
3965
4192
  describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
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, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, isMulti } from "./capabilities/index.mjs";
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
3
 
4
4
  //#region src/auth-resolver.d.ts
@@ -47,6 +47,12 @@ interface RepoConfig {
47
47
  protection?: RepoProtection;
48
48
  /** CI check context names required on the default branch (only used when `protection` is "strict"). */
49
49
  requiredChecks?: string[];
50
+ /**
51
+ * GitHub teams granted repository access. Synced by `holocron setup`, which
52
+ * also writes `.github/CODEOWNERS` for teams with write-or-higher permission.
53
+ * String shorthand defaults to `push` (Write).
54
+ */
55
+ teams?: TeamEntry[];
50
56
  /** GitHub topics set on the repository. */
51
57
  topics?: string[];
52
58
  /** GitHub custom properties synced to the org dashboard. */
@@ -93,6 +99,22 @@ interface HolocronConfig {
93
99
  providers: RawProvidersConfig;
94
100
  apps?: AppConfig[];
95
101
  doctor?: DoctorConfig;
102
+ /**
103
+ * Agent runtime that determines where skills are installed by `holocron setup`.
104
+ * Skills are installed to `.agents/skills/<name>/` (canonical) with a
105
+ * relative symlink at the agent-specific path:
106
+ * - `"claude"` → `.claude/skills/<name>` → `../../.agents/skills/<name>`
107
+ * - `"codex"` | `"gemini"` → logged as unsupported; skipped gracefully.
108
+ */
109
+ agent?: "claude" | "codex" | "gemini";
110
+ /**
111
+ * Skill names from `@theholocron/skills` to install during `holocron setup`.
112
+ * Installed paths are gitignored and managed by setup — do not commit them.
113
+ *
114
+ * @example
115
+ * ["git-safety", "pr-workflow", "commit-standards"]
116
+ */
117
+ skills?: string[];
96
118
  }
97
119
  interface ResolvedTuple {
98
120
  provider: string;
@@ -119,6 +141,8 @@ interface ResolvedHolocronConfig {
119
141
  providers: ResolvedProvidersConfig;
120
142
  apps: AppConfig[];
121
143
  doctor: DoctorConfig;
144
+ agent?: "claude" | "codex" | "gemini";
145
+ skills?: string[];
122
146
  }
123
147
  declare class ConfigError extends Error {
124
148
  name: string;
@@ -195,4 +219,4 @@ interface LoadedConfig {
195
219
  */
196
220
  declare function loadConfig(cwd: string): Promise<LoadedConfig>;
197
221
  //#endregion
198
- export { Analytics, AppConfig, Auth, AuthDescription, AuthError, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityConfigPackage, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConfigError, ConfigFileError, ConnectionStringOptions, CreateAuthUserInput, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, DoctorConfig, EnsureResult, Environment, EnvironmentReviewer, Environments, HolocronConfig, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, LoadedConfig, MultiEntry, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, ProviderOptions, REQUIRED_CAPABILITIES, RawProviderEntry, RawProvidersConfig, RepoConfig, RepoProperties, RepoProtection, RepoRef, RepoSettings, type RequestOptions, ResolveTokenConfig, type ResolveTokenInput, ResolvedCapability, ResolvedHolocronConfig, ResolvedProviderEntry, ResolvedProvidersConfig, ResolvedTuple, type RestClient, type RestClientConfig, Ruleset, SecretScope, Secrets, SingleEntry, Source, StatusCategory, Storage, StorageBranch, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, createResolveToken, createRestClient, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
222
+ export { Analytics, AppConfig, Auth, AuthDescription, AuthError, AuthEvent, AuthEventType, AuthIdentity, AuthUser, CARDINALITY, CapabilityConfigPackage, CapabilityImpls, CapabilityKey, Cardinality, CardinalityFor, Ci, CiRun, CiRunFilter, CiRunStatus, ConfigError, ConfigFileError, ConnectionStringOptions, CreateAuthUserInput, Deployment, DeploymentProject, DeploymentProjectSettings, DeploymentRecord, DeploymentTarget, DeploymentTrigger, Dns, DnsRecord, DnsRecordType, DoctorConfig, EnsureResult, Environment, EnvironmentReviewer, Environments, HolocronConfig, Issue, IssueSearchFilter, Issues, LabelDef, LifecycleResult, LifecycleSlot, LoadedConfig, MultiEntry, NormalizedAuthUser, Notifications, Observability, ParseWebhookInput, ProviderApiError, ProviderIdentity, ProviderOptions, REQUIRED_CAPABILITIES, RawProviderEntry, RawProvidersConfig, RepoConfig, RepoProperties, RepoProtection, RepoRef, RepoSettings, type RequestOptions, ResolveTokenConfig, type ResolveTokenInput, ResolvedCapability, ResolvedHolocronConfig, ResolvedProviderEntry, ResolvedProvidersConfig, ResolvedTuple, type RestClient, type RestClientConfig, Ruleset, SecretScope, Secrets, SingleEntry, Source, StatusCategory, Storage, StorageBranch, TeamEntry, TeamPermission, Tooling, ToolingDoctorReport, TrackerDoctorReport, TrackerUser, Vault, WebhookDashboardInfo, WebhookVerificationError, createResolveToken, createRestClient, defineConfig, deleteToken, getToken, isMulti, listStoredProviders, loadConfig, resolveConfig, resolveEntry, resolvePluginPackage, setToken };
package/dist/index.mjs CHANGED
@@ -196,7 +196,9 @@ function resolveConfig(raw) {
196
196
  workflows: raw.workflows,
197
197
  providers,
198
198
  apps: raw.apps ?? [],
199
- doctor: raw.doctor ?? {}
199
+ doctor: raw.doctor ?? {},
200
+ agent: raw.agent,
201
+ skills: raw.skills
200
202
  };
201
203
  }
202
204
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theholocron/cli",
3
- "version": "2.0.0-alpha.67",
3
+ "version": "2.0.0-alpha.69",
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",
@@ -34,8 +34,8 @@
34
34
  ],
35
35
  "dependencies": {
36
36
  "@napi-rs/keyring": "^1.3.0",
37
- "@theholocron/github-client": "^0.11.3",
38
- "@theholocron/http-client": "^0.11.3",
37
+ "@theholocron/github-client": "^1.1.0",
38
+ "@theholocron/http-client": "^1.1.0",
39
39
  "chalk": "^5.4.1",
40
40
  "ora": "^8.2.0",
41
41
  "tsx": "^4.22.4",