rman 1.0.12 → 1.2.1

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.
Files changed (102) hide show
  1. package/README.md +90 -70
  2. package/cli.js +226 -14
  3. package/commands/build.command.js +1 -0
  4. package/commands/changed.command.js +2 -2
  5. package/commands/changelog.command.js +13 -15
  6. package/commands/config.command.js +61 -0
  7. package/commands/diff.command.js +9 -4
  8. package/commands/exec.command.js +2 -8
  9. package/commands/github-release.command.js +1 -0
  10. package/commands/info.command.d.ts +9 -0
  11. package/commands/info.command.js +12 -2
  12. package/commands/run.command.js +5 -8
  13. package/commands/test.command.js +1 -0
  14. package/commands/version.command.js +53 -14
  15. package/constants.js +1 -1
  16. package/core/config.d.ts +265 -17
  17. package/core/config.js +651 -76
  18. package/core/custom-command.d.ts +133 -0
  19. package/core/custom-command.js +99 -0
  20. package/core/extends-config.d.ts +27 -0
  21. package/core/extends-config.js +89 -0
  22. package/core/manifest.d.ts +222 -0
  23. package/core/manifest.js +150 -0
  24. package/core/merge-config.d.ts +70 -0
  25. package/core/merge-config.js +193 -0
  26. package/core/package.d.ts +73 -7
  27. package/core/package.js +86 -24
  28. package/core/plugin.d.ts +112 -0
  29. package/core/plugin.js +189 -0
  30. package/core/repository.d.ts +91 -1
  31. package/core/repository.js +277 -132
  32. package/core/resolve-target.d.ts +12 -0
  33. package/core/resolve-target.js +33 -0
  34. package/core/run-step.d.ts +75 -0
  35. package/core/run-step.js +1 -0
  36. package/core/version-scheme.d.ts +134 -0
  37. package/core/version-scheme.js +148 -0
  38. package/core/workspace.d.ts +68 -0
  39. package/core/workspace.js +83 -0
  40. package/index.d.ts +55 -8
  41. package/index.js +42 -7
  42. package/interfaces/rman-config.interface.d.ts +222 -46
  43. package/package.json +16 -7
  44. package/services/change-hash.service.d.ts +88 -0
  45. package/services/change-hash.service.js +112 -0
  46. package/services/changelog.service.d.ts +8 -13
  47. package/services/changelog.service.js +12 -11
  48. package/services/conventional-commits.service.d.ts +73 -0
  49. package/services/conventional-commits.service.js +116 -0
  50. package/services/docker-publish.service.js +1 -1
  51. package/services/exec.service.js +1 -1
  52. package/services/github-release.service.d.ts +2 -2
  53. package/services/github-release.service.js +10 -5
  54. package/services/list.service.js +5 -2
  55. package/services/run.service.d.ts +112 -6
  56. package/services/run.service.js +265 -89
  57. package/services/system-info.d.ts +22 -7
  58. package/services/system-info.js +8 -23
  59. package/services/version-plan.service.d.ts +244 -0
  60. package/services/version-plan.service.js +414 -0
  61. package/services/version.service.d.ts +102 -82
  62. package/services/version.service.js +226 -434
  63. package/services.d.ts +5 -3
  64. package/services.js +5 -3
  65. package/utils/bin-path.d.ts +59 -0
  66. package/utils/bin-path.js +82 -0
  67. package/utils/child-tracker.d.ts +16 -0
  68. package/utils/child-tracker.js +30 -0
  69. package/utils/exec.d.ts +13 -2
  70. package/utils/exec.js +17 -17
  71. package/utils/git.d.ts +9 -3
  72. package/utils/git.js +10 -2
  73. package/utils/package-filter.d.ts +33 -2
  74. package/utils/package-filter.js +47 -7
  75. package/utils/printable-config.d.ts +15 -0
  76. package/utils/printable-config.js +42 -0
  77. package/utils/release-version.js +3 -3
  78. package/utils/run-bin.d.ts +46 -0
  79. package/utils/run-bin.js +63 -0
  80. package/utils/version-stamp.d.ts +14 -6
  81. package/utils/version-stamp.js +25 -13
  82. package/commands/ci.command.js +0 -30
  83. package/commands/clean.command.d.ts +0 -3
  84. package/commands/clean.command.js +0 -36
  85. package/commands/publish.command.d.ts +0 -3
  86. package/commands/publish.command.js +0 -225
  87. package/rmanrc.schema.json +0 -392
  88. package/services/ci.service.d.ts +0 -40
  89. package/services/ci.service.js +0 -204
  90. package/services/clean.service.d.ts +0 -42
  91. package/services/clean.service.js +0 -226
  92. package/services/publish.service.d.ts +0 -79
  93. package/services/publish.service.js +0 -273
  94. package/utils/change-hash.d.ts +0 -68
  95. package/utils/change-hash.js +0 -98
  96. package/utils/conventional-commits.d.ts +0 -52
  97. package/utils/conventional-commits.js +0 -90
  98. package/utils/npm-run-path.d.ts +0 -67
  99. package/utils/npm-run-path.js +0 -63
  100. package/utils/workspace-range.d.ts +0 -17
  101. package/utils/workspace-range.js +0 -28
  102. /package/commands/{ci.command.d.ts → config.command.d.ts} +0 -0
@@ -0,0 +1,63 @@
1
+ import { spawn } from 'node:child_process';
2
+ import colors from 'ansi-colors';
3
+ import { BinPath } from './bin-path.js';
4
+ import { trackChild } from './child-tracker.js';
5
+ import { LOG_LEVELS } from './logger.js';
6
+ /**
7
+ * Runs one of the repository's locally installed binaries, passing **argv as an array** - no shell.
8
+ *
9
+ * Use this over `exec` whenever the arguments aren't a fixed string: `exec` runs through a shell, so
10
+ * an interpolated path containing a space silently becomes two arguments, and a value containing
11
+ * `;` or `&&` becomes another command. Here they cannot - each element of `argv` arrives as exactly
12
+ * one argument. `exec` remains the right tool for a command the config author wrote as a string,
13
+ * shell operators and all (`run.<script>`, `version` hooks).
14
+ *
15
+ * The binary is found by PATH, not spawned by path: `BinPath.env` prepends whatever the repository's
16
+ * plugins say a local install lives in (`node_modules/.bin`
17
+ * from `cwd` up the directory tree, which is what makes `eslint` resolve to the repository's own
18
+ * copy - and on Windows resolve `eslint.cmd`, which spawning a bare path would not.
19
+ *
20
+ * Rejects on a non-zero exit (the error names the command and the code) and on a missing binary,
21
+ * where it asks the useful question instead of reporting `ENOENT`. A non-zero exit has to be an
22
+ * error rather than a returned code: a lint or type-check step that passes in CI having checked
23
+ * nothing is the outcome worth ruling out, and that is what silently discarding a code produces.
24
+ */
25
+ export async function runBin(bin, argv, options = {}) {
26
+ const cwd = options.cwd ?? process.cwd();
27
+ const logLevel = options.logLevel ?? 'info';
28
+ const atLeast = (level) => LOG_LEVELS.indexOf(logLevel) >= LOG_LEVELS.indexOf(level);
29
+ /** Below 'info' the output is captured rather than streamed, so a quiet run stays quiet and a
30
+ * failing one can still say what went wrong. */
31
+ const stdio = options.stdio ?? (atLeast('info') ? 'inherit' : 'pipe');
32
+ if (atLeast('verbose'))
33
+ console.log(colors.magenta('verbose'), colors.gray('$'), bin, argv.join(' '));
34
+ const child = spawn(process.platform === 'win32' ? `${bin}.cmd` : bin, argv, {
35
+ cwd,
36
+ stdio: stdio === 'inherit' ? 'inherit' : 'pipe',
37
+ env: BinPath.env({ cwd, env: options.env }),
38
+ windowsHide: true,
39
+ });
40
+ /** So an interrupted rman does not leave this running - `exec` always did this and this did not,
41
+ * which meant a plugin command's child outlived a Ctrl-C. See `trackChild`. */
42
+ trackChild(child);
43
+ let output = '';
44
+ child.stdout?.on('data', (d) => (output += d.toString()));
45
+ child.stderr?.on('data', (d) => (output += d.toString()));
46
+ return new Promise((resolve, reject) => {
47
+ child.on('error', (e) => {
48
+ reject(e?.code === 'ENOENT' ? new Error(`"${bin}" was not found - is it installed in this repository?`) : e);
49
+ });
50
+ child.on('close', code => {
51
+ if (code === 0)
52
+ return resolve({ code: 0, output });
53
+ /** Captured output has to be surfaced here or it is lost with the process - the one thing
54
+ * worse than a noisy failure is a silent one. 'silent' is the caller saying otherwise. */
55
+ if (stdio === 'pipe' && output && logLevel !== 'silent')
56
+ process.stderr.write(output);
57
+ const err = new Error(`"${bin} ${argv.join(' ')}" exited with code ${code}`);
58
+ err.code = code;
59
+ err.output = output;
60
+ reject(err);
61
+ });
62
+ });
63
+ }
@@ -19,18 +19,26 @@ export declare const OCI_VERSION_LABEL = "org.opencontainers.image.version";
19
19
  */
20
20
  export declare function stampVersionLabel(content: string, version: string): string | undefined;
21
21
  /**
22
- * Rewrites the string assigned to a `name` constant (default `version`) in a source file to
23
- * `version`, returning the new content - or `undefined` when there was nothing to change, so a
24
- * caller can skip writing the file at all. Both an assignment and an object property are matched:
22
+ * Rewrites the **quoted string assigned to `name`** (default `version`) to `version`, returning the
23
+ * new content - or `undefined` when there was nothing to change, so a caller can write nothing and
24
+ * report a file that matched nothing. Both an assignment and an object property are matched:
25
25
  *
26
26
  * ```ts
27
27
  * export const version = '1'; -> export const version = '6.0.10';
28
28
  * { name: 'app', version: '1' } -> { name: 'app', version: '6.0.10' }
29
29
  * ```
30
30
  *
31
- * Unlike the Dockerfile label there is no standard naming a file as holding the version, so this
32
- * only ever runs against files `.rmanrc "version.stamp"` explicitly lists - which is also what
33
- * keeps a match this broad safe.
31
+ * **A helper, not a policy**: which shape a language declares a version in is
32
+ * `ManifestProvider.stampVersion`'s answer, and this is the one most of them share, exported so
33
+ * every provider need not re-derive the same regex. Measured, it reaches a Go `const version =
34
+ * "…"`, a Gradle/TOML `version = "…"` and a JS/TS `const version = '…'`.
35
+ *
36
+ * **What it does not reach**, all measured, and the reason the decision is a seam: an identifier
37
+ * with anything between it and the value (`pub const VERSION: &str = "…"`, and equally TypeScript's
38
+ * own `const version: string = '…'`), an unquoted value (`pom.xml`'s `<version>1.0.0</version>`),
39
+ * and - unless `name` says so - any other spelling (`VERSION`, `Version`, `__version__`,
40
+ * `appVersion`). `name` was dead for exactly that reason: nothing passed it, so only the lowercase
41
+ * word `version` was ever matched.
34
42
  *
35
43
  * Stamping the *source* rather than the build output is the point: a build-time rewrite leaves the
36
44
  * checked-in file claiming some placeholder, so anything running from source (tests, ts-node, the
@@ -41,18 +41,26 @@ export function stampVersionLabel(content, version) {
41
41
  return changed ? lines.join('\n') : undefined;
42
42
  }
43
43
  /**
44
- * Rewrites the string assigned to a `name` constant (default `version`) in a source file to
45
- * `version`, returning the new content - or `undefined` when there was nothing to change, so a
46
- * caller can skip writing the file at all. Both an assignment and an object property are matched:
44
+ * Rewrites the **quoted string assigned to `name`** (default `version`) to `version`, returning the
45
+ * new content - or `undefined` when there was nothing to change, so a caller can write nothing and
46
+ * report a file that matched nothing. Both an assignment and an object property are matched:
47
47
  *
48
48
  * ```ts
49
49
  * export const version = '1'; -> export const version = '6.0.10';
50
50
  * { name: 'app', version: '1' } -> { name: 'app', version: '6.0.10' }
51
51
  * ```
52
52
  *
53
- * Unlike the Dockerfile label there is no standard naming a file as holding the version, so this
54
- * only ever runs against files `.rmanrc "version.stamp"` explicitly lists - which is also what
55
- * keeps a match this broad safe.
53
+ * **A helper, not a policy**: which shape a language declares a version in is
54
+ * `ManifestProvider.stampVersion`'s answer, and this is the one most of them share, exported so
55
+ * every provider need not re-derive the same regex. Measured, it reaches a Go `const version =
56
+ * "…"`, a Gradle/TOML `version = "…"` and a JS/TS `const version = '…'`.
57
+ *
58
+ * **What it does not reach**, all measured, and the reason the decision is a seam: an identifier
59
+ * with anything between it and the value (`pub const VERSION: &str = "…"`, and equally TypeScript's
60
+ * own `const version: string = '…'`), an unquoted value (`pom.xml`'s `<version>1.0.0</version>`),
61
+ * and - unless `name` says so - any other spelling (`VERSION`, `Version`, `__version__`,
62
+ * `appVersion`). `name` was dead for exactly that reason: nothing passed it, so only the lowercase
63
+ * word `version` was ever matched.
56
64
  *
57
65
  * Stamping the *source* rather than the build output is the point: a build-time rewrite leaves the
58
66
  * checked-in file claiming some placeholder, so anything running from source (tests, ts-node, the
@@ -60,15 +68,19 @@ export function stampVersionLabel(content, version) {
60
68
  * to be redone on every build.
61
69
  */
62
70
  export function stampVersionConstant(content, version, name = 'version') {
63
- let changed = false;
71
+ let matched = false;
64
72
  const pattern = new RegExp(`(\\b${escapeRegExp(name)}\\s*[:=]\\s*)(['"\`])([^'"\`\\n]*)\\2`, 'g');
65
- const result = content.replace(pattern, (whole, prefix, quote) => {
66
- const next = `${prefix}${quote}${version}${quote}`;
67
- if (next !== whole)
68
- changed = true;
69
- return next;
73
+ const result = content.replace(pattern, (_whole, prefix, quote) => {
74
+ matched = true;
75
+ return `${prefix}${quote}${version}${quote}`;
70
76
  });
71
- return changed ? result : undefined;
77
+ /**
78
+ * `undefined` means **nothing matched**, not "no change was needed" - the two used to be the same
79
+ * answer, and that made the caller's error message a liar: a file already holding the target
80
+ * version reported that nothing in it could be rewritten. A caller that wants to skip an identical
81
+ * write compares the result with what it passed in.
82
+ */
83
+ return matched ? result : undefined;
72
84
  }
73
85
  const LABEL_VALUE = new RegExp(`(${OCI_VERSION_LABEL.replace(/\./g, '\\.')}\\s*=\\s*)("[^"\\n]*"|'[^'\\n]*'|[^\\s\\\\]+)`, 'g');
74
86
  function escapeRegExp(value) {
@@ -1,30 +0,0 @@
1
- import { CiService } from '../services/ci.service.js';
2
- import { applyBranchGuardOptions, assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
3
- import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
4
- export function initCli(repository, program) {
5
- program.command({
6
- command: 'ci',
7
- describe: 'Deletes node_modules and lockfiles in every package, then reinstalls from scratch',
8
- builder: cmd => applyBranchGuardOptions(applyPackageFilterOptions(cmd))
9
- .example('$0 ci', '')
10
- .option('package-manager', {
11
- describe: 'Package manager to install with (default: npm, or .rmanrc "packageManager")',
12
- choices: PACKAGE_MANAGERS,
13
- })
14
- .option('progress', {
15
- describe: 'Show a live progress panel while running (default: true; auto-disabled when not a TTY). ' +
16
- 'Unlike run/build, completion is not reported as a per-package tally - only failures are called out.',
17
- type: 'boolean',
18
- }),
19
- handler: async (args) => {
20
- await assertAllowedBranch(repository, readBranchGuardOptions(args));
21
- await CiService.reinstall(repository, {
22
- ...readPackageFilterOptions(args),
23
- packageManager: args.packageManager,
24
- progress: args.progress,
25
- logLevel: args.logLevel,
26
- });
27
- },
28
- });
29
- }
30
- const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'];
@@ -1,3 +0,0 @@
1
- import type { Argv } from 'yargs';
2
- import type { Repository } from '../core/repository.js';
3
- export declare function initCli(repository: Repository, program: Argv): void;
@@ -1,36 +0,0 @@
1
- import { CleanService } from '../services/clean.service.js';
2
- import { applyBranchGuardOptions, assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
3
- import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
4
- export function initCli(repository, program) {
5
- program.command({
6
- command: 'clean',
7
- describe: 'Removes compiled TypeScript output and any extra files/dirs configured via .rmanrc "clean"',
8
- builder: cmd => applyBranchGuardOptions(applyPackageFilterOptions(cmd))
9
- .example('$0 clean', '')
10
- .example('$0 clean --dry-run', '# Preview what would be removed')
11
- .option('progress', {
12
- describe: 'Show a live progress panel (default: true; auto-disabled when not a TTY)',
13
- type: 'boolean',
14
- })
15
- .option('dry-run', {
16
- describe: 'Report what would be removed without actually removing anything',
17
- type: 'boolean',
18
- })
19
- .option('root', {
20
- alias: 'r',
21
- describe: 'Clean the whole repository even when the current directory is inside a single ' +
22
- 'package (which otherwise scopes cleaning to just that package). No effect elsewhere.',
23
- type: 'boolean',
24
- }),
25
- handler: async (args) => {
26
- await assertAllowedBranch(repository, readBranchGuardOptions(args));
27
- await CleanService.clean(repository, {
28
- ...readPackageFilterOptions(args),
29
- progress: args.progress,
30
- dryRun: args.dryRun,
31
- root: args.root,
32
- logLevel: args.logLevel,
33
- });
34
- },
35
- });
36
- }
@@ -1,3 +0,0 @@
1
- import type { Argv } from 'yargs';
2
- import type { Repository } from '../core/repository.js';
3
- export declare function initCli(repository: Repository, program: Argv): void;
@@ -1,225 +0,0 @@
1
- import readline from 'node:readline/promises';
2
- import colors from 'ansi-colors';
3
- import { CiService } from '../services/ci.service.js';
4
- import { DockerPublishService } from '../services/docker-publish.service.js';
5
- import { PublishService } from '../services/publish.service.js';
6
- import { applyBranchGuardOptions, assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
7
- import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
8
- export function initCli(repository, program) {
9
- program.command({
10
- command: 'publish',
11
- describe: 'Publishes every package to its configured target(s) (npm by default, or .rmanrc "publish.target")',
12
- builder: cmd => applyBranchGuardOptions(applyPackageFilterOptions(cmd))
13
- .example('$0 publish', '# Show the plan, then ask for confirmation')
14
- .example('$0 publish --yes', '# Publish immediately, no confirmation')
15
- .example('$0 publish --dry-run', '# Only show the plan, never publish')
16
- .example('$0 publish --target docker', '# Only the packages configured for the "docker" target')
17
- .option('yes', {
18
- alias: 'y',
19
- describe: 'Skip the confirmation prompt and publish immediately',
20
- type: 'boolean',
21
- })
22
- .option('dry-run', {
23
- describe: 'Only show the plan - never publishes, regardless of --yes',
24
- type: 'boolean',
25
- })
26
- .option('json', {
27
- alias: 'j',
28
- describe: 'Print the plan as JSON instead of text - one entry per package and target. Combine with ' +
29
- '--dry-run to ask "is there anything to publish?" without publishing (e.g. a CI release gate).',
30
- type: 'boolean',
31
- })
32
- .option('target', {
33
- describe: 'Restrict this run to just these publish target(s) ("npm"/"docker", repeatable) - default: ' +
34
- 'every target each package itself is configured for (.rmanrc "publish.target", "npm" when unset). ' +
35
- 'A package that opts into "docker" but has no "publish.docker" config errors clearly instead of ' +
36
- 'being silently skipped.',
37
- type: 'array',
38
- choices: ['npm', 'docker'],
39
- })
40
- .option('ignore-dirty', {
41
- describe: 'Exclude a package with uncommitted local changes instead of aborting the whole run',
42
- type: 'boolean',
43
- })
44
- .option('package-manager', {
45
- describe: 'Package manager to publish with (default: npm, or .rmanrc "packageManager")',
46
- choices: PACKAGE_MANAGERS,
47
- })
48
- .option('access', {
49
- describe: 'npm publish --access <public|restricted> - required by the registry for a new scoped package',
50
- choices: ['public', 'restricted'],
51
- })
52
- .option('tag', {
53
- describe: 'npm publish --tag <tag> - the dist-tag this version is published under (default "latest")',
54
- type: 'string',
55
- })
56
- .option('otp', {
57
- describe: 'npm publish --otp <otp> - a 2FA one-time password, for registries that require it',
58
- type: 'string',
59
- })
60
- .option('registry', {
61
- describe: 'Registry to check against and publish to (default: whatever .npmrc already configures)',
62
- type: 'string',
63
- })
64
- .option('userconfig', {
65
- describe: 'Path to a custom .npmrc to use for both the registry check and the actual publish',
66
- type: 'string',
67
- })
68
- .option('contents', {
69
- describe: "Subdirectory to publish from, relative to each package's own directory - only consulted when a " +
70
- 'package has no "publishConfig.directory" of its own (that always wins when present)',
71
- type: 'string',
72
- })
73
- .option('docker-namespace', {
74
- describe: 'Prefixed onto a bare (no "/") "publish.docker.image" - default: the DOCKERHUB_NAMESPACE ' +
75
- 'environment variable.',
76
- type: 'string',
77
- }),
78
- handler: async (args) => {
79
- await assertAllowedBranch(repository, readBranchGuardOptions(args));
80
- const targets = resolveTargets(args.target);
81
- const explicitTargets = !!args.target?.length;
82
- const explicitDockerTarget = explicitTargets && targets.has('docker');
83
- const ignoreDirty = args.ignoreDirty;
84
- const npmOptions = {
85
- ...readPackageFilterOptions(args),
86
- ignoreDirty,
87
- registry: args.registry,
88
- userconfig: args.userconfig,
89
- };
90
- const dockerOptions = {
91
- ...readPackageFilterOptions(args),
92
- ignoreDirty,
93
- namespace: args.dockerNamespace,
94
- };
95
- const npmPlan = targets.has('npm') ? await PublishService.getPlan(repository, npmOptions) : [];
96
- const dockerPlan = targets.has('docker') ? await DockerPublishService.getPlan(repository, dockerOptions) : [];
97
- if (args.json) {
98
- console.log(JSON.stringify([...npmPlan.map(e => jsonEntry(e, 'npm')), ...dockerPlan.map(e => jsonEntry(e, 'docker'))], undefined, 2));
99
- }
100
- else {
101
- printPlan(npmPlan);
102
- printPlan(dockerPlan, 'docker');
103
- }
104
- if (explicitDockerTarget && !dockerPlan.length) {
105
- const message = '--target docker was given, but no package\'s .rmanrc configures "publish.docker".';
106
- console.log(colors.red(message));
107
- const err = new Error(message);
108
- err.logged = true;
109
- throw err;
110
- }
111
- const errors = [...npmPlan, ...dockerPlan].filter(e => e.status === 'error');
112
- if (errors.length) {
113
- const allDirty = errors.every(e => e.reason === 'uncommitted local changes');
114
- const message = allDirty
115
- ? `${errors.length} package(s) have uncommitted local changes ` +
116
- '(pass --ignore-dirty to exclude them instead of aborting)'
117
- : `${errors.length} package(s) failed to prepare for publish - see the errors above`;
118
- console.log(colors.red(message));
119
- const err = new Error(message);
120
- err.logged = true;
121
- throw err;
122
- }
123
- if (![...npmPlan, ...dockerPlan].some(e => e.status === 'publish')) {
124
- if (!args.json)
125
- console.log(colors.gray('Nothing to publish.'));
126
- return;
127
- }
128
- if (args.dryRun)
129
- return;
130
- let proceed = !!args.yes;
131
- if (!proceed) {
132
- if (!process.stdout.isTTY) {
133
- console.log(colors.gray('Not a TTY - refusing to prompt. Pass --yes to publish non-interactively.'));
134
- return;
135
- }
136
- proceed = await confirm('Publish these packages?');
137
- }
138
- if (!proceed)
139
- return;
140
- const appliedNpm = targets.has('npm')
141
- ? await PublishService.applyPlan(repository, npmPlan, {
142
- ...npmOptions,
143
- packageManager: args.packageManager,
144
- access: args.access,
145
- tag: args.tag,
146
- otp: args.otp,
147
- contents: args.contents,
148
- })
149
- : [];
150
- const appliedDocker = targets.has('docker') ? await DockerPublishService.applyPlan(repository, dockerPlan) : [];
151
- let failed = false;
152
- for (const entry of appliedNpm) {
153
- if (entry.status === 'publish') {
154
- console.log(colors.green('published'), colors.cyan(entry.package.name), entry.version);
155
- }
156
- else if (entry.status === 'error' && npmPlan.find(e => e.package === entry.package)?.status === 'publish') {
157
- failed = true;
158
- console.log(colors.red('failed'), colors.cyan(entry.package.name), colors.red(entry.reason ?? ''));
159
- }
160
- }
161
- for (const entry of appliedDocker) {
162
- if (entry.status === 'publish') {
163
- console.log(colors.green('published'), colors.gray('[docker]'), colors.cyan(entry.package.name), entry.image);
164
- }
165
- else if (entry.status === 'error' &&
166
- dockerPlan.find(e => e.package === entry.package)?.status === 'publish') {
167
- failed = true;
168
- console.log(colors.red('failed'), colors.gray('[docker]'), colors.cyan(entry.package.name), colors.red(entry.reason ?? ''));
169
- }
170
- }
171
- if (failed) {
172
- const err = new Error('"publish" failed');
173
- err.logged = true;
174
- throw err;
175
- }
176
- },
177
- });
178
- }
179
- function resolveTargets(input) {
180
- if (!input?.length)
181
- return new Set(['npm', 'docker']);
182
- return new Set(input);
183
- }
184
- /** One `--json` row. `target` is what distinguishes otherwise-identical rows for a package that
185
- * ships to several targets at once, so a consumer can tell which one still needs publishing. */
186
- function jsonEntry(entry, target) {
187
- return {
188
- name: entry.package.name,
189
- target,
190
- status: entry.status,
191
- version: entry.version,
192
- reason: entry.reason,
193
- };
194
- }
195
- function printPlan(entries, label) {
196
- const prefix = label ? colors.gray(`[${label}] `) : '';
197
- for (const e of entries) {
198
- const name = prefix + colors.cyan(e.package.name);
199
- switch (e.status) {
200
- case 'publish':
201
- console.log(colors.green('publish'), name, e.version, colors.gray(e.reason ?? ''));
202
- break;
203
- case 'up-to-date':
204
- console.log(colors.gray('up-to-date'), name, e.version);
205
- break;
206
- case 'skip':
207
- console.log(colors.cyan('skip'), name, colors.gray(e.reason ?? ''));
208
- break;
209
- case 'error':
210
- console.log(colors.red('error'), name, colors.red(e.reason ?? ''));
211
- break;
212
- }
213
- }
214
- }
215
- async function confirm(question) {
216
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
217
- try {
218
- const answer = await rl.question(`${question} (y/N) `);
219
- return /^y(es)?$/i.test(answer.trim());
220
- }
221
- finally {
222
- rl.close();
223
- }
224
- }
225
- const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'];