@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.
- package/dist/capabilities/index.d.mts +4 -3
- package/dist/cli.mjs +118 -11
- package/dist/index.d.mts +0 -1
- package/package.json +1 -1
|
@@ -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[];
|
|
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;
|
|
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
|
-
|
|
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
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
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
|
-
...
|
|
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.
|
|
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",
|