rman 0.38.0 → 1.0.2

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 (119) hide show
  1. package/README.md +371 -21
  2. package/cli.js +51 -29
  3. package/commands/build.command.d.ts +3 -0
  4. package/commands/build.command.js +14 -0
  5. package/commands/changed.command.d.ts +3 -0
  6. package/commands/changed.command.js +29 -0
  7. package/commands/changelog.command.d.ts +3 -0
  8. package/commands/changelog.command.js +63 -0
  9. package/commands/ci.command.d.ts +3 -0
  10. package/commands/ci.command.js +30 -0
  11. package/commands/clean.command.d.ts +3 -0
  12. package/commands/clean.command.js +36 -0
  13. package/commands/diff.command.d.ts +3 -0
  14. package/commands/diff.command.js +51 -0
  15. package/commands/exec.command.d.ts +3 -0
  16. package/commands/exec.command.js +83 -0
  17. package/commands/import.command.d.ts +3 -0
  18. package/commands/import.command.js +27 -0
  19. package/commands/info.command.d.ts +3 -0
  20. package/commands/info.command.js +61 -0
  21. package/commands/list.command.d.ts +3 -0
  22. package/commands/list.command.js +113 -0
  23. package/commands/publish.command.d.ts +3 -0
  24. package/commands/publish.command.js +147 -0
  25. package/commands/run.command.d.ts +6 -0
  26. package/commands/run.command.js +80 -0
  27. package/commands/test.command.d.ts +3 -0
  28. package/commands/test.command.js +14 -0
  29. package/commands/version.command.d.ts +3 -0
  30. package/commands/version.command.js +125 -0
  31. package/constants.js +1 -1
  32. package/core/config.d.ts +13 -0
  33. package/core/config.js +61 -0
  34. package/core/package.d.ts +2 -0
  35. package/core/package.js +2 -0
  36. package/core/repository.d.ts +36 -3
  37. package/core/repository.js +90 -45
  38. package/index.d.ts +17 -1
  39. package/index.js +15 -1
  40. package/package.json +8 -4
  41. package/rmanrc.schema.json +202 -0
  42. package/services/changelog.service.d.ts +99 -0
  43. package/services/changelog.service.js +272 -0
  44. package/services/ci.service.d.ts +40 -0
  45. package/services/ci.service.js +204 -0
  46. package/services/clean.service.d.ts +42 -0
  47. package/services/clean.service.js +226 -0
  48. package/services/exec.service.d.ts +35 -0
  49. package/services/exec.service.js +114 -0
  50. package/services/import.service.d.ts +30 -0
  51. package/services/import.service.js +84 -0
  52. package/services/list.service.d.ts +27 -0
  53. package/services/list.service.js +25 -0
  54. package/services/publish.service.d.ts +79 -0
  55. package/services/publish.service.js +199 -0
  56. package/services/run.service.d.ts +99 -0
  57. package/services/run.service.js +432 -0
  58. package/services/system-info.d.ts +16 -0
  59. package/services/system-info.js +25 -0
  60. package/services/version.service.d.ts +95 -0
  61. package/services/version.service.js +450 -0
  62. package/services.d.ts +10 -0
  63. package/services.js +10 -0
  64. package/utils/branch-guard.d.ts +25 -0
  65. package/utils/branch-guard.js +61 -0
  66. package/utils/change-hash.d.ts +55 -0
  67. package/utils/change-hash.js +80 -0
  68. package/utils/conventional-commits.d.ts +39 -0
  69. package/utils/conventional-commits.js +43 -0
  70. package/utils/exec.d.ts +5 -6
  71. package/utils/exec.js +45 -59
  72. package/utils/git.d.ts +86 -0
  73. package/utils/git.js +276 -0
  74. package/utils/live-region.d.ts +16 -0
  75. package/utils/live-region.js +59 -0
  76. package/utils/logger.d.ts +24 -0
  77. package/utils/logger.js +39 -0
  78. package/utils/npm-run-path.d.ts +1 -1
  79. package/utils/npm-run-path.js +10 -10
  80. package/utils/package-filter.d.ts +32 -0
  81. package/utils/package-filter.js +73 -0
  82. package/utils/progress-panel.d.ts +59 -0
  83. package/utils/progress-panel.js +149 -0
  84. package/utils/workspace-range.d.ts +17 -0
  85. package/utils/workspace-range.js +28 -0
  86. package/commands/build-command.d.ts +0 -11
  87. package/commands/build-command.js +0 -26
  88. package/commands/changed-command.d.ts +0 -16
  89. package/commands/changed-command.js +0 -35
  90. package/commands/ci-command.d.ts +0 -24
  91. package/commands/ci-command.js +0 -71
  92. package/commands/execute-command.d.ts +0 -19
  93. package/commands/execute-command.js +0 -75
  94. package/commands/info-command.d.ts +0 -10
  95. package/commands/info-command.js +0 -57
  96. package/commands/list-command.d.ts +0 -38
  97. package/commands/list-command.js +0 -137
  98. package/commands/multi-task-command.d.ts +0 -22
  99. package/commands/multi-task-command.js +0 -68
  100. package/commands/publish-command.d.ts +0 -24
  101. package/commands/publish-command.js +0 -135
  102. package/commands/run-command.d.ts +0 -28
  103. package/commands/run-command.js +0 -134
  104. package/commands/version-command.d.ts +0 -25
  105. package/commands/version-command.js +0 -198
  106. package/core/command.d.ts +0 -35
  107. package/core/command.js +0 -117
  108. package/core/constants.d.ts +0 -1
  109. package/core/constants.js +0 -2
  110. package/core/logger.d.ts +0 -12
  111. package/core/logger.js +0 -2
  112. package/utils/file-utils.d.ts +0 -4
  113. package/utils/file-utils.js +0 -29
  114. package/utils/git-utils.d.ts +0 -25
  115. package/utils/git-utils.js +0 -66
  116. package/utils/npm-utils.d.ts +0 -11
  117. package/utils/npm-utils.js +0 -33
  118. package/utils/package-not-found-error.d.ts +0 -2
  119. package/utils/package-not-found-error.js +0 -2
@@ -0,0 +1,30 @@
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'];
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ import type { Repository } from '../core/repository.js';
3
+ export declare function initCli(repository: Repository, program: Argv): void;
@@ -0,0 +1,36 @@
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
+ }
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ import type { Repository } from '../core/repository.js';
3
+ export declare function initCli(repository: Repository, program: Argv): void;
@@ -0,0 +1,51 @@
1
+ import path from 'node:path';
2
+ import colors from 'ansi-colors';
3
+ import { findLatestTag } from '../utils/change-hash.js';
4
+ import { GitHelper } from '../utils/git.js';
5
+ export function initCli(repository, program) {
6
+ program.command({
7
+ command: 'diff [package]',
8
+ describe: "Shows the git diff since a package's (or the whole repository's) last release tag",
9
+ builder: cmd => cmd
10
+ .example('$0 diff', "# Since the repository's own last tag")
11
+ .example('$0 diff pkg-a', "# Since pkg-a's own last tag, scoped to its directory")
12
+ .positional('package', {
13
+ describe: 'Package name - diffs just that package, since its own last tag. Omit to diff the whole ' +
14
+ "repository since its own last tag (or the current directory's package, if standing inside one).",
15
+ type: 'string',
16
+ }),
17
+ handler: async (args) => {
18
+ const git = new GitHelper({ cwd: repository.dirname });
19
+ const packageName = args.package;
20
+ let target = repository.rootPackage;
21
+ let pathspec;
22
+ if (packageName) {
23
+ const pkg = repository.getPackage(packageName);
24
+ if (!pkg) {
25
+ const message = `No such package "${packageName}"`;
26
+ console.log(colors.red(message));
27
+ const err = new Error(message);
28
+ err.logged = true;
29
+ throw err;
30
+ }
31
+ target = pkg;
32
+ pathspec = path.relative(repository.dirname, pkg.dirname);
33
+ }
34
+ else if (repository.currentPackage) {
35
+ target = repository.currentPackage;
36
+ pathspec = path.relative(repository.dirname, target.dirname);
37
+ }
38
+ const tag = await findLatestTag(git, target);
39
+ if (!tag) {
40
+ console.log(colors.gray(`No release tag found for "${target.name}" - nothing to diff against.`));
41
+ return;
42
+ }
43
+ const text = await git.diff(tag, pathspec);
44
+ if (!text.trim()) {
45
+ console.log(colors.gray(`No changes since ${tag}.`));
46
+ return;
47
+ }
48
+ console.log(text);
49
+ },
50
+ });
51
+ }
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ import type { Repository } from '../core/repository.js';
3
+ export declare function initCli(repository: Repository, program: Argv): void;
@@ -0,0 +1,83 @@
1
+ import { ExecService } from '../services/exec.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: 'exec [command..]',
7
+ describe: 'Runs an arbitrary shell command in each package - unlike run, not tied to any npm script',
8
+ builder: cmd => applyBranchGuardOptions(applyPackageFilterOptions(cmd))
9
+ .parserConfiguration({ 'populate--': true, 'unknown-options-as-args': true })
10
+ .example('$0 exec rm -rf dist', '# Not an npm script - runs directly in every package')
11
+ .example('$0 exec --scope pkg-a -- ls -la', '# "--" needed only if the command shares a flag name with exec\'s own')
12
+ .positional('command', {
13
+ describe: "The command (and its own arguments) to run in each package - its own flags don't need to be " +
14
+ 'escaped unless one happens to share a name with one of these options below, in which case put ' +
15
+ '"--" first',
16
+ type: 'string',
17
+ })
18
+ .option('parallel', {
19
+ describe: 'Max packages at once: omit/true for CPU count, a number for that many, false to run serially. ' +
20
+ 'Packages always run in dependency order unless --no-topo.',
21
+ coerce: (v) => {
22
+ if (v === undefined)
23
+ return undefined;
24
+ if (v === 'false' || v === false)
25
+ return false;
26
+ if (v === 'true' || v === true)
27
+ return true;
28
+ const n = Number(v);
29
+ return Number.isFinite(n) ? n : true;
30
+ },
31
+ })
32
+ .option('bail', {
33
+ describe: 'Stop on first failure (default: true)',
34
+ type: 'boolean',
35
+ })
36
+ .option('topo', {
37
+ describe: 'Respect package dependency order: a package waits for its dependencies and is skipped if one ' +
38
+ 'fails. Set false to run in every matching package independently, alphabetically. Default: true.',
39
+ type: 'boolean',
40
+ })
41
+ .option('progress', {
42
+ describe: 'Show a live progress panel (default: true; auto-disabled when not a TTY)',
43
+ type: 'boolean',
44
+ })
45
+ .option('changed', {
46
+ alias: 'c',
47
+ describe: 'Only run in packages that have changed since the last publish',
48
+ type: 'boolean',
49
+ })
50
+ .option('changed-since', {
51
+ describe: 'Only run in packages that have changed since the given git commit/hash',
52
+ type: 'string',
53
+ })
54
+ .option('root', {
55
+ alias: 'r',
56
+ describe: 'Run across the whole repository even when the current directory is inside a single ' +
57
+ 'package (which otherwise scopes the run to just that package). No effect elsewhere.',
58
+ type: 'boolean',
59
+ })
60
+ .conflicts('changed', 'changed-since'),
61
+ handler: async (args) => {
62
+ await assertAllowedBranch(repository, readBranchGuardOptions(args));
63
+ const afterDashDash = args['--'];
64
+ const tokens = afterDashDash?.length ? afterDashDash : args.command;
65
+ if (!tokens?.length) {
66
+ const err = new Error('No command given - e.g. "rman exec ls" or "rman exec -- eslint --bail"');
67
+ throw err;
68
+ }
69
+ const command = tokens.join(' ');
70
+ await ExecService.exec(repository, command, {
71
+ ...readPackageFilterOptions(args),
72
+ parallel: args.parallel,
73
+ topo: args.topo,
74
+ bail: args.bail,
75
+ progress: args.progress,
76
+ changed: args.changed,
77
+ changedSince: args.changedSince,
78
+ logLevel: args.logLevel,
79
+ root: args.root,
80
+ });
81
+ },
82
+ });
83
+ }
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ import type { Repository } from '../core/repository.js';
3
+ export declare function initCli(repository: Repository, program: Argv): void;
@@ -0,0 +1,27 @@
1
+ import path from 'node:path';
2
+ import colors from 'ansi-colors';
3
+ import { ImportService } from '../services/import.service.js';
4
+ export function initCli(repository, program) {
5
+ program.command({
6
+ command: 'import <path>',
7
+ describe: 'Imports an external git repository as a new package, preserving its full commit history',
8
+ builder: cmd => cmd
9
+ .example('$0 import ../my-old-repo', '')
10
+ .example('$0 import ../my-old-repo --dest libs', '# Under libs/ instead of packages/')
11
+ .positional('path', {
12
+ describe: 'Path to a local clone of the repository to import (not a URL - clone it first)',
13
+ type: 'string',
14
+ })
15
+ .option('dest', {
16
+ describe: 'Subdirectory the new package is placed under (default: "packages")',
17
+ type: 'string',
18
+ }),
19
+ handler: async (args) => {
20
+ const result = await ImportService.importRepo(repository, args.path, {
21
+ dest: args.dest,
22
+ });
23
+ console.log(colors.green('imported'), colors.cyan(result.name), '->', path.relative(repository.dirname, result.targetDir), colors.gray(`(${result.commitCount} commit(s))`));
24
+ console.log(colors.gray('Add it to your workspaces glob if it is not already covered, then reinstall ("ci").'));
25
+ },
26
+ });
27
+ }
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ import type { Repository } from '../core/repository.js';
3
+ export declare function initCli(repository: Repository, program: Argv): void;
@@ -0,0 +1,61 @@
1
+ import colors from 'ansi-colors';
2
+ import semver from 'semver';
3
+ import { SystemInfo } from '../services/system-info.js';
4
+ function printSystemInfo(systemInfo) {
5
+ const maxName = Object.keys(systemInfo).reduce((l, p) => Object.keys(systemInfo[p]).reduce((i, x) => Math.max(i, x.length), l), 0);
6
+ for (const [categoryName, category] of Object.entries(systemInfo)) {
7
+ console.log('', colors.whiteBright(categoryName) + ':');
8
+ for (const [n, v] of Object.entries(category)) {
9
+ const label = ' ' + colors.reset(n) + ' '.repeat(maxName - n.length) + ' :';
10
+ if (typeof v === 'string') {
11
+ console.log(label, colors.yellowBright(v));
12
+ continue;
13
+ }
14
+ if (v.version) {
15
+ console.log(label, colors.yellowBright(v.version), v.path ? ' ' + colors.yellow(v.path) : '');
16
+ }
17
+ if (v.installed) {
18
+ if (v.wanted === 'latest' || semver.intersects(v.installed, v.wanted)) {
19
+ console.log(label, colors.yellowBright(v.installed));
20
+ }
21
+ else {
22
+ console.log(label, colors.red(v.installed), ' => ', colors.yellowBright(v.wanted));
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ function printRepositoryInfo(info) {
29
+ console.log('', colors.whiteBright('Repository') + ':');
30
+ console.log(' ' + colors.reset('Type') + ' :', colors.yellowBright(info.type === 'monorepo' ? 'Monorepo' : 'Single package'));
31
+ console.log(' ' + colors.reset('Name') + ' :', colors.yellowBright(info.name || '(none)'));
32
+ console.log(' ' + colors.reset('Version') + ' :', colors.yellowBright(info.version || '(none)'));
33
+ console.log(' ' + colors.reset('Root') + ' :', colors.yellowBright(info.root));
34
+ if (info.type === 'monorepo') {
35
+ console.log(' ' + colors.reset('Packages') + ' :', colors.yellowBright(String(info.packageCount)), colors.gray('(run "list" to see them)'));
36
+ }
37
+ }
38
+ export function initCli(repository, program) {
39
+ program.command({
40
+ command: 'info',
41
+ describe: 'Prints local environment and repository information',
42
+ builder: cmd => cmd
43
+ .example('$0 info', '# Prints information')
44
+ .example('$0 info --json', '# Prints information in JSON format')
45
+ .option('json', {
46
+ alias: 'j',
47
+ describe: 'Print output as JSON',
48
+ type: 'boolean',
49
+ }),
50
+ handler: async (args) => {
51
+ const systemInfo = await SystemInfo.getSystemInfo();
52
+ const repositoryInfo = SystemInfo.getRepositoryInfo(repository);
53
+ if (args.json) {
54
+ console.log(JSON.stringify({ ...systemInfo, repository: repositoryInfo }, undefined, 2));
55
+ return;
56
+ }
57
+ printSystemInfo(systemInfo);
58
+ printRepositoryInfo(repositoryInfo);
59
+ },
60
+ });
61
+ }
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ import type { Repository } from '../core/repository.js';
3
+ export declare function initCli(repository: Repository, program: Argv): void;
@@ -0,0 +1,113 @@
1
+ import colors from 'ansi-colors';
2
+ import EasyTable from 'easy-table';
3
+ import { ListService } from '../services/list.service.js';
4
+ import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
5
+ function statusLabel(status) {
6
+ switch (status) {
7
+ case 'dirty':
8
+ return colors.magenta('dirty');
9
+ case 'committed':
10
+ return colors.yellow('committed');
11
+ case 'changed':
12
+ return colors.cyan('changed');
13
+ default:
14
+ return '';
15
+ }
16
+ }
17
+ function printTable(items) {
18
+ const table = new EasyTable();
19
+ for (const it of items) {
20
+ table.cell('Package', colors.yellowBright(it.name));
21
+ table.cell('Version', colors.yellow(it.version));
22
+ table.cell('Private', it.private ? colors.magentaBright('yes') : '');
23
+ table.cell('Changed', statusLabel(it.status));
24
+ table.cell('Path', it.location);
25
+ table.newRow();
26
+ }
27
+ console.log(table.toString().trim());
28
+ console.log('');
29
+ console.log(colors.gray(`${items.length} Package(s) found`));
30
+ }
31
+ function printParseable(items) {
32
+ for (const it of items) {
33
+ console.log([
34
+ it.location,
35
+ it.name,
36
+ it.version,
37
+ it.private ? 'PRIVATE' : '',
38
+ it.status !== 'clean' ? it.status.toUpperCase() : '',
39
+ ].join('::'));
40
+ }
41
+ }
42
+ function printGraph(items) {
43
+ const graph = {};
44
+ for (const it of items)
45
+ graph[it.name] = it.dependencies;
46
+ console.log(JSON.stringify(graph, undefined, 2));
47
+ }
48
+ export function initCli(repository, program) {
49
+ program.command({
50
+ command: 'list [options...]',
51
+ aliases: ['ls'],
52
+ describe: 'Lists packages in repository',
53
+ builder: cmd => applyPackageFilterOptions(cmd)
54
+ .example('$0 list', '# List all packages')
55
+ .example('$0 list --json', '# List all packages in JSON format')
56
+ .conflicts('graph', ['parseable', 'json'])
57
+ .conflicts('short', ['parseable', 'json'])
58
+ .conflicts('changed', 'changed-since')
59
+ .option('short', {
60
+ alias: 's',
61
+ describe: 'Do not show extended information',
62
+ type: 'boolean',
63
+ })
64
+ .option('parseable', {
65
+ alias: 'p',
66
+ describe: 'Show parseable output',
67
+ type: 'boolean',
68
+ })
69
+ .option('toposort', {
70
+ alias: 't',
71
+ describe: 'Sort packages in topological order (dependencies before dependents) instead of lexical by directory',
72
+ type: 'boolean',
73
+ })
74
+ .option('graph', {
75
+ alias: 'g',
76
+ describe: 'Show dependency graph as a JSON-formatted adjacency list',
77
+ type: 'boolean',
78
+ })
79
+ .option('json', {
80
+ alias: 'j',
81
+ describe: 'Show output in JSON format',
82
+ type: 'boolean',
83
+ })
84
+ .option('changed', {
85
+ alias: 'c',
86
+ describe: 'Only list packages that have changed since the last publish (dirty or committed but not yet published)',
87
+ type: 'boolean',
88
+ })
89
+ .option('changed-since', {
90
+ describe: 'Only list packages that have changed since the given git commit/hash',
91
+ type: 'string',
92
+ }),
93
+ handler: async (args) => {
94
+ const items = await ListService.getPackages(repository, {
95
+ ...readPackageFilterOptions(args),
96
+ toposort: args.toposort,
97
+ changed: args.changed,
98
+ changedSince: args.changedSince,
99
+ });
100
+ if (args.graph)
101
+ printGraph(items);
102
+ else if (args.json)
103
+ console.log(JSON.stringify(items, undefined, 2));
104
+ else if (args.parseable)
105
+ printParseable(items);
106
+ else if (args.short)
107
+ for (const it of items)
108
+ console.log(it.name);
109
+ else
110
+ printTable(items);
111
+ },
112
+ });
113
+ }
@@ -0,0 +1,3 @@
1
+ import type { Argv } from 'yargs';
2
+ import type { Repository } from '../core/repository.js';
3
+ export declare function initCli(repository: Repository, program: Argv): void;
@@ -0,0 +1,147 @@
1
+ import readline from 'node:readline/promises';
2
+ import colors from 'ansi-colors';
3
+ import { CiService } from '../services/ci.service.js';
4
+ import { PublishService } from '../services/publish.service.js';
5
+ import { applyBranchGuardOptions, assertAllowedBranch, readBranchGuardOptions } from '../utils/branch-guard.js';
6
+ import { applyPackageFilterOptions, readPackageFilterOptions } from '../utils/package-filter.js';
7
+ export function initCli(repository, program) {
8
+ program.command({
9
+ command: 'publish',
10
+ describe: 'Publishes every non-private package whose local version is not already on the registry',
11
+ builder: cmd => applyBranchGuardOptions(applyPackageFilterOptions(cmd))
12
+ .example('$0 publish', '# Show the plan, then ask for confirmation')
13
+ .example('$0 publish --yes', '# Publish immediately, no confirmation')
14
+ .example('$0 publish --dry-run', '# Only show the plan, never publish')
15
+ .option('yes', {
16
+ alias: 'y',
17
+ describe: 'Skip the confirmation prompt and publish immediately',
18
+ type: 'boolean',
19
+ })
20
+ .option('dry-run', {
21
+ describe: 'Only show the plan - never publishes, regardless of --yes',
22
+ type: 'boolean',
23
+ })
24
+ .option('ignore-dirty', {
25
+ describe: 'Exclude a package with uncommitted local changes instead of aborting the whole run',
26
+ type: 'boolean',
27
+ })
28
+ .option('package-manager', {
29
+ describe: 'Package manager to publish with (default: npm, or .rmanrc "packageManager")',
30
+ choices: PACKAGE_MANAGERS,
31
+ })
32
+ .option('access', {
33
+ describe: 'npm publish --access <public|restricted> - required by the registry for a new scoped package',
34
+ choices: ['public', 'restricted'],
35
+ })
36
+ .option('tag', {
37
+ describe: 'npm publish --tag <tag> - the dist-tag this version is published under (default "latest")',
38
+ type: 'string',
39
+ })
40
+ .option('otp', {
41
+ describe: 'npm publish --otp <otp> - a 2FA one-time password, for registries that require it',
42
+ type: 'string',
43
+ })
44
+ .option('registry', {
45
+ describe: 'Registry to check against and publish to (default: whatever .npmrc already configures)',
46
+ type: 'string',
47
+ })
48
+ .option('userconfig', {
49
+ describe: 'Path to a custom .npmrc to use for both the registry check and the actual publish',
50
+ type: 'string',
51
+ })
52
+ .option('contents', {
53
+ describe: "Subdirectory to publish from, relative to each package's own directory - only consulted when a " +
54
+ 'package has no "publishConfig.directory" of its own (that always wins when present)',
55
+ type: 'string',
56
+ }),
57
+ handler: async (args) => {
58
+ await assertAllowedBranch(repository, readBranchGuardOptions(args));
59
+ const options = {
60
+ ...readPackageFilterOptions(args),
61
+ ignoreDirty: args.ignoreDirty,
62
+ registry: args.registry,
63
+ userconfig: args.userconfig,
64
+ };
65
+ const plan = await PublishService.getPlan(repository, options);
66
+ printPlan(plan);
67
+ const errors = plan.filter(e => e.status === 'error');
68
+ if (errors.length) {
69
+ const message = `${errors.length} package(s) have uncommitted local changes ` +
70
+ '(pass --ignore-dirty to exclude them instead of aborting)';
71
+ console.log(colors.red(message));
72
+ const err = new Error(message);
73
+ err.logged = true;
74
+ throw err;
75
+ }
76
+ if (!plan.some(e => e.status === 'publish')) {
77
+ console.log(colors.gray('Nothing to publish.'));
78
+ return;
79
+ }
80
+ if (args.dryRun)
81
+ return;
82
+ let proceed = !!args.yes;
83
+ if (!proceed) {
84
+ if (!process.stdout.isTTY) {
85
+ console.log(colors.gray('Not a TTY - refusing to prompt. Pass --yes to publish non-interactively.'));
86
+ return;
87
+ }
88
+ proceed = await confirm('Publish these packages?');
89
+ }
90
+ if (!proceed)
91
+ return;
92
+ const applied = await PublishService.applyPlan(repository, plan, {
93
+ ...options,
94
+ packageManager: args.packageManager,
95
+ access: args.access,
96
+ tag: args.tag,
97
+ otp: args.otp,
98
+ contents: args.contents,
99
+ });
100
+ let failed = false;
101
+ for (const entry of applied) {
102
+ if (entry.status === 'publish') {
103
+ console.log(colors.green('published'), colors.cyan(entry.package.name), entry.version);
104
+ }
105
+ else if (entry.status === 'error' && plan.find(e => e.package === entry.package)?.status === 'publish') {
106
+ failed = true;
107
+ console.log(colors.red('failed'), colors.cyan(entry.package.name), colors.red(entry.reason ?? ''));
108
+ }
109
+ }
110
+ if (failed) {
111
+ const err = new Error('"publish" failed');
112
+ err.logged = true;
113
+ throw err;
114
+ }
115
+ },
116
+ });
117
+ }
118
+ function printPlan(entries) {
119
+ for (const e of entries) {
120
+ const name = colors.cyan(e.package.name);
121
+ switch (e.status) {
122
+ case 'publish':
123
+ console.log(colors.green('publish'), name, e.version, colors.gray(e.reason ?? ''));
124
+ break;
125
+ case 'up-to-date':
126
+ console.log(colors.gray('up-to-date'), name, e.version);
127
+ break;
128
+ case 'skip':
129
+ console.log(colors.cyan('skip'), name, colors.gray(e.reason ?? ''));
130
+ break;
131
+ case 'error':
132
+ console.log(colors.red('error'), name, colors.red(e.reason ?? ''));
133
+ break;
134
+ }
135
+ }
136
+ }
137
+ async function confirm(question) {
138
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
139
+ try {
140
+ const answer = await rl.question(`${question} (y/N) `);
141
+ return /^y(es)?$/i.test(answer.trim());
142
+ }
143
+ finally {
144
+ rl.close();
145
+ }
146
+ }
147
+ const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'];
@@ -0,0 +1,6 @@
1
+ import type { Argv } from 'yargs';
2
+ import type { Repository } from '../core/repository.js';
3
+ import { RunService } from '../services/run.service.js';
4
+ export declare function applyRunOptions<T>(cmd: Argv<T>): Argv<T>;
5
+ export declare function readRunOptions(args: any): RunService.Options;
6
+ export declare function initCli(repository: Repository, program: Argv): void;