@theholocron/cli 5.0.0-alpha.177 → 5.0.0-alpha.178
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 +52 -4
- package/dist/cli.mjs +576 -76
- package/dist/cli.mjs.map +1 -1
- package/package.json +3 -3
package/dist/cli.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { homedir, hostname, tmpdir, userInfo } from "node:os";
|
|
5
5
|
import path, { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
6
|
-
import { checkbox, confirm, input, select } from "@inquirer/prompts";
|
|
6
|
+
import { checkbox, confirm, input, password, select } from "@inquirer/prompts";
|
|
7
7
|
import { KNOWN_WORKFLOWS, createAlexignore, createAlexrc, createAstromech, createCommitMsg, createDependabot, createDevmoji, createEditorconfig, createEditorconfigChecker, createHeader, createLabeler, createPreCommit, createPrePush, createPrepareCommitMsg, createYamlignore, createYamllint, defaultListDirs, defaultPaths, defaultReadJson, deployOnRelease, extractPreviewConfig, readWorkspacePackages, reusableTemplates, workspacePackages } from "@theholocron/astromech";
|
|
8
8
|
import { loadTasksConfig } from "@theholocron/astromech/config";
|
|
9
9
|
import { lintCommitMsgFile } from "@theholocron/astromech/lint";
|
|
@@ -691,7 +691,8 @@ function searchChoices(items, term) {
|
|
|
691
691
|
return [...pool].sort((a, b) => rank(a) - rank(b)).map((i) => ({
|
|
692
692
|
name: i.name,
|
|
693
693
|
value: i.value,
|
|
694
|
-
description: i.description
|
|
694
|
+
description: i.description,
|
|
695
|
+
short: i.name.replace(/ ›$/, "")
|
|
695
696
|
}));
|
|
696
697
|
}
|
|
697
698
|
//#endregion
|
|
@@ -750,6 +751,200 @@ async function withSpinner(label, fn) {
|
|
|
750
751
|
}
|
|
751
752
|
}
|
|
752
753
|
//#endregion
|
|
754
|
+
//#region src/ui/prompt-options/prompt-options.ts
|
|
755
|
+
/**
|
|
756
|
+
* A required option must not resolve to an empty answer — `optionArgv`/
|
|
757
|
+
* `secretEnv` (child-argv.ts) both drop an empty-string value entirely,
|
|
758
|
+
* which would silently invoke the child without a flag the caller asked
|
|
759
|
+
* for. Passed as `validate` so inquirer re-prompts instead of accepting it.
|
|
760
|
+
*/
|
|
761
|
+
const requireNonEmpty = (value) => value.trim().length > 0 || "required";
|
|
762
|
+
async function promptOne(option) {
|
|
763
|
+
if (option.secret || option.type === "password") return await password({
|
|
764
|
+
message: option.message,
|
|
765
|
+
...option.required ? { validate: requireNonEmpty } : {}
|
|
766
|
+
});
|
|
767
|
+
if (option.type === "confirm") return await confirm({
|
|
768
|
+
message: option.message,
|
|
769
|
+
default: option.default
|
|
770
|
+
});
|
|
771
|
+
if (option.type === "select") {
|
|
772
|
+
const choices = typeof option.choices === "function" ? option.choices() : option.choices ?? [];
|
|
773
|
+
return await select({
|
|
774
|
+
message: option.message,
|
|
775
|
+
choices: choices.map((c) => ({ value: c }))
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
if (option.arity && option.arity > 1) {
|
|
779
|
+
const values = [];
|
|
780
|
+
for (let n = 0; n < option.arity; n++) values.push(await input({
|
|
781
|
+
message: `${option.message} (${n + 1} of ${option.arity})`,
|
|
782
|
+
...option.required ? { validate: requireNonEmpty } : {}
|
|
783
|
+
}));
|
|
784
|
+
return values;
|
|
785
|
+
}
|
|
786
|
+
return await input({
|
|
787
|
+
message: option.message,
|
|
788
|
+
default: option.default,
|
|
789
|
+
...option.required ? { validate: requireNonEmpty } : {}
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* For each of `options` not already present in `argv`, resolves a value —
|
|
794
|
+
* `required` ones are always included, like required positionals, never
|
|
795
|
+
* behind a selector; everything else is offered through one `checkbox()`
|
|
796
|
+
* ("set any options?") and only the picked ones are then prompted in
|
|
797
|
+
* `options`' own declaration order. An empty list (or every optional one
|
|
798
|
+
* left unpicked) means zero prompts.
|
|
799
|
+
*
|
|
800
|
+
* A picked option (whether required or checkbox-picked) with
|
|
801
|
+
* `askLast: true` (holocron#1052 follow-up — a time-sensitive secret like
|
|
802
|
+
* an OTP, which should be typed as close as possible to when it's actually
|
|
803
|
+
* used, or one that's always needed for a specific operator's own account
|
|
804
|
+
* but not everyone's) isn't given a value here yet: it's returned in
|
|
805
|
+
* `deferred` instead, for the caller to resolve via
|
|
806
|
+
* `promptForDeferredOptions` right before the real (non-dry-run) spawn.
|
|
807
|
+
*
|
|
808
|
+
* `when`, evaluated against answers collected so far in that same
|
|
809
|
+
* declaration order, can skip a picked option entirely. Skipped this way,
|
|
810
|
+
* the option is simply absent from the result, exactly as if it had never
|
|
811
|
+
* been picked — acceptable for a minor, genuinely optional dependency, but
|
|
812
|
+
* prefer `OptionMode` (see `promptForOptions`) when the options have a
|
|
813
|
+
* real either/or relationship, so nothing contingent is shown at all.
|
|
814
|
+
*/
|
|
815
|
+
async function promptForFlatOptions(commandName, options, argv) {
|
|
816
|
+
if (options.length === 0) return {
|
|
817
|
+
answers: {},
|
|
818
|
+
deferred: []
|
|
819
|
+
};
|
|
820
|
+
const answers = {};
|
|
821
|
+
const deferred = [];
|
|
822
|
+
const needsPrompt = (option) => {
|
|
823
|
+
const existing = argv[option.key];
|
|
824
|
+
return existing === void 0 || existing === "";
|
|
825
|
+
};
|
|
826
|
+
const required = options.filter((o) => o.required);
|
|
827
|
+
const optional = options.filter((o) => !o.required);
|
|
828
|
+
if ((required.some(needsPrompt) || optional.some(needsPrompt)) && !process.stdin.isTTY) throw new NonInteractiveError(`\`${commandName}\` has options only settable interactively — pass them directly: holocron ${commandName} [options]`);
|
|
829
|
+
for (const option of required) {
|
|
830
|
+
if (!needsPrompt(option)) {
|
|
831
|
+
answers[option.key] = argv[option.key];
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (option.askLast) {
|
|
835
|
+
deferred.push(option);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
answers[option.key] = await promptOne(option);
|
|
839
|
+
}
|
|
840
|
+
for (const option of optional) if (!needsPrompt(option)) answers[option.key] = argv[option.key];
|
|
841
|
+
const selectable = optional.filter(needsPrompt);
|
|
842
|
+
if (selectable.length === 0) return {
|
|
843
|
+
answers,
|
|
844
|
+
deferred
|
|
845
|
+
};
|
|
846
|
+
const picked = await checkbox({
|
|
847
|
+
message: "Set any options? (space to select, enter to skip)",
|
|
848
|
+
choices: selectable.map((o) => ({
|
|
849
|
+
name: o.message,
|
|
850
|
+
value: o.key
|
|
851
|
+
}))
|
|
852
|
+
});
|
|
853
|
+
if (picked.length === 0) return {
|
|
854
|
+
answers,
|
|
855
|
+
deferred
|
|
856
|
+
};
|
|
857
|
+
const pickedSet = new Set(picked);
|
|
858
|
+
for (const option of optional) {
|
|
859
|
+
if (!pickedSet.has(option.key)) continue;
|
|
860
|
+
if (option.when && !option.when(answers)) continue;
|
|
861
|
+
if (option.askLast) {
|
|
862
|
+
deferred.push(option);
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
answers[option.key] = await promptOne(option);
|
|
866
|
+
}
|
|
867
|
+
return {
|
|
868
|
+
answers,
|
|
869
|
+
deferred
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* For a `modes`-based command (holocron#1052 follow-up): asks which
|
|
874
|
+
* mutually exclusive workflow first ("What would you like to do?"), then
|
|
875
|
+
* offers only that mode's own options, via the exact same
|
|
876
|
+
* `promptForFlatOptions` flow. The picked mode's `flag`, if it has one, is
|
|
877
|
+
* folded in as an already-resolved `confirm`-type option so
|
|
878
|
+
* `buildChildArgv` renders it with no special-casing — it just sees
|
|
879
|
+
* another boolean option whose answer happens to already be `true`.
|
|
880
|
+
*/
|
|
881
|
+
async function promptForModes(entry, modes, argv) {
|
|
882
|
+
if (!process.stdin.isTTY) throw new NonInteractiveError(`\`${entry.name}\` has options only settable interactively — pass them directly: holocron ${entry.name} [options]`);
|
|
883
|
+
const modeKey = await select({
|
|
884
|
+
message: "What would you like to do?",
|
|
885
|
+
choices: modes.map((m) => ({
|
|
886
|
+
name: m.message,
|
|
887
|
+
value: m.key,
|
|
888
|
+
description: m.description
|
|
889
|
+
}))
|
|
890
|
+
});
|
|
891
|
+
const mode = modes.find((m) => m.key === modeKey);
|
|
892
|
+
const options = mode.options ?? [];
|
|
893
|
+
const { answers, deferred } = await promptForFlatOptions(entry.name, options, argv);
|
|
894
|
+
if (!mode.flag) return {
|
|
895
|
+
options,
|
|
896
|
+
answers,
|
|
897
|
+
deferred
|
|
898
|
+
};
|
|
899
|
+
const flagOption = {
|
|
900
|
+
key: `__mode_${mode.key}`,
|
|
901
|
+
flag: mode.flag,
|
|
902
|
+
type: "confirm",
|
|
903
|
+
message: mode.message
|
|
904
|
+
};
|
|
905
|
+
return {
|
|
906
|
+
options: [flagOption, ...options],
|
|
907
|
+
answers: {
|
|
908
|
+
[flagOption.key]: true,
|
|
909
|
+
...answers
|
|
910
|
+
},
|
|
911
|
+
deferred
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
/**
|
|
915
|
+
* Resolves `entry`'s options into a {@link ResolvedOptions} ready for
|
|
916
|
+
* `buildChildArgv`/`secretEnv` — delegating to {@link promptForModes} when
|
|
917
|
+
* `entry.modes` is set, or treating `entry.options` as a flat list
|
|
918
|
+
* otherwise. See `promptForFlatOptions` for the flat-list prompting
|
|
919
|
+
* algorithm itself. `result.deferred` still needs
|
|
920
|
+
* {@link promptForDeferredOptions} before the real spawn.
|
|
921
|
+
*/
|
|
922
|
+
async function promptForOptions(entry, argv) {
|
|
923
|
+
if (entry.modes && entry.modes.length > 0) return promptForModes(entry, entry.modes, argv);
|
|
924
|
+
const options = entry.options ?? [];
|
|
925
|
+
const { answers, deferred } = await promptForFlatOptions(entry.name, options, argv);
|
|
926
|
+
return {
|
|
927
|
+
options,
|
|
928
|
+
answers,
|
|
929
|
+
deferred
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Collects a value for each of `deferred` — the `askLast: true` options
|
|
934
|
+
* `promptForOptions` deliberately left unresolved. Call this as the very
|
|
935
|
+
* last thing before the real (non-dry-run) spawn, never before: a dry-run
|
|
936
|
+
* preview never reaches the real npm call these options exist for (an OTP
|
|
937
|
+
* is never checked during `--dry-run`), and a time-sensitive code typed
|
|
938
|
+
* any earlier — before a dry-run preview runs, before "proceed with the
|
|
939
|
+
* real run?" waits on a human — risks going stale before it's actually
|
|
940
|
+
* used. A no-op (no prompt at all) when `deferred` is empty.
|
|
941
|
+
*/
|
|
942
|
+
async function promptForDeferredOptions(deferred) {
|
|
943
|
+
const answers = {};
|
|
944
|
+
for (const option of deferred) answers[option.key] = await promptOne(option);
|
|
945
|
+
return answers;
|
|
946
|
+
}
|
|
947
|
+
//#endregion
|
|
753
948
|
//#region src/ui/prompt-positionals/prompt-positionals.ts
|
|
754
949
|
/**
|
|
755
950
|
* For each of `entry.positionals` not already present in `argv`, prompts for
|
|
@@ -4181,8 +4376,22 @@ function printNextSteps$1(print, inputs) {
|
|
|
4181
4376
|
print(" 7. Commit + push when capability is functionally complete.");
|
|
4182
4377
|
}
|
|
4183
4378
|
//#endregion
|
|
4379
|
+
//#region src/commands/publish/resolve-otp.ts
|
|
4380
|
+
/**
|
|
4381
|
+
* `argvOtp` if given, else the `HOLOCRON_OTP` env var — the secret-delivery
|
|
4382
|
+
* side of the interactive menu's options prompt (holocron#1052): a
|
|
4383
|
+
* menu-launched `package publish` delivers `--otp` through this env var
|
|
4384
|
+
* instead of argv, since `ps` and shell history both expose a child's argv
|
|
4385
|
+
* but never its env. A direct `holocron package publish --otp <code>`
|
|
4386
|
+
* invocation is unaffected; the flag always wins when both are present.
|
|
4387
|
+
*/
|
|
4388
|
+
function resolveOtp(argvOtp, env) {
|
|
4389
|
+
return argvOtp ?? env.get("HOLOCRON_OTP");
|
|
4390
|
+
}
|
|
4391
|
+
//#endregion
|
|
4184
4392
|
//#region src/commands/publish/default-exec.ts
|
|
4185
4393
|
const defaultExec$1 = async (cmd, args, opts) => {
|
|
4394
|
+
const { HOLOCRON_OTP: _omittedOtp, ...env } = process.env;
|
|
4186
4395
|
const result = spawnSync(cmd, args, {
|
|
4187
4396
|
cwd: opts.cwd,
|
|
4188
4397
|
encoding: "utf8",
|
|
@@ -4190,7 +4399,8 @@ const defaultExec$1 = async (cmd, args, opts) => {
|
|
|
4190
4399
|
"inherit",
|
|
4191
4400
|
"pipe",
|
|
4192
4401
|
"pipe"
|
|
4193
|
-
]
|
|
4402
|
+
],
|
|
4403
|
+
env
|
|
4194
4404
|
});
|
|
4195
4405
|
return {
|
|
4196
4406
|
exitCode: result.status ?? -1,
|
|
@@ -4244,46 +4454,117 @@ function discoverManifests(cwd) {
|
|
|
4244
4454
|
}));
|
|
4245
4455
|
}
|
|
4246
4456
|
//#endregion
|
|
4247
|
-
//#region src/commands/publish/discover-public-
|
|
4248
|
-
/**
|
|
4249
|
-
|
|
4250
|
-
|
|
4457
|
+
//#region src/commands/publish/discover-public-package-entries.ts
|
|
4458
|
+
/**
|
|
4459
|
+
* Every public package (`!private && name`) with its version and a
|
|
4460
|
+
* `pnpm --filter`-ready path — {@link runSteadyPublish}'s `skipAlreadyPublished`
|
|
4461
|
+
* mode needs both to check `npm view <pkg>@<version>` and publish one
|
|
4462
|
+
* workspace at a time.
|
|
4463
|
+
*/
|
|
4464
|
+
function discoverPublicPackageEntries(cwd) {
|
|
4465
|
+
return discoverManifests(cwd).flatMap(({ manifest, dir }) => !manifest.private && manifest.name && manifest.version ? [{
|
|
4466
|
+
name: manifest.name,
|
|
4467
|
+
version: manifest.version,
|
|
4468
|
+
dir
|
|
4469
|
+
}] : []);
|
|
4470
|
+
}
|
|
4471
|
+
//#endregion
|
|
4472
|
+
//#region src/commands/publish/filter-new-packages.ts
|
|
4473
|
+
/**
|
|
4474
|
+
* Keeps only the entries that don't exist on npm yet — the actual
|
|
4475
|
+
* chicken-and-egg case `--initial` exists for (ADR: npm requires a package
|
|
4476
|
+
* to exist before Trusted Publishing can be configured on it). Once a name
|
|
4477
|
+
* exists on npm at all, regardless of version, Trusted Publishing can
|
|
4478
|
+
* already be configured for it and every later version goes through the
|
|
4479
|
+
* steady-state publish instead — re-running `--initial` against it would
|
|
4480
|
+
* just hit a stale `pnpm publish` with no `--skip-already-published` safety
|
|
4481
|
+
* net. Existence, not version: unlike the steady-state publish's own
|
|
4482
|
+
* already-published check, this intentionally ignores the local version —
|
|
4483
|
+
* a package that exists on npm at an older version is still not `--initial`'s
|
|
4484
|
+
* problem.
|
|
4485
|
+
*
|
|
4486
|
+
* Prints one `statusRow("skip", …)` line per already-published entry as it
|
|
4487
|
+
* finds them — same shape `clone` uses for an already-cloned repo, with the
|
|
4488
|
+
* registry's own version (not the local one) in the message, since that's
|
|
4489
|
+
* what "already published" actually means — so the caller's own summary
|
|
4490
|
+
* line is the only other thing it needs to print. `nameWidth`/`dirWidth`
|
|
4491
|
+
* (computed by the caller across every discovered entry, not just the ones
|
|
4492
|
+
* landing here) keep these rows column-aligned with the "would
|
|
4493
|
+
* publish"/"published" rows the caller prints afterward for the entries
|
|
4494
|
+
* that come back from this function — the two sets only read as one table
|
|
4495
|
+
* when both pad to the same widths.
|
|
4496
|
+
*/
|
|
4497
|
+
async function filterNewPackages(entries, cwd, exec, print, nameWidth, dirWidth) {
|
|
4498
|
+
const results = [];
|
|
4499
|
+
for (const entry of entries) {
|
|
4500
|
+
const view = await exec("npm", [
|
|
4501
|
+
"view",
|
|
4502
|
+
entry.name,
|
|
4503
|
+
"version"
|
|
4504
|
+
], { cwd });
|
|
4505
|
+
const publishedVersion = view.exitCode === 0 ? view.stdout.trim() : "";
|
|
4506
|
+
if (publishedVersion) {
|
|
4507
|
+
print(statusRow("skip", [
|
|
4508
|
+
pad(entry.name, nameWidth),
|
|
4509
|
+
pad(entry.dir, dirWidth),
|
|
4510
|
+
`already published (v${publishedVersion})`
|
|
4511
|
+
]));
|
|
4512
|
+
continue;
|
|
4513
|
+
}
|
|
4514
|
+
results.push(entry);
|
|
4515
|
+
}
|
|
4516
|
+
return results;
|
|
4251
4517
|
}
|
|
4252
4518
|
//#endregion
|
|
4253
4519
|
//#region src/commands/publish/print-next-steps.ts
|
|
4254
|
-
function printNextSteps(print, env, packageNames,
|
|
4520
|
+
function printNextSteps(print, env, packageNames, identity) {
|
|
4521
|
+
print("");
|
|
4522
|
+
print("→ next: configure Trusted Publisher for each package on npm:");
|
|
4523
|
+
print(" Publisher: GitHub Actions");
|
|
4524
|
+
print(" Label: Holocron Release");
|
|
4525
|
+
print(` Organization: ${identity.owner}`);
|
|
4526
|
+
print(` Repository: ${identity.repo}`);
|
|
4527
|
+
print(" Workflow filename: delivery.publish.yml");
|
|
4255
4528
|
print("");
|
|
4256
|
-
print("
|
|
4529
|
+
print("→ packages to be published:");
|
|
4257
4530
|
for (const name of packageNames) print(` https://www.npmjs.com/package/${name}/access`);
|
|
4258
|
-
print(` Publisher: GitHub Actions Org: theholocron Repo: ${repoName} Workflow: delivery.publish.yml`);
|
|
4259
4531
|
if (env.get("NPM_TOKEN")) {
|
|
4260
4532
|
print("");
|
|
4261
|
-
print("
|
|
4533
|
+
print("→ cleanup: $NPM_TOKEN was used. Revoke it now (no API for self-revoke; UI-only):");
|
|
4262
4534
|
print(" https://www.npmjs.com/settings/~/tokens");
|
|
4263
4535
|
}
|
|
4264
4536
|
}
|
|
4265
4537
|
//#endregion
|
|
4266
|
-
//#region src/commands/publish/
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4538
|
+
//#region src/commands/publish/redact-otp.ts
|
|
4539
|
+
/**
|
|
4540
|
+
* Replaces the literal `otp` value in a "would run: pnpm ..." dry-run
|
|
4541
|
+
* preview line with `***` — the real `args` passed to `exec()` still need
|
|
4542
|
+
* the raw OTP (npm's own `--otp` flag has no env-var alternative), but the
|
|
4543
|
+
* preview is printed straight to the terminal / captured as dry-run output
|
|
4544
|
+
* and must never echo a live 2FA code back out.
|
|
4545
|
+
*/
|
|
4546
|
+
function redactOtp(args, otp) {
|
|
4547
|
+
if (!otp) return [...args];
|
|
4548
|
+
return args.map((arg) => arg === otp ? "***" : arg);
|
|
4273
4549
|
}
|
|
4274
4550
|
//#endregion
|
|
4275
|
-
//#region src/commands/publish/
|
|
4551
|
+
//#region src/commands/publish/resolve-repo-identity.ts
|
|
4276
4552
|
/**
|
|
4277
|
-
*
|
|
4278
|
-
*
|
|
4279
|
-
*
|
|
4280
|
-
*
|
|
4553
|
+
* One git-remote read for both the org and repo shown in the Trusted
|
|
4554
|
+
* Publisher next-steps output — `readGitRemote` already parses out `owner`
|
|
4555
|
+
* alongside `repo`, so there's no reason to shell out to git twice for the
|
|
4556
|
+
* two halves of the same remote URL.
|
|
4281
4557
|
*/
|
|
4282
|
-
function
|
|
4283
|
-
const
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4558
|
+
async function resolveRepoIdentity(cwd, exec) {
|
|
4559
|
+
const remote = await readGitRemote(cwd, { exec: async (args, options) => {
|
|
4560
|
+
const result = await exec("git", args, options);
|
|
4561
|
+
if (result.exitCode !== 0) throw new Error(`git ${args.join(" ")} exited ${result.exitCode}`);
|
|
4562
|
+
return result.stdout;
|
|
4563
|
+
} });
|
|
4564
|
+
return {
|
|
4565
|
+
owner: remote?.owner ?? "unknown",
|
|
4566
|
+
repo: remote?.repo ?? "unknown"
|
|
4567
|
+
};
|
|
4287
4568
|
}
|
|
4288
4569
|
//#endregion
|
|
4289
4570
|
//#region src/commands/publish/run-publish/run-publish.ts
|
|
@@ -4313,10 +4594,31 @@ function workspaceFilterArgs(cwd) {
|
|
|
4313
4594
|
* outside the semantic-release/OIDC steady state) isn't built yet; the flag
|
|
4314
4595
|
* exists so the surface doesn't need another rename when it is.
|
|
4315
4596
|
*
|
|
4597
|
+
* Self-narrowing: a package that already exists on npm (any version) isn't
|
|
4598
|
+
* the chicken-and-egg case anymore — Trusted Publishing can already be
|
|
4599
|
+
* configured for it, and every later version goes through the steady-state
|
|
4600
|
+
* publish instead. So every publicly publishable package on disk is checked
|
|
4601
|
+
* against the registry first, and only the ones that don't exist yet are
|
|
4602
|
+
* actually targeted. Re-running `--initial` after adding one new package to
|
|
4603
|
+
* an already-bootstrapped monorepo publishes just that one. Nothing left to
|
|
4604
|
+
* bootstrap is reported as a success, not a failure.
|
|
4605
|
+
*
|
|
4316
4606
|
* If `NPM_TOKEN` is detected in env, the command prints a final
|
|
4317
4607
|
* "revoke this token at <url>" reminder — same pattern as `rando vc
|
|
4318
4608
|
* setup` for the ephemeral GH admin PAT.
|
|
4319
4609
|
*/
|
|
4610
|
+
/**
|
|
4611
|
+
* The same `N <verb>, N failed, N skipped` shape `clone` ends on, styled the
|
|
4612
|
+
* same way (dim for a dry-run preview, success/fail for the real thing) —
|
|
4613
|
+
* `--initial` publishes in one atomic `pnpm publish` call rather than
|
|
4614
|
+
* `clone`'s per-repo loop, so `failedCount` is always either 0 or every
|
|
4615
|
+
* targeted package at once, never a partial count.
|
|
4616
|
+
*/
|
|
4617
|
+
function printSummary(print, dryRun, publishedCount, failedCount, skippedCount) {
|
|
4618
|
+
const summary = `${publishedCount} published, ${failedCount} failed, ${skippedCount} skipped`;
|
|
4619
|
+
print("");
|
|
4620
|
+
print(dryRun ? style.dim(` dry-run: ${summary}`) : failedCount > 0 ? style.fail(` ${summary}`) : style.success(` ${summary}`));
|
|
4621
|
+
}
|
|
4320
4622
|
async function runPublish(input = {}) {
|
|
4321
4623
|
const print = input.print ?? ((line) => console.log(line));
|
|
4322
4624
|
const logger = input.logger ?? getLogger();
|
|
@@ -4327,26 +4629,12 @@ async function runPublish(input = {}) {
|
|
|
4327
4629
|
const env = makeEnv(input.env);
|
|
4328
4630
|
const exec = input.exec ?? defaultExec$1;
|
|
4329
4631
|
const login = input.login ?? defaultLogin;
|
|
4330
|
-
const publishArgs = [
|
|
4331
|
-
...workspaceFilterArgs(cwd),
|
|
4332
|
-
"publish",
|
|
4333
|
-
"--access",
|
|
4334
|
-
"public",
|
|
4335
|
-
"--no-git-checks",
|
|
4336
|
-
"--tag",
|
|
4337
|
-
tag,
|
|
4338
|
-
...otp ? ["--otp", otp] : []
|
|
4339
|
-
];
|
|
4340
|
-
print(`Holocron publish --initial${dryRun ? " (dry-run)" : ""}`);
|
|
4341
|
-
print(` cwd: ${cwd}`);
|
|
4342
|
-
print(` tag: ${tag}`);
|
|
4343
4632
|
logger.info({
|
|
4344
4633
|
tag,
|
|
4345
4634
|
dryRun: dryRun || void 0
|
|
4346
4635
|
}, "publish --initial: start");
|
|
4347
|
-
if (otp) print(` otp: <${otp.length} chars>`);
|
|
4348
4636
|
print("");
|
|
4349
|
-
print(" → verifying npm auth (`npm whoami`)…");
|
|
4637
|
+
print(style.dim(" → verifying npm auth (`npm whoami`)…"));
|
|
4350
4638
|
let whoami = await exec("npm", ["whoami"], { cwd });
|
|
4351
4639
|
if (whoami.exitCode !== 0) {
|
|
4352
4640
|
print(" not authenticated — running `npm login --auth-type=web`…");
|
|
@@ -4354,7 +4642,7 @@ async function runPublish(input = {}) {
|
|
|
4354
4642
|
if ((await login(cwd)).exitCode !== 0) {
|
|
4355
4643
|
const message = "npm login failed. Run `npm login --auth-type=web` (or `npm login`) manually, then re-run.";
|
|
4356
4644
|
print(` ✗ ${message}`);
|
|
4357
|
-
const packageNames = input.packages ??
|
|
4645
|
+
const packageNames = (input.packages ?? discoverPublicPackageEntries(cwd)).map((e) => e.name);
|
|
4358
4646
|
logger.warn({ reason: "npm login failed" }, "publish --initial: done");
|
|
4359
4647
|
return {
|
|
4360
4648
|
status: "fail",
|
|
@@ -4366,7 +4654,7 @@ async function runPublish(input = {}) {
|
|
|
4366
4654
|
if (whoami.exitCode !== 0) {
|
|
4367
4655
|
const message = "npm login completed but `npm whoami` still fails — check your npm account.";
|
|
4368
4656
|
print(` ✗ ${message}`);
|
|
4369
|
-
const packageNames = input.packages ??
|
|
4657
|
+
const packageNames = (input.packages ?? discoverPublicPackageEntries(cwd)).map((e) => e.name);
|
|
4370
4658
|
logger.warn({ reason: "npm whoami still fails after login" }, "publish --initial: done");
|
|
4371
4659
|
return {
|
|
4372
4660
|
status: "fail",
|
|
@@ -4375,10 +4663,35 @@ async function runPublish(input = {}) {
|
|
|
4375
4663
|
};
|
|
4376
4664
|
}
|
|
4377
4665
|
}
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4666
|
+
const whoamiName = whoami.stdout.trim() || "<unknown>";
|
|
4667
|
+
print(style.dim(` ✓ authed as ${whoamiName}`));
|
|
4668
|
+
print("");
|
|
4669
|
+
if (otp) print(style.dim(` otp: <${otp.length} chars>`));
|
|
4670
|
+
const discoveredEntries = input.packages ?? discoverPublicPackageEntries(cwd);
|
|
4671
|
+
const nameWidth = Math.max(0, ...discoveredEntries.map((e) => e.name.length));
|
|
4672
|
+
const dirWidth = Math.max(0, ...discoveredEntries.map((e) => e.dir.length));
|
|
4673
|
+
const newEntries = input.packages ? [...discoveredEntries] : await filterNewPackages(discoveredEntries, cwd, exec, print, nameWidth, dirWidth);
|
|
4674
|
+
const packageNames = newEntries.map((e) => e.name);
|
|
4675
|
+
const skippedCount = input.packages ? 0 : discoveredEntries.length - newEntries.length;
|
|
4676
|
+
const resolvedIdentity = input.repoName !== void 0 && input.orgName !== void 0 ? {
|
|
4677
|
+
owner: input.orgName,
|
|
4678
|
+
repo: input.repoName
|
|
4679
|
+
} : await resolveRepoIdentity(cwd, exec);
|
|
4680
|
+
const identity = {
|
|
4681
|
+
owner: input.orgName ?? resolvedIdentity.owner,
|
|
4682
|
+
repo: input.repoName ?? resolvedIdentity.repo
|
|
4683
|
+
};
|
|
4381
4684
|
if (packageNames.length === 0) {
|
|
4685
|
+
if (input.packages === void 0 && discoveredEntries.length > 0) {
|
|
4686
|
+
const message = "nothing to publish — every public package already exists on npm; nothing new to bootstrap";
|
|
4687
|
+
printSummary(print, dryRun, 0, 0, skippedCount);
|
|
4688
|
+
logger.info({ reason: message }, "publish --initial: done");
|
|
4689
|
+
return {
|
|
4690
|
+
status: "ok",
|
|
4691
|
+
message,
|
|
4692
|
+
packageNames
|
|
4693
|
+
};
|
|
4694
|
+
}
|
|
4382
4695
|
const message = "nothing to publish (no packages/*, plugins/*, or root package.json is private or unnamed)";
|
|
4383
4696
|
print(` ✗ ${message}`);
|
|
4384
4697
|
logger.warn({ reason: message }, "publish --initial: done");
|
|
@@ -4388,11 +4701,22 @@ async function runPublish(input = {}) {
|
|
|
4388
4701
|
packageNames
|
|
4389
4702
|
};
|
|
4390
4703
|
}
|
|
4704
|
+
const publishArgs = [
|
|
4705
|
+
...hasPackagesDir(cwd) || hasPluginsDir(cwd) ? ["-r", ...packageNames.map((name) => `--filter=${name}`)] : [],
|
|
4706
|
+
"publish",
|
|
4707
|
+
"--access",
|
|
4708
|
+
"public",
|
|
4709
|
+
"--no-git-checks",
|
|
4710
|
+
"--tag",
|
|
4711
|
+
tag,
|
|
4712
|
+
...otp ? ["--otp", otp] : []
|
|
4713
|
+
];
|
|
4391
4714
|
if (dryRun) {
|
|
4392
4715
|
print("");
|
|
4393
|
-
print(
|
|
4394
|
-
print(`
|
|
4395
|
-
|
|
4716
|
+
for (const entry of newEntries) print(style.dim(` ${pad(entry.name, nameWidth)} ${pad(entry.dir, dirWidth)} would publish (v${entry.version})`));
|
|
4717
|
+
print(style.dim(` would run: pnpm ${redactOtp(publishArgs, otp).join(" ")}`));
|
|
4718
|
+
printSummary(print, dryRun, packageNames.length, 0, skippedCount);
|
|
4719
|
+
printNextSteps(print, env, packageNames, identity);
|
|
4396
4720
|
logger.info({
|
|
4397
4721
|
tag,
|
|
4398
4722
|
status: "dry-run",
|
|
@@ -4405,7 +4729,7 @@ async function runPublish(input = {}) {
|
|
|
4405
4729
|
};
|
|
4406
4730
|
}
|
|
4407
4731
|
print("");
|
|
4408
|
-
print(
|
|
4732
|
+
print(` → publishing ${packageNames.length} new package${packageNames.length === 1 ? "" : "s"}…`);
|
|
4409
4733
|
const publish = await exec("pnpm", publishArgs, { cwd });
|
|
4410
4734
|
if (publish.exitCode !== 0) {
|
|
4411
4735
|
const message = `publish failed (exit ${publish.exitCode}): ${publish.stderr.trim() || publish.stdout.trim() || "no output"}`;
|
|
@@ -4415,6 +4739,12 @@ async function runPublish(input = {}) {
|
|
|
4415
4739
|
print(" → hint: your npm account requires 2FA for writes. Re-run with `--otp <code>`:");
|
|
4416
4740
|
print(` pnpm exec tsx packages/cli/src/cli.ts package publish --initial --otp <6-digit-code>`);
|
|
4417
4741
|
}
|
|
4742
|
+
for (const entry of newEntries) print(statusRow("fail", [
|
|
4743
|
+
pad(entry.name, nameWidth),
|
|
4744
|
+
pad(entry.dir, dirWidth),
|
|
4745
|
+
"publish failed"
|
|
4746
|
+
]));
|
|
4747
|
+
printSummary(print, dryRun, 0, packageNames.length, skippedCount);
|
|
4418
4748
|
logger.warn({
|
|
4419
4749
|
tag,
|
|
4420
4750
|
reason: message
|
|
@@ -4425,8 +4755,13 @@ async function runPublish(input = {}) {
|
|
|
4425
4755
|
packageNames
|
|
4426
4756
|
};
|
|
4427
4757
|
}
|
|
4428
|
-
print("
|
|
4429
|
-
|
|
4758
|
+
for (const entry of newEntries) print(statusRow("ok", [
|
|
4759
|
+
pad(entry.name, nameWidth),
|
|
4760
|
+
pad(entry.dir, dirWidth),
|
|
4761
|
+
`published (v${entry.version})`
|
|
4762
|
+
]));
|
|
4763
|
+
printSummary(print, dryRun, packageNames.length, 0, skippedCount);
|
|
4764
|
+
printNextSteps(print, env, packageNames, identity);
|
|
4430
4765
|
logger.info({
|
|
4431
4766
|
tag,
|
|
4432
4767
|
status: "ok",
|
|
@@ -4438,19 +4773,18 @@ async function runPublish(input = {}) {
|
|
|
4438
4773
|
};
|
|
4439
4774
|
}
|
|
4440
4775
|
//#endregion
|
|
4441
|
-
//#region src/commands/publish/
|
|
4776
|
+
//#region src/commands/publish/workspace-filter-args.ts
|
|
4442
4777
|
/**
|
|
4443
|
-
*
|
|
4444
|
-
*
|
|
4445
|
-
*
|
|
4446
|
-
*
|
|
4778
|
+
* `-r --filter=./packages/* --filter=./plugins/*` — whichever workspace
|
|
4779
|
+
* roots exist. Empty (single-package repo, no `-r`) when neither does.
|
|
4780
|
+
* Shared by every `pnpm publish` invocation below so a repo with only
|
|
4781
|
+
* `plugins/*` (or both) is covered the same as `packages/*`-only.
|
|
4447
4782
|
*/
|
|
4448
|
-
function
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
}] : []);
|
|
4783
|
+
function workspaceFilterArgs(cwd) {
|
|
4784
|
+
const filters = [];
|
|
4785
|
+
if (hasPackagesDir(cwd)) filters.push("--filter=./packages/*");
|
|
4786
|
+
if (hasPluginsDir(cwd)) filters.push("--filter=./plugins/*");
|
|
4787
|
+
return filters.length > 0 ? ["-r", ...filters] : [];
|
|
4454
4788
|
}
|
|
4455
4789
|
//#endregion
|
|
4456
4790
|
//#region src/commands/publish/run-steady-publish/run-steady-publish.ts
|
|
@@ -4524,7 +4858,7 @@ async function runSteadyPublish(input = {}) {
|
|
|
4524
4858
|
...otpFlags
|
|
4525
4859
|
];
|
|
4526
4860
|
print(` … (dry-run) skipping actual publish`);
|
|
4527
|
-
print(` would run: pnpm ${args.join(" ")}`);
|
|
4861
|
+
print(` would run: pnpm ${redactOtp(args, otp).join(" ")}`);
|
|
4528
4862
|
}
|
|
4529
4863
|
logger.info({
|
|
4530
4864
|
tag,
|
|
@@ -4557,6 +4891,15 @@ async function runSteadyPublish(input = {}) {
|
|
|
4557
4891
|
...otpFlags
|
|
4558
4892
|
], { cwd });
|
|
4559
4893
|
if (publish.exitCode !== 0) {
|
|
4894
|
+
const recheck = await exec("npm", [
|
|
4895
|
+
"view",
|
|
4896
|
+
`${entry.name}@${entry.version}`,
|
|
4897
|
+
"version"
|
|
4898
|
+
], { cwd });
|
|
4899
|
+
if (recheck.exitCode === 0 && recheck.stdout.trim()) {
|
|
4900
|
+
print(` skip ${entry.name}@${entry.version} (published concurrently by another run)`);
|
|
4901
|
+
continue;
|
|
4902
|
+
}
|
|
4560
4903
|
const message = `publish failed for ${entry.name}@${entry.version} (exit ${publish.exitCode}): ${publish.stderr.trim() || publish.stdout.trim() || "no output"}`;
|
|
4561
4904
|
print(` ✗ ${message}`);
|
|
4562
4905
|
logger.warn({
|
|
@@ -4619,6 +4962,12 @@ async function runSteadyPublish(input = {}) {
|
|
|
4619
4962
|
};
|
|
4620
4963
|
}
|
|
4621
4964
|
//#endregion
|
|
4965
|
+
//#region src/commands/publish/discover-public-packages.ts
|
|
4966
|
+
/** The names of `discoverPublicPackageEntries`' packages, without requiring a version. */
|
|
4967
|
+
function discoverPublicPackages(cwd) {
|
|
4968
|
+
return discoverManifests(cwd).flatMap(({ manifest }) => !manifest.private && manifest.name ? [manifest.name] : []);
|
|
4969
|
+
}
|
|
4970
|
+
//#endregion
|
|
4622
4971
|
//#region src/commands/publish/is-eotp.ts
|
|
4623
4972
|
function isEotp(parsed) {
|
|
4624
4973
|
return typeof parsed === "object" && parsed !== null && "error" in parsed && parsed.error?.code === "EOTP";
|
|
@@ -8814,6 +9163,18 @@ async function runUpgradeNode(input) {
|
|
|
8814
9163
|
//#endregion
|
|
8815
9164
|
//#region src/interactive-menu/child-argv/child-argv.ts
|
|
8816
9165
|
/**
|
|
9166
|
+
* Whether `option`'s answer must be delivered via env, never argv — true for
|
|
9167
|
+
* an explicit `secret: true`, and also for any `type: "password"` option
|
|
9168
|
+
* even if `secret` was left unset. `promptOne` (prompt-options.ts) already
|
|
9169
|
+
* treats these two as interchangeable triggers for masking the input
|
|
9170
|
+
* itself; `buildChildArgv`/`secretEnv` need to agree, or a password-typed
|
|
9171
|
+
* option that forgot to also set `secret: true` would mask the keystrokes
|
|
9172
|
+
* on screen while still leaking the typed value into the child's argv.
|
|
9173
|
+
*/
|
|
9174
|
+
function isSecretOption(option) {
|
|
9175
|
+
return Boolean(option.secret) || option.type === "password";
|
|
9176
|
+
}
|
|
9177
|
+
/**
|
|
8817
9178
|
* `--token` (repeatable), `--org`, `--cwd`, `--dry-run` — the global flags a
|
|
8818
9179
|
* menu-launched child should inherit from the parent invocation. Everything
|
|
8819
9180
|
* else (command-specific options) was never captured by the top-level `$0`
|
|
@@ -8833,17 +9194,78 @@ function forwardedFlags(argv, dryRunOverride) {
|
|
|
8833
9194
|
if (dryRunOverride ?? argv.dryRun === true) out.push("--dry-run");
|
|
8834
9195
|
return out;
|
|
8835
9196
|
}
|
|
8836
|
-
/**
|
|
8837
|
-
|
|
9197
|
+
/**
|
|
9198
|
+
* `option`'s own argv tokens for `value` (never called for a `secret`
|
|
9199
|
+
* option — those are delivered via {@link secretEnv} instead, never argv).
|
|
9200
|
+
*
|
|
9201
|
+
* - boolean `true` → `--flag`; `false` → `--no-flag` when the yargs
|
|
9202
|
+
* default is `true` (`negatable: true` — answering "no" must actively
|
|
9203
|
+
* override it, not just omit the flag and silently keep the default),
|
|
9204
|
+
* otherwise omitted (the yargs default is already `false`).
|
|
9205
|
+
* - string/select → `--flag value`, omitted when empty.
|
|
9206
|
+
* - `arity`-valued array → `--flag v1 v2 ...`.
|
|
9207
|
+
*/
|
|
9208
|
+
function optionArgv(option, value) {
|
|
9209
|
+
if (value === void 0) return [];
|
|
9210
|
+
if (typeof value === "boolean") {
|
|
9211
|
+
if (value) return [option.flag];
|
|
9212
|
+
return option.negatable ? [`--no-${option.flag.replace(/^--/, "")}`] : [];
|
|
9213
|
+
}
|
|
9214
|
+
if (Array.isArray(value)) return value.length === 0 ? [] : [option.flag, ...value];
|
|
9215
|
+
return value === "" ? [] : [option.flag, value];
|
|
9216
|
+
}
|
|
9217
|
+
/**
|
|
9218
|
+
* `entry.spawnArgs` if set, else `entry.name` split into tokens — resolved
|
|
9219
|
+
* positionals, then non-secret option flags, then forwarded flags, appended
|
|
9220
|
+
* in that order. `resolved` is whatever {@link promptForOptions} returned —
|
|
9221
|
+
* for a `modes`-based command, that's already the picked mode's own options
|
|
9222
|
+
* (plus a synthetic entry for its `flag`, if any), so this renders either
|
|
9223
|
+
* shape identically with no need to know which kind of command it came from.
|
|
9224
|
+
*/
|
|
9225
|
+
function buildChildArgv(entry, positionals, resolved, parentArgv, dryRunOverride) {
|
|
9226
|
+
const optionArgs = resolved.options.flatMap((option) => isSecretOption(option) ? [] : optionArgv(option, resolved.answers[option.key]));
|
|
8838
9227
|
return [
|
|
8839
9228
|
...entry.spawnArgs ?? entry.name.split(" "),
|
|
8840
9229
|
...positionals,
|
|
9230
|
+
...optionArgs,
|
|
8841
9231
|
...forwardedFlags(parentArgv, dryRunOverride)
|
|
8842
9232
|
];
|
|
8843
9233
|
}
|
|
9234
|
+
/**
|
|
9235
|
+
* Secret option answers, keyed by the env var their handler reads instead
|
|
9236
|
+
* of argv (`ps` and shell history expose argv, never a child's own env) —
|
|
9237
|
+
* `HOLOCRON_<KEY_IN_SCREAMING_SNAKE_CASE>`, e.g. `otp` → `HOLOCRON_OTP`.
|
|
9238
|
+
* Merged into {@link spawnChild}'s `extraEnv` alongside `NO_UPDATE_NOTIFIER`
|
|
9239
|
+
* and (for a preview spawn) `DRY_RUN_PREVIEW_ENV_VAR`.
|
|
9240
|
+
*/
|
|
9241
|
+
function secretEnv(resolved) {
|
|
9242
|
+
const out = {};
|
|
9243
|
+
for (const option of resolved.options) {
|
|
9244
|
+
if (!isSecretOption(option)) continue;
|
|
9245
|
+
const value = resolved.answers[option.key];
|
|
9246
|
+
if (typeof value !== "string" || value === "") continue;
|
|
9247
|
+
out[`HOLOCRON_${option.key.replace(/([A-Z])/g, "_$1").toUpperCase()}`] = value;
|
|
9248
|
+
}
|
|
9249
|
+
return out;
|
|
9250
|
+
}
|
|
8844
9251
|
//#endregion
|
|
8845
9252
|
//#region src/interactive-menu/command-registry/command-registry.ts
|
|
8846
9253
|
const numeric = (value) => /^\d+$/.test(value) ? true : "enter a number";
|
|
9254
|
+
const PUBLISH_TAG_OPTION = {
|
|
9255
|
+
key: "tag",
|
|
9256
|
+
flag: "--tag",
|
|
9257
|
+
type: "input",
|
|
9258
|
+
message: "npm dist-tag (blank = alpha for a bootstrap publish, latest otherwise):"
|
|
9259
|
+
};
|
|
9260
|
+
const PUBLISH_OTP_OPTION = {
|
|
9261
|
+
key: "otp",
|
|
9262
|
+
flag: "--otp",
|
|
9263
|
+
type: "password",
|
|
9264
|
+
secret: true,
|
|
9265
|
+
askLast: true,
|
|
9266
|
+
required: Boolean(env.get("HOLOCRON_NPM_ENFORCE_2FA")),
|
|
9267
|
+
message: "One-time password (2FA), if npm requires it for this publish:"
|
|
9268
|
+
};
|
|
8847
9269
|
/**
|
|
8848
9270
|
* The full command surface, hand-maintained in parallel with `cli.ts`'s
|
|
8849
9271
|
* yargs registrations (this module does not introspect yargs at runtime).
|
|
@@ -8954,7 +9376,56 @@ const COMMAND_REGISTRY = [
|
|
|
8954
9376
|
name: "package publish",
|
|
8955
9377
|
description: "Publish @theholocron/* packages to npm",
|
|
8956
9378
|
positionals: [],
|
|
8957
|
-
supportsDryRun: true
|
|
9379
|
+
supportsDryRun: true,
|
|
9380
|
+
modes: [
|
|
9381
|
+
{
|
|
9382
|
+
key: "steady",
|
|
9383
|
+
message: "Publish a new version",
|
|
9384
|
+
options: [
|
|
9385
|
+
PUBLISH_TAG_OPTION,
|
|
9386
|
+
PUBLISH_OTP_OPTION,
|
|
9387
|
+
{
|
|
9388
|
+
key: "provenance",
|
|
9389
|
+
flag: "--provenance",
|
|
9390
|
+
type: "confirm",
|
|
9391
|
+
default: true,
|
|
9392
|
+
negatable: true,
|
|
9393
|
+
message: "Publish with --provenance? (on by default)"
|
|
9394
|
+
},
|
|
9395
|
+
{
|
|
9396
|
+
key: "skipAlreadyPublished",
|
|
9397
|
+
flag: "--skip-already-published",
|
|
9398
|
+
type: "confirm",
|
|
9399
|
+
default: false,
|
|
9400
|
+
message: "Skip a package already published at this exact version, instead of one bulk publish?"
|
|
9401
|
+
}
|
|
9402
|
+
]
|
|
9403
|
+
},
|
|
9404
|
+
{
|
|
9405
|
+
key: "initial",
|
|
9406
|
+
message: "Initial publish (--initial)",
|
|
9407
|
+
description: "Use only for a package's very first publish.",
|
|
9408
|
+
flag: "--initial",
|
|
9409
|
+
options: [PUBLISH_TAG_OPTION, PUBLISH_OTP_OPTION]
|
|
9410
|
+
},
|
|
9411
|
+
{
|
|
9412
|
+
key: "syncTrust",
|
|
9413
|
+
message: "Sync Trusted Publisher workflow filename across every public package (--sync-trust)",
|
|
9414
|
+
options: [{
|
|
9415
|
+
key: "syncTrust",
|
|
9416
|
+
flag: "--sync-trust",
|
|
9417
|
+
type: "input",
|
|
9418
|
+
arity: 2,
|
|
9419
|
+
required: true,
|
|
9420
|
+
message: "Workflow filename (old, then new)"
|
|
9421
|
+
}, {
|
|
9422
|
+
key: "repo",
|
|
9423
|
+
flag: "--repo",
|
|
9424
|
+
type: "input",
|
|
9425
|
+
message: "owner/repo for Trusted Publisher config (blank = theholocron/holocron):"
|
|
9426
|
+
}]
|
|
9427
|
+
}
|
|
9428
|
+
]
|
|
8958
9429
|
},
|
|
8959
9430
|
{
|
|
8960
9431
|
name: "repo sync",
|
|
@@ -9176,6 +9647,25 @@ function spawnChild(args, extraEnv) {
|
|
|
9176
9647
|
});
|
|
9177
9648
|
}
|
|
9178
9649
|
/**
|
|
9650
|
+
* Collects `resolved.deferred`'s values (holocron#1052 follow-up) and folds
|
|
9651
|
+
* them into `resolved.answers` — call this once, right before building the
|
|
9652
|
+
* real (non-dry-run) spawn's argv, never any earlier: a dry-run preview and
|
|
9653
|
+
* a human confirming "proceed with the real run?" can both take a while,
|
|
9654
|
+
* and a time-sensitive secret (an OTP) typed before either risks going
|
|
9655
|
+
* stale before it's actually used. A no-op when nothing was deferred.
|
|
9656
|
+
*/
|
|
9657
|
+
async function resolveForRealSpawn(resolved) {
|
|
9658
|
+
if (!resolved.deferred || resolved.deferred.length === 0) return resolved;
|
|
9659
|
+
const deferredAnswers = await promptForDeferredOptions(resolved.deferred);
|
|
9660
|
+
return {
|
|
9661
|
+
...resolved,
|
|
9662
|
+
answers: {
|
|
9663
|
+
...resolved.answers,
|
|
9664
|
+
...deferredAnswers
|
|
9665
|
+
}
|
|
9666
|
+
};
|
|
9667
|
+
}
|
|
9668
|
+
/**
|
|
9179
9669
|
* Whether `launchMenu` should auto-preview a picked command before running
|
|
9180
9670
|
* it for real — only when the command's handler actually honours
|
|
9181
9671
|
* `--dry-run` ({@link CommandEntry.supportsDryRun}) and the top-level
|
|
@@ -9228,8 +9718,13 @@ async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessag
|
|
|
9228
9718
|
else picked = entries.find((e) => e.name === value);
|
|
9229
9719
|
}
|
|
9230
9720
|
const positionals = await promptForPositionals(picked, {});
|
|
9721
|
+
const resolvedOptions = await promptForOptions(picked, {});
|
|
9722
|
+
const extraEnv = secretEnv(resolvedOptions);
|
|
9231
9723
|
if (shouldPreviewDryRun(picked, parentArgv)) {
|
|
9232
|
-
const dryExit = await spawnChild(buildChildArgv(picked, positionals, parentArgv, true), {
|
|
9724
|
+
const dryExit = await spawnChild(buildChildArgv(picked, positionals, resolvedOptions, parentArgv, true), {
|
|
9725
|
+
[DRY_RUN_PREVIEW_ENV_VAR]: "1",
|
|
9726
|
+
...extraEnv
|
|
9727
|
+
});
|
|
9233
9728
|
if (dryExit === 2) {
|
|
9234
9729
|
process.exitCode = 0;
|
|
9235
9730
|
return;
|
|
@@ -9245,10 +9740,12 @@ async function launchMenu(entries, parentArgv, pickMessage, nonInteractiveMessag
|
|
|
9245
9740
|
process.exitCode = 0;
|
|
9246
9741
|
return;
|
|
9247
9742
|
}
|
|
9248
|
-
|
|
9743
|
+
const forReal = await resolveForRealSpawn(resolvedOptions);
|
|
9744
|
+
process.exitCode = await spawnChild(buildChildArgv(picked, positionals, forReal, parentArgv, false), secretEnv(forReal));
|
|
9249
9745
|
return;
|
|
9250
9746
|
}
|
|
9251
|
-
|
|
9747
|
+
const forReal = await resolveForRealSpawn(resolvedOptions);
|
|
9748
|
+
process.exitCode = await spawnChild(buildChildArgv(picked, positionals, forReal, parentArgv), secretEnv(forReal));
|
|
9252
9749
|
}
|
|
9253
9750
|
//#endregion
|
|
9254
9751
|
//#region src/interactive-menu/run-menu-entries/run-menu-entries.ts
|
|
@@ -10383,6 +10880,7 @@ try {
|
|
|
10383
10880
|
default: false,
|
|
10384
10881
|
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."
|
|
10385
10882
|
}), async (argv) => {
|
|
10883
|
+
const otp = resolveOtp(argv.otp, env);
|
|
10386
10884
|
if (argv.syncTrust) {
|
|
10387
10885
|
const [oldFile, newFile] = argv.syncTrust;
|
|
10388
10886
|
if (!oldFile || !newFile) {
|
|
@@ -10400,12 +10898,14 @@ try {
|
|
|
10400
10898
|
return;
|
|
10401
10899
|
}
|
|
10402
10900
|
if (argv.initial) {
|
|
10403
|
-
|
|
10901
|
+
const report = await runPublish({
|
|
10404
10902
|
cwd: argv.cwd,
|
|
10405
10903
|
dryRun: argv.dryRun,
|
|
10406
10904
|
...argv.tag ? { tag: argv.tag } : {},
|
|
10407
|
-
...
|
|
10408
|
-
})
|
|
10905
|
+
...otp ? { otp } : {}
|
|
10906
|
+
});
|
|
10907
|
+
if (report.status === "fail") process.exitCode = 1;
|
|
10908
|
+
else if (report.status === "ok" && report.packageNames.length === 0 && env.get("HOLOCRON_DRY_RUN_PREVIEW") === "1") process.exitCode = 2;
|
|
10409
10909
|
return;
|
|
10410
10910
|
}
|
|
10411
10911
|
if ((await runSteadyPublish({
|
|
@@ -10414,7 +10914,7 @@ try {
|
|
|
10414
10914
|
provenance: argv.provenance,
|
|
10415
10915
|
skipAlreadyPublished: argv.skipAlreadyPublished,
|
|
10416
10916
|
...argv.tag ? { tag: argv.tag } : {},
|
|
10417
|
-
...
|
|
10917
|
+
...otp ? { otp } : {}
|
|
10418
10918
|
})).status === "fail") process.exitCode = 1;
|
|
10419
10919
|
}).command("$0", false, () => {}, async (argv) => {
|
|
10420
10920
|
await launchGroupMenu("package", argv);
|