@travetto/repo 8.0.0-alpha.8 → 8.0.0

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
@@ -13,21 +13,27 @@ npm install @travetto/repo
13
13
  yarn add @travetto/repo
14
14
  ```
15
15
 
16
- The repo module aims to provide concise monorepo based tools. The monorepo support within the [Travetto](https://travetto.dev) framework, is backed by the built in functionality of [Npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)/[Yarn](https://yarnpkg.com). This module is not a requirement for monorepo support, but provides some quality of life improvements for:
16
+ The repo module aims to provide concise monorepo based tools. The monorepo support within the [Travetto](https://travetto.dev) framework, is backed by the built in functionality of [Npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm)/[Yarn](https://yarnpkg.com). This module is not a requirement for monorepo support, but provides some quality of life improvements for:
17
17
  * Versioning releases
18
18
  * Publishing releases
19
19
  * Listing local modules
20
20
  * Running commands on all workspace modules
21
21
 
22
- ## CLI - Version
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.
22
+ ## CLI - repo:version
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
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
+ Description:
32
+ Bump workspace module versions and optionally commit/tag release metadata.
33
+
34
+ Supports changed/all/direct module targeting and synchronizes dependency
35
+ versions after the version bump operation completes.
36
+
31
37
  Options:
32
38
  --mode <all|changed|direct> The mode for versioning
33
39
  -f, --force Force operation, even in a dirty workspace (default: false)
@@ -37,7 +43,7 @@ Options:
37
43
  --help display help for command
38
44
  ```
39
45
 
40
- 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:
46
+ 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:
41
47
 
42
48
  **Terminal: Cutting a release candidate**
43
49
  ```bash
@@ -56,17 +62,24 @@ Date: Thu Feb 23 17:51:37 2023 -0500
56
62
  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
63
  ```
58
64
 
59
- ## CLI - Publish
60
- 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.
65
+ ## CLI - repo:publish
66
+ 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
67
 
62
- **Terminal: Publish execution**
68
+ **Terminal: Help for repo:publish**
63
69
  ```bash
64
70
  $ trv repo:publish --help
65
71
 
66
72
  Usage: repo:publish [options]
67
73
 
74
+ Description:
75
+ Publish unpublished workspace modules to the package registry.
76
+
77
+ The command performs a staleness scan first, then publishes candidates.
78
+ Dry-run mode is enabled by default.
79
+
68
80
  Options:
69
81
  --dry-run, --no-dry-run Dry Run? (default: true)
82
+ -o, --otp <string> OTP Token
70
83
  --help display help for command
71
84
  ```
72
85
 
@@ -79,15 +92,24 @@ npx trv repo:publish --no-dry-run
79
92
 
80
93
  If no modules are currently changed, then the command will indicate there is no work to do, and exit gracefully.
81
94
 
82
- ## CLI - List
95
+ ## CLI - repo:list
83
96
  The listing functionality provides the ability to get the workspace modules in the following formats:
97
+ * `list` - Standard text list, each module on its own line
98
+ * `graph` - Modules as a digraph, mapping interdependencies
99
+ * `json` - Graph of modules in JSON form, with additional data (useful for quickly building a dependency graph)
84
100
 
85
- **Terminal: List execution**
101
+ **Terminal: Help for repo:list**
86
102
  ```bash
87
103
  $ trv repo:list --help
88
104
 
89
105
  Usage: repo:list [options]
90
106
 
107
+ Description:
108
+ List workspace modules and their relationships.
109
+
110
+ Output can be emitted as plain list, Graphviz digraph, or JSON dependency
111
+ graph suitable for automation.
112
+
91
113
  Options:
92
114
  -c, --changed Only show changed modules (default: false)
93
115
  -f, --format <graph|json|list> Output format (default: "list")
@@ -96,10 +118,6 @@ Options:
96
118
  --help display help for command
97
119
  ```
98
120
 
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
121
  **Terminal: List execution of Monorepo**
104
122
  ```bash
105
123
  $ trv repo:list
@@ -127,8 +145,9 @@ module/email
127
145
  module/email-compiler
128
146
  module/email-inky
129
147
  module/email-nodemailer
130
- module/eslint
131
148
  module/image
149
+ module/lint
150
+ module/llm-support
132
151
  module/log
133
152
  module/manifest
134
153
  module/model
@@ -136,6 +155,7 @@ module/model-dynamodb
136
155
  module/model-elasticsearch
137
156
  module/model-file
138
157
  module/model-firestore
158
+ module/model-indexed
139
159
  module/model-memory
140
160
  module/model-mongo
141
161
  module/model-mysql
@@ -151,7 +171,6 @@ module/pack
151
171
  module/registry
152
172
  module/repo
153
173
  module/runtime
154
- module/scaffold
155
174
  module/schema
156
175
  module/schema-faker
157
176
  module/terminal
@@ -167,15 +186,20 @@ module/worker
167
186
  related/todo-app
168
187
  ```
169
188
 
170
- ## CLI - Exec
171
- The exec command allows for running commands on all modules, or just changed modules.
189
+ ## CLI - repo:exec
172
190
 
173
- **Terminal: Exec execution**
191
+ **Terminal: Help for repo:exec**
174
192
  ```bash
175
193
  $ trv repo:exec --help
176
194
 
177
195
  Usage: repo:exec [options] <cmd:string> [args...:string]
178
196
 
197
+ Description:
198
+ Execute a shell command across workspace modules.
199
+
200
+ Supports running for all modules or only changed modules, with optional
201
+ concurrency and output prefixing controls.
202
+
179
203
  Options:
180
204
  -c, --changed Only changed modules (default: false)
181
205
  -w, --workers <number> Number of concurrent workers (default: 9)
@@ -213,8 +237,9 @@ global-test/model_auth-session <workspace-root>/global-test/model_auth-session
213
237
  module/email-compiler <workspace-root>/module/email-compiler
214
238
  module/email-inky <workspace-root>/module/email-inky
215
239
  module/email-nodemailer <workspace-root>/module/email-nodemailer
216
- module/eslint <workspace-root>/module/eslint
217
240
  module/image <workspace-root>/module/image
241
+ module/lint <workspace-root>/module/lint
242
+ module/llm-support <workspace-root>/module/llm-support
218
243
  module/log <workspace-root>/module/log
219
244
  module/manifest <workspace-root>/module/manifest
220
245
  module/model <workspace-root>/module/model
@@ -222,6 +247,7 @@ global-test/model_auth-session <workspace-root>/global-test/model_auth-session
222
247
  module/model-elasticsearch <workspace-root>/module/model-elasticsearch
223
248
  module/model-file <workspace-root>/module/model-file
224
249
  module/model-firestore <workspace-root>/module/model-firestore
250
+ module/model-indexed <workspace-root>/module/model-indexed
225
251
  module/model-memory <workspace-root>/module/model-memory
226
252
  module/model-mongo <workspace-root>/module/model-mongo
227
253
  module/model-mysql <workspace-root>/module/model-mysql
@@ -237,7 +263,6 @@ global-test/model_auth-session <workspace-root>/global-test/model_auth-session
237
263
  module/registry <workspace-root>/module/registry
238
264
  module/repo .
239
265
  module/runtime <workspace-root>/module/runtime
240
- module/scaffold <workspace-root>/module/scaffold
241
266
  module/schema <workspace-root>/module/schema
242
267
  module/schema-faker <workspace-root>/module/schema-faker
243
268
  module/terminal <workspace-root>/module/terminal
package/__index__.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from './support/bin/exec.ts';
2
- export * from './support/bin/package-manager.ts';
2
+ export * from './support/bin/package-manager.ts';
package/package.json CHANGED
@@ -1,32 +1,35 @@
1
1
  {
2
2
  "name": "@travetto/repo",
3
- "version": "8.0.0-alpha.8",
4
- "type": "module",
3
+ "version": "8.0.0",
5
4
  "description": "Monorepo utilities",
6
5
  "keywords": [
6
+ "monorepo",
7
7
  "travetto",
8
- "typescript",
9
- "monorepo"
8
+ "typescript"
10
9
  ],
11
- "main": "__index__.ts",
12
10
  "homepage": "https://travetto.io",
13
11
  "license": "MIT",
14
12
  "author": {
15
- "email": "travetto.framework@gmail.com",
16
- "name": "Travetto Framework"
13
+ "name": "Travetto Framework",
14
+ "email": "travetto.framework@gmail.com"
17
15
  },
18
- "files": [
19
- "support"
20
- ],
21
16
  "repository": {
22
17
  "url": "git+https://github.com/travetto/travetto.git",
23
18
  "directory": "module/repo"
24
19
  },
20
+ "files": [
21
+ "support"
22
+ ],
23
+ "type": "module",
24
+ "main": "__index__.ts",
25
+ "publishConfig": {
26
+ "access": "public"
27
+ },
25
28
  "dependencies": {
26
- "@travetto/worker": "^8.0.0-alpha.7"
29
+ "@travetto/worker": "^8.0.0"
27
30
  },
28
31
  "peerDependencies": {
29
- "@travetto/cli": "^8.0.0-alpha.12"
32
+ "@travetto/cli": "^8.0.0"
30
33
  },
31
34
  "peerDependenciesMeta": {
32
35
  "@travetto/cli": {
@@ -35,8 +38,5 @@
35
38
  },
36
39
  "travetto": {
37
40
  "displayName": "Repo"
38
- },
39
- "publishConfig": {
40
- "access": "public"
41
41
  }
42
42
  }
@@ -1,27 +1,80 @@
1
1
  import type { ChildProcess } from 'node:child_process';
2
2
 
3
- import { type ExecutionResult, Env, Util, ExecUtil, castTo, CodecUtil } from '@travetto/runtime';
4
3
  import { CliModuleUtil } from '@travetto/cli';
5
4
  import type { IndexedModule } from '@travetto/manifest';
6
- import { StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
5
+ import { AsyncQueue, CodecUtil, Env, ExecUtil, type ExecutionResult, Runtime, RuntimeIndex, Util } from '@travetto/runtime';
6
+ import { type ProgressEvent, StyleUtil, Terminal, TerminalUtil } from '@travetto/terminal';
7
7
  import { WorkPool } from '@travetto/worker';
8
8
 
9
- const COLORS = ([...[
10
- '#8787ff', '#87afff', '#87d7ff', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af87ff', '#afafd7', '#afafff', '#afd7af', '#afd7d7', '#afd7ff', '#afff87', '#afffaf',
11
- '#afffd7', '#afffff', '#d787ff', '#d7afaf', '#d7afd7', '#d7afff', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff8787', '#ff87af',
12
- '#ff87d7', '#ff87ff', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd787', '#ffd7af', '#ffd7d7', '#ffd7ff', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#bcbcbc',
13
- '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee'
14
- ] as const]).toSorted(() => Math.random() < .5 ? -1 : 1).map(color => StyleUtil.getStyle(color));
15
-
16
- type ModuleRunConfig<T = ExecutionResult<string>> = {
9
+ const COLORS = [
10
+ ...([
11
+ '#8787ff',
12
+ '#87afff',
13
+ '#87d7ff',
14
+ '#87ff87',
15
+ '#87ffaf',
16
+ '#87ffd7',
17
+ '#87ffff',
18
+ '#af87ff',
19
+ '#afafd7',
20
+ '#afafff',
21
+ '#afd7af',
22
+ '#afd7d7',
23
+ '#afd7ff',
24
+ '#afff87',
25
+ '#afffaf',
26
+ '#afffd7',
27
+ '#afffff',
28
+ '#d787ff',
29
+ '#d7afaf',
30
+ '#d7afd7',
31
+ '#d7afff',
32
+ '#d7d7af',
33
+ '#d7d7d7',
34
+ '#d7d7ff',
35
+ '#d7ff87',
36
+ '#d7ffaf',
37
+ '#d7ffd7',
38
+ '#d7ffff',
39
+ '#ff8787',
40
+ '#ff87af',
41
+ '#ff87d7',
42
+ '#ff87ff',
43
+ '#ffaf87',
44
+ '#ffafaf',
45
+ '#ffafd7',
46
+ '#ffafff',
47
+ '#ffd787',
48
+ '#ffd7af',
49
+ '#ffd7d7',
50
+ '#ffd7ff',
51
+ '#ffff87',
52
+ '#ffffaf',
53
+ '#ffffd7',
54
+ '#ffffff',
55
+ '#bcbcbc',
56
+ '#c6c6c6',
57
+ '#d0d0d0',
58
+ '#dadada',
59
+ '#e4e4e4',
60
+ '#eeeeee'
61
+ ] as const)
62
+ ]
63
+ .toSorted(() => (Math.random() < 0.5 ? -1 : 1))
64
+ .map(color => StyleUtil.getStyle(color));
65
+
66
+ type ModuleRunConfig = {
17
67
  progressMessage?: (module: IndexedModule | undefined) => string;
18
68
  filter?: (module: IndexedModule) => boolean | Promise<boolean>;
19
- transformResult?: (module: IndexedModule, result: ExecutionResult<string>) => T;
69
+ progressListKey?: (module: IndexedModule) => string;
70
+ isSuccess?: (result: ExecutionResult) => boolean;
71
+ showProgressList?: boolean;
20
72
  workerCount?: number;
21
73
  prefixOutput?: boolean;
22
74
  showStdout?: boolean;
23
75
  showStderr?: boolean;
24
76
  stableOutput?: boolean;
77
+ includeMonorepoRoot?: boolean;
25
78
  };
26
79
 
27
80
  const colorize = (value: string, idx: number): string => COLORS[idx % COLORS.length](value);
@@ -30,7 +83,6 @@ const colorize = (value: string, idx: number): string => COLORS[idx % COLORS.len
30
83
  * Tools for running commands across all modules of the monorepo
31
84
  */
32
85
  export class RepoExecUtil {
33
-
34
86
  /**
35
87
  * Build equal sized prefix labels for outputting
36
88
  * @param modules
@@ -45,56 +97,108 @@ export class RepoExecUtil {
45
97
  /**
46
98
  * Run on all modules
47
99
  */
48
- static async execOnModules<T = ExecutionResult>(
100
+ static async execOnModules(
49
101
  mode: 'all' | 'workspace' | 'changed',
50
102
  operation: (module: IndexedModule) => ChildProcess,
51
- config: ModuleRunConfig<T> = {}
52
- ): Promise<Map<IndexedModule, T>> {
53
-
103
+ config: ModuleRunConfig = {}
104
+ ): Promise<Map<IndexedModule, ExecutionResult>> {
54
105
  config.showStdout = config.showStdout ?? (Env.DEBUG.isSet && !Env.DEBUG.isFalse);
55
106
  config.showStderr = config.showStderr ?? true;
56
- const transform = config.transformResult ?? ((module, result): T => castTo(result));
57
107
 
58
108
  const workerCount = config.workerCount ?? WorkPool.DEFAULT_SIZE;
59
109
 
60
110
  const modules = await CliModuleUtil.findModules(mode);
61
- const results = new Map<IndexedModule, T>();
111
+ const results = new Map<IndexedModule, ExecutionResult>();
62
112
  const processes = new Map<IndexedModule, ChildProcess>();
63
113
 
64
114
  const prefixes = config.prefixOutput !== false ? this.#buildPrefixes(modules) : {};
65
115
  const stdoutTerm = new Terminal(process.stdout);
66
116
  const stderrTerm = new Terminal(process.stderr);
67
117
 
68
- const work = WorkPool.runStreamProgress(async (module) => {
69
- try {
70
- if (!(await config.filter?.(module) === false)) {
118
+ if (Runtime.workspace.mono && config.includeMonorepoRoot && !modules.some(module => module.name === Runtime.workspace.name)) {
119
+ modules.push(RuntimeIndex.getModule(Runtime.workspace.name)!);
120
+ }
121
+
122
+ const active = new Set<string>();
123
+ const activeItems = new AsyncQueue<{ idx: number; text: string }>();
124
+ const listHeader = ['', 'Modules in progress', '-------------------'];
125
+
126
+ if (stdoutTerm.interactive && config.showProgressList) {
127
+ stdoutTerm.writer.writeLines(listHeader).commit();
128
+ }
129
+
130
+ const modulesToRun = (
131
+ config.filter ? await Promise.all(modules.map(async module => ((await config.filter!(module)) ? module : undefined))) : modules
132
+ ).filter((module): module is IndexedModule => !!module);
133
+
134
+ const work = WorkPool.runStream<IndexedModule, ExecutionResult>(
135
+ async module => {
136
+ try {
71
137
  const prefix = prefixes[module.sourceFolder] ?? '';
138
+ const listKey = config.progressListKey?.(module) ?? ` * ${module.name}`;
139
+ active.add(listKey);
140
+ [...active].sort().forEach((text, idx) => {
141
+ activeItems.add({ idx, text });
142
+ });
143
+
72
144
  const subProcess = operation(module);
73
145
  processes.set(module, subProcess);
74
146
 
75
147
  if (config.showStdout && subProcess.stdout) {
76
- CodecUtil.readLines(subProcess.stdout, line =>
77
- stdoutTerm.writer.writeLine(`${prefix}${line.trimEnd()}`).commit()
78
- );
148
+ CodecUtil.readLines(subProcess.stdout, line => stdoutTerm.writer.writeLine(`${prefix}${line.trimEnd()}`).commit());
79
149
  }
80
150
  if (config.showStderr && subProcess.stderr) {
81
- CodecUtil.readLines(subProcess.stderr, line =>
82
- stderrTerm.writer.writeLine(`${prefix}${line.trimEnd()}`).commit()
83
- );
151
+ CodecUtil.readLines(subProcess.stderr, line => stderrTerm.writer.writeLine(`${prefix}${line.trimEnd()}`).commit());
84
152
  }
85
153
 
86
154
  const result = await ExecUtil.getResult(subProcess, { catch: true });
87
- const output = transform(module, result);
88
- results.set(module, output);
155
+
156
+ active.delete(listKey);
157
+ [...active].sort().forEach((text, idx) => {
158
+ activeItems.add({ idx, text });
159
+ });
160
+ for (let j = active.size; j < workerCount; j += 1) {
161
+ activeItems.add({ idx: j, text: '' }); // Force update to remove item if needed
162
+ }
163
+
164
+ results.set(module, result);
165
+ return result;
166
+ } finally {
167
+ processes.get(module!)?.kill();
89
168
  }
90
- return config.progressMessage?.(module) ?? module.name;
91
- } finally {
92
- processes.get(module!)?.kill();
169
+ },
170
+ modulesToRun,
171
+ {
172
+ max: workerCount,
173
+ min: workerCount,
174
+ total: modules.length,
175
+ isSuccess: config.isSuccess ?? ((result: ExecutionResult): boolean => result.valid)
93
176
  }
94
- }, modules, modules.length, { max: workerCount, min: workerCount });
177
+ );
95
178
 
96
179
  if (config.progressMessage && stdoutTerm.interactive) {
97
- await stdoutTerm.streamToBottom(Util.mapAsyncIterable(work, TerminalUtil.progressBarUpdater(stdoutTerm, { withWaiting: true })));
180
+ if (config.showProgressList) {
181
+ stdoutTerm.streamList(activeItems);
182
+ }
183
+ await stdoutTerm.streamToBottom(
184
+ Util.mapAsyncIterable(
185
+ Util.mapAsyncIterable(work, (event): ProgressEvent<string> => {
186
+ const message =
187
+ config.progressMessage?.(modulesToRun[event.progress.completed]) ?? 'Completed %completed/%total (%failed failed)';
188
+ return { ...event.progress, value: message };
189
+ }),
190
+ TerminalUtil.progressBarUpdater(stdoutTerm, { withWaiting: true })
191
+ )
192
+ );
193
+
194
+ if (stdoutTerm.interactive && config.showProgressList) {
195
+ stdoutTerm.writer
196
+ .changePosition({ y: -listHeader.length, x: 0 })
197
+ .storePosition()
198
+ .writeLines(Array(listHeader.length).fill(''))
199
+ .restoreOnCommit()
200
+ .commit();
201
+ }
98
202
  } else {
99
203
  for await (const _ of work) {
100
204
  // Ensure its all consumed
@@ -103,4 +207,4 @@ export class RepoExecUtil {
103
207
 
104
208
  return results;
105
209
  }
106
- }
210
+ }
@@ -1,17 +1,79 @@
1
- import path from 'node:path';
2
- import { spawn, type ChildProcess } from 'node:child_process';
1
+ import { type ChildProcess, spawn } from 'node:child_process';
3
2
  import fs from 'node:fs/promises';
3
+ import path from 'node:path';
4
4
 
5
- import { JSONUtil, type ExecutionResult, Runtime } from '@travetto/runtime';
6
- import { type IndexedModule, type Package, PackageUtil } from '@travetto/manifest';
7
5
  import { CliModuleUtil } from '@travetto/cli';
6
+ import { type IndexedModule, type Package, PackageUtil } from '@travetto/manifest';
7
+ import { ExecUtil, type ExecutionResult, JSONUtil, Runtime } from '@travetto/runtime';
8
+ import { TerminalUtil } from '@travetto/terminal';
8
9
 
9
10
  export type SemverLevel = 'minor' | 'patch' | 'major' | 'prerelease' | 'premajor' | 'preminor' | 'prepatch';
10
11
 
12
+ /* cspell:words EOTP EPUBLISHCONFLICT ENEEDAUTH */
13
+
11
14
  /**
12
15
  * Utilities for working with package managers
13
16
  */
14
17
  export class PackageManager {
18
+ /**
19
+ * Check if npm login is needed
20
+ */
21
+ static async needsLogin(): Promise<boolean> {
22
+ const result = await ExecUtil.getResult(spawn('npm', ['whoami']), { catch: true });
23
+ return !result.valid;
24
+ }
25
+
26
+ /**
27
+ * Check if OTP is needed for publishing
28
+ */
29
+ static async needsOtp(): Promise<boolean> {
30
+ try {
31
+ const result = await ExecUtil.getResult(spawn('npm', ['profile', 'get', '--json']), { catch: true });
32
+ if (result.valid) {
33
+ const info = JSONUtil.fromUTF8<{ tfa?: { mode?: string } }>(result.stdout);
34
+ return !!info?.tfa?.mode;
35
+ }
36
+ } catch {
37
+ // Ignore errors checking for OTP profile
38
+ }
39
+ return false;
40
+ }
41
+
42
+ /**
43
+ * Request an OTP token if required
44
+ */
45
+ static async requestOtp(): Promise<string | undefined> {
46
+ if (TerminalUtil.isInteractive()) {
47
+ console.log(
48
+ [
49
+ 'OTP token is required for publishing. Please provide an OTP token to proceed with publishing.',
50
+ 'This value will not be stored, and is only used for the current publish operation.'
51
+ ].join(' ')
52
+ );
53
+ return await TerminalUtil.prompt('Enter OTP token: ');
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Classify publish error from execution result
59
+ */
60
+ static classifyPublishError(result: ExecutionResult): string {
61
+ const errorText = ExecUtil.toString(result, 'any');
62
+ if (/EOTP|one-time password|two-factor|OTP/i.test(errorText)) {
63
+ return 'Two-factor authentication (OTP) failed or was missing.';
64
+ }
65
+ if (/EPUBLISHCONFLICT|E403(.){,100}previously published|cannot publish over/i.test(errorText)) {
66
+ return 'Version conflict: This version has already been published to the registry.';
67
+ }
68
+ if (/ENEEDAUTH|E401|login|unauthorized/i.test(errorText)) {
69
+ return 'Authentication failed: Please log in or check your registry credentials.';
70
+ }
71
+ if (/E403|permission|not allowed/i.test(errorText)) {
72
+ return 'Permission denied: You do not have access to publish this package.';
73
+ }
74
+
75
+ return errorText || 'Unknown publishing error.';
76
+ }
15
77
 
16
78
  /**
17
79
  * Is a module already published
@@ -21,7 +83,9 @@ export class PackageManager {
21
83
  switch (Runtime.workspace.manager) {
22
84
  case 'npm':
23
85
  case 'yarn':
24
- case 'pnpm': args = ['info', `${module.name}@${module.version}`, '--json']; break;
86
+ case 'pnpm':
87
+ args = ['info', `${module.name}@${module.version}`, '--json'];
88
+ break;
25
89
  }
26
90
  return spawn(Runtime.workspace.manager, args, { cwd: module.sourceFolder });
27
91
  }
@@ -29,14 +93,17 @@ export class PackageManager {
29
93
  /**
30
94
  * Validate published result
31
95
  */
32
- static validatePublishedResult(result: ExecutionResult<string>): boolean {
33
- if (!result.valid && !result.stderr.includes('E404')) {
34
- throw new Error(result.stderr);
96
+ static validatePublishedResult(result: ExecutionResult): boolean {
97
+ const stderr = ExecUtil.toString(result, 'stderr');
98
+ if (!result.valid && !stderr.includes('E404')) {
99
+ throw new Error(stderr);
35
100
  }
36
101
 
102
+ const stdout = ExecUtil.toString(result, 'stdout');
37
103
  type PackageInfo = { dist?: { integrity?: string } };
38
- let parsed = JSONUtil.fromUTF8<PackageInfo | { data: PackageInfo }>(result.stdout || '{}');
39
- if ('data' in parsed) { // Yarn support
104
+ let parsed = JSONUtil.fromUTF8<PackageInfo | { data: PackageInfo }>(stdout || '{}');
105
+ if ('data' in parsed) {
106
+ // Yarn support
40
107
  parsed = parsed.data;
41
108
  }
42
109
  return parsed.dist?.integrity !== undefined;
@@ -51,7 +118,9 @@ export class PackageManager {
51
118
  switch (Runtime.workspace.manager) {
52
119
  case 'npm':
53
120
  case 'yarn':
54
- case 'pnpm': args = ['version', '--no-workspaces-update', level, ...(preid ? ['--preid', preid] : [])]; break;
121
+ case 'pnpm':
122
+ args = ['version', '--no-workspaces-update', level, ...(preid ? ['--preid', preid] : [])];
123
+ break;
55
124
  }
56
125
  return spawn(Runtime.workspace.manager, [...args, ...moduleArgs], { cwd: Runtime.workspace.path, stdio: 'inherit' });
57
126
  }
@@ -64,7 +133,9 @@ export class PackageManager {
64
133
  switch (Runtime.workspace.manager) {
65
134
  case 'npm':
66
135
  case 'yarn':
67
- case 'pnpm': args = ['pack', '--dry-run']; break;
136
+ case 'pnpm':
137
+ args = ['pack', '--dry-run'];
138
+ break;
68
139
  }
69
140
  return spawn(Runtime.workspace.manager, args, { cwd: module.sourcePath });
70
141
  }
@@ -72,7 +143,7 @@ export class PackageManager {
72
143
  /**
73
144
  * Publish a module
74
145
  */
75
- static publish(module: IndexedModule, dryRun: boolean | undefined): ChildProcess {
146
+ static publish(module: IndexedModule, dryRun: boolean | undefined, otp?: string): ChildProcess {
76
147
  if (dryRun) {
77
148
  return this.dryRunPackaging(module);
78
149
  }
@@ -82,7 +153,9 @@ export class PackageManager {
82
153
  switch (Runtime.workspace.manager) {
83
154
  case 'npm':
84
155
  case 'yarn':
85
- case 'pnpm': args = ['publish', '--tag', versionTag, '--access', 'public']; break;
156
+ case 'pnpm':
157
+ args = ['publish', '--tag', versionTag, '--access', 'public', ...(otp ? ['--otp', otp] : [])];
158
+ break;
86
159
  }
87
160
  return spawn(Runtime.workspace.manager, args, { cwd: module.sourcePath });
88
161
  }
@@ -112,12 +185,7 @@ export class PackageManager {
112
185
  });
113
186
 
114
187
  for (const { pkg } of packages) {
115
- for (const group of [
116
- pkg.dependencies ?? {},
117
- pkg.devDependencies ?? {},
118
- pkg.optionalDependencies ?? {},
119
- pkg.peerDependencies ?? {}
120
- ]) {
188
+ for (const group of [pkg.dependencies ?? {}, pkg.devDependencies ?? {}, pkg.optionalDependencies ?? {}, pkg.peerDependencies ?? {}]) {
121
189
  for (const [module, ver] of Object.entries(versions)) {
122
190
  if (module in group && !/^[*]|(file:.*)$/.test(group[module])) {
123
191
  group[module] = ver;
@@ -132,4 +200,4 @@ export class PackageManager {
132
200
 
133
201
  return versions;
134
202
  }
135
- }
203
+ }
@@ -1,23 +1,26 @@
1
1
  import { spawn } from 'node:child_process';
2
2
 
3
- import { CliCommand, CliParseUtil, type CliCommandShape } from '@travetto/cli';
4
- import { WorkPool } from '@travetto/worker';
3
+ import { CliCommand, type CliCommandShape, CliParseUtil } from '@travetto/cli';
5
4
  import { Env } from '@travetto/runtime';
6
5
  import { Max, Min } from '@travetto/schema';
6
+ import { WorkPool } from '@travetto/worker';
7
7
 
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 {
15
-
16
18
  /** Only changed modules */
17
19
  changed = false;
18
20
 
19
21
  /** Number of concurrent workers */
20
- @Min(1) @Max(WorkPool.MAX_SIZE)
22
+ @Min(1)
23
+ @Max(WorkPool.MAX_SIZE)
21
24
  workers = WorkPool.DEFAULT_SIZE;
22
25
 
23
26
  /** Prefix output by folder */
@@ -32,23 +35,24 @@ export class RepoExecCommand implements CliCommandShape {
32
35
 
33
36
  async main(cmd: string, args: string[] = []): Promise<void> {
34
37
  const parsed = CliParseUtil.getState(this);
35
- const finalArgs = [...args, ...parsed?.unknown ?? []];
38
+ const finalArgs = [...args, ...(parsed?.unknown ?? [])];
36
39
 
37
40
  await RepoExecUtil.execOnModules(
38
41
  this.changed ? 'changed' : 'workspace',
39
- module => spawn(cmd, finalArgs, {
40
- cwd: module.sourceFolder,
41
- env: {
42
- ...process.env,
43
- ...Env.TRV_MODULE.export(module.name),
44
- ...Env.TRV_MANIFEST.export(undefined)
45
- }
46
- }),
42
+ module =>
43
+ spawn(cmd, finalArgs, {
44
+ cwd: module.sourceFolder,
45
+ env: {
46
+ ...process.env,
47
+ ...Env.TRV_MODULE.export(module.name),
48
+ ...Env.TRV_MANIFEST.export(undefined)
49
+ }
50
+ }),
47
51
  {
48
- progressMessage: module => `Running '${cmd} ${finalArgs.join(' ')}' [%idx/%total] ${module?.sourceFolder ?? ''}`,
52
+ progressMessage: module => `Running '${cmd} ${finalArgs.join(' ')}' [%completed/%total] ${module?.sourceFolder ?? ''}`,
49
53
  showStdout: this.showStdout,
50
54
  prefixOutput: this.prefixOutput,
51
- workerCount: this.workers,
55
+ workerCount: this.workers
52
56
  }
53
57
  );
54
58
  }
@@ -1,14 +1,16 @@
1
- import { type CliCommandShape, CliCommand, CliModuleUtil } from '@travetto/cli';
1
+ import { CliCommand, type CliCommandShape, CliModuleUtil } from '@travetto/cli';
2
2
  import { JSONUtil, Runtime, RuntimeIndex } from '@travetto/runtime';
3
3
 
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 {
11
-
12
14
  /** Only show changed modules */
13
15
  changed = false;
14
16
 
@@ -28,7 +30,6 @@ export class ListModuleCommand implements CliCommandShape {
28
30
  toHash?: string;
29
31
 
30
32
  async main(): Promise<void> {
31
-
32
33
  const modules = await CliModuleUtil.findModules(this.changed ? 'changed' : 'workspace', this.fromHash, this.toHash);
33
34
  switch (this.format) {
34
35
  case 'list': {
@@ -39,11 +40,12 @@ export class ListModuleCommand implements CliCommandShape {
39
40
  }
40
41
  case 'json': {
41
42
  const outputMap = CliModuleUtil.getDependencyGraph(modules);
42
- await write(JSONUtil.toUTF8(
43
- Object.entries(outputMap)
44
- .map(([name, children]) => ({ name, children, workspace: RuntimeIndex.getModule(name)?.workspace })),
45
- { indent: 2 }
46
- ));
43
+ await write(
44
+ JSONUtil.toUTF8(
45
+ Object.entries(outputMap).map(([name, children]) => ({ name, children, workspace: RuntimeIndex.getModule(name)?.workspace })),
46
+ { indent: 2 }
47
+ )
48
+ );
47
49
  break;
48
50
  }
49
51
  case 'graph': {
@@ -60,4 +62,4 @@ export class ListModuleCommand implements CliCommandShape {
60
62
  }
61
63
  }
62
64
  }
63
- }
65
+ }
@@ -1,36 +1,77 @@
1
- import { type CliCommandShape, CliCommand } from '@travetto/cli';
1
+ import { CliCommand, type CliCommandShape, cliTpl } from '@travetto/cli';
2
+ import { RuntimeError } from '@travetto/runtime';
2
3
 
3
- import { PackageManager } from './bin/package-manager.ts';
4
4
  import { RepoExecUtil } from './bin/exec.ts';
5
+ import { PackageManager } from './bin/package-manager.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 staleness 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 {
11
-
12
15
  /** Dry Run? */
13
16
  dryRun = true;
14
17
 
18
+ /** OTP Token */
19
+ otp?: string;
20
+
15
21
  async main(): Promise<void> {
16
22
  const published = await RepoExecUtil.execOnModules('workspace', module => PackageManager.isPublished(module), {
17
23
  filter: module => !!module.workspace && !module.internal,
18
- progressMessage: (module) => `Checking published [%idx/%total] -- ${module?.name}`,
24
+ progressMessage: module => `Checking published [%completed/%total] -- ${module?.name ?? ''}`,
19
25
  showStderr: false,
20
- transformResult: (module, result) => PackageManager.validatePublishedResult(result),
26
+ showProgressList: true,
27
+ isSuccess: () => true
21
28
  });
22
29
 
30
+ const unpublished = [...published.entries()]
31
+ .filter(([, result]) => !PackageManager.validatePublishedResult(result))
32
+ .map(([module]) => module);
33
+
23
34
  if (this.dryRun) {
24
- console.log('Unpublished modules', [...published.entries()].filter(entry => !entry[1]).map(([module]) => module.sourceFolder));
35
+ console.log(
36
+ 'Unpublished modules',
37
+ unpublished.map(module => module.sourceFolder)
38
+ );
25
39
  }
26
40
 
27
- await RepoExecUtil.execOnModules(
28
- 'workspace', module => PackageManager.publish(module, this.dryRun),
29
- {
30
- progressMessage: (module) => `Published [%idx/%total] -- ${module?.name}`,
31
- showStdout: false,
32
- filter: module => published.get(module) === false
41
+ let otp = this.otp;
42
+ if (unpublished.length > 0 && !this.dryRun) {
43
+ if (await PackageManager.needsLogin()) {
44
+ throw new RuntimeError('NPM login is required to publish. Please run "npm login" to authenticate.');
45
+ }
46
+ if (!otp && (await PackageManager.needsOtp())) {
47
+ otp = await PackageManager.requestOtp();
33
48
  }
34
- );
49
+ if (!otp) {
50
+ throw new RuntimeError('OTP token is required for publishing, but was not provided.');
51
+ }
52
+ }
53
+
54
+ const unpublishedSet = new Set(unpublished);
55
+
56
+ const results = await RepoExecUtil.execOnModules('workspace', module => PackageManager.publish(module, this.dryRun, otp), {
57
+ progressMessage: module => `Publishing [%completed/%total] -- ${module?.name ?? ''} (Failed %failed)`,
58
+ showStdout: false,
59
+ showStderr: false,
60
+ showProgressList: true,
61
+ filter: module => unpublishedSet.has(module)
62
+ });
63
+
64
+ const failures = [...results.entries()].filter(([, result]) => !result.valid);
65
+ if (failures.length > 0) {
66
+ console.error(cliTpl`\n${{ title: 'Failed to publish the following modules:' }}`);
67
+ console.error(cliTpl`${'-'.repeat(50)}`);
68
+ const nameWidth = Math.max(...failures.map(([module]) => module.name.length));
69
+ for (const [module, result] of failures) {
70
+ console.error(
71
+ cliTpl`${{ identifier: module.name.padStart(nameWidth, ' ') }}: ${{ description: PackageManager.classifyPublishError(result) }}`
72
+ );
73
+ }
74
+ process.exitCode = 1;
75
+ }
35
76
  }
36
- }
77
+ }
@@ -1,12 +1,16 @@
1
- import { type CliCommandShape, CliCommand } from '@travetto/cli';
1
+ import { CliCommand, type CliCommandShape } from '@travetto/cli';
2
+
2
3
  import { PackageManager } from './bin/package-manager.ts';
3
4
 
4
5
  /**
5
- * Enforces all packages to write out their versions and dependencies
6
+ * Synchronize package versions and dependency ranges across the monorepo.
7
+ *
8
+ * Ensures package metadata reflects workspace version policy before publishing
9
+ * or release operations.
6
10
  */
7
11
  @CliCommand()
8
12
  export class VersionSyncCommand implements CliCommandShape {
9
13
  async main(): Promise<void> {
10
14
  await PackageManager.synchronizeVersions();
11
15
  }
12
- }
16
+ }
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
 
3
- import { CliModuleUtil, type CliCommandShape, CliCommand, CliScmUtil } from '@travetto/cli';
4
- import { ExecUtil, Runtime } from '@travetto/runtime';
3
+ import { CliCommand, type CliCommandShape, CliModuleUtil, CliScmUtil } from '@travetto/cli';
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,11 +9,14 @@ 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
- @Validator(async (cmd) => {
16
- if (!cmd.force && await CliScmUtil.isWorkspaceDirty()) {
18
+ @Validator(async cmd => {
19
+ if (!cmd.force && (await CliScmUtil.isWorkspaceDirty())) {
17
20
  return { message: 'Cannot update versions with uncommitted changes', path: 'custom', kind: 'invalid' };
18
21
  }
19
22
  })
@@ -35,7 +38,7 @@ export class RepoVersionCommand implements CliCommandShape {
35
38
  async main(level: SemverLevel, prefix?: string): Promise<void> {
36
39
  const mode = this.mode ?? (CHANGE_LEVELS.has(level) ? 'changed' : 'workspace');
37
40
 
38
- this.tag ??= (level === 'minor' || level === 'major');
41
+ this.tag ??= level === 'minor' || level === 'major';
39
42
 
40
43
  const allModules = await CliModuleUtil.findModules(mode === 'direct' ? 'all' : mode);
41
44
 
@@ -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
+ }