@theholocron/cli 4.17.1 → 4.18.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/README.md +25 -0
- package/dist/cli.mjs +394 -125
- package/dist/cli.mjs.map +1 -1
- package/package.json +5 -4
package/dist/cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path, { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import { checkbox, input, select } from "@inquirer/prompts";
|
|
6
|
-
import { KNOWN_WORKFLOWS, createAstromech, extractPreviewConfig, reusableTemplates } from "@theholocron/astromech";
|
|
6
|
+
import { KNOWN_WORKFLOWS, createAstromech, extractPreviewConfig, readWorkspacePackages, reusableTemplates } from "@theholocron/astromech";
|
|
7
7
|
import { loadTasksConfig } from "@theholocron/astromech/config";
|
|
8
8
|
import yargs from "yargs";
|
|
9
9
|
import { hideBin } from "yargs/helpers";
|
|
@@ -14,7 +14,7 @@ import { pathToFileURL } from "node:url";
|
|
|
14
14
|
import { createLogger, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/observability/logger";
|
|
15
15
|
import ora from "ora";
|
|
16
16
|
import chalk from "chalk";
|
|
17
|
-
import { execFile, execFileSync, spawnSync } from "node:child_process";
|
|
17
|
+
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
18
18
|
import { homedir, hostname, userInfo } from "node:os";
|
|
19
19
|
import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
20
20
|
import { createHash } from "node:crypto";
|
|
@@ -26,6 +26,7 @@ import { NoopAnalyticsSink, NoopErrorSink, redactObject } from "@theholocron/obs
|
|
|
26
26
|
import { SentrySink } from "@theholocron/observability/errors";
|
|
27
27
|
import { promisify } from "node:util";
|
|
28
28
|
import { ConfigFileError, loadConfigFile } from "@theholocron/datapad";
|
|
29
|
+
import search from "@inquirer/search";
|
|
29
30
|
//#region src/env.ts
|
|
30
31
|
/** Singleton env for simple global lookups throughout the CLI. */
|
|
31
32
|
const env = createEnvLookup();
|
|
@@ -3570,86 +3571,6 @@ function createIgnoreConfig() {
|
|
|
3570
3571
|
return `${workflowHeader$6()}${alexignore_default}`;
|
|
3571
3572
|
}
|
|
3572
3573
|
//#endregion
|
|
3573
|
-
//#region src/templates/configs/codecov/codecov.yml
|
|
3574
|
-
var codecov_default = "codecov:\n require_ci_to_pass: true\n\ncoverage:\n precision: 2\n round: down\n status:\n project:\n default:\n target: auto\n threshold: 2%\n # Infra-only PRs (dep bumps, config, lockfile) touch no coverable code,\n # so codecov produces no report to compare. Pass instead of blocking\n # forever on a required check that will never post.\n if_not_found: success\n patch:\n default:\n target: 80%\n if_not_found: success\n\ncomment:\n layout: \"reach,diff,flags,components\"\n behavior: default\n require_changes: true\n\ncomponent_management:\n default_rules:\n statuses:\n - type: patch\n target: 80%\n if_not_found: success\n individual_components:\n";
|
|
3575
|
-
//#endregion
|
|
3576
|
-
//#region src/templates/configs/codecov/utils.ts
|
|
3577
|
-
const INDIVIDUAL_COMPONENTS_MARKER = " individual_components:";
|
|
3578
|
-
function codecovComponentBlock(packages) {
|
|
3579
|
-
if (packages.length === 0) return "\n []\n";
|
|
3580
|
-
return "\n" + packages.flatMap(({ slug }) => [
|
|
3581
|
-
` - component_id: ${slug}`,
|
|
3582
|
-
` name: "${slug}"`,
|
|
3583
|
-
` paths:`,
|
|
3584
|
-
` - packages/${slug}/**`,
|
|
3585
|
-
``
|
|
3586
|
-
]).join("\n");
|
|
3587
|
-
}
|
|
3588
|
-
/**
|
|
3589
|
-
* Ensure every codecov status default carries `if_not_found: success`, so an
|
|
3590
|
-
* infra-only PR (dep bump, config, lockfile — no coverable change) passes
|
|
3591
|
-
* instead of hanging forever on a required check codecov never posts.
|
|
3592
|
-
*
|
|
3593
|
-
* Injects the key on the line after each `target:` that sits inside a status
|
|
3594
|
-
* block (`coverage.status.*.default` or a `statuses:` list item — indent ≥ 8,
|
|
3595
|
-
* or ≥ 8 for a `- type:` list entry), when the immediate sibling lines do not
|
|
3596
|
-
* already declare it.
|
|
3597
|
-
*/
|
|
3598
|
-
function ensureIfNotFound(yaml) {
|
|
3599
|
-
const lines = yaml.split("\n");
|
|
3600
|
-
const out = [];
|
|
3601
|
-
for (let i = 0; i < lines.length; i++) {
|
|
3602
|
-
const line = lines[i];
|
|
3603
|
-
out.push(line);
|
|
3604
|
-
const m = /^(\s{8,})target:\s+\S+\s*$/.exec(line);
|
|
3605
|
-
if (!m) continue;
|
|
3606
|
-
const indent = m[1];
|
|
3607
|
-
let alreadySet = false;
|
|
3608
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
3609
|
-
const sib = lines[j];
|
|
3610
|
-
if (sib.trim() === "") continue;
|
|
3611
|
-
const sibIndent = sib.match(/^\s*/)[0].length;
|
|
3612
|
-
if (sibIndent < indent.length) break;
|
|
3613
|
-
if (sibIndent === indent.length && /^\s*if_not_found\s*:/.test(sib)) {
|
|
3614
|
-
alreadySet = true;
|
|
3615
|
-
break;
|
|
3616
|
-
}
|
|
3617
|
-
if (sibIndent === indent.length && /^\s*(- |\w)/.test(sib) && !/^\s*(threshold|flags|paths|branches)\s*:/.test(sib)) break;
|
|
3618
|
-
}
|
|
3619
|
-
if (!alreadySet) out.push(`${indent}if_not_found: success`);
|
|
3620
|
-
}
|
|
3621
|
-
return out.join("\n");
|
|
3622
|
-
}
|
|
3623
|
-
function mergeCodecovComponents(existing, packages) {
|
|
3624
|
-
const idx = existing.indexOf(INDIVIDUAL_COMPONENTS_MARKER);
|
|
3625
|
-
if (idx === -1) return ensureIfNotFound(existing);
|
|
3626
|
-
return ensureIfNotFound(existing.slice(0, idx + 24)) + codecovComponentBlock(packages);
|
|
3627
|
-
}
|
|
3628
|
-
async function readWorkspacePackages(repoRoot) {
|
|
3629
|
-
const packagesDir = join(repoRoot, "packages");
|
|
3630
|
-
const entries = await readdir(packagesDir, { withFileTypes: true }).catch(() => null);
|
|
3631
|
-
if (!entries) return [];
|
|
3632
|
-
const packages = [];
|
|
3633
|
-
for (const entry of entries) {
|
|
3634
|
-
if (!entry.isDirectory()) continue;
|
|
3635
|
-
try {
|
|
3636
|
-
const raw = await readFile(join(packagesDir, entry.name, "package.json"), "utf8");
|
|
3637
|
-
const pkg = JSON.parse(raw);
|
|
3638
|
-
if (typeof pkg.name === "string") packages.push({
|
|
3639
|
-
slug: entry.name,
|
|
3640
|
-
name: pkg.name
|
|
3641
|
-
});
|
|
3642
|
-
} catch {}
|
|
3643
|
-
}
|
|
3644
|
-
return packages.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
3645
|
-
}
|
|
3646
|
-
//#endregion
|
|
3647
|
-
//#region src/templates/configs/codecov/create-config.ts
|
|
3648
|
-
const { scaffoldHeader } = createHeader({ source: "packages/cli/src/templates/configs/codecov/create-config.ts" });
|
|
3649
|
-
function createConfig$5(packages) {
|
|
3650
|
-
return `${scaffoldHeader()}${codecov_default.trimEnd()}${codecovComponentBlock(packages)}`;
|
|
3651
|
-
}
|
|
3652
|
-
//#endregion
|
|
3653
3574
|
//#region src/templates/configs/devmoji/create-config.ts
|
|
3654
3575
|
const { workflowHeader: workflowHeader$5 } = createHeader({ source: "packages/cli/src/templates/configs/devmoji/create-config.ts" });
|
|
3655
3576
|
function createConfig$4() {
|
|
@@ -4720,7 +4641,6 @@ async function runSetup(input) {
|
|
|
4720
4641
|
}
|
|
4721
4642
|
{
|
|
4722
4643
|
const hasTestWorkflow = (config.tasks ?? []).map((e) => typeof e === "string" ? e : e.name).includes("test");
|
|
4723
|
-
const packages = await readWorkspacePackages(input.context.repoRoot);
|
|
4724
4644
|
const existing = await readFile(join(input.context.repoRoot, "codecov.yml"), "utf8").catch(() => null);
|
|
4725
4645
|
if (!hasTestWorkflow && existing == null) steps.push({
|
|
4726
4646
|
capability: "source",
|
|
@@ -4729,9 +4649,10 @@ async function runSetup(input) {
|
|
|
4729
4649
|
message: "no test workflow configured"
|
|
4730
4650
|
});
|
|
4731
4651
|
else steps.push(await runStep("source", "write codecov.yml", dryRun, async () => {
|
|
4732
|
-
const content =
|
|
4652
|
+
const content = createAstromech({ cwd: input.context.repoRoot }).codecovConfig(existing);
|
|
4733
4653
|
await source.writeRepoFile("codecov.yml", content);
|
|
4734
|
-
|
|
4654
|
+
const packageCount = readWorkspacePackages(input.context.repoRoot).length;
|
|
4655
|
+
return packageCount > 0 ? `${packageCount} components` : "no components";
|
|
4735
4656
|
}));
|
|
4736
4657
|
print(formatStep(steps[steps.length - 1]));
|
|
4737
4658
|
}
|
|
@@ -6958,6 +6879,350 @@ function parseGitRemoteUrl(url) {
|
|
|
6958
6879
|
if (sshMatch) return sshMatch[1];
|
|
6959
6880
|
}
|
|
6960
6881
|
//#endregion
|
|
6882
|
+
//#region src/interactive-menu.ts
|
|
6883
|
+
/**
|
|
6884
|
+
* The interactive fallback — replaces every "run --help to see commands"
|
|
6885
|
+
* dead end with a prompt. Three layers, all driven by {@link COMMAND_REGISTRY}:
|
|
6886
|
+
*
|
|
6887
|
+
* 1. `holocron` with no command → searchable picker over every command
|
|
6888
|
+
* 2. a parent command with no sub → `select()` over that group's commands
|
|
6889
|
+
* 3. a leaf command missing a → `input()` / `select()` per positional
|
|
6890
|
+
* required positional
|
|
6891
|
+
*
|
|
6892
|
+
* Layers 1–2 are driven from `cli.ts`'s `$0` handlers via {@link launchMenu}:
|
|
6893
|
+
* pick a command, prompt for its required positionals, then **spawn a fresh
|
|
6894
|
+
* `holocron <command> …` child process** (`stdio: "inherit"`) rather than
|
|
6895
|
+
* re-entering yargs' own parse pipeline — see the spec's "spawn vs re-parse"
|
|
6896
|
+
* section (theholocron/holocron#438) for why. Layer 3 is driven directly from
|
|
6897
|
+
* each leaf command's own handler in `cli.ts`, calling
|
|
6898
|
+
* {@link promptForPositionals} for just that one command — no spawn needed,
|
|
6899
|
+
* the handler already has everything else it needs.
|
|
6900
|
+
*
|
|
6901
|
+
* Every prompt is preceded by a `process.stdin.isTTY` check. A non-TTY run
|
|
6902
|
+
* (CI, a script, a pipe) throws {@link NonInteractiveError} instead of
|
|
6903
|
+
* hanging on `@inquirer/prompts` (which otherwise throws its own
|
|
6904
|
+
* `ExitPromptError` the moment stdin closes) — `cli.ts`'s `USER_FACING_ERRORS`
|
|
6905
|
+
* catch prints its message cleanly, restoring the old demandCommand-style
|
|
6906
|
+
* hard failure for non-interactive callers.
|
|
6907
|
+
*/
|
|
6908
|
+
/** Raised when a prompt is needed but stdin isn't a TTY — see the module doc. */
|
|
6909
|
+
var NonInteractiveError = class extends Error {
|
|
6910
|
+
name = "NonInteractiveError";
|
|
6911
|
+
};
|
|
6912
|
+
const numeric = (value) => /^\d+$/.test(value) ? true : "enter a number";
|
|
6913
|
+
/**
|
|
6914
|
+
* The full command surface, hand-maintained in parallel with `cli.ts`'s
|
|
6915
|
+
* yargs registrations (this module does not introspect yargs at runtime).
|
|
6916
|
+
* `run <task>` is deliberately excluded — it's CI/scripting-oriented, and an
|
|
6917
|
+
* interactive prompt in front of it would work against that.
|
|
6918
|
+
*/
|
|
6919
|
+
const COMMAND_REGISTRY = [
|
|
6920
|
+
{
|
|
6921
|
+
name: "version",
|
|
6922
|
+
description: "Print the CLI version",
|
|
6923
|
+
positionals: []
|
|
6924
|
+
},
|
|
6925
|
+
{
|
|
6926
|
+
name: "clone",
|
|
6927
|
+
description: "Clone all repos in a GitHub org as siblings under a single directory",
|
|
6928
|
+
positionals: []
|
|
6929
|
+
},
|
|
6930
|
+
{
|
|
6931
|
+
name: "doctor",
|
|
6932
|
+
description: "Load the config and run a smoke check against every provider",
|
|
6933
|
+
positionals: []
|
|
6934
|
+
},
|
|
6935
|
+
{
|
|
6936
|
+
name: "setup",
|
|
6937
|
+
description: "Apply infra setup actions across every configured capability",
|
|
6938
|
+
positionals: []
|
|
6939
|
+
},
|
|
6940
|
+
{
|
|
6941
|
+
name: "secret set",
|
|
6942
|
+
description: "Set a single secret via the configured `secrets` capability",
|
|
6943
|
+
positionals: [{
|
|
6944
|
+
key: "name",
|
|
6945
|
+
message: "Secret name:",
|
|
6946
|
+
type: "input"
|
|
6947
|
+
}]
|
|
6948
|
+
},
|
|
6949
|
+
{
|
|
6950
|
+
name: "secrets sync",
|
|
6951
|
+
description: "Read a vault environment + fan KEY=VALUEs out to secrets + deployment env vars",
|
|
6952
|
+
positionals: [{
|
|
6953
|
+
key: "environmentId",
|
|
6954
|
+
message: "Vault environment id:",
|
|
6955
|
+
type: "input"
|
|
6956
|
+
}]
|
|
6957
|
+
},
|
|
6958
|
+
{
|
|
6959
|
+
name: "deploy",
|
|
6960
|
+
description: "Trigger a deployment via the configured `deployment` capability",
|
|
6961
|
+
positionals: [{
|
|
6962
|
+
key: "branch",
|
|
6963
|
+
message: "Branch to deploy:",
|
|
6964
|
+
type: "input"
|
|
6965
|
+
}]
|
|
6966
|
+
},
|
|
6967
|
+
{
|
|
6968
|
+
name: "cleanup-preview",
|
|
6969
|
+
description: "List and delete Cloudflare Pages preview deployments for a GitHub PR",
|
|
6970
|
+
positionals: [{
|
|
6971
|
+
key: "pr",
|
|
6972
|
+
message: "PR number:",
|
|
6973
|
+
type: "input",
|
|
6974
|
+
validate: numeric
|
|
6975
|
+
}]
|
|
6976
|
+
},
|
|
6977
|
+
{
|
|
6978
|
+
name: "bump-versions",
|
|
6979
|
+
description: "Bump all non-private package versions in lockstep (semantic-release prepareCmd)",
|
|
6980
|
+
positionals: [{
|
|
6981
|
+
key: "newVersion",
|
|
6982
|
+
cliArg: "new-version",
|
|
6983
|
+
message: "New version:",
|
|
6984
|
+
type: "input"
|
|
6985
|
+
}]
|
|
6986
|
+
},
|
|
6987
|
+
{
|
|
6988
|
+
name: "publish",
|
|
6989
|
+
description: "Publish @theholocron/* packages to npm",
|
|
6990
|
+
positionals: []
|
|
6991
|
+
},
|
|
6992
|
+
{
|
|
6993
|
+
name: "sync",
|
|
6994
|
+
description: "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)",
|
|
6995
|
+
positionals: []
|
|
6996
|
+
},
|
|
6997
|
+
{
|
|
6998
|
+
name: "ci",
|
|
6999
|
+
description: "Run the merge-gating checks locally, in CI order — 'will CI pass?'",
|
|
7000
|
+
positionals: []
|
|
7001
|
+
},
|
|
7002
|
+
{
|
|
7003
|
+
name: "sync-github",
|
|
7004
|
+
description: "Sync workflow templates and composite actions to theholocron/.github",
|
|
7005
|
+
positionals: []
|
|
7006
|
+
},
|
|
7007
|
+
{
|
|
7008
|
+
name: "sync-readme",
|
|
7009
|
+
description: "Sync the Installation + Usage block in README.md from package.json",
|
|
7010
|
+
positionals: []
|
|
7011
|
+
},
|
|
7012
|
+
{
|
|
7013
|
+
name: "config show",
|
|
7014
|
+
description: "Print the resolved holocron config",
|
|
7015
|
+
positionals: []
|
|
7016
|
+
},
|
|
7017
|
+
{
|
|
7018
|
+
name: "new",
|
|
7019
|
+
description: "Scaffold a new repo from a GitHub template (e.g. cli, react, nextjs, node, monorepo, base)",
|
|
7020
|
+
positionals: []
|
|
7021
|
+
},
|
|
7022
|
+
{
|
|
7023
|
+
name: "plugin create",
|
|
7024
|
+
description: "Scaffold a new @theholocron/holocron-plugin-<slug> package",
|
|
7025
|
+
positionals: [{
|
|
7026
|
+
key: "slug",
|
|
7027
|
+
message: "Package slug (kebab-case):",
|
|
7028
|
+
type: "input"
|
|
7029
|
+
}, {
|
|
7030
|
+
key: "vendor",
|
|
7031
|
+
message: "Vendor display name (PascalCase):",
|
|
7032
|
+
type: "input"
|
|
7033
|
+
}]
|
|
7034
|
+
},
|
|
7035
|
+
{
|
|
7036
|
+
name: "skills install",
|
|
7037
|
+
description: "Copy skills from @theholocron/skills into .agents/ with agent symlinks",
|
|
7038
|
+
positionals: [],
|
|
7039
|
+
group: "skills"
|
|
7040
|
+
},
|
|
7041
|
+
{
|
|
7042
|
+
name: "skills remove",
|
|
7043
|
+
description: "Remove installed skills via npx skills remove",
|
|
7044
|
+
positionals: [],
|
|
7045
|
+
group: "skills"
|
|
7046
|
+
},
|
|
7047
|
+
{
|
|
7048
|
+
name: "skills update",
|
|
7049
|
+
description: "Update installed skills to their latest upstream versions via npx skills update",
|
|
7050
|
+
positionals: [],
|
|
7051
|
+
group: "skills"
|
|
7052
|
+
},
|
|
7053
|
+
{
|
|
7054
|
+
name: "upgrade node",
|
|
7055
|
+
description: "Scan the repo and update every Node.js version pin to a new major",
|
|
7056
|
+
positionals: [{
|
|
7057
|
+
key: "to",
|
|
7058
|
+
message: "Target Node.js major version:",
|
|
7059
|
+
type: "input",
|
|
7060
|
+
validate: numeric
|
|
7061
|
+
}],
|
|
7062
|
+
group: "upgrade"
|
|
7063
|
+
},
|
|
7064
|
+
{
|
|
7065
|
+
name: "upgrade deps",
|
|
7066
|
+
description: "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API",
|
|
7067
|
+
positionals: [],
|
|
7068
|
+
group: "upgrade"
|
|
7069
|
+
},
|
|
7070
|
+
{
|
|
7071
|
+
name: "auth set",
|
|
7072
|
+
description: "Verify + store a bootstrap token for a provider",
|
|
7073
|
+
positionals: [{
|
|
7074
|
+
key: "provider",
|
|
7075
|
+
message: "Provider name:",
|
|
7076
|
+
type: "input"
|
|
7077
|
+
}],
|
|
7078
|
+
group: "auth"
|
|
7079
|
+
},
|
|
7080
|
+
{
|
|
7081
|
+
name: "auth unset",
|
|
7082
|
+
description: "Remove a stored bootstrap token",
|
|
7083
|
+
positionals: [{
|
|
7084
|
+
key: "provider",
|
|
7085
|
+
message: "Provider:",
|
|
7086
|
+
type: "select",
|
|
7087
|
+
choices: listStoredProviders
|
|
7088
|
+
}],
|
|
7089
|
+
group: "auth"
|
|
7090
|
+
},
|
|
7091
|
+
{
|
|
7092
|
+
name: "auth check",
|
|
7093
|
+
description: "Re-verify a stored bootstrap token",
|
|
7094
|
+
positionals: [{
|
|
7095
|
+
key: "provider",
|
|
7096
|
+
message: "Provider:",
|
|
7097
|
+
type: "select",
|
|
7098
|
+
choices: listStoredProviders
|
|
7099
|
+
}],
|
|
7100
|
+
group: "auth"
|
|
7101
|
+
},
|
|
7102
|
+
{
|
|
7103
|
+
name: "auth list",
|
|
7104
|
+
description: "List every provider with a stored bootstrap token",
|
|
7105
|
+
positionals: [],
|
|
7106
|
+
group: "auth"
|
|
7107
|
+
}
|
|
7108
|
+
];
|
|
7109
|
+
/** Look up a {@link CommandEntry} by its full name — for Layer 3 call sites in `cli.ts`. */
|
|
7110
|
+
function getEntry(name) {
|
|
7111
|
+
const entry = COMMAND_REGISTRY.find((e) => e.name === name);
|
|
7112
|
+
if (!entry) throw new Error(`interactive-menu: no COMMAND_REGISTRY entry named "${name}"`);
|
|
7113
|
+
return entry;
|
|
7114
|
+
}
|
|
7115
|
+
/**
|
|
7116
|
+
* Searchable autocomplete over `entries` — the Layer 1 top-level picker, and
|
|
7117
|
+
* (passed a `group`-filtered subset) the Layer 2 parent-command picker.
|
|
7118
|
+
* `select()` would work too at these list sizes, but `search()` degrades
|
|
7119
|
+
* gracefully as the surface grows and costs nothing when it doesn't.
|
|
7120
|
+
*/
|
|
7121
|
+
async function pickCommand(entries, message = "What would you like to do?") {
|
|
7122
|
+
const byName = new Map(entries.map((e) => [e.name, e]));
|
|
7123
|
+
const picked = await search({
|
|
7124
|
+
message,
|
|
7125
|
+
source: (term) => searchChoices(entries, term)
|
|
7126
|
+
});
|
|
7127
|
+
return byName.get(picked);
|
|
7128
|
+
}
|
|
7129
|
+
/**
|
|
7130
|
+
* `@inquirer/search`'s `source` callback, factored out as a plain function —
|
|
7131
|
+
* unit-testable directly instead of only through a mocked `search()` call.
|
|
7132
|
+
* Empty/undefined `term` (nothing typed yet) returns every entry.
|
|
7133
|
+
*/
|
|
7134
|
+
function searchChoices(entries, term) {
|
|
7135
|
+
return (!term ? entries : entries.filter((e) => matches(e, term))).map((e) => ({
|
|
7136
|
+
name: e.name,
|
|
7137
|
+
value: e.name,
|
|
7138
|
+
description: e.description
|
|
7139
|
+
}));
|
|
7140
|
+
}
|
|
7141
|
+
function matches(entry, term) {
|
|
7142
|
+
const needle = term.toLowerCase();
|
|
7143
|
+
return entry.name.toLowerCase().includes(needle) || entry.description.toLowerCase().includes(needle);
|
|
7144
|
+
}
|
|
7145
|
+
/**
|
|
7146
|
+
* For each of `entry.positionals` not already present in `argv`, prompts for
|
|
7147
|
+
* it (`input()` or `select()`, per the positional's `type`) and returns the
|
|
7148
|
+
* resolved values in positional order — ready to append to a spawned child's
|
|
7149
|
+
* argv, or to use directly in the current handler (Layer 3).
|
|
7150
|
+
*
|
|
7151
|
+
* Throws {@link NonInteractiveError} the first time it would need to prompt
|
|
7152
|
+
* on a non-TTY stdin, and again if a `select` positional's choices resolve
|
|
7153
|
+
* empty (nothing to pick from — e.g. `auth unset` with no stored tokens).
|
|
7154
|
+
*/
|
|
7155
|
+
async function promptForPositionals(entry, argv) {
|
|
7156
|
+
const out = [];
|
|
7157
|
+
for (const positional of entry.positionals) {
|
|
7158
|
+
const existing = argv[positional.key];
|
|
7159
|
+
if (existing !== void 0 && existing !== "") {
|
|
7160
|
+
out.push(String(existing));
|
|
7161
|
+
continue;
|
|
7162
|
+
}
|
|
7163
|
+
if (!process.stdin.isTTY) throw new NonInteractiveError(`\`${entry.name}\` needs "${positional.key}" — pass it directly: holocron ${entry.name} <${positional.cliArg ?? positional.key}>`);
|
|
7164
|
+
if (positional.type === "select") {
|
|
7165
|
+
const choices = typeof positional.choices === "function" ? positional.choices() : positional.choices ?? [];
|
|
7166
|
+
if (choices.length === 0) throw new NonInteractiveError(`\`${entry.name}\`: no ${positional.key} to choose from — nothing stored yet`);
|
|
7167
|
+
out.push(await select({
|
|
7168
|
+
message: positional.message,
|
|
7169
|
+
choices: choices.map((c) => ({ value: c }))
|
|
7170
|
+
}));
|
|
7171
|
+
} else out.push(await input({
|
|
7172
|
+
message: positional.message,
|
|
7173
|
+
...positional.validate ? { validate: positional.validate } : {}
|
|
7174
|
+
}));
|
|
7175
|
+
}
|
|
7176
|
+
return out;
|
|
7177
|
+
}
|
|
7178
|
+
/**
|
|
7179
|
+
* `--token` (repeatable), `--org`, `--cwd`, `--dry-run` — the global flags a
|
|
7180
|
+
* menu-launched child should inherit from the parent invocation. Everything
|
|
7181
|
+
* else (command-specific options) was never captured by the top-level `$0`
|
|
7182
|
+
* handler in the first place, so there's nothing else to forward.
|
|
7183
|
+
*/
|
|
7184
|
+
function forwardedFlags(argv) {
|
|
7185
|
+
const out = [];
|
|
7186
|
+
const tokens = argv.token;
|
|
7187
|
+
for (const t of tokens ?? []) out.push("--token", t);
|
|
7188
|
+
if (typeof argv.org === "string") out.push("--org", argv.org);
|
|
7189
|
+
if (typeof argv.cwd === "string") out.push("--cwd", argv.cwd);
|
|
7190
|
+
if (argv.dryRun === true) out.push("--dry-run");
|
|
7191
|
+
return out;
|
|
7192
|
+
}
|
|
7193
|
+
/** `entry.name` split into tokens, resolved positionals appended, then forwarded flags. */
|
|
7194
|
+
function buildChildArgv(entry, positionals, parentArgv) {
|
|
7195
|
+
return [
|
|
7196
|
+
...entry.name.split(" "),
|
|
7197
|
+
...positionals,
|
|
7198
|
+
...forwardedFlags(parentArgv)
|
|
7199
|
+
];
|
|
7200
|
+
}
|
|
7201
|
+
/** Spawn `holocron <...args>` inheriting stdio; resolve with its exit code. */
|
|
7202
|
+
function spawnChild(args) {
|
|
7203
|
+
return new Promise((resolve) => {
|
|
7204
|
+
const child = spawn(process.execPath, [process.argv[1], ...args], { stdio: "inherit" });
|
|
7205
|
+
child.on("exit", (code) => resolve(code ?? 0));
|
|
7206
|
+
child.on("error", () => resolve(1));
|
|
7207
|
+
});
|
|
7208
|
+
}
|
|
7209
|
+
/**
|
|
7210
|
+
* Layers 1 and 2: pick a command from `entries`, prompt for its required
|
|
7211
|
+
* positionals, spawn it as a fresh `holocron` invocation, and set
|
|
7212
|
+
* `process.exitCode` from the child — the caller (a `$0` handler in
|
|
7213
|
+
* `cli.ts`) returns normally afterward so the parent's own telemetry
|
|
7214
|
+
* flush / update-notifier tail still runs. The parent's own
|
|
7215
|
+
* `command_completed` event fires with command name "unknown" (the
|
|
7216
|
+
* middleware ran before any command was picked) — left as-is rather than
|
|
7217
|
+
* suppressed; it's a real, useful signal ("the menu got used").
|
|
7218
|
+
*/
|
|
7219
|
+
async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessage = "Run `holocron --help` to see available commands.") {
|
|
7220
|
+
if (!process.stdin.isTTY) throw new NonInteractiveError(nonInteractiveMessage);
|
|
7221
|
+
const picked = await pickCommand(entries, pickMessage);
|
|
7222
|
+
const childArgv = buildChildArgv(picked, await promptForPositionals(picked, {}), parentArgv);
|
|
7223
|
+
process.exitCode = await spawnChild(childArgv);
|
|
7224
|
+
}
|
|
7225
|
+
//#endregion
|
|
6961
7226
|
//#region src/update-notifier.ts
|
|
6962
7227
|
const PACKAGE_NAME = "@theholocron/cli";
|
|
6963
7228
|
const CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
@@ -7083,7 +7348,11 @@ const resolveSyncToken = createFeatureResolver({
|
|
|
7083
7348
|
* Error class names whose `.message` is a complete, actionable sentence —
|
|
7084
7349
|
* the top-level catch prints it and suppresses the stack trace.
|
|
7085
7350
|
*/
|
|
7086
|
-
const USER_FACING_ERRORS = /* @__PURE__ */ new Set([
|
|
7351
|
+
const USER_FACING_ERRORS = /* @__PURE__ */ new Set([
|
|
7352
|
+
"WorkspaceContextError",
|
|
7353
|
+
"ConfigFileError",
|
|
7354
|
+
"NonInteractiveError"
|
|
7355
|
+
]);
|
|
7087
7356
|
const { version: CLI_VERSION } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
7088
7357
|
/** Whether to print the correlation id at command end (`--debug` / `--verbose`). */
|
|
7089
7358
|
let printRunId = false;
|
|
@@ -7271,9 +7540,10 @@ try {
|
|
|
7271
7540
|
},
|
|
7272
7541
|
...argv.name ? { name: argv.name } : {}
|
|
7273
7542
|
}).status === "fail") process.exitCode = 1;
|
|
7274
|
-
}).
|
|
7543
|
+
}).command("$0", false, () => {}, async (argv) => {
|
|
7544
|
+
await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "skills"), argv, "skills — choose a subcommand:", "Run `holocron skills --help` to see available skills subcommands.");
|
|
7545
|
+
}), () => {}).command("secret set [name] [value]", "Set a single secret via the configured `secrets` capability", (y) => y.positional("name", {
|
|
7275
7546
|
type: "string",
|
|
7276
|
-
demandOption: true,
|
|
7277
7547
|
describe: "Secret name (e.g., NPM_TOKEN)"
|
|
7278
7548
|
}).positional("value", {
|
|
7279
7549
|
type: "string",
|
|
@@ -7292,6 +7562,7 @@ try {
|
|
|
7292
7562
|
}), async (argv) => {
|
|
7293
7563
|
const tokens = tokenContext(argv.token);
|
|
7294
7564
|
if (!tokens) return;
|
|
7565
|
+
const [name] = await promptForPositionals(getEntry("secret set"), argv);
|
|
7295
7566
|
const scopeArg = argv.scope;
|
|
7296
7567
|
const scope = parseScope(scopeArg);
|
|
7297
7568
|
const loaded = await loadConfig(argv.cwd);
|
|
@@ -7304,15 +7575,14 @@ try {
|
|
|
7304
7575
|
...tokens,
|
|
7305
7576
|
org: resolveOrg(argv, loaded.resolved)
|
|
7306
7577
|
},
|
|
7307
|
-
name
|
|
7578
|
+
name,
|
|
7308
7579
|
...argv.value ? { value: argv.value } : {},
|
|
7309
7580
|
...argv.fromStdin ? { fromStdin: true } : {},
|
|
7310
7581
|
...argv.fromEnv ? { fromEnv: argv.fromEnv } : {},
|
|
7311
7582
|
scope
|
|
7312
7583
|
})).status === "fail") process.exitCode = 1;
|
|
7313
|
-
}).command("secrets sync
|
|
7584
|
+
}).command("secrets sync [environmentId]", "Read a vault environment + fan KEY=VALUEs out to secrets + deployment env vars", (y) => y.positional("environmentId", {
|
|
7314
7585
|
type: "string",
|
|
7315
|
-
demandOption: true,
|
|
7316
7586
|
describe: "Vault environment id to read (1P Environment id, etc.)"
|
|
7317
7587
|
}).option("project-id", {
|
|
7318
7588
|
type: "string",
|
|
@@ -7324,6 +7594,7 @@ try {
|
|
|
7324
7594
|
}), async (argv) => {
|
|
7325
7595
|
const tokens = tokenContext(argv.token);
|
|
7326
7596
|
if (!tokens) return;
|
|
7597
|
+
const [environmentId] = await promptForPositionals(getEntry("secrets sync"), argv);
|
|
7327
7598
|
const loaded = await loadConfig(argv.cwd);
|
|
7328
7599
|
applyResolvedConfig(argv, loaded.resolved);
|
|
7329
7600
|
if ((await runSecretsSync({
|
|
@@ -7334,13 +7605,12 @@ try {
|
|
|
7334
7605
|
...tokens,
|
|
7335
7606
|
org: resolveOrg(argv, loaded.resolved)
|
|
7336
7607
|
},
|
|
7337
|
-
environmentId
|
|
7608
|
+
environmentId,
|
|
7338
7609
|
...argv.projectId ? { projectId: argv.projectId } : {},
|
|
7339
7610
|
targets: argv.target
|
|
7340
7611
|
})).summary.fail > 0) process.exitCode = 1;
|
|
7341
|
-
}).command("deploy
|
|
7612
|
+
}).command("deploy [branch]", "Trigger a deployment via the configured `deployment` capability", (y) => y.positional("branch", {
|
|
7342
7613
|
type: "string",
|
|
7343
|
-
demandOption: true,
|
|
7344
7614
|
describe: "Git branch to deploy"
|
|
7345
7615
|
}).option("project-id", {
|
|
7346
7616
|
type: "string",
|
|
@@ -7353,6 +7623,7 @@ try {
|
|
|
7353
7623
|
}), async (argv) => {
|
|
7354
7624
|
const tokens = tokenContext(argv.token);
|
|
7355
7625
|
if (!tokens) return;
|
|
7626
|
+
const [branch] = await promptForPositionals(getEntry("deploy"), argv);
|
|
7356
7627
|
const loaded = await loadConfig(argv.cwd);
|
|
7357
7628
|
applyResolvedConfig(argv, loaded.resolved);
|
|
7358
7629
|
if ((await runDeploy({
|
|
@@ -7364,12 +7635,11 @@ try {
|
|
|
7364
7635
|
org: resolveOrg(argv, loaded.resolved)
|
|
7365
7636
|
},
|
|
7366
7637
|
projectId: argv.projectId,
|
|
7367
|
-
branch
|
|
7638
|
+
branch,
|
|
7368
7639
|
...argv.target ? { target: argv.target } : {}
|
|
7369
7640
|
})).status === "fail") process.exitCode = 1;
|
|
7370
|
-
}).command("cleanup-preview
|
|
7641
|
+
}).command("cleanup-preview [pr]", "List and delete Cloudflare Pages preview deployments for a GitHub PR", (y) => y.positional("pr", {
|
|
7371
7642
|
type: "number",
|
|
7372
|
-
demandOption: true,
|
|
7373
7643
|
describe: "PR number to clean up"
|
|
7374
7644
|
}).option("project", {
|
|
7375
7645
|
type: "string",
|
|
@@ -7381,6 +7651,7 @@ try {
|
|
|
7381
7651
|
}), async (argv) => {
|
|
7382
7652
|
const tokens = tokenContext(argv.token);
|
|
7383
7653
|
if (!tokens) return;
|
|
7654
|
+
const [pr] = await promptForPositionals(getEntry("cleanup-preview"), argv);
|
|
7384
7655
|
const loaded = await loadConfig(argv.cwd);
|
|
7385
7656
|
applyResolvedConfig(argv, loaded.resolved);
|
|
7386
7657
|
if ((await runCleanupPreview({
|
|
@@ -7391,17 +7662,17 @@ try {
|
|
|
7391
7662
|
...tokens,
|
|
7392
7663
|
org: resolveOrg(argv, loaded.resolved)
|
|
7393
7664
|
},
|
|
7394
|
-
prNumber:
|
|
7665
|
+
prNumber: Number(pr),
|
|
7395
7666
|
project: argv.project,
|
|
7396
7667
|
...argv.repo ? { repo: argv.repo } : {}
|
|
7397
7668
|
})).status === "fail") process.exitCode = 1;
|
|
7398
|
-
}).command("bump-versions
|
|
7669
|
+
}).command("bump-versions [new-version]", "Bump all non-private package versions in lockstep (semantic-release prepareCmd)", (y) => y.positional("new-version", {
|
|
7399
7670
|
type: "string",
|
|
7400
|
-
demandOption: true,
|
|
7401
7671
|
describe: "Version to set (e.g., 4.2.0 or 2.0.0-alpha.1)"
|
|
7402
7672
|
}), async (argv) => {
|
|
7673
|
+
const [newVersion] = await promptForPositionals(getEntry("bump-versions"), argv);
|
|
7403
7674
|
if ((await runNpmBumpVersions({
|
|
7404
|
-
version:
|
|
7675
|
+
version: newVersion,
|
|
7405
7676
|
cwd: argv.cwd,
|
|
7406
7677
|
dryRun: argv.dryRun
|
|
7407
7678
|
})).status === "fail") process.exitCode = 1;
|
|
@@ -7824,13 +8095,11 @@ try {
|
|
|
7824
8095
|
}
|
|
7825
8096
|
throw err;
|
|
7826
8097
|
}
|
|
7827
|
-
}).command("plugin create
|
|
8098
|
+
}).command("plugin create [slug] [vendor]", "Scaffold a new @theholocron/holocron-plugin-<slug> package", (y) => y.positional("slug", {
|
|
7828
8099
|
type: "string",
|
|
7829
|
-
demandOption: true,
|
|
7830
8100
|
describe: "Package slug (kebab-case)"
|
|
7831
8101
|
}).positional("vendor", {
|
|
7832
8102
|
type: "string",
|
|
7833
|
-
demandOption: true,
|
|
7834
8103
|
describe: "Vendor display name (PascalCase)"
|
|
7835
8104
|
}).option("capability", {
|
|
7836
8105
|
type: "string",
|
|
@@ -7850,7 +8119,7 @@ try {
|
|
|
7850
8119
|
describe: "Run post-scaffold pnpm install + typecheck + lint + test (default true; --no-verify skips)"
|
|
7851
8120
|
}), async (argv) => {
|
|
7852
8121
|
try {
|
|
7853
|
-
const vendor = argv
|
|
8122
|
+
const [slug, vendor] = await promptForPositionals(getEntry("plugin create"), argv);
|
|
7854
8123
|
const { capability, vendorEnv, baseUrl } = await resolvePluginCreateInputs({
|
|
7855
8124
|
capability: argv.capability,
|
|
7856
8125
|
vendorEnv: argv.vendorEnv,
|
|
@@ -7867,7 +8136,7 @@ try {
|
|
|
7867
8136
|
inputBaseUrl: () => input({ message: `REST base URL for the ${vendor} API (e.g. https://api.myvendor.com):` })
|
|
7868
8137
|
});
|
|
7869
8138
|
if (runPluginCreate({
|
|
7870
|
-
slug
|
|
8139
|
+
slug,
|
|
7871
8140
|
vendorName: vendor,
|
|
7872
8141
|
capability,
|
|
7873
8142
|
vendorEnv,
|
|
@@ -7885,14 +8154,14 @@ try {
|
|
|
7885
8154
|
}
|
|
7886
8155
|
throw err;
|
|
7887
8156
|
}
|
|
7888
|
-
}).command("upgrade", "Upgrade toolchain version pins across the repo", (y) => y.command("node
|
|
8157
|
+
}).command("upgrade", "Upgrade toolchain version pins across the repo", (y) => y.command("node [to]", "Scan the repo and update every Node.js version pin to a new major", (yy) => yy.positional("to", {
|
|
7889
8158
|
type: "number",
|
|
7890
|
-
demandOption: true,
|
|
7891
8159
|
describe: "Target Node.js major version (e.g., 22)"
|
|
7892
8160
|
}).option("from", {
|
|
7893
8161
|
type: "number",
|
|
7894
8162
|
describe: "Current major version to replace. Auto-detected from .nvmrc / engines.node when omitted."
|
|
7895
8163
|
}), async (argv) => {
|
|
8164
|
+
const [to] = await promptForPositionals(getEntry("upgrade node"), argv);
|
|
7896
8165
|
let extra = [];
|
|
7897
8166
|
try {
|
|
7898
8167
|
const raw = readFileSync(join(argv.cwd, "holocron.config.json"), "utf8");
|
|
@@ -7900,7 +8169,7 @@ try {
|
|
|
7900
8169
|
if (Array.isArray(extraRaw)) extra = extraRaw;
|
|
7901
8170
|
} catch {}
|
|
7902
8171
|
const report = await runUpgradeNode({
|
|
7903
|
-
to:
|
|
8172
|
+
to: Number(to),
|
|
7904
8173
|
...argv.from != null ? { from: argv.from } : {},
|
|
7905
8174
|
cwd: argv.cwd,
|
|
7906
8175
|
dryRun: argv.dryRun,
|
|
@@ -7924,31 +8193,31 @@ try {
|
|
|
7924
8193
|
if (report.message) getLogger().error(`upgrade deps: ${report.message}`);
|
|
7925
8194
|
process.exitCode = 1;
|
|
7926
8195
|
}
|
|
7927
|
-
}).
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
8196
|
+
}).command("$0", false, () => {}, async (argv) => {
|
|
8197
|
+
await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "upgrade"), argv, "upgrade — choose a subcommand:", "Run `holocron upgrade --help` to see available upgrade subcommands.");
|
|
8198
|
+
}), () => {}).command("auth <subcommand>", "Manage bootstrap credentials in the OS keyring", (y) => y.command("set [provider] [value]", "Verify + store a bootstrap token for a provider", (yy) => yy.positional("provider", { type: "string" }).positional("value", { type: "string" }), async (argv) => {
|
|
8199
|
+
const [provider] = await promptForPositionals(getEntry("auth set"), argv);
|
|
7931
8200
|
if ((await runAuthSet({
|
|
7932
|
-
provider
|
|
8201
|
+
provider,
|
|
7933
8202
|
...argv.value ? { positional: argv.value } : {},
|
|
7934
8203
|
...argv.org ? { org: argv.org } : {}
|
|
7935
8204
|
})).status === "fail") process.exitCode = 1;
|
|
7936
|
-
}).command("unset
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
}), (argv) => {
|
|
7940
|
-
|
|
7941
|
-
}).command("check <provider>", "Re-verify a stored bootstrap token", (yy) => yy.positional("provider", {
|
|
7942
|
-
type: "string",
|
|
7943
|
-
demandOption: true
|
|
7944
|
-
}), async (argv) => {
|
|
8205
|
+
}).command("unset [provider]", "Remove a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
|
|
8206
|
+
const [provider] = await promptForPositionals(getEntry("auth unset"), argv);
|
|
8207
|
+
runAuthUnset({ provider });
|
|
8208
|
+
}).command("check [provider]", "Re-verify a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
|
|
8209
|
+
const [provider] = await promptForPositionals(getEntry("auth check"), argv);
|
|
7945
8210
|
if ((await runAuthCheck({
|
|
7946
|
-
provider
|
|
8211
|
+
provider,
|
|
7947
8212
|
...argv.org ? { org: argv.org } : {}
|
|
7948
8213
|
})).status === "fail") process.exitCode = 1;
|
|
7949
8214
|
}).command("list", "List every provider with a stored bootstrap token", () => {}, async () => {
|
|
7950
8215
|
await runAuthList();
|
|
7951
|
-
}).
|
|
8216
|
+
}).command("$0", false, () => {}, async (argv) => {
|
|
8217
|
+
await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "auth"), argv, "auth — choose a subcommand:", "Run `holocron auth --help` to see available auth subcommands.");
|
|
8218
|
+
}), () => {}).command("$0", false, () => {}, async (argv) => {
|
|
8219
|
+
await launchMenu(COMMAND_REGISTRY.filter((e) => !e.group), argv);
|
|
8220
|
+
}).strict().help().epilogue(`Execution contexts:
|
|
7952
8221
|
global works from a bare 'npm i -g': ${commandsInContext("global").join(", ")}\n repo-aware needs ./holocron.config in cwd: ${commandsInContext("repo-aware").join(", ")}\n workspace also needs the plugin packages: ${commandsInContext("workspace").join(", ")}\n https://theholocron.github.io/holocron/execution-contexts`).fail((msg, err) => {
|
|
7953
8222
|
if (err instanceof Error && USER_FACING_ERRORS.has(err.name)) {
|
|
7954
8223
|
captureException(err);
|