@smoothbricks/cli 0.1.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.
Files changed (86) hide show
  1. package/README.md +289 -0
  2. package/bin/smoo +4 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +141 -0
  6. package/dist/github-ci/index.d.ts +11 -0
  7. package/dist/github-ci/index.d.ts.map +1 -0
  8. package/dist/github-ci/index.js +174 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +1 -0
  12. package/dist/lib/json.d.ts +13 -0
  13. package/dist/lib/json.d.ts.map +1 -0
  14. package/dist/lib/json.js +57 -0
  15. package/dist/lib/run.d.ts +5 -0
  16. package/dist/lib/run.d.ts.map +1 -0
  17. package/dist/lib/run.js +64 -0
  18. package/dist/lib/workspace.d.ts +23 -0
  19. package/dist/lib/workspace.d.ts.map +1 -0
  20. package/dist/lib/workspace.js +101 -0
  21. package/dist/monorepo/commit-msg.d.ts +2 -0
  22. package/dist/monorepo/commit-msg.d.ts.map +1 -0
  23. package/dist/monorepo/commit-msg.js +34 -0
  24. package/dist/monorepo/git-config.d.ts +2 -0
  25. package/dist/monorepo/git-config.d.ts.map +1 -0
  26. package/dist/monorepo/git-config.js +37 -0
  27. package/dist/monorepo/index.d.ts +19 -0
  28. package/dist/monorepo/index.d.ts.map +1 -0
  29. package/dist/monorepo/index.js +59 -0
  30. package/dist/monorepo/lockfile.d.ts +2 -0
  31. package/dist/monorepo/lockfile.d.ts.map +1 -0
  32. package/dist/monorepo/lockfile.js +34 -0
  33. package/dist/monorepo/managed-files.d.ts +8 -0
  34. package/dist/monorepo/managed-files.d.ts.map +1 -0
  35. package/dist/monorepo/managed-files.js +130 -0
  36. package/dist/monorepo/nx-sync.d.ts +2 -0
  37. package/dist/monorepo/nx-sync.d.ts.map +1 -0
  38. package/dist/monorepo/nx-sync.js +5 -0
  39. package/dist/monorepo/package-hygiene.d.ts +3 -0
  40. package/dist/monorepo/package-hygiene.d.ts.map +1 -0
  41. package/dist/monorepo/package-hygiene.js +12 -0
  42. package/dist/monorepo/package-policy.d.ts +8 -0
  43. package/dist/monorepo/package-policy.d.ts.map +1 -0
  44. package/dist/monorepo/package-policy.js +279 -0
  45. package/dist/monorepo/packed-package.d.ts +2 -0
  46. package/dist/monorepo/packed-package.d.ts.map +1 -0
  47. package/dist/monorepo/packed-package.js +92 -0
  48. package/dist/monorepo/packs/index.d.ts +10 -0
  49. package/dist/monorepo/packs/index.d.ts.map +1 -0
  50. package/dist/monorepo/packs/index.js +87 -0
  51. package/dist/monorepo/runtime.d.ts +2 -0
  52. package/dist/monorepo/runtime.d.ts.map +1 -0
  53. package/dist/monorepo/runtime.js +37 -0
  54. package/dist/release/index.d.ts +24 -0
  55. package/dist/release/index.d.ts.map +1 -0
  56. package/dist/release/index.js +325 -0
  57. package/managed/raw/.git-format-staged.yml +47 -0
  58. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +66 -0
  59. package/managed/raw/tooling/git-hooks/commit-msg.sh +9 -0
  60. package/managed/raw/tooling/git-hooks/pre-commit.sh +19 -0
  61. package/managed/templates/github/actions/cache-nix-devenv/action.yml +79 -0
  62. package/managed/templates/github/actions/cache-node-modules/action.yml +13 -0
  63. package/managed/templates/github/actions/cache-nx/action.yml +21 -0
  64. package/managed/templates/github/actions/save-nix-devenv/action.yml +39 -0
  65. package/managed/templates/github/actions/setup-devenv/action.yml +63 -0
  66. package/managed/templates/github/workflows/ci.yml +98 -0
  67. package/managed/templates/github/workflows/publish.yml +136 -0
  68. package/package.json +68 -0
  69. package/src/cli.ts +149 -0
  70. package/src/github-ci/index.ts +196 -0
  71. package/src/index.ts +1 -0
  72. package/src/lib/json.ts +68 -0
  73. package/src/lib/run.ts +76 -0
  74. package/src/lib/workspace.ts +125 -0
  75. package/src/monorepo/commit-msg.ts +35 -0
  76. package/src/monorepo/git-config.ts +43 -0
  77. package/src/monorepo/index.ts +78 -0
  78. package/src/monorepo/lockfile.ts +37 -0
  79. package/src/monorepo/managed-files.ts +153 -0
  80. package/src/monorepo/nx-sync.ts +6 -0
  81. package/src/monorepo/package-hygiene.ts +14 -0
  82. package/src/monorepo/package-policy.ts +307 -0
  83. package/src/monorepo/packed-package.ts +101 -0
  84. package/src/monorepo/packs/index.ts +115 -0
  85. package/src/monorepo/runtime.ts +41 -0
  86. package/src/release/index.ts +394 -0
@@ -0,0 +1,41 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { $ } from 'bun';
4
+ import { getOrCreateRecord, readJsonObject, setStringProperty, writeJsonObject } from '../lib/json.js';
5
+
6
+ export async function syncRootRuntimeVersions(root: string): Promise<void> {
7
+ const packageJsonPath = join(root, 'package.json');
8
+ const packageJson = readJsonObject(packageJsonPath);
9
+ if (!packageJson) {
10
+ throw new Error('package.json not found or invalid');
11
+ }
12
+ const node = runtimeCommand(root, 'node');
13
+ const bun = runtimeCommand(root, 'bun');
14
+ const nodeVersion = (await $`${node} --version`.cwd(root).text()).trim().replace(/^v/, '');
15
+ const bunVersion = (await $`${bun} --version`.cwd(root).text()).trim();
16
+ const nodeMajor = nodeVersion.split('.', 1)[0];
17
+ if (!nodeMajor) {
18
+ throw new Error(`Unable to derive Node major version from ${nodeVersion}`);
19
+ }
20
+
21
+ let changed = false;
22
+ const engines = getOrCreateRecord(packageJson, 'engines');
23
+ changed = setStringProperty(engines, 'node', `>=${nodeMajor}.0.0`) || changed;
24
+ changed = setStringProperty(packageJson, 'packageManager', `bun@${bunVersion}`) || changed;
25
+ const devDependencies = getOrCreateRecord(packageJson, 'devDependencies');
26
+ changed = setStringProperty(devDependencies, '@types/node', `~${nodeMajor}.0.0`) || changed;
27
+ changed = setStringProperty(devDependencies, '@types/bun', bunVersion) || changed;
28
+
29
+ if (changed) {
30
+ writeJsonObject(packageJsonPath, packageJson);
31
+ console.log('updated package.json runtime versions');
32
+ }
33
+ }
34
+
35
+ function runtimeCommand(root: string, name: 'bun' | 'node'): string {
36
+ const candidates = [
37
+ join(root, 'tooling', 'direnv', '.devenv', 'profile', 'bin', name),
38
+ join(root, 'tooling', 'devenv', '.profile', 'bin', name),
39
+ ];
40
+ return candidates.find((path) => existsSync(path)) ?? name;
41
+ }
@@ -0,0 +1,394 @@
1
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { createInterface } from 'node:readline/promises';
5
+ import { Writable } from 'node:stream';
6
+ import { $ } from 'bun';
7
+ import { decode, run, runStatus } from '../lib/run.js';
8
+ import { listPublicPackages, readPackageJson, repositoryInfo } from '../lib/workspace.js';
9
+
10
+ export interface ReleaseVersionOptions {
11
+ bump: string;
12
+ dryRun?: boolean;
13
+ }
14
+
15
+ export interface ReleasePublishOptions {
16
+ bump: string;
17
+ tag?: string;
18
+ npmTag?: string;
19
+ dryRun?: boolean;
20
+ }
21
+
22
+ export interface ReleaseGithubOptions extends ReleasePublishOptions {
23
+ tags?: string;
24
+ }
25
+
26
+ export interface ReleaseTrustPublisherOptions {
27
+ dryRun?: boolean;
28
+ otp?: string;
29
+ skipLogin?: boolean;
30
+ }
31
+
32
+ export async function releaseVersion(root: string, options: ReleaseVersionOptions): Promise<void> {
33
+ const bump = releaseBumpArg(options.bump);
34
+ const projects = listPublicPackages(root)
35
+ .map((pkg) => pkg.name)
36
+ .join(',');
37
+ const state = await getReleaseState(root);
38
+ if (state.allPublished) {
39
+ console.log('Current package versions are already published; skipping version bump.');
40
+ return;
41
+ }
42
+
43
+ const headBeforeVersioning = await gitHead(root);
44
+
45
+ // Nx owns local release mutation: package versions, bun.lock updates, the
46
+ // release commit, and annotated tags. smoo owns the remote push so retries do
47
+ // not push an older checkout when Nx skips the release commit.
48
+ const nxArgs = ['release', 'version'];
49
+ if (bump !== 'auto') {
50
+ nxArgs.push(bump);
51
+ }
52
+ nxArgs.push(`--projects=${projects}`, '--git-commit=true', '--git-tag=true', '--git-push=false');
53
+ if (options.dryRun) {
54
+ nxArgs.push('--dryRun');
55
+ }
56
+ await run('nx', nxArgs, root);
57
+ if (!options.dryRun) {
58
+ // Guard against future Nx/config regressions that leave release files, such
59
+ // as bun.lock, outside the release commit after tagging/pushing.
60
+ await assertCleanGitTree(root);
61
+ const headAfterVersioning = await gitHead(root);
62
+ if (headAfterVersioning === headBeforeVersioning) {
63
+ console.log('Nx did not create a release commit; skipping git push.');
64
+ return;
65
+ }
66
+ await pushReleaseCommit(root);
67
+ }
68
+ }
69
+
70
+ export async function releasePublish(root: string, options: ReleasePublishOptions): Promise<void> {
71
+ const tag = releaseNpmTagArg(options);
72
+ const unpublishedPackages = await listUnpublishedPackages(root);
73
+ if (unpublishedPackages.length === 0) {
74
+ console.log('Current package versions are already published; skipping npm publish.');
75
+ return;
76
+ }
77
+ // npm package versions are immutable. Publishing only missing name@version
78
+ // pairs makes auth/network retries idempotent after a partial publish failure.
79
+ for (const pkg of unpublishedPackages) {
80
+ await publishPackedPackage(root, pkg, tag, options.dryRun === true);
81
+ }
82
+ }
83
+
84
+ export async function releaseGithubRelease(root: string, options: ReleaseGithubOptions): Promise<void> {
85
+ if (options.dryRun) {
86
+ console.log('Dry run; skipping GitHub Release creation.');
87
+ return;
88
+ }
89
+ const npmTag = releaseNpmTagArg(options);
90
+ const tags = options.tags ? options.tags.split(/\s+/).filter(Boolean) : await gitTagsAtHead(root);
91
+ if (tags.length === 0) {
92
+ throw new Error('No release tags found. Pass --tags or run from a tagged release commit.');
93
+ }
94
+ const latestFlag = npmTag === 'latest' ? 'true' : 'false';
95
+ for (const tag of tags) {
96
+ const exists = (await runStatus('gh', ['release', 'view', tag], root, true)) === 0;
97
+ if (exists) {
98
+ await run('gh', ['release', 'edit', tag, '--title', tag, `--latest=${latestFlag}`], root);
99
+ } else {
100
+ await run('gh', ['release', 'create', tag, '--title', tag, '--generate-notes', `--latest=${latestFlag}`], root);
101
+ }
102
+ }
103
+ }
104
+
105
+ export async function releaseTrustPublisher(root: string, options: ReleaseTrustPublisherOptions): Promise<void> {
106
+ const repository = githubRepositoryFromRootPackage(root);
107
+ const workflow = 'publish.yml';
108
+ const packages = listPublicPackages(root);
109
+ if (packages.length === 0) {
110
+ throw new Error('No npm:public packages found.');
111
+ }
112
+
113
+ if (!options.dryRun) {
114
+ const packageStates = await Promise.all(
115
+ packages.map(async (pkg) => ((await npmPackageExists(pkg.name)) ? null : pkg.name)),
116
+ );
117
+ const missingPackages = packageStates.filter((name): name is string => name !== null);
118
+ if (missingPackages.length > 0) {
119
+ throw new Error(
120
+ 'npm trusted publishing can only be configured after packages exist on the registry. ' +
121
+ 'Bootstrap the first publish with a temporary NPM_TOKEN, then rerun trust-publisher. ' +
122
+ `Missing packages: ${missingPackages.join(', ')}`,
123
+ );
124
+ }
125
+ }
126
+
127
+ if (!options.dryRun && !options.skipLogin) {
128
+ await runLatestNpm(root, ['login', '--auth-type=web']);
129
+ }
130
+
131
+ for (const pkg of packages) {
132
+ console.log(`${pkg.name}: trusting GitHub Actions ${repository}/${workflow}`);
133
+ const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
134
+ if (options.dryRun) {
135
+ args.push('--dry-run');
136
+ }
137
+ const otp = options.dryRun ? undefined : (options.otp ?? (await promptForNpmOtp(pkg.name)));
138
+ await runLatestNpm(root, args, otp ? { NPM_CONFIG_OTP: otp } : undefined);
139
+ }
140
+ }
141
+
142
+ export async function printReleaseState(root: string): Promise<void> {
143
+ console.log(JSON.stringify(await getReleaseState(root), null, 2));
144
+ }
145
+
146
+ interface ReleaseState {
147
+ packages: { name: string; version: string; published: boolean }[];
148
+ allPublished: boolean;
149
+ }
150
+
151
+ type PublicPackage = ReturnType<typeof listPublicPackages>[number];
152
+
153
+ async function getReleaseState(root: string): Promise<ReleaseState> {
154
+ const packages = listPublicPackages(root);
155
+ const states = await Promise.all(
156
+ packages.map(async (pkg) => {
157
+ const published = await npmVersionExists(pkg.name, pkg.version);
158
+ return { name: pkg.name, version: pkg.version, published };
159
+ }),
160
+ );
161
+ return { packages: states, allPublished: states.every((state) => state.published) };
162
+ }
163
+
164
+ async function listUnpublishedPackages(root: string): Promise<PublicPackage[]> {
165
+ const packages = listPublicPackages(root);
166
+ const states = await Promise.all(
167
+ packages.map(async (pkg) => ({ pkg, published: await npmVersionExists(pkg.name, pkg.version) })),
168
+ );
169
+ return states.filter((state) => !state.published).map((state) => state.pkg);
170
+ }
171
+
172
+ async function publishPackedPackage(root: string, pkg: PublicPackage, tag: string, dryRun: boolean): Promise<void> {
173
+ const tempDir = await mkdtemp(join(tmpdir(), 'smoo-publish-'));
174
+ const tarball = join(tempDir, `${safeTarballPrefix(pkg.name)}-${pkg.version}.tgz`);
175
+ try {
176
+ console.log(`${pkg.name}@${pkg.version}: packing with bun pm pack`);
177
+ await run('bun', ['pm', 'pack', '--filename', tarball, '--ignore-scripts', '--quiet'], join(root, pkg.path));
178
+ await assertNoWorkspaceProtocolInTarball(root, tarball, pkg);
179
+ // npm CLI owns authentication here: trusted publishing OIDC when configured,
180
+ // or the temporary NODE_AUTH_TOKEN bootstrap path below before trust exists.
181
+ // Bun still produces the tarball so workspace:* dependencies are resolved the
182
+ // same way smoo validates packed packages before release.
183
+ const args = ['publish', tarball, '--access', 'public', '--tag', tag, '--provenance'];
184
+ if (dryRun) {
185
+ args.push('--dry-run');
186
+ }
187
+ await runLatestNpmPublish(root, args);
188
+ } finally {
189
+ await rm(tempDir, { recursive: true, force: true });
190
+ }
191
+ }
192
+
193
+ function safeTarballPrefix(name: string): string {
194
+ return name.replace(/^@/, '').replace(/[^a-zA-Z0-9._-]+/g, '-');
195
+ }
196
+
197
+ async function assertNoWorkspaceProtocolInTarball(root: string, tarball: string, pkg: PublicPackage): Promise<void> {
198
+ const result = await $`tar -xOf ${tarball} package/package.json`.cwd(root).quiet().nothrow();
199
+ if (result.exitCode !== 0) {
200
+ throw new Error(`${pkg.name}: unable to inspect packed package.json before publish.`);
201
+ }
202
+ const manifest = decode(result.stdout);
203
+ if (manifest.includes('workspace:')) {
204
+ throw new Error(`${pkg.name}: packed package.json still contains workspace: dependency references.`);
205
+ }
206
+ }
207
+
208
+ async function npmVersionExists(name: string, version: string): Promise<boolean> {
209
+ const result = await $`bun pm view ${`${name}@${version}`} version`.cwd(process.cwd()).quiet().nothrow();
210
+ return result.exitCode === 0 && decode(result.stdout).trim() === version;
211
+ }
212
+
213
+ async function npmPackageExists(name: string): Promise<boolean> {
214
+ const result = await $`bun pm view ${name} name`.cwd(process.cwd()).quiet().nothrow();
215
+ return result.exitCode === 0;
216
+ }
217
+
218
+ async function gitTagsAtHead(root: string): Promise<string[]> {
219
+ const result = await $`git tag --points-at HEAD`.cwd(root).quiet().nothrow();
220
+ if (result.exitCode !== 0) {
221
+ return [];
222
+ }
223
+ return decode(result.stdout)
224
+ .split('\n')
225
+ .map((tag) => tag.trim())
226
+ .filter(Boolean);
227
+ }
228
+
229
+ async function assertCleanGitTree(root: string): Promise<void> {
230
+ const result = await $`git status --porcelain --untracked-files=no`.cwd(root).quiet().nothrow();
231
+ if (result.exitCode !== 0) {
232
+ throw new Error('Unable to inspect git status after release versioning.');
233
+ }
234
+ const status = decode(result.stdout).trim();
235
+ if (status) {
236
+ throw new Error(`nx release version left tracked changes after tagging:\n${status}`);
237
+ }
238
+ }
239
+
240
+ async function gitHead(root: string): Promise<string> {
241
+ const result = await $`git rev-parse HEAD`.cwd(root).quiet().nothrow();
242
+ if (result.exitCode !== 0) {
243
+ throw new Error('Unable to resolve git HEAD before release versioning.');
244
+ }
245
+ return decode(result.stdout).trim();
246
+ }
247
+
248
+ async function pushReleaseCommit(root: string): Promise<void> {
249
+ const branch = await releaseBranch(root);
250
+ const remote = await releaseRemote(root, branch);
251
+ await run('git', ['push', '--follow-tags', '--atomic', remote, `HEAD:refs/heads/${branch}`], root);
252
+ }
253
+
254
+ async function releaseBranch(root: string): Promise<string> {
255
+ const githubBranch = process.env.GITHUB_REF_NAME?.trim();
256
+ if (githubBranch) {
257
+ return githubBranch;
258
+ }
259
+ const result = await $`git branch --show-current`.cwd(root).quiet().nothrow();
260
+ const branch = decode(result.stdout).trim();
261
+ if (result.exitCode === 0 && branch) {
262
+ return branch;
263
+ }
264
+ throw new Error('Unable to resolve current git branch for release push.');
265
+ }
266
+
267
+ async function releaseRemote(root: string, branch: string): Promise<string> {
268
+ const configured = await gitConfigValue(root, `branch.${branch}.remote`);
269
+ if (configured) {
270
+ return configured;
271
+ }
272
+ if (await gitRemoteExists(root, 'origin')) {
273
+ return 'origin';
274
+ }
275
+ const result = await $`git remote`.cwd(root).quiet().nothrow();
276
+ if (result.exitCode !== 0) {
277
+ throw new Error('Unable to list git remotes for release push.');
278
+ }
279
+ const remotes = decode(result.stdout)
280
+ .split('\n')
281
+ .map((remote) => remote.trim())
282
+ .filter(Boolean);
283
+ if (remotes.length === 1 && remotes[0]) {
284
+ return remotes[0];
285
+ }
286
+ throw new Error('Unable to choose git remote for release push. Configure branch upstream or add origin.');
287
+ }
288
+
289
+ async function gitConfigValue(root: string, key: string): Promise<string | null> {
290
+ const result = await $`git config --get ${key}`.cwd(root).quiet().nothrow();
291
+ const value = decode(result.stdout).trim();
292
+ return result.exitCode === 0 && value ? value : null;
293
+ }
294
+
295
+ async function gitRemoteExists(root: string, remote: string): Promise<boolean> {
296
+ const result = await $`git remote get-url ${remote}`.cwd(root).quiet().nothrow();
297
+ return result.exitCode === 0;
298
+ }
299
+
300
+ function releaseBumpArg(bump = 'auto'): string {
301
+ if (!['auto', 'patch', 'minor', 'major', 'prerelease'].includes(bump)) {
302
+ throw new Error(`Invalid --bump "${bump}". Expected auto, patch, minor, major, or prerelease.`);
303
+ }
304
+ return bump;
305
+ }
306
+
307
+ function releaseNpmTagArg(options: ReleasePublishOptions): string {
308
+ const bump = releaseBumpArg(options.bump);
309
+ const derivedTag = bump === 'prerelease' ? 'next' : 'latest';
310
+ const explicitTag = options.tag ?? options.npmTag;
311
+ if (!explicitTag) {
312
+ return derivedTag;
313
+ }
314
+ if (explicitTag !== derivedTag) {
315
+ throw new Error(`--bump ${bump} publishes with npm dist-tag ${derivedTag}, not ${explicitTag}.`);
316
+ }
317
+ return explicitTag;
318
+ }
319
+
320
+ function githubRepositoryFromRootPackage(root: string): string {
321
+ const pkg = readPackageJson(join(root, 'package.json'));
322
+ const repository = pkg ? repositoryInfo(pkg.json) : null;
323
+ if (!repository) {
324
+ throw new Error('Root package.json must define repository.url before configuring npm trusted publishing.');
325
+ }
326
+ return githubRepositoryFromUrl(repository.url);
327
+ }
328
+
329
+ function githubRepositoryFromUrl(url: string): string {
330
+ const normalized = url
331
+ .replace(/^git\+/, '')
332
+ .replace(/#.*$/, '')
333
+ .replace(/\.git$/, '');
334
+ const https = /^https:\/\/github\.com\/([^/]+)\/([^/]+)$/.exec(normalized);
335
+ if (https?.[1] && https[2]) {
336
+ return `${https[1]}/${https[2]}`;
337
+ }
338
+ const ssh = /^git@github\.com:([^/]+)\/([^/]+)$/.exec(normalized);
339
+ if (ssh?.[1] && ssh[2]) {
340
+ return `${ssh[1]}/${ssh[2]}`;
341
+ }
342
+ const shorthand = /^github:([^/]+)\/([^/]+)$/.exec(normalized);
343
+ if (shorthand?.[1] && shorthand[2]) {
344
+ return `${shorthand[1]}/${shorthand[2]}`;
345
+ }
346
+ throw new Error(`Root package.json repository.url must be a GitHub repository URL, got ${url}`);
347
+ }
348
+
349
+ async function runLatestNpm(root: string, npmArgs: string[], env?: Record<string, string>): Promise<void> {
350
+ await run('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
351
+ }
352
+
353
+ async function runLatestNpmPublish(root: string, npmArgs: string[]): Promise<void> {
354
+ const token = process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN;
355
+ if (!token || process.env.NPM_CONFIG_USERCONFIG) {
356
+ await runLatestNpm(root, npmArgs);
357
+ return;
358
+ }
359
+
360
+ const tempDir = await mkdtemp(join(tmpdir(), 'smoo-npm-auth-'));
361
+ const npmrc = join(tempDir, '.npmrc');
362
+ try {
363
+ await writeFile(npmrc, `registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=${token}\n`);
364
+ await runLatestNpm(root, npmArgs, { NPM_CONFIG_USERCONFIG: npmrc });
365
+ } finally {
366
+ await rm(tempDir, { recursive: true, force: true });
367
+ }
368
+ }
369
+
370
+ async function promptForNpmOtp(packageName: string): Promise<string> {
371
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
372
+ throw new Error(
373
+ `npm trust requires a one-time password for ${packageName}. Pass --otp <code> in non-interactive shells.`,
374
+ );
375
+ }
376
+
377
+ const mutedOutput = new Writable({
378
+ write(_chunk, _encoding, callback) {
379
+ callback();
380
+ },
381
+ });
382
+ const rl = createInterface({ input: process.stdin, output: mutedOutput, terminal: true });
383
+ process.stdout.write(`Enter npm OTP for ${packageName}: `);
384
+ try {
385
+ const otp = (await rl.question('')).trim();
386
+ process.stdout.write('\n');
387
+ if (!otp) {
388
+ throw new Error(`npm OTP is required for ${packageName}.`);
389
+ }
390
+ return otp;
391
+ } finally {
392
+ rl.close();
393
+ }
394
+ }