@theholocron/cli 5.0.0-alpha.120 → 5.0.0-alpha.122
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -5
- package/dist/cli.mjs +370 -87
- package/dist/cli.mjs.map +1 -1
- package/package.json +5 -5
package/dist/cli.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { homedir, hostname, tmpdir, userInfo } from "node:os";
|
|
4
5
|
import path, { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { checkbox, input, select } from "@inquirer/prompts";
|
|
6
|
+
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
6
7
|
import { KNOWN_WORKFLOWS, createAstromech, deployOnRelease, extractPreviewConfig, readWorkspacePackages, reusableTemplates } from "@theholocron/astromech";
|
|
7
8
|
import { loadTasksConfig } from "@theholocron/astromech/config";
|
|
9
|
+
import { LOG_LEVELS, NoopAnalyticsSink, NoopErrorSink, redactObject } from "@theholocron/observability/core";
|
|
8
10
|
import yargs from "yargs";
|
|
9
11
|
import { hideBin } from "yargs/helpers";
|
|
10
12
|
import { AuthError, ProviderApiError, ProviderApiError as ProviderApiError$1, createRestClient } from "@theholocron/http-client";
|
|
@@ -12,18 +14,16 @@ import { createEnvLookup } from "@theholocron/env-utils";
|
|
|
12
14
|
import { Entry, findCredentials } from "@napi-rs/keyring";
|
|
13
15
|
import { pathToFileURL } from "node:url";
|
|
14
16
|
import { errorMessage } from "@theholocron/misc-utils";
|
|
15
|
-
import { createLogger, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/observability/logger";
|
|
16
|
-
import ora from "ora";
|
|
17
|
+
import { createLogger, isCI, parseLogLevel, resolveAxiomFromEnv } from "@theholocron/observability/logger";
|
|
17
18
|
import chalk from "chalk";
|
|
19
|
+
import ora from "ora";
|
|
18
20
|
import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
19
|
-
import { homedir, hostname, userInfo } from "node:os";
|
|
20
21
|
import { access, copyFile, mkdir, readFile, readdir, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
21
22
|
import { createHash } from "node:crypto";
|
|
22
23
|
import { generateReadme } from "@theholocron/components-doc/markdown";
|
|
23
24
|
import { getClients, getConfigs, getDocs, getPlugins, getSkills, getThemes, getUtils } from "@theholocron/registry-doc";
|
|
24
25
|
import { createGitHubClient } from "@theholocron/github-client";
|
|
25
26
|
import { PostHogSink } from "@theholocron/observability/analytics";
|
|
26
|
-
import { NoopAnalyticsSink, NoopErrorSink, redactObject } from "@theholocron/observability/core";
|
|
27
27
|
import { SentrySink } from "@theholocron/observability/errors";
|
|
28
28
|
import load from "@commitlint/load";
|
|
29
29
|
import lint from "@commitlint/lint";
|
|
@@ -303,23 +303,52 @@ function resolveConfig(raw) {
|
|
|
303
303
|
};
|
|
304
304
|
}
|
|
305
305
|
//#endregion
|
|
306
|
+
//#region src/ui/style.ts
|
|
307
|
+
const style = {
|
|
308
|
+
success: (msg) => `${chalk.green("✓")} ${msg}`,
|
|
309
|
+
warn: (msg) => `${chalk.yellow("⚠")} ${msg}`,
|
|
310
|
+
fail: (msg) => `${chalk.red("✗")} ${msg}`,
|
|
311
|
+
step: (msg) => `${chalk.cyan("→")} ${msg}`,
|
|
312
|
+
hint: (msg) => chalk.dim(msg),
|
|
313
|
+
dim: (msg) => chalk.dim(msg),
|
|
314
|
+
header: (msg) => chalk.bold(msg)
|
|
315
|
+
};
|
|
316
|
+
//#endregion
|
|
306
317
|
//#region src/logger.ts
|
|
307
318
|
/**
|
|
308
319
|
* Resolve the explicit level to hand to `createLogger`, in priority order:
|
|
309
320
|
*
|
|
310
|
-
* 1. `--verbose` → `"debug"`
|
|
311
|
-
*
|
|
312
|
-
*
|
|
321
|
+
* 1. `--log <level>` 2. `--verbose` → `"debug"` 3. `--quiet` → `"error"`
|
|
322
|
+
* 4. `HOLOCRON_LOG_LEVEL` env var
|
|
323
|
+
* 5. `holocron.config` `log.level` (`configLevel`)
|
|
313
324
|
*
|
|
314
325
|
* Returns `undefined` when nothing applies — `createLogger` then defaults
|
|
315
326
|
* to `"info"`. Resolving the full chain here (rather than passing
|
|
316
327
|
* `configLevel` straight through) keeps config below the env var.
|
|
317
328
|
*/
|
|
318
329
|
function resolveLogLevel(argv, configLevel) {
|
|
330
|
+
if (argv.log) return argv.log;
|
|
319
331
|
if (argv.verbose) return "debug";
|
|
320
332
|
if (argv.quiet) return "error";
|
|
321
333
|
return parseLogLevel(env.get("HOLOCRON_LOG_LEVEL")) ?? configLevel;
|
|
322
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Resolve whether human-visible console output (pretty or NDJSON) should run
|
|
337
|
+
* at all, independent of level — the `@theholocron/observability`
|
|
338
|
+
* `consoleOutput` override.
|
|
339
|
+
*
|
|
340
|
+
* Local/interactive runs are quiet by default: any raw structured logger
|
|
341
|
+
* output (colorized or not) stays off regardless of the resolved level,
|
|
342
|
+
* unless the user explicitly asked to see it via `--log <level>`,
|
|
343
|
+
* `--verbose`, or `--quiet`. CI is untouched either way — returning
|
|
344
|
+
* `undefined` there preserves `createLogger`'s own `ci`/`tty`
|
|
345
|
+
* auto-detection exactly, so CI's existing NDJSON-to-stdout behavior is
|
|
346
|
+
* unaffected by this flag.
|
|
347
|
+
*/
|
|
348
|
+
function resolveConsoleOutput(argv) {
|
|
349
|
+
if (argv.log || argv.verbose || argv.quiet) return true;
|
|
350
|
+
return isCI() ? void 0 : false;
|
|
351
|
+
}
|
|
323
352
|
let root;
|
|
324
353
|
let rootLevel;
|
|
325
354
|
let rootCommand;
|
|
@@ -358,13 +387,15 @@ function buildCliLogger(argv, opts = {}) {
|
|
|
358
387
|
const { command, configLevel } = opts;
|
|
359
388
|
if (command) rootCommand = command;
|
|
360
389
|
const level = resolveLogLevel(argv, configLevel);
|
|
390
|
+
const consoleOutput = resolveConsoleOutput(argv);
|
|
361
391
|
const axiom = resolveCliAxiom(opts);
|
|
362
392
|
const axiomKey = axiom?.dataset;
|
|
363
|
-
const rebuildForConfig = configLevel !== void 0 && level !== rootLevel && !argv.verbose && !argv.quiet;
|
|
393
|
+
const rebuildForConfig = configLevel !== void 0 && level !== rootLevel && !argv.verbose && !argv.quiet && !argv.log;
|
|
364
394
|
if (!root || rebuildForConfig || axiomKey !== void 0 && axiomKey !== rootAxiomKey) {
|
|
365
395
|
const built = createLogger({
|
|
366
396
|
...level ? { level } : {},
|
|
367
|
-
...axiom ? { axiom } : {}
|
|
397
|
+
...axiom ? { axiom } : {},
|
|
398
|
+
...consoleOutput !== void 0 ? { consoleOutput } : {}
|
|
368
399
|
});
|
|
369
400
|
root = {
|
|
370
401
|
logger: rootCommand ? built.logger.child({ command: rootCommand }) : built.logger,
|
|
@@ -375,9 +406,35 @@ function buildCliLogger(argv, opts = {}) {
|
|
|
375
406
|
}
|
|
376
407
|
return root;
|
|
377
408
|
}
|
|
378
|
-
/**
|
|
409
|
+
/**
|
|
410
|
+
* Lazily-memoized `Logger` for module-level call sites with no `argv` in
|
|
411
|
+
* scope — notably yargs' own `.fail()` handler, which fires on a
|
|
412
|
+
* pre-middleware validation failure (a missing required argument, an
|
|
413
|
+
* unknown command) and therefore runs before `buildCliLogger` has ever
|
|
414
|
+
* built the real root. Without an explicit `consoleOutput` here, that
|
|
415
|
+
* fallback would use `createLogger`'s bare auto-detection (pretty on any
|
|
416
|
+
* local TTY) and leak a structured block even though no flag asked for one
|
|
417
|
+
* — so it resolves the same "quiet unless CI" default `buildCliLogger`
|
|
418
|
+
* would, as if no flags were passed.
|
|
419
|
+
*/
|
|
379
420
|
function getLogger() {
|
|
380
|
-
return (root ??= createLogger()).logger;
|
|
421
|
+
return (root ??= createLogger({ consoleOutput: resolveConsoleOutput({}) })).logger;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Report a fatal, command-ending CLI error: always printed to the console
|
|
425
|
+
* (regardless of `consoleOutput` suppression) via `style.fail`, and also
|
|
426
|
+
* sent through the structured logger for Axiom/telemetry visibility.
|
|
427
|
+
*
|
|
428
|
+
* Use this when a handler is returning or exiting right here with nothing
|
|
429
|
+
* else for the user to see — a resolved `--token` failure, a yargs
|
|
430
|
+
* validation failure, a thrown domain error. Don't use it for a per-step
|
|
431
|
+
* warning inside a multi-step orchestrator that already prints its own
|
|
432
|
+
* `ok / fail / skip` summary — that summary is the user-facing signal;
|
|
433
|
+
* the per-step reason belongs in the structured log alone.
|
|
434
|
+
*/
|
|
435
|
+
function reportError(message, log = getLogger()) {
|
|
436
|
+
console.error(style.fail(message));
|
|
437
|
+
log.error(message);
|
|
381
438
|
}
|
|
382
439
|
/** The current root logger's correlation id, if a root has been built. */
|
|
383
440
|
function getRunId() {
|
|
@@ -637,17 +694,6 @@ async function withSpinner(label, fn) {
|
|
|
637
694
|
}
|
|
638
695
|
}
|
|
639
696
|
//#endregion
|
|
640
|
-
//#region src/ui/style.ts
|
|
641
|
-
const style = {
|
|
642
|
-
success: (msg) => `${chalk.green("✓")} ${msg}`,
|
|
643
|
-
warn: (msg) => `${chalk.yellow("⚠")} ${msg}`,
|
|
644
|
-
fail: (msg) => `${chalk.red("✗")} ${msg}`,
|
|
645
|
-
step: (msg) => `${chalk.cyan("→")} ${msg}`,
|
|
646
|
-
hint: (msg) => chalk.dim(msg),
|
|
647
|
-
dim: (msg) => chalk.dim(msg),
|
|
648
|
-
header: (msg) => chalk.bold(msg)
|
|
649
|
-
};
|
|
650
|
-
//#endregion
|
|
651
697
|
//#region src/commands/auth.ts
|
|
652
698
|
/**
|
|
653
699
|
* `holocron auth <subcommand>` — manage bootstrap credentials in the
|
|
@@ -1072,12 +1118,34 @@ async function runCleanupPreview(input) {
|
|
|
1072
1118
|
}
|
|
1073
1119
|
//#endregion
|
|
1074
1120
|
//#region src/commands/clone.ts
|
|
1121
|
+
/**
|
|
1122
|
+
* Accepts a bare org (`"theholocron"`) — clones every repo in it — or an
|
|
1123
|
+
* `org/repo` coordinate (`"theholocron/new-repo"`) — clones just that one
|
|
1124
|
+
* repo. The repo half is never silently discarded: if it's there, it's used.
|
|
1125
|
+
* A trailing slash with nothing after it (`"theholocron/"`) is treated the
|
|
1126
|
+
* same as a bare org.
|
|
1127
|
+
*/
|
|
1128
|
+
function parseCloneTarget(raw) {
|
|
1129
|
+
const trimmed = raw.trim();
|
|
1130
|
+
const slash = trimmed.indexOf("/");
|
|
1131
|
+
if (slash === -1) return { org: trimmed };
|
|
1132
|
+
const org = trimmed.slice(0, slash);
|
|
1133
|
+
const repo = trimmed.slice(slash + 1);
|
|
1134
|
+
return repo ? {
|
|
1135
|
+
org,
|
|
1136
|
+
repo
|
|
1137
|
+
} : { org };
|
|
1138
|
+
}
|
|
1075
1139
|
function encodeTokenForGitHttpAuth(token) {
|
|
1076
1140
|
const trimmed = token.trim();
|
|
1077
1141
|
if (!trimmed) throw new Error("empty token");
|
|
1078
1142
|
if (/[\u0000-\u001F\u007F\s]/.test(trimmed)) throw new Error("token contains whitespace or control characters");
|
|
1079
1143
|
return encodeURIComponent(trimmed);
|
|
1080
1144
|
}
|
|
1145
|
+
/** Strip a leading dot so a hidden repo (e.g. `.github`) is still visible in Finder once cloned. */
|
|
1146
|
+
function repoDirName(repo) {
|
|
1147
|
+
return repo.name.startsWith(".") ? repo.name.slice(1) : repo.name;
|
|
1148
|
+
}
|
|
1081
1149
|
async function listOrgRepos(org, token, fetchFn) {
|
|
1082
1150
|
const repos = [];
|
|
1083
1151
|
let url = `https://api.github.com/orgs/${org}/repos?per_page=100&type=all`;
|
|
@@ -1094,6 +1162,16 @@ async function listOrgRepos(org, token, fetchFn) {
|
|
|
1094
1162
|
}
|
|
1095
1163
|
return repos;
|
|
1096
1164
|
}
|
|
1165
|
+
async function getRepo(org, repo, token, fetchFn) {
|
|
1166
|
+
const res = await fetchFn(`https://api.github.com/repos/${org}/${repo}`, { headers: {
|
|
1167
|
+
Authorization: `Bearer ${token}`,
|
|
1168
|
+
Accept: "application/vnd.github+json",
|
|
1169
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
1170
|
+
} });
|
|
1171
|
+
if (res.status === 404) throw new Error(`${org}/${repo} not found — check the repo name and that the token can see it`);
|
|
1172
|
+
if (!res.ok) throw new Error(`GitHub API ${res.status}: ${res.statusText} — check that the token has repo:read scope`);
|
|
1173
|
+
return await res.json();
|
|
1174
|
+
}
|
|
1097
1175
|
async function runClone(input) {
|
|
1098
1176
|
const print = input.print ?? ((line) => console.log(line));
|
|
1099
1177
|
const logger = input.logger ?? getLogger();
|
|
@@ -1102,6 +1180,7 @@ async function runClone(input) {
|
|
|
1102
1180
|
const targetDir = resolve(input.dir ?? join(homedir(), "Code", input.org));
|
|
1103
1181
|
logger.info({
|
|
1104
1182
|
org: input.org,
|
|
1183
|
+
repo: input.repo,
|
|
1105
1184
|
targetDir,
|
|
1106
1185
|
dryRun: dryRun || void 0
|
|
1107
1186
|
}, "clone: start");
|
|
@@ -1111,20 +1190,20 @@ async function runClone(input) {
|
|
|
1111
1190
|
stdio: "inherit"
|
|
1112
1191
|
}).status };
|
|
1113
1192
|
});
|
|
1114
|
-
print(style.header(`Holocron clone — org=${input.org} → ${targetDir}${dryRun ? " (dry-run)" : ""}`));
|
|
1115
1193
|
if (!existsSync(targetDir)) {
|
|
1116
1194
|
if (dryRun) print(style.dim(` would create ${targetDir}`));
|
|
1117
1195
|
else mkdirSync(targetDir, { recursive: true });
|
|
1118
1196
|
}
|
|
1119
1197
|
let repos;
|
|
1120
1198
|
try {
|
|
1121
|
-
repos = await listOrgRepos(input.org, input.token, fetchFn);
|
|
1199
|
+
repos = input.repo ? [await getRepo(input.org, input.repo, input.token, fetchFn)] : await listOrgRepos(input.org, input.token, fetchFn);
|
|
1122
1200
|
} catch (err) {
|
|
1123
1201
|
const message = errorMessage(err);
|
|
1124
1202
|
logger.warn({
|
|
1125
1203
|
org: input.org,
|
|
1204
|
+
repo: input.repo,
|
|
1126
1205
|
reason: message
|
|
1127
|
-
}, "clone: failed to list
|
|
1206
|
+
}, "clone: failed to list repos");
|
|
1128
1207
|
return {
|
|
1129
1208
|
status: "fail",
|
|
1130
1209
|
cloned: 0,
|
|
@@ -1133,14 +1212,35 @@ async function runClone(input) {
|
|
|
1133
1212
|
message
|
|
1134
1213
|
};
|
|
1135
1214
|
}
|
|
1215
|
+
if (!input.repo && !dryRun && input.confirmWholeOrg) {
|
|
1216
|
+
const alreadyCloned = repos.filter((r) => existsSync(join(targetDir, repoDirName(r)))).length;
|
|
1217
|
+
const scope = {
|
|
1218
|
+
total: repos.length,
|
|
1219
|
+
alreadyCloned,
|
|
1220
|
+
toClone: repos.length - alreadyCloned
|
|
1221
|
+
};
|
|
1222
|
+
if (!await input.confirmWholeOrg(scope)) {
|
|
1223
|
+
print(style.dim(" aborted — nothing cloned"));
|
|
1224
|
+
logger.info({
|
|
1225
|
+
org: input.org,
|
|
1226
|
+
...scope
|
|
1227
|
+
}, "clone: aborted by user");
|
|
1228
|
+
return {
|
|
1229
|
+
status: "aborted",
|
|
1230
|
+
cloned: 0,
|
|
1231
|
+
skipped: 0,
|
|
1232
|
+
failed: 0,
|
|
1233
|
+
message: "aborted by user"
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1136
1237
|
let cloned = 0;
|
|
1137
1238
|
let skipped = 0;
|
|
1138
1239
|
let failed = 0;
|
|
1139
1240
|
for (const repo of repos) {
|
|
1140
|
-
const
|
|
1141
|
-
const dest = join(targetDir, dirName);
|
|
1241
|
+
const dest = join(targetDir, repoDirName(repo));
|
|
1142
1242
|
if (existsSync(dest)) {
|
|
1143
|
-
print(style.dim(` skip ${repo.full_name}`));
|
|
1243
|
+
print(style.dim(` skip ${repo.full_name} → ${dest} (already exists)`));
|
|
1144
1244
|
skipped++;
|
|
1145
1245
|
continue;
|
|
1146
1246
|
}
|
|
@@ -1149,10 +1249,10 @@ async function runClone(input) {
|
|
|
1149
1249
|
cloned++;
|
|
1150
1250
|
continue;
|
|
1151
1251
|
}
|
|
1152
|
-
print(style.step(` clone ${repo.full_name}`));
|
|
1252
|
+
print(style.step(` clone ${repo.full_name} → ${dest}`));
|
|
1153
1253
|
const { clone_url } = repo;
|
|
1154
1254
|
if (!clone_url.startsWith("https://github.com/")) {
|
|
1155
|
-
print(style.fail(` failed ${repo.full_name}
|
|
1255
|
+
print(style.fail(` failed ${repo.full_name} → unexpected clone URL: ${clone_url}`));
|
|
1156
1256
|
failed++;
|
|
1157
1257
|
continue;
|
|
1158
1258
|
}
|
|
@@ -1160,7 +1260,7 @@ async function runClone(input) {
|
|
|
1160
1260
|
try {
|
|
1161
1261
|
encodedToken = encodeTokenForGitHttpAuth(input.token);
|
|
1162
1262
|
} catch (err) {
|
|
1163
|
-
print(style.fail(` failed ${repo.full_name}
|
|
1263
|
+
print(style.fail(` failed ${repo.full_name} → invalid token format: ${errorMessage(err)}`));
|
|
1164
1264
|
failed++;
|
|
1165
1265
|
continue;
|
|
1166
1266
|
}
|
|
@@ -1173,20 +1273,26 @@ async function runClone(input) {
|
|
|
1173
1273
|
print(style.fail(` failed ${repo.full_name}`));
|
|
1174
1274
|
failed++;
|
|
1175
1275
|
} else {
|
|
1176
|
-
print(style.success(` cloned ${repo.name}`));
|
|
1276
|
+
print(style.success(` cloned ${repo.name} → ${dest}`));
|
|
1177
1277
|
cloned++;
|
|
1178
1278
|
}
|
|
1179
1279
|
}
|
|
1180
1280
|
const summary = `${cloned} cloned, ${skipped} skipped, ${failed} failed`;
|
|
1181
|
-
print(
|
|
1281
|
+
print("");
|
|
1282
|
+
print(dryRun ? style.dim(` dry-run: ${summary}`) : failed > 0 ? style.fail(` ${summary}`) : style.success(` ${summary}`));
|
|
1182
1283
|
const status = dryRun ? "dry-run" : failed > 0 ? "fail" : "ok";
|
|
1183
1284
|
logger[failed > 0 ? "warn" : "info"]({
|
|
1184
1285
|
org: input.org,
|
|
1286
|
+
repo: input.repo,
|
|
1185
1287
|
status,
|
|
1186
1288
|
cloned,
|
|
1187
1289
|
skipped,
|
|
1188
1290
|
failed
|
|
1189
1291
|
}, "clone: done");
|
|
1292
|
+
if (input.confirmOpen) {
|
|
1293
|
+
const openTarget = input.repo ? join(targetDir, repoDirName(repos[0])) : targetDir;
|
|
1294
|
+
if (existsSync(openTarget)) await input.confirmOpen(openTarget, dryRun && cloned > 0);
|
|
1295
|
+
}
|
|
1190
1296
|
return {
|
|
1191
1297
|
status,
|
|
1192
1298
|
cloned,
|
|
@@ -7986,6 +8092,15 @@ function parseGitRemoteUrl(url) {
|
|
|
7986
8092
|
* {@link promptForPositionals} for just that one command — no spawn needed,
|
|
7987
8093
|
* the handler already has everything else it needs.
|
|
7988
8094
|
*
|
|
8095
|
+
* For a command whose handler honours `--dry-run` ({@link
|
|
8096
|
+
* CommandEntry.supportsDryRun}), `launchMenu` always previews it first — a
|
|
8097
|
+
* dry run doesn't mutate anything, so there's nothing to ask permission
|
|
8098
|
+
* for — then, once that preview comes back clean and found something to
|
|
8099
|
+
* do, asks the one real decision: "proceed with the real run?". A preview
|
|
8100
|
+
* that finds nothing to do skips that question too. `--skip-dry-run` opts
|
|
8101
|
+
* a top-level invocation out of the preview entirely, straight to the
|
|
8102
|
+
* real run, for when you already know what you want.
|
|
8103
|
+
*
|
|
7989
8104
|
* Every prompt is preceded by a `process.stdin.isTTY` check. A non-TTY run
|
|
7990
8105
|
* (CI, a script, a pipe) throws {@link NonInteractiveError} instead of
|
|
7991
8106
|
* hanging on `@inquirer/prompts` (which otherwise throws its own
|
|
@@ -8009,18 +8124,26 @@ const numeric = (value) => /^\d+$/.test(value) ? true : "enter a number";
|
|
|
8009
8124
|
const COMMAND_REGISTRY = [
|
|
8010
8125
|
{
|
|
8011
8126
|
name: "clone",
|
|
8012
|
-
description: "Clone
|
|
8013
|
-
positionals: [
|
|
8127
|
+
description: "Clone a single repo (org/repo) or every repo in an org (org)",
|
|
8128
|
+
positionals: [{
|
|
8129
|
+
key: "target",
|
|
8130
|
+
message: "org/repo to clone (or a bare org to clone every repo in it):",
|
|
8131
|
+
type: "input",
|
|
8132
|
+
validate: (v) => v.trim().length > 0 || "org/repo or org is required"
|
|
8133
|
+
}],
|
|
8134
|
+
supportsDryRun: true
|
|
8014
8135
|
},
|
|
8015
8136
|
{
|
|
8016
8137
|
name: "doctor",
|
|
8017
8138
|
description: "Load the config and run a smoke check against every provider",
|
|
8018
|
-
positionals: []
|
|
8139
|
+
positionals: [],
|
|
8140
|
+
supportsDryRun: true
|
|
8019
8141
|
},
|
|
8020
8142
|
{
|
|
8021
8143
|
name: "setup",
|
|
8022
8144
|
description: "Apply infra setup actions across every configured capability",
|
|
8023
|
-
positionals: []
|
|
8145
|
+
positionals: [],
|
|
8146
|
+
supportsDryRun: true
|
|
8024
8147
|
},
|
|
8025
8148
|
{
|
|
8026
8149
|
name: "secret set",
|
|
@@ -8029,7 +8152,8 @@ const COMMAND_REGISTRY = [
|
|
|
8029
8152
|
key: "name",
|
|
8030
8153
|
message: "Secret name:",
|
|
8031
8154
|
type: "input"
|
|
8032
|
-
}]
|
|
8155
|
+
}],
|
|
8156
|
+
supportsDryRun: true
|
|
8033
8157
|
},
|
|
8034
8158
|
{
|
|
8035
8159
|
name: "secrets sync",
|
|
@@ -8038,7 +8162,8 @@ const COMMAND_REGISTRY = [
|
|
|
8038
8162
|
key: "environmentId",
|
|
8039
8163
|
message: "Vault environment id:",
|
|
8040
8164
|
type: "input"
|
|
8041
|
-
}]
|
|
8165
|
+
}],
|
|
8166
|
+
supportsDryRun: true
|
|
8042
8167
|
},
|
|
8043
8168
|
{
|
|
8044
8169
|
name: "deploy",
|
|
@@ -8047,7 +8172,8 @@ const COMMAND_REGISTRY = [
|
|
|
8047
8172
|
key: "branch",
|
|
8048
8173
|
message: "Branch to deploy:",
|
|
8049
8174
|
type: "input"
|
|
8050
|
-
}]
|
|
8175
|
+
}],
|
|
8176
|
+
supportsDryRun: true
|
|
8051
8177
|
},
|
|
8052
8178
|
{
|
|
8053
8179
|
name: "cleanup-preview",
|
|
@@ -8057,7 +8183,8 @@ const COMMAND_REGISTRY = [
|
|
|
8057
8183
|
message: "PR number:",
|
|
8058
8184
|
type: "input",
|
|
8059
8185
|
validate: numeric
|
|
8060
|
-
}]
|
|
8186
|
+
}],
|
|
8187
|
+
supportsDryRun: true
|
|
8061
8188
|
},
|
|
8062
8189
|
{
|
|
8063
8190
|
name: "bump-versions",
|
|
@@ -8067,32 +8194,38 @@ const COMMAND_REGISTRY = [
|
|
|
8067
8194
|
cliArg: "new-version",
|
|
8068
8195
|
message: "New version:",
|
|
8069
8196
|
type: "input"
|
|
8070
|
-
}]
|
|
8197
|
+
}],
|
|
8198
|
+
supportsDryRun: true
|
|
8071
8199
|
},
|
|
8072
8200
|
{
|
|
8073
8201
|
name: "publish",
|
|
8074
8202
|
description: "Publish @theholocron/* packages to npm",
|
|
8075
|
-
positionals: []
|
|
8203
|
+
positionals: [],
|
|
8204
|
+
supportsDryRun: true
|
|
8076
8205
|
},
|
|
8077
8206
|
{
|
|
8078
8207
|
name: "sync",
|
|
8079
8208
|
description: "Sync state from config to the provider and local files (labels, properties, teams, topics, keywords, description, homepage, readme, workflows, scripts, wiki)",
|
|
8080
|
-
positionals: []
|
|
8209
|
+
positionals: [],
|
|
8210
|
+
supportsDryRun: true
|
|
8081
8211
|
},
|
|
8082
8212
|
{
|
|
8083
8213
|
name: "ci",
|
|
8084
8214
|
description: "Run the merge-gating checks locally, in CI order — 'will CI pass?'",
|
|
8085
|
-
positionals: []
|
|
8215
|
+
positionals: [],
|
|
8216
|
+
supportsDryRun: true
|
|
8086
8217
|
},
|
|
8087
8218
|
{
|
|
8088
8219
|
name: "sync-github",
|
|
8089
8220
|
description: "Sync workflow templates and composite actions to theholocron/.github",
|
|
8090
|
-
positionals: []
|
|
8221
|
+
positionals: [],
|
|
8222
|
+
supportsDryRun: true
|
|
8091
8223
|
},
|
|
8092
8224
|
{
|
|
8093
8225
|
name: "sync-readme",
|
|
8094
8226
|
description: "Sync the Installation + Usage block in README.md from package.json",
|
|
8095
|
-
positionals: []
|
|
8227
|
+
positionals: [],
|
|
8228
|
+
supportsDryRun: true
|
|
8096
8229
|
},
|
|
8097
8230
|
{
|
|
8098
8231
|
name: "config show",
|
|
@@ -8102,7 +8235,8 @@ const COMMAND_REGISTRY = [
|
|
|
8102
8235
|
{
|
|
8103
8236
|
name: "new",
|
|
8104
8237
|
description: "Scaffold a new repo from a GitHub template (e.g. cli, react, nextjs, node, monorepo, base)",
|
|
8105
|
-
positionals: []
|
|
8238
|
+
positionals: [],
|
|
8239
|
+
supportsDryRun: true
|
|
8106
8240
|
},
|
|
8107
8241
|
{
|
|
8108
8242
|
name: "plugin create",
|
|
@@ -8115,25 +8249,29 @@ const COMMAND_REGISTRY = [
|
|
|
8115
8249
|
key: "vendor",
|
|
8116
8250
|
message: "Vendor display name (PascalCase):",
|
|
8117
8251
|
type: "input"
|
|
8118
|
-
}]
|
|
8252
|
+
}],
|
|
8253
|
+
supportsDryRun: true
|
|
8119
8254
|
},
|
|
8120
8255
|
{
|
|
8121
8256
|
name: "skills install",
|
|
8122
8257
|
description: "Copy skills from @theholocron/skills into .agents/ with agent symlinks",
|
|
8123
8258
|
positionals: [],
|
|
8124
|
-
group: "skills"
|
|
8259
|
+
group: "skills",
|
|
8260
|
+
supportsDryRun: true
|
|
8125
8261
|
},
|
|
8126
8262
|
{
|
|
8127
8263
|
name: "skills remove",
|
|
8128
8264
|
description: "Remove installed skills via npx skills remove",
|
|
8129
8265
|
positionals: [],
|
|
8130
|
-
group: "skills"
|
|
8266
|
+
group: "skills",
|
|
8267
|
+
supportsDryRun: true
|
|
8131
8268
|
},
|
|
8132
8269
|
{
|
|
8133
8270
|
name: "skills update",
|
|
8134
8271
|
description: "Update installed skills to their latest upstream versions via npx skills update",
|
|
8135
8272
|
positionals: [],
|
|
8136
|
-
group: "skills"
|
|
8273
|
+
group: "skills",
|
|
8274
|
+
supportsDryRun: true
|
|
8137
8275
|
},
|
|
8138
8276
|
{
|
|
8139
8277
|
name: "upgrade node",
|
|
@@ -8144,13 +8282,15 @@ const COMMAND_REGISTRY = [
|
|
|
8144
8282
|
type: "input",
|
|
8145
8283
|
validate: numeric
|
|
8146
8284
|
}],
|
|
8147
|
-
group: "upgrade"
|
|
8285
|
+
group: "upgrade",
|
|
8286
|
+
supportsDryRun: true
|
|
8148
8287
|
},
|
|
8149
8288
|
{
|
|
8150
8289
|
name: "upgrade deps",
|
|
8151
8290
|
description: "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API",
|
|
8152
8291
|
positionals: [],
|
|
8153
|
-
group: "upgrade"
|
|
8292
|
+
group: "upgrade",
|
|
8293
|
+
supportsDryRun: true
|
|
8154
8294
|
},
|
|
8155
8295
|
{
|
|
8156
8296
|
name: "auth set",
|
|
@@ -8265,25 +8405,41 @@ async function promptForPositionals(entry, argv) {
|
|
|
8265
8405
|
* menu-launched child should inherit from the parent invocation. Everything
|
|
8266
8406
|
* else (command-specific options) was never captured by the top-level `$0`
|
|
8267
8407
|
* handler in the first place, so there's nothing else to forward.
|
|
8408
|
+
*
|
|
8409
|
+
* `dryRunOverride`, when passed, wins over `argv.dryRun` — `launchMenu`'s
|
|
8410
|
+
* dry-run offer needs to force `--dry-run` on for the preview spawn and
|
|
8411
|
+
* force it back off for the real-run spawn that follows, regardless of
|
|
8412
|
+
* what was on the original invocation.
|
|
8268
8413
|
*/
|
|
8269
|
-
function forwardedFlags(argv) {
|
|
8414
|
+
function forwardedFlags(argv, dryRunOverride) {
|
|
8270
8415
|
const out = [];
|
|
8271
8416
|
const tokens = argv.token;
|
|
8272
8417
|
for (const t of tokens ?? []) out.push("--token", t);
|
|
8273
8418
|
if (typeof argv.org === "string") out.push("--org", argv.org);
|
|
8274
8419
|
if (typeof argv.cwd === "string") out.push("--cwd", argv.cwd);
|
|
8275
|
-
if (argv.dryRun === true) out.push("--dry-run");
|
|
8420
|
+
if (dryRunOverride ?? argv.dryRun === true) out.push("--dry-run");
|
|
8276
8421
|
return out;
|
|
8277
8422
|
}
|
|
8278
8423
|
/** `entry.name` split into tokens, resolved positionals appended, then forwarded flags. */
|
|
8279
|
-
function buildChildArgv(entry, positionals, parentArgv) {
|
|
8424
|
+
function buildChildArgv(entry, positionals, parentArgv, dryRunOverride) {
|
|
8280
8425
|
return [
|
|
8281
8426
|
...entry.name.split(" "),
|
|
8282
8427
|
...positionals,
|
|
8283
|
-
...forwardedFlags(parentArgv)
|
|
8428
|
+
...forwardedFlags(parentArgv, dryRunOverride)
|
|
8284
8429
|
];
|
|
8285
8430
|
}
|
|
8286
8431
|
/**
|
|
8432
|
+
* Set on a dry-run preview child's env ({@link spawnChild}'s `extraEnv`) so
|
|
8433
|
+
* its handler knows this specific `--dry-run` came from `launchMenu`'s own
|
|
8434
|
+
* preview-then-ask flow, not a direct `holocron <cmd> --dry-run` invocation.
|
|
8435
|
+
* A handler that supports it uses this — never bare `argv.dryRun` — to
|
|
8436
|
+
* decide whether to report {@link DRY_RUN_NOOP_EXIT_CODE}: that exit code
|
|
8437
|
+
* only has special meaning to `launchMenu` itself, so a direct/scripted
|
|
8438
|
+
* `--dry-run` call must never produce it — doing so unconditionally would
|
|
8439
|
+
* silently change the public exit-code contract for a successful dry run.
|
|
8440
|
+
*/
|
|
8441
|
+
const DRY_RUN_PREVIEW_ENV_VAR = "HOLOCRON_DRY_RUN_PREVIEW";
|
|
8442
|
+
/**
|
|
8287
8443
|
* Spawn `holocron <...args>` inheriting stdio; resolve with its exit code.
|
|
8288
8444
|
*
|
|
8289
8445
|
* `NO_UPDATE_NOTIFIER` is forced on in the child's env — the parent's own
|
|
@@ -8291,15 +8447,17 @@ function buildChildArgv(entry, positionals, parentArgv) {
|
|
|
8291
8447
|
* docstring), so without this the freshly-spawned child independently runs
|
|
8292
8448
|
* its own `checkForUpdates()` too and the notice prints twice: once from
|
|
8293
8449
|
* the child right after its command output, once more from the parent
|
|
8294
|
-
* once the child exits.
|
|
8450
|
+
* once the child exits. `extraEnv` layers on top — used only for {@link
|
|
8451
|
+
* DRY_RUN_PREVIEW_ENV_VAR} on the dry-run preview spawn.
|
|
8295
8452
|
*/
|
|
8296
|
-
function spawnChild(args) {
|
|
8453
|
+
function spawnChild(args, extraEnv) {
|
|
8297
8454
|
return new Promise((resolve) => {
|
|
8298
8455
|
const child = spawn(process.execPath, [process.argv[1], ...args], {
|
|
8299
8456
|
stdio: "inherit",
|
|
8300
8457
|
env: {
|
|
8301
8458
|
...process.env,
|
|
8302
|
-
NO_UPDATE_NOTIFIER: "1"
|
|
8459
|
+
NO_UPDATE_NOTIFIER: "1",
|
|
8460
|
+
...extraEnv
|
|
8303
8461
|
}
|
|
8304
8462
|
});
|
|
8305
8463
|
child.on("exit", (code) => resolve(code ?? 0));
|
|
@@ -8307,6 +8465,17 @@ function spawnChild(args) {
|
|
|
8307
8465
|
});
|
|
8308
8466
|
}
|
|
8309
8467
|
/**
|
|
8468
|
+
* Whether `launchMenu` should auto-preview a picked command before running
|
|
8469
|
+
* it for real — only when the command's handler actually honours
|
|
8470
|
+
* `--dry-run` ({@link CommandEntry.supportsDryRun}) and the top-level
|
|
8471
|
+
* invocation didn't pass `--skip-dry-run` to opt out of the preview
|
|
8472
|
+
* entirely (an escape hatch for "I already know what I want, skip
|
|
8473
|
+
* straight to the real run").
|
|
8474
|
+
*/
|
|
8475
|
+
function shouldPreviewDryRun(entry, parentArgv) {
|
|
8476
|
+
return Boolean(entry.supportsDryRun) && parentArgv.skipDryRun !== true;
|
|
8477
|
+
}
|
|
8478
|
+
/**
|
|
8310
8479
|
* Layers 1 and 2: pick a command from `entries`, prompt for its required
|
|
8311
8480
|
* positionals, spawn it as a fresh `holocron` invocation, and set
|
|
8312
8481
|
* `process.exitCode` from the child — the caller (a `$0` handler in
|
|
@@ -8315,12 +8484,79 @@ function spawnChild(args) {
|
|
|
8315
8484
|
* `command_completed` event fires with command name "unknown" (the
|
|
8316
8485
|
* middleware ran before any command was picked) — left as-is rather than
|
|
8317
8486
|
* suppressed; it's a real, useful signal ("the menu got used").
|
|
8487
|
+
*
|
|
8488
|
+
* For a command that honours `--dry-run` ({@link CommandEntry.supportsDryRun})
|
|
8489
|
+
* and wasn't opted out via `--skip-dry-run` ({@link shouldPreviewDryRun}),
|
|
8490
|
+
* this always previews first — a dry run doesn't mutate anything, so
|
|
8491
|
+
* there's nothing to ask permission for. A preview that exits non-zero
|
|
8492
|
+
* stops here; one that reports {@link DRY_RUN_NOOP_EXIT_CODE} (nothing
|
|
8493
|
+
* would change) stops here too, skipping the follow-up question since the
|
|
8494
|
+
* real run would have nothing to do either. Otherwise it asks "proceed
|
|
8495
|
+
* with the real run?" before the real spawn — the one actual decision
|
|
8496
|
+
* left to make, since this one does mutate something.
|
|
8318
8497
|
*/
|
|
8319
8498
|
async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessage = "Run `holocron --help` to see available commands.") {
|
|
8320
8499
|
if (!process.stdin.isTTY) throw new NonInteractiveError(nonInteractiveMessage);
|
|
8321
8500
|
const picked = await pickCommand(entries, pickMessage);
|
|
8322
|
-
const
|
|
8323
|
-
|
|
8501
|
+
const positionals = await promptForPositionals(picked, {});
|
|
8502
|
+
if (shouldPreviewDryRun(picked, parentArgv)) {
|
|
8503
|
+
const dryExit = await spawnChild(buildChildArgv(picked, positionals, parentArgv, true), { [DRY_RUN_PREVIEW_ENV_VAR]: "1" });
|
|
8504
|
+
if (dryExit === 2) {
|
|
8505
|
+
process.exitCode = 0;
|
|
8506
|
+
return;
|
|
8507
|
+
}
|
|
8508
|
+
if (dryExit !== 0) {
|
|
8509
|
+
process.exitCode = dryExit;
|
|
8510
|
+
return;
|
|
8511
|
+
}
|
|
8512
|
+
if (!await confirm({
|
|
8513
|
+
message: "Dry run complete — proceed with the real run?",
|
|
8514
|
+
default: false
|
|
8515
|
+
})) {
|
|
8516
|
+
process.exitCode = 0;
|
|
8517
|
+
return;
|
|
8518
|
+
}
|
|
8519
|
+
process.exitCode = await spawnChild(buildChildArgv(picked, positionals, parentArgv, false));
|
|
8520
|
+
return;
|
|
8521
|
+
}
|
|
8522
|
+
process.exitCode = await spawnChild(buildChildArgv(picked, positionals, parentArgv));
|
|
8523
|
+
}
|
|
8524
|
+
//#endregion
|
|
8525
|
+
//#region src/ui/open-path.ts
|
|
8526
|
+
/** Platform-appropriate command to open a path in the OS's file browser. */
|
|
8527
|
+
function openCommand(path) {
|
|
8528
|
+
if (process.platform === "darwin") return {
|
|
8529
|
+
cmd: "open",
|
|
8530
|
+
args: [path]
|
|
8531
|
+
};
|
|
8532
|
+
if (process.platform === "win32") return {
|
|
8533
|
+
cmd: "cmd",
|
|
8534
|
+
args: [
|
|
8535
|
+
"/c",
|
|
8536
|
+
"start",
|
|
8537
|
+
"",
|
|
8538
|
+
path
|
|
8539
|
+
]
|
|
8540
|
+
};
|
|
8541
|
+
return {
|
|
8542
|
+
cmd: "xdg-open",
|
|
8543
|
+
args: [path]
|
|
8544
|
+
};
|
|
8545
|
+
}
|
|
8546
|
+
/**
|
|
8547
|
+
* Open `path` in the OS's default file browser (Finder / Explorer /
|
|
8548
|
+
* whatever `xdg-open` resolves to on Linux). Fire-and-forget — detached so
|
|
8549
|
+
* it outlives this process, and never throws when the opener binary isn't
|
|
8550
|
+
* on PATH (e.g. no `xdg-open` in a minimal container).
|
|
8551
|
+
*/
|
|
8552
|
+
function openPath(path) {
|
|
8553
|
+
const { cmd, args } = openCommand(path);
|
|
8554
|
+
const child = spawn(cmd, args, {
|
|
8555
|
+
stdio: "ignore",
|
|
8556
|
+
detached: true
|
|
8557
|
+
});
|
|
8558
|
+
child.on("error", () => {});
|
|
8559
|
+
child.unref();
|
|
8324
8560
|
}
|
|
8325
8561
|
//#endregion
|
|
8326
8562
|
//#region src/update-notifier.ts
|
|
@@ -8466,6 +8702,19 @@ function resolveOrg(argv, config) {
|
|
|
8466
8702
|
return argv.org ?? env.get("HOLOCRON_ORG") ?? config.org;
|
|
8467
8703
|
}
|
|
8468
8704
|
/**
|
|
8705
|
+
* Print the dim version line once per terminal session — keyed by the
|
|
8706
|
+
* parent shell's PID, since that's stable across repeated bare `holocron`
|
|
8707
|
+
* invocations in the same shell but changes in a new tab/window. A marker
|
|
8708
|
+
* file in the OS tmpdir tracks whether this shell has already seen it, so
|
|
8709
|
+
* re-running `holocron` doesn't repeat the line every time.
|
|
8710
|
+
*/
|
|
8711
|
+
function printVersionHeaderOnce() {
|
|
8712
|
+
const marker = join(tmpdir(), `.holocron-version-shown-${process.ppid}`);
|
|
8713
|
+
if (existsSync(marker)) return;
|
|
8714
|
+
console.log(style.dim(`v${CLI_VERSION}`));
|
|
8715
|
+
writeFileSync(marker, "");
|
|
8716
|
+
}
|
|
8717
|
+
/**
|
|
8469
8718
|
* `buildCliLogger` options derived from a loaded `holocron.config` — the
|
|
8470
8719
|
* level, the Axiom dataset, and the resolved org for a keyring-backed
|
|
8471
8720
|
* Axiom token. Used by every handler that has already called `loadConfig`.
|
|
@@ -8495,7 +8744,7 @@ function tokenContext(rawTokens) {
|
|
|
8495
8744
|
return parseTokenArgs(rawTokens);
|
|
8496
8745
|
} catch (err) {
|
|
8497
8746
|
if (err instanceof TokenParseError) {
|
|
8498
|
-
|
|
8747
|
+
reportError(`--token: ${err.message}`);
|
|
8499
8748
|
process.exitCode = 1;
|
|
8500
8749
|
return null;
|
|
8501
8750
|
}
|
|
@@ -8512,6 +8761,10 @@ try {
|
|
|
8512
8761
|
type: "boolean",
|
|
8513
8762
|
default: false,
|
|
8514
8763
|
describe: "Print what would be mutated without calling capability mutators. Commands branch on this; read-only commands ignore it."
|
|
8764
|
+
}).option("skip-dry-run", {
|
|
8765
|
+
type: "boolean",
|
|
8766
|
+
default: false,
|
|
8767
|
+
describe: "Interactive mode only: skip the automatic dry-run preview for a dry-run-capable command and run for real immediately."
|
|
8515
8768
|
}).option("token", {
|
|
8516
8769
|
type: "string",
|
|
8517
8770
|
array: true,
|
|
@@ -8535,6 +8788,10 @@ try {
|
|
|
8535
8788
|
type: "boolean",
|
|
8536
8789
|
default: false,
|
|
8537
8790
|
describe: "Set the log level to error — suppress info and warn."
|
|
8791
|
+
}).option("log", {
|
|
8792
|
+
type: "string",
|
|
8793
|
+
choices: LOG_LEVELS,
|
|
8794
|
+
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."
|
|
8538
8795
|
}).middleware((argv) => {
|
|
8539
8796
|
const name = argv._.slice(0, 2).join(" ") || "unknown";
|
|
8540
8797
|
printRunId = Boolean(argv.debug || argv.verbose);
|
|
@@ -8543,10 +8800,9 @@ try {
|
|
|
8543
8800
|
org: argv.org
|
|
8544
8801
|
});
|
|
8545
8802
|
finishCommand = startCommand(name);
|
|
8546
|
-
}).command("clone", "Clone
|
|
8803
|
+
}).command("clone [target]", "Clone a single repo (org/repo) or every repo in an org (org)", (y) => y.positional("target", {
|
|
8547
8804
|
type: "string",
|
|
8548
|
-
|
|
8549
|
-
describe: "GitHub org to clone (e.g., theholocron)"
|
|
8805
|
+
describe: "org/repo to clone, or a bare org to clone every repo in it (confirms first)."
|
|
8550
8806
|
}).option("dir", {
|
|
8551
8807
|
type: "string",
|
|
8552
8808
|
describe: "Parent directory to clone into (default: ~/Code/<org>)"
|
|
@@ -8557,16 +8813,39 @@ try {
|
|
|
8557
8813
|
try {
|
|
8558
8814
|
token = resolveCloneToken({ cliToken: tokens.cliTokens?.["github"] ?? tokens.cliToken });
|
|
8559
8815
|
} catch (err) {
|
|
8560
|
-
|
|
8816
|
+
reportError(`clone: ${err instanceof AuthError ? err.message : String(err)}`);
|
|
8561
8817
|
process.exitCode = 1;
|
|
8562
8818
|
return;
|
|
8563
8819
|
}
|
|
8564
|
-
|
|
8565
|
-
|
|
8820
|
+
const [targetInput] = await promptForPositionals(getEntry("clone"), argv);
|
|
8821
|
+
const { org, repo } = parseCloneTarget(targetInput);
|
|
8822
|
+
const report = await runClone({
|
|
8823
|
+
org,
|
|
8824
|
+
...repo ? { repo } : {},
|
|
8566
8825
|
token,
|
|
8567
8826
|
dryRun: argv.dryRun,
|
|
8568
|
-
...argv.dir ? { dir: argv.dir } : {}
|
|
8569
|
-
|
|
8827
|
+
...argv.dir ? { dir: argv.dir } : {},
|
|
8828
|
+
...!repo ? { confirmWholeOrg: async ({ total, alreadyCloned, toClone }) => {
|
|
8829
|
+
if (!process.stdin.isTTY) throw new NonInteractiveError(`clone: cloning every repo in "${org}" needs interactive confirmation — run this from a terminal, or pass a specific org/repo instead`);
|
|
8830
|
+
if (toClone === 0) return true;
|
|
8831
|
+
return confirm({
|
|
8832
|
+
message: `${org} has ${total} repos — ${alreadyCloned} already cloned locally, ${toClone} new. Clone them all?`,
|
|
8833
|
+
default: false
|
|
8834
|
+
});
|
|
8835
|
+
} } : {},
|
|
8836
|
+
confirmOpen: async (openTarget, moreToCome) => {
|
|
8837
|
+
if (!process.stdin.isTTY) return;
|
|
8838
|
+
if (moreToCome && env.get("HOLOCRON_DRY_RUN_PREVIEW") === "1") return;
|
|
8839
|
+
if (await confirm({
|
|
8840
|
+
message: `Open ${openTarget}?`,
|
|
8841
|
+
default: false
|
|
8842
|
+
})) openPath(openTarget);
|
|
8843
|
+
}
|
|
8844
|
+
});
|
|
8845
|
+
if (report.status === "fail") {
|
|
8846
|
+
if (report.message) reportError(`clone: ${report.message}`);
|
|
8847
|
+
process.exitCode = 1;
|
|
8848
|
+
} else if (report.status === "dry-run" && report.cloned === 0 && env.get("HOLOCRON_DRY_RUN_PREVIEW") === "1") process.exitCode = 2;
|
|
8570
8849
|
}).command("doctor", "Load the config and run a smoke check against every provider", (y) => y.option("repo", {
|
|
8571
8850
|
type: "string",
|
|
8572
8851
|
describe: "Repo coords (\"owner/name\"). Defaults to plugin-specific resolution."
|
|
@@ -8826,7 +9105,7 @@ try {
|
|
|
8826
9105
|
if (argv.syncTrust) {
|
|
8827
9106
|
const [oldFile, newFile] = argv.syncTrust;
|
|
8828
9107
|
if (!oldFile || !newFile) {
|
|
8829
|
-
|
|
9108
|
+
reportError("publish --sync-trust requires exactly two values: <old-file> <new-file>");
|
|
8830
9109
|
process.exitCode = 1;
|
|
8831
9110
|
return;
|
|
8832
9111
|
}
|
|
@@ -8986,7 +9265,7 @@ try {
|
|
|
8986
9265
|
else try {
|
|
8987
9266
|
token = resolveSyncToken({ cliToken: parsed.cliTokens?.["github"] ?? parsed.cliToken });
|
|
8988
9267
|
} catch (err) {
|
|
8989
|
-
|
|
9268
|
+
reportError(`sync-github: ${err instanceof AuthError ? err.message : String(err)}`);
|
|
8990
9269
|
process.exitCode = 1;
|
|
8991
9270
|
return;
|
|
8992
9271
|
}
|
|
@@ -9006,13 +9285,17 @@ try {
|
|
|
9006
9285
|
}), async (argv) => {
|
|
9007
9286
|
const loaded = await loadConfig(argv.cwd);
|
|
9008
9287
|
applyResolvedConfig(argv, loaded.resolved);
|
|
9009
|
-
|
|
9288
|
+
const report = await runSyncReadme({
|
|
9010
9289
|
loaded,
|
|
9011
9290
|
context: {
|
|
9012
9291
|
repoRoot: argv.cwd,
|
|
9013
9292
|
dryRun: argv.dryRun
|
|
9014
9293
|
}
|
|
9015
|
-
})
|
|
9294
|
+
});
|
|
9295
|
+
if (report.status === "fail") {
|
|
9296
|
+
if (report.message) reportError(`sync-readme: ${report.message}`);
|
|
9297
|
+
process.exitCode = 1;
|
|
9298
|
+
}
|
|
9016
9299
|
}).command("config show", "Print the resolved holocron config", () => {}, async (argv) => {
|
|
9017
9300
|
const loaded = await loadConfig(argv.cwd);
|
|
9018
9301
|
console.log(JSON.stringify(loaded.resolved, null, 2));
|
|
@@ -9241,12 +9524,12 @@ try {
|
|
|
9241
9524
|
}) === "yes";
|
|
9242
9525
|
if (skills.length === 0) skills = parseTopics(await input({ message: "Agent skills (comma-separated, optional):" }));
|
|
9243
9526
|
if (!type) {
|
|
9244
|
-
|
|
9527
|
+
reportError("new: template type is required");
|
|
9245
9528
|
process.exitCode = 1;
|
|
9246
9529
|
return;
|
|
9247
9530
|
}
|
|
9248
9531
|
if (!name) {
|
|
9249
|
-
|
|
9532
|
+
reportError("new: repo name is required");
|
|
9250
9533
|
process.exitCode = 1;
|
|
9251
9534
|
return;
|
|
9252
9535
|
}
|
|
@@ -9275,7 +9558,7 @@ try {
|
|
|
9275
9558
|
})).status === "fail") process.exitCode = 1;
|
|
9276
9559
|
} catch (err) {
|
|
9277
9560
|
if (err instanceof NewError) {
|
|
9278
|
-
|
|
9561
|
+
reportError(`new: ${err.message}`);
|
|
9279
9562
|
process.exitCode = 1;
|
|
9280
9563
|
return;
|
|
9281
9564
|
}
|
|
@@ -9334,7 +9617,7 @@ try {
|
|
|
9334
9617
|
}).status === "fail") process.exitCode = 1;
|
|
9335
9618
|
} catch (err) {
|
|
9336
9619
|
if (err instanceof PluginCreateError) {
|
|
9337
|
-
|
|
9620
|
+
reportError(`plugin create: ${err.message}`);
|
|
9338
9621
|
process.exitCode = 1;
|
|
9339
9622
|
return;
|
|
9340
9623
|
}
|
|
@@ -9362,7 +9645,7 @@ try {
|
|
|
9362
9645
|
extra
|
|
9363
9646
|
});
|
|
9364
9647
|
if (report.status === "fail") {
|
|
9365
|
-
if (report.message)
|
|
9648
|
+
if (report.message) reportError(`upgrade node: ${report.message}`);
|
|
9366
9649
|
process.exitCode = 1;
|
|
9367
9650
|
}
|
|
9368
9651
|
}).command("deps", "Bump every @theholocron/* pin to latest and migrate holocron.config.ts to the current preset API", (yy) => yy.option("pins-only", {
|
|
@@ -9376,7 +9659,7 @@ try {
|
|
|
9376
9659
|
pinsOnly: argv.pinsOnly
|
|
9377
9660
|
});
|
|
9378
9661
|
if (report.status === "fail") {
|
|
9379
|
-
if (report.message)
|
|
9662
|
+
if (report.message) reportError(`upgrade deps: ${report.message}`);
|
|
9380
9663
|
process.exitCode = 1;
|
|
9381
9664
|
}
|
|
9382
9665
|
}).command("$0", false, () => {}, async (argv) => {
|
|
@@ -9402,25 +9685,25 @@ try {
|
|
|
9402
9685
|
}).command("$0", false, () => {}, async (argv) => {
|
|
9403
9686
|
await launchMenu(COMMAND_REGISTRY.filter((e) => e.group === "auth"), argv, "auth — choose a subcommand:", "Run `holocron auth --help` to see available auth subcommands.");
|
|
9404
9687
|
}), () => {}).command("$0", false, () => {}, async (argv) => {
|
|
9405
|
-
|
|
9688
|
+
printVersionHeaderOnce();
|
|
9406
9689
|
await launchMenu(COMMAND_REGISTRY.filter((e) => !e.group), argv);
|
|
9407
9690
|
}).strict().help().epilogue(`Execution contexts:
|
|
9408
9691
|
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) => {
|
|
9409
9692
|
if (err instanceof Error && USER_FACING_ERRORS.has(err.name)) {
|
|
9410
9693
|
captureException(err);
|
|
9411
|
-
|
|
9694
|
+
reportError(err.message);
|
|
9412
9695
|
errorReported = true;
|
|
9413
9696
|
process.exitCode = 1;
|
|
9414
9697
|
return;
|
|
9415
9698
|
}
|
|
9416
9699
|
if (err) throw err;
|
|
9417
|
-
|
|
9700
|
+
reportError(msg);
|
|
9418
9701
|
errorReported = true;
|
|
9419
9702
|
process.exitCode = 1;
|
|
9420
9703
|
}).parse();
|
|
9421
9704
|
} catch (err) {
|
|
9422
9705
|
captureException(err);
|
|
9423
|
-
if (!errorReported && err instanceof Error && USER_FACING_ERRORS.has(err.name))
|
|
9706
|
+
if (!errorReported && err instanceof Error && USER_FACING_ERRORS.has(err.name)) reportError(err.message);
|
|
9424
9707
|
if (!process.exitCode) process.exitCode = 1;
|
|
9425
9708
|
}
|
|
9426
9709
|
finishCommand(!process.exitCode);
|