@theholocron/cli 2.1.1 → 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/cli.mjs +106 -3
- package/package.json +1 -1
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";
|
|
@@ -4513,6 +4594,28 @@ await yargs(hideBin(process.argv)).scriptName("holocron").usage("$0 <command> [o
|
|
|
4513
4594
|
describe: "Directory to search for holocron.config.json"
|
|
4514
4595
|
}).command("version", "Print the CLI version", () => {}, () => {
|
|
4515
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;
|
|
4516
4619
|
}).command("doctor", "Load the config and run a smoke check against every provider", (y) => y.option("repo", {
|
|
4517
4620
|
type: "string",
|
|
4518
4621
|
describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
|
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",
|