@travetto/repo 8.0.0-alpha.2 → 8.0.0-alpha.20

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 CHANGED
@@ -19,22 +19,27 @@ The repo module aims to provide concise monorepo based tools. The monorepo supp
19
19
  * Listing local modules
20
20
  * Running commands on all workspace modules
21
21
 
22
- ## CLI - Version
22
+ ## CLI - repo:version
23
23
  The versioning operation will find all the changed modules (and the modules that depend on the changed), and will update the versions in accordance with the user preferences. The versioning logic is backed by [Npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)/[Yarn](https://yarnpkg.com)'s versioning functionality and so it is identical to using the tool manually. The determination of what has or hasn't changed is relative to the last versioning commit.
24
24
 
25
- **Terminal: Version execution**
25
+ **Terminal: Help for repo:version**
26
26
  ```bash
27
- $ trv repo:version -h
27
+ $ trv repo:version --help
28
28
 
29
29
  Usage: repo:version [options] <level:major|minor|patch|premajor|preminor|prepatch|prerelease> [prefix:string]
30
30
 
31
+ Bump workspace module versions and optionally commit/tag release metadata.
32
+
33
+ Supports changed/all/direct module targeting and synchronizes dependency
34
+ versions after the version bump operation completes.
35
+
31
36
  Options:
32
37
  --mode <all|changed|direct> The mode for versioning
33
38
  -f, --force Force operation, even in a dirty workspace (default: false)
34
39
  --commit, --no-commit Produce release commit message (default: true)
35
40
  -m, --modules <string> The module when mode is single
36
41
  -t, --tag Should we create a tag for the version
37
- -h, --help display help for command
42
+ --help display help for command
38
43
  ```
39
44
 
40
45
  Level is a standard semver level of: major, minor, patch or prerelease. The prefix argument only applies to the prerelease and allows for determining the prerelease level. For example:
@@ -56,18 +61,23 @@ Date: Thu Feb 23 17:51:37 2023 -0500
56
61
  Publish @travetto/asset,@travetto/asset-web,@travetto/auth,@travetto/auth-model,@travetto/auth-web,@travetto/auth-web-jwt,@travetto/auth-web-passport,@travetto/auth-web-session,...
57
62
  ```
58
63
 
59
- ## CLI - Publish
64
+ ## CLI - repo:publish
60
65
  The publish functionality is relatively naive, but consistent. The code will look at all modules in the mono-repo and check the listed version against what is available in the npm registry. If the local version is newer, it is a candidate for publishing.
61
66
 
62
- **Terminal: Publish execution**
67
+ **Terminal: Help for repo:publish**
63
68
  ```bash
64
- $ trv repo:publish -h
69
+ $ trv repo:publish --help
65
70
 
66
71
  Usage: repo:publish [options]
67
72
 
73
+ Publish unpublished workspace modules to the package registry.
74
+
75
+ The command performs a publishability scan first, then publishes candidates.
76
+ Dry-run mode is enabled by default.
77
+
68
78
  Options:
69
79
  --dry-run, --no-dry-run Dry Run? (default: true)
70
- -h, --help display help for command
80
+ --help display help for command
71
81
  ```
72
82
 
73
83
  By default the tool will execute a dry run only, and requires passing a flag to disable the dry run.
@@ -79,27 +89,31 @@ npx trv repo:publish --no-dry-run
79
89
 
80
90
  If no modules are currently changed, then the command will indicate there is no work to do, and exit gracefully.
81
91
 
82
- ## CLI - List
92
+ ## CLI - repo:list
83
93
  The listing functionality provides the ability to get the workspace modules in the following formats:
94
+ * `list` - Standard text list, each module on its own line
95
+ * `graph` - Modules as a digraph, mapping interdependencies
96
+ * `json` - Graph of modules in JSON form, with additional data (useful for quickly building a dependency graph)
84
97
 
85
- **Terminal: List execution**
98
+ **Terminal: Help for repo:list**
86
99
  ```bash
87
- $ trv repo:list -h
100
+ $ trv repo:list --help
88
101
 
89
102
  Usage: repo:list [options]
90
103
 
104
+ List workspace modules and their relationships.
105
+
106
+ Output can be emitted as plain list, graphviz digraph, or JSON dependency
107
+ graph suitable for automation.
108
+
91
109
  Options:
92
110
  -c, --changed Only show changed modules (default: false)
93
111
  -f, --format <graph|json|list> Output format (default: "list")
94
112
  -fh, --from-hash <string> Start revision to check against
95
113
  -th, --to-hash <string> End revision to check against
96
- -h, --help display help for command
114
+ --help display help for command
97
115
  ```
98
116
 
99
- * `list` - Standard text list, each module on its own line
100
- * `graph` - Modules as a digraph, mapping interdependencies
101
- * `json` - Graph of modules in JSON form, with additional data (useful for quickly building a dependency graph)
102
-
103
117
  **Terminal: List execution of Monorepo**
104
118
  ```bash
105
119
  $ trv repo:list
@@ -129,6 +143,7 @@ module/email-inky
129
143
  module/email-nodemailer
130
144
  module/eslint
131
145
  module/image
146
+ module/llm-support
132
147
  module/log
133
148
  module/manifest
134
149
  module/model
@@ -136,6 +151,7 @@ module/model-dynamodb
136
151
  module/model-elasticsearch
137
152
  module/model-file
138
153
  module/model-firestore
154
+ module/model-indexed
139
155
  module/model-memory
140
156
  module/model-mongo
141
157
  module/model-mysql
@@ -151,7 +167,6 @@ module/pack
151
167
  module/registry
152
168
  module/repo
153
169
  module/runtime
154
- module/scaffold
155
170
  module/schema
156
171
  module/schema-faker
157
172
  module/terminal
@@ -167,21 +182,25 @@ module/worker
167
182
  related/todo-app
168
183
  ```
169
184
 
170
- ## CLI - Exec
171
- The exec command allows for running commands on all modules, or just changed modules.
185
+ ## CLI - repo:exec
172
186
 
173
- **Terminal: Exec execution**
187
+ **Terminal: Help for repo:exec**
174
188
  ```bash
175
- $ trv repo:exec -h
189
+ $ trv repo:exec --help
176
190
 
177
191
  Usage: repo:exec [options] <cmd:string> [args...:string]
178
192
 
193
+ Execute a shell command across workspace modules.
194
+
195
+ Supports running for all modules or only changed modules, with optional
196
+ concurrency and output prefixing controls.
197
+
179
198
  Options:
180
199
  -c, --changed Only changed modules (default: false)
181
200
  -w, --workers <number> Number of concurrent workers (default: 9)
182
201
  --prefix-output, --no-prefix-output Prefix output by folder (default: true)
183
202
  --show-stdout, --no-show-stdout Show stdout (default: true)
184
- -h, --help display help for command
203
+ --help display help for command
185
204
  ```
186
205
 
187
206
  The standard format includes prefixed output to help identify which module produced which output.
@@ -215,6 +234,7 @@ global-test/model_auth-session <workspace-root>/global-test/model_auth-session
215
234
  module/email-nodemailer <workspace-root>/module/email-nodemailer
216
235
  module/eslint <workspace-root>/module/eslint
217
236
  module/image <workspace-root>/module/image
237
+ module/llm-support <workspace-root>/module/llm-support
218
238
  module/log <workspace-root>/module/log
219
239
  module/manifest <workspace-root>/module/manifest
220
240
  module/model <workspace-root>/module/model
@@ -222,6 +242,7 @@ global-test/model_auth-session <workspace-root>/global-test/model_auth-session
222
242
  module/model-elasticsearch <workspace-root>/module/model-elasticsearch
223
243
  module/model-file <workspace-root>/module/model-file
224
244
  module/model-firestore <workspace-root>/module/model-firestore
245
+ module/model-indexed <workspace-root>/module/model-indexed
225
246
  module/model-memory <workspace-root>/module/model-memory
226
247
  module/model-mongo <workspace-root>/module/model-mongo
227
248
  module/model-mysql <workspace-root>/module/model-mysql
@@ -237,7 +258,6 @@ global-test/model_auth-session <workspace-root>/global-test/model_auth-session
237
258
  module/registry <workspace-root>/module/registry
238
259
  module/repo .
239
260
  module/runtime <workspace-root>/module/runtime
240
- module/scaffold <workspace-root>/module/scaffold
241
261
  module/schema <workspace-root>/module/schema
242
262
  module/schema-faker <workspace-root>/module/schema-faker
243
263
  module/terminal <workspace-root>/module/terminal
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/repo",
3
- "version": "8.0.0-alpha.2",
3
+ "version": "8.0.0-alpha.20",
4
4
  "type": "module",
5
5
  "description": "Monorepo utilities",
6
6
  "keywords": [
@@ -23,10 +23,10 @@
23
23
  "directory": "module/repo"
24
24
  },
25
25
  "dependencies": {
26
- "@travetto/worker": "^8.0.0-alpha.2"
26
+ "@travetto/worker": "^8.0.0-alpha.16"
27
27
  },
28
28
  "peerDependencies": {
29
- "@travetto/cli": "^8.0.0-alpha.3"
29
+ "@travetto/cli": "^8.0.0-alpha.22"
30
30
  },
31
31
  "peerDependenciesMeta": {
32
32
  "@travetto/cli": {
@@ -1,9 +1,9 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
2
 
3
- import { type ExecutionResult, Env, Util, ExecUtil, castTo, CodecUtil } from '@travetto/runtime';
3
+ import { type ExecutionResult, Env, Util, ExecUtil, CodecUtil, Runtime, RuntimeIndex, AsyncQueue } from '@travetto/runtime';
4
4
  import { CliModuleUtil } from '@travetto/cli';
5
5
  import type { IndexedModule } from '@travetto/manifest';
6
- import { StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
6
+ import { StyleUtil, Terminal, TerminalUtil, type ProgressEvent } from '@travetto/terminal';
7
7
  import { WorkPool } from '@travetto/worker';
8
8
 
9
9
  const COLORS = ([...[
@@ -13,15 +13,18 @@ const COLORS = ([...[
13
13
  '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee'
14
14
  ] as const]).toSorted(() => Math.random() < .5 ? -1 : 1).map(color => StyleUtil.getStyle(color));
15
15
 
16
- type ModuleRunConfig<T = ExecutionResult<string>> = {
16
+ type ModuleRunConfig = {
17
17
  progressMessage?: (module: IndexedModule | undefined) => string;
18
18
  filter?: (module: IndexedModule) => boolean | Promise<boolean>;
19
- transformResult?: (module: IndexedModule, result: ExecutionResult<string>) => T;
19
+ progressListKey?: (module: IndexedModule) => string;
20
+ isSuccess?: (result: ExecutionResult) => boolean;
21
+ showProgressList?: boolean;
20
22
  workerCount?: number;
21
23
  prefixOutput?: boolean;
22
24
  showStdout?: boolean;
23
25
  showStderr?: boolean;
24
26
  stableOutput?: boolean;
27
+ includeMonorepoRoot?: boolean;
25
28
  };
26
29
 
27
30
  const colorize = (value: string, idx: number): string => COLORS[idx % COLORS.length](value);
@@ -45,56 +48,99 @@ export class RepoExecUtil {
45
48
  /**
46
49
  * Run on all modules
47
50
  */
48
- static async execOnModules<T = ExecutionResult>(
51
+ static async execOnModules(
49
52
  mode: 'all' | 'workspace' | 'changed',
50
53
  operation: (module: IndexedModule) => ChildProcess,
51
- config: ModuleRunConfig<T> = {}
52
- ): Promise<Map<IndexedModule, T>> {
54
+ config: ModuleRunConfig = {}
55
+ ): Promise<Map<IndexedModule, ExecutionResult>> {
53
56
 
54
57
  config.showStdout = config.showStdout ?? (Env.DEBUG.isSet && !Env.DEBUG.isFalse);
55
58
  config.showStderr = config.showStderr ?? true;
56
- const transform = config.transformResult ?? ((module, result): T => castTo(result));
57
59
 
58
60
  const workerCount = config.workerCount ?? WorkPool.DEFAULT_SIZE;
59
61
 
60
62
  const modules = await CliModuleUtil.findModules(mode);
61
- const results = new Map<IndexedModule, T>();
63
+ const results = new Map<IndexedModule, ExecutionResult>();
62
64
  const processes = new Map<IndexedModule, ChildProcess>();
63
65
 
64
66
  const prefixes = config.prefixOutput !== false ? this.#buildPrefixes(modules) : {};
65
67
  const stdoutTerm = new Terminal(process.stdout);
66
68
  const stderrTerm = new Terminal(process.stderr);
67
69
 
68
- const work = WorkPool.runStreamProgress(async (module) => {
70
+ if (Runtime.workspace.mono && config.includeMonorepoRoot && !modules.some(module => module.name === Runtime.workspace.name)) {
71
+ modules.push(RuntimeIndex.getModule(Runtime.workspace.name)!);
72
+ }
73
+
74
+ const active = new Set<string>();
75
+ const activeItems = new AsyncQueue<{ idx: number, text: string }>();
76
+ const listHeader = ['', 'Modules in progress', '-------------------'];
77
+
78
+ if (stdoutTerm.interactive && config.showProgressList) {
79
+ stdoutTerm.writer.writeLines(listHeader).commit();
80
+ }
81
+
82
+ const modulesToRun = (config.filter ? await Promise.all(
83
+ modules.map(async module => (await config.filter!(module)) ? module : undefined)
84
+ ) : modules)
85
+ .filter((module): module is IndexedModule => !!module);
86
+
87
+ const work = WorkPool.runStream<IndexedModule, ExecutionResult>(async (module) => {
69
88
  try {
70
- if (!(await config.filter?.(module) === false)) {
71
- const prefix = prefixes[module.sourceFolder] ?? '';
72
- const subProcess = operation(module);
73
- processes.set(module, subProcess);
74
-
75
- if (config.showStdout && subProcess.stdout) {
76
- CodecUtil.readLines(subProcess.stdout, line =>
77
- stdoutTerm.writer.writeLine(`${prefix}${line.trimEnd()}`).commit()
78
- );
79
- }
80
- if (config.showStderr && subProcess.stderr) {
81
- CodecUtil.readLines(subProcess.stderr, line =>
82
- stderrTerm.writer.writeLine(`${prefix}${line.trimEnd()}`).commit()
83
- );
84
- }
85
-
86
- const result = await ExecUtil.getResult(subProcess, { catch: true });
87
- const output = transform(module, result);
88
- results.set(module, output);
89
+ const prefix = prefixes[module.sourceFolder] ?? '';
90
+ const listKey = config.progressListKey?.(module) ?? ` * ${module.name}`;
91
+ active.add(listKey);
92
+ [...active].sort().forEach((text, idx) => activeItems.add({ idx, text }));
93
+
94
+ const subProcess = operation(module);
95
+ processes.set(module, subProcess);
96
+
97
+ if (config.showStdout && subProcess.stdout) {
98
+ CodecUtil.readLines(subProcess.stdout, line =>
99
+ stdoutTerm.writer.writeLine(`${prefix}${line.trimEnd()}`).commit()
100
+ );
101
+ }
102
+ if (config.showStderr && subProcess.stderr) {
103
+ CodecUtil.readLines(subProcess.stderr, line =>
104
+ stderrTerm.writer.writeLine(`${prefix}${line.trimEnd()}`).commit()
105
+ );
89
106
  }
90
- return config.progressMessage?.(module) ?? module.name;
107
+
108
+ const result = await ExecUtil.getResult(subProcess, { catch: true });
109
+
110
+ active.delete(listKey);
111
+ [...active].sort().forEach((text, idx) => activeItems.add({ idx, text }));
112
+ for (let j = active.size; j < workerCount; j++) {
113
+ activeItems.add({ idx: j, text: '' }); // Force update to remove item if needed
114
+ }
115
+
116
+ results.set(module, result);
117
+ return result;
91
118
  } finally {
92
119
  processes.get(module!)?.kill();
93
120
  }
94
- }, modules, modules.length, { max: workerCount, min: workerCount });
121
+ }, modulesToRun, {
122
+ max: workerCount,
123
+ min: workerCount,
124
+ total: modules.length,
125
+ isSuccess: config.isSuccess ?? ((result: ExecutionResult): boolean => result.valid),
126
+ });
95
127
 
96
128
  if (config.progressMessage && stdoutTerm.interactive) {
97
- await stdoutTerm.streamToBottom(Util.mapAsyncIterable(work, TerminalUtil.progressBarUpdater(stdoutTerm, { withWaiting: true })));
129
+ if (config.showProgressList) {
130
+ stdoutTerm.streamList(activeItems);
131
+ }
132
+ await stdoutTerm.streamToBottom(
133
+ Util.mapAsyncIterable(
134
+ Util.mapAsyncIterable(work, (event): ProgressEvent<string> => {
135
+ const message = config.progressMessage?.(modulesToRun[event.progress.completed]) ?? 'Completed %completed/%total (%failed failed)';
136
+ return { ...event.progress, value: message };
137
+ }),
138
+ TerminalUtil.progressBarUpdater(stdoutTerm, { withWaiting: true }))
139
+ );
140
+
141
+ if (stdoutTerm.interactive && config.showProgressList) {
142
+ stdoutTerm.writer.changePosition({ y: -listHeader.length, x: 0 }).storePosition().writeLines(Array(listHeader.length).fill('')).restoreOnCommit().commit();
143
+ }
98
144
  } else {
99
145
  for await (const _ of work) {
100
146
  // Ensure its all consumed
@@ -2,9 +2,10 @@ import path from 'node:path';
2
2
  import { spawn, type ChildProcess } from 'node:child_process';
3
3
  import fs from 'node:fs/promises';
4
4
 
5
- import { JSONUtil, type ExecutionResult, Runtime } from '@travetto/runtime';
5
+ import { JSONUtil, type ExecutionResult, Runtime, ExecUtil } from '@travetto/runtime';
6
6
  import { type IndexedModule, type Package, PackageUtil } from '@travetto/manifest';
7
7
  import { CliModuleUtil } from '@travetto/cli';
8
+ import { TerminalUtil } from '@travetto/terminal';
8
9
 
9
10
  export type SemverLevel = 'minor' | 'patch' | 'major' | 'prerelease' | 'premajor' | 'preminor' | 'prepatch';
10
11
 
@@ -13,6 +14,65 @@ export type SemverLevel = 'minor' | 'patch' | 'major' | 'prerelease' | 'premajor
13
14
  */
14
15
  export class PackageManager {
15
16
 
17
+ /**
18
+ * Check if npm login is needed
19
+ */
20
+ static async needsLogin(): Promise<boolean> {
21
+ const result = await ExecUtil.getResult(spawn('npm', ['whoami']), { catch: true });
22
+ return !result.valid;
23
+ }
24
+
25
+ /**
26
+ * Check if OTP is needed for publishing
27
+ */
28
+ static async needsOtp(): Promise<boolean> {
29
+ try {
30
+ const result = await ExecUtil.getResult(spawn('npm', ['profile', 'get', '--json']), { catch: true });
31
+ if (result.valid) {
32
+ const info = JSONUtil.fromUTF8<{ tfa?: { mode?: string } }>(result.stdout);
33
+ return !!info?.tfa?.mode;
34
+ }
35
+ } catch {
36
+ // Ignore errors checking for OTP profile
37
+ }
38
+ return false;
39
+ }
40
+
41
+ /**
42
+ * Request an OTP token if required
43
+ */
44
+ static async requestOtp(): Promise<string | undefined> {
45
+ if (TerminalUtil.isInteractive()) {
46
+ console.log([
47
+ 'OTP token is required for publishing. Please provide an OTP token to proceed with publishing.',
48
+ 'This value will not be stored, and is only used for the current publish operation.'
49
+ ].join(' '));
50
+ return await TerminalUtil.prompt('Enter OTP token: ');
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Classify publish error from execution result
56
+ */
57
+ static classifyPublishError(result: ExecutionResult): string {
58
+ const errorText = ExecUtil.toString(result, 'any');
59
+
60
+ if (/EOTP|one-time password|two-factor|OTP/i.test(errorText)) {
61
+ return 'Two-factor authentication (OTP) failed or was missing.';
62
+ }
63
+ if (/EPUBLISHCONFLICT|E403(.){,100}previously published|cannot publish over/i.test(errorText)) {
64
+ return 'Version conflict: This version has already been published to the registry.';
65
+ }
66
+ if (/ENEEDAUTH|E401|login|unauthorized/i.test(errorText)) {
67
+ return 'Authentication failed: Please log in or check your registry credentials.';
68
+ }
69
+ if (/E403|permission|not allowed/i.test(errorText)) {
70
+ return 'Permission denied: You do not have access to publish this package.';
71
+ }
72
+
73
+ return errorText || 'Unknown publishing error.';
74
+ }
75
+
16
76
  /**
17
77
  * Is a module already published
18
78
  */
@@ -29,13 +89,19 @@ export class PackageManager {
29
89
  /**
30
90
  * Validate published result
31
91
  */
32
- static validatePublishedResult(result: ExecutionResult<string>): boolean {
33
- if (!result.valid && !result.stderr.includes('E404')) {
34
- throw new Error(result.stderr);
92
+ static validatePublishedResult(result: ExecutionResult): boolean {
93
+ const stderr = ExecUtil.toString(result, 'stderr');
94
+ if (!result.valid && !stderr.includes('E404')) {
95
+ throw new Error(stderr);
35
96
  }
36
97
 
37
- const parsed = JSONUtil.fromUTF8<{ data: { dist?: { integrity?: string } } }>(result.stdout || '{}');
38
- return parsed.data?.dist?.integrity !== undefined;
98
+ const stdout = ExecUtil.toString(result, 'stdout');
99
+ type PackageInfo = { dist?: { integrity?: string } };
100
+ let parsed = JSONUtil.fromUTF8<PackageInfo | { data: PackageInfo }>(stdout || '{}');
101
+ if ('data' in parsed) { // Yarn support
102
+ parsed = parsed.data;
103
+ }
104
+ return parsed.dist?.integrity !== undefined;
39
105
  }
40
106
 
41
107
  /**
@@ -68,7 +134,7 @@ export class PackageManager {
68
134
  /**
69
135
  * Publish a module
70
136
  */
71
- static publish(module: IndexedModule, dryRun: boolean | undefined): ChildProcess {
137
+ static publish(module: IndexedModule, dryRun: boolean | undefined, otp?: string): ChildProcess {
72
138
  if (dryRun) {
73
139
  return this.dryRunPackaging(module);
74
140
  }
@@ -78,7 +144,7 @@ export class PackageManager {
78
144
  switch (Runtime.workspace.manager) {
79
145
  case 'npm':
80
146
  case 'yarn':
81
- case 'pnpm': args = ['publish', '--tag', versionTag, '--access', 'public']; break;
147
+ case 'pnpm': args = ['publish', '--tag', versionTag, '--access', 'public', ...(otp ? ['--otp', otp] : [])]; break;
82
148
  }
83
149
  return spawn(Runtime.workspace.manager, args, { cwd: module.sourcePath });
84
150
  }
@@ -8,7 +8,10 @@ import { Max, Min } from '@travetto/schema';
8
8
  import { RepoExecUtil } from './bin/exec.ts';
9
9
 
10
10
  /**
11
- * Repo execution
11
+ * Execute a shell command across workspace modules.
12
+ *
13
+ * Supports running for all modules or only changed modules, with optional
14
+ * concurrency and output prefixing controls.
12
15
  */
13
16
  @CliCommand()
14
17
  export class RepoExecCommand implements CliCommandShape {
@@ -45,7 +48,7 @@ export class RepoExecCommand implements CliCommandShape {
45
48
  }
46
49
  }),
47
50
  {
48
- progressMessage: module => `Running '${cmd} ${finalArgs.join(' ')}' [%idx/%total] ${module?.sourceFolder ?? ''}`,
51
+ progressMessage: module => `Running '${cmd} ${finalArgs.join(' ')}' [%completed/%total] ${module?.sourceFolder ?? ''}`,
49
52
  showStdout: this.showStdout,
50
53
  prefixOutput: this.prefixOutput,
51
54
  workerCount: this.workers,
@@ -4,7 +4,10 @@ import { JSONUtil, Runtime, RuntimeIndex } from '@travetto/runtime';
4
4
  const write = (line: string): Promise<void> => new Promise(resolve => process.stdout.write(`${line}\n`, () => resolve()));
5
5
 
6
6
  /**
7
- * Allows for listing of modules
7
+ * List workspace modules and their relationships.
8
+ *
9
+ * Output can be emitted as plain list, graphviz digraph, or JSON dependency
10
+ * graph suitable for automation.
8
11
  */
9
12
  @CliCommand()
10
13
  export class ListModuleCommand implements CliCommandShape {
@@ -1,10 +1,14 @@
1
- import { type CliCommandShape, CliCommand } from '@travetto/cli';
1
+ import { type CliCommandShape, CliCommand, cliTpl } from '@travetto/cli';
2
+ import { RuntimeError } from '@travetto/runtime';
2
3
 
3
4
  import { PackageManager } from './bin/package-manager.ts';
4
5
  import { RepoExecUtil } from './bin/exec.ts';
5
6
 
6
7
  /**
7
- * Publish all pending modules
8
+ * Publish unpublished workspace modules to the package registry.
9
+ *
10
+ * The command performs a publishability scan first, then publishes candidates.
11
+ * Dry-run mode is enabled by default.
8
12
  */
9
13
  @CliCommand()
10
14
  export class RepoPublishCommand implements CliCommandShape {
@@ -12,25 +16,62 @@ export class RepoPublishCommand implements CliCommandShape {
12
16
  /** Dry Run? */
13
17
  dryRun = true;
14
18
 
19
+ /** OTP Token */
20
+ otp?: string;
21
+
15
22
  async main(): Promise<void> {
16
23
  const published = await RepoExecUtil.execOnModules('workspace', module => PackageManager.isPublished(module), {
17
24
  filter: module => !!module.workspace && !module.internal,
18
- progressMessage: (module) => `Checking published [%idx/%total] -- ${module?.name}`,
25
+ progressMessage: (module) => `Checking published [%completed/%total] -- ${module?.name ?? ''}`,
19
26
  showStderr: false,
20
- transformResult: (module, result) => PackageManager.validatePublishedResult(result),
27
+ showProgressList: true,
28
+ isSuccess: () => true
21
29
  });
22
30
 
31
+ const unpublished = [...published.entries()]
32
+ .filter(([, result]) => !PackageManager.validatePublishedResult(result))
33
+ .map(([module]) => module);
34
+
23
35
  if (this.dryRun) {
24
- console.log('Unpublished modules', [...published.entries()].filter(entry => !entry[1]).map(([module]) => module.sourceFolder));
36
+ console.log('Unpublished modules', unpublished.map(module => module.sourceFolder));
25
37
  }
26
38
 
27
- await RepoExecUtil.execOnModules(
28
- 'workspace', module => PackageManager.publish(module, this.dryRun),
39
+ let otp = this.otp;
40
+ if (unpublished.length > 0 && !this.dryRun) {
41
+ if (await PackageManager.needsLogin()) {
42
+ throw new RuntimeError('NPM login is required to publish. Please run "npm login" to authenticate.');
43
+ }
44
+ if (!otp && await PackageManager.needsOtp()) {
45
+ otp = await PackageManager.requestOtp();
46
+ }
47
+ if (!otp) {
48
+ throw new RuntimeError('OTP token is required for publishing, but was not provided.');
49
+ }
50
+ }
51
+
52
+ const unpublishedSet = new Set(unpublished);
53
+
54
+ const results = await RepoExecUtil.execOnModules(
55
+ 'workspace',
56
+ module => PackageManager.publish(module, this.dryRun, otp),
29
57
  {
30
- progressMessage: (module) => `Published [%idx/%total] -- ${module?.name}`,
58
+ progressMessage: (module) => `Publishing [%completed/%total] -- ${module?.name ?? ''} (Failed %failed)`,
31
59
  showStdout: false,
32
- filter: module => published.get(module) === false
60
+ showStderr: false,
61
+ showProgressList: true,
62
+ filter: module => unpublishedSet.has(module)
33
63
  }
34
64
  );
65
+
66
+ const failures = [...results.entries()].filter(([, result]) => !result.valid);
67
+ if (failures.length > 0) {
68
+ console.error(cliTpl`\n${{ title: 'Failed to publish the following modules:' }}`);
69
+ console.error(cliTpl`${'-'.repeat(50)}`);
70
+ const nameWidth = Math.max(...failures.map(([module]) => module.name.length));
71
+ for (const [module, result] of failures) {
72
+ console.error(cliTpl`${{ identifier: module.name.padStart(nameWidth, ' ') }}: ${{ description: PackageManager.classifyPublishError(result) }}`);
73
+ }
74
+ process.exitCode = 1;
75
+ }
35
76
  }
36
77
  }
@@ -2,7 +2,10 @@ import { type CliCommandShape, CliCommand } from '@travetto/cli';
2
2
  import { PackageManager } from './bin/package-manager.ts';
3
3
 
4
4
  /**
5
- * Enforces all packages to write out their versions and dependencies
5
+ * Synchronize package versions and dependency ranges across the monorepo.
6
+ *
7
+ * Ensures package metadata reflects workspace version policy before publishing
8
+ * or release operations.
6
9
  */
7
10
  @CliCommand()
8
11
  export class VersionSyncCommand implements CliCommandShape {
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
 
3
3
  import { CliModuleUtil, type CliCommandShape, CliCommand, CliScmUtil } from '@travetto/cli';
4
- import { ExecUtil, Runtime } from '@travetto/runtime';
4
+ import { ExecUtil, Runtime, RuntimeError } from '@travetto/runtime';
5
5
  import { Validator } from '@travetto/schema';
6
6
 
7
7
  import { PackageManager, type SemverLevel } from './bin/package-manager.ts';
@@ -9,7 +9,10 @@ import { PackageManager, type SemverLevel } from './bin/package-manager.ts';
9
9
  const CHANGE_LEVELS = new Set<SemverLevel>(['prerelease', 'patch', 'prepatch']);
10
10
 
11
11
  /**
12
- * Version all changed dependencies
12
+ * Bump workspace module versions and optionally commit/tag release metadata.
13
+ *
14
+ * Supports changed/all/direct module targeting and synchronizes dependency
15
+ * versions after the version bump operation completes.
13
16
  */
14
17
  @CliCommand()
15
18
  @Validator(async (cmd) => {
@@ -43,7 +46,7 @@ export class RepoVersionCommand implements CliCommandShape {
43
46
 
44
47
  // Do we have valid changes?
45
48
  if (!modules.length) {
46
- throw new Error('No modules available for versioning');
49
+ throw new RuntimeError('No modules available for versioning');
47
50
  }
48
51
 
49
52
  await ExecUtil.getResult(PackageManager.version(modules, level, prefix));
@@ -57,7 +60,7 @@ export class RepoVersionCommand implements CliCommandShape {
57
60
  }
58
61
 
59
62
  // Touch package when done to trigger restart of compiler
60
- await fs.utimes(Runtime.workspaceRelative('package.json'), Date.now(), Date.now());
63
+ await fs.utimes(Runtime.workspaceRelative('package.json'), new Date(), new Date());
61
64
  }
62
65
  }
63
66
  }