@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 +25 -0
- package/dist/capabilities/index.d.mts +12 -1
- package/dist/cli.mjs +268 -41
- package/dist/index.d.mts +26 -2
- package/dist/index.mjs +3 -1
- package/package.json +3 -3
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 === "
|
|
3532
|
-
const
|
|
3533
|
-
if (
|
|
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
|
|
3707
|
+
step: "sync teams",
|
|
3537
3708
|
status: "skip",
|
|
3538
|
-
message: "no
|
|
3709
|
+
message: "no teams configured"
|
|
3539
3710
|
});
|
|
3540
3711
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
3541
|
-
} else if (source.
|
|
3542
|
-
steps.push(await runSyncStep("source", "sync
|
|
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
|
|
3735
|
+
step: "sync teams",
|
|
3548
3736
|
status: "skip",
|
|
3549
|
-
message: "provider does not implement
|
|
3737
|
+
message: "provider does not implement syncTeams"
|
|
3550
3738
|
});
|
|
3551
3739
|
print(formatSyncStep(steps[steps.length - 1]));
|
|
3552
3740
|
}
|
|
3553
3741
|
}
|
|
3554
|
-
if (stepName === "
|
|
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
|
|
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
|
|
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
|
|
3758
|
+
step: "sync topics",
|
|
3577
3759
|
status: "skip",
|
|
3578
|
-
message: "
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theholocron/cli",
|
|
3
|
-
"version": "2.0.0-alpha.
|
|
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": "^
|
|
38
|
-
"@theholocron/http-client": "^
|
|
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",
|