@theholocron/cli 5.0.0-alpha.129 → 5.0.0-alpha.133
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 +48 -40
- package/dist/cli.mjs +810 -594
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +21 -21
- package/dist/index.mjs +5 -5
- package/dist/plugin/capabilities.d.mts +49 -3
- package/dist/plugin/capabilities.mjs +1 -0
- package/package.json +6 -6
package/dist/cli.mjs
CHANGED
|
@@ -184,6 +184,7 @@ const CARDINALITY = {
|
|
|
184
184
|
analytics: "many",
|
|
185
185
|
errors: "single",
|
|
186
186
|
logs: "single",
|
|
187
|
+
monitoring: "single",
|
|
187
188
|
wiki: "single",
|
|
188
189
|
workers: "single"
|
|
189
190
|
};
|
|
@@ -305,9 +306,9 @@ function resolveConfig(raw) {
|
|
|
305
306
|
//#endregion
|
|
306
307
|
//#region src/commands/contexts.ts
|
|
307
308
|
/**
|
|
308
|
-
* Command → context. Keys are the command
|
|
309
|
-
* `argv._`
|
|
310
|
-
*
|
|
309
|
+
* Command → context. Keys are the full command path as it lands in
|
|
310
|
+
* yargs' `argv._` (`"auth set"`, `"repo upgrade node"`), the bare verb
|
|
311
|
+
* otherwise; a longer, more specific key wins over its parent.
|
|
311
312
|
*
|
|
312
313
|
* - **`global`** — needs nothing but the CLI binary. Works anywhere.
|
|
313
314
|
* - **`repo-aware`** — reads `./holocron.config` + `./package.json`
|
|
@@ -317,33 +318,33 @@ function resolveConfig(raw) {
|
|
|
317
318
|
*/
|
|
318
319
|
const COMMAND_CONTEXTS = {
|
|
319
320
|
clone: "global",
|
|
320
|
-
new: "global",
|
|
321
|
-
"
|
|
322
|
-
"upgrade
|
|
323
|
-
"
|
|
321
|
+
"new repo": "global",
|
|
322
|
+
"new plugin": "global",
|
|
323
|
+
"repo upgrade node": "global",
|
|
324
|
+
"repo upgrade deps": "global",
|
|
324
325
|
"auth set": "global",
|
|
325
326
|
"auth unset": "global",
|
|
326
327
|
"auth list": "global",
|
|
327
328
|
"auth check": "global",
|
|
328
|
-
"bump-versions": "global",
|
|
329
|
-
publish: "global",
|
|
329
|
+
"package bump-versions": "global",
|
|
330
|
+
"package publish": "global",
|
|
330
331
|
"skills install": "global",
|
|
331
332
|
"skills remove": "global",
|
|
332
333
|
"skills update": "global",
|
|
333
334
|
"run lint commit-msg": "global",
|
|
334
335
|
run: "repo-aware",
|
|
335
336
|
"run ci": "repo-aware",
|
|
336
|
-
"deploy
|
|
337
|
+
"deploy on-release": "repo-aware",
|
|
337
338
|
"config show": "repo-aware",
|
|
338
|
-
"sync
|
|
339
|
+
"repo sync readme": "repo-aware",
|
|
339
340
|
doctor: "workspace",
|
|
340
|
-
setup: "workspace",
|
|
341
|
-
"
|
|
341
|
+
"repo setup": "workspace",
|
|
342
|
+
"secrets set": "workspace",
|
|
342
343
|
"secrets sync": "workspace",
|
|
343
344
|
deploy: "workspace",
|
|
344
|
-
"cleanup-preview": "workspace",
|
|
345
|
-
sync: "workspace",
|
|
346
|
-
"sync
|
|
345
|
+
"deploy cleanup-preview": "workspace",
|
|
346
|
+
"repo sync": "workspace",
|
|
347
|
+
"sync github": "workspace"
|
|
347
348
|
};
|
|
348
349
|
/**
|
|
349
350
|
* The context for a command name from `argv._`. Falls back to the leading
|
|
@@ -352,8 +353,11 @@ const COMMAND_CONTEXTS = {
|
|
|
352
353
|
*/
|
|
353
354
|
function contextForCommand(name) {
|
|
354
355
|
const map = COMMAND_CONTEXTS;
|
|
355
|
-
|
|
356
|
-
|
|
356
|
+
const tokens = name.split(" ").filter(Boolean);
|
|
357
|
+
for (let n = tokens.length; n > 0; n--) {
|
|
358
|
+
const ctx = map[tokens.slice(0, n).join(" ")];
|
|
359
|
+
if (ctx) return ctx;
|
|
360
|
+
}
|
|
357
361
|
}
|
|
358
362
|
/** Command names in a given context, in registration order. */
|
|
359
363
|
function commandsInContext(context) {
|
|
@@ -1067,7 +1071,7 @@ async function runCleanupPreview(input) {
|
|
|
1067
1071
|
// c8 ignore next -- real PluginLoader construction is integration-level; unit tests always supply loader
|
|
1068
1072
|
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
1069
1073
|
await loader.load();
|
|
1070
|
-
assertPluginsResolvable(loader, "cleanup-preview");
|
|
1074
|
+
assertPluginsResolvable(loader, "deploy cleanup-preview");
|
|
1071
1075
|
logger.info({
|
|
1072
1076
|
pr: input.prNumber,
|
|
1073
1077
|
project: input.project
|
|
@@ -1790,6 +1794,14 @@ async function smokeCheck(key, provider, impl) {
|
|
|
1790
1794
|
}
|
|
1791
1795
|
return mk(key, provider, "ok", `provider: ${desc.provider}; env keys: ${desc.envKeys.join(", ")}`);
|
|
1792
1796
|
}
|
|
1797
|
+
case "monitoring": {
|
|
1798
|
+
const monitoring = impl;
|
|
1799
|
+
if (monitoring.whoami) {
|
|
1800
|
+
const who = await monitoring.whoami();
|
|
1801
|
+
return mk(key, provider, who.ok ? "ok" : "fail", who.ok ? "token verified" : "token check failed");
|
|
1802
|
+
}
|
|
1803
|
+
return mk(key, provider, "skip", "loaded (no smoke check defined for this capability)");
|
|
1804
|
+
}
|
|
1793
1805
|
default: return mk(key, provider, "skip", "loaded (no smoke check defined for this capability)");
|
|
1794
1806
|
}
|
|
1795
1807
|
} catch (err) {
|
|
@@ -1810,7 +1822,7 @@ function mk(capability, provider, status, message) {
|
|
|
1810
1822
|
//#endregion
|
|
1811
1823
|
//#region src/commands/new.ts
|
|
1812
1824
|
/**
|
|
1813
|
-
* `holocron new <type> <name>` — create a GitHub repo from a template and
|
|
1825
|
+
* `holocron new repo <type> <name>` — create a GitHub repo from a template and
|
|
1814
1826
|
* bootstrap it by replacing all template-slug casing variants with the new
|
|
1815
1827
|
* project name, then generate a `holocron.config.ts` from the answers given
|
|
1816
1828
|
* during the interactive wizard.
|
|
@@ -2164,7 +2176,7 @@ async function runNew(input) {
|
|
|
2164
2176
|
};
|
|
2165
2177
|
}
|
|
2166
2178
|
print("");
|
|
2167
|
-
print(" Running holocron setup…");
|
|
2179
|
+
print(" Running holocron repo setup…");
|
|
2168
2180
|
try {
|
|
2169
2181
|
const setupArgs = ["setup"];
|
|
2170
2182
|
const setupEnv = {};
|
|
@@ -2184,7 +2196,7 @@ async function runNew(input) {
|
|
|
2184
2196
|
env: setupEnv
|
|
2185
2197
|
});
|
|
2186
2198
|
} catch {
|
|
2187
|
-
print(" ✗ holocron setup failed — run it manually after checking your config");
|
|
2199
|
+
print(" ✗ holocron repo setup failed — run it manually after checking your config");
|
|
2188
2200
|
}
|
|
2189
2201
|
}
|
|
2190
2202
|
print("");
|
|
@@ -2194,7 +2206,7 @@ async function runNew(input) {
|
|
|
2194
2206
|
print(` 1. cd ${repoDir}`);
|
|
2195
2207
|
if (input.noVerify) {
|
|
2196
2208
|
print(` 2. pnpm install`);
|
|
2197
|
-
print(` 3. holocron setup # wire up secrets, teams, labels, etc.`);
|
|
2209
|
+
print(` 3. holocron repo setup # wire up secrets, teams, labels, etc.`);
|
|
2198
2210
|
print(` 4. git push -u origin HEAD`);
|
|
2199
2211
|
} else print(` 2. git push -u origin HEAD`);
|
|
2200
2212
|
logger.info({
|
|
@@ -2550,12 +2562,12 @@ function render$10(inputs) {
|
|
|
2550
2562
|
version: "2.0.0-alpha.1",
|
|
2551
2563
|
description: `Holocron plugin for ${inputs.vendorName}. Implements the ${inputs.capability} capability against ${inputs.vendorName}'s REST API, plus exports verifyToken + AUTH_HINT for \`holocron auth\`.`,
|
|
2552
2564
|
keywords,
|
|
2553
|
-
homepage: `https://github.com/theholocron/holocron/tree/main/
|
|
2565
|
+
homepage: `https://github.com/theholocron/holocron/tree/main/plugins/holocron-plugin-${inputs.slug}#readme`,
|
|
2554
2566
|
bugs: "https://github.com/theholocron/holocron/issues",
|
|
2555
2567
|
repository: {
|
|
2556
2568
|
type: "git",
|
|
2557
2569
|
url: "git+https://github.com/theholocron/holocron.git",
|
|
2558
|
-
directory: `
|
|
2570
|
+
directory: `plugins/holocron-plugin-${inputs.slug}`
|
|
2559
2571
|
},
|
|
2560
2572
|
license: "MIT",
|
|
2561
2573
|
author: "Newton Koumantzelis",
|
|
@@ -2680,7 +2692,7 @@ export * from "./auth.js";
|
|
|
2680
2692
|
export { type ${clientClass}, ${factoryName} } from "./rest.js";
|
|
2681
2693
|
export { ${inputs.capabilityClass} } from "./capabilities/${inputs.capability}.js";
|
|
2682
2694
|
export { verifyToken } from "./verify-token.js";
|
|
2683
|
-
export type { VerifyTokenResult, VerifyTokenSuccess
|
|
2695
|
+
export type { VerifyTokenFailure, VerifyTokenResult, VerifyTokenSuccess } from "@theholocron/cli";
|
|
2684
2696
|
`;
|
|
2685
2697
|
}
|
|
2686
2698
|
//#endregion
|
|
@@ -2747,7 +2759,7 @@ TODO: fill in as capability methods land.
|
|
|
2747
2759
|
|
|
2748
2760
|
## Status
|
|
2749
2761
|
|
|
2750
|
-
**\`v2.0.0-alpha.1\`** — scaffolded via \`holocron plugin
|
|
2762
|
+
**\`v2.0.0-alpha.1\`** — scaffolded via \`holocron new plugin\`.
|
|
2751
2763
|
Not yet published; capability methods are stubs.
|
|
2752
2764
|
`;
|
|
2753
2765
|
}
|
|
@@ -2910,7 +2922,7 @@ function render$3(inputs) {
|
|
|
2910
2922
|
* TODO: replace the positional args below with whatever your
|
|
2911
2923
|
* capability's methods need (e.g., project id, environment slug,
|
|
2912
2924
|
* secret name). Adapt the test steps to your capability's method
|
|
2913
|
-
* surface. Model on \`
|
|
2925
|
+
* surface. Model on \`plugins/holocron-plugin-infisical/scripts/validate.mjs\`.
|
|
2914
2926
|
*/
|
|
2915
2927
|
|
|
2916
2928
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- filled in by operator
|
|
@@ -2935,7 +2947,7 @@ try {
|
|
|
2935
2947
|
} catch (err) {
|
|
2936
2948
|
if (err instanceof AuthError) {
|
|
2937
2949
|
console.error(err.message);
|
|
2938
|
-
console.error(" see:
|
|
2950
|
+
console.error(" see: plugins/holocron-plugin-${inputs.slug}/README.md#setup");
|
|
2939
2951
|
process.exit(2);
|
|
2940
2952
|
}
|
|
2941
2953
|
throw err;
|
|
@@ -3118,7 +3130,7 @@ export default defineConfig({
|
|
|
3118
3130
|
//#endregion
|
|
3119
3131
|
//#region src/commands/plugin-create/index.ts
|
|
3120
3132
|
/**
|
|
3121
|
-
* `holocron plugin
|
|
3133
|
+
* `holocron new plugin <slug> <vendor>` — scaffold a new plugin
|
|
3122
3134
|
* package matching the proven template.
|
|
3123
3135
|
*
|
|
3124
3136
|
* Design: see `docs/wiki/specifications/tool-plugin-create.spec.md`.
|
|
@@ -3126,11 +3138,11 @@ export default defineConfig({
|
|
|
3126
3138
|
* Flow:
|
|
3127
3139
|
* 1. Preflight — verify CWD is a workspace root (pnpm-workspace.yaml
|
|
3128
3140
|
* + packages/ dir present).
|
|
3129
|
-
* 2. Slug collision —
|
|
3141
|
+
* 2. Slug collision — plugins/holocron-plugin-<slug>/ must not exist.
|
|
3130
3142
|
* 3. Capability sanity — must be one of the 14 known keys; warn for
|
|
3131
3143
|
* many-cardinality caps.
|
|
3132
3144
|
* 4. Generate — for each template, write to
|
|
3133
|
-
*
|
|
3145
|
+
* plugins/holocron-plugin-<slug>/<path>.
|
|
3134
3146
|
* 5. Verify (unless --no-verify) — runs pnpm install +
|
|
3135
3147
|
* pnpm --filter <pkg> typecheck lint test.
|
|
3136
3148
|
* 6. Print next steps.
|
|
@@ -3236,7 +3248,7 @@ function runPluginCreate(input) {
|
|
|
3236
3248
|
preflight(cwd);
|
|
3237
3249
|
validateSlug(input.slug);
|
|
3238
3250
|
validateVendorName(input.vendorName);
|
|
3239
|
-
const packageDir = path.join(cwd, "
|
|
3251
|
+
const packageDir = path.join(cwd, "plugins", `holocron-plugin-${input.slug}`);
|
|
3240
3252
|
if (existsSync(packageDir)) throw new PluginCreateError(`\`${packageDir}\` already exists — edit in place or pick a different slug.`);
|
|
3241
3253
|
validateCapability(input.capability, print);
|
|
3242
3254
|
const derived = deriveDefaults$1({
|
|
@@ -3340,7 +3352,7 @@ function runPluginCreate(input) {
|
|
|
3340
3352
|
};
|
|
3341
3353
|
}
|
|
3342
3354
|
function preflight(cwd) {
|
|
3343
|
-
if (!existsSync(path.join(cwd, "pnpm-workspace.yaml"))) throw new PluginCreateError(`\`pnpm-workspace.yaml\` not found in \`${cwd}\`. Run \`holocron plugin
|
|
3355
|
+
if (!existsSync(path.join(cwd, "pnpm-workspace.yaml"))) throw new PluginCreateError(`\`pnpm-workspace.yaml\` not found in \`${cwd}\`. Run \`holocron new plugin\` from the monorepo root.`);
|
|
3344
3356
|
if (!existsSync(path.join(cwd, "packages"))) throw new PluginCreateError(`\`packages/\` directory not found in \`${cwd}\`.`);
|
|
3345
3357
|
}
|
|
3346
3358
|
/**
|
|
@@ -3397,7 +3409,7 @@ function printNextSteps$1(print, inputs) {
|
|
|
3397
3409
|
//#endregion
|
|
3398
3410
|
//#region src/commands/publish.ts
|
|
3399
3411
|
/**
|
|
3400
|
-
* `holocron publish --initial` — bottles up the chicken-and-egg bootstrap
|
|
3412
|
+
* `holocron package publish --initial` — bottles up the chicken-and-egg bootstrap
|
|
3401
3413
|
* that every new npm-published holocron monorepo hits.
|
|
3402
3414
|
*
|
|
3403
3415
|
* npm requires a package to exist before Trusted Publishing can be
|
|
@@ -3411,14 +3423,14 @@ function printNextSteps$1(print, inputs) {
|
|
|
3411
3423
|
*
|
|
3412
3424
|
* $ pnpm install --frozen-lockfile
|
|
3413
3425
|
* $ pnpm build
|
|
3414
|
-
* $ pnpm exec tsx packages/cli/src/cli.ts publish --initial
|
|
3426
|
+
* $ pnpm exec tsx packages/cli/src/cli.ts package publish --initial
|
|
3415
3427
|
*
|
|
3416
3428
|
* `npm login --auth-type=web` runs automatically the first time — no
|
|
3417
3429
|
* separate manual step. `pnpm install` + `pnpm build` stay outside the
|
|
3418
3430
|
* command (no pnpm-inside-pnpm).
|
|
3419
3431
|
*
|
|
3420
3432
|
* `--initial` is required today — this command only implements the
|
|
3421
|
-
* bootstrap publish. A non-initial `holocron publish` (for a manual publish
|
|
3433
|
+
* bootstrap publish. A non-initial `holocron package publish` (for a manual publish
|
|
3422
3434
|
* outside the semantic-release/OIDC steady state) isn't built yet; the flag
|
|
3423
3435
|
* exists so the surface doesn't need another rename when it is.
|
|
3424
3436
|
*
|
|
@@ -3427,7 +3439,7 @@ function printNextSteps$1(print, inputs) {
|
|
|
3427
3439
|
* setup` for the ephemeral GH admin PAT.
|
|
3428
3440
|
*/
|
|
3429
3441
|
/**
|
|
3430
|
-
* `holocron publish --sync-trust <old-file> <new-file>` — bulk-migrates npm
|
|
3442
|
+
* `holocron package publish --sync-trust <old-file> <new-file>` — bulk-migrates npm
|
|
3431
3443
|
* Trusted Publisher (OIDC) config to a new workflow filename across every
|
|
3432
3444
|
* public package. See issue #689 / `scripts/npm-trust-migrate.sh` (the
|
|
3433
3445
|
* shell-script prototype this supersedes) for the full "why": npm ties
|
|
@@ -3709,7 +3721,7 @@ async function runPublish(input = {}) {
|
|
|
3709
3721
|
if (publish.stdout.includes("EOTP") || publish.stderr.includes("EOTP")) {
|
|
3710
3722
|
print("");
|
|
3711
3723
|
print(" → hint: your npm account requires 2FA for writes. Re-run with `--otp <code>`:");
|
|
3712
|
-
print(` pnpm exec tsx packages/cli/src/cli.ts publish --initial --otp <6-digit-code>`);
|
|
3724
|
+
print(` pnpm exec tsx packages/cli/src/cli.ts package publish --initial --otp <6-digit-code>`);
|
|
3713
3725
|
}
|
|
3714
3726
|
logger.warn({
|
|
3715
3727
|
tag,
|
|
@@ -3734,7 +3746,7 @@ async function runPublish(input = {}) {
|
|
|
3734
3746
|
};
|
|
3735
3747
|
}
|
|
3736
3748
|
/**
|
|
3737
|
-
* `holocron publish` (no `--initial`) — the steady-state publish
|
|
3749
|
+
* `holocron package publish` (no `--initial`) — the steady-state publish
|
|
3738
3750
|
* `exec.publishCmd` in `release.config.ts` should call, replacing each
|
|
3739
3751
|
* repo's hand-typed `pnpm -r --filter=./packages/* publish ...` shell (which
|
|
3740
3752
|
* differs, today, only in monorepo-vs-single-package, provenance on/off, and
|
|
@@ -3874,7 +3886,7 @@ async function runSteadyPublish(input = {}) {
|
|
|
3874
3886
|
if (publish.stdout.includes("EOTP") || publish.stderr.includes("EOTP")) {
|
|
3875
3887
|
print("");
|
|
3876
3888
|
print(" → hint: your npm account requires 2FA for writes. Re-run with `--otp <code>`:");
|
|
3877
|
-
print(` pnpm exec holocron publish --otp <6-digit-code>`);
|
|
3889
|
+
print(` pnpm exec holocron package publish --otp <6-digit-code>`);
|
|
3878
3890
|
}
|
|
3879
3891
|
logger.warn({
|
|
3880
3892
|
tag,
|
|
@@ -4008,7 +4020,7 @@ const defaultLogin = async (cwd) => {
|
|
|
4008
4020
|
//#endregion
|
|
4009
4021
|
//#region src/commands/secret-set.ts
|
|
4010
4022
|
/**
|
|
4011
|
-
* `holocron
|
|
4023
|
+
* `holocron secrets set` — one-shot single-secret push.
|
|
4012
4024
|
*
|
|
4013
4025
|
* Complements `holocron secrets sync` (bulk vault → destinations) for
|
|
4014
4026
|
* ad-hoc scenarios:
|
|
@@ -4030,7 +4042,7 @@ async function runSecretSet(input) {
|
|
|
4030
4042
|
const logger = input.logger ?? getLogger();
|
|
4031
4043
|
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
4032
4044
|
await loader.load();
|
|
4033
|
-
assertPluginsResolvable(loader, "
|
|
4045
|
+
assertPluginsResolvable(loader, "secrets set");
|
|
4034
4046
|
const dryRun = input.context.dryRun ?? false;
|
|
4035
4047
|
const scope = input.scope ?? { kind: "repo" };
|
|
4036
4048
|
if (!loader.has("secrets")) throw new Error("`secrets` capability is not configured — add a `secrets` provider to holocron.config.json");
|
|
@@ -4129,7 +4141,7 @@ function describeScope(scope) {
|
|
|
4129
4141
|
*
|
|
4130
4142
|
* The vault MUST implement `readEnvironment(envId)` (optional method).
|
|
4131
4143
|
* If it doesn't, the command errors with a hint to populate the keys
|
|
4132
|
-
* individually via `holocron
|
|
4144
|
+
* individually via `holocron secrets set` (also planned for v5.1).
|
|
4133
4145
|
*/
|
|
4134
4146
|
async function runSecretsSync(input) {
|
|
4135
4147
|
const print = input.print ?? ((line) => console.log(line));
|
|
@@ -4334,7 +4346,7 @@ function deriveCapabilities(providers) {
|
|
|
4334
4346
|
return Object.keys(providers ?? {}).sort();
|
|
4335
4347
|
}
|
|
4336
4348
|
/**
|
|
4337
|
-
* `source` + `ci` are the minimal baseline every repo `holocron setup`
|
|
4349
|
+
* `source` + `ci` are the minimal baseline every repo `holocron repo setup`
|
|
4338
4350
|
* touches wires — every `holocron.config.ts` in this org already satisfies
|
|
4339
4351
|
* this, so `non-compliant` here means drift (a provider manually removed
|
|
4340
4352
|
* after setup), not an unmet aspirational policy. Deliberately not a richer
|
|
@@ -4462,7 +4474,7 @@ var config_default = "# Configuration for sentiment-bot - https://github.com/beh
|
|
|
4462
4474
|
//#endregion
|
|
4463
4475
|
//#region src/utils/create-header.ts
|
|
4464
4476
|
function createHeader(options) {
|
|
4465
|
-
const { source, tool = "holocron setup", forPrimary = false } = options;
|
|
4477
|
+
const { source, tool = "holocron repo setup", forPrimary = false } = options;
|
|
4466
4478
|
const doNotEdit = forPrimary ? `AUTO-GENERATED — do not edit in theholocron/.github directly.` : `AUTO-GENERATED — do not edit directly.`;
|
|
4467
4479
|
return {
|
|
4468
4480
|
workflowHeader(format = "yaml") {
|
|
@@ -4486,7 +4498,7 @@ function createHeader(options) {
|
|
|
4486
4498
|
},
|
|
4487
4499
|
scaffoldHeader() {
|
|
4488
4500
|
return [
|
|
4489
|
-
`# Scaffolded by holocron setup — edit this file freely.`,
|
|
4501
|
+
`# Scaffolded by holocron repo setup — edit this file freely.`,
|
|
4490
4502
|
`# Source: theholocron/holocron · ${source}`,
|
|
4491
4503
|
``
|
|
4492
4504
|
].join("\n");
|
|
@@ -4528,7 +4540,7 @@ const { workflowHeader: workflowHeader$8 } = createHeader({ source: "packages/cl
|
|
|
4528
4540
|
/**
|
|
4529
4541
|
* `.husky/commit-msg` — lints the commit message via `holocron run lint
|
|
4530
4542
|
* commit-msg` (holocron#789), the real programmatic replacement for
|
|
4531
|
-
* shelling out to `commitlint --edit`. Installed by `holocron setup` when
|
|
4543
|
+
* shelling out to `commitlint --edit`. Installed by `holocron repo setup` when
|
|
4532
4544
|
* hooks are enabled. Same `__HOLOCRON_SCRIPT__` substitution as `pre-push`.
|
|
4533
4545
|
*
|
|
4534
4546
|
* @param holocronScript - the command that invokes the CLI. Defaults to
|
|
@@ -4608,7 +4620,7 @@ var pre_commit_default = "# Run GitLeaks against staged changes only — a full
|
|
|
4608
4620
|
const { workflowHeader: workflowHeader$5 } = createHeader({ source: "packages/cli/src/templates/configs/pre-commit/create-config.ts" });
|
|
4609
4621
|
/**
|
|
4610
4622
|
* `.husky/pre-commit` — runs GitLeaks (scoped to staged changes) and
|
|
4611
|
-
* lint-staged before every commit. Installed by `holocron setup` when hooks
|
|
4623
|
+
* lint-staged before every commit. Installed by `holocron repo setup` when hooks
|
|
4612
4624
|
* are enabled (default for `protection: "strict"`). `git commit --no-verify`
|
|
4613
4625
|
* bypasses it.
|
|
4614
4626
|
*
|
|
@@ -4629,7 +4641,7 @@ var pre_push_default = "# Run the merge-gating checks locally before pushing —
|
|
|
4629
4641
|
const { workflowHeader: workflowHeader$4 } = createHeader({ source: "packages/cli/src/templates/configs/pre-push/create-config.ts" });
|
|
4630
4642
|
/**
|
|
4631
4643
|
* `.husky/pre-push` — runs `holocron run ci` (the merge-gating checks, in CI order)
|
|
4632
|
-
* before every push. Installed by `holocron setup` when hooks are enabled
|
|
4644
|
+
* before every push. Installed by `holocron repo setup` when hooks are enabled
|
|
4633
4645
|
* (default for `protection: "strict"`). `git push --no-verify` bypasses it.
|
|
4634
4646
|
*
|
|
4635
4647
|
* @param holocronScript - the command that invokes the CLI. Defaults to
|
|
@@ -4676,7 +4688,7 @@ var labeler_default = "bug:\n - \"^fix\"\n\nchore:\n - '^chore(?!\\(deps)'\n\n
|
|
|
4676
4688
|
/**
|
|
4677
4689
|
* Canonical AI engineering workflow role prompts.
|
|
4678
4690
|
*
|
|
4679
|
-
* Written to `.agents/prompts/<role>.md` by `holocron setup` when `agent` is
|
|
4691
|
+
* Written to `.agents/prompts/<role>.md` by `holocron repo setup` when `agent` is
|
|
4680
4692
|
* configured. Paths are gitignored and regenerated on every setup run so the
|
|
4681
4693
|
* content always reflects the current CLI version.
|
|
4682
4694
|
*
|
|
@@ -5463,7 +5475,7 @@ async function updateSkillsGitignore(gitignorePath, existingContent, skills, sym
|
|
|
5463
5475
|
//#endregion
|
|
5464
5476
|
//#region src/commands/setup/run-setup.ts
|
|
5465
5477
|
/**
|
|
5466
|
-
* `holocron setup` — orchestrates per-capability setup actions across
|
|
5478
|
+
* `holocron repo setup` — orchestrates per-capability setup actions across
|
|
5467
5479
|
* every plugin loaded from `holocron.config.json`.
|
|
5468
5480
|
*
|
|
5469
5481
|
* Per CLAUDE.md soft-skip: each step is wrapped in a try/catch and
|
|
@@ -5483,18 +5495,18 @@ async function updateSkillsGitignore(gitignorePath, existingContent, skills, sym
|
|
|
5483
5495
|
*/
|
|
5484
5496
|
const { workflowHeader: workflowHeader$1 } = createHeader({
|
|
5485
5497
|
source: "packages/cli/src/commands/setup/run-setup.ts",
|
|
5486
|
-
tool: "holocron setup"
|
|
5498
|
+
tool: "holocron repo setup"
|
|
5487
5499
|
});
|
|
5488
5500
|
const { workflowHeader: thinCallerHeader } = createHeader({
|
|
5489
5501
|
source: "packages/astromech/src/thin-callers.ts",
|
|
5490
|
-
tool: "holocron setup"
|
|
5502
|
+
tool: "holocron repo setup"
|
|
5491
5503
|
});
|
|
5492
5504
|
async function runSetup(input) {
|
|
5493
5505
|
const print = input.print ?? ((line) => console.log(line));
|
|
5494
5506
|
const logger = getLogger();
|
|
5495
5507
|
const loader = input.loader ?? new PluginLoader(input.loaded.resolved, input.context);
|
|
5496
5508
|
await withSpinner("Loading plugins…", () => loader.load());
|
|
5497
|
-
assertPluginsResolvable(loader, "setup");
|
|
5509
|
+
assertPluginsResolvable(loader, "repo setup");
|
|
5498
5510
|
const config = input.loaded.resolved;
|
|
5499
5511
|
const dryRun = input.context.dryRun ?? false;
|
|
5500
5512
|
const steps = [];
|
|
@@ -6036,6 +6048,17 @@ async function runSetup(input) {
|
|
|
6036
6048
|
print(formatStep(steps[steps.length - 1]));
|
|
6037
6049
|
}
|
|
6038
6050
|
}
|
|
6051
|
+
if (loader.has("monitoring")) {
|
|
6052
|
+
const monitoring = loader.get("monitoring");
|
|
6053
|
+
print(style.step("monitoring"));
|
|
6054
|
+
for (const checkInput of monitoring.checks) {
|
|
6055
|
+
steps.push(await runStep("monitoring", `ensureCheck ${checkInput.name}`, dryRun, async () => {
|
|
6056
|
+
const result = await monitoring.ensureCheck(checkInput);
|
|
6057
|
+
return `${checkInput.name} ${result.alreadyExists ? "exists" : "created"}`;
|
|
6058
|
+
}));
|
|
6059
|
+
print(formatStep(steps[steps.length - 1]));
|
|
6060
|
+
}
|
|
6061
|
+
}
|
|
6039
6062
|
if (loader.has("tooling")) {
|
|
6040
6063
|
const tools = loader.get("tooling");
|
|
6041
6064
|
print(style.step("tooling"));
|
|
@@ -6122,7 +6145,7 @@ async function runSetup(input) {
|
|
|
6122
6145
|
print(style.hint(" · Metadata — read (repository permission, auto-included)"));
|
|
6123
6146
|
print("");
|
|
6124
6147
|
print(style.hint(" Create tokens at: https://github.com/settings/personal-access-tokens/new"));
|
|
6125
|
-
print(style.hint(" Then re-run: holocron setup --token <your-admin-pat>"));
|
|
6148
|
+
print(style.hint(" Then re-run: holocron repo setup --token <your-admin-pat>"));
|
|
6126
6149
|
print(style.hint(" Store org token: HOLOCRON_ORG_TOKEN env var or keyring key github.org"));
|
|
6127
6150
|
}
|
|
6128
6151
|
return {
|
|
@@ -6160,7 +6183,7 @@ async function ensurePrepareScript(repoRoot) {
|
|
|
6160
6183
|
/**
|
|
6161
6184
|
* `holocron skills` — install, remove, and update agent skills from @theholocron/skills.
|
|
6162
6185
|
*
|
|
6163
|
-
* Unlike `holocron setup`, this command is purely local (no GitHub token
|
|
6186
|
+
* Unlike `holocron repo setup`, this command is purely local (no GitHub token
|
|
6164
6187
|
* required). It reads `agent` and `skills` from the config and installs
|
|
6165
6188
|
* the listed skills into `.agents/skills/<name>/` with a symlink at the
|
|
6166
6189
|
* agent-specific path (e.g. `.claude/skills/<name>` for Claude Code).
|
|
@@ -6627,7 +6650,7 @@ async function runSyncWiki(input) {
|
|
|
6627
6650
|
//#region src/commands/sync.ts
|
|
6628
6651
|
const { workflowHeader } = createHeader({
|
|
6629
6652
|
source: "packages/astromech/src/thin-callers.ts",
|
|
6630
|
-
tool: "holocron sync"
|
|
6653
|
+
tool: "holocron repo sync"
|
|
6631
6654
|
});
|
|
6632
6655
|
const SYNC_STEPS = [
|
|
6633
6656
|
"labels",
|
|
@@ -7363,7 +7386,7 @@ const DEFAULT_REPO = "theholocron/.github";
|
|
|
7363
7386
|
* The file set `sync-github` pushes to `theholocron/.github` — the reusable
|
|
7364
7387
|
* `workflow_call` implementations, composite actions and starter-workflow
|
|
7365
7388
|
* metadata, generated by `@theholocron/astromech`. Secondary repos get nothing:
|
|
7366
|
-
* their thin callers are managed by `holocron sync --steps workflows`, which
|
|
7389
|
+
* their thin callers are managed by `holocron repo sync --steps workflows`, which
|
|
7367
7390
|
* executes the resolved config (regex parsing here can't resolve preset spreads
|
|
7368
7391
|
* like `...tasks` from `nodeDocs()` — see issue #464).
|
|
7369
7392
|
*/
|
|
@@ -7389,7 +7412,7 @@ async function runSyncGithub(input) {
|
|
|
7389
7412
|
token,
|
|
7390
7413
|
fetch: input.fetch
|
|
7391
7414
|
});
|
|
7392
|
-
print(`holocron sync
|
|
7415
|
+
print(`holocron sync github${dryRun ? " (dry-run)" : ""}`);
|
|
7393
7416
|
print(` repo: ${repo}`);
|
|
7394
7417
|
if (branch) print(` branch: ${branch}`);
|
|
7395
7418
|
print("");
|
|
@@ -7581,7 +7604,7 @@ async function runSyncGithub(input) {
|
|
|
7581
7604
|
title: message.split("\n")[0],
|
|
7582
7605
|
head: branch,
|
|
7583
7606
|
base: "main",
|
|
7584
|
-
body: "Auto-generated by `holocron sync
|
|
7607
|
+
body: "Auto-generated by `holocron sync github`. Review and merge to apply template updates."
|
|
7585
7608
|
})).html_url;
|
|
7586
7609
|
print(` → PR opened: ${prUrl}`);
|
|
7587
7610
|
} catch (err) {
|
|
@@ -7599,7 +7622,7 @@ async function runSyncGithub(input) {
|
|
|
7599
7622
|
//#endregion
|
|
7600
7623
|
//#region src/commands/upgrade-deps.ts
|
|
7601
7624
|
/**
|
|
7602
|
-
* `holocron upgrade deps` — bring a repo's `@theholocron/*` dependency pins and
|
|
7625
|
+
* `holocron repo upgrade deps` — bring a repo's `@theholocron/*` dependency pins and
|
|
7603
7626
|
* its `holocron.config.ts` up to the current major.
|
|
7604
7627
|
*
|
|
7605
7628
|
* Two concerns:
|
|
@@ -8191,13 +8214,119 @@ var NonInteractiveError = class extends Error {
|
|
|
8191
8214
|
};
|
|
8192
8215
|
const numeric = (value) => /^\d+$/.test(value) ? true : "enter a number";
|
|
8193
8216
|
/**
|
|
8217
|
+
* This repo's own resolved task manifest, filtered to what's actually
|
|
8218
|
+
* runnable locally — `Astromech.runnableTasks()`, reused rather than
|
|
8219
|
+
* reimplemented (deliberately *not* `packageScripts()`, whose
|
|
8220
|
+
* `syncScripts: false` short-circuit returns `{}` for repos — this one
|
|
8221
|
+
* included — that opt out of generated `package.json` scripts but still
|
|
8222
|
+
* have real runnable tasks). Empty (never throws) when there's no config to
|
|
8223
|
+
* resolve — feeds {@link buildRunMenuEntries}, which renders an empty list
|
|
8224
|
+
* as "run ›" simply having no children.
|
|
8225
|
+
*/
|
|
8226
|
+
async function listRunnableTasks(argv) {
|
|
8227
|
+
const cwd = typeof argv.cwd === "string" ? argv.cwd : process.cwd();
|
|
8228
|
+
const config = await loadTasksConfig(cwd).catch(() => void 0);
|
|
8229
|
+
return createAstromech({
|
|
8230
|
+
cwd,
|
|
8231
|
+
config
|
|
8232
|
+
}).runnableTasks();
|
|
8233
|
+
}
|
|
8234
|
+
/**
|
|
8235
|
+
* Namespace display order for the `run` menu — deliberately its own thing,
|
|
8236
|
+
* not `@theholocron/astromech`'s `CI_ORDER`: that one is tuned for "cheapest
|
|
8237
|
+
* signal first" in an automated gate, a different goal from "how a person
|
|
8238
|
+
* would naturally work through a repo" (build it, clean up its style, check
|
|
8239
|
+
* it's secure, test it, validate it, document it). A namespace absent here
|
|
8240
|
+
* (a repo-specific one this list doesn't know about) sorts after every
|
|
8241
|
+
* named one, in whatever order it was first seen.
|
|
8242
|
+
*/
|
|
8243
|
+
const RUN_MENU_NAMESPACE_ORDER = [
|
|
8244
|
+
"delivery",
|
|
8245
|
+
"sourceQuality",
|
|
8246
|
+
"security",
|
|
8247
|
+
"verification",
|
|
8248
|
+
"platform",
|
|
8249
|
+
"knowledge"
|
|
8250
|
+
];
|
|
8251
|
+
/**
|
|
8252
|
+
* Splits `taskNames` (`"sourceQuality.staticAnalysis"`, …) into namespace
|
|
8253
|
+
* groups, ordered by {@link RUN_MENU_NAMESPACE_ORDER} — insertion order
|
|
8254
|
+
* within each group is `taskNames`' own order (already declaration order,
|
|
8255
|
+
* from {@link listRunnableTasks}). A name with no `.` at all (defensive;
|
|
8256
|
+
* every real registry task has a namespace) is its own bucket, keyed by the
|
|
8257
|
+
* full name, sorted after every namespaced group.
|
|
8258
|
+
*/
|
|
8259
|
+
function groupByNamespace(taskNames) {
|
|
8260
|
+
const groups = /* @__PURE__ */ new Map();
|
|
8261
|
+
for (const name of taskNames) {
|
|
8262
|
+
const dot = name.indexOf(".");
|
|
8263
|
+
const key = dot === -1 ? name : name.slice(0, dot);
|
|
8264
|
+
const list = groups.get(key);
|
|
8265
|
+
if (list) list.push(name);
|
|
8266
|
+
else groups.set(key, [name]);
|
|
8267
|
+
}
|
|
8268
|
+
const ordered = /* @__PURE__ */ new Map();
|
|
8269
|
+
const byMenuOrder = (a, b) => {
|
|
8270
|
+
const ia = RUN_MENU_NAMESPACE_ORDER.indexOf(a);
|
|
8271
|
+
const ib = RUN_MENU_NAMESPACE_ORDER.indexOf(b);
|
|
8272
|
+
if (ia !== -1 || ib !== -1) return (ia === -1 ? Infinity : ia) - (ib === -1 ? Infinity : ib);
|
|
8273
|
+
return 0;
|
|
8274
|
+
};
|
|
8275
|
+
for (const key of [...groups.keys()].sort(byMenuOrder)) ordered.set(key, groups.get(key));
|
|
8276
|
+
return ordered;
|
|
8277
|
+
}
|
|
8278
|
+
/**
|
|
8279
|
+
* Dynamically-built `run` sub-entries for every locally-runnable task
|
|
8280
|
+
* ({@link listRunnableTasks}), grouped by namespace and merged into
|
|
8281
|
+
* `COMMAND_REGISTRY` at `cli.ts`'s `$0` call site — not a registry entry
|
|
8282
|
+
* itself, since the task set is per-repo, not a static surface this module
|
|
8283
|
+
* can hand-enumerate.
|
|
8284
|
+
*
|
|
8285
|
+
* Each namespace becomes a group with a leading "<namespace> (run it)" item
|
|
8286
|
+
* (`holocron run <namespace>` — every task in it, via
|
|
8287
|
+
* `Astromech.runGroup()`) alongside each of its individual tasks, the exact
|
|
8288
|
+
* same "a group that's also runnable" shape `menuItems` already renders for
|
|
8289
|
+
* `repo sync` / `deploy` — reached here for free by using a space, not the
|
|
8290
|
+
* task's own `.`, between namespace and leaf in `name` (`spawnArgs` carries
|
|
8291
|
+
* the real dotted invocation `buildChildArgv` actually spawns).
|
|
8292
|
+
*/
|
|
8293
|
+
function buildRunMenuEntries(taskNames) {
|
|
8294
|
+
const entries = [];
|
|
8295
|
+
for (const [namespace, tasks] of groupByNamespace(taskNames)) {
|
|
8296
|
+
const isRealNamespace = tasks[0] !== namespace || tasks.length > 1;
|
|
8297
|
+
if (isRealNamespace) entries.push({
|
|
8298
|
+
name: `run ${namespace}`,
|
|
8299
|
+
description: `Run every ${namespace}.* task in sequence`,
|
|
8300
|
+
positionals: [],
|
|
8301
|
+
supportsDryRun: true,
|
|
8302
|
+
spawnArgs: ["run", namespace]
|
|
8303
|
+
});
|
|
8304
|
+
for (const task of tasks) {
|
|
8305
|
+
const leaf = task === namespace ? namespace : task.slice(namespace.length + 1);
|
|
8306
|
+
entries.push({
|
|
8307
|
+
name: isRealNamespace ? `run ${namespace} ${leaf}` : `run ${leaf}`,
|
|
8308
|
+
description: `Run ${task}`,
|
|
8309
|
+
positionals: [],
|
|
8310
|
+
supportsDryRun: true,
|
|
8311
|
+
spawnArgs: ["run", task]
|
|
8312
|
+
});
|
|
8313
|
+
}
|
|
8314
|
+
}
|
|
8315
|
+
return entries;
|
|
8316
|
+
}
|
|
8317
|
+
/**
|
|
8194
8318
|
* The full command surface, hand-maintained in parallel with `cli.ts`'s
|
|
8195
8319
|
* yargs registrations (this module does not introspect yargs at runtime).
|
|
8196
|
-
* `run <task>`
|
|
8197
|
-
*
|
|
8198
|
-
*
|
|
8199
|
-
*
|
|
8200
|
-
*
|
|
8320
|
+
* `run <task>` has no entry here at all — `cli.ts`'s `$0` handler merges in
|
|
8321
|
+
* {@link buildRunMenuEntries}'s dynamically-built ones instead, since the
|
|
8322
|
+
* task namespace is this repo's own resolved manifest, not a static set
|
|
8323
|
+
* this module can hand-enumerate the way every other entry's positionals
|
|
8324
|
+
* are. `job` and `passthrough` stay un-prompted either way; picking a task
|
|
8325
|
+
* and running it plain covers the common case, and typing
|
|
8326
|
+
* `holocron run <task> <job> -- <args>` directly is one step away for
|
|
8327
|
+
* everything else. `run lint commit-msg <file>` stays excluded entirely:
|
|
8328
|
+
* it's invoked exclusively from `.husky/commit-msg "$1"`, never typed by a
|
|
8329
|
+
* human.
|
|
8201
8330
|
*/
|
|
8202
8331
|
const COMMAND_REGISTRY = [
|
|
8203
8332
|
{
|
|
@@ -8218,13 +8347,13 @@ const COMMAND_REGISTRY = [
|
|
|
8218
8347
|
supportsDryRun: true
|
|
8219
8348
|
},
|
|
8220
8349
|
{
|
|
8221
|
-
name: "setup",
|
|
8350
|
+
name: "repo setup",
|
|
8222
8351
|
description: "Apply infra setup actions across every configured capability",
|
|
8223
8352
|
positionals: [],
|
|
8224
8353
|
supportsDryRun: true
|
|
8225
8354
|
},
|
|
8226
8355
|
{
|
|
8227
|
-
name: "
|
|
8356
|
+
name: "secrets set",
|
|
8228
8357
|
description: "Set a single secret via the configured `secrets` capability",
|
|
8229
8358
|
positionals: [{
|
|
8230
8359
|
key: "name",
|
|
@@ -8254,7 +8383,7 @@ const COMMAND_REGISTRY = [
|
|
|
8254
8383
|
supportsDryRun: true
|
|
8255
8384
|
},
|
|
8256
8385
|
{
|
|
8257
|
-
name: "cleanup-preview",
|
|
8386
|
+
name: "deploy cleanup-preview",
|
|
8258
8387
|
description: "List and delete Cloudflare Pages preview deployments for a GitHub PR",
|
|
8259
8388
|
positionals: [{
|
|
8260
8389
|
key: "pr",
|
|
@@ -8265,7 +8394,13 @@ const COMMAND_REGISTRY = [
|
|
|
8265
8394
|
supportsDryRun: true
|
|
8266
8395
|
},
|
|
8267
8396
|
{
|
|
8268
|
-
name: "
|
|
8397
|
+
name: "deploy on-release",
|
|
8398
|
+
description: "Deploy every workspace package whose delivery.deploy task is `on: release`",
|
|
8399
|
+
positionals: [],
|
|
8400
|
+
supportsDryRun: true
|
|
8401
|
+
},
|
|
8402
|
+
{
|
|
8403
|
+
name: "package bump-versions",
|
|
8269
8404
|
description: "Bump all non-private package versions in lockstep (semantic-release prepareCmd)",
|
|
8270
8405
|
positionals: [{
|
|
8271
8406
|
key: "newVersion",
|
|
@@ -8276,13 +8411,13 @@ const COMMAND_REGISTRY = [
|
|
|
8276
8411
|
supportsDryRun: true
|
|
8277
8412
|
},
|
|
8278
8413
|
{
|
|
8279
|
-
name: "publish",
|
|
8414
|
+
name: "package publish",
|
|
8280
8415
|
description: "Publish @theholocron/* packages to npm",
|
|
8281
8416
|
positionals: [],
|
|
8282
8417
|
supportsDryRun: true
|
|
8283
8418
|
},
|
|
8284
8419
|
{
|
|
8285
|
-
name: "sync",
|
|
8420
|
+
name: "repo sync",
|
|
8286
8421
|
description: "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)",
|
|
8287
8422
|
positionals: [],
|
|
8288
8423
|
supportsDryRun: true
|
|
@@ -8294,13 +8429,13 @@ const COMMAND_REGISTRY = [
|
|
|
8294
8429
|
supportsDryRun: true
|
|
8295
8430
|
},
|
|
8296
8431
|
{
|
|
8297
|
-
name: "sync
|
|
8432
|
+
name: "sync github",
|
|
8298
8433
|
description: "Sync workflow templates and composite actions to theholocron/.github",
|
|
8299
8434
|
positionals: [],
|
|
8300
8435
|
supportsDryRun: true
|
|
8301
8436
|
},
|
|
8302
8437
|
{
|
|
8303
|
-
name: "sync
|
|
8438
|
+
name: "repo sync readme",
|
|
8304
8439
|
description: "Sync the Installation + Usage block in README.md from package.json",
|
|
8305
8440
|
positionals: [],
|
|
8306
8441
|
supportsDryRun: true
|
|
@@ -8311,13 +8446,13 @@ const COMMAND_REGISTRY = [
|
|
|
8311
8446
|
positionals: []
|
|
8312
8447
|
},
|
|
8313
8448
|
{
|
|
8314
|
-
name: "new",
|
|
8449
|
+
name: "new repo",
|
|
8315
8450
|
description: "Scaffold a new repo from a GitHub template (e.g. cli, react, nextjs, node, monorepo, base)",
|
|
8316
8451
|
positionals: [],
|
|
8317
8452
|
supportsDryRun: true
|
|
8318
8453
|
},
|
|
8319
8454
|
{
|
|
8320
|
-
name: "plugin
|
|
8455
|
+
name: "new plugin",
|
|
8321
8456
|
description: "Scaffold a new @theholocron/holocron-plugin-<slug> package",
|
|
8322
8457
|
positionals: [{
|
|
8323
8458
|
key: "slug",
|
|
@@ -8334,25 +8469,22 @@ const COMMAND_REGISTRY = [
|
|
|
8334
8469
|
name: "skills install",
|
|
8335
8470
|
description: "Copy skills from @theholocron/skills into .agents/ with agent symlinks",
|
|
8336
8471
|
positionals: [],
|
|
8337
|
-
group: "skills",
|
|
8338
8472
|
supportsDryRun: true
|
|
8339
8473
|
},
|
|
8340
8474
|
{
|
|
8341
8475
|
name: "skills remove",
|
|
8342
8476
|
description: "Remove installed skills via npx skills remove",
|
|
8343
8477
|
positionals: [],
|
|
8344
|
-
group: "skills",
|
|
8345
8478
|
supportsDryRun: true
|
|
8346
8479
|
},
|
|
8347
8480
|
{
|
|
8348
8481
|
name: "skills update",
|
|
8349
8482
|
description: "Update installed skills to their latest upstream versions via npx skills update",
|
|
8350
8483
|
positionals: [],
|
|
8351
|
-
group: "skills",
|
|
8352
8484
|
supportsDryRun: true
|
|
8353
8485
|
},
|
|
8354
8486
|
{
|
|
8355
|
-
name: "upgrade node",
|
|
8487
|
+
name: "repo upgrade node",
|
|
8356
8488
|
description: "Scan the repo and update every Node.js version pin to a new major",
|
|
8357
8489
|
positionals: [{
|
|
8358
8490
|
key: "to",
|
|
@@ -8360,14 +8492,12 @@ const COMMAND_REGISTRY = [
|
|
|
8360
8492
|
type: "input",
|
|
8361
8493
|
validate: numeric
|
|
8362
8494
|
}],
|
|
8363
|
-
group: "upgrade",
|
|
8364
8495
|
supportsDryRun: true
|
|
8365
8496
|
},
|
|
8366
8497
|
{
|
|
8367
|
-
name: "upgrade deps",
|
|
8498
|
+
name: "repo upgrade deps",
|
|
8368
8499
|
description: "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API",
|
|
8369
8500
|
positionals: [],
|
|
8370
|
-
group: "upgrade",
|
|
8371
8501
|
supportsDryRun: true
|
|
8372
8502
|
},
|
|
8373
8503
|
{
|
|
@@ -8377,8 +8507,7 @@ const COMMAND_REGISTRY = [
|
|
|
8377
8507
|
key: "provider",
|
|
8378
8508
|
message: "Provider name:",
|
|
8379
8509
|
type: "input"
|
|
8380
|
-
}]
|
|
8381
|
-
group: "auth"
|
|
8510
|
+
}]
|
|
8382
8511
|
},
|
|
8383
8512
|
{
|
|
8384
8513
|
name: "auth unset",
|
|
@@ -8388,8 +8517,7 @@ const COMMAND_REGISTRY = [
|
|
|
8388
8517
|
message: "Provider:",
|
|
8389
8518
|
type: "select",
|
|
8390
8519
|
choices: listStoredProviders
|
|
8391
|
-
}]
|
|
8392
|
-
group: "auth"
|
|
8520
|
+
}]
|
|
8393
8521
|
},
|
|
8394
8522
|
{
|
|
8395
8523
|
name: "auth check",
|
|
@@ -8399,14 +8527,12 @@ const COMMAND_REGISTRY = [
|
|
|
8399
8527
|
message: "Provider:",
|
|
8400
8528
|
type: "select",
|
|
8401
8529
|
choices: listStoredProviders
|
|
8402
|
-
}]
|
|
8403
|
-
group: "auth"
|
|
8530
|
+
}]
|
|
8404
8531
|
},
|
|
8405
8532
|
{
|
|
8406
8533
|
name: "auth list",
|
|
8407
8534
|
description: "List every provider with a stored bootstrap token",
|
|
8408
|
-
positionals: []
|
|
8409
|
-
group: "auth"
|
|
8535
|
+
positionals: []
|
|
8410
8536
|
}
|
|
8411
8537
|
];
|
|
8412
8538
|
/** Look up a {@link CommandEntry} by its full name — for Layer 3 call sites in `cli.ts`. */
|
|
@@ -8415,36 +8541,89 @@ function getEntry(name) {
|
|
|
8415
8541
|
if (!entry) throw new Error(`interactive-menu: no COMMAND_REGISTRY entry named "${name}"`);
|
|
8416
8542
|
return entry;
|
|
8417
8543
|
}
|
|
8544
|
+
const GROUP = "group:";
|
|
8545
|
+
/** The "← back" item's value; only offered below the first level. */
|
|
8546
|
+
const BACK = "back";
|
|
8547
|
+
/**
|
|
8548
|
+
* One level of the menu, derived from the registered command names: the next word
|
|
8549
|
+
* of every entry under `path` (`[]` is the top). A word is
|
|
8550
|
+
*
|
|
8551
|
+
* - a **command** when nothing is nested under it (`doctor`);
|
|
8552
|
+
* - a **group** when commands are nested under it (`repo` → `repo setup`, `repo sync`, …),
|
|
8553
|
+
* which can nest as deep as the names go (`repo upgrade` → `repo upgrade node`);
|
|
8554
|
+
*
|
|
8555
|
+
* Every row is a single word, never a "<command> <subcommand>" path: `config show` is
|
|
8556
|
+
* `config ›` then `show`, even when a group holds just one command.
|
|
8557
|
+
*
|
|
8558
|
+
* A path that is itself runnable (`repo sync`, `deploy`) gets a leading "<word> (run it)" item.
|
|
8559
|
+
* Registration order is kept.
|
|
8560
|
+
*/
|
|
8561
|
+
function menuItems(entries, path = []) {
|
|
8562
|
+
const prefix = path.join(" ");
|
|
8563
|
+
const own = prefix ? entries.find((e) => e.name === prefix) : void 0;
|
|
8564
|
+
const under = prefix ? entries.filter((e) => e.name.startsWith(`${prefix} `)) : entries;
|
|
8565
|
+
const haystackOf = (list) => list.map((e) => `${e.name} ${e.description}`.toLowerCase()).join(" ");
|
|
8566
|
+
const items = [];
|
|
8567
|
+
if (own) items.push({
|
|
8568
|
+
name: `${path[path.length - 1]} (run it)`,
|
|
8569
|
+
value: own.name,
|
|
8570
|
+
description: own.description,
|
|
8571
|
+
haystack: haystackOf([own])
|
|
8572
|
+
});
|
|
8573
|
+
const words = [...new Set(under.map((e) => e.name.split(" ")[path.length]))];
|
|
8574
|
+
for (const word of words) {
|
|
8575
|
+
const full = [...path, word].join(" ");
|
|
8576
|
+
const family = under.filter((e) => e.name === full || e.name.startsWith(`${full} `));
|
|
8577
|
+
const self = family.find((e) => e.name === full);
|
|
8578
|
+
const nested = family.filter((e) => e.name !== full);
|
|
8579
|
+
if (nested.length === 0 && self) items.push({
|
|
8580
|
+
name: word,
|
|
8581
|
+
value: full,
|
|
8582
|
+
description: self.description,
|
|
8583
|
+
haystack: haystackOf([self])
|
|
8584
|
+
});
|
|
8585
|
+
else {
|
|
8586
|
+
const children = [...new Set(nested.map((e) => e.name.split(" ")[path.length + 1]))];
|
|
8587
|
+
items.push({
|
|
8588
|
+
name: `${word} ›`,
|
|
8589
|
+
value: `${GROUP}${full}`,
|
|
8590
|
+
description: children.join(", "),
|
|
8591
|
+
haystack: haystackOf(family)
|
|
8592
|
+
});
|
|
8593
|
+
}
|
|
8594
|
+
}
|
|
8595
|
+
return items;
|
|
8596
|
+
}
|
|
8418
8597
|
/**
|
|
8419
|
-
* Searchable autocomplete over
|
|
8420
|
-
*
|
|
8421
|
-
*
|
|
8422
|
-
* gracefully as the surface grows and costs nothing when it doesn't.
|
|
8598
|
+
* Searchable autocomplete over one menu level's `items`. `select()` would work too
|
|
8599
|
+
* at these list sizes, but `search()` degrades gracefully as the surface grows and
|
|
8600
|
+
* costs nothing when it doesn't. Resolves to the picked item's `value`.
|
|
8423
8601
|
*/
|
|
8424
|
-
async function
|
|
8425
|
-
|
|
8426
|
-
const picked = await search({
|
|
8602
|
+
async function pickMenuItem(items, message = "What would you like to do?") {
|
|
8603
|
+
return search({
|
|
8427
8604
|
message,
|
|
8428
|
-
source: (term) => searchChoices(
|
|
8605
|
+
source: (term) => searchChoices(items, term)
|
|
8429
8606
|
});
|
|
8430
|
-
return byName.get(picked);
|
|
8431
8607
|
}
|
|
8432
8608
|
/**
|
|
8433
8609
|
* `@inquirer/search`'s `source` callback, factored out as a plain function —
|
|
8434
8610
|
* unit-testable directly instead of only through a mocked `search()` call.
|
|
8435
|
-
* Empty/undefined `term` (nothing typed yet) returns every
|
|
8611
|
+
* Empty/undefined `term` (nothing typed yet) returns every item.
|
|
8436
8612
|
*/
|
|
8437
|
-
function searchChoices(
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8613
|
+
function searchChoices(items, term) {
|
|
8614
|
+
const needle = term?.toLowerCase();
|
|
8615
|
+
const pool = !needle ? items : items.filter((i) => i.haystack.includes(needle));
|
|
8616
|
+
const rank = (i) => {
|
|
8617
|
+
if (!needle) return 0;
|
|
8618
|
+
const name = i.name.toLowerCase();
|
|
8619
|
+
return name.startsWith(needle) ? 0 : name.includes(needle) ? 1 : 2;
|
|
8620
|
+
};
|
|
8621
|
+
return [...pool].sort((a, b) => rank(a) - rank(b)).map((i) => ({
|
|
8622
|
+
name: i.name,
|
|
8623
|
+
value: i.value,
|
|
8624
|
+
description: i.description
|
|
8442
8625
|
}));
|
|
8443
8626
|
}
|
|
8444
|
-
function matches(entry, term) {
|
|
8445
|
-
const needle = term.toLowerCase();
|
|
8446
|
-
return entry.name.toLowerCase().includes(needle) || entry.description.toLowerCase().includes(needle);
|
|
8447
|
-
}
|
|
8448
8627
|
/**
|
|
8449
8628
|
* For each of `entry.positionals` not already present in `argv`, prompts for
|
|
8450
8629
|
* it (`input()` or `select()`, per the positional's `type`) and returns the
|
|
@@ -8498,10 +8677,10 @@ function forwardedFlags(argv, dryRunOverride) {
|
|
|
8498
8677
|
if (dryRunOverride ?? argv.dryRun === true) out.push("--dry-run");
|
|
8499
8678
|
return out;
|
|
8500
8679
|
}
|
|
8501
|
-
/** `entry.name` split into tokens
|
|
8680
|
+
/** `entry.spawnArgs` if set, else `entry.name` split into tokens — resolved positionals appended, then forwarded flags. */
|
|
8502
8681
|
function buildChildArgv(entry, positionals, parentArgv, dryRunOverride) {
|
|
8503
8682
|
return [
|
|
8504
|
-
...entry.name.split(" "),
|
|
8683
|
+
...entry.spawnArgs ?? entry.name.split(" "),
|
|
8505
8684
|
...positionals,
|
|
8506
8685
|
...forwardedFlags(parentArgv, dryRunOverride)
|
|
8507
8686
|
];
|
|
@@ -8554,14 +8733,17 @@ function shouldPreviewDryRun(entry, parentArgv) {
|
|
|
8554
8733
|
return Boolean(entry.supportsDryRun) && parentArgv.skipDryRun !== true;
|
|
8555
8734
|
}
|
|
8556
8735
|
/**
|
|
8557
|
-
* Layers 1 and 2:
|
|
8558
|
-
*
|
|
8559
|
-
* `process.exitCode` from the child —
|
|
8560
|
-
* `cli.ts`) returns normally afterward so the
|
|
8561
|
-
* flush / update-notifier tail still runs. The parent's
|
|
8562
|
-
* `command_completed` event fires with command name "unknown" (the
|
|
8563
|
-
*
|
|
8564
|
-
*
|
|
8736
|
+
* Layers 1 and 2: walk the nested menu over `entries` (starting at `startPath`,
|
|
8737
|
+
* `[]` for the top), prompt for the picked command's required positionals, spawn
|
|
8738
|
+
* it as a fresh `holocron` invocation, and set `process.exitCode` from the child —
|
|
8739
|
+
* the caller (a `$0` handler in `cli.ts`) returns normally afterward so the
|
|
8740
|
+
* parent's own telemetry flush / update-notifier tail still runs. The parent's
|
|
8741
|
+
* own `command_completed` event fires with command name "unknown" (the middleware
|
|
8742
|
+
* ran before any command was picked) — left as-is rather than suppressed; it's a
|
|
8743
|
+
* real, useful signal ("the menu got used").
|
|
8744
|
+
*
|
|
8745
|
+
* Picking a group opens its submenu, which has a "← back" item; a command ends
|
|
8746
|
+
* the walk. See {@link menuItems} for how a level is built.
|
|
8565
8747
|
*
|
|
8566
8748
|
* For a command that honours `--dry-run` ({@link CommandEntry.supportsDryRun})
|
|
8567
8749
|
* and wasn't opted out via `--skip-dry-run` ({@link shouldPreviewDryRun}),
|
|
@@ -8573,9 +8755,24 @@ function shouldPreviewDryRun(entry, parentArgv) {
|
|
|
8573
8755
|
* with the real run?" before the real spawn — the one actual decision
|
|
8574
8756
|
* left to make, since this one does mutate something.
|
|
8575
8757
|
*/
|
|
8576
|
-
async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessage = "Run `holocron --help` to see available commands.") {
|
|
8758
|
+
async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessage = "Run `holocron --help` to see available commands.", startPath = []) {
|
|
8577
8759
|
if (!process.stdin.isTTY) throw new NonInteractiveError(nonInteractiveMessage);
|
|
8578
|
-
const
|
|
8760
|
+
const stack = [startPath];
|
|
8761
|
+
let picked;
|
|
8762
|
+
while (!picked) {
|
|
8763
|
+
const path = stack[stack.length - 1];
|
|
8764
|
+
const items = menuItems(entries, path);
|
|
8765
|
+
if (stack.length > 1) items.push({
|
|
8766
|
+
name: "← back",
|
|
8767
|
+
value: BACK,
|
|
8768
|
+
description: "Return to the previous menu",
|
|
8769
|
+
haystack: "back"
|
|
8770
|
+
});
|
|
8771
|
+
const value = await pickMenuItem(items, stack.length === 1 ? pickMessage : `${path.join(" ")} — choose a subcommand:`);
|
|
8772
|
+
if (value === "back") stack.pop();
|
|
8773
|
+
else if (value.startsWith(GROUP)) stack.push(value.slice(6).split(" "));
|
|
8774
|
+
else picked = entries.find((e) => e.name === value);
|
|
8775
|
+
}
|
|
8579
8776
|
const positionals = await promptForPositionals(picked, {});
|
|
8580
8777
|
if (shouldPreviewDryRun(picked, parentArgv)) {
|
|
8581
8778
|
const dryExit = await spawnChild(buildChildArgv(picked, positionals, parentArgv, true), { [DRY_RUN_PREVIEW_ENV_VAR]: "1" });
|
|
@@ -9048,6 +9245,10 @@ function tokenContext(rawTokens) {
|
|
|
9048
9245
|
throw err;
|
|
9049
9246
|
}
|
|
9050
9247
|
}
|
|
9248
|
+
/** Layer 2: a bare group command (`holocron repo`, `holocron repo upgrade`, …) opens the menu at that level. */
|
|
9249
|
+
async function launchGroupMenu(parent, argv) {
|
|
9250
|
+
await launchMenu(COMMAND_REGISTRY, argv, `${parent} — choose a subcommand:`, `Run \`holocron ${parent} --help\` to see available ${parent} subcommands.`, parent.split(" "));
|
|
9251
|
+
}
|
|
9051
9252
|
init(CLI_VERSION);
|
|
9052
9253
|
const updateCheckPromise = checkForUpdates(CLI_VERSION);
|
|
9053
9254
|
let finishCommand = () => {};
|
|
@@ -9090,7 +9291,7 @@ try {
|
|
|
9090
9291
|
choices: LOG_LEVELS,
|
|
9091
9292
|
describe: "Show structured logger output at this level. Local/interactive runs are quiet by default (no console output regardless of level) — this opts back in."
|
|
9092
9293
|
}).middleware((argv) => {
|
|
9093
|
-
const name = argv._.slice(0,
|
|
9294
|
+
const name = argv._.slice(0, 3).join(" ") || "unknown";
|
|
9094
9295
|
printRunId = Boolean(argv.debug || argv.verbose);
|
|
9095
9296
|
buildCliLogger(argv, {
|
|
9096
9297
|
command: name,
|
|
@@ -9161,28 +9362,6 @@ try {
|
|
|
9161
9362
|
org: resolveOrg(argv, loaded.resolved)
|
|
9162
9363
|
}
|
|
9163
9364
|
})).summary.fail > 0) process.exitCode = 1;
|
|
9164
|
-
}).command("setup", "Apply infra setup actions across every configured capability", (y) => y.option("repo", {
|
|
9165
|
-
type: "string",
|
|
9166
|
-
describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
|
|
9167
|
-
}).option("hooks", {
|
|
9168
|
-
type: "boolean",
|
|
9169
|
-
describe: "Install git hooks (.husky/pre-push → holocron run ci). Default: on for protection:'strict'. --no-hooks to skip."
|
|
9170
|
-
}), async (argv) => {
|
|
9171
|
-
const tokens = tokenContext(argv.token);
|
|
9172
|
-
if (!tokens) return;
|
|
9173
|
-
const loaded = await loadConfig(argv.cwd);
|
|
9174
|
-
applyResolvedConfig(argv, loaded.resolved);
|
|
9175
|
-
if ((await runSetup({
|
|
9176
|
-
loaded,
|
|
9177
|
-
context: {
|
|
9178
|
-
repoRoot: argv.cwd,
|
|
9179
|
-
dryRun: argv.dryRun,
|
|
9180
|
-
...argv.repo ? { repo: argv.repo } : {},
|
|
9181
|
-
...tokens,
|
|
9182
|
-
org: resolveOrg(argv, loaded.resolved)
|
|
9183
|
-
},
|
|
9184
|
-
...argv.hooks !== void 0 ? { hooks: argv.hooks } : {}
|
|
9185
|
-
})).summary.fail > 0) process.exitCode = 1;
|
|
9186
9365
|
}).command("skills", "Manage agent skills from the @theholocron/skills registry", (y) => y.command("install", "Copy skills from @theholocron/skills into .agents/ with agent symlinks", () => {}, async (argv) => {
|
|
9187
9366
|
await runSkillsInstall({
|
|
9188
9367
|
loaded: await loadConfig(argv.cwd),
|
|
@@ -9215,399 +9394,214 @@ try {
|
|
|
9215
9394
|
...argv.name ? { name: argv.name } : {}
|
|
9216
9395
|
}).status === "fail") process.exitCode = 1;
|
|
9217
9396
|
}).command("$0", false, () => {}, async (argv) => {
|
|
9218
|
-
await
|
|
9219
|
-
}), () => {}).command("
|
|
9397
|
+
await launchGroupMenu("skills", argv);
|
|
9398
|
+
}), () => {}).command("run <task> [job] [passthrough..]", "Run a task locally (test, typecheck, lint, build, audit) — figures out turbo / the tool / the package manager. `run ci` runs every merge-gating check; `run lint commit-msg <file>` lints a commit message.", (y) => y.positional("task", {
|
|
9220
9399
|
type: "string",
|
|
9221
|
-
|
|
9222
|
-
|
|
9400
|
+
demandOption: true,
|
|
9401
|
+
describe: "Task name (test, typecheck, lint, build, audit, or `ci` for the merge-gating suite). See `holocron run --help`."
|
|
9402
|
+
}).positional("job", {
|
|
9223
9403
|
type: "string",
|
|
9224
|
-
describe: "
|
|
9225
|
-
}).
|
|
9404
|
+
describe: "Sub-job within the task (e.g. `holocron run audit performance`, `holocron run lint commit-msg <file>`). Omit to run every job."
|
|
9405
|
+
}).positional("passthrough", {
|
|
9406
|
+
type: "string",
|
|
9407
|
+
array: true,
|
|
9408
|
+
describe: "Args forwarded to the tool — put them after `--`."
|
|
9409
|
+
}).option("required", {
|
|
9226
9410
|
type: "boolean",
|
|
9227
9411
|
default: false,
|
|
9228
|
-
describe: "
|
|
9229
|
-
}).option("
|
|
9412
|
+
describe: "Fail (exit 1) if this repo has no such task, instead of skipping."
|
|
9413
|
+
}).option("all", {
|
|
9414
|
+
type: "boolean",
|
|
9415
|
+
default: false,
|
|
9416
|
+
describe: "With `run ci`: run every `ci: true` task, not just the required ones."
|
|
9417
|
+
}).option("filter", {
|
|
9230
9418
|
type: "string",
|
|
9231
|
-
describe: "
|
|
9232
|
-
})
|
|
9419
|
+
describe: "turbo --filter=<pkg> passthrough (monorepo)."
|
|
9420
|
+
}), async (argv) => {
|
|
9421
|
+
const { logger } = buildCliLogger(argv, { command: argv.task === "ci" ? "run ci" : argv.task === "lint" && argv.job === "commit-msg" ? "run lint commit-msg" : "run" });
|
|
9422
|
+
const passthrough = [...argv.passthrough ?? [], ...argv["--"] ?? []];
|
|
9423
|
+
if (argv.task === "lint" && argv.job === "commit-msg") {
|
|
9424
|
+
const file = passthrough[0];
|
|
9425
|
+
if (!file) {
|
|
9426
|
+
reportError("run lint commit-msg: missing <file> (the commit message file git passes as $1).");
|
|
9427
|
+
process.exitCode = 1;
|
|
9428
|
+
return;
|
|
9429
|
+
}
|
|
9430
|
+
const result = await lintCommitMsgFile(file, argv.cwd);
|
|
9431
|
+
if (!result.valid) {
|
|
9432
|
+
for (const violation of result.violations) console.error(`✖ ${violation.rule}: ${violation.message}`);
|
|
9433
|
+
process.exitCode = 1;
|
|
9434
|
+
}
|
|
9435
|
+
return;
|
|
9436
|
+
}
|
|
9437
|
+
if (argv.task === "ci") {
|
|
9438
|
+
const ciConfig = await loadTasksConfig(argv.cwd).catch(() => void 0);
|
|
9439
|
+
if (createAstromech({
|
|
9440
|
+
cwd: argv.cwd,
|
|
9441
|
+
logger,
|
|
9442
|
+
config: ciConfig
|
|
9443
|
+
}).ci({
|
|
9444
|
+
dryRun: argv.dryRun,
|
|
9445
|
+
scope: argv.all ? "all" : "required",
|
|
9446
|
+
...argv.filter ? { filter: argv.filter } : {}
|
|
9447
|
+
}).status === "fail") process.exitCode = 1;
|
|
9448
|
+
return;
|
|
9449
|
+
}
|
|
9450
|
+
const config = await loadTasksConfig(argv.cwd).catch(() => void 0);
|
|
9451
|
+
const report = createAstromech({
|
|
9452
|
+
cwd: argv.cwd,
|
|
9453
|
+
logger,
|
|
9454
|
+
config
|
|
9455
|
+
}).run(argv.task, {
|
|
9456
|
+
...argv.job !== void 0 ? { job: argv.job } : {},
|
|
9457
|
+
passthrough,
|
|
9458
|
+
dryRun: argv.dryRun,
|
|
9459
|
+
required: argv.required,
|
|
9460
|
+
...argv.filter ? { filter: argv.filter } : {}
|
|
9461
|
+
});
|
|
9462
|
+
if (report.status === "fail" || report.status === "unknown") process.exitCode = 1;
|
|
9463
|
+
}).command("config show", "Print the resolved holocron config", (y) => y.option("depth", {
|
|
9464
|
+
type: "number",
|
|
9465
|
+
describe: "Collapse objects/arrays nested deeper than N levels (TTY output only)"
|
|
9466
|
+
}).option("interactive", {
|
|
9467
|
+
type: "boolean",
|
|
9468
|
+
describe: "Browse the config in a foldable tree (default on a TTY; --no-interactive for static output)"
|
|
9469
|
+
}), async (argv) => {
|
|
9470
|
+
await viewJson((await loadConfig(argv.cwd)).resolved, {
|
|
9471
|
+
depth: argv.depth,
|
|
9472
|
+
interactive: argv.interactive
|
|
9473
|
+
});
|
|
9474
|
+
}).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) => {
|
|
9475
|
+
const [provider] = await promptForPositionals(getEntry("auth set"), argv);
|
|
9476
|
+
if ((await runAuthSet({
|
|
9477
|
+
provider,
|
|
9478
|
+
...argv.value ? { positional: argv.value } : {},
|
|
9479
|
+
...argv.org ? { org: argv.org } : {}
|
|
9480
|
+
})).status === "fail") process.exitCode = 1;
|
|
9481
|
+
}).command("unset [provider]", "Remove a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
|
|
9482
|
+
const [provider] = await promptForPositionals(getEntry("auth unset"), argv);
|
|
9483
|
+
runAuthUnset({ provider });
|
|
9484
|
+
}).command("check [provider]", "Re-verify a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
|
|
9485
|
+
const [provider] = await promptForPositionals(getEntry("auth check"), argv);
|
|
9486
|
+
if ((await runAuthCheck({
|
|
9487
|
+
provider,
|
|
9488
|
+
...argv.org ? { org: argv.org } : {}
|
|
9489
|
+
})).status === "fail") process.exitCode = 1;
|
|
9490
|
+
}).command("list", "List every provider with a stored bootstrap token", () => {}, async () => {
|
|
9491
|
+
await runAuthList();
|
|
9492
|
+
}).command("$0", false, () => {}, async (argv) => {
|
|
9493
|
+
await launchGroupMenu("auth", argv);
|
|
9494
|
+
}), () => {}).command("$0", false, () => {}, async (argv) => {
|
|
9495
|
+
printVersionHeaderOnce();
|
|
9496
|
+
const runEntries = buildRunMenuEntries(await listRunnableTasks(argv));
|
|
9497
|
+
await launchMenu([...COMMAND_REGISTRY, ...runEntries], argv);
|
|
9498
|
+
}).command("repo", "Set up, sync and upgrade the current repo", (g) => g.command("setup", "Apply infra setup actions across every configured capability", (y) => y.option("repo", {
|
|
9233
9499
|
type: "string",
|
|
9234
|
-
|
|
9235
|
-
|
|
9500
|
+
describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
|
|
9501
|
+
}).option("hooks", {
|
|
9502
|
+
type: "boolean",
|
|
9503
|
+
describe: "Install git hooks (.husky/pre-push → holocron run ci). Default: on for protection:'strict'. --no-hooks to skip."
|
|
9236
9504
|
}), async (argv) => {
|
|
9237
9505
|
const tokens = tokenContext(argv.token);
|
|
9238
9506
|
if (!tokens) return;
|
|
9239
|
-
const [name] = await promptForPositionals(getEntry("secret set"), argv);
|
|
9240
|
-
const scopeArg = argv.scope;
|
|
9241
|
-
const scope = parseScope(scopeArg);
|
|
9242
9507
|
const loaded = await loadConfig(argv.cwd);
|
|
9243
9508
|
applyResolvedConfig(argv, loaded.resolved);
|
|
9244
|
-
if ((await
|
|
9509
|
+
if ((await runSetup({
|
|
9245
9510
|
loaded,
|
|
9246
9511
|
context: {
|
|
9247
9512
|
repoRoot: argv.cwd,
|
|
9248
9513
|
dryRun: argv.dryRun,
|
|
9514
|
+
...argv.repo ? { repo: argv.repo } : {},
|
|
9249
9515
|
...tokens,
|
|
9250
9516
|
org: resolveOrg(argv, loaded.resolved)
|
|
9251
9517
|
},
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
|
|
9256
|
-
|
|
9257
|
-
|
|
9258
|
-
})
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9518
|
+
...argv.hooks !== void 0 ? { hooks: argv.hooks } : {}
|
|
9519
|
+
})).summary.fail > 0) process.exitCode = 1;
|
|
9520
|
+
}).command("sync [steps..]", "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)", (y) => y.command("readme", "Sync the Installation + Usage block in README.md from package.json", (y) => y.option("dry-run", {
|
|
9521
|
+
type: "boolean",
|
|
9522
|
+
describe: "Print what would change without writing",
|
|
9523
|
+
default: false
|
|
9524
|
+
}), async (argv) => {
|
|
9525
|
+
const loaded = await loadConfig(argv.cwd);
|
|
9526
|
+
applyResolvedConfig(argv, loaded.resolved);
|
|
9527
|
+
const report = await runSyncReadme({
|
|
9528
|
+
loaded,
|
|
9529
|
+
context: {
|
|
9530
|
+
repoRoot: argv.cwd,
|
|
9531
|
+
dryRun: argv.dryRun
|
|
9532
|
+
}
|
|
9533
|
+
});
|
|
9534
|
+
if (report.status === "fail") {
|
|
9535
|
+
if (report.message) reportError(`repo sync readme: ${report.message}`);
|
|
9536
|
+
process.exitCode = 1;
|
|
9537
|
+
}
|
|
9538
|
+
}).positional("steps", {
|
|
9262
9539
|
type: "string",
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
default: ["production", "preview"],
|
|
9267
|
-
describe: "Deployment targets to sync to. Defaults to production + preview."
|
|
9268
|
-
}).option("github-secret", {
|
|
9269
|
-
type: "array",
|
|
9270
|
-
describe: "Vault key(s) to push to GH Actions secrets (repeatable). Omit to push every key."
|
|
9271
|
-
}).option("github-secret-scope", {
|
|
9540
|
+
array: true,
|
|
9541
|
+
describe: "Steps to run: labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki (default: all)"
|
|
9542
|
+
}).option("repo", {
|
|
9272
9543
|
type: "string",
|
|
9273
|
-
|
|
9274
|
-
describe: "Scope applied to every --github-secret key: \"repo\" (default), \"env=<name>\", or \"org=<name>\"."
|
|
9544
|
+
describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
|
|
9275
9545
|
}), async (argv) => {
|
|
9276
9546
|
const tokens = tokenContext(argv.token);
|
|
9277
9547
|
if (!tokens) return;
|
|
9278
|
-
const githubSecretScope = parseScope(argv.githubSecretScope);
|
|
9279
|
-
const [environmentId] = await promptForPositionals(getEntry("secrets sync"), argv);
|
|
9280
9548
|
const loaded = await loadConfig(argv.cwd);
|
|
9281
9549
|
applyResolvedConfig(argv, loaded.resolved);
|
|
9282
|
-
if ((await
|
|
9550
|
+
if ((await runSync({
|
|
9283
9551
|
loaded,
|
|
9284
9552
|
context: {
|
|
9285
9553
|
repoRoot: argv.cwd,
|
|
9286
9554
|
dryRun: argv.dryRun,
|
|
9555
|
+
...argv.repo ? { repo: argv.repo } : {},
|
|
9287
9556
|
...tokens,
|
|
9288
9557
|
org: resolveOrg(argv, loaded.resolved)
|
|
9289
9558
|
},
|
|
9290
|
-
|
|
9291
|
-
...argv.projectId ? { projectId: argv.projectId } : {},
|
|
9292
|
-
...argv.githubSecret?.length ? { githubSecretKeys: argv.githubSecret } : {},
|
|
9293
|
-
githubSecretScope,
|
|
9294
|
-
targets: argv.target
|
|
9559
|
+
...argv.steps && argv.steps.length > 0 ? { steps: argv.steps } : {}
|
|
9295
9560
|
})).summary.fail > 0) process.exitCode = 1;
|
|
9296
|
-
}).command("
|
|
9297
|
-
type: "string",
|
|
9298
|
-
describe: "Git branch to deploy (omit when using --files)"
|
|
9299
|
-
}).option("project-id", {
|
|
9300
|
-
type: "string",
|
|
9301
|
-
demandOption: true,
|
|
9302
|
-
describe: "Deployment project id (e.g., Vercel prj_*)"
|
|
9303
|
-
}).option("target", {
|
|
9304
|
-
type: "string",
|
|
9305
|
-
choices: ["production", "staging"],
|
|
9306
|
-
describe: "Named environment to deploy into. Omit for a branch preview."
|
|
9307
|
-
}).option("files", {
|
|
9308
|
-
type: "string",
|
|
9309
|
-
describe: "Deploy from a local directory of source files instead of a Git branch — calls deployFunction(), no linked repo required. Conflicts with `branch`."
|
|
9310
|
-
}).conflicts("files", "branch"), async (argv) => {
|
|
9311
|
-
const tokens = tokenContext(argv.token);
|
|
9312
|
-
if (!tokens) return;
|
|
9313
|
-
const loaded = await loadConfig(argv.cwd);
|
|
9314
|
-
applyResolvedConfig(argv, loaded.resolved);
|
|
9315
|
-
const context = {
|
|
9316
|
-
repoRoot: argv.cwd,
|
|
9317
|
-
dryRun: argv.dryRun,
|
|
9318
|
-
...tokens,
|
|
9319
|
-
org: resolveOrg(argv, loaded.resolved)
|
|
9320
|
-
};
|
|
9321
|
-
if (argv.files) {
|
|
9322
|
-
if ((await runDeployFromFiles({
|
|
9323
|
-
loaded,
|
|
9324
|
-
context,
|
|
9325
|
-
projectId: argv.projectId,
|
|
9326
|
-
dir: argv.files,
|
|
9327
|
-
...argv.target ? { target: argv.target } : {}
|
|
9328
|
-
})).status === "fail") process.exitCode = 1;
|
|
9329
|
-
return;
|
|
9330
|
-
}
|
|
9331
|
-
const [branch] = await promptForPositionals(getEntry("deploy"), argv);
|
|
9332
|
-
if ((await runDeploy({
|
|
9333
|
-
loaded,
|
|
9334
|
-
context,
|
|
9335
|
-
projectId: argv.projectId,
|
|
9336
|
-
branch,
|
|
9337
|
-
...argv.target ? { target: argv.target } : {}
|
|
9338
|
-
})).status === "fail") process.exitCode = 1;
|
|
9339
|
-
}).command("cleanup-preview [pr]", "List and delete Cloudflare Pages preview deployments for a GitHub PR", (y) => y.positional("pr", {
|
|
9561
|
+
}).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", {
|
|
9340
9562
|
type: "number",
|
|
9341
|
-
describe: "
|
|
9342
|
-
}).option("project", {
|
|
9343
|
-
type: "string",
|
|
9344
|
-
demandOption: true,
|
|
9345
|
-
describe: "Cloudflare Pages project name (e.g. theholocron-preview)"
|
|
9346
|
-
}).option("repo", {
|
|
9347
|
-
type: "string",
|
|
9348
|
-
describe: "GitHub repo as owner/name — defaults to the repo in holocron.config"
|
|
9349
|
-
}), async (argv) => {
|
|
9350
|
-
const tokens = tokenContext(argv.token);
|
|
9351
|
-
if (!tokens) return;
|
|
9352
|
-
const [pr] = await promptForPositionals(getEntry("cleanup-preview"), argv);
|
|
9353
|
-
const loaded = await loadConfig(argv.cwd);
|
|
9354
|
-
applyResolvedConfig(argv, loaded.resolved);
|
|
9355
|
-
if ((await runCleanupPreview({
|
|
9356
|
-
loaded,
|
|
9357
|
-
context: {
|
|
9358
|
-
repoRoot: argv.cwd,
|
|
9359
|
-
dryRun: argv.dryRun,
|
|
9360
|
-
...tokens,
|
|
9361
|
-
org: resolveOrg(argv, loaded.resolved)
|
|
9362
|
-
},
|
|
9363
|
-
prNumber: Number(pr),
|
|
9364
|
-
project: argv.project,
|
|
9365
|
-
...argv.repo ? { repo: argv.repo } : {}
|
|
9366
|
-
})).status === "fail") process.exitCode = 1;
|
|
9367
|
-
}).command("bump-versions [new-version]", "Bump all non-private package versions in lockstep (semantic-release prepareCmd)", (y) => y.positional("new-version", {
|
|
9368
|
-
type: "string",
|
|
9369
|
-
describe: "Version to set (e.g., 4.2.0 or 2.0.0-alpha.1)"
|
|
9370
|
-
}), async (argv) => {
|
|
9371
|
-
const [newVersion] = await promptForPositionals(getEntry("bump-versions"), argv);
|
|
9372
|
-
if ((await runNpmBumpVersions({
|
|
9373
|
-
version: newVersion,
|
|
9374
|
-
cwd: argv.cwd,
|
|
9375
|
-
dryRun: argv.dryRun
|
|
9376
|
-
})).status === "fail") process.exitCode = 1;
|
|
9377
|
-
}).command("publish", "Publish @theholocron/* packages to npm", (y) => y.option("initial", {
|
|
9378
|
-
type: "boolean",
|
|
9379
|
-
default: false,
|
|
9380
|
-
describe: "One-shot bootstrap publish for trusted-publishing-eligible packages (npm needs the package to exist before Trusted Publishing can be configured for it). Skips the steady-state OIDC path below — use only for a package's very first publish."
|
|
9381
|
-
}).option("tag", {
|
|
9382
|
-
type: "string",
|
|
9383
|
-
describe: "npm distribution tag. Defaults to alpha for --initial, latest for a steady-state publish."
|
|
9384
|
-
}).option("otp", {
|
|
9385
|
-
type: "string",
|
|
9386
|
-
describe: "One-time password from your authenticator (required if npm needs 2FA for writes)"
|
|
9387
|
-
}).option("sync-trust", {
|
|
9388
|
-
type: "array",
|
|
9389
|
-
describe: "Migrate npm Trusted Publisher config to a new workflow filename across every public package: --sync-trust <old-file> <new-file>. See issue #689 — npm requires fresh interactive 2FA per package; re-run after approving the printed authUrl if it stops early."
|
|
9390
|
-
}).option("repo", {
|
|
9391
|
-
type: "string",
|
|
9392
|
-
describe: "owner/repo for Trusted Publisher config (--sync-trust). Defaults to theholocron/holocron."
|
|
9393
|
-
}).option("provenance", {
|
|
9394
|
-
type: "boolean",
|
|
9395
|
-
default: true,
|
|
9396
|
-
describe: "Publish with --provenance (steady-state only). Default on — free under CI/OIDC."
|
|
9397
|
-
}).option("skip-already-published", {
|
|
9398
|
-
type: "boolean",
|
|
9399
|
-
default: false,
|
|
9400
|
-
describe: "Steady-state only: check `npm view <pkg>@<version>` before publishing each package and skip it if that exact version already exists, instead of one bulk publish. For repos that ship many independently-versioned packages where a release doesn't bump every one."
|
|
9401
|
-
}), async (argv) => {
|
|
9402
|
-
if (argv.syncTrust) {
|
|
9403
|
-
const [oldFile, newFile] = argv.syncTrust;
|
|
9404
|
-
if (!oldFile || !newFile) {
|
|
9405
|
-
reportError("publish --sync-trust requires exactly two values: <old-file> <new-file>");
|
|
9406
|
-
process.exitCode = 1;
|
|
9407
|
-
return;
|
|
9408
|
-
}
|
|
9409
|
-
if ((await runSyncTrust({
|
|
9410
|
-
cwd: argv.cwd,
|
|
9411
|
-
oldFile,
|
|
9412
|
-
newFile,
|
|
9413
|
-
dryRun: argv.dryRun,
|
|
9414
|
-
...argv.repo ? { repo: argv.repo } : {}
|
|
9415
|
-
})).status === "fail") process.exitCode = 1;
|
|
9416
|
-
return;
|
|
9417
|
-
}
|
|
9418
|
-
if (argv.initial) {
|
|
9419
|
-
if ((await runPublish({
|
|
9420
|
-
cwd: argv.cwd,
|
|
9421
|
-
dryRun: argv.dryRun,
|
|
9422
|
-
...argv.tag ? { tag: argv.tag } : {},
|
|
9423
|
-
...argv.otp ? { otp: argv.otp } : {}
|
|
9424
|
-
})).status === "fail") process.exitCode = 1;
|
|
9425
|
-
return;
|
|
9426
|
-
}
|
|
9427
|
-
if ((await runSteadyPublish({
|
|
9428
|
-
cwd: argv.cwd,
|
|
9429
|
-
dryRun: argv.dryRun,
|
|
9430
|
-
provenance: argv.provenance,
|
|
9431
|
-
skipAlreadyPublished: argv.skipAlreadyPublished,
|
|
9432
|
-
...argv.tag ? { tag: argv.tag } : {},
|
|
9433
|
-
...argv.otp ? { otp: argv.otp } : {}
|
|
9434
|
-
})).status === "fail") process.exitCode = 1;
|
|
9435
|
-
}).command("sync [steps..]", "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)", (y) => y.positional("steps", {
|
|
9436
|
-
type: "string",
|
|
9437
|
-
array: true,
|
|
9438
|
-
describe: "Steps to run: labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki (default: all)"
|
|
9439
|
-
}).option("repo", {
|
|
9440
|
-
type: "string",
|
|
9441
|
-
describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
|
|
9442
|
-
}), async (argv) => {
|
|
9443
|
-
const tokens = tokenContext(argv.token);
|
|
9444
|
-
if (!tokens) return;
|
|
9445
|
-
const loaded = await loadConfig(argv.cwd);
|
|
9446
|
-
applyResolvedConfig(argv, loaded.resolved);
|
|
9447
|
-
if ((await runSync({
|
|
9448
|
-
loaded,
|
|
9449
|
-
context: {
|
|
9450
|
-
repoRoot: argv.cwd,
|
|
9451
|
-
dryRun: argv.dryRun,
|
|
9452
|
-
...argv.repo ? { repo: argv.repo } : {},
|
|
9453
|
-
...tokens,
|
|
9454
|
-
org: resolveOrg(argv, loaded.resolved)
|
|
9455
|
-
},
|
|
9456
|
-
...argv.steps && argv.steps.length > 0 ? { steps: argv.steps } : {}
|
|
9457
|
-
})).summary.fail > 0) process.exitCode = 1;
|
|
9458
|
-
}).command("run <task> [job] [passthrough..]", "Run a task locally (test, typecheck, lint, build, audit) — figures out turbo / the tool / the package manager. `run ci` runs every merge-gating check; `run lint commit-msg <file>` lints a commit message.", (y) => y.positional("task", {
|
|
9459
|
-
type: "string",
|
|
9460
|
-
demandOption: true,
|
|
9461
|
-
describe: "Task name (test, typecheck, lint, build, audit, or `ci` for the merge-gating suite). See `holocron run --help`."
|
|
9462
|
-
}).positional("job", {
|
|
9463
|
-
type: "string",
|
|
9464
|
-
describe: "Sub-job within the task (e.g. `holocron run audit performance`, `holocron run lint commit-msg <file>`). Omit to run every job."
|
|
9465
|
-
}).positional("passthrough", {
|
|
9466
|
-
type: "string",
|
|
9467
|
-
array: true,
|
|
9468
|
-
describe: "Args forwarded to the tool — put them after `--`."
|
|
9469
|
-
}).option("required", {
|
|
9470
|
-
type: "boolean",
|
|
9471
|
-
default: false,
|
|
9472
|
-
describe: "Fail (exit 1) if this repo has no such task, instead of skipping."
|
|
9473
|
-
}).option("all", {
|
|
9474
|
-
type: "boolean",
|
|
9475
|
-
default: false,
|
|
9476
|
-
describe: "With `run ci`: run every `ci: true` task, not just the required ones."
|
|
9477
|
-
}).option("filter", {
|
|
9478
|
-
type: "string",
|
|
9479
|
-
describe: "turbo --filter=<pkg> passthrough (monorepo)."
|
|
9480
|
-
}), async (argv) => {
|
|
9481
|
-
const { logger } = buildCliLogger(argv, { command: argv.task === "ci" ? "run ci" : argv.task === "lint" && argv.job === "commit-msg" ? "run lint commit-msg" : "run" });
|
|
9482
|
-
const passthrough = [...argv.passthrough ?? [], ...argv["--"] ?? []];
|
|
9483
|
-
if (argv.task === "lint" && argv.job === "commit-msg") {
|
|
9484
|
-
const file = passthrough[0];
|
|
9485
|
-
if (!file) {
|
|
9486
|
-
reportError("run lint commit-msg: missing <file> (the commit message file git passes as $1).");
|
|
9487
|
-
process.exitCode = 1;
|
|
9488
|
-
return;
|
|
9489
|
-
}
|
|
9490
|
-
const result = await lintCommitMsgFile(file, argv.cwd);
|
|
9491
|
-
if (!result.valid) {
|
|
9492
|
-
for (const violation of result.violations) console.error(`✖ ${violation.rule}: ${violation.message}`);
|
|
9493
|
-
process.exitCode = 1;
|
|
9494
|
-
}
|
|
9495
|
-
return;
|
|
9496
|
-
}
|
|
9497
|
-
if (argv.task === "ci") {
|
|
9498
|
-
const ciConfig = await loadTasksConfig(argv.cwd).catch(() => void 0);
|
|
9499
|
-
if (createAstromech({
|
|
9500
|
-
cwd: argv.cwd,
|
|
9501
|
-
logger,
|
|
9502
|
-
config: ciConfig
|
|
9503
|
-
}).ci({
|
|
9504
|
-
dryRun: argv.dryRun,
|
|
9505
|
-
scope: argv.all ? "all" : "required",
|
|
9506
|
-
...argv.filter ? { filter: argv.filter } : {}
|
|
9507
|
-
}).status === "fail") process.exitCode = 1;
|
|
9508
|
-
return;
|
|
9509
|
-
}
|
|
9510
|
-
const config = await loadTasksConfig(argv.cwd).catch(() => void 0);
|
|
9511
|
-
const report = createAstromech({
|
|
9512
|
-
cwd: argv.cwd,
|
|
9513
|
-
logger,
|
|
9514
|
-
config
|
|
9515
|
-
}).run(argv.task, {
|
|
9516
|
-
...argv.job !== void 0 ? { job: argv.job } : {},
|
|
9517
|
-
passthrough,
|
|
9518
|
-
dryRun: argv.dryRun,
|
|
9519
|
-
required: argv.required,
|
|
9520
|
-
...argv.filter ? { filter: argv.filter } : {}
|
|
9521
|
-
});
|
|
9522
|
-
if (report.status === "fail" || report.status === "unknown") process.exitCode = 1;
|
|
9523
|
-
}).command("deploy-on-release", "Deploy every workspace package whose delivery.deploy task is `on: release` — run by the delivery.publish workflow's deploy job", (y) => y.option("channel", {
|
|
9524
|
-
type: "string",
|
|
9525
|
-
default: "",
|
|
9526
|
-
describe: "The release's channel: its prerelease identifier (`alpha`); empty or `main` for a stable release."
|
|
9563
|
+
describe: "Target Node.js major version (e.g., 22)"
|
|
9527
9564
|
}).option("from", {
|
|
9528
|
-
type: "
|
|
9529
|
-
describe: "
|
|
9530
|
-
}).option("to", {
|
|
9531
|
-
type: "string",
|
|
9532
|
-
describe: "The release commit. Default HEAD."
|
|
9565
|
+
type: "number",
|
|
9566
|
+
describe: "Current major version to replace. Auto-detected from .nvmrc / engines.node when omitted."
|
|
9533
9567
|
}), async (argv) => {
|
|
9534
|
-
|
|
9568
|
+
const [to] = await promptForPositionals(getEntry("repo upgrade node"), argv);
|
|
9569
|
+
let extra = [];
|
|
9570
|
+
try {
|
|
9571
|
+
const raw = readFileSync(join(argv.cwd, "holocron.config.json"), "utf8");
|
|
9572
|
+
const extraRaw = JSON.parse(raw).upgrade?.node?.extra;
|
|
9573
|
+
if (Array.isArray(extraRaw)) extra = extraRaw;
|
|
9574
|
+
} catch {}
|
|
9575
|
+
const report = await runUpgradeNode({
|
|
9576
|
+
to: Number(to),
|
|
9577
|
+
...argv.from != null ? { from: argv.from } : {},
|
|
9535
9578
|
cwd: argv.cwd,
|
|
9536
|
-
channel: argv.channel,
|
|
9537
|
-
...argv.from ? { from: argv.from } : {},
|
|
9538
|
-
...argv.to ? { to: argv.to } : {},
|
|
9539
9579
|
dryRun: argv.dryRun,
|
|
9540
|
-
|
|
9541
|
-
})
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
default: "theholocron/.github",
|
|
9545
|
-
describe: "Target org/repo (default: theholocron/.github)"
|
|
9546
|
-
}).option("branch", {
|
|
9547
|
-
type: "string",
|
|
9548
|
-
describe: "Push to this branch instead of the default branch (enables PR-based workflow for protected repos)"
|
|
9549
|
-
}).option("pr", {
|
|
9550
|
-
type: "boolean",
|
|
9551
|
-
default: false,
|
|
9552
|
-
describe: "Open a PR after pushing to --branch (no-op without --branch)"
|
|
9553
|
-
}).option("message", {
|
|
9554
|
-
type: "string",
|
|
9555
|
-
describe: "Commit message (default: chore: sync from theholocron/holocron)"
|
|
9556
|
-
}).option("output-dir", {
|
|
9557
|
-
type: "string",
|
|
9558
|
-
describe: "Write generated files to this local directory instead of pushing (for validation)"
|
|
9559
|
-
}), async (argv) => {
|
|
9560
|
-
const outputDir = argv["output-dir"];
|
|
9561
|
-
const parsed = tokenContext(argv.token);
|
|
9562
|
-
if (!parsed) return;
|
|
9563
|
-
let token;
|
|
9564
|
-
if (outputDir) token = "no-token-needed";
|
|
9565
|
-
else try {
|
|
9566
|
-
token = resolveSyncToken({ cliToken: parsed.cliTokens?.["github"] ?? parsed.cliToken });
|
|
9567
|
-
} catch (err) {
|
|
9568
|
-
reportError(`sync-github: ${err instanceof AuthError ? err.message : String(err)}`);
|
|
9580
|
+
extra
|
|
9581
|
+
});
|
|
9582
|
+
if (report.status === "fail") {
|
|
9583
|
+
if (report.message) reportError(`upgrade node: ${report.message}`);
|
|
9569
9584
|
process.exitCode = 1;
|
|
9570
|
-
return;
|
|
9571
9585
|
}
|
|
9572
|
-
|
|
9573
|
-
token,
|
|
9574
|
-
repo: argv.repo,
|
|
9575
|
-
dryRun: argv.dryRun,
|
|
9576
|
-
...argv.branch ? { branch: argv.branch } : {},
|
|
9577
|
-
...argv.pr ? { createPr: true } : {},
|
|
9578
|
-
...argv.message ? { message: argv.message } : {},
|
|
9579
|
-
...outputDir ? { outputDir } : {}
|
|
9580
|
-
})).status === "fail") process.exitCode = 1;
|
|
9581
|
-
}).command("sync-readme", "Sync the Installation + Usage block in README.md from package.json", (y) => y.option("dry-run", {
|
|
9586
|
+
}).command("deps", "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API", (yy) => yy.option("pins-only", {
|
|
9582
9587
|
type: "boolean",
|
|
9583
|
-
|
|
9584
|
-
|
|
9588
|
+
default: false,
|
|
9589
|
+
describe: "Only bump the catalog pins — skip the holocron.config.ts migration"
|
|
9585
9590
|
}), async (argv) => {
|
|
9586
|
-
const
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
context: {
|
|
9591
|
-
repoRoot: argv.cwd,
|
|
9592
|
-
dryRun: argv.dryRun
|
|
9593
|
-
}
|
|
9591
|
+
const report = await runUpgradeDeps({
|
|
9592
|
+
cwd: argv.cwd,
|
|
9593
|
+
dryRun: argv.dryRun,
|
|
9594
|
+
pinsOnly: argv.pinsOnly
|
|
9594
9595
|
});
|
|
9595
9596
|
if (report.status === "fail") {
|
|
9596
|
-
if (report.message) reportError(`
|
|
9597
|
+
if (report.message) reportError(`upgrade deps: ${report.message}`);
|
|
9597
9598
|
process.exitCode = 1;
|
|
9598
9599
|
}
|
|
9599
|
-
}).command("
|
|
9600
|
-
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
describe: "Browse the config in a foldable tree (default on a TTY; --no-interactive for static output)"
|
|
9605
|
-
}), async (argv) => {
|
|
9606
|
-
await viewJson((await loadConfig(argv.cwd)).resolved, {
|
|
9607
|
-
depth: argv.depth,
|
|
9608
|
-
interactive: argv.interactive
|
|
9609
|
-
});
|
|
9610
|
-
}).command("new [type] [name]", "Scaffold a new repo from a GitHub template (e.g. cli, react, nextjs, node, monorepo, base)", (y) => y.positional("type", {
|
|
9600
|
+
}).command("$0", false, () => {}, async (argv) => {
|
|
9601
|
+
await launchGroupMenu("repo upgrade", argv);
|
|
9602
|
+
}), () => {}).command("$0", false, () => {}, async (argv) => {
|
|
9603
|
+
await launchGroupMenu("repo", argv);
|
|
9604
|
+
}), () => {}).command("new", "Scaffold a new repo or plugin", (g) => g.command("repo [type] [name]", "Scaffold a new repo from a GitHub template (e.g. cli, react, nextjs, node, monorepo, base)", (y) => y.positional("type", {
|
|
9611
9605
|
type: "string",
|
|
9612
9606
|
describe: "Template type — maps to theholocron/<type>-template (e.g. cli, react, nextjs, node, monorepo, base)"
|
|
9613
9607
|
}).positional("name", {
|
|
@@ -9656,7 +9650,7 @@ try {
|
|
|
9656
9650
|
}).option("verify", {
|
|
9657
9651
|
type: "boolean",
|
|
9658
9652
|
default: true,
|
|
9659
|
-
describe: "Run pnpm install + holocron setup after bootstrapping (--no-verify skips)"
|
|
9653
|
+
describe: "Run pnpm install + holocron repo setup after bootstrapping (--no-verify skips)"
|
|
9660
9654
|
}), async (argv) => {
|
|
9661
9655
|
try {
|
|
9662
9656
|
const tokens = tokenContext(argv.token);
|
|
@@ -9872,129 +9866,351 @@ try {
|
|
|
9872
9866
|
}
|
|
9873
9867
|
throw err;
|
|
9874
9868
|
}
|
|
9875
|
-
}).command("plugin
|
|
9869
|
+
}).command("plugin [slug] [vendor]", "Scaffold a new @theholocron/holocron-plugin-<slug> package", (y) => y.positional("slug", {
|
|
9870
|
+
type: "string",
|
|
9871
|
+
describe: "Package slug (kebab-case)"
|
|
9872
|
+
}).positional("vendor", {
|
|
9873
|
+
type: "string",
|
|
9874
|
+
describe: "Vendor display name (PascalCase)"
|
|
9875
|
+
}).option("capability", {
|
|
9876
|
+
type: "string",
|
|
9877
|
+
describe: "Capability key: source|ci|secrets|environments|issues|deployment|storage|auth|vault|dns|tooling|notifications|analytics|errors|logs|wiki|workers"
|
|
9878
|
+
}).option("token-env", {
|
|
9879
|
+
type: "string",
|
|
9880
|
+
describe: "Holocron env var name (defaults to HOLOCRON_<VENDOR>_TOKEN)"
|
|
9881
|
+
}).option("vendor-env", {
|
|
9882
|
+
type: "string",
|
|
9883
|
+
describe: "Vendor-native env var name"
|
|
9884
|
+
}).option("base-url", {
|
|
9885
|
+
type: "string",
|
|
9886
|
+
describe: "REST base URL"
|
|
9887
|
+
}).option("verify", {
|
|
9888
|
+
type: "boolean",
|
|
9889
|
+
default: true,
|
|
9890
|
+
describe: "Run post-scaffold pnpm install + typecheck + lint + test (default true; --no-verify skips)"
|
|
9891
|
+
}), async (argv) => {
|
|
9892
|
+
try {
|
|
9893
|
+
const [slug, vendor] = await promptForPositionals(getEntry("new plugin"), argv);
|
|
9894
|
+
const { capability, vendorEnv, baseUrl } = await resolvePluginCreateInputs({
|
|
9895
|
+
capability: argv.capability,
|
|
9896
|
+
vendorEnv: argv.vendorEnv,
|
|
9897
|
+
baseUrl: argv.baseUrl
|
|
9898
|
+
}, {
|
|
9899
|
+
selectCapability: () => select({
|
|
9900
|
+
message: "Capability:",
|
|
9901
|
+
choices: Object.keys(CARDINALITY).map((k) => ({
|
|
9902
|
+
name: k,
|
|
9903
|
+
value: k
|
|
9904
|
+
}))
|
|
9905
|
+
}),
|
|
9906
|
+
inputVendorEnv: () => input({ message: `Vendor-native env var for the ${vendor} token (e.g. MYVENDOR_API_KEY):` }),
|
|
9907
|
+
inputBaseUrl: () => input({ message: `REST base URL for the ${vendor} API (e.g. https://api.myvendor.com):` })
|
|
9908
|
+
});
|
|
9909
|
+
if (runPluginCreate({
|
|
9910
|
+
slug,
|
|
9911
|
+
vendorName: vendor,
|
|
9912
|
+
capability,
|
|
9913
|
+
vendorEnv,
|
|
9914
|
+
baseUrl,
|
|
9915
|
+
...argv.tokenEnv ? { tokenEnv: argv.tokenEnv } : {},
|
|
9916
|
+
dryRun: argv.dryRun,
|
|
9917
|
+
noVerify: !argv.verify,
|
|
9918
|
+
cwd: argv.cwd
|
|
9919
|
+
}).status === "fail") process.exitCode = 1;
|
|
9920
|
+
} catch (err) {
|
|
9921
|
+
if (err instanceof PluginCreateError) {
|
|
9922
|
+
reportError(`plugin create: ${err.message}`);
|
|
9923
|
+
process.exitCode = 1;
|
|
9924
|
+
return;
|
|
9925
|
+
}
|
|
9926
|
+
throw err;
|
|
9927
|
+
}
|
|
9928
|
+
}).command("$0", false, () => {}, async (argv) => {
|
|
9929
|
+
await launchGroupMenu("new", argv);
|
|
9930
|
+
}), () => {}).command("secrets", "Set secrets and sync vault environments", (g) => g.command("set [name] [value]", "Set a single secret via the configured `secrets` capability", (y) => y.positional("name", {
|
|
9931
|
+
type: "string",
|
|
9932
|
+
describe: "Secret name (e.g., NPM_TOKEN)"
|
|
9933
|
+
}).positional("value", {
|
|
9934
|
+
type: "string",
|
|
9935
|
+
describe: "Secret value (positional). If omitted, sources from --from-stdin, --from-env, or env var matching <name>."
|
|
9936
|
+
}).option("from-stdin", {
|
|
9937
|
+
type: "boolean",
|
|
9938
|
+
default: false,
|
|
9939
|
+
describe: "Read the secret value from stdin"
|
|
9940
|
+
}).option("from-env", {
|
|
9941
|
+
type: "string",
|
|
9942
|
+
describe: "Read the secret value from the named env var (otherwise: env var matching <name>)"
|
|
9943
|
+
}).option("scope", {
|
|
9944
|
+
type: "string",
|
|
9945
|
+
default: "repo",
|
|
9946
|
+
describe: "Scope: \"repo\" (default), \"env=<name>\", or \"org=<name>\""
|
|
9947
|
+
}), async (argv) => {
|
|
9948
|
+
const tokens = tokenContext(argv.token);
|
|
9949
|
+
if (!tokens) return;
|
|
9950
|
+
const [name] = await promptForPositionals(getEntry("secrets set"), argv);
|
|
9951
|
+
const scopeArg = argv.scope;
|
|
9952
|
+
const scope = parseScope(scopeArg);
|
|
9953
|
+
const loaded = await loadConfig(argv.cwd);
|
|
9954
|
+
applyResolvedConfig(argv, loaded.resolved);
|
|
9955
|
+
if ((await runSecretSet({
|
|
9956
|
+
loaded,
|
|
9957
|
+
context: {
|
|
9958
|
+
repoRoot: argv.cwd,
|
|
9959
|
+
dryRun: argv.dryRun,
|
|
9960
|
+
...tokens,
|
|
9961
|
+
org: resolveOrg(argv, loaded.resolved)
|
|
9962
|
+
},
|
|
9963
|
+
name,
|
|
9964
|
+
...argv.value ? { value: argv.value } : {},
|
|
9965
|
+
...argv.fromStdin ? { fromStdin: true } : {},
|
|
9966
|
+
...argv.fromEnv ? { fromEnv: argv.fromEnv } : {},
|
|
9967
|
+
scope
|
|
9968
|
+
})).status === "fail") process.exitCode = 1;
|
|
9969
|
+
}).command("sync [environmentId]", "Read a vault environment + fan KEY=VALUEs out to secrets + deployment env vars", (y) => y.positional("environmentId", {
|
|
9970
|
+
type: "string",
|
|
9971
|
+
describe: "Vault environment id to read (1P Environment id, etc.)"
|
|
9972
|
+
}).option("project-id", {
|
|
9973
|
+
type: "string",
|
|
9974
|
+
describe: "Deployment project id (e.g., Vercel prj_*). Required when deployment is loaded."
|
|
9975
|
+
}).option("target", {
|
|
9976
|
+
type: "array",
|
|
9977
|
+
default: ["production", "preview"],
|
|
9978
|
+
describe: "Deployment targets to sync to. Defaults to production + preview."
|
|
9979
|
+
}).option("github-secret", {
|
|
9980
|
+
type: "array",
|
|
9981
|
+
describe: "Vault key(s) to push to GH Actions secrets (repeatable). Omit to push every key."
|
|
9982
|
+
}).option("github-secret-scope", {
|
|
9983
|
+
type: "string",
|
|
9984
|
+
default: "repo",
|
|
9985
|
+
describe: "Scope applied to every --github-secret key: \"repo\" (default), \"env=<name>\", or \"org=<name>\"."
|
|
9986
|
+
}), async (argv) => {
|
|
9987
|
+
const tokens = tokenContext(argv.token);
|
|
9988
|
+
if (!tokens) return;
|
|
9989
|
+
const githubSecretScope = parseScope(argv.githubSecretScope);
|
|
9990
|
+
const [environmentId] = await promptForPositionals(getEntry("secrets sync"), argv);
|
|
9991
|
+
const loaded = await loadConfig(argv.cwd);
|
|
9992
|
+
applyResolvedConfig(argv, loaded.resolved);
|
|
9993
|
+
if ((await runSecretsSync({
|
|
9994
|
+
loaded,
|
|
9995
|
+
context: {
|
|
9996
|
+
repoRoot: argv.cwd,
|
|
9997
|
+
dryRun: argv.dryRun,
|
|
9998
|
+
...tokens,
|
|
9999
|
+
org: resolveOrg(argv, loaded.resolved)
|
|
10000
|
+
},
|
|
10001
|
+
environmentId,
|
|
10002
|
+
...argv.projectId ? { projectId: argv.projectId } : {},
|
|
10003
|
+
...argv.githubSecret?.length ? { githubSecretKeys: argv.githubSecret } : {},
|
|
10004
|
+
githubSecretScope,
|
|
10005
|
+
targets: argv.target
|
|
10006
|
+
})).summary.fail > 0) process.exitCode = 1;
|
|
10007
|
+
}).command("$0", false, () => {}, async (argv) => {
|
|
10008
|
+
await launchGroupMenu("secrets", argv);
|
|
10009
|
+
}), () => {}).command("sync", "Sync generated files to other repos", (g) => g.command("github", "Sync workflow templates and composite actions to theholocron/.github", (y) => y.option("repo", {
|
|
10010
|
+
type: "string",
|
|
10011
|
+
default: "theholocron/.github",
|
|
10012
|
+
describe: "Target org/repo (default: theholocron/.github)"
|
|
10013
|
+
}).option("branch", {
|
|
10014
|
+
type: "string",
|
|
10015
|
+
describe: "Push to this branch instead of the default branch (enables PR-based workflow for protected repos)"
|
|
10016
|
+
}).option("pr", {
|
|
10017
|
+
type: "boolean",
|
|
10018
|
+
default: false,
|
|
10019
|
+
describe: "Open a PR after pushing to --branch (no-op without --branch)"
|
|
10020
|
+
}).option("message", {
|
|
9876
10021
|
type: "string",
|
|
9877
|
-
describe: "
|
|
9878
|
-
}).
|
|
10022
|
+
describe: "Commit message (default: chore: sync from theholocron/holocron)"
|
|
10023
|
+
}).option("output-dir", {
|
|
9879
10024
|
type: "string",
|
|
9880
|
-
describe: "
|
|
9881
|
-
})
|
|
10025
|
+
describe: "Write generated files to this local directory instead of pushing (for validation)"
|
|
10026
|
+
}), async (argv) => {
|
|
10027
|
+
const outputDir = argv["output-dir"];
|
|
10028
|
+
const parsed = tokenContext(argv.token);
|
|
10029
|
+
if (!parsed) return;
|
|
10030
|
+
let token;
|
|
10031
|
+
if (outputDir) token = "no-token-needed";
|
|
10032
|
+
else try {
|
|
10033
|
+
token = resolveSyncToken({ cliToken: parsed.cliTokens?.["github"] ?? parsed.cliToken });
|
|
10034
|
+
} catch (err) {
|
|
10035
|
+
reportError(`sync-github: ${err instanceof AuthError ? err.message : String(err)}`);
|
|
10036
|
+
process.exitCode = 1;
|
|
10037
|
+
return;
|
|
10038
|
+
}
|
|
10039
|
+
if ((await runSyncGithub({
|
|
10040
|
+
token,
|
|
10041
|
+
repo: argv.repo,
|
|
10042
|
+
dryRun: argv.dryRun,
|
|
10043
|
+
...argv.branch ? { branch: argv.branch } : {},
|
|
10044
|
+
...argv.pr ? { createPr: true } : {},
|
|
10045
|
+
...argv.message ? { message: argv.message } : {},
|
|
10046
|
+
...outputDir ? { outputDir } : {}
|
|
10047
|
+
})).status === "fail") process.exitCode = 1;
|
|
10048
|
+
}).command("$0", false, () => {}, async (argv) => {
|
|
10049
|
+
await launchGroupMenu("sync", argv);
|
|
10050
|
+
}), () => {}).command("package", "Version and publish workspace packages", (g) => g.command("bump-versions [new-version]", "Bump all non-private package versions in lockstep (semantic-release prepareCmd)", (y) => y.positional("new-version", {
|
|
9882
10051
|
type: "string",
|
|
9883
|
-
describe: "
|
|
9884
|
-
})
|
|
10052
|
+
describe: "Version to set (e.g., 4.2.0 or 2.0.0-alpha.1)"
|
|
10053
|
+
}), async (argv) => {
|
|
10054
|
+
const [newVersion] = await promptForPositionals(getEntry("package bump-versions"), argv);
|
|
10055
|
+
if ((await runNpmBumpVersions({
|
|
10056
|
+
version: newVersion,
|
|
10057
|
+
cwd: argv.cwd,
|
|
10058
|
+
dryRun: argv.dryRun
|
|
10059
|
+
})).status === "fail") process.exitCode = 1;
|
|
10060
|
+
}).command("publish", "Publish @theholocron/* packages to npm", (y) => y.option("initial", {
|
|
10061
|
+
type: "boolean",
|
|
10062
|
+
default: false,
|
|
10063
|
+
describe: "One-shot bootstrap publish for trusted-publishing-eligible packages (npm needs the package to exist before Trusted Publishing can be configured for it). Skips the steady-state OIDC path below — use only for a package's very first publish."
|
|
10064
|
+
}).option("tag", {
|
|
9885
10065
|
type: "string",
|
|
9886
|
-
describe: "
|
|
9887
|
-
}).option("
|
|
10066
|
+
describe: "npm distribution tag. Defaults to alpha for --initial, latest for a steady-state publish."
|
|
10067
|
+
}).option("otp", {
|
|
9888
10068
|
type: "string",
|
|
9889
|
-
describe: "
|
|
9890
|
-
}).option("
|
|
10069
|
+
describe: "One-time password from your authenticator (required if npm needs 2FA for writes)"
|
|
10070
|
+
}).option("sync-trust", {
|
|
10071
|
+
type: "array",
|
|
10072
|
+
describe: "Migrate npm Trusted Publisher config to a new workflow filename across every public package: --sync-trust <old-file> <new-file>. See issue #689 — npm requires fresh interactive 2FA per package; re-run after approving the printed authUrl if it stops early."
|
|
10073
|
+
}).option("repo", {
|
|
9891
10074
|
type: "string",
|
|
9892
|
-
describe: "
|
|
9893
|
-
}).option("
|
|
10075
|
+
describe: "owner/repo for Trusted Publisher config (--sync-trust). Defaults to theholocron/holocron."
|
|
10076
|
+
}).option("provenance", {
|
|
9894
10077
|
type: "boolean",
|
|
9895
10078
|
default: true,
|
|
9896
|
-
describe: "
|
|
10079
|
+
describe: "Publish with --provenance (steady-state only). Default on — free under CI/OIDC."
|
|
10080
|
+
}).option("skip-already-published", {
|
|
10081
|
+
type: "boolean",
|
|
10082
|
+
default: false,
|
|
10083
|
+
describe: "Steady-state only: check `npm view <pkg>@<version>` before publishing each package and skip it if that exact version already exists, instead of one bulk publish. For repos that ship many independently-versioned packages where a release doesn't bump every one."
|
|
9897
10084
|
}), async (argv) => {
|
|
9898
|
-
|
|
9899
|
-
const [
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
vendorEnv: argv.vendorEnv,
|
|
9903
|
-
baseUrl: argv.baseUrl
|
|
9904
|
-
}, {
|
|
9905
|
-
selectCapability: () => select({
|
|
9906
|
-
message: "Capability:",
|
|
9907
|
-
choices: Object.keys(CARDINALITY).map((k) => ({
|
|
9908
|
-
name: k,
|
|
9909
|
-
value: k
|
|
9910
|
-
}))
|
|
9911
|
-
}),
|
|
9912
|
-
inputVendorEnv: () => input({ message: `Vendor-native env var for the ${vendor} token (e.g. MYVENDOR_API_KEY):` }),
|
|
9913
|
-
inputBaseUrl: () => input({ message: `REST base URL for the ${vendor} API (e.g. https://api.myvendor.com):` })
|
|
9914
|
-
});
|
|
9915
|
-
if (runPluginCreate({
|
|
9916
|
-
slug,
|
|
9917
|
-
vendorName: vendor,
|
|
9918
|
-
capability,
|
|
9919
|
-
vendorEnv,
|
|
9920
|
-
baseUrl,
|
|
9921
|
-
...argv.tokenEnv ? { tokenEnv: argv.tokenEnv } : {},
|
|
9922
|
-
dryRun: argv.dryRun,
|
|
9923
|
-
noVerify: !argv.verify,
|
|
9924
|
-
cwd: argv.cwd
|
|
9925
|
-
}).status === "fail") process.exitCode = 1;
|
|
9926
|
-
} catch (err) {
|
|
9927
|
-
if (err instanceof PluginCreateError) {
|
|
9928
|
-
reportError(`plugin create: ${err.message}`);
|
|
10085
|
+
if (argv.syncTrust) {
|
|
10086
|
+
const [oldFile, newFile] = argv.syncTrust;
|
|
10087
|
+
if (!oldFile || !newFile) {
|
|
10088
|
+
reportError("publish --sync-trust requires exactly two values: <old-file> <new-file>");
|
|
9929
10089
|
process.exitCode = 1;
|
|
9930
10090
|
return;
|
|
9931
10091
|
}
|
|
9932
|
-
|
|
10092
|
+
if ((await runSyncTrust({
|
|
10093
|
+
cwd: argv.cwd,
|
|
10094
|
+
oldFile,
|
|
10095
|
+
newFile,
|
|
10096
|
+
dryRun: argv.dryRun,
|
|
10097
|
+
...argv.repo ? { repo: argv.repo } : {}
|
|
10098
|
+
})).status === "fail") process.exitCode = 1;
|
|
10099
|
+
return;
|
|
9933
10100
|
}
|
|
9934
|
-
|
|
10101
|
+
if (argv.initial) {
|
|
10102
|
+
if ((await runPublish({
|
|
10103
|
+
cwd: argv.cwd,
|
|
10104
|
+
dryRun: argv.dryRun,
|
|
10105
|
+
...argv.tag ? { tag: argv.tag } : {},
|
|
10106
|
+
...argv.otp ? { otp: argv.otp } : {}
|
|
10107
|
+
})).status === "fail") process.exitCode = 1;
|
|
10108
|
+
return;
|
|
10109
|
+
}
|
|
10110
|
+
if ((await runSteadyPublish({
|
|
10111
|
+
cwd: argv.cwd,
|
|
10112
|
+
dryRun: argv.dryRun,
|
|
10113
|
+
provenance: argv.provenance,
|
|
10114
|
+
skipAlreadyPublished: argv.skipAlreadyPublished,
|
|
10115
|
+
...argv.tag ? { tag: argv.tag } : {},
|
|
10116
|
+
...argv.otp ? { otp: argv.otp } : {}
|
|
10117
|
+
})).status === "fail") process.exitCode = 1;
|
|
10118
|
+
}).command("$0", false, () => {}, async (argv) => {
|
|
10119
|
+
await launchGroupMenu("package", argv);
|
|
10120
|
+
}), () => {}).command("deploy [branch]", "Trigger a deployment via the configured `deployment` capability", (y) => y.command("cleanup-preview [pr]", "List and delete Cloudflare Pages preview deployments for a GitHub PR", (y) => y.positional("pr", {
|
|
9935
10121
|
type: "number",
|
|
9936
|
-
describe: "
|
|
10122
|
+
describe: "PR number to clean up"
|
|
10123
|
+
}).option("project", {
|
|
10124
|
+
type: "string",
|
|
10125
|
+
demandOption: true,
|
|
10126
|
+
describe: "Cloudflare Pages project name (e.g. theholocron-preview)"
|
|
10127
|
+
}).option("repo", {
|
|
10128
|
+
type: "string",
|
|
10129
|
+
describe: "GitHub repo as owner/name — defaults to the repo in holocron.config"
|
|
10130
|
+
}), async (argv) => {
|
|
10131
|
+
const tokens = tokenContext(argv.token);
|
|
10132
|
+
if (!tokens) return;
|
|
10133
|
+
const [pr] = await promptForPositionals(getEntry("deploy cleanup-preview"), argv);
|
|
10134
|
+
const loaded = await loadConfig(argv.cwd);
|
|
10135
|
+
applyResolvedConfig(argv, loaded.resolved);
|
|
10136
|
+
if ((await runCleanupPreview({
|
|
10137
|
+
loaded,
|
|
10138
|
+
context: {
|
|
10139
|
+
repoRoot: argv.cwd,
|
|
10140
|
+
dryRun: argv.dryRun,
|
|
10141
|
+
...tokens,
|
|
10142
|
+
org: resolveOrg(argv, loaded.resolved)
|
|
10143
|
+
},
|
|
10144
|
+
prNumber: Number(pr),
|
|
10145
|
+
project: argv.project,
|
|
10146
|
+
...argv.repo ? { repo: argv.repo } : {}
|
|
10147
|
+
})).status === "fail") process.exitCode = 1;
|
|
10148
|
+
}).command("on-release", "Deploy every workspace package whose delivery.deploy task is `on: release` — run by the delivery.publish workflow's deploy job", (y) => y.option("channel", {
|
|
10149
|
+
type: "string",
|
|
10150
|
+
default: "",
|
|
10151
|
+
describe: "The release's channel: its prerelease identifier (`alpha`); empty or `main` for a stable release."
|
|
9937
10152
|
}).option("from", {
|
|
9938
|
-
type: "
|
|
9939
|
-
describe: "
|
|
10153
|
+
type: "string",
|
|
10154
|
+
describe: "The previous release's commit, to diff against. Omit when there is none."
|
|
10155
|
+
}).option("to", {
|
|
10156
|
+
type: "string",
|
|
10157
|
+
describe: "The release commit. Default HEAD."
|
|
9940
10158
|
}), async (argv) => {
|
|
9941
|
-
|
|
9942
|
-
let extra = [];
|
|
9943
|
-
try {
|
|
9944
|
-
const raw = readFileSync(join(argv.cwd, "holocron.config.json"), "utf8");
|
|
9945
|
-
const extraRaw = JSON.parse(raw).upgrade?.node?.extra;
|
|
9946
|
-
if (Array.isArray(extraRaw)) extra = extraRaw;
|
|
9947
|
-
} catch {}
|
|
9948
|
-
const report = await runUpgradeNode({
|
|
9949
|
-
to: Number(to),
|
|
9950
|
-
...argv.from != null ? { from: argv.from } : {},
|
|
10159
|
+
if ((await deployOnRelease({
|
|
9951
10160
|
cwd: argv.cwd,
|
|
10161
|
+
channel: argv.channel,
|
|
10162
|
+
...argv.from ? { from: argv.from } : {},
|
|
10163
|
+
...argv.to ? { to: argv.to } : {},
|
|
9952
10164
|
dryRun: argv.dryRun,
|
|
9953
|
-
|
|
9954
|
-
});
|
|
9955
|
-
|
|
9956
|
-
|
|
10165
|
+
print: (line) => console.log(line)
|
|
10166
|
+
})).status === "fail") process.exitCode = 1;
|
|
10167
|
+
}).positional("branch", {
|
|
10168
|
+
type: "string",
|
|
10169
|
+
describe: "Git branch to deploy (omit when using --files)"
|
|
10170
|
+
}).option("project-id", {
|
|
10171
|
+
type: "string",
|
|
10172
|
+
describe: "Deployment project id (required to trigger a deployment, e.g. Vercel prj_*)"
|
|
10173
|
+
}).option("target", {
|
|
10174
|
+
type: "string",
|
|
10175
|
+
choices: ["production", "staging"],
|
|
10176
|
+
describe: "Named environment to deploy into. Omit for a branch preview."
|
|
10177
|
+
}).option("files", {
|
|
10178
|
+
type: "string",
|
|
10179
|
+
describe: "Deploy from a local directory of source files instead of a Git branch — calls deployFunction(), no linked repo required. Conflicts with `branch`."
|
|
10180
|
+
}).conflicts("files", "branch"), async (argv) => {
|
|
10181
|
+
if (!argv.projectId) {
|
|
10182
|
+
reportError("deploy: missing required argument --project-id");
|
|
9957
10183
|
process.exitCode = 1;
|
|
10184
|
+
return;
|
|
9958
10185
|
}
|
|
9959
|
-
|
|
9960
|
-
|
|
9961
|
-
|
|
9962
|
-
|
|
9963
|
-
|
|
9964
|
-
|
|
9965
|
-
cwd: argv.cwd,
|
|
10186
|
+
const tokens = tokenContext(argv.token);
|
|
10187
|
+
if (!tokens) return;
|
|
10188
|
+
const loaded = await loadConfig(argv.cwd);
|
|
10189
|
+
applyResolvedConfig(argv, loaded.resolved);
|
|
10190
|
+
const context = {
|
|
10191
|
+
repoRoot: argv.cwd,
|
|
9966
10192
|
dryRun: argv.dryRun,
|
|
9967
|
-
|
|
9968
|
-
|
|
9969
|
-
|
|
9970
|
-
|
|
9971
|
-
|
|
10193
|
+
...tokens,
|
|
10194
|
+
org: resolveOrg(argv, loaded.resolved)
|
|
10195
|
+
};
|
|
10196
|
+
if (argv.files) {
|
|
10197
|
+
if ((await runDeployFromFiles({
|
|
10198
|
+
loaded,
|
|
10199
|
+
context,
|
|
10200
|
+
projectId: argv.projectId,
|
|
10201
|
+
dir: argv.files,
|
|
10202
|
+
...argv.target ? { target: argv.target } : {}
|
|
10203
|
+
})).status === "fail") process.exitCode = 1;
|
|
10204
|
+
return;
|
|
9972
10205
|
}
|
|
9973
|
-
|
|
9974
|
-
|
|
9975
|
-
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
...argv.
|
|
9980
|
-
...argv.org ? { org: argv.org } : {}
|
|
9981
|
-
})).status === "fail") process.exitCode = 1;
|
|
9982
|
-
}).command("unset [provider]", "Remove a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
|
|
9983
|
-
const [provider] = await promptForPositionals(getEntry("auth unset"), argv);
|
|
9984
|
-
runAuthUnset({ provider });
|
|
9985
|
-
}).command("check [provider]", "Re-verify a stored bootstrap token", (yy) => yy.positional("provider", { type: "string" }), async (argv) => {
|
|
9986
|
-
const [provider] = await promptForPositionals(getEntry("auth check"), argv);
|
|
9987
|
-
if ((await runAuthCheck({
|
|
9988
|
-
provider,
|
|
9989
|
-
...argv.org ? { org: argv.org } : {}
|
|
10206
|
+
const [branch] = await promptForPositionals(getEntry("deploy"), argv);
|
|
10207
|
+
if ((await runDeploy({
|
|
10208
|
+
loaded,
|
|
10209
|
+
context,
|
|
10210
|
+
projectId: argv.projectId,
|
|
10211
|
+
branch,
|
|
10212
|
+
...argv.target ? { target: argv.target } : {}
|
|
9990
10213
|
})).status === "fail") process.exitCode = 1;
|
|
9991
|
-
}).command("list", "List every provider with a stored bootstrap token", () => {}, async () => {
|
|
9992
|
-
await runAuthList();
|
|
9993
|
-
}).command("$0", false, () => {}, async (argv) => {
|
|
9994
|
-
await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "auth"), argv, "auth — choose a subcommand:", "Run `holocron auth --help` to see available auth subcommands.");
|
|
9995
|
-
}), () => {}).command("$0", false, () => {}, async (argv) => {
|
|
9996
|
-
printVersionHeaderOnce();
|
|
9997
|
-
await launchMenu(COMMAND_REGISTRY.filter((e) => !e.group), argv);
|
|
9998
10214
|
}).strict().help().epilogue(`Execution contexts:
|
|
9999
10215
|
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) => {
|
|
10000
10216
|
if (err instanceof Error && USER_FACING_ERRORS.has(err.name)) {
|